"""時間序列動能 TSMOM 12-1：三項穩健性驗證（/blog/python-time-series-pandas-tutorial 驗證段的數字來源）。

條件與文中主策略相同，只是不截斷結束日：
- 股票池：近 20 日成交金額移動平均排名前 250
- 訊號：還原股價 12-1 自身報酬（20 個交易日前對 240 個交易日前）大於 0
- 權重：60 日年化波動度倒數，正規化後總和為 1；2018 年起每月換股，
  sim() 預設手續費 0.1425% 與證交稅 0.3%

三項檢查：
1. 鄰近參數：回看天數 180、240、300 乘上股票池 200、250、300 檔，9 組日夏普要有 7 組以上高於同期 0050 含息
2. 前後兩段：期間對半切，兩段都要高於 0050 含息
3. 交易成本：sim() 預設手續費與證交稅（另跑不扣成本的對照）

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

    pip install finlab
    python strategy.py

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

import json

import numpy as np
import pandas as pd

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

STRATEGY_NAME = "時間序列動能 TSMOM 12-1"
START = "2018-01-01"
LOOKBACK = 240    # 動能回看交易日（剔除最近 20 日）
POOL_SIZE = 250   # 成交金額排名前幾檔
LOOKBACK_GRID = [180, 240, 300]
POOL_SIZE_GRID = [200, 250, 300]


def build_report(lookback=LOOKBACK, pool_size=POOL_SIZE, **cost):
    adj = data.get("etl:adj_close")
    amount = data.get("price:成交金額")

    pool = amount.rolling(20).mean().rank(axis=1, ascending=False) <= pool_size
    signal = (adj.shift(20) / adj.shift(lookback) - 1 > 0) & pool
    vol_60d = adj.pct_change().rolling(60).std() * np.sqrt(252)

    weights = (1.0 / vol_60d).where(signal)
    position = weights.div(weights.sum(axis=1), axis=0).fillna(0)
    return sim(position.loc[START:], resample="M", upload=False, name=STRATEGY_NAME, **cost)


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 trimmed(creturn: pd.Series) -> pd.Series:
    """從第一次有部位的前一天起算。"""
    creturn = creturn.dropna()
    first_move = (creturn != creturn.iloc[0]).argmax()
    return creturn.iloc[max(first_move - 1, 0):]


def window_stats(creturn: pd.Series, bench_all: pd.Series) -> dict:
    bench = bench_all.loc[creturn.index[0]:creturn.index[-1]]
    return {"window": f"{creturn.index[0].date()}/{creturn.index[-1].date()}",
            "strategy": curve_stats(creturn), "benchmark_0050": curve_stats(bench)}


def rounded(obj):
    if isinstance(obj, dict):
        return {k: rounded(v) for k, v in obj.items()}
    return round(float(obj), 6) if isinstance(obj, float) else obj


def main() -> None:
    finlab.login()
    bench_all = data.get("etl:adj_close")["0050"].dropna()
    creturn = trimmed(build_report().creturn)
    mid = creturn.loc[creturn.index[0] + (creturn.index[-1] - creturn.index[0]) / 2:].index[0]
    result = {
        "main": window_stats(creturn, bench_all),
        "no_cost": curve_stats(trimmed(build_report(fee_ratio=0, tax_ratio=0).creturn)),
        "grid": {f"股票池 {p} 檔、回看 {n} 日": window_stats(trimmed(build_report(n, p).creturn), bench_all)
                 for p in POOL_SIZE_GRID for n in LOOKBACK_GRID},
        "halves": {"first": window_stats(creturn.loc[:mid], bench_all),
                   "second": window_stats(creturn.loc[mid:], bench_all)},
    }

    wins = sum(g["strategy"]["daily_sharpe"] > g["benchmark_0050"]["daily_sharpe"] for g in result["grid"].values())
    print(f"期間 {result['main']['window']}")
    for label, s in [("策略", result["main"]["strategy"]), ("0050 含息", result["main"]["benchmark_0050"]),
                     ("不扣成本", result["no_cost"])]:
        print(f"{label}：年化 {s['cagr']:.2%}，日夏普 {s['daily_sharpe']:.2f}，最大回撤 {s['max_drawdown']:.2%}")
    print(f"鄰近參數：{wins}/9 組日夏普高於 0050 含息")
    for label, g in result["grid"].items():
        print(f"  {label}：{g['strategy']['daily_sharpe']:.2f}"
              f"（年化 {g['strategy']['cagr']:.2%}；0050 {g['benchmark_0050']['daily_sharpe']:.2f}）")
    for key, part in result["halves"].items():
        print(f"{key} {part['window']}：{part['strategy']['daily_sharpe']:.2f} 對 {part['benchmark_0050']['daily_sharpe']:.2f}")

    metrics = {
        "star": STRATEGY_NAME,
        **result,
        "run_date": str(pd.Timestamp.today().date()),
        "finlab_version": finlab.__version__,
        "generated_by": "static/blog/python-time-series-pandas-tutorial/validation/strategy.py",
    }
    with open("metrics.json", "w", encoding="utf-8") as f:
        json.dump(rounded(metrics), f, ensure_ascii=False, indent=2)


if __name__ == "__main__":
    main()
