"""設定選股條件的 5 個步驟：完整可重現腳本。

把文章裡的五個步驟依序跑一次，輸出的數字就是文章表格裡的數字：

  步驟 2/4  ROE 排名切成前 80/60/40/20% 四種門檻，季換股
  步驟 3    低負債比前 20%（測起來就該丟掉的因子）
  進階      ROE 五分位，檢驗報酬是否隨 ROE 遞增
  步驟 5    在前 40% 的池子上疊價格動能，收斂到 30 檔

⚠️ 流動性過濾是必要的一步：沒有它，「前 80%」會是一千六百多檔含大量無法成交的
   冷門股，回測數字與文章對不上。文章的 863 檔就是加了 20 日均量 200 張門檻後的結果。

執行環境：
    pip install finlab
    python strategy.py

finlab 需要資料時會自動引導登入，不需要在程式裡填 token。
文章：https://finlab.finance/blog/stock-screening-5-steps
"""

import numpy as np
import pandas as pd
from finlab import data
from finlab.backtest import sim

START = "2015-01-01"
END = "2026-07-24"          # 回測釘日，與文章數字同一口徑
RISK_FREE = 0.01


def load():
    close = data.get("price:收盤價")
    volume = data.get("price:成交股數")
    roe = data.get("fundamental_features:ROE稅後")
    debt = data.get("fundamental_features:負債比率")
    return close, volume, roe, debt


def describe(curve, label):
    curve = curve.dropna()
    curve = curve / curve.iloc[0]
    daily = curve.pct_change().dropna()
    years = (curve.index[-1] - curve.index[0]).days / 365.25
    cagr = curve.iloc[-1] ** (1 / years) - 1
    sharpe = (daily.mean() * 252 - RISK_FREE) / (daily.std() * np.sqrt(252))
    mdd = (curve / curve.cummax() - 1).min()
    print(f"{label:26s} 年化 {cagr:6.1%}  日夏普 {sharpe:5.2f}  最大回撤 {mdd:6.1%}")
    return curve


def main():
    close, volume, roe, debt = load()

    # 橫向百分位排名：0 代表全市場最低，1 代表最高
    roe_rank = roe.rank(axis=1, pct=True)

    # 流動性過濾：20 日平均成交量 200 張以上
    liquid = volume.average(20) > 200 * 1000

    curves = {}

    # ── 步驟 2 與 4：同一個因子，四種寬嚴門檻 ──────────────────────────
    for label, threshold in [("前 80%", 0.2), ("前 60%", 0.4), ("前 40%", 0.6), ("前 20%", 0.8)]:
        position = ((roe_rank > threshold) & liquid).loc[START:END]
        report = sim(position, resample="Q", upload=False, name=f"ROE {label}")
        curves[f"ROE {label}"] = report.creturn.loc[START:END]
        holdings = position.sum(axis=1).replace(0, pd.NA).mean()
        print(f"ROE {label} 平均持股 {holdings:.0f} 檔")

    # ── 步驟 3：測起來沒效果的因子，直接棄用 ───────────────────────────
    debt_rank = debt.rank(axis=1, pct=True)
    position = ((debt_rank < 0.2) & liquid).loc[START:END]
    report = sim(position, resample="Q", upload=False, name="低負債比前 20%")
    curves["低負債比前 20%"] = report.creturn.loc[START:END]

    # ── 進階：ROE 五分位，檢驗「越高越好」這個假設 ─────────────────────
    for i in range(5):
        low, high = i / 5, (i + 1) / 5
        position = ((roe_rank > low) & (roe_rank <= high) & liquid).loc[START:END]
        report = sim(position, resample="Q", upload=False, name=f"ROE Q{i + 1}")
        curves[f"ROE Q{i + 1}"] = report.creturn.loc[START:END]

    # ── 步驟 5：在存活的池子上疊第二個因子，收斂到 30 檔 ────────────────
    momentum = close / close.shift(60) - 1
    base = (roe_rank > 0.6) & liquid
    # momentum 有正有負，加 1 讓整段為正，否則負值相乘會把排序翻過來
    position = (base * (momentum + 1)).is_largest(30).loc[START:END]
    report = sim(position, resample="Q", upload=False, name="ROE 前 40% ＋ 動能 30 檔")
    curves["ROE 前 40%＋動能 30 檔"] = report.creturn.loc[START:END]

    # ── 基準：0050 含息，切到與策略相同的起始日再各自歸一 ───────────────
    benchmark = data.get("etl:adj_close")["0050"].dropna().loc[START:END]
    common_start = max(min(c.index[0] for c in curves.values()), benchmark.index[0])
    benchmark = benchmark.loc[common_start:]
    print(f"\n共同起始日 {common_start.date()}\n")

    for label, curve in curves.items():
        describe(curve.loc[common_start:], label)
    bench_curve = describe(benchmark, "0050 含息買進持有")

    # ── 分段檢查：全期輸給大盤，是不是集中在某一段 ──────────────────────
    stacked = curves["ROE 前 40%＋動能 30 檔"].loc[common_start:]
    print("\n分段對照")
    for start, end, label in [(common_start, "2024-12-31", "2015-03 ~ 2024-12"),
                              ("2025-01-01", END, "2025-01 ~ 2026-07")]:
        describe(stacked.loc[start:end], f"疊加版 {label}")
        describe(bench_curve.loc[start:end], f"0050  {label}")


if __name__ == "__main__":
    main()
