# Setup QuantBookfrom AlgorithmImports import*import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport warningswarnings.filterwarnings('ignore')plt.style.use('seaborn-v0_8-darkgrid')plt.rcParams['figure.figsize'] = (14, 6)qb = QuantBook()print("QuantBook initialise.")
QuantBook initialise.
1. Chargement des données
SVXY (short vol -0.5x), VIX, VIX3M, SHY (cash alternative). Note: SVXY avant Feb 2018 etait -1x, post-VIXplosion = -0.5x.
# ETFsetf_tickers = ['SVXY', 'SHY', 'SPY']symbols = {}for ticker in etf_tickers: symbols[ticker] = qb.add_equity(ticker, Resolution.DAILY).symbol# VIX datavix_symbol = qb.add_data(CBOE, 'VIX', Resolution.DAILY).symbolvix3m_symbol = qb.add_data(CBOE, 'VIX3M', Resolution.DAILY).symbolstart = datetime(2012, 1, 1)end = datetime(2026, 1, 1)# ETF historyetf_history = qb.history(list(symbols.values()), start, end, Resolution.DAILY)closes = etf_history['close'].unstack(level=0)stm = {str(v): k for k, v in symbols.items()}closes.columns = [stm.get(str(c), str(c)) for c in closes.columns]# VIX historyvix_hist = qb.history(vix_symbol, start, end, Resolution.DAILY)vix3m_hist = qb.history(vix3m_symbol, start, end, Resolution.DAILY)vix =Nonevix3m =Noneifnot vix_hist.empty andnot vix3m_hist.empty: vix = vix_hist['close'].droplevel(0) vix3m = vix3m_hist['close'].droplevel(0) vix.name ='VIX' vix3m.name ='VIX3M'# Align all data all_data = pd.concat([closes, vix, vix3m], axis=1).dropna() closes = all_data[etf_tickers] vix = all_data['VIX'] vix3m = all_data['VIX3M']print(f"Periode: {all_data.index[0].date()} a {all_data.index[-1].date()}")print(f"Donnees: {len(all_data)} jours")print(f"VIX range: [{vix.min():.1f}, {vix.max():.1f}]")else:# CBOE custom-data type needs QC Cloud alternative-data infra (empty in local# Docker research). VIX/VIX3M are CBOE's PUBLIC 30-/93-day implied-vol indices;# the identical series is available via yfinance (^VIX / ^VIX3M), provisioned as# local CSVs -- regenerable via scripts/quantconnect/provision_vix_csv.py (the# CSVs are gitignored like the LEAN data folder, NOT committed; same data, local# pipe -- consistent with how sibling quantbooks use local LEAN equity/crypto# data instead of QC Cloud).import osdef _find(name):for p in ['/Lean/Data/'+ name, name, '../data/'+ name, '../../data/'+ name]:if os.path.exists(p):return praiseFileNotFoundError(name +' not found beside notebook or in LEAN data folder') vix = pd.read_csv(_find('vix_daily.csv'), parse_dates=['date'], index_col='date')['close'] vix3m = pd.read_csv(_find('vix3m_daily.csv'), parse_dates=['date'], index_col='date')['close'] vix = vix.loc[start:end]; vix3m = vix3m.loc[start:end] vix.name ='VIX'; vix3m.name ='VIX3M'# Normalize all indices to naive date-only: closes comes from Lean qb.history# (Timestamps with a time/tz component), vix/vix3m from CSV (midnight naive).# Without this, concat axis=1 matches ZERO timestamps -> dropna -> empty.def _norm(s): idx = pd.to_datetime(s.index)ifgetattr(idx, 'tz', None) isnotNone: idx = idx.tz_convert('UTC').tz_localize(None)return idx.normalize() closes.index = _norm(closes); vix.index = _norm(vix); vix3m.index = _norm(vix3m) all_data = pd.concat([closes, vix, vix3m], axis=1).dropna() closes = all_data[etf_tickers] vix = all_data['VIX']; vix3m = all_data['VIX3M']print("[local VIX data] CBOE custom type unavailable locally (needs QC Cloud);")print(" loaded genuine VIX/VIX3M series from local CSV (yfinance ^VIX/^VIX3M = CBOE public indices).")print(f"Periode: {all_data.index[0].date()} a {all_data.index[-1].date()}")print(f"Donnees: {len(all_data)} jours")print(f"VIX range: [{vix.min():.1f}, {vix.max():.1f}]")
[local VIX data] CBOE custom type unavailable locally (needs QC Cloud);
loaded genuine VIX/VIX3M series from local CSV (yfinance ^VIX/^VIX3M = CBOE public indices).
Periode: 2012-01-03 a 2025-12-31
Donnees: 3520 jours
VIX range: [9.1, 82.7]
2. Analyse du term structure
Le ratio VIX3M/VIX mesure la pente de la courbe de volatilite. - Ratio > 1 = contango (normal, premium a harvester) - Ratio < 1 = backwardation (stress, danger)
if vix isNoneor vix3m isNone:print("VIX data not available — skipping term structure analysis.") ratio =Noneelse: ratio = vix3m / vixprint(f"VIX3M/VIX ratio stats:")print(f" Mean: {ratio.mean():.3f}")print(f" Std: {ratio.std():.3f}")print(f" Contango (>1.05): {(ratio >1.05).mean():.0%} du temps")print(f" Backwardation (<1.0): {(ratio <1.0).mean():.0%} du temps")print(f" Deep backwardation (<0.9): {(ratio <0.9).mean():.0%} du temps")# VIX distributionprint(f"\nVIX distribution:")for threshold in [15, 18, 22, 25, 30]: pct = (vix < threshold).mean()print(f" VIX < {threshold}: {pct:.0%}")
VIX3M/VIX ratio stats:
Mean: 1.130
Std: 0.088
Contango (>1.05): 83% du temps
Backwardation (<1.0): 7% du temps
Deep backwardation (<0.9): 1% du temps
VIX distribution:
VIX < 15: 41%
VIX < 18: 65%
VIX < 22: 82%
VIX < 25: 89%
VIX < 30: 96%
results_pos = {}if vix isNone:print("VIX data not available — skipping position size analysis.")else:print(f"{'Position':<15}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}{'Trades':>8}")print("-"*50)for pos in [0.20, 0.30, 0.45, 0.60]: r = backtest_vix_strategy(closes, vix, vix3m, position_size=pos) name =f'{pos:.0%}' results_pos[name] = rprint(f"{name:<15}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}{r['n_trades']:>7}")
Plus la position est petite, moins la MaxDD. Mais le CAGR baisse aussi. Post-VIXplosion (SVXY -0.5x), le premium est halve -> position plus petite = plus safe.
5. Hypothese 2: VIX threshold
results_vix = {}if vix isNone:print("VIX data not available — skipping VIX threshold analysis.")else:print(f"{'VIX Threshold':<15}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}")print("-"*40)for vt in [18, 22, 25, 30]: r = backtest_vix_strategy(closes, vix, vix3m, vix_threshold=vt) name =f'VIX<{vt}' results_vix[name] = rprint(f"{name:<15}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}")
VIX < 22 est le seuil actuel. VIX < 18 trop restrictif (trop peu d’entrees). VIX < 25-30 : plus d’entrees mais dans des periodes plus risquees.
6. Hypothese 3: Contango depth
results_ratio = {}if vix isNone:print("VIX data not available — skipping contango depth analysis.")else:print(f"{'Ratio Entry':<15}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}")print("-"*40)for re in [1.03, 1.05, 1.08, 1.10]: r = backtest_vix_strategy(closes, vix, vix3m, ratio_entry=re) name =f'Ratio>{re:.2f}' results_ratio[name] = rprint(f"{name:<15}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}")
Partiellement : 60% maximise le Sharpe mais 30% offre un meilleur ratio Sharpe/MaxDD
H2 VIX threshold
VIX<30 Sharpe 0.136 > VIX<22 Sharpe 0.088
Non : seuil plus large (VIX<30) perf mieux sur 2012-2026
H3 Contango depth
Ratio>1.03 Sharpe 0.142 > Ratio>1.05 Sharpe 0.088
Non : seuil plus permissif (1.03) capture plus d’entrees
H4 Trailing stop
10% Sharpe 0.088 ; 7% Sharpe 0.042
Partiellement : 10% legerement meilleur
H5 SHY cash
SHY Sharpe 0.086 vs Idle 0.088 (~equivalent)
Non : SHY n’ajoute pas de CAGR net sur 2012-2026
Plafond structurel
Post-VIXplosion 2018, SVXY est passe de -1x a -0.5x. Le premium est halve. MaxDD 35% est structural (tail events VIX). Sharpe 0.051 reflete le fait que le risk-free rate (~3-5%) mange presque tout le CAGR.