# -*- coding: utf-8 -*-
"""VIX 恐慌指數美股抄底事件研究（1990-2026）。

對應文章：
  https://finlab.finance/blog/vix-us-stock-crash-investing-python

執行（任一含 yfinance / pandas / matplotlib 的 Python 環境）：
  pip install yfinance pandas matplotlib
  python strategy.py

口徑：
  - VIX 與 S&P 500（^GSPC，價格指數，不含股息）取自 Yahoo Finance
  - 事件研究為純算術前瞻報酬，無交易成本、無滑價
  - 連續部位版用 SPY 調整收盤（含股息），每次進出場各扣 0.1% 摩擦成本
  - 文章引用的數字以資料截至 2026-06-10 計算，重跑時資料更長、數字會略有不同

投資警語：本程式僅供量化研究與教學用途，過去績效不代表未來表現，
不構成任何投資建議。
"""
import numpy as np
import pandas as pd
import yfinance as yf

DATA_END = "2026-06-10"   # 文章數字的資料截止日；想跑最新資料可改成今天
COOLDOWN = 30             # 訊號去重：條件成立前 30 個交易日內不得再有條件成立
HOLD = 250                # 持有 250 個交易日（約一年）
COST_PER_SIDE = 0.001     # 連續部位版每次進出場各 0.1%


# ---------- 下載資料 ----------
def download_close(ticker: str, start: str) -> pd.Series:
    df = yf.download(ticker, start=start, auto_adjust=True, progress=False)
    s = df["Close"]
    if isinstance(s, pd.DataFrame):
        s = s.iloc[:, 0]
    s = s[s.index <= DATA_END].dropna()
    s.name = ticker
    return s


vix = download_close("^VIX", "1990-01-01")
sp500 = download_close("^GSPC", "1990-01-01")
spy = download_close("SPY", "1993-01-01")

# 對齊交易日
common = vix.index.intersection(sp500.index)
vix = vix.reindex(common)
sp500 = sp500.reindex(common)


# ---------- 訊號 ----------
def dedupe(raw_condition: pd.Series, cooldown: int = COOLDOWN) -> pd.Series:
    """只取「條件首次成立」：前 cooldown 個交易日內不得有原始條件成立。"""
    prior_hits = raw_condition.shift(1).rolling(cooldown, min_periods=1).sum()
    return raw_condition & (prior_hits.fillna(0) == 0)


# 訊號 A：VIX 收盤首次突破 40
panic = vix > 40
signal_a = dedupe(panic)

# 訊號 B：近 120 個交易日曾出現 VIX>40，且當日收盤回落到 30 以下（降溫進場）
cooled = (panic.rolling(120, min_periods=1).sum() > 0) & (vix < 30)
signal_b = dedupe(cooled)


# ---------- 事件研究：每次訊號後的前瞻報酬 ----------
def forward_return(price: pd.Series, horizon: int) -> pd.Series:
    return price.shift(-horizon) / price - 1


def event_study(signal: pd.Series, name: str) -> pd.DataFrame:
    rows = []
    for day in signal[signal].index:
        segment = sp500.loc[day:].head(HOLD + 1)
        rows.append({
            "進場日": day.date(),
            "VIX 收盤": round(float(vix.loc[day]), 2),
            "250 日報酬 %": round(float(forward_return(sp500, HOLD).loc[day]) * 100, 1),
            "期間最深跌幅 %": round(float(segment.min() / segment.iloc[0] - 1) * 100, 1),
        })
    table = pd.DataFrame(rows)
    print(f"\n=== {name}（{len(table)} 次）===")
    print(table.to_string(index=False))
    return table


events_a = event_study(signal_a, "訊號 A：VIX 突破 40 即買")
events_b = event_study(signal_b, "訊號 B：降溫至 30 以下再買")

# 無條件基準：任意一天買進持有 250 個交易日
baseline = forward_return(sp500, HOLD).dropna()
print(f"\n任意時點買進持有 250 日：平均 {baseline.mean() * 100:.1f}%，"
      f"勝率 {(baseline > 0).mean() * 100:.0f}%")


# ---------- 敏感度矩陣：門檻 × 持有期 ----------
print("\n=== 敏感度矩陣（平均前瞻報酬 % / 勝率 % / 樣本數）===")
for threshold in (30, 40, 50):
    sig = dedupe(vix > threshold)
    for horizon in (60, 125, 250):
        returns = forward_return(sp500, horizon)[sig].dropna()
        if len(returns) == 0:
            continue
        print(f"VIX>{threshold} 持有 {horizon} 日: "
              f"平均 {returns.mean() * 100:5.1f}% / "
              f"勝率 {(returns > 0).mean() * 100:3.0f}% / n={len(returns)}")


# ---------- 連續部位版：訊號 B 觸發後持有 SPY 250 個交易日 ----------
signal_on_spy = signal_b.reindex(spy.index).fillna(False)
position = pd.Series(0.0, index=spy.index)
hold_until = -1
for i in range(len(spy)):
    if signal_on_spy.iloc[i]:
        hold_until = max(hold_until, i + HOLD)
    if i < hold_until:
        position.iloc[i] = 1.0

spy_returns = spy.pct_change().fillna(0)
strategy_returns = spy_returns * position.shift(1).fillna(0)
trades = position.diff().abs().fillna(position)
strategy_returns = strategy_returns - trades * COST_PER_SIDE


def report(returns: pd.Series, label: str) -> None:
    equity = (1 + returns).cumprod()
    years = (equity.index[-1] - equity.index[0]).days / 365.25
    cagr = float(equity.iloc[-1]) ** (1 / years) - 1
    sharpe = returns.mean() / returns.std() * np.sqrt(252)
    mdd = float((equity / equity.cummax() - 1).min())
    print(f"{label}: CAGR {cagr * 100:.2f}% / 日夏普 {sharpe:.2f} / 最大回撤 {mdd * 100:.1f}%")


print("\n=== 連續部位版 vs SPY 買進持有（1993 起，SPY 含股息）===")
report(strategy_returns, "訊號 B 連續部位版")
report(spy_returns, "SPY 買進持有　　　")
print(f"持倉時間佔比：{position.mean() * 100:.1f}%")
