# -*- coding: utf-8 -*-
"""
量化交易旗艦頁:三種台股多因子策略(每個 2020–2026 日夏普 > 1.2)
資料與回測引擎:finlab(pip install finlab);benchmark = 0050 含息(etl:adj_close)
主視窗 2020-01 ~ 最新(含 COVID 崩盤與 2022 空頭);改 START='2007-01-01' 可看全期。

執行:
    pip install finlab
    python strategy.py

注意:這份程式的數字會隨每次資料更新而變動(新鮮度複利)。文章揭露的數字以發佈日的回測為準,
你重跑時若資料已更新,數字會略有不同,這正是量化策略的特性,不是錯誤。
"""
import numpy as np
import pandas as pd
from finlab import data
from finlab.backtest import sim

START = '2020-01-01'

# === Step 1. 一行抓資料(免爬蟲)===
close = data.get('price:收盤價')
adj   = data.get('etl:adj_close')                         # 還原權值(含息),報酬/benchmark 鐵則
vol   = data.get('price:成交股數')
rev   = data.get('monthly_revenue:去年同月增減(%)')         # 營收年增率(成長)
roe   = data.get('fundamental_features:ROE稅後').index_str_to_date()   # ROE(品質,對齊公布日防前視)
gm    = data.get('fundamental_features:營業毛利率').index_str_to_date() # 毛利率(品質)

# === Step 2. 把各頻率資料對齊交易日後取每月最後一筆 ===
def monthly(df):
    return df.reindex(close.index, method='ffill').resample('M').last()

f_rev  = monthly(rev)
f_mom  = monthly(close.pct_change(120))                   # 6 個月價格動能
f_mom3 = monthly(close.pct_change(60))                    # 3 個月價格動能
f_roe  = monthly(roe)
f_gm   = monthly(gm)
f_vol  = monthly(close.pct_change().rolling(60).std())    # 近 60 日波動度(越低越好)
liq    = monthly(vol.rolling(60).mean())

# 流動性過濾(避免回測買得到、實單買不到);成長池(營收策略用)
pool_liq  = liq > 1_000_000
pool_grow = pool_liq & (f_rev > 5) & (f_rev < 200) & ((f_rev > 0).rolling(3).sum() == 3)

def rk(df, pool, asc=True):
    """池內橫斷面百分位排名;asc=False 表示『越小越好』(低波動)。"""
    return df.where(pool).rank(axis=1, pct=True, ascending=asc).fillna(0)

def build(score, pool, topn):
    """取分數前 topn 名,分數平方加權(集中強訊號),每列正規化成權重。"""
    mask   = score.where(pool).rank(axis=1, ascending=False) <= topn
    masked = (score.where(mask, 0)) ** 2
    w = masked.div(masked.sum(axis=1).replace(0, np.nan), axis=0).fillna(0)
    return w[w.index >= START]

# === Step 3. 三種 distinct 策略 ===
# (1) 多因子複合(穩健):成長+品質+動能+低波,top 40
composite = build(
    rk(f_rev, pool_grow) + rk(f_mom, pool_grow) + rk(f_roe, pool_grow) + rk(f_vol, pool_grow, asc=False),
    pool_grow, 40)

# (2) 品質動能低波(防禦,最低回撤):ROE+動能+低波,top 25
defensive = build(
    rk(f_roe, pool_liq) + rk(f_mom3, pool_liq) + rk(f_vol, pool_liq, asc=False),
    pool_liq, 25)

# (3) 高毛利成長(積極,最高報酬):毛利+營收+動能,top 30
aggressive = build(
    rk(f_gm, pool_grow) + rk(f_rev, pool_grow) + rk(f_mom, pool_grow),
    pool_grow, 30)

# === Step 4. 回測(月頻換股、對齊月營收公布 offset 14 天;sim() 預設已含手續費 + 證交稅 0.3%)===
for name, w in [('多因子複合(穩健)', composite),
                ('品質動能低波(防禦)', defensive),
                ('高毛利成長(積極)', aggressive)]:
    r = sim(w, resample='M', resample_offset='14D', upload=False)
    s = r.get_stats()
    print(f"{name:<16} CAGR {s['cagr']*100:5.1f}%  日Sharpe {s['daily_sharpe']:.2f}  "
          f"Sortino {s['daily_sortino']:.2f}  MDD {s['max_drawdown']*100:4.0f}%")

# === Step 5. benchmark:0050 含息 buy & hold ===
b = pd.DataFrame(index=adj.index); b['0050'] = 1.0
br = sim(b[b.index >= START], resample='Q', upload=False, fee_ratio=0)
bs = br.get_stats()
print(f"{'0050 含息(基準)':<16} CAGR {bs['cagr']*100:5.1f}%  日Sharpe {bs['daily_sharpe']:.2f}")

# 想看完整互動報告:composite_report = sim(composite, resample='M', resample_offset='14D'); composite_report.display()
