# -*- coding: utf-8 -*-
"""OBV(能量潮)當「大盤風控開關」:OBV 廣度擇時 + 防禦輪動(台股)。

核心洞見:OBV 是累積量能 level,跨股不可比(被股本與上市年資主導),
直接拿「OBV 最高」選股等於變相選大型股,沒有獨立 alpha。
OBV 在台股最乾淨的 edge 是當「市場廣度 regime 濾網」——
全市場有多少比例的個股 OBV 站上自身均線,代表大盤量能多空力道。
當量能強(廣度高)就滿倉大盤,量能退潮就「不空手、轉防禦資產」。

規則:
  1) 每日算流動性宇宙中「OBV 站上自身 60 日均」的個股佔比 = OBV 廣度。
  2) 廣度 > 0.58 → 持有含息 0050(滿倉台股大盤)。
  3) 否則 → 不空手,改持防禦 ETF 00646(標普 500)。
  4) 月再平衡。

資料來源: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:收盤價")
volume = data.get("price:成交股數")
amount = data.get("price:成交金額")
adj = data.get("etl:adj_close")          # 還原價,含息買進持有用

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

# 3) OBV(能量潮)與「站上自身 60 日均」的個股
obv = data.indicator("OBV")
obv_above_self_ma = obv > obv.rolling(60).mean()

# 4) OBV 廣度:可交易宇宙中,OBV 站上自身均線的個股比例
breadth_count = obv_above_self_ma.where(universe).sum(axis=1)
universe_count = universe.sum(axis=1).replace(0, np.nan)
obv_breadth = breadth_count / universe_count

# 5) 大盤切換訊號:廣度 > 0.58 = 強多 regime(滿倉),否則 = 量能退潮(轉防禦)
risk_on = obv_breadth > 0.58

# 6) 配置:risk-on 持 0050、risk-off 轉 00646(標普 500),日層級訊號 ffill 到交易日
risk_on = risk_on.reindex(adj.index).ffill().fillna(False)
position = pd.DataFrame(0.0, index=adj.index, columns=["0050", "00646"])
position.loc[risk_on, "0050"] = 1.0
position.loc[~risk_on, "00646"] = 1.0

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