# -*- coding: utf-8 -*-
"""把均線當「選股因子＋個股停損」的純技術策略(台股),月頻換股、全程不加槓桿。

核心洞見:均線不該拿「對短均線的瞬間乖離」或「兩條均線剛交叉」當訊號(那是高頻、
雙義的雜訊,在台股研究段都贏不過買進持有 0050),而該換個角度接成中長期趨勢結構:
  1. 選股因子:收盤對自身過去 280 個交易日滾動最高價的「新高靠近度」
     (= 收盤 ÷ 280 日滾動最高價,越接近 1 越貼近約 52 週的波段高點),
     每月底在可交易宇宙裡取最高的 20 檔等權持有——挑最靠近自己年度新高的強勢股。
  2. 個股停損:持股期間日頻檢查,任一持股收盤跌破自身 20 日均線即清空該檔、
     降為現金(不補倉、不加任何槓桿),替每檔個股設一條趨勢止血線。
完全不使用任何財報或月營收等基本面資料。

資料來源:finlab 套件(price 收盤價、成交金額)。finlab 會在需要資料時自動引導登入。
回測區間 2015-01 ~ 2026-06。過去績效不代表未來,不構成投資建議。
"""
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) 可交易宇宙:60 日均量 > 100 萬股、收盤 > 10 元、成交額前 300 大,
#    排除 ETF(00 開頭)與權證/KY(非 4 碼純數字)
liquidity_vol = volume.rolling(60).mean()
liquidity_amt = 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 = ((liquidity_vol > 1_000_000) & (close > 10)
            & liquidity_amt.is_largest(300) & is_common)

# 3) 選股因子:收盤對自身 280 日滾動最高價的靠近度(越接近 1 越貼近 52 週新高)
rolling_high = close.rolling(280).max()
prox = close / rolling_high

# 4) 每月底取池內 prox 最高的 20 檔等權
ranked = prox.where(universe).rank(axis=1, ascending=False)
basket = ranked <= 20

# 5) 個股停損:收盤跌破自身 20 日均線就清空該檔
ma20 = close.rolling(20).mean()
hold = close > ma20  # 維持持有的條件:收盤站在自身 20 日均線之上

# 6) 進場部位:當月選入且未跌破 20 日線才續抱(月再平衡 + 個股級停損)
position = basket & hold

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