# -*- coding: utf-8 -*-
"""威廉指標(Williams %R)在台股的純技術用法:中期相對強勢主因子 + 指數擇時半倉。

核心洞見:威廉指標在台股不是「超賣買、超買賣」的均值回歸擺盪指標(教科書用法在台股賠錢),
而是衡量「中期相對強勢」的排序工具。把長窗(35 日)威廉指標當主因子(複合分權重 0.6)、
疊 60 日報酬動能(0.4),取綜合分最高的 11 檔、以反波動度加權集中持有;外層再用
「0050 還原價站上 60 日均線」做純技術擇時,多頭滿倉、空頭把整體部位縮到半倉。
完全不使用任何基本面資料(無月營收 / YoY / ROE / EPS)。

資料來源:finlab 套件(price / etl:adj_close)。finlab 會在需要資料時自動引導登入。
回測區間 2015-01 ~ 2026-06。過去績效不代表未來,不構成投資建議。
全期(2015-2026)年化 31.27%、月 Sharpe 1.21、最大回撤 -34.38%,全程勝過含息 0050。
"""
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_close = data.get("etl:adj_close")  # 還原權值股價,算 0050 指數擇時用

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

# 3) 因子一:威廉指標(35 日)橫截面排名,越接近 0、近 35 日越強勢排名越高(主因子,權重 0.6)
willr = data.indicator("WILLR", timeperiod=35)
willr_rank = willr.where(universe).rank(axis=1, pct=True)

# 4) 因子二:60 日報酬動能橫截面排名(交叉確認,權重 0.4)
mom_rank = (close / close.shift(60) - 1).where(universe).rank(axis=1, pct=True)

# 5) 綜合分取最高 11 檔,再以反波動度(1 / 60 日報酬波動度)加權、池內正規化
score = 0.6 * willr_rank + 0.4 * mom_rank
basket = (score.where(universe).rank(axis=1, ascending=False) <= 11) & universe
inv_vol = 1.0 / close.pct_change().rolling(60, min_periods=30).std().clip(lower=1e-4)
weights = inv_vol.where(basket)
weights = weights.div(weights.sum(axis=1), axis=0).fillna(0.0)

# 6) 指數擇時:0050 還原價站上自身 60 日均線才算 risk-on,
#    再用「近 10 個交易日多數決(>= 50% 站上)」平滑,壓掉均線附近的假訊號
idx = adj_close["0050"].dropna()
idx_ma60 = idx.rolling(60, min_periods=30).mean()
raw_on = (idx > idx_ma60).reindex(close.index).ffill().fillna(False)
risk_on = raw_on.rolling(10, min_periods=1).mean() >= 0.5

# 7) 部位調節:risk-on 滿倉、risk-off 把整體部位縮到半倉(其餘轉現金,不加碼、不用槓桿)
scale = pd.Series(np.where(risk_on.reindex(weights.index).fillna(False).values, 1.0, 0.5),
                  index=weights.index)
position = weights.mul(scale, axis=0)

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