# -*- coding: utf-8 -*-
"""時間序列動能(TSMOM)台股回測:pandas 時間序列工具的完整應用。

對應文章:
  https://finlab.finance/blog/python-time-series-pandas-tutorial

策略規則(Moskowitz, Ooi & Pedersen 2012 的 TSMOM 機制,台股長多版):
  1. 股票池:近 20 日成交金額移動平均排名前 250(流動性過濾)
  2. 訊號:每檔股票「自己」過去 12 個月剔除最近 1 個月的報酬 > 0 才持有
  3. 權重:60 日年化波動度的倒數加權(波動越大,部位越小)
  4. 再平衡:每月初;整個市場都沒有訊號時持有現金
  5. 成本:finlab sim() 台股預設(手續費 0.1425% + 證交稅 0.3%)

回測窗口與文章相同:2018-01-01 到 2026-06-09(資料快照日)。
要看最新數據,把 END 改成今天即可;統計數字會隨資料更新而不同。

執行方式:
  pip install finlab
  python strategy.py
  (finlab 會自動引導登入,不需要手動填 token)

投資警語:本程式僅供量化研究與教學用途,過去績效不代表未來表現,
不構成任何投資建議;實際交易前請自行評估風險、滑價與交易容量。
"""
import numpy as np
from finlab import data
from finlab.backtest import sim

# 與文章一致的回測窗口
START = "2018-01-01"
END = "2026-06-09"

# ---------- 1. 載入資料(截到快照日,確保與文章同一份數據) ----------
# etl:adj_close 是還原股價(含息),date x 股票代號 的時間序列寬表
adj = data.get("etl:adj_close")
adj = adj[adj.index <= END]
amount = data.get("price:成交金額")
amount = amount[amount.index <= END]

# ---------- 2. 流動性過濾 ----------
# 近 20 日成交金額移動平均,排名前 250 檔才納入
liquidity = amount.rolling(20).mean()
pool = liquidity.rank(axis=1, ascending=False) <= 250

# ---------- 3. 計算時間序列動能訊號 ----------
# 12-1 自身報酬:過去約 240 個交易日的報酬,剔除最近 20 個交易日
momentum_12_1 = adj.shift(20) / adj.shift(240) - 1
signal = (momentum_12_1 > 0) & pool

# ---------- 4. 波動度倒數加權 ----------
# 60 日報酬標準差年化(乘 sqrt(252)),波動越大的股票拿越小的權重
daily_returns = adj.pct_change()
vol_60d = daily_returns.rolling(60).std() * np.sqrt(252)

weights = (1.0 / vol_60d).where(signal)
position = weights.div(weights.sum(axis=1), axis=0).fillna(0)
position = position[position.index >= START]

# ---------- 5. 回測 ----------
# resample="M":每月初再平衡;成本用 finlab 台股預設值
report = sim(position, resample="M", upload=False)

# ---------- 6. 統計(與文章同一套公式) ----------
# sim() 的累積淨值 creturn 會延伸到執行當日,
# 統計前先把兩端截到回測窗口,數字才會跟文章對得起來
creturn = report.creturn
creturn = creturn[(creturn.index >= START) & (creturn.index <= END)]

ret = creturn.pct_change().dropna()
mret = creturn.resample("ME").last().pct_change().dropna()
total = float(creturn.iloc[-1] / creturn.iloc[0] - 1)
years = (creturn.index[-1] - creturn.index[0]).days / 365.25

print("回測區間:", creturn.index[0].date(), "至", creturn.index[-1].date())
print("總報酬: {:.1f}%".format(total * 100))
print("年化報酬 CAGR: {:.2f}%".format(((1 + total) ** (1 / years) - 1) * 100))
print("日夏普: {:.2f}".format(float(ret.mean() / ret.std() * 252 ** 0.5)))
print("日索提諾: {:.2f}".format(float(ret.mean() / ret[ret < 0].std() * 252 ** 0.5)))
print("月索提諾: {:.2f}".format(float(mret.mean() / mret[mret < 0].std() * 12 ** 0.5)))
print("最大回撤: {:.2f}%".format(float((creturn / creturn.cummax() - 1).min()) * 100))

report.display()
