Spaces:
Runtime error
Runtime error
File size: 10,133 Bytes
f788fad 7fcf8ce f788fad 1420bc4 f788fad 7fcf8ce f788fad 1420bc4 f788fad 7fcf8ce f788fad 30cacbc 8444ae5 30cacbc 8444ae5 30cacbc 8444ae5 30cacbc 7fcf8ce 8444ae5 30cacbc 8444ae5 30cacbc 8444ae5 30cacbc 8444ae5 30cacbc 8444ae5 7fcf8ce 30cacbc 8444ae5 30cacbc 8444ae5 30cacbc 8444ae5 30cacbc 8444ae5 1420bc4 8444ae5 30cacbc 1420bc4 8444ae5 1420bc4 30cacbc 1420bc4 8444ae5 30cacbc 8444ae5 1420bc4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | # src/features.py
import pandas as pd
import numpy as np
import yfinance as yf
from .config import Config
from .data import data_service
import twstock
class FeatureLayer:
@staticmethod
def calculate_scores(ticker, df_price, df_inst, df_rev, fund, price, data_source):
# ... (這部分保持不變,為了節省篇幅省略) ...
scores = {}
details = {}
close = df_price['Close']
ma20 = close.rolling(20).mean().iloc[-1]
ma60 = close.rolling(60).mean().iloc[-1]
ts = 0; tm = []
if price > ma20: ts += 40; tm.append("站上月線")
if price > ma60: ts += 40; tm.append("站上季線")
if close.rolling(5).mean().iloc[-1] > ma20 > ma60: ts += 20; tm.append("多頭排列")
scores['技術'] = min(ts, 100); details['技術'] = tm
cs = 50; cm = []
if data_source == "FinMind":
if not df_inst.empty and '外資' in df_inst.columns:
f_sum = df_inst['外資'].tail(5).sum()
if f_sum > 1: cs += 20; cm.append("外資買超")
elif f_sum < -1: cs -= 20; cm.append("外資賣超")
else:
if not df_inst.empty and '主力動向' in df_inst.columns:
m_sum = df_inst['主力動向'].tail(5).sum()
if m_sum > 0: cs += 15; cm.append("主力吸籌(估)")
else: cs -= 15; cm.append("主力調節(估)")
scores['籌碼'] = max(0, min(cs, 100)); details['籌碼'] = cm if cm else ["籌碼中性"]
vs = 0; vm = []
dy = (fund['avg_div']/price)*100 if price > 0 else 0
if dy > 4: vs += 50; vm.append(f"殖利率 {dy:.1f}%")
try:
pe = yf.Ticker(ticker).info.get('trailingPE', 999)
if 0 < pe < 15: vs += 50; vm.append(f"PE {pe:.1f}")
except: pass
scores['價值'] = min(vs, 100); details['價值'] = vm
ms = 50; mm = []
ret = (close.iloc[-1]/close.iloc[-20]-1)*100
if ret > 5: ms += 30; mm.append("月勢轉強")
elif ret < -5: ms -= 20; mm.append("月勢轉弱")
scores['動能'] = min(ms, 100); details['動能'] = mm
gs = 40; gm = []
if not df_rev.empty:
try:
yoy = ((df_rev.iloc[-1]['revenue'] - df_rev.iloc[-13]['revenue']) / df_rev.iloc[-13]['revenue'])*100
if yoy > 20: gs = 100; gm.append(f"YoY +{yoy:.1f}%")
elif yoy > 0: gs = 70; gm.append("營收成長")
else: gs = 30; gm.append("營收衰退")
except: pass
scores['成長'] = gs; details['成長'] = gm
rs = 80; rm = []
if df_price['Volume'].iloc[-1] > df_price['Volume'].tail(20).mean()*2:
if price < ma60: rs += 20; rm.append("低檔爆量")
else: rs -= 40; rm.append("高檔爆量")
scores['風險'] = max(0, min(rs, 100)); details['風險'] = rm
return scores, details
@staticmethod
def calculate_weighted_score(scores, strategy_name):
weights = Config.STRATEGY_PROFILES.get(strategy_name, Config.STRATEGY_PROFILES["成長型 (預設)"])
total_score = 0
for k, v in scores.items():
total_score += v * weights.get(k, 0.16)
return round(total_score, 1)
@staticmethod
def simple_backtest(df, hold_days=20):
# ... (保持不變) ...
if len(df) < 60: return None
ma20 = df['Close'].rolling(20).mean()
signal = (df['Close'] > ma20) & (ma20 > ma20.shift(1))
results = []
start_idx = max(20, len(df) - 120)
for i in range(start_idx, len(df)-hold_days, 5):
if signal.iloc[i]:
entry = df['Open'].iloc[i+1]
exit_p = df['Close'].iloc[i+hold_days]
ret = (exit_p / entry - 1) * 100
results.append(ret)
start_price = df['Close'].iloc[start_idx]
end_price = df['Close'].iloc[-1]
bench_ret = (end_price / start_price - 1) * 100
if not results: return {"勝率": 0, "平均報酬": 0, "次數": 0, "基準報酬": round(bench_ret, 2)}
wins = [r for r in results if r > 0]
return {"勝率": round(len(wins)/len(results)*100, 1), "平均報酬": round(np.mean(results), 2), "次數": len(results), "基準報酬": round(bench_ret, 2)}
# ★★★ 重點修改區域:ETF 智能顧問 ★★★
@staticmethod
def run_etf_screener(category, sort_by):
if category != "全部": target_dict = Config.ETF_DATABASE.get(category, {})
else: target_dict = {k:v for d in Config.ETF_DATABASE.values() for k,v in d.items()}
tickers = list(target_dict.keys())
if not tickers: return pd.DataFrame()
data = data_service.fetch_batch_history(tickers)
results = []
for ticker in tickers:
try:
hist = data[ticker] if len(tickers) > 1 else data
hist = hist.dropna(subset=['Close'])
if len(hist) < 60: continue
# 1. 數據計算
price = hist['Close'].iloc[-1]
# 季報酬 (動能)
r3 = ((hist['Close'].iloc[-1]/hist['Close'].iloc[-60])-1)*100
# 月報酬 (短期)
r1 = ((hist['Close'].iloc[-1]/hist['Close'].iloc[-20])-1)*100
# 波動率 (風險)
vol = hist['Close'].pct_change().std() * 100 * (252**0.5)
# 季線趨勢 (MA60)
ma60 = hist['Close'].rolling(60).mean().iloc[-1]
# 2. AI 評分與診斷
score = 0
tags = []
# 動能分 (40%)
if r3 > 10: score += 40; tags.append("🚀 強勢")
elif r3 > 0: score += 20
elif r3 < -5: tags.append("📉 弱勢")
# 趨勢分 (30%)
if price > ma60: score += 30
# 風險分 (30%) - 波動越低分越高 (針對 ETF 屬性)
if vol < 12: score += 30; tags.append("🛡️ 穩健")
elif vol < 20: score += 20
else: tags.append("🌊 活潑") # 波動大
# 亮點標籤:高CP值 (漲幅/波動 > 1)
if vol > 0 and (r3 / vol) > 1: tags.append("💎 高CP")
ai_comment = " ".join(tags[:2]) # 取前兩個
# 3. 操作建議
if score >= 80: suggestion = "🟢 順勢佈局"
elif score >= 60: suggestion = "🟡 區間操作"
else: suggestion = "⚪ 暫時觀望"
results.append({
"代碼": ticker.replace('.TW',''),
"ETF名稱": target_dict[ticker],
"建議": suggestion, # 新增
"AI 診斷": ai_comment, # 新增
"綜合評分": score, # 新增
"現價": round(price, 2),
"季報酬%": round(r3, 2),
"波動率%": round(vol, 1)
})
except: continue
df = pd.DataFrame(results)
if df.empty: return df
# 欄位排序
cols = ["代碼", "ETF名稱", "建議", "AI 診斷", "綜合評分", "季報酬%", "波動率%", "現價"]
df = df[cols]
# 根據使用者選擇排序
sort_col = sort_by if sort_by in df.columns else "綜合評分"
return df.sort_values(sort_col, ascending=False)
@staticmethod
def run_value_screener(category):
# ... (保持不變) ...
candidates = Config.STOCK_POOLS.get(category, [])
results = []
for t in candidates:
try:
stock = yf.Ticker(t)
info = stock.info
pe = info.get('trailingPE', 999) or 999
dy = (info.get('dividendYield', 0) or 0) * 100
pb = info.get('priceToBook', 999) or 999
roe = (info.get('returnOnEquity', 0) or 0) * 100
beta = info.get('beta', 1.0) or 1.0
price = info.get('currentPrice', 0)
score = 0
if 0 < pe < 12: score += 25
elif pe < 18: score += 15
if dy > 5: score += 25
elif dy > 3: score += 15
if pb < 1.2: score += 15
if roe > 15: score += 20
elif roe > 10: score += 10
if 0 < beta < 0.8: score += 15
elif beta > 1.5: score -= 10
tags = []
if roe > 15: tags.append("🏆 績優")
if dy > 5: tags.append("💰 現金牛")
if 0 < pe < 10: tags.append("💎 超值")
if 0 < beta < 0.6: tags.append("🛡️ 抗跌")
if not tags: tags.append("⚖️ 中性")
ai_comment = " ".join(tags[:2])
if score >= 75: suggestion = "🟢 積極佈局"
elif score >= 60: suggestion = "🟡 逢低買進"
else: suggestion = "⚪ 再觀察"
name = t
if t.split('.')[0] in twstock.codes: name = twstock.codes[t.split('.')[0]].name
results.append({
"代碼": t, "名稱": name, "AI 診斷": ai_comment, "建議": suggestion,
"評分": int(score), "ROE%": round(roe, 1), "殖利率%": round(dy, 1),
"本益比": round(pe, 1) if pe != 999 else "N/A", "風險係數": round(beta, 2), "現價": price
})
except: continue
df = pd.DataFrame(results)
if df.empty: return df
cols = ["代碼", "名稱", "建議", "AI 診斷", "評分", "ROE%", "殖利率%", "本益比", "風險係數", "現價"]
df = df[cols]
return df.sort_values("評分", ascending=False) |