# ============================================================================
# 程式交易示範:動能輪動策略(月頻定期再平衡)
# ----------------------------------------------------------------------------
# 本檔產出文章中所有 CAGR / Sharpe / Sortino / MDD 數字,可重現(data_traceability)。
# 主軸:程式交易的價值 = 把規則自動化、每月紀律性再平衡,不靠盤感。
# 對照組 = 純複合動能(無紀律過濾);優化組 = 動能 + 低波動 + ROE 品質。
# 環境:conda activate finlab。執行:python strategy.py
# Benchmark:0050 用 etl:adj_close(含息,benchmark 鐵則)。
# ============================================================================
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:收盤價')
adj   = data.get('etl:adj_close')                                    # 含息,benchmark 用
rev   = data.get('monthly_revenue:去年同月增減(%)')
roe   = data.get('fundamental_features:ROE稅後').index_str_to_date()  # 對齊公布日,避免前視偏差
mktv  = data.get('etl:market_value')
START = '2018-01-01'

def monthly(df):
    return df.reindex(close.index, method='ffill').resample('M').last()

# 複合動能:近 3 個月(60 交易日) + 近 6 個月(扣最近 1 月,避免短期反轉)
mom3m = monthly(close.pct_change(60))
mom6m = monthly(close.pct_change(120).shift(20))
volm  = monthly(close.pct_change().rolling(60).std())   # 近 60 日波動
roem  = monthly(roe)
revm  = monthly(rev)
mktvm = monthly(mktv)

# 選股池:市值前 200 大 + ROE>0 + 營收年增>0(避免動能抓到沒基本面的飆股)
pool = (mktvm.rank(axis=1, ascending=False) <= 200) & (roem > 0) & (revm > 0)

def rk(df, asc=True):
    return df.where(pool).rank(axis=1, pct=True, ascending=asc).fillna(0)

def backtest(score, n=20):
    mask   = score.where(pool).rank(axis=1, ascending=False) <= n
    masked = (score.where(mask, 0)) ** 2                 # 分數平方加權,集中在高分股
    w      = masked.div(masked.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)
    w      = w[w.index >= START]
    return sim(w, resample='M', resample_offset='14D', upload=False)   # 月頻再平衡,+14D 對齊月報

# --- 對照組:純複合動能(無紀律過濾,只追漲)------------------------------------
base = backtest(rk(mom3m) + rk(mom6m), n=20)
sb = base.get_stats()

# --- 優化組:動能 + 低波動 + ROE 品質(加上紀律)-------------------------------
opt = backtest(rk(mom3m) + rk(mom6m) + rk(volm, asc=False) + rk(roem), n=20)
so = opt.get_stats()

# --- Benchmark:0050 含息 buy & hold -----------------------------------------
e = adj['0050'].dropna(); e = e[e.index >= START]
cagr0050 = (e.iloc[-1] / e.iloc[0]) ** (365.25 / ((e.index[-1] - e.index[0]).days)) - 1
ret0050  = e.pct_change().dropna()
sharpe0050 = ret0050.mean() / ret0050.std() * np.sqrt(252)
mdd0050  = (e / e.cummax() - 1).min()

def show(name, s):
    print(f"{name:24s} CAGR={s['cagr']:.4f} dSharpe={s.get('daily_sharpe'):.3f} "
          f"dSortino={s.get('daily_sortino'):.3f} mSortino={s.get('monthly_sortino'):.3f} "
          f"MDD={s['max_drawdown']:.3f}")

print("=" * 78)
show("對照組 純動能", sb)
show("優化組 動能+低波動+品質", so)
print(f"{'0050 含息 buy&hold':24s} CAGR={cagr0050:.4f} dSharpe={sharpe0050:.3f} "
      f"dSortino=---   mSortino=---   MDD={mdd0050:.3f}")
print("=" * 78)

# 匯出互動式報告(供文章 inline embed)+ 對照 CSV
opt.to_html('report_strategy.html')
base.to_html('report_baseline.html')
