# -*- coding: utf-8 -*-
"""ATR(真實波幅)在台股的正確用法:低波選股池 + 1/NATR^3 反波動加權 + ATR 廣度減碼風控。

對應文章:/blog/atr-indicator-taiwan-volatility(ATR 三重風控純技術策略)

核心洞見:ATR 是「風險底盤」,不是「報酬訊號」。它的價值在波動度的「水準」(選低波)、
不在方向、不在個股追蹤停損。正確用法是三重橫截面角色疊加:
  ① 低波選股池:用 NATR = ATR / 收盤價取波動最低的 58% 為池,先砍掉會崩盤的高波股(低波動異象);
  ② 1/NATR^3 強反波動加權:池內持股以 1/NATR^3 配權,低波股給極高權重(經典 ATR 部位控制的極端版);
  ③ ATR 廣度減碼:每日統計池內「NATR > 自身 60 日均」的個股比例,當此廣度 > 0.55
     (多數股波動同步上升 = 系統性風險升溫)時,整體部位溫和減碼到 0.85(其餘轉現金,不加槓桿)。
低波本身報酬天花板低,所以池內再用「多窗動能(20/60/120 日)」補報酬、集中持有 6 檔。

回測結果(完全不含基本面,月換股,台股預設成本):
  研究段 2015-2021 年化 27.7% / 月 Sharpe 1.26;樣本外 2022-2026 年化 26.0% / 月 Sharpe 1.06;
  全期 2015-2026 年化 26.8% / 月 Sharpe 1.13、最大回撤 -41.3%,三段報酬均高於含息 0050。

資料來源: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()  # 套件會在需要資料時自動引導完成登入

DATA_END = "2026-06-09"  # 釘住資料快照日,讓回測可重現


def cap(df):
    """把資料截到快照日,並轉成原生 pandas DataFrame(後續布林運算才不會踩到索引重塑問題)。"""
    return pd.DataFrame(df[df.index <= DATA_END])


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

# 2) 可交易宇宙:60 日均成交金額前 300 大、60 日均量 > 100 萬股、收盤 > 10 元,
#    排除 ETF(00 開頭)與權證 / KY / 特別股(非 4 碼純數字)、金融保險股、處置 / 全額交割股
liquidity_vol = cap(volume).rolling(60, min_periods=20).mean()
top300 = pd.DataFrame(
    amount.rolling(60, min_periods=20).mean().is_largest(300)
).reindex_like(close).fillna(False).astype(bool)

is_common = pd.Series({
    sid: (len(str(sid)) >= 4 and str(sid)[:4].isdigit() and not str(sid).startswith("00"))
    for sid in close.columns
})
categories = data.get("security_categories")
financials = set(
    categories.loc[
        categories["category"].astype(str).str.contains("金融|保險|銀行", na=False),
        "stock_id",
    ].astype(str)
)
for sid in close.columns:
    if str(sid) in financials:
        is_common[sid] = False
common_mask = pd.DataFrame(
    np.tile(is_common.reindex(close.columns).fillna(False).values, (len(close.index), 1)),
    index=close.index, columns=close.columns,
)
flagged = cap(data.get("etl:is_flagged_stock")).reindex_like(close).fillna(False).astype(bool)

universe = (top300 & (liquidity_vol > 1_000_000) & (close > 10)
            & close.notna() & common_mask & (~flagged))

# 3) NATR = ATR(14) / 收盤價(標準化波動度,跨個股可比)
atr = cap(data.indicator("ATR", timeperiod=14))
natr = atr / close

# 4) 零件一 — 低波池:NATR 最低的 58%(低波動異象 + 砍掉易崩盤的高波股)
median_universe = int(universe.sum(axis=1).median())
pool_size = int(median_universe * 0.58)
low_vol_pool = natr.where(universe).rank(axis=1, ascending=True) <= pool_size

# 5) 多窗動能複合分數 = 20 / 60 / 120 日報酬的橫截面百分位排名平均
mom = sum((close / close.shift(w) - 1).rank(axis=1, pct=True) for w in (20, 60, 120)) / 3

# 6) 低波池內挑多窗動能前 6 檔(高度集中)
basket = mom.where(universe & low_vol_pool).rank(axis=1, ascending=False) <= 6

# 7) 零件二 — 1/NATR^3 強反波動加權(低波股給極高權重),逐列歸一
inv = (1.0 / natr ** 3).where(basket)
weights = inv.div(inv.sum(axis=1).replace(0, np.nan), axis=0).fillna(0.0)

# 8) 零件三 — ATR 廣度減碼 overlay:
#    每日統計池內「NATR > 自身 60 日均」的個股比例(波動上升廣度)。
#    廣度 > 0.55(多數股波動同步上升 = 風險盤)時整體部位減碼到 0.85,其餘轉現金、不加槓桿。
natr_ma60 = natr.rolling(60, min_periods=30).mean()
rising = (natr > natr_ma60).where(universe)
breadth = rising.sum(axis=1) / universe.sum(axis=1).replace(0, np.nan)
risk_on = (breadth < 0.55).reindex(close.index).ffill().fillna(True)
scale = risk_on.astype(float).replace(0.0, 0.85)        # risk-on=1.0,風險盤=0.85
weights = weights.mul(scale.reindex(weights.index).fillna(1.0), axis=0)

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