# 台股 8 大選股條件回測 — 可重現原始碼
# 對應 https://finlab.finance/tools/stock-screener 頁面上的所有數字。
#
# 安裝與登入:
#   pip install finlab
#   import finlab; finlab.login()    # 不帶 token,finlab 會自動引導你完成登入
#
# 口徑說明:
#   - 基準 = 含息 0050(etl:adj_close,還原權值含息),用純算術淨值計算 CAGR / 夏普 / 最大回撤
#   - 策略腿 = finlab sim() 的累積淨值,先截斷到資料快照日,再用同一套純算術公式計算,口徑一致
#   - 交易成本 = sim() 台股預設(手續費 0.1425% + 證交稅 0.3%)
#   - 前視處理:ROE / 負債比用 index_str_to_date() 對齊財報公布日,全腿月頻換股延後 14 天
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import finlab
from finlab import data
from finlab.backtest import sim

finlab.login()

START = "2015-01-01"
N = 50  # 每個單一條件挑出的檔數

close = data.get("price:收盤價")
adj = data.get("etl:adj_close")
vol = data.get("price:成交股數")
roe = data.get("fundamental_features:ROE稅後").index_str_to_date()
debt = data.get("fundamental_features:負債比率").index_str_to_date()
pe = data.get("price_earning_ratio:本益比")
pb = data.get("price_earning_ratio:股價淨值比")
rev = data.get("monthly_revenue:去年同月增減(%)")
trust = data.get("institutional_investors_trading_summary:投信買賣超股數")

SNAP = close.index[-1]


def monthly(df):
    """對齊交易日後取每月最後一筆。"""
    return df.reindex(close.index, method="ffill").resample("ME").last()


# 流動性池:近 60 日平均成交金額前 50%,濾掉冷門股
liquid = monthly((close * vol).rolling(60).mean()).rank(axis=1, pct=True) > 0.5

roem, debtm, pem, pbm = monthly(roe), monthly(debt), monthly(pe), monthly(pb)
revm, trustm = monthly(rev), monthly(trust)
momm = monthly(close.pct_change(60))                  # 價格動能(近 60 交易日)
volm = monthly(close.pct_change().rolling(60).std())  # 低波動(越低越好)


def topn(df, largest=True, cond=None):
    """在流動性池內取因子前 N 名,回傳等權持股 (0/1) 矩陣。"""
    d = df.where(liquid)
    if cond is not None:
        d = d.where(cond)
    return (d.rank(axis=1, ascending=not largest) <= N).astype(float)


def run(position, fee=0.001425):
    # fee_ratio 是手續費率(台股預設 0.1425%);調高它就能看到高周轉策略的報酬衰退
    position = position[position.index >= START]
    nav = sim(position, resample="M", resample_offset="14D", fee_ratio=fee, upload=False).creturn
    return nav[(nav.index >= START) & (nav.index <= SNAP)]


def stats(nav):
    nav = nav.dropna()
    nav = nav / nav.iloc[0]
    years = (nav.index[-1] - nav.index[0]).days / 365.25
    cagr = nav.iloc[-1] ** (1 / years) - 1
    rets = nav.pct_change().dropna()
    sharpe = rets.mean() / rets.std() * np.sqrt(252)
    mdd = (nav / nav.cummax() - 1).min()
    return cagr, sharpe, mdd


def rk(df, good_large=True, cond=None):
    d = df.where(liquid)
    if cond is not None:
        d = d.where(cond)
    return d.rank(axis=1, pct=True, ascending=good_large).fillna(0)


# 含息 0050 基準(純算術)
b = adj["0050"].dropna()
b = b[(b.index >= START) & (b.index <= SNAP)]

# 6 個單一條件
singles = {
    "高ROE": topn(roem, largest=True),
    "低本益比": topn(pem, largest=False, cond=(pem > 0)),
    "低股價淨值比": topn(pbm, largest=False, cond=(pbm > 0)),
    "高營收成長": topn(revm, largest=True, cond=(revm < 300)),
    "低負債比": topn(debtm, largest=False),
    "投信買超": topn(trustm, largest=True, cond=(trustm > 0)),
}

# 2 個合成條件
naive_value = (
    (rk(roem, True) + rk(pem, False, cond=(pem > 0)) + rk(debtm, False)).rank(
        axis=1, ascending=False
    )
    <= 40
).astype(float)
multi_factor = (
    (rk(revm, cond=(revm < 300)) + rk(momm) + rk(roem) + rk(volm, good_large=False)).rank(
        axis=1, ascending=False
    )
    <= 40
).astype(float)

print(f"回測 {START} ～ {SNAP.date()}(月頻換股、含交易成本,基準=含息 0050)\n")
bc, bs, bm = stats(b)
print(f"{'含息 0050(基準)':<22} CAGR {bc:7.2%}  夏普 {bs:5.2f}  最大回撤 {bm:7.2%}")
legs = {**singles, "品質+價值+安全": naive_value, "多因子複合": multi_factor}
for name, pos in legs.items():
    c, s, m = stats(run(pos))
    flag = "✓贏0050" if c > bc else ""
    print(f"{name:<22} CAGR {c:7.2%}  夏普 {s:5.2f}  最大回撤 {m:7.2%}  {flag}")

# 交易成本敏感度示範:把 fee_ratio 調高,看高周轉策略的年化報酬如何被成本侵蝕
print("\n交易成本敏感度(以高周轉的「品質+價值+安全」複合為例):")
for fee in [0.0, 0.001425, 0.003, 0.005]:
    c, _, _ = stats(run(naive_value, fee=fee))
    print(f"  fee_ratio={fee:7.4%}  CAGR={c:6.2%}")
