# -*- coding: utf-8 -*-
"""Aroon 震盪選股 + Aroon 自家廣度風控:完全不含基本面的純技術策略(台股)。

核心洞見:Aroon 量的不是漲多少,而是「距離上次創新高 / 新低過了幾天」——
AroonUp 減 AroonDown 得到的震盪(−100 ~ +100)是一個乾淨的「趨勢起點偵測器」:
震盪剛翻正、且近期持續站在零軸上方,代表這檔股票剛從盤整轉入上升趨勢、起漲還新鮮。
把它組成純技術策略:
  (1) 趨勢持續性濾網(去假突破):近 20 交易日內「震盪 > 0」天數 >= 15。
  (2) 在合格池內取震盪橫截面最高 10 檔。
  (3) 反波動加權:權重 ∝ 1 / 個股 60 日日報酬標準差。
  (4) 風控 overlay:全市場「震盪 > 0」家數比例(廣度)平滑 14 日,跌破其
      120 日滾動均 − 1.2 倍滾動標準差時,把股票部位減碼到 50%、其餘持現金。
報酬引擎完全來自 Aroon 機制,無任何財報或月營收輸入。

資料來源: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 大、60 日均量 > 100 萬股、收盤 > 10 元,
#    排除 ETF(00 開頭)與權證 / KY(非 4 碼純數字)
liquidity_amount = amount.rolling(60).mean()
liquidity_volume = 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_amount.is_largest(300)
            & (liquidity_volume > 1_000_000)
            & (close > 10)
            & is_common)

# 3) Aroon(20):震盪 = AroonUp − AroonDown(越高 = 上升趨勢越領先)
aroon_down, aroon_up = data.indicator("AROON", timeperiod=20)
oscillator = aroon_up - aroon_down

# 4) 趨勢持續性濾網:近 20 交易日內「震盪 > 0」天數 >= 15(濾掉假突破)
persistence = (oscillator > 0).rolling(20).sum()
qualified = universe & (persistence >= 15)

# 5) 選股:合格池內 Aroon 震盪橫截面最高 10 檔
basket = oscillator.where(qualified).rank(axis=1, ascending=False) <= 10

# 6) 反波動加權:權重 ∝ 1 / 個股 60 日日報酬標準差,逐日組內正規化
vol60 = close.pct_change().rolling(60).std()
inv_vol = (1.0 / vol60).where(basket)
weights = inv_vol.div(inv_vol.sum(axis=1).replace(0, np.nan), axis=0).fillna(0.0)

# 7) 大盤風控:全市場「震盪 > 0」家數比例 = 廣度,平滑 14 日,
#    跌破 120 日滾動均 − 1.2 倍滾動標準差 → 減碼到 50%
breadth = (oscillator > 0).where(universe).sum(axis=1) / universe.sum(axis=1)
breadth = breadth.ewm(span=14).mean()
floor = breadth.rolling(120).mean() - 1.2 * breadth.rolling(120).std()
risk_on = (breadth >= floor).reindex(weights.index).ffill().fillna(True)

# 8) 配置:廣度崩潰日整體股票部位減碼到 50%(其餘持現金,不放空、不加槓桿)
scale = np.where(risk_on.values, 1.0, 0.5)
weights = weights.mul(scale, axis=0)

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