# -*- coding: utf-8 -*-
"""多週期動能 + ROE 品質 + 雙倍低波 + 趨勢過濾(波動率倒數加權)— finlab 台股真實回測(可重現)。

對應文章:https://finlab.finance/blog/codex-finlab-quant-backtest
回測區間:2018-01-01 ~ 2026-06-09(全站 canonical 快照日)
基準:0050 含息(etl:adj_close 還原價買進持有)
成本:finlab sim() 台股預設(手續費 0.1425%、賣出證交稅 0.3%),未另設滑價

這是文章中 Codex 第二輪迭代後的推薦版:在第一輪(多週期動能+低波控+趨勢)之上
加入 ROE 獲利品質、把低波權重加倍,並把持股權重從分數平方改成 60 日波動率倒數。
全段回測:年化 18.4%、最大回撤 -30.2%、年化波動 16.0%,
回撤與波動兩項優於同視窗 0050 含息(-34.0%、20.8%),年化報酬仍低於 0050 的 25.0%。

執行:
  cd ~/Documents/finlab && UV_ENV_FILE=.env uv run --with finlab python strategy.py

本程式僅供量化研究與教學,過去績效不代表未來表現,不構成任何投資建議。
"""
import finlab
from finlab import data
from finlab.backtest import sim

finlab.login()

# 1) 載入資料(首次執行會自動引導登入)
close = data.get("price:收盤價")
volume = data.get("price:成交股數")
# ROE 是季頻財報,先對齊公布日再向後填值到日頻,避免用到當下還沒公布的數字
roe = data.get("fundamental_features:ROE稅後").index_str_to_date()
roe = roe.reindex(close.index, method="ffill")

# 2) 流動性過濾 + 趨勢過濾(只在多頭結構中持有)
amount = (close * volume).rolling(20).mean()
moving_average_200 = close.rolling(200).mean()
liquid = (amount > 10_000_000) & close.notna()
tradable = liquid & (close > moving_average_200)

# 3) 因子:多週期動能(短中長)+ 低波動 + ROE 獲利品質
momentum_short = close.pct_change(60)
momentum_mid = close.pct_change(120)
momentum_long = close.pct_change(240)
volatility = close.pct_change().rolling(60).std()

rank_short = momentum_short.where(liquid).rank(axis=1, pct=True)
rank_mid = momentum_mid.where(liquid).rank(axis=1, pct=True)
rank_long = momentum_long.where(liquid).rank(axis=1, pct=True)
rank_low_vol = volatility.where(liquid).rank(axis=1, pct=True, ascending=False)
rank_quality = roe.where(liquid).rank(axis=1, pct=True)

# 低波權重加倍:防守因子在綜合分數裡的話語權比任一單一動能週期大
score = rank_short + rank_mid + rank_long + 2 * rank_low_vol + rank_quality

# 4) 取前 20 名,改用波動率倒數加權(波動越低,權重越高),每月再平衡
top20 = score.where(tradable).rank(axis=1, ascending=False) <= 20
inverse_volatility = (1 / volatility).replace([float("inf"), float("-inf")], 0)
weight = inverse_volatility.where(top20, 0).fillna(0)
weight = weight.div(weight.sum(axis=1), axis=0)

report = sim(weight, resample="M", name="多週期動能+品質+雙倍低波(波動率倒數加權)")
report.display()
