# -*- coding: utf-8 -*-
"""加速度指標（均線曲率）選股策略：台股 2018-2026 完整回測。

對應文章：
  https://finlab.finance/blog/acceleration-indicator-strategy

執行（需安裝 finlab 套件，首次執行會自動引導登入）：
  pip install finlab pandas numpy
  python strategy.py

策略規格：
  - 股票池：TSE + OTC 普通股（排除 ETF），60 日均成交金額 > 5,000 萬
  - 指標：60 日均線（還原價）曲率，(MA[t-n] + MA[t]) / 2 > MA[t-n/2]，
    n = 10 / 15 / 20 / 50 全數成立（多組採樣排除假曲率）
  - 初速濾網：還原收盤價 > 60 日均線
  - 基本面濾網：ROE 稅後 > 3%（財報季別經 index_str_to_date 對齊公布期限）
  - 再平衡：週（可改 None＝每日、'M'＝每月，對照成本侵蝕）
  - 交易成本：finlab sim() 台股預設（手續費 0.1425% + 證交稅 0.3%）
  - 基準：0050 還原價買進持有（純指數算術，不含交易成本）
  - 統計口徑：sim() 的累積淨值會延伸到執行當日，本程式一律先把
    淨值序列雙端截斷到 START ~ END（2026-06-09 資料快照日），再以
    純算術公式計算 CAGR、夏普、最大回撤，與文章及 0050 基準同口徑

投資警語：本程式僅供量化研究與教學用途，過去績效不代表未來表現，
不構成任何投資建議；實際交易前請自行評估風險、滑價與交易容量。
文章結論：本策略 2018-01 ~ 2026-06 費後年化 -2.58%，輸給 0050 的 25.05%，
請把這份程式碼當成研究流程的示範，而非可直接上線的策略。
"""
import finlab
from finlab import data
from finlab.backtest import sim

finlab.login()

START = "2018-01-01"
END = "2026-06-09"  # 文章數據快照日；要重現本文數字必須釘住這一天

# 只取 TSE + OTC 普通股（排除 ETF 等非個股）
data.universe(market="TSE_OTC")

# ---------- 載入資料 ----------
adj = data.get("etl:adj_close")            # 還原收盤價（含除權息調整）
amount = data.get("price:成交金額")          # 每日成交金額

# ---------- 計算加速度指標 ----------
ma60 = adj.rolling(60).mean()


def accel(n):
    """均線曲率為正：n 天前與今日均線的中點，高於 n/2 天前的均線。"""
    return (ma60.shift(n) + ma60) / 2 > ma60.shift(n // 2)


# 多組採樣交集：四個週期的曲率都要為正，排除單一採樣的假訊號
acceleration = accel(10) & accel(15) & accel(20) & accel(50)

# ---------- 濾網 ----------
# 初速濾網：股價站上 60 日均線（買起漲股，不買剛止跌的股票）
velocity = adj > ma60

# 基本面濾網：ROE 稅後 > 3%
roe = data.get("fundamental_features:ROE稅後").index_str_to_date()
roe_ok = roe.reindex(adj.index, method="ffill") > 3

# 流動性濾網：60 日均成交金額 > 5,000 萬，避免回測買得到、實單買不到
liquid = amount.rolling(60).mean() > 50_000_000

# ---------- 組合持股與回測 ----------
position = (acceleration & velocity & roe_ok & liquid)[START:END]

# resample='W' 為週再平衡；改 None 重現每日換股、改 'M' 重現每月換股
report = sim(position, resample="W", upload=False)

# ---------- 統計（與文章同口徑） ----------
# sim() 的 creturn 會延伸到執行當日，統計前先雙端截斷到資料快照日，
# 再用純算術公式計算，數字才會與文章及 0050 基準一致
cr = report.creturn
cr = cr[(cr.index >= START) & (cr.index <= END)]
ret = cr.pct_change().dropna()
total = cr.iloc[-1] / cr.iloc[0] - 1
years = (cr.index[-1] - cr.index[0]).days / 365.25

print(f"回測區間：{cr.index[0].date()} ~ {cr.index[-1].date()}")
print(f"年化報酬 CAGR：{((1 + total) ** (1 / years) - 1) * 100:.2f}%")
print(f"日夏普：{ret.mean() / ret.std() * 252 ** 0.5:.2f}")
print(f"最大回撤：{(cr / cr.cummax() - 1).min() * 100:.2f}%")
print(f"總報酬：{total * 100:.1f}%")

report.display()
