# -*- coding: utf-8 -*-
"""乖離率(BIAS)在台股的正確用法:MA60 乖離當動能因子選強勢股 + 純技術多層風控
(非教科書的負乖離抄底)。全程不使用任何基本面資料。

核心洞見:台股的乖離率是「相對均線的動能/位置因子」,不是均值回歸指標。
  - 教科書「負乖離過大抄底」在台股研究段最弱(逆勢接刀被趨勢碾壓)。
  - 反過來「橫截面取正乖離最大的強勢股」(相對均線最強)才有真實 edge。
  - 乖離均線用 MA60(季線)而非散戶慣用的 MA20:抓中期動能、雜訊更低。

策略(純技術、不加槓桿):
  1. 流動性宇宙內以站上季線 + 低波過濾框出品質池;再加 200 日動能濾網確認多頭。
  2. 池內取 MA60 乖離最大的 15 檔,以反波動(1 / vol60 ** 0.5)加權。
  3. 個股 ATR(14) 現金停損:回落超過 2.5×ATR 即該格換現金、不補進(真降曝險)。
  4. 趨勢廣度 gentle 減碼:全市場站上 MA60 比例偏低時,線性把整籃曝險減到最低四成。

資料來源: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:成交金額")
volume = data.get("price:成交股數")

# 2) 可交易宇宙:成交額前 300 大、收盤 > 10 元、60 日均量 > 100 萬股,
#    排除 ETF(00 開頭)與權證/KY(非 4 碼純數字)
liquidity_amt = amount.rolling(60).mean()
liquidity_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) & (close > 10) & (liquidity_vol > 1_000_000) & is_common

# 3) 核心因子與輔助序列(全部純技術,本地計算)
ma60 = close.rolling(60).mean()
bias = (close - ma60) / ma60                 # 季線乖離率:相對 MA60 的位置/動能
mom200 = close / close.shift(200) - 1        # 200 日動能:半年趨勢濾網(取代基本面)
atr = data.indicator("ATR", timeperiod=14)
ret = close.pct_change()
vol60 = ret.rolling(60, min_periods=30).std()

# 4) 品質池:站上季線 → 剔除 60 日報酬波動最高的一半 → 再加 200 日動能 > 0
uptrend = close > ma60
lowvol_half = vol60.where(universe & uptrend).rank(axis=1, ascending=True) <= 150
quality_pool = (universe & uptrend) & lowvol_half & (mom200 > 0)

# 5) 池內取 MA60 乖離最大的 15 檔(動能選股,相對季線最強者)
basket = bias.where(quality_pool).rank(axis=1, ascending=False) <= 15

# 6) 個股 ATR 現金停損:近 180 日高點回落超過 2.5×ATR(14) → 該格換現金
roll_high = close.rolling(180, min_periods=20).max()
still_holding = close > (roll_high - 2.5 * atr)

# 7) 反波動加權(1 / vol60 ** 0.5);被停損的格子歸現金、不重分配 = 真降曝險
held = basket & still_holding
inv_vol = (1.0 / (vol60 ** 0.5)).where(held)
weights = inv_vol.div(inv_vol.where(basket).sum(axis=1), axis=0).fillna(0.0)

# 8) 趨勢廣度 gentle 減碼:全市場站上 MA60 比例為訊號,
#    expo = clip(1 + (breadth - 0.55) / 0.55, 0.40, 1.0),低廣度時整籃線性減碼到四成
breadth = uptrend.where(universe).sum(axis=1) / universe.sum(axis=1)
expo = (1.0 + (breadth - 0.55) / 0.55).clip(lower=0.40, upper=1.0)
weights = weights.mul(expo.reindex(close.index).ffill().fillna(1.0), axis=0)

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