# -*- coding: utf-8 -*-
"""CCI 順勢指標在台股的正確用法:拆成趨勢濾網 + 大盤斷路器的純技術策略。

核心洞見:整個 CCI 當買賣開關失效(超賣買進研究段年化僅 2.0%),但拆成兩個零件就有效——
  零件一(個股 CCI 趨勢持有濾網):只把錢留在站季線且 CCI(14) > -50 的順勢股,
    剔除順勢深跌的接刀股;不是當「誰高買誰」的橫截面排序。
  零件二(0050 自身 CCI 大盤斷路器):用 0050 自己的 CCI(20) 偵測系統性深弱勢,
    跌破 -100 時整籃清倉持現金避開崩盤尾部,平時滿倉吃複利。
選股 edge 由「站季線 + 60 日低波 + 籃內動能/波動傾斜」提供。全程僅用價格,不含任何基本面資料。

研究段(2015-2021)年化 25.5%、月夏普 1.23;樣本外(2022-2026)年化 24.7%、月夏普 1.32;
全期(2015-2026)年化 24.9%、月夏普 1.25、全期最大回撤 -21.0%。

資料來源:finlab 套件(price / etl:adj_close)。finlab 會在需要資料時自動引導登入。
回測區間 2015-01 ~ 2026-06。過去績效不代表未來,不構成投資建議。
"""
import numpy as np
import pandas as pd
import talib
import finlab
from finlab import data
from finlab.backtest import sim

finlab.login()  # 依套件指示完成登入即可

# 1) 載入價量資料
close = data.get("price:收盤價")
high = data.get("price:最高價")
low = data.get("price:最低價")
volume = data.get("price:成交股數")
amount = data.get("price:成交金額")

# 2) 可交易宇宙:成交額前 300 大、60 日均量 > 100 萬股、收盤 > 10 元,
#    排除 ETF(00 開頭)與權證 / KY(非 4 碼純數字)
liquidity_amt = amount.rolling(60).mean()
avg_vol = volume.rolling(60).mean()
is_common = pd.Series({sid: (str(sid)[:4].isdigit() and not str(sid).startswith("00"))
                       for sid in close.columns})
universe = (liquidity_amt.is_largest(300) & (avg_vol > 1_000_000)
            & (close > 10) & is_common)

# 3) 零件一:個股 CCI(14) 趨勢濾網。站季線 + CCI > -50(順勢、剔除接刀股)
cci14 = data.indicator("CCI", timeperiod=14)
above_ma60 = close > close.rolling(60).mean()
pool = universe & above_ma60 & (cci14 > -50)

# 4) 選股:池內取 60 日報酬日波動度最低的 24 檔
vol60 = close.pct_change().rolling(60, min_periods=30).std()
basket = vol60.where(pool).rank(axis=1, ascending=True) <= 24

# 5) 加權:籃內動能傾斜除以波動。混合 60 / 120 日動能分位,往高動能、低波傾斜
r60 = (close / close.shift(60) - 1).where(basket).rank(axis=1, pct=True)
r120 = (close / close.shift(120) - 1).where(basket).rank(axis=1, pct=True)
blend = 0.4 * r60 + 0.6 * r120
raw_w = (blend ** 3.5) / vol60
weights = raw_w.where(basket).div(raw_w.where(basket).sum(axis=1), axis=0).fillna(0.0)

# 6) 零件二:0050 自身 CCI(20) 大盤斷路器。跌破 -100(系統性深弱勢)→ 整籃清倉持現金
cci_0050 = pd.Series(
    talib.CCI(high["0050"].values.astype(float),
              low["0050"].values.astype(float),
              close["0050"].values.astype(float),
              timeperiod=20),
    index=close.index,
)
risk_on = (cci_0050 > -100).reindex(weights.index).ffill().fillna(False)
weights = weights.where(risk_on, 0.0)

# 7) 回測(月再平衡,台股預設成本:手續費 + 賣出證交稅 0.3%)
report = sim(weights, resample="M", fee_ratio=1.425 / 1000 * 0.3, upload=False)
print(report.get_stats())
