"""四因子選股（營收＋動能＋ROE＋低波）：跑到最新資料的版本（/strategies/multi-factor-stock-selection-beat-0050 的數字來源）。

條件與 /blog/multi-factor-stock-selection-beat-0050 附的 strategy.py 相同，只是不截斷結束日：
- 選股池：月營收年增連續 3 個月為正，且介於 10% 到 150%
- 營收年增、60 日動能、ROE、60 日低波動四個池內百分位相加，取前 40 檔
- 分數平方加權，每月營收公布後（月初延後 14 天）換股，sim() 預設手續費 0.1425% 與證交稅 0.3%

統計口徑：日夏普 = 日報酬平均 / 標準差 × √252，不扣無風險利率；
0050 含息 = etl:adj_close 還原價買進持有，與策略同一段期間。
執行後會在目前目錄輸出 data.csv（淨值曲線）與 metrics.json（統計）。

    pip install finlab
    python strategy.py

finlab 需要資料時會自動引導登入，不需要在程式裡填 token。
"""

import json

import pandas as pd

import finlab
from finlab import data
from finlab.backtest import sim

STRATEGY_NAME = "四因子選股（營收＋動能＋ROE＋低波）"
START = "2018-01-01"
LOOKBACK = 60   # 動能與波動回看交易日
TOP_N = 40

def build_report():
    close = data.get("price:收盤價")
    rev = data.get("monthly_revenue:去年同月增減(%)")
    roe = data.get("fundamental_features:ROE稅後").index_str_to_date()

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

    revm = monthly(rev)
    momm = monthly(close.pct_change(LOOKBACK))
    roem = monthly(roe)
    volm = monthly(close.pct_change().rolling(LOOKBACK).std())
    pool = (revm > 10) & (revm < 150) & ((revm > 0).rolling(3).sum() == 3)

    def rk(df, asc=True):
        """池內百分位排名；asc=False 代表越小越好。"""
        return df.where(pool).rank(axis=1, pct=True, ascending=asc).fillna(0)

    score = rk(revm) + rk(momm) + rk(roem) + rk(volm, asc=False)
    mask = score.rank(axis=1, ascending=False) <= TOP_N
    masked = score.where(mask, 0) ** 2
    weight = masked.div(masked.sum(axis=1), axis=0).fillna(0)
    weight = weight[weight.index >= START]
    return sim(weight, resample="M", resample_offset="14D", upload=False, name=STRATEGY_NAME)


def curve_stats(curve: pd.Series) -> dict:
    curve = curve / curve.iloc[0]
    daily = curve.pct_change().dropna()
    years = (curve.index[-1] - curve.index[0]).days / 365.25
    return {
        "cagr": curve.iloc[-1] ** (1 / years) - 1,
        "daily_sharpe": daily.mean() / daily.std() * 252 ** 0.5,
        "max_drawdown": (curve / curve.cummax() - 1).min(),
        "total_return": curve.iloc[-1] - 1,
    }


def main() -> None:
    finlab.login()
    # 0050 要在策略設定股票池之前取得
    bench_all = data.get("etl:adj_close")["0050"].dropna()
    report = build_report()

    # 從第一次有部位的那天起算，0050 含息用同一段期間
    creturn = report.creturn.dropna()
    first_move = (creturn != creturn.iloc[0]).argmax()
    creturn = creturn.iloc[max(first_move - 1, 0):]
    bench = bench_all.loc[creturn.index[0]:creturn.index[-1]]

    strategy_stats = curve_stats(creturn)
    bench_stats = curve_stats(bench)
    holdings = (report.position.fillna(0) != 0).sum(axis=1)
    strategy_stats["avg_holdings"] = holdings[holdings > 0].mean()

    start, end = creturn.index[0].date(), creturn.index[-1].date()
    print(f"期間 {start} 至 {end}")
    for label, s in [("策略", strategy_stats), ("0050 含息", bench_stats)]:
        print(f"{label}：年化 {s['cagr']:.2%}，日夏普 {s['daily_sharpe']:.2f}，最大回撤 {s['max_drawdown']:.2%}")

    curves = pd.DataFrame({STRATEGY_NAME: creturn / creturn.iloc[0], "0050含息": bench / bench.iloc[0]})
    curves.index.name = "date"
    curves.round(6).to_csv("data.csv")
    metrics = {
        "window": f"{start}/{end}",
        "star": STRATEGY_NAME,
        "variants": {STRATEGY_NAME: {k: round(float(v), 6) for k, v in strategy_stats.items()}},
        "benchmark_0050": {k: round(float(v), 6) for k, v in bench_stats.items()},
        "run_date": str(pd.Timestamp.today().date()),
        "finlab_version": finlab.__version__,
        "generated_by": "static/blog/multi-factor-stock-selection-beat-0050/validation/strategy.py",
    }
    with open("metrics.json", "w", encoding="utf-8") as f:
        json.dump(metrics, f, ensure_ascii=False, indent=2)


if __name__ == "__main__":
    main()
