Analyser la stratégie RegimeSwitching qui adapte son comportement selon le régime de marché: - Bull market: Momentum (SPY 70%, QQQ 30%) - Bear market: Défensif (GLD 50%, IEF 50%) - Sideways: Mean-reversion sur RSI oversold ou allocation réduite
Détection des régimes
Bull: Prix > SMA200 ET SMA50 > SMA200
Bear: Prix < SMA200 ET SMA50 < SMA200
Sideways: Autres cas
Performance de référence
Sharpe 0.553 (2008-2026) - robustesse grâce à l’adaptation aux régimes.
Hypothèses à tester
Impact des seuils RSI pour mean-reversion (20/30/40)
Allocation bull market (70/30 vs 80/20 vs 60/40)
Defensive assets: IEF vs TLT
Prérequis
Environnement Lean Research
Durée estimée: ~10 minutes
# 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, 5)qb = QuantBook()print("QuantBook initialisé.")
QuantBook initialisé.
1. Chargement des données
On charge les 4 assets de l’univers RegimeSwitching: SPY, QQQ, IEF, GLD.
Pivot de la série ‘close’ en DataFrame large, avec remapping des colonnes Symbol → ticker pour RegimeSwitching.
# Pivoter les donnéescloses = history['close'].unstack(level=0)symbol_to_ticker = {str(v): k for k, v in symbols.items()}closes.columns = [symbol_to_ticker.get(str(c), str(c)) for c in closes.columns]closes = closes.dropna()print(f"Période: {closes.index[0].date()} à {closes.index[-1].date()}")print(f"Données: {len(closes)} jours de trading")print(f"Tickers: {list(closes.columns)}")
Période: 2011-03-23 à 2025-12-31
Données: 3717 jours de trading
Tickers: ['QQQ', 'SPY']
2. Détection des régimes de marché
def detect_regime(closes, spy_ticker="SPY"):"""Détecte le régime de marché basé sur SMA50/SMA200."""if spy_ticker notin closes.columns:return pd.Series(index=closes.index, data='unknown') prices = closes[spy_ticker] sma50 = prices.rolling(50).mean() sma200 = prices.rolling(200).mean() regimes = []for i inrange(len(closes)):if pd.isna(sma50.iloc[i]) or pd.isna(sma200.iloc[i]): regimes.append('unknown')continue price = prices.iloc[i] s50 = sma50.iloc[i] s200 = sma200.iloc[i]if price > s200 and s50 > s200: regimes.append('bull')elif price < s200 and s50 < s200: regimes.append('bear')else: regimes.append('sideways')return pd.Series(regimes, index=closes.index)# Détecter les régimesregimes = detect_regime(closes)print("Régimes de marché (derniers 10 jours):")print(regimes.iloc[-10:])print(f"\nDistribution des régimes:")print(regimes.value_counts())
Bull: Trend haussier confirmé (prix et SMA50 au-dessus de SMA200)
Bear: Trend baissier confirmé (prix et SMA50 en-dessous de SMA200)
Sideways: Transition ou indécision
La stratégie adapte son comportement selon le régime pour optimiser le ratio risque/rendement.
3. Calcul du RSI pour mean-reversion
def compute_rsi(prices, period=14):"""Calcule le RSI.""" delta = prices.diff() gain = (delta.where(delta >0, 0)).rolling(window=period).mean() loss = (-delta.where(delta <0, 0)).rolling(window=period).mean() rs = gain / loss rsi =100- (100/ (1+ rs))return rsi# Calculer RSI pour SPY et QQQrsi_spy = compute_rsi(closes['SPY'])rsi_qqq = compute_rsi(closes['QQQ'])print("RSI SPY (derniers 5 jours):")print(rsi_spy.iloc[-5:])print(f"\nRSI actuel: SPY={rsi_spy.iloc[-1]:.1f}, QQQ={rsi_qqq.iloc[-1]:.1f}")
RSI SPY (derniers 5 jours):
time
2025-12-24 13:00:00 NaN
2025-12-26 16:00:00 NaN
2025-12-29 16:00:00 NaN
2025-12-30 16:00:00 NaN
2025-12-31 16:00:00 NaN
Name: SPY, dtype: float64
RSI actuel: SPY=nan, QQQ=nan
4. Génération des signaux RegimeSwitching
def compute_regime_signals(closes, regimes, rsi_spy, rsi_qqq, tickers, oversold_threshold=30, bull_alloc={"SPY": 0.7, "QQQ": 0.3}):"""Génère les signaux RegimeSwitching.""" signals = pd.DataFrame(index=closes.index, columns=tickers)for i inrange(252, len(closes)): regime = regimes.iloc[i]if regime =='bull':# Momentum: allocation bullfor t in tickers: signals[t].iloc[i] = bull_alloc.get(t, 0)elif regime =='bear':# Défensif: GLD 50%, IEF 50%for t in tickers:if t =="GLD": signals[t].iloc[i] =0.5elif t =="IEF": signals[t].iloc[i] =0.5else: signals[t].iloc[i] =0else: # sideways# Check RSI oversold pour mean-reversion oversold = []if i <len(rsi_spy) and pd.notna(rsi_spy.iloc[i]):if rsi_spy.iloc[i] < oversold_threshold: oversold.append("SPY")if i <len(rsi_qqq) and pd.notna(rsi_qqq.iloc[i]):if rsi_qqq.iloc[i] < oversold_threshold: oversold.append("QQQ")if oversold:# Mean-reversion: oversold 30%, GLD 35%, IEF 35%for t in tickers:if t in oversold: signals[t].iloc[i] =0.30/len(oversold)elif t =="GLD": signals[t].iloc[i] =0.35elif t =="IEF": signals[t].iloc[i] =0.35else: signals[t].iloc[i] =0else:# Allocation réduite: SPY 30%, GLD 35%, IEF 35%for t in tickers:if t =="SPY": signals[t].iloc[i] =0.30elif t =="GLD": signals[t].iloc[i] =0.35elif t =="IEF": signals[t].iloc[i] =0.35else: signals[t].iloc[i] =0return signals# Signaux avec paramètres par défautsignals = compute_regime_signals(closes, regimes, rsi_spy, rsi_qqq, tickers)print("Signaux RegimeSwitching (derniers 5 jours):")print(signals.iloc[-5:])print(f"\nAllocation actuelle:")last_signals = signals.iloc[-1]for t in tickers:if last_signals[t] >0:print(f" {t}: {last_signals[t]*100:.1f}%")
# Distribution des régimesregime_counts = regimes.value_counts()regime_pct = regimes.value_counts(normalize=True) *100print("Distribution des régimes (2008-2026):")print(f"{'Régime':<12}{'Jours':>10}{'Pourcentage':>12}")print("-"*35)for regime in ['bull', 'bear', 'sideways']: count = regime_counts.get(regime, 0) pct = regime_pct.get(regime, 0)print(f"{regime:<12}{count:>10}{pct:>11.1f}%")# Performance par régimewarmup =252regime_returns = {regime: [] for regime in ['bull', 'bear', 'sideways']}returns_df = closes.pct_change()for i inrange(warmup, len(closes)): regime = regimes.iloc[i]if regime in regime_returns:# Return du portefeuille à ce moment port_return =0for t in tickers: weight = signals[t].iloc[i]if pd.notna(weight) and weight >0and t in returns_df.columns: port_return += weight * returns_df[t].iloc[i] regime_returns[regime].append(port_return)print(f"\nPerformance moyenne par régime (annualisée):")for regime in ['bull', 'bear', 'sideways']:if regime_returns[regime]: avg_return = np.mean(regime_returns[regime]) *252print(f" {regime}: {avg_return:.1%}")
Distribution des régimes (2008-2026):
Régime Jours Pourcentage
-----------------------------------
bull 2156 58.0%
bear 150 4.0%
sideways 1212 32.6%
Performance moyenne par régime (annualisée):
bull: 18.7%
bear: 0.0%
sideways: -2.1%