# -*- coding: utf-8 -*-
"""MACD 在台股的正確用法:雙均線趨勢閘 + 多窗動能低波核心 + MACD 反轉因子 + SMA120 廣度擇時(台股)。

核心洞見:教科書把 MACD 柱狀(hist)當「越高越多方」的動能訊號,
但在台股月頻橫截面,hist 最低(剛超賣)的一籃子反而穩定贏 hist 最高的一籃子——
MACD hist 是反轉訊號,不是動能訊號。它的正確位置是「複合策略裡的小權重反轉加分因子」,
疊在多窗動能 + 低波核心上;選股池先用「收盤同時站上 SMA60 與 SMA120」的雙均線趨勢閘縮池,
壓回撤則交給「站上 SMA120 比例 > 0.5 才持倉」的廣度擇時,而不是 MACD 本身。
本策略完全不含基本面,純技術。

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

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

# 1) 載入價量資料
close = data.get("price:收盤價")
amount = data.get("price:成交金額")

# 2) 可交易宇宙:成交額前 300 大、收盤 > 10 元,排除 ETF(00 開頭)與權證、KY(非 4 碼純數字)
liquidity = amount.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.is_largest(300) & (close > 10) & is_common

# 3) 雙均線趨勢閘:收盤同時站上自身 SMA60 與 SMA120,把選股池縮到結構偏多的個股
sma60 = data.indicator("SMA", timeperiod=60)
sma120 = data.indicator("SMA", timeperiod=120)
trend_gate = (close > sma60) & (close > sma120)
pool = universe & trend_gate

# 4) 三個橫截面分位分數(全部「分位越小 = 越好」)
#    多窗動能:20 / 60 / 120 日報酬三個分位取平均(報酬越高分位越小 = 越好)
mom_rank = pd.concat([
    (-close.pct_change(w)).where(pool).rank(axis=1, pct=True) for w in (20, 60, 120)
]).groupby(level=0).mean()
#    低波:NATR(14 日 ATR / 收盤)越低越好
natr = data.indicator("NATR", timeperiod=14)
vol_rank = natr.where(pool).rank(axis=1, pct=True)
#    MACD 反轉:8-17-9 的 hist 越低(越超賣)越好——這是台股的正確方向,不是追高
dif, dea, hist = data.indicator("MACD", fastperiod=8, slowperiod=17, signalperiod=9)
rev_rank = hist.where(pool).rank(axis=1, pct=True)

# 5) 綜合分 = 0.65 多窗動能 + 0.20 低波 + 0.15 MACD 反轉,選前 15 檔等權
score = 0.65 * mom_rank + 0.20 * vol_rank + 0.15 * rev_rank
basket = score.where(pool).rank(axis=1) <= 15

# 6) 市場廣度擇時:可交易宇宙裡收盤站上 120 日均線的個股比例 > 0.5 才持倉,否則純現金
above_sma120 = (close > sma120).where(universe)
breadth = above_sma120.sum(axis=1) / universe.sum(axis=1)
risk_on = (breadth > 0.5)

# 7) 套擇時:risk-off 的交易日清空整個籃子(純現金,不加槓桿)
risk_on_daily = risk_on.reindex(basket.index).ffill().fillna(False)
mask = pd.DataFrame(np.repeat(risk_on_daily.values[:, None], basket.shape[1], axis=1),
                    index=basket.index, columns=basket.columns)
position = basket & mask

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