# -*- coding: utf-8 -*-
"""MFI(資金流量指標)在台股的正確用法:純技術雙核心策略。

核心洞見:教科書說 MFI >80 超買要賣、<20 超賣要買,但在台股橫截面上這個方向是錯的——
低 MFI 的弱勢股是「資金棄守」而非「超賣反彈買點」,抄底幾乎無效。
正確用法是把 MFI 拆成兩個零件:
  零件一(選股因子):在多窗動能選出的強勢股裡,剔掉 MFI 最低(資金正在流出)的一群。
  零件二(大盤擇時):用全市場站上 MFI 中位的廣度,當資金流入廣度急轉差時整籃減碼。
全程僅用價格與成交量,不含任何基本面資料。

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

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

# 3) 趨勢閘(品質池):站上季線 MA60 + 剔 60 日報酬波動最高一半 + 站上半年線 MA120
ma60 = close.rolling(60).mean()
ma120 = close.rolling(120).mean()
ret = close.pct_change()
vol60 = ret.rolling(60, min_periods=30).std()
uptrend = (close > ma60) & universe
lowvol_half = vol60.where(uptrend).rank(axis=1, ascending=True) <= 150  # 池內波動最低一半
pool = uptrend & lowvol_half & (close > ma120)

# 4) 主排序:多窗動能合成(20 / 60 / 120 日報酬各自池內橫截面百分位後等權平均),取 top-12
mom20 = close / close.shift(20) - 1
mom60 = close / close.shift(60) - 1
mom120 = close / close.shift(120) - 1
r20 = mom20.where(pool).rank(axis=1, pct=True)
r60 = mom60.where(pool).rank(axis=1, pct=True)
r120 = mom120.where(pool).rank(axis=1, pct=True)
mom_multi = (r20 + r60 + r120) / 3
top12 = mom_multi.where(pool).rank(axis=1, ascending=False) <= 12

# 5) 零件一 MFI(21)資金流出濾網:剔除這 12 檔中 MFI 池內橫截面最低 20%(資金正在流出)
mfi = data.indicator("MFI", timeperiod=21)
mfi_pct = mfi.where(pool).rank(axis=1, pct=True)         # 池內 MFI 百分位,越大=資金越流入
basket = top12 & (mfi_pct > 0.20)

# 6) 等權配置
weights = basket.astype(float)
weights = weights.div(weights.sum(axis=1).replace(0, np.nan), axis=0).fillna(0.0)

# 7) 零件二 MFI 廣度 speed 擇時:全市場站上 MFI 中位(MFI(21) > 50)的個股比例 = 大盤資金流入廣度
#    當這個廣度的 20 日變化轉負(資金流入廣度急轉差)時,整籃曝險縮到 0.5 倉(其餘轉現金,不加槓桿)
mfi_above_mid = (mfi > 50).where(pool)
breadth = mfi_above_mid.sum(axis=1) / pool.sum(axis=1).replace(0, np.nan)
risk_on = (breadth - breadth.shift(20)) >= 0.0
risk_on = risk_on.reindex(close.index).ffill().fillna(True)
scale = pd.Series(np.where(risk_on.values, 1.0, 0.5), index=close.index)
weights = weights.mul(scale, axis=0)

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