# 本文 Hero 策略:品質 + 動能 + 低波(+ 營收動能輔助)四因子複合(2018~)
# 設計目標 = 風險調整後勝過 0050(月索提諾 2.5 vs 0050 約 1.6),且最大回撤更淺。
# 環境:finlab(conda)。benchmark 0050 用 etl:adj_close(含息,品質引擎鐵則)。
# 跑法:conda activate finlab && python hero-quality-momentum-lowvol.py
import warnings; warnings.filterwarnings("ignore")
import numpy as np, pandas as pd
from finlab import data
from finlab.backtest import sim

close  = data.get('price:收盤價')
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')
amount = (close * data.get('price:成交股數')).rolling(60).mean()
START  = '2018-01-01'

def pr(df):
    """橫斷面百分位排名(0~1),NaN 不參與。"""
    return pd.DataFrame(df).astype(float).rank(axis=1, pct=True)

# --- 四個方向不同、彼此低相關的因子,各自轉成百分位分數 ---
quality  = pr(roe)                                       # 品質:ROE 越高越好
momentum = pr(close / close.shift(120) - 1)              # 動能:近 120 日漲幅(較平滑)
lowvol   = pr(-close.pct_change().rolling(120).std())    # 低波:近 120 日波動度越低越好
revenue  = pr(rev)                                       # 營收動能:年增越高越好

# 低波給雙倍權重(壓回撤),四項加權平均成總分
score = (quality + momentum + 2 * lowvol + revenue) / 5

# 流動性過濾:近 60 日成交額後 50% 剔除,確保實單可成交
liquid = amount.rank(axis=1, pct=True) > 0.5
score = score.where(liquid)

# 選分數最高的 25 檔,依分數三次方加權(集中在最高分股)
topn = score.rank(axis=1, ascending=False) <= 25
w = (score ** 3).where(topn)
position = w.div(w.sum(axis=1), axis=0).fillna(0)
position = position[position.index >= START]

# 月頻換股,對齊月營收公布日(resample_offset='14D')捕捉 PEAD alpha
report = sim(position, resample='M', resample_offset='14D',
             upload=False, name='品質動能低波複合')
s = report.get_stats()
print("== Hero:品質+動能+低波(+營收)四因子複合(2018~)==")
for k in ['cagr', 'daily_sharpe', 'daily_sortino',
          'monthly_sharpe', 'monthly_sortino', 'max_drawdown']:
    print(f"  {k:16s} = {s.get(k)}")

# 0050 含息對照(benchmark)
adj = data.get('etl:adj_close'); e = adj['0050'].dropna(); e = e[e.index >= START]
yrs = (e.index[-1] - e.index[0]).days / 365.25
cagr0050 = (e.iloc[-1] / e.iloc[0]) ** (1 / yrs) - 1
r0 = e.pct_change().dropna()
print(f"\n0050(含息) CAGR={cagr0050:.4f} "
      f"Sharpe={r0.mean()/r0.std()*252**0.5:.4f} "
      f"MDD={((e/e.cummax())-1).min():.4f}")

# 互動式報告(供文章 inline embed)
report.to_html('report_strategy.html')
print("\nsaved report_strategy.html")
