# ============================================================================ # 股票選股三策略回測(finlab.finance/tools/stock-selection 頁面數字來源) # ---------------------------------------------------------------------------- # 三套策略各代表一個因子家族: # 1. 四因子複合(品質+動能+低波(2×)+營收,分數三次方加權前 25) ← 頁面主軸 # 2. 價值+品質+股息(低 PB+高 ROE+高殖利率,動能>0 確認,季換前 20) # 3. 籌碼選股(投信 20 日買超強度+動能,月換前 15) # 單因子與敏感度實驗另見 factor-comparison.py / robustness.py。 # Benchmark:0050 含息(etl:adj_close,純算術買進持有,不經 sim、不含成本)。 # 執行:安裝 finlab 後直接跑(finlab.login() 會自動引導登入)。 # 回測區間:2018-01-01 ~ 2026-06-09;sim() 已內扣手續費 0.1425% 與證交稅 0.3%。 # ============================================================================ import numpy as np from finlab import data from finlab.backtest import sim START, END = '2018-01-01', '2026-06-09' # ---- 載入資料 --------------------------------------------------------------- close = data.get('price:收盤價') adj = data.get('etl:adj_close') # 含息,benchmark 用 pb = data.get('price_earning_ratio:股價淨值比') dy = data.get('price_earning_ratio:殖利率(%)') roe = data.get('fundamental_features:ROE稅後').index_str_to_date().reindex(close.index, method='ffill') rev = data.get('monthly_revenue:去年同月增減(%)').reindex(close.index, method='ffill') trust = data.get('institutional_investors_trading_summary:投信買賣超股數') volume = data.get('price:成交股數') amount = (close * volume).rolling(60).mean() # 近 60 日日均成交額 liquid = amount.rank(axis=1, pct=True) > 0.5 # 流動性過濾:成交額前 50% def run(position, resample='M', offset=None, name=''): kwargs = dict(resample=resample, fee_ratio=0.001425, tax_ratio=0.003, upload=False, name=name) if offset: kwargs['resample_offset'] = offset report = sim(position[position.index >= START], **kwargs) print(name, report.get_stats()) return report # ---- 策略一:四因子複合(品質+動能+低波(2×)+營收,頁面主軸)---------------- quality = roe.rank(axis=1, pct=True) momentum = (close / close.shift(120) - 1).rank(axis=1, pct=True) lowvol = (-close.pct_change().rolling(120).std()).rank(axis=1, pct=True) revenue = rev.rank(axis=1, pct=True) score1 = (quality + momentum + 2 * lowvol + revenue) / 5 score1 = score1.where(liquid) sel1 = score1.rank(axis=1, ascending=False) <= 25 w1 = (score1 ** 3).where(sel1) # 分數三次方加權,集中在高分股 w1 = w1.div(w1.sum(axis=1), axis=0).fillna(0) report_composite = run(w1, offset='14D', name='四因子複合') # ---- 策略二:價值+品質+股息(動能>0 確認,季換)----------------------------- mom6 = close / close.shift(120) - 1 pool2 = liquid & (pb > 0) & (mom6 > 0) def rk2(df): return df.where(pool2).rank(axis=1, pct=True).fillna(0) score2 = rk2(-pb) + rk2(roe) + rk2(dy) mask2 = score2.where(pool2).rank(axis=1, ascending=False) <= 20 w2 = score2.where(mask2, 0) w2 = w2.div(w2.sum(axis=1).replace(0, np.nan), axis=0).fillna(0) report_value = run(w2, resample='Q', name='價值+品質+股息') # ---- 策略三:籌碼選股(投信 20 日買超強度+動能)------------------------------ mktv = data.get('etl:market_value') trust_daily = trust.reindex(close.index).fillna(0) strength = trust_daily.rolling(20).sum() / volume.rolling(20).mean().replace(0, np.nan) pool3 = liquid & (mktv.rank(axis=1, ascending=False) <= 600) & (strength > 0) mom3 = close.pct_change(60) score3 = (strength.where(pool3).rank(axis=1, pct=True).fillna(0) + mom3.where(pool3).rank(axis=1, pct=True).fillna(0)) mask3 = score3.where(pool3).rank(axis=1, ascending=False) <= 15 w3 = score3.where(mask3, 0) w3 = w3.div(w3.sum(axis=1).replace(0, np.nan), axis=0).fillna(0) report_chips = run(w3, name='籌碼選股') # ---- Benchmark:0050 含息(純算術買進持有)---------------------------------- px = adj['0050'].dropna().loc[START:END] curve = (1 + px.pct_change().fillna(0)).cumprod() years = (curve.index[-1] - curve.index[0]).days / 365.25 print('0050 含息 buy & hold:CAGR %.2f%%,總報酬 %.1f%%,最大回撤 %.2f%%' % ( (curve.iloc[-1] ** (1 / years) - 1) * 100, (curve.iloc[-1] - 1) * 100, (curve / curve.cummax() - 1).min() * 100, ))