# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Implementation complete de RegimeSwitching pour QuantConnect
qc_regime_switching_code = '''
from AlgorithmImports import *
class RegimeSwitching(QCAlgorithm):
"""
Regime-Switching Strategy (Mean-Reversion <-> Momentum)
========================================================
Edge: Apply momentum in bull markets, mean-reversion in bear/sideways
Universe: SPY, QQQ, IEF, GLD
Rebalancing: Monthly + regime-change trigger
Risk: Trailing stop-loss -10% on equity positions
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# Universe
self.risky = ["SPY", "QQQ"]
self.defensive = ["IEF", "GLD"]
self.all_tickers = self.risky + self.defensive
self.symbols = {}
for ticker in self.all_tickers:
equity = self.AddEquity(ticker, Resolution.Daily)
equity.SetDataNormalizationMode(DataNormalizationMode.ADJUSTED)
self.symbols[ticker] = equity.Symbol
# Regime detection (on SPY as market proxy)
spy_symbol = self.symbols["SPY"]
self.sma50 = self.SMA(spy_symbol, 50, Resolution.Daily)
self.sma200 = self.SMA(spy_symbol, 200, Resolution.Daily)
# RSI for mean-reversion signals
self.rsi_indicators = {}
for ticker in self.risky:
indicator = self.RSI(ticker, 14, MovingAverageType.WILDERS)
self.RegisterIndicator(self.symbols[ticker], indicator, Resolution.Daily)
self.rsi_indicators[ticker] = indicator
# Parameters
self.rsi_oversold = 30
self.rsi_exit = 50
self.stop_loss_pct = -0.10 # Trailing stop-loss -10%
# State
self.current_regime = None
self.last_rebalance = self.StartDate
self.equity_high_water = {} # For trailing stop
# Monthly rebalance
self.Schedule.On(
self.DateRules.MonthStart("SPY", 1),
self.TimeRules.AfterMarketOpen("SPY", 30),
self.MonthlyRebalance
)
# Daily regime check (triggers rebalance on change)
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.AfterMarketOpen("SPY", 31),
self.CheckRegimeChange
)
# Daily stop-loss check
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.AfterMarketOpen("SPY", 45),
self.CheckStopLoss
)
self.SetWarmUp(200, Resolution.Daily)
def DetectRegime(self):
"""Detect market regime using SMA50/SMA200."""
if not self.sma50.IsReady or not self.sma200.IsReady:
return "unknown"
spy_price = self.Securities[self.symbols["SPY"]].Price
sma50_val = self.sma50.Current.Value
sma200_val = self.sma200.Current.Value
if spy_price > sma200_val and sma50_val > sma200_val:
return "bull"
elif spy_price < sma200_val and sma50_val < sma200_val:
return "bear"
else:
return "sideways"
def CheckRegimeChange(self):
"""Check if regime changed and trigger rebalance."""
if self.IsWarmingUp:
return
regime = self.DetectRegime()
if regime == "unknown":
return
# Only rebalance on regime CHANGE
if regime != self.current_regime and self.current_regime is not None:
self.Debug(f"Regime change: {self.current_regime} -> {regime}")
self.current_regime = regime
self.ExecuteStrategy(regime)
if self.current_regime is None:
self.current_regime = regime
def MonthlyRebalance(self):
"""Monthly rebalancing."""
if self.IsWarmingUp:
return
regime = self.DetectRegime()
if regime == "unknown":
return
self.current_regime = regime
self.ExecuteStrategy(regime)
def ExecuteStrategy(self, regime):
"""Execute strategy based on regime."""
if regime == "bull":
self.ApplyMomentumStrategy()
else:
self.ApplyMeanReversionStrategy(regime)
# Reset high-water marks
for ticker in self.risky:
sym = self.symbols[ticker]
if self.Portfolio[sym].Invested:
self.equity_high_water[ticker] = self.Securities[sym].Price
def ApplyMomentumStrategy(self):
"""Momentum strategy for bull markets."""
# Risk-adjusted momentum: return / volatility
risk_adj_mom = {}
for ticker in self.risky:
history = self.History(self.symbols[ticker], 63, Resolution.Daily)
if history.empty:
continue
prices = history["close"]
if len(prices) < 63:
continue
raw_return = prices.iloc[-1] / prices.iloc[-63] - 1
daily_returns = prices.pct_change().dropna()
vol = daily_returns.std() * np.sqrt(252)
if vol > 0.01:
risk_adj_mom[ticker] = raw_return / vol
else:
risk_adj_mom[ticker] = raw_return
if not risk_adj_mom:
return
best = max(risk_adj_mom, key=risk_adj_mom.get)
other = [t for t in self.risky if t != best]
# Liquidate defensive
for ticker in self.defensive:
self.Liquidate(self.symbols[ticker])
self.SetHoldings(self.symbols[best], 0.70)
if other:
self.SetHoldings(self.symbols[other[0]], 0.30)
def ApplyMeanReversionStrategy(self, regime):
"""Mean-reversion strategy for bear/sideways."""
oversold = []
for ticker in self.risky:
if ticker in self.rsi_indicators and self.rsi_indicators[ticker].IsReady:
rsi_val = self.rsi_indicators[ticker].Current.Value
if rsi_val < self.rsi_oversold:
oversold.append(ticker)
if oversold:
weight_per_asset = 0.30 / len(oversold)
for ticker in oversold:
self.SetHoldings(self.symbols[ticker], weight_per_asset)
self.SetHoldings(self.symbols["GLD"], 0.35)
self.SetHoldings(self.symbols["IEF"], 0.35)
else:
# Exit if RSI recovered
for ticker in self.risky:
if ticker in self.rsi_indicators and self.rsi_indicators[ticker].IsReady:
if self.rsi_indicators[ticker].Current.Value > self.rsi_exit:
self.Liquidate(self.symbols[ticker])
if regime == "bear":
self.SetHoldings(self.symbols["GLD"], 0.50)
self.SetHoldings(self.symbols["IEF"], 0.50)
else: # sideways
self.SetHoldings(self.symbols["GLD"], 0.35)
self.SetHoldings(self.symbols["IEF"], 0.35)
self.SetHoldings(self.symbols["SPY"], 0.30)
def CheckStopLoss(self):
"""Check trailing stop-loss."""
if self.IsWarmingUp:
return
for ticker in self.risky:
sym = self.symbols[ticker]
if not self.Portfolio[sym].Invested:
continue
current_price = self.Securities[sym].Price
# Update high-water mark
if ticker not in self.equity_high_water:
self.equity_high_water[ticker] = current_price
else:
self.equity_high_water[ticker] = max(
self.equity_high_water[ticker], current_price
)
# Check trailing stop
drawdown = (current_price / self.equity_high_water[ticker]) - 1
if drawdown < self.stop_loss_pct:
self.Liquidate(sym)
self.Debug(f"STOP-LOSS {ticker}: {drawdown:.2%} from high")
del self.equity_high_water[ticker]
def OnData(self, data):
pass
'''
print("RegimeSwitching - Code QuantConnect")
print("="*60)
print("\\nFonctionnalites implementees :")
print(" - Detection SMA50/SMA200 des regimes")
print(" - Momentum en bull market (SPY/QQQ)")
print(" - Mean-reversion en sideways (RSI oversold)")
print(" - Allocation defensive en bear (GLD/IEF)")
print(" - Stop-loss trailing -10%")
print(" - Rebalancement mensuel + trigger regime")
print("\\nCopiez ce code dans main.py de votre projet QC Lab.")<< Sommaire QC | Précédent : QC-Py-27-Production-Deployment << | Suivant : QC-Py-28b-Macro-Cycle-Regimes >>
QC-Py-28 - Market Regime Detection
[QC CLOUD] Ce notebook utilise QuantBook / AlgorithmImports et necessite un environnement QuantConnect Cloud. Les cellules de code ne sont pas executables localement.
Navigation : Index | << Précédent | Suivant >>
Detection et anticipation des regimes de marche pour stratégies adaptatives Duree: 60 minutes | Niveau: Intermediaire-Avance | Python + QuantConnect
Objectifs d’Apprentissage
A la fin de ce notebook, vous serez capable de :
- Comprendre le concept de regimes de marche (bull/bear/sideways)
- Implementer des indicateurs de detection (SMA crossovers, trend filters)
- Utiliser le RSI pour mean-reversion dans les regimes defensifs
- Construire une stratégie adaptative qui change selon le regime
- Backtester et optimiser les seuils de detection
- Analyser la performance par regime pour identifier les forces/faiblesses
Prerequis
- Notebooks QC-Py-01 a 12 completes (fondations + indicateurs)
- Comprehension des moyennes mobiles (SMA/EMA)
- Notions de base sur RSI et indicateurs de momentum
- Compte QuantConnect (free tier suffisant)
Structure du Notebook
| Partie | Sujet | Duree |
|---|---|---|
| 1 | Introduction aux Regimes de Marche | 8 min |
| 2 | Detection SMA: Trend Following | 12 min |
| 3 | Oscillateurs: RSI et Mean-Reversion | 10 min |
| 4 | Strategy Adaptative: RegimeSwitching | 15 min |
| 5 | Optimisation des Seuils | 10 min |
| 6 | Analyse par Regime | 5 min |
Note de conception : Ce notebook contient du code de reference a copier dans QuantConnect Lab (main.py). Les cellules ne sont pas prevues pour etre executees en tant que notebook Jupyter. L’absence d’outputs (execution_count: null) est intentionnelle.
[REFERENCE QC Cloud] Ce notebook illustre du code QuantConnect a executer dans l’IDE Cloud (https://www.quantconnect.com/research). L’environnement local ne dispose pas de QuantBook ni de l’historical data feed. Pour executer : cloner le projet QC associe, ouvrir
research.ipynb, executer cellule par cellule.
Ce que QuantBook apporte ici : la partie locale de ce notebook tourne sur des donnees synthetiques (sans frais ni slippage) — parfaite pour comprendre la mecanique, insuffisante pour valider une strategie. Le passage en QC Cloud donne acces a l’historique reel (SPY, QQQ, GLD, IEF) avec frais et slippage simules ; le backtest
RegimeSwitchingrappele en tete de notebook en est le resultat. La double approche — local pour la pedagogie, Cloud pour la validation — est le motif de ce notebook a deux parties.
Conclusion : Resume et Prochaines Étapes
Points cles retenus
| Concept | Description |
|---|---|
| Regimes de marche | Bull (trend haussier), Bear (trend baissier), Sideways (range) |
| Detection SMA | Utiliser SMA50/SMA200 pour identifier le regime |
| RSI (Wilder, 1978) | Oscillateur pour mean-reversion (oversold < 30) |
| Stratégie adaptative | Changer d’allocation selon le regime |
| Optimisation | Ajuster les seuils pour maximiser le Sharpe |
| Analyse par regime | Comprendre les forces/faiblesses |
Tableau recapitulatif
| Metrique | Valeur (exemple) | Signification |
|---|---|---|
| Detection SMA | Bull/bear/sideways | Identification du regime |
| Seuil RSI optimal | 30 (range: 20-40) | Mean-reversion trigger |
| Allocation bull | SPY 70% / QQQ 30% | Momentum agressif |
| Allocation bear | GLD 50% / IEF 50% | Protection du capital |
| Allocation sideways | GLD 35% / IEF 35% / SPY 30% | Mixte avec mean-reversion |
Forces de la stratégie
- Adaptativite : S’ajuste automatiquement au regime
- Protection : Allocation defensive en bear market
- Diversification : 4 assets non correles (SPY, QQQ, GLD, IEF)
- Risque contrôle : Stop-loss trailing et positions reduites
Limitations et risques
- Lag de detection : Les SMA ont un delai de reaction
- Whipsaws : Transitions frequentes entre regimes
- Mean-reversion risque : Peut perdre si le trend continue contre nous
- Dépendance aux paramètres : Performance sensible aux seuils choisis
Prochaines étapes
- Backtester sur QuantConnect : Tester la stratégie sur différentes periodes
- Paper trading : Valider la performance en temps reel (2 semaines minimum)
- Optimisation avancee : Walk-forward optimization (Pardo, 2008), test de robustesse
- Combinaison : Integrer RegimeSwitching dans un composite avec d’autres stratégies
- Monitoring : Mettre en place des alertes sur les changements de regime
Ressources complementaires
- Projet QuantConnect :
projects/RegimeSwitching/main.py - Notebook QC-Py-13 : Algorithm Framework - Alpha Models
- Notebook QC-Py-15 : Algorithm Framework - Portfolio Construction
- Documentation : QuantConnect Docs
References
- Sharpe, W. F. (1966). Mutually Exclusive Criteria for Portfolio Sélection. Journal of Business. Sharpe ratio revisite dans Sharpe (1994), The Journal of Portfolio Management.
- Sortino, F. A. & Price, L. N. (1994). Performance Measurement in a Downside Risk Framework. Journal of Investing. Sortino ratio (penalise uniquement la volatilite baissiere).
- Wilder, J. W. (1978). New Concepts in Technical Trading Systems. Trend Research. RSI (Relative Strength Index).
- Murphy, J. J. (1999). Technical Analysis of the Financial Markets. New York Institute of Finance. SMA, Golden Cross / Death Cross.
- Pardo, R. (2008). The Evaluation and Optimization of Trading Stratégies (2nd ed.). Wiley. Walk-forward optimization.
- Ang, A. & Bekaert, G. (2002). International Asset Allocation With Regime Shifts. Review of Financial Studies. Regime-switching applique aux marches (les Hidden Markov Models de Baum & Petrie 1966 / Rabiner 1989 sont l’extension naturelle mentionnee en exercice).
- Hamilton, J. D. (1989). A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle. Econometrica, 57(2), 357-384. Modèle Markov a changements de regime – originateur canonique du regime-switching en econometrie, fondement théorique de la stratégie RegimeSwitching.
Felicitations ! Vous avez appris a detecter les regimes de marche et a construire une stratégie adaptative. Cette approche peut etre appliquee a de nombreuses autres stratégies pour les rendre plus robustes.
Résultat du backtest QC Cloud -
RegimeSwitching(2015-2024, $100k initial)
Metrique Valeur Dates negociables 2516 Sharpe Ratio 0.522 CAGR 11.445% Max Drawdown 26.900% Net Profit +195.5% Equity finale $295,533 Probabilistic Sharpe Ratio 11.380% Performance 2015-2024 : La stratégie regime-switching (momentum en bull, mean-reversion + defensif en bear/sideways, trailing stop -10%) genere un Sharpe de 0.522 et un CAGR de 11.4% sur 10 ans. Le MaxDD de 26.9% reflete le lag de detection SMA50/200 (whipsaws aux transitions de regime), et le PSR de 11.4% indique une confiance modeste dans l’edge.
Backtest QC Cloud 2015-2024 (projet 33177683, bt 97a99ceb9a70b6a18e27b3408c24029c). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).
Partie 7 : Implementation QuantConnect
Code de reference pour QC Lab
Voici le code complet a copier dans main.py de votre projet QuantConnect. Ce code implemente la stratégie RegimeSwitching avec toutes les fonctionnalites decrites dans ce notebook.
Pourquoi porter en QC Lab ? Les parties precedentes valident la strategie sur donnees synthetiques : aucun frais, aucun slippage, aucune contrainte de liquidite. Le code de reference ci-dessous transpose le detecteur SMA50/SMA200 et l’allocation par regime dans l’API QuantConnect : donnees reellement historiques, resolution quotidienne, rebalancement conditionne aux transitions de regime. C’est la que la strategie rencontre la realite — le backtest RegimeSwitching (2015-2024, 100 000 $ initial) rappele en tete de notebook en est le resultat, pas une extrapolation des donnees synthetiques.
# Setup : imports, donnees synthetiques et fonctions de base
# (consolide les pre-requis des Parties 1-6 pour l'execution sequentielle)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Optional
import warnings
warnings.filterwarnings('ignore')
plt.style.use('seaborn-v0_8-darkgrid')
plt.rcParams['figure.figsize'] = (14, 6)
plt.rcParams['font.size'] = 10
# --- Donnees synthetiques (Partie 1) ---
np.random.seed(42)
n_days = 1500
dates = pd.date_range(start='2015-01-01', periods=n_days, freq='D')
price = np.zeros(n_days)
price[0] = 100
for i in range(1, 500):
price[i] = price[i-1] * (1 + np.random.normal(0.0005, 0.01))
for i in range(500, 900):
price[i] = price[i-1] * (1 + np.random.normal(-0.001, 0.02))
for i in range(900, n_days):
price[i] = price[i-1] * (1 + np.random.normal(0, 0.012))
df = pd.DataFrame({'price': price}, index=dates)
# --- Detection SMA (Partie 2) ---
def detect_regime_sma(prices: pd.Series, fast_period: int = 50, slow_period: int = 200) -> pd.Series:
sma_fast = prices.rolling(window=fast_period).mean()
sma_slow = prices.rolling(window=slow_period).mean()
regimes = []
for i in range(len(prices)):
if pd.isna(sma_fast.iloc[i]) or pd.isna(sma_slow.iloc[i]):
regimes.append('unknown')
elif prices.iloc[i] > sma_slow.iloc[i] and sma_fast.iloc[i] > sma_slow.iloc[i]:
regimes.append('bull')
elif prices.iloc[i] < sma_slow.iloc[i] and sma_fast.iloc[i] < sma_slow.iloc[i]:
regimes.append('bear')
else:
regimes.append('sideways')
return pd.Series(regimes, index=prices.index)
regimes_sma = detect_regime_sma(df['price'])
# --- RSI (Partie 3) ---
def compute_rsi(prices: pd.Series, period: int = 14) -> pd.Series:
delta = prices.diff()
gains = delta.where(delta > 0, 0)
losses = -delta.where(delta < 0, 0)
avg_gains = gains.rolling(window=period).mean()
avg_losses = losses.rolling(window=period).mean()
rs = avg_gains / avg_losses
return 100 - (100 / (1 + rs))
rsi = compute_rsi(df['price'], period=14)
# --- RegimeSwitching signals (Partie 4) ---
def compute_regime_switching_signals(
prices_spy, prices_qqq, prices_gld, prices_ief,
rsi_spy, rsi_qqq, regimes,
rsi_oversold=30.0, rsi_exit=50.0, bull_spy_weight=0.70
):
signals = pd.DataFrame(index=prices_spy.index, columns=['SPY', 'QQQ', 'GLD', 'IEF'])
for i in range(200, len(prices_spy)):
regime = regimes.iloc[i]
if regime == 'bull':
signals.iloc[i] = [bull_spy_weight, 1.0 - bull_spy_weight, 0.0, 0.0]
elif regime == 'bear':
signals.iloc[i] = [0.0, 0.0, 0.50, 0.50]
else:
oversold = []
if i < len(rsi_spy) and pd.notna(rsi_spy.iloc[i]) and rsi_spy.iloc[i] < rsi_oversold:
oversold.append('SPY')
if i < len(rsi_qqq) and pd.notna(rsi_qqq.iloc[i]) and rsi_qqq.iloc[i] < rsi_oversold:
oversold.append('QQQ')
if oversold:
w = 0.30 / len(oversold)
vals = [0.0, 0.0, 0.35, 0.35]
for a in oversold:
vals[['SPY','QQQ','GLD','IEF'].index(a)] = w
signals.iloc[i] = vals
else:
signals.iloc[i] = [0.30, 0.0, 0.35, 0.35]
return signals
np.random.seed(42)
prices_all = pd.DataFrame(index=dates)
prices_all['SPY'] = df['price'].values
prices_all['QQQ'] = df['price'].values * np.random.uniform(0.95, 1.05, n_days).cumprod()
prices_all['GLD'] = 100 + np.random.normal(0, 0.5, n_days).cumsum()
prices_all['IEF'] = 100 + np.random.normal(0, 0.2, n_days).cumsum()
rsi_qqq = compute_rsi(prices_all['QQQ'])
signals = compute_regime_switching_signals(
prices_all['SPY'], prices_all['QQQ'], prices_all['GLD'], prices_all['IEF'],
rsi, rsi_qqq, regimes_sma
)
# --- Conversion float pour les visualisations (fix dtype object) ---
signals = signals.astype(float)
# --- Variables derivees pour les cellules de visualisation ---
# (Partie 6 - regime_counts et regime_returns)
regime_counts = regimes_sma.value_counts()
regime_pct = regimes_sma.value_counts(normalize=True) * 100
warmup = 200
returns_df = prices_all.pct_change().fillna(0)
regime_returns = {'bull': [], 'bear': [], 'sideways': []}
for i in range(warmup, len(prices_all)):
regime = regimes_sma.iloc[i]
if regime in regime_returns:
port_return = 0
sig = signals.iloc[i]
for asset in ['SPY', 'QQQ', 'GLD', 'IEF']:
if asset in returns_df.columns:
port_return += float(sig[asset]) * returns_df[asset].iloc[i]
regime_returns[regime].append(port_return)
# (Partie 5 - optimisation RSI)
def backtest_regime_switching(prices: pd.DataFrame, signals: pd.DataFrame, rebal_freq: int = 20) -> Dict:
returns = prices.pct_change().fillna(0)
warmup_bt = 200
portfolio_values = [1.0]
current_alloc = signals.iloc[warmup_bt].fillna(0)
counter = 0
for i in range(warmup_bt + 1, len(prices)):
counter += 1
if counter >= rebal_freq:
counter = 0
current_alloc = signals.iloc[i].fillna(0)
port_return = 0
for asset in ['SPY', 'QQQ', 'GLD', 'IEF']:
if asset in returns.columns:
port_return += current_alloc[asset] * returns[asset].iloc[i]
portfolio_values.append(portfolio_values[-1] * (1 + port_return))
returns_array = np.diff(portfolio_values) / np.array(portfolio_values[:-1])
returns_array = returns_array[~np.isnan(returns_array)]
if len(returns_array) == 0:
return {'sharpe': 0, 'cagr': 0, 'max_dd': 0}
total_return = portfolio_values[-1] / portfolio_values[0] - 1
years = len(returns_array) / 252
cagr = (1 + total_return) ** (1 / years) - 1 if years > 0 else 0
vol = np.std(returns_array) * np.sqrt(252) if len(returns_array) > 1 else 0
sharpe = (cagr - 0.02) / vol if vol > 0.001 else 0
cum_returns = pd.Series(portfolio_values)
running_max = cum_returns.expanding().max()
drawdown = (cum_returns - running_max) / running_max
max_dd = drawdown.min()
return {'sharpe': sharpe, 'cagr': cagr, 'max_dd': max_dd, 'vol': vol, 'final_value': portfolio_values[-1]}
results_rsi = {}
rsi_thresholds = [20, 25, 30, 35, 40]
for threshold in rsi_thresholds:
sig_opt = compute_regime_switching_signals(
prices_all['SPY'], prices_all['QQQ'], prices_all['GLD'], prices_all['IEF'],
rsi, rsi_qqq, regimes_sma, rsi_oversold=threshold
)
sig_opt = sig_opt.astype(float)
result = backtest_regime_switching(prices_all, sig_opt)
results_rsi[threshold] = result
best_rsi = max(results_rsi.items(), key=lambda x: x[1]['sharpe'])
# (Partie 2 - colonnes SMA)
df['sma50'] = df['price'].rolling(50).mean()
df['sma200'] = df['price'].rolling(200).mean()
print("Setup complet : {} jours, {} cellules pretes".format(n_days, len(df)))
print("Regimes : {}".format(regimes_sma.value_counts().to_dict()))
print("Meilleur seuil RSI: {} (Sharpe={:.3f})".format(best_rsi[0], best_rsi[1]['sharpe']))Setup complet : 1500 jours, 1500 cellules pretesRegimes : {'bull': 859, 'bear': 365, 'unknown': 199, 'sideways': 77}Meilleur seuil RSI: 20 (Sharpe=1.146)
Exercice 1 : Calcul du Sortino Ratio par regime
Le Sharpe Ratio penalise la volatilite haussiere et baissiere. Calculez le Sortino Ratio pour chaque regime, qui ne penalise que la volatilite baissiere.
Indices : - # Indice : Le Sortino Ratio = mean(returns) / std(returns[returns < 0]) * sqrt(252) - # Indice : Filtrez les returns negatifs avec returns[returns < 0] - # Étape 1 : Extraire les returns par regime - # Étape 2 : Calculer le downside deviation par regime - # Étape 3 : Calculer et afficher le Sortino Ratio par regime
Critère de validation : affichez le Sortino par régime et vérifiez l’ordre attendu : supérieur au Sharpe en bull (la volatilité baissière y est plus faible que la volatilité totale), et la hiérarchie entre régimes doit rester stable. Testez aussi la cohérence numérique : un régime sans retour négatif produit une division par zéro — documentez ce cas au lieu de planter.
# Exercice 3 : Sortino Ratio par regime
# TODO etudiant : Calculer le Sortino Ratio (downside deviation) pour chaque regime
# Etape 1 : Extraire les returns par regime
# Etape 2 : Calculer le downside deviation (std des returns negatifs)
# Etape 3 : Calculer Sortino = mean(return) / downside_dev * sqrt(252)
sortino_by_regime = None # TODO etudiant : remplacer par le calcul
print("Exercice a completer : Sortino Ratio par regime")Exercice a completer
Exercice 2 : Ajouter un regime CRISIS au detecteur
Le detecteur actuel utilise 3 regimes. Ajoutez un 4eme regime CRISIS declenche quand le drawdown depasse -15% ET la volatilite rolling est au-dessus de la moyenne + 2 ecarts-types.
Indices : - # Indice : Calculez le drawdown comme (price / cummax - 1) - # Indice : Calculez la volatilite rolling 20j annualisee - # Étape 1 : Calculer le drawdown quotidien - # Étape 2 : Calculer la volatilite rolling 20j - # Étape 3 : Ajouter la condition CRISIS a la classification
Critère de validation : injectez une séquence de forte baisse (par exemple -20 % en 10 jours) et vérifiez que le régime CRISIS est déclenché pendant la chute ET pendant le pic de volatilité qui suit. La conjonction ET (drawdown > 15 % ET vol > moyenne + 2 écarts-types) doit être testée dans les deux sens : une forte baisse sans pic de volatilité ne doit pas déclencher le régime.
# Exercice 2 : Detecteur de regime CRISIS
# TODO etudiant : Ajouter un regime CRISIS quand drawdown < -15% ET vol > mean + 2*std
# Etape 1 : Calculer le drawdown quotidien
# Etape 2 : Calculer la volatilite rolling 20j
# Etape 3 : Ajouter la condition CRISIS
regime_crisis = None # TODO etudiant : remplacer par la serie de regimes avec CRISIS
print("Exercice a completer : Detecteur de regime CRISIS")Exercice a completer
Exercice 3 : Calcul de la volatilite par regime
Calculez la volatilite annualisee des returns journaliers pour chaque regime (BULL, BEAR, NEUTRAL). Cela permet de verifier que la classification capture bien des regimes de volatilite distincts.
Indices : - # Indice : Groupez les returns par regime avec .groupby(regimes) puis calculez .std() * sqrt(252) - # Étape 1 : Calculer les returns journaliers - # Étape 2 : Grouper par regime et calculer l’ecart-type - # Étape 3 : Annualiser et afficher
Critère de validation : comparez les volatilités annualisées par régime entre elles — en bull elles doivent être inférieures ou égales à celles du bear, le générateur paramétrant des régimes de volatilité distincts. Vérifiez enfin la cohérence d’ensemble : la volatilité globale de la série est une moyenne pondérée des volatilités par régime uniquement si les corrélations entre régimes sont nulles — écartez ce point dans votre commentaire.
# Exercice 1 : Volatilite par regime
# TODO etudiant : Calculer la volatilite annualisee des returns par regime
# Etape 1 : Calculer les returns journaliers (pct_change)
# Etape 2 : Grouper par regime avec .groupby()
# Etape 3 : Annualiser (std * sqrt(252)) et afficher
vol_by_regime = None # TODO etudiant : remplacer par le calcul
print("Exercice a completer : Volatilite par regime")Exercice a completer
# Visualisation de la performance par regime
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 1. Distribution des regimes (pie chart)
ax = axes[0]
regime_labels = ['Bull', 'Bear', 'Sideways']
regime_values = [regime_counts.get(r.lower(), 0) for r in regime_labels]
colors_pie = ['green', 'red', 'gray']
wedges, texts, autotexts = ax.pie(regime_values, labels=regime_labels, autopct='%1.1f%%',
colors=colors_pie, startangle=90)
ax.set_title('Distribution des Regimes', fontweight='bold')
# 2. Performance par regime (bar chart)
ax = axes[1]
regime_perf = []
for regime in ['bull', 'bear', 'sideways']:
if regime_returns[regime]:
regime_perf.append(np.mean(regime_returns[regime]) * 252 * 100) # En %
else:
regime_perf.append(0)
bars = ax.bar(regime_labels, regime_perf, color=colors_pie, alpha=0.7)
ax.set_title('Performance Annualisee par Regime', fontweight='bold')
ax.set_ylabel('Return (%)')
ax.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax.grid(True, alpha=0.3)
# Ajouter les valeurs sur les barres
for bar, val in zip(bars, regime_perf):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height,
f'{val:.1f}%', ha='center', va='bottom' if height >= 0 else 'top')
plt.tight_layout()
plt.show()
print("\\nResume de l'analyse par regime :")
print(" - La strategie s'adapte automatiquement au regime")
print(" - Le momentum en bull capture les trends haussiers")
print(" - L'allocation defensive en bear protege le capital")
print(" - Le mean-reversion en sideways ajoute de l'alpha")\nResume de l'analyse par regime : - La strategie s'adapte automatiquement au regime - Le momentum en bull capture les trends haussiers - L'allocation defensive en bear protege le capital - Le mean-reversion en sideways ajoute de l'alpha

Interpretation : Analyse par Regime
Sortie obtenue : Les graphiques montrent la distribution et la performance par regime.
| Aspect | Observation |
|---|---|
| Distribution | Le marche passe différents temps dans chaque regime selon la periode |
| Performance bull | Positive, la stratégie capture les trends haussiers |
| Performance bear | Negative mais limitee grace a l’allocation defensive |
| Performance sideways | Variable, depend de la qualite des signaux RSI |
Points cles : 1. La stratégie protege le capital en bear market (allocation defensive) 2. Le mean-reversion en sideways est risquee mais peut ajouter de l’alpha 3. La performance globale depend de la distribution des regimes sur la periode
Note technique : Pour une analyse plus complete, on peut calculer la performance “regime-adjusted” : normaliser la performance par le temps passe dans chaque regime. Cela permet de comparer des stratégies sur différentes periodes.
Lecture chiffrée de la sortie : le tableau d’analyse par régime ventile 1 300 jours utiles après le warmup de 200 jours : le marché passe 57,3 % de son temps en bull (859 jours), 24,3 % en bear (365 jours) et seulement 5,1 % en sideways (77 jours) — les 199 jours restants ne sont pas classés. La performance moyenne annualisée révèle l’asymétrie : bull +42,2 % avec une volatilité de 24,3 %, bear +1,5 % avec seulement 3,7 % de volatilité (l’allocation défensive comprime le risque), et sideways -5,1 % : c’est le régime qui coûte. Cette hiérarchie justifie la stratégie : capturer les trends bull et limiter l’exposition bear est rentable, mais l’alpha de mean-reversion en sideways doit dépasser la référence de -5,1 % pour se justifier.
# Analyse de la performance par regime
# Distribution des regimes
regime_counts = regimes_sma.value_counts()
regime_pct = regimes_sma.value_counts(normalize=True) * 100
print("="*60)
print("ANALYSE PAR REGIME")
print("="*60)
print("\\n1. Distribution des regimes :")
print(f"{'Regime':<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 regime (simplifiee)
warmup = 200
returns = prices_all.pct_change().fillna(0)
regime_returns = {'bull': [], 'bear': [], 'sideways': []}
for i in range(warmup, len(prices_all)):
regime = regimes_sma.iloc[i]
if regime in regime_returns:
# Return du portefeuille a ce moment
port_return = 0
sig = signals.iloc[i]
for asset in ['SPY', 'QQQ', 'GLD', 'IEF']:
if asset in returns.columns:
port_return += sig[asset] * returns[asset].iloc[i]
regime_returns[regime].append(port_return)
print("\\n2. Performance moyenne par regime (annualisee) :")
for regime in ['bull', 'bear', 'sideways']:
if regime_returns[regime]:
avg_return = np.mean(regime_returns[regime]) * 252
std_return = np.std(regime_returns[regime]) * np.sqrt(252)
print(f" {regime}: {avg_return:>7.1%} (vol: {std_return:>6.1%})")
else:
print(f" {regime}: N/A")
print("\\n3. Statistiques globales :")
print(f" Total regimes: {len(regimes_sma)}")
print(f" Warmup period: {warmup} jours")
print(f" Periode d'analyse: {len(prices_all) - warmup} jours ({(len(prices_all) - warmup)/252:.1f} ans)")============================================================ANALYSE PAR REGIME============================================================\n1. Distribution des regimes :Regime Jours Pourcentage-----------------------------------bull 859 57.3%bear 365 24.3%sideways 77 5.1%\n2. Performance moyenne par regime (annualisee) : bull: 42.2% (vol: 24.3%) bear: 1.5% (vol: 3.7%) sideways: -5.1% (vol: 8.2%)\n3. Statistiques globales : Total regimes: 1500 Warmup period: 200 jours Periode d'analyse: 1300 jours (5.2 ans)
Exercice 4 : Regime performance attribution
Decomposez la performance d’une stratégie par regime pour identifier les forces et faiblesses.
Objectif : Calculer les metriques de performance par regime de marche.
Règles : - Identifiez 3 regimes : bull, bear, flat (via les labels du HMM ci-dessus) - Pour chaque regime, calculez : nombre de jours, rendement moyen, volatilite, Sharpe, max drawdown - Affichez un tableau comparatif par regime - Identifiez le regime ou la stratégie sous-performe le plus
Indices : - Indice : Groupby par label de regime : df.groupby('regime')['returns'].agg(...) - Indice : Sharpe par regime = mean / std avec annualisation
Critère de validation : votre tableau doit lister, pour chaque régime, le nombre de jours, le rendement moyen, la volatilité, le Sharpe et le MaxDD. Le régime qui sous-performe le plus devrait être le sideways (référence -5,1 % de la sortie committée). Point de contrôle : la somme des jours par régime doit être inférieure ou égale au total, les jours non classés étant comptés à part.
# Exercice 3 : Performance par regime
# TODO etudiant : Decomposer la performance par regime
# Indice : Groupby regime + agg(mean, std, sharpe, max_dd)
# Etape 1 : Attribuer un regime a chaque jour
# Etape 2 : Calculer les metriques par regime
# Etape 3 : Construire le tableau comparatif
# Etape 4 : Identifier le regime sous-performant
result = None # TODO etudiant : remplacer par l'attribution par regime
print("Exercice a completer")Partie 6 : Analyse par Regime (5 min)
Performance par regime
Pour comprendre les forces et faiblesses de la stratégie, on analyse la performance par regime :
| Regime | Performance attendue | Risque |
|---|---|---|
| Bull | Positif (momentum) | Modere |
| Bear | Negatif maitrise (defensif) | Faible |
| Sideways | Positif (mean-reversion) | Eleve |
Metriques cles
- Distribution des regimes : Pourcentage du temps passe dans chaque regime
- Return moyen par regime : Performance annualisee par regime
- Drawdown par regime : Risque maximal par regime
- Stabilite : Variance de la performance dans le temps
Ce que l’attribution revele : la question n’est pas seulement « la strategie gagne-t-elle ? » mais « ou gagne-t-elle, et ou perd-elle ? ». Une strategie peut afficher un Sharpe honnete tout en concentrant ses pertes sur un seul regime — une fragilite invisible dans la courbe de capitaux globale. L’analyse ci-dessous ventile les jours utiles par regime detecte et expose cette structure : c’est ce diagnostic qui decide si un parametre doit etre ajuste par regime, ou si le detecteur lui-meme doit etre ameliore.
# Visualisation des resultats d'optimisation
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
# 1. Sharpe par seuil RSI
ax = axes[0]
thresholds = list(results_rsi.keys())
sharpes = [results_rsi[t]['sharpe'] for t in thresholds]
ax.bar(range(len(thresholds)), sharpes, color='steelblue', alpha=0.7)
ax.set_xticks(range(len(thresholds)))
ax.set_xticklabels([f'RSI<{t}' for t in thresholds])
ax.set_title('Sharpe Ratio par Seuil RSI', fontweight='bold')
ax.set_ylabel('Sharpe Ratio')
ax.grid(True, alpha=0.3)
ax.axhline(y=best_rsi[1]['sharpe'], color='r', linestyle='--', alpha=0.5, label='Best')
ax.legend()
# 2. CAGR par seuil RSI
ax = axes[1]
cagrs = [results_rsi[t]['cagr'] for t in thresholds]
ax.bar(range(len(thresholds)), [c*100 for c in cagrs], color='coral', alpha=0.7)
ax.set_xticks(range(len(thresholds)))
ax.set_xticklabels([f'RSI<{t}' for t in thresholds])
ax.set_title('CAGR par Seuil RSI', fontweight='bold')
ax.set_ylabel('CAGR (%)')
ax.grid(True, alpha=0.3)
# 3. Max Drawdown par seuil RSI
ax = axes[2]
max_dds = [abs(results_rsi[t]['max_dd']) * 100 for t in thresholds]
ax.bar(range(len(thresholds)), max_dds, color='crimson', alpha=0.7)
ax.set_xticks(range(len(thresholds)))
ax.set_xticklabels([f'RSI<{t}' for t in thresholds])
ax.set_title('Max Drawdown par Seuil RSI', fontweight='bold')
ax.set_ylabel('Max Drawdown (%)')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("\\nResume de l'optimisation :")
print(f" Meilleur Sharpe: {best_rsi[1]['sharpe']:.3f} (RSI<{best_rsi[0]})")
print(f" CAGR correspondant: {best_rsi[1]['cagr']:.1%}")
print(f" Max DD correspondant: {best_rsi[1]['max_dd']:.1%}")\nResume de l'optimisation : Meilleur Sharpe: 1.146 (RSI<20) CAGR correspondant: 24.7% Max DD correspondant: -19.8%

Interpretation : Optimisation des Seuils
Sortie obtenue : Les graphiques montrent la performance selon le seuil RSI.
| Aspect | Observation |
|---|---|
| Sharpe optimal | Le seuil RSI qui maximise le ratio rendement/risque |
| Trade-off | Seuil bas = plus de signaux mais plus de whipsaws |
| Robustesse | Verifier que le meilleur seuil n’est pas un outlier |
Points cles : 1. L’optimisation doit etre faite out-of-sample pour eviter l’overfitting 2. Le seuil optimal depend de la periode etudiee (regime dependent) 3. Il est preferable de choisir un seuil robuste (performance stable) plutot qu’optimal
Note technique : Pour une implementation en production, utiliser walk-forward optimization : optimiser sur une periode, tester sur la periode suivante, et repeter. Cela evite l’overfitting et simule les conditions reelles de trading.
Lecture chiffrée de la sortie : l’optimisation par grille retourne un Sharpe de 1,146 pour un seuil RSI < 20, avec un CAGR de 24,7 % et un MaxDD de -19,8 %. Le point important est la platitude du plateau : les seuils 20, 25, 30, 35 et 40 donnent exactement les mêmes trois métriques — le signal RSI n’est pas déclenché ou le portefeuille reste dans la même position sur cette période. Un Sharpe identique sur toute la grille n’est pas une preuve de robustesse mais de non-sensibilité : l’optimum n’est pas un pic mais un palier, la configuration idéale pour éviter l’overfitting de seuil.
# Optimisation des seuils RSI
def backtest_regime_switching(prices: pd.DataFrame, signals: pd.DataFrame, rebal_freq: int = 20) -> Dict:
"""
Backtest simplifie de RegimeSwitching.
Args:
prices: Prix des 4 assets
signals: Signaux d'allocation
rebal_freq: Frequence de rebalancement (jours)
Returns:
Dict avec metriques de performance
"""
returns = prices.pct_change().fillna(0)
# Warmup
warmup = 200
portfolio_values = [1.0]
current_alloc = signals.iloc[warmup].fillna(0)
counter = 0
for i in range(warmup + 1, len(prices)):
# Rebalancement periodique
counter += 1
if counter >= rebal_freq:
counter = 0
current_alloc = signals.iloc[i].fillna(0)
# Return du portefeuille
port_return = 0
for asset in ['SPY', 'QQQ', 'GLD', 'IEF']:
if asset in returns.columns:
port_return += current_alloc[asset] * returns[asset].iloc[i]
portfolio_values.append(portfolio_values[-1] * (1 + port_return))
# Metriques
returns_array = np.diff(portfolio_values) / np.array(portfolio_values[:-1])
returns_array = returns_array[~np.isnan(returns_array)]
if len(returns_array) == 0:
return {'sharpe': 0, 'cagr': 0, 'max_dd': 0}
total_return = portfolio_values[-1] / portfolio_values[0] - 1
years = len(returns_array) / 252
cagr = (1 + total_return) ** (1 / years) - 1 if years > 0 else 0
vol = np.std(returns_array) * np.sqrt(252) if len(returns_array) > 1 else 0
sharpe = (cagr - 0.02) / vol if vol > 0.001 else 0
# Drawdown
cum_returns = pd.Series(portfolio_values)
running_max = cum_returns.expanding().max()
drawdown = (cum_returns - running_max) / running_max
max_dd = drawdown.min()
return {
'sharpe': sharpe,
'cagr': cagr,
'max_dd': max_dd,
'vol': vol,
'final_value': portfolio_values[-1]
}
# Grid search sur les seuils RSI
rsi_thresholds = [20, 25, 30, 35, 40]
results_rsi = {}
print("Optimisation des seuils RSI :")
print(f"{'Seuil RSI':<12} {'Sharpe':>8} {'CAGR':>8} {'MaxDD':>8}")
print("-" * 40)
for threshold in rsi_thresholds:
sig = compute_regime_switching_signals(
prices_all['SPY'], prices_all['QQQ'], prices_all['GLD'], prices_all['IEF'],
rsi, rsi_qqq, regimes_sma,
rsi_oversold=threshold
)
result = backtest_regime_switching(prices_all, sig)
results_rsi[threshold] = result
print(f"RSI<{threshold:<9} {result['sharpe']:>8.3f} {result['cagr']:>7.1%} {result['max_dd']:>7.1%}")
best_rsi = max(results_rsi.items(), key=lambda x: x[1]['sharpe'])
print(f"\\nMeilleur seuil RSI: {best_rsi[0]} (Sharpe={best_rsi[1]['sharpe']:.3f})")Optimisation des seuils RSI :Seuil RSI Sharpe CAGR MaxDD----------------------------------------RSI<20 1.146 24.7% -19.8%RSI<25 1.146 24.7% -19.8%RSI<30 1.146 24.7% -19.8%RSI<35 1.146 24.7% -19.8%RSI<40 1.146 24.7% -19.8%\nMeilleur seuil RSI: 20 (Sharpe=1.146)
Partie 5 : Optimisation des Seuils (10 min)
Pourquoi optimiser les paramètres ?
Les paramètres par defaut (RSI=30, bull=70/30) ne sont pas optimaux pour toutes les periodes. L’optimisation permet de :
- Maximiser le Sharpe ratio : Trouver le meilleur couple rendement/risque
- Adapter au marche : Les regimes peuvent changer de caracteriques
- Robustesse : Tester la sensibilite aux paramètres
Paramètres a optimiser
| Paramètre | Range | Impact |
|---|---|---|
| RSI oversold | 20-40 | Plus bas = plus de signaux mais plus de risque |
| Bull allocation | 60/40, 70/30, 80/20 | Plus agressif = plus de rendement mais plus de volatilite |
Méthode d’optimisation
On utilise une grid search sur les paramètres, avec les contraintes suivantes :
- Tester chaque combinaison sur la periode complete
- Calculer Sharpe, CAGR, Max Drawdown
- Sélectionner la combinaison avec le meilleur Sharpe
- Verifier la robustesse (pas d’overfitting)
Les trois pieges de la grid search :
- Le pic isole : un optimum net sur un seul seuil, entoure de voisins mediocres, signale l’overfitting — la vraie robustesse est un plateau.
- Le plateau plat : des metriques strictement identiques d’un bout a l’autre de la grille (ce que la sortie ci-dessous illustre) ne prouvent pas la robustesse mais la non-sensibilite : le parametre ne joue aucun role sur la periode testee.
- L’in-sample cache : optimiser et evaluer sur la meme periode sur-estime systematiquement la performance ; seul le out-of-sample ou le walk-forward compte pour une decision de deploiement.
# Visualiser les allocations RegimeSwitching
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 10), sharex=True)
# Graphique 1: Allocation stackplot
ax1.stackplot(signals.index,
signals['SPY'], signals['QQQ'], signals['GLD'], signals['IEF'],
labels=['SPY', 'QQQ', 'GLD', 'IEF'],
colors=['blue', 'cyan', 'gold', 'orange'],
alpha=0.7, linewidth=0.5)
ax1.set_title('Allocation RegimeSwitching', fontsize=12, fontweight='bold')
ax1.set_ylabel('Allocation')
ax1.legend(loc='upper left')
ax1.set_ylim(0, 1)
# Graphique 2: Regime sous-jacent
regime_numeric = regimes_sma.map({'bull': 1, 'sideways': 0, 'bear': -1, 'unknown': 0})
ax2.fill_between(signals.index, regime_numeric, alpha=0.3,
where=regime_numeric==1, color='green', label='Bull')
ax2.fill_between(signals.index, regime_numeric, alpha=0.3,
where=regime_numeric==-1, color='red', label='Bear')
ax2.fill_between(signals.index, regime_numeric, alpha=0.3,
where=regime_numeric==0, color='gray', label='Sideways')
ax2.set_title('Regime de Marche', fontsize=12, fontweight='bold')
ax2.set_ylabel('Regime')
ax2.set_xlabel('Date')
ax2.set_yticks([-1, 0, 1])
ax2.set_yticklabels(['Bear', 'Sideways', 'Bull'])
ax2.legend(loc='upper left')
plt.tight_layout()
plt.show()
print("\\nStatistiques d'allocation :")
print(f" Jours avec SPY > 0 : {(signals['SPY'] > 0).sum()} ({(signals['SPY'] > 0).sum() / len(signals) * 100:.1f}%)")
print(f" Jours avec GLD > 0 : {(signals['GLD'] > 0).sum()} ({(signals['GLD'] > 0).sum() / len(signals) * 100:.1f}%)")
print(f" Jours avec IEF > 0 : {(signals['IEF'] > 0).sum()} ({(signals['IEF'] > 0).sum() / len(signals) * 100:.1f}%)")\nStatistiques d'allocation : Jours avec SPY > 0 : 933 (62.2%) Jours avec GLD > 0 : 442 (29.5%) Jours avec IEF > 0 : 442 (29.5%)

Exercice 5 : Stratégie de rotation adaptative
Une stratégie adaptative change de regime en fonction de la volatilite et de la tendance.
Objectif : Implementer un signal de rotation qui adapte le style (trend vs mean-reversion) au regime.
Règles : - Regime trend (SMA50 > SMA200) : utiliser un signal momentum (retour 20j) - Regime mean-reversion (RSI extremes) : utiliser un signal RSI inversé - Regime neutre : signal = 0 (flat, ne pas trader) - Calculez le PnL cumulé et comparez avec un buy-and-hold
Indices : - Indice : np.where(condition_trend, signal_momentum, np.where(condition_mr, signal_rsi_inv, 0)) - Indice : RSI inverse = -1 * (RSI - 50) / 50
Critère de validation : tracez le PnL cumulé de la rotation et du buy-and-hold sur le même graphique. Le résultat attendu : la rotation réduit le drawdown en bear (passage défensif) sans rattraper le buy-and-hold en bull fort — le verdict honnête est rarement « gagner partout » mais « meilleur risque ajusté ». Comparez les Sharpe des deux courbes, pas seulement les valeurs finales.
# Exercice 2 : Rotation adaptative trend/mean-reversion
# TODO etudiant : Adapter le signal au regime detecte
# Indice : np.where pour switcher entre momentum et RSI inverse
# Etape 1 : Detecter le regime (trend/mean-reversion/neutre)
# Etape 2 : Calculer le signal momentum
# Etape 3 : Calculer le signal RSI inverse
# Etape 4 : Combiner et afficher le PnL vs B&H
result = None # TODO etudiant : remplacer par la rotation adaptative
print("Exercice a completer")Interpretation : Allocation RegimeSwitching
Sortie obtenue : Le graphique montre l’allocation dynamique selon le regime.
| Aspect | Observation |
|---|---|
| Bull market | Allocation 100% equities (SPY/QQQ) pour participer au trend haussier |
| Bear market | Allocation 100% defensifs (GLD/IEF) pour proteger le capital |
| Sideways | Allocation mixte avec mean-reversion sur RSI oversold |
Points cles : 1. L’allocation s’adapte automatiquement au regime sans intervention manuelle 2. La stratégie est defensive par defaut (bear/sideways = 70% du temps typiquement) 3. Le mean-reversion en sideways ajoute de l’alpha mais augmente le risque
Note technique : Cette implementation est simplifiee. La version complete (voir
projects/RegimeSwitching/main.py) inclut le rebalancement mensuel, le stop-loss trailing, et la gestion des transitions de regime.
Lecture chiffrée de la sortie : sur 1 500 jours, l’actif actions SPY est en position 933 jours (62,2 % du temps) alors que les refuges GLD et IEF ne le sont que 442 jours (29,5 % chacun) — la stratégie est investie en risque la majorité du temps. L’allocation moyenne confirme le biais : SPY 48,0 %, QQQ 20,2 %, GLD 16,1 %, IEF 16,1 %. Les 10 derniers signaux affichent la matrice 0,7/0,3/0,0/0,0 : le régime détecté en fin de série est un bull, avec 100 % du portefeuille en actions et zéro refuge — cohérent avec un marché de janvier 2019 qui remonte après le creux de décembre 2018.
# Implementation simplifiee de RegimeSwitching
def compute_regime_switching_signals(
prices_spy: pd.Series,
prices_qqq: pd.Series,
prices_gld: pd.Series,
prices_ief: pd.Series,
rsi_spy: pd.Series,
rsi_qqq: pd.Series,
regimes: pd.Series,
rsi_oversold: float = 30.0,
rsi_exit: float = 50.0,
bull_spy_weight: float = 0.70
) -> pd.DataFrame:
"""
Calcule les signaux de la strategie RegimeSwitching.
Args:
prices: Prix des 4 assets (SPY, QQQ, GLD, IEF)
rsi: RSI de SPY et QQQ
regimes: Serie des regimes detectes
rsi_oversold: Seuil RSI pour oversold (default 30)
rsi_exit: Seuil RSI pour exit (default 50)
bull_spy_weight: Poids SPY en bull (default 0.70)
Returns:
DataFrame avec les allocations cibles
"""
signals = pd.DataFrame(index=prices_spy.index, columns=['SPY', 'QQQ', 'GLD', 'IEF'])
for i in range(200, len(prices_spy)): # Warmup period
regime = regimes.iloc[i]
if regime == 'bull':
# Momentum: SPY/QQQ selon allocation
signals.loc[signals.index[i], 'SPY'] = bull_spy_weight
signals.loc[signals.index[i], 'QQQ'] = 1.0 - bull_spy_weight
signals.loc[signals.index[i], 'GLD'] = 0.0
signals.loc[signals.index[i], 'IEF'] = 0.0
elif regime == 'bear':
# Defensif: GLD/IEF
signals.loc[signals.index[i], 'SPY'] = 0.0
signals.loc[signals.index[i], 'QQQ'] = 0.0
signals.loc[signals.index[i], 'GLD'] = 0.50
signals.loc[signals.index[i], 'IEF'] = 0.50
else: # sideways
# Mean-reversion si RSI oversold
oversold_assets = []
if i < len(rsi_spy) and pd.notna(rsi_spy.iloc[i]) and rsi_spy.iloc[i] < rsi_oversold:
oversold_assets.append('SPY')
if i < len(rsi_qqq) and pd.notna(rsi_qqq.iloc[i]) and rsi_qqq.iloc[i] < rsi_oversold:
oversold_assets.append('QQQ')
if oversold_assets:
# Mean-reversion: oversold 30% total, GLD/IEF 35% chacun
weight_per_oversold = 0.30 / len(oversold_assets)
for asset in oversold_assets:
signals.loc[signals.index[i], asset] = weight_per_oversold
signals.loc[signals.index[i], 'GLD'] = 0.35
signals.loc[signals.index[i], 'IEF'] = 0.35
else:
# Allocation reduite: SPY 30%, GLD/IEF 35% chacun
signals.loc[signals.index[i], 'SPY'] = 0.30
signals.loc[signals.index[i], 'QQQ'] = 0.0
signals.loc[signals.index[i], 'GLD'] = 0.35
signals.loc[signals.index[i], 'IEF'] = 0.35
return signals
# Generer des donnees pour les 4 assets
np.random.seed(42)
n_days = 1500
dates = pd.date_range(start='2015-01-01', periods=n_days, freq='D')
# Prix synthetiques corrèles
prices_all = pd.DataFrame(index=dates)
prices_all['SPY'] = df['price'].values
prices_all['QQQ'] = df['price'].values * np.random.uniform(0.95, 1.05, n_days).cumprod()
prices_all['GLD'] = 100 + np.random.normal(0, 0.5, n_days).cumsum()
prices_all['IEF'] = 100 + np.random.normal(0, 0.2, n_days).cumsum()
# RSI pour QQQ
rsi_qqq = compute_rsi(prices_all['QQQ'])
# Signaux RegimeSwitching
signals = compute_regime_switching_signals(
prices_all['SPY'], prices_all['QQQ'], prices_all['GLD'], prices_all['IEF'],
rsi, rsi_qqq, regimes_sma
)
print("Signaux RegimeSwitching (10 derniers jours) :")
print(signals.tail(10))
print(f"\\nAllocation moyenne :")
print(signals.mean())Signaux RegimeSwitching (10 derniers jours) : SPY QQQ GLD IEF2019-01-30 0.7 0.3 0.0 0.02019-01-31 0.7 0.3 0.0 0.02019-02-01 0.7 0.3 0.0 0.02019-02-02 0.7 0.3 0.0 0.02019-02-03 0.7 0.3 0.0 0.02019-02-04 0.7 0.3 0.0 0.02019-02-05 0.7 0.3 0.0 0.02019-02-06 0.7 0.3 0.0 0.02019-02-07 0.7 0.3 0.0 0.02019-02-08 0.7 0.3 0.0 0.0\nAllocation moyenne :SPY 0.479931QQQ 0.20168GLD 0.161115IEF 0.161115dtype: object
Partie 4 : Strategy Adaptative - RegimeSwitching (15 min)
Concept de la stratégie
La stratégie RegimeSwitching adapte son comportement selon le regime detecte :
Ancre savante – Hamilton (1989), « A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle », Econometrica 57(2), 357-384 (DOI:10.2307/1912559) – modèle Markov a changements de regime, originateur canonique du regime-switching en econometrie.
| Regime | Approche | Allocation cible |
|---|---|---|
| Bull | Momentum (risk-on) | SPY 70% / QQQ 30% |
| Bear | Defensif (risk-off) | GLD 50% / IEF 50% |
| Sideways | Mean-reversion ou reduit | GLD 35% / IEF 35% / SPY 30% |
Avantages de l’approche adaptative
- Participe aux tendances : Allocation agressive en bull market
- Protege le capital : Allocation defensive en bear market
- Exploite les opportunitses : Mean-reversion en sideways
- Reduit le drawdown : En evitant les equites pendant les crises
Paramètres cles
- Seuil RSI oversold : 30 (peut etre optimise : 20-40)
- Allocation bull : 70/30 (peut etre optimise : 60/40, 80/20)
- Frequence rebalancement : Mensuel + trigger sur changement de regime
- Stop-loss : Trailing stop a -10% sur les positions equities
# Visualiser le RSI avec les zones d'oversold/overbought
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 10), sharex=True)
# Graphique 1: Prix
ax1.plot(df.index, df['price'], 'k-', linewidth=1, label='Prix')
ax1.set_title('Prix et RSI', fontsize=12, fontweight='bold')
ax1.set_ylabel('Prix')
ax1.legend(loc='upper left')
# Graphique 2: RSI avec zones
ax2.plot(df.index, rsi, 'b-', linewidth=1, label='RSI(14)')
ax2.axhline(70, color='r', linestyle='--', linewidth=1, alpha=0.7, label='Overbought (>70)')
ax2.axhline(30, color='g', linestyle='--', linewidth=1, alpha=0.7, label='Oversold (<30)')
ax2.axhline(50, color='gray', linestyle='-', linewidth=0.5, alpha=0.5)
# Colorer les zones
ax2.fill_between(df.index, 70, 100, alpha=0.1, color='red')
ax2.fill_between(df.index, 0, 30, alpha=0.1, color='green')
# Marquer les signaux
oversold_dates = rsi[rsi < 30].index
overbought_dates = rsi[rsi > 70].index
if len(oversold_dates) > 0:
ax2.scatter(oversold_dates, rsi[oversold_dates], color='green', s=20, alpha=0.6, label='Oversold signals', zorder=5)
if len(overbought_dates) > 0:
ax2.scatter(overbought_dates, rsi[overbought_dates], color='red', s=20, alpha=0.6, label='Overbought signals', zorder=5)
ax2.set_title('RSI(14) - Zones de Surachat/Survente', fontsize=12, fontweight='bold')
ax2.set_ylabel('RSI')
ax2.set_xlabel('Date')
ax2.set_ylim(0, 100)
ax2.legend(loc='upper left')
plt.tight_layout()
plt.show()
print("\\nSignaux RSI :")
print(f" Signaux oversold (RSI < 30) : {len(oversold_dates)}")
print(f" Signaux overbought (RSI > 70) : {len(overbought_dates)}")\nSignaux RSI : Signaux oversold (RSI < 30) : 129 Signaux overbought (RSI > 70) : 256

Interpretation : RSI et Mean-Reversion
Sortie obtenue : Le graphique montre le RSI avec les zones de surachat/survente.
| Aspect | Observation |
|---|---|
| Zones extremes | Le RSI oscille entre 0 et 100, avec des zones extremes (<30, >70) |
| Signaux oversold | Moments ou le prix est “trop bas” -> potentiel de rebond |
| Signaux overbought | Moments ou le prix est “trop haut” -> potentiel de correction |
Points cles : 1. Le RSI est contrarian : il indique un rebond potentiel quand tout le monde est bearish 2. Les signaux extremes sont rares (typiquement 5-10% du temps) 3. Le RSI doit etre combine avec le regime de marche pour etre efficace
Note technique : Le RSI(14) est standard, mais on peut ajuster la periode. RSI(7) est plus reactif mais plus bruite, RSI(21) est plus lent mais plus fiable.
Lecture chiffrée de la sortie : la statistique du RSI(14) sur la série complète donne une moyenne de 52,7, un minimum de 7,0 et un maximum de 97,8 — l’oscillateur explore bien les zones extrêmes. Le compte des signaux est déséquilibré : 129 jours en oversold (RSI < 30) contre 256 jours en overbought (RSI > 70), un ratio de 1 pour 2 : dans une série à tendance haussière dominante, les prix passent deux fois plus de temps à l’étage surachat qu’en survente — la signature de la dérive (drift) du marché synthétique, pas un biais du calcul.
# Implementation du RSI
def compute_rsi(prices: pd.Series, period: int = 14) -> pd.Series:
"""
Calcule le RSI (Relative Strength Index).
Args:
prices: Serie des prix
period: Periode de calcul (default 14)
Returns:
Serie du RSI (0-100)
"""
# Calculer les variations de prix
delta = prices.diff()
# Separer gains et pertes
gains = delta.where(delta > 0, 0)
losses = -delta.where(delta < 0, 0)
# Moyenne mobile des gains/pertes (Wilders smoothing)
avg_gains = gains.rolling(window=period).mean()
avg_losses = losses.rolling(window=period).mean()
# Relative Strength
rs = avg_gains / avg_losses
# RSI
rsi = 100 - (100 / (1 + rs))
return rsi
# Calculer le RSI sur les donnees synthetiques
rsi = compute_rsi(df['price'], period=14)
print("RSI (10 derniers jours) :")
print(rsi.tail(10))
print(f"\\nStatistiques RSI :")
print(f" Moyenne : {rsi.mean():.1f}")
print(f" Min : {rsi.min():.1f}")
print(f" Max : {rsi.max():.1f}")
print(f" Jours oversold (< 30) : {(rsi < 30).sum()}")
print(f" Jours overbought (> 70) : {(rsi > 70).sum()}")RSI (10 derniers jours) :2019-01-30 42.9889862019-01-31 41.5562112019-02-01 49.8095422019-02-02 50.9380072019-02-03 53.1491652019-02-04 55.7446182019-02-05 62.4382922019-02-06 63.3960782019-02-07 67.4660192019-02-08 68.994284Freq: D, Name: price, dtype: float64\nStatistiques RSI : Moyenne : 52.7 Min : 7.0 Max : 97.8 Jours oversold (< 30) : 129 Jours overbought (> 70) : 256
Exercice 6 : Detecteur de regime multi-indicateurs
Un seul indicateur (SMA, RSI, volatilite) est insuffisant. Un detecteur robuste combine plusieurs signaux.
Objectif : Implementer un score de regime combinant tendance, momentum et volatilite.
Règles : - Score tendance : +1 si SMA50 > SMA200, -1 sinon - Score momentum : +1 si retour 20j > 0, -1 sinon - Score volatilite : +1 si vol 20j < vol 60j (calme), -1 sinon - Score composite = 0.4tendance + 0.3momentum + 0.3*volatilite - Regime : > 0.5 = bull, < -0.5 = bear, sinon = flat - Affichez la distribution des regimes
Indices : - Indice : Chaque sous-score est np.where(condition, +1, -1) - Indice : Le score composite est une somme ponderee
Critère de validation : vérifiez que votre score composite produit une distribution à trois classes non vides. Test de discrimination : fabriquez un cas où tendance et momentum divergent (prix sous SMA200 mais retour 20j positif) — le score pondéré doit rester proche de zéro plutôt que trancher brutalement, preuve que le score synthétise au lieu de laisser un indicateur dominer.
# Exercice 1 : Detecteur multi-indicateurs
# TODO etudiant : Combiner tendance + momentum + volatilite
# Indice : 3 sous-scores binaires, somme ponderee, seuils +/-0.5
# Etape 1 : Calculer le score tendance
# Etape 2 : Calculer le score momentum
# Etape 3 : Calculer le score volatilite
# Etape 4 : Combiner et classifier les regimes
result = None # TODO etudiant : remplacer par le detecteur multi-indicateurs
print("Exercice a completer")Partie 3 : Oscillateurs - RSI et Mean-Reversion (10 min)
Le RSI pour detecter les extremes
Le Relative Strength Index (RSI) est un oscillateur qui mesure la vitesse et l’amplitude des mouvements de prix :
| Valeur RSI | Interpretation | Action |
|---|---|---|
| > 70 | Surachat (overbought) | Potentiel de baisse |
| < 30 | Survente (oversold) | Potentiel de hausse (mean-reversion) |
| 30-70 | Neutre | Pas de signal clair |
Mean-Reversion en regime defensif
Dans les regimes bear ou sideways, le momentum ne fonctionne pas bien. On utilise alors la mean-reversion :
- Oversold entry : Quand RSI < 30, le marche est “trop bas” -> acheter
- Exit signal : Quand RSI > 50, le marche est revenu a la moyenne -> vendre
- Stop-loss : Limiter les pertes si le trend continue contre nous
Cette approche contre-trend est risquee mais peut etre rentable si : 1. On est en regime defensif (bear/sideways) 2. On utilise des positions reduites (30% max) 3. On a un stop-loss strict
Dependence au regime : un RSI < 30 en bull market signale souvent un point d’entree favorable (le trend reprend apres le creux) ; le meme signal en bear market est un « couteau qui tombe » — le rebond attendu peut ne jamais venir et l’achat contre-trend aggraver la perte. C’est precisement pourquoi la strategie de ce notebook ne declenche le mean-reversion qu’en regime defensif, avec position reduite et stop-loss : l’indicateur fournit le signal, le regime fournit le droit de l’utiliser.
# Visualiser la detection SMA avec le prix
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(16, 10), sharex=True)
# Graphique 1: Prix + SMA + Regimes
ax1.plot(df.index, df['price'], 'k-', linewidth=1, alpha=0.5, label='Prix')
ax1.plot(df.index, df['sma50'], 'b-', linewidth=1.5, label='SMA50', alpha=0.7)
ax1.plot(df.index, df['sma200'], 'r-', linewidth=1.5, label='SMA200', alpha=0.7)
# Colorer les zones par regime
colors_regime = regimes_sma.map({'bull': 'green', 'bear': 'red', 'sideways': 'gray', 'unknown': 'lightgray'})
for i in range(1, len(df)):
if regimes_sma.iloc[i] != regimes_sma.iloc[i-1]:
ax1.axvline(df.index[i], color='black', linestyle='--', linewidth=0.5, alpha=0.3)
ax1.scatter(df.index, df['price'], c=colors_regime, s=2, alpha=0.3)
ax1.set_title('Detection SMA - Prix et Moyennes Mobiles', fontsize=12, fontweight='bold')
ax1.set_ylabel('Prix')
ax1.legend(loc='upper left')
# Graphique 2: Regime au cours du temps
regime_numeric = regimes_sma.map({'bull': 1, 'sideways': 0, 'bear': -1, 'unknown': 0})
ax2.fill_between(df.index, regime_numeric, alpha=0.3,
where=regime_numeric==1, color='green', label='Bull')
ax2.fill_between(df.index, regime_numeric, alpha=0.3,
where=regime_numeric==-1, color='red', label='Bear')
ax2.fill_between(df.index, regime_numeric, alpha=0.3,
where=regime_numeric==0, color='gray', label='Sideways')
ax2.set_title('Regime Detecte', fontsize=12, fontweight='bold')
ax2.set_ylabel('Regime')
ax2.set_xlabel('Date')
ax2.set_yticks([-1, 0, 1])
ax2.set_yticklabels(['Bear', 'Sideways', 'Bull'])
ax2.legend(loc='upper left')
plt.tight_layout()
plt.show()
print("\\nResume de la detection SMA :")
print(f" Pourcentage en bull : {(regimes_sma == 'bull').sum() / len(regimes_sma) * 100:.1f}%")
print(f" Pourcentage en bear : {(regimes_sma == 'bear').sum() / len(regimes_sma) * 100:.1f}%")
print(f" Pourcentage en sideways : {(regimes_sma == 'sideways').sum() / len(regimes_sma) * 100:.1f}%")\nResume de la detection SMA : Pourcentage en bull : 57.3% Pourcentage en bear : 24.3% Pourcentage en sideways : 5.1%

Interpretation : Detection SMA
Sortie obtenue : Le graphique montre la detection des regimes et leur distribution.
| Aspect | Observation |
|---|---|
| Transitions | Les regimes changent lorsque le prix et SMA50 traversent SMA200 |
| Bull | Le marche est en tendance haussiere, les stratégies de momentum fonctionnent bien |
| Bear | Le marche est en tendance baissiere, les stratégies defensives sont privilegiees |
| Sideways | Le marche est indecis, les stratégies de mean-reversion peuvent etre efficaces |
Points cles : 1. La detection SMA est retrospective mais utilise des données historiques seulement 2. Le regime “sideways” est souvent une phase de transition 3. La distribution des regimes depend de la periode etudiee (marche haussier vs baissier)
Note technique : Les periodes SMA (50/200) sont standards mais peuvent etre ajustees. SMA50 represente environ 3 mois de trading, SMA200 environ 10 mois.
Lecture chiffrée de la sortie : la détection SMA50/SMA200 classe 57,3 % de la série en bull, 24,3 % en bear et 5,1 % en sideways — les 199 jours « unknown » restants sont les transitions de croisement. Les 10 derniers jours affichés sont tous classés bull, cohérent avec la pente haussière de la fin de série. La règle est binaire : chaque jour appartient à une classe, sans gradation. C’est sa force (simplicité, zéro paramètre de réglage) et sa limite (les whipsaws aux croisements, dont le MaxDD de 26,9 % du backtest QC Cloud rapporté en tête de notebook porte la trace).
# Implementation de la detection SMA
def detect_regime_sma(prices: pd.Series, fast_period: int = 50, slow_period: int = 200) -> pd.Series:
"""
Detecte le regime de marche en utilisant les SMA.
Args:
prices: Serie des prix
fast_period: Periode SMA rapide (default 50)
slow_period: Periode SMA lente (default 200)
Returns:
Serie des regimes ('bull', 'bear', 'sideways', 'unknown')
"""
# Calculer les SMA
sma_fast = prices.rolling(window=fast_period).mean()
sma_slow = prices.rolling(window=slow_period).mean()
# Detecter le regime pour chaque jour
regimes = []
for i in range(len(prices)):
price = prices.iloc[i]
fast = sma_fast.iloc[i]
slow = sma_slow.iloc[i]
# Verifier si les SMA sont disponibles
if pd.isna(fast) or pd.isna(slow):
regimes.append('unknown')
elif price > slow and fast > slow:
regimes.append('bull')
elif price < slow and fast < slow:
regimes.append('bear')
else:
regimes.append('sideways')
return pd.Series(regimes, index=prices.index)
# Tester sur les donnees synthetiques
regimes_sma = detect_regime_sma(df['price'])
print("Detection SMA - Regimes (10 derniers jours) :")
print(regimes_sma.tail(10))
print(f"\\nDistribution :")
print(regimes_sma.value_counts())Detection SMA - Regimes (10 derniers jours) :2019-01-30 bull2019-01-31 bull2019-02-01 bull2019-02-02 bull2019-02-03 bull2019-02-04 bull2019-02-05 bull2019-02-06 bull2019-02-07 bull2019-02-08 bullFreq: D, dtype: object\nDistribution :bull 859bear 365unknown 199sideways 77Name: count, dtype: int64
Partie 2 : Detection SMA - Trend Following (12 min)
Les moyennes mobiles comme filtres de tendance
Les moyennes mobiles (Simple Moving Average - SMA) sont les indicateurs les plus utilises pour detecter les trends :
| SMA | Usage | Signal |
|---|---|---|
| SMA50 | Trend court terme (3 mois) | Momentum recent |
| SMA200 | Trend long terme (12 mois) | Trend fondamental |
| SMA50 > SMA200 | Golden Cross | Signal haussier |
| SMA50 < SMA200 | Death Cross | Signal baissier |
Logique de detection des regimes
IF prix > SMA200 AND SMA50 > SMA200:
regime = BULL
# Trend haussier confirme
# Le marche est dans une phase d'expansion
ELIF prix < SMA200 AND SMA50 < SMA200:
regime = BEAR
# Trend baissier confirme
# Le marche est dans une phase de contraction
ELSE:
regime = SIDEWAYS
# Transition ou indecision
# Les signaux sont contradictoires
Cette logique est simple, robuste et eprouvee. Elle evite les whipsaws (faux signaux) en exigeant que TANT le prix que SMA50 soient du même cote de SMA200.
Le cout de la simplicite : cette regle binaire ne reagit qu’apres le croisement effectif des moyennes — le regime est confirme avec retard, car SMA200 integre 200 jours d’historique. Le haut et le bas d’un retournement sont donc toujours detectes trop tard, et la serie comptera des jours non classes pendant les transitions de croisement. Ce compromis retard/whipsaw est assume : exiger DEUX conditions (prix ET SMA50 du meme cote de SMA200) echange de la reactivite contre de la fiabilite du signal.
# Visualisation des regimes
# Calculer SMAs
df['sma50'] = df['price'].rolling(50).mean()
df['sma200'] = df['price'].rolling(200).mean()
# Detecter les regimes
def detect_regime_simple(row):
if pd.isna(row['sma50']) or pd.isna(row['sma200']):
return 'unknown'
if row['price'] > row['sma200'] and row['sma50'] > row['sma200']:
return 'bull'
elif row['price'] < row['sma200'] and row['sma50'] < row['sma200']:
return 'bear'
else:
return 'sideways'
df['regime'] = df.apply(detect_regime_simple, axis=1)
# Plot
fig, ax = plt.subplots(figsize=(16, 6))
# Couleurs par regime
colors = df['regime'].map({'bull': 'green', 'bear': 'red', 'sideways': 'gray', 'unknown': 'lightgray'})
# Prix
ax.plot(df.index, df['price'], 'k-', linewidth=0.5, alpha=0.3, label='Prix')
ax.scatter(df.index, df['price'], c=colors, s=1, alpha=0.5)
# SMAs
ax.plot(df.index, df['sma50'], 'b-', linewidth=1.5, label='SMA50', alpha=0.7)
ax.plot(df.index, df['sma200'], 'r-', linewidth=1.5, label='SMA200', alpha=0.7)
# Labels et titre
ax.set_title('Regimes de Marche - Illustration', fontsize=14, fontweight='bold')
ax.set_ylabel('Prix')
ax.legend(loc='upper left')
# Texte explicatif
ax.text(0.02, 0.98, 'Vert = Bull\\nRouge = Bear\\nGris = Sideways',
transform=ax.transAxes, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
plt.tight_layout()
plt.show()
print("\\nDistribution des regimes detectes :")
print(df['regime'].value_counts())\nDistribution des regimes detectes :regimebull 859bear 365unknown 199sideways 77Name: count, dtype: int64

Interpretation : Regimes de Marche
Sortie obtenue : Le graphique montre les 3 regimes avec des couleurs distinctes.
| Aspect | Observation |
|---|---|
| Detection | Les regimes sont identifies par la position relative du prix et des SMA |
| Bull (vert) | Prix et SMA50 au-dessus de SMA200 - tendance haussiere |
| Bear (rouge) | Prix et SMA50 en-dessous de SMA200 - tendance baissiere |
| Sideways (gris) | Transition entre les regimes, croisement des SMA |
Points cles : 1. La detection des regimes est retrospective (on connait le regime après coup) 2. Les transitions sont graduelles, pas instantanees 3. Le nombre de jours dans chaque regime depend de la periode etudiee
Note technique : Cette approche SMA50/SMA200 est simple mais efficace. Des méthodes plus avancees existent (HMM, modèles de Markov, ML), mais cette approche est robuste et interpretable.
Lecture chiffrée de la sortie : la distribution des régimes détectés sur les 1 500 jours synthétiques est bull 859, bear 365, sideways 77, avec 199 jours non classés — soit un marché haussier près des deux tiers du temps (57,3 %), cohérent avec le drift paramétré du générateur. L’exemple de données affiché (181,57 $ le 4 février 2019 → 195,68 $ le 8 février, soit +7,8 % en cinq jours) illustre la phase bull : tendance régulière sans repli. Cette répartition biaisée est un choix pédagogique assumé : observer les trois régimes tout en gardant une majorité de jours où la stratégie peut démontrer son momentum.
# Donnees synthetiques pour illustrer les regimes
# Generer un prix synthetique avec 3 regimes
np.random.seed(42)
# Parametres
n_days = 1500
dates = pd.date_range(start='2015-01-01', periods=n_days, freq='D')
# Regimes
regime_bull = slice(0, 500) # 2015-2016 : Trend haussier
regime_bear = slice(500, 900) # 2016-2017 : Trend baissier
regime_sideways = slice(900, 1500) # 2017-2019 : Range
# Generer les prix
price = np.zeros(n_days)
price[0] = 100
# Bull: drift positif + faible vol
for i in range(1, 500):
price[i] = price[i-1] * (1 + np.random.normal(0.0005, 0.01))
# Bear: drift negatif + haute vol
for i in range(500, 900):
price[i] = price[i-1] * (1 + np.random.normal(-0.001, 0.02))
# Sideways: drift zero + vol moyenne
for i in range(900, n_days):
price[i] = price[i-1] * (1 + np.random.normal(0, 0.012))
df = pd.DataFrame({'price': price}, index=dates)
print("Exemple de donnees avec 3 regimes :")
print(df.tail())Exemple de donnees avec 3 regimes : price2019-02-04 181.5717982019-02-05 185.9427972019-02-06 190.5426582019-02-07 193.3056012019-02-08 195.681086
Partie 1 : Introduction aux Regimes de Marche (8 min)
Definition des 3 regimes
Un regime de marche est une periode ou le marche exhibe des caracteriques statistiques persistantes :
1. Bull Market (Marche haussier)
- Prix au-dessus de la moyenne mobile long terme (SMA200)
- SMA50 (court terme) au-dessus de SMA200
- Volatilite typiquement faible
- Trends haussiers sustenus
2. Bear Market (Marche baissier)
- Prix en-dessous de la SMA200
- SMA50 en-dessous de SMA200
- Volatilite elevee
- Trends baissiers, panic selling
3. Sideways (Range / Transition)
- Prix autour de la SMA200
- SMA50 et SMA200 croises ou convergents
- Pas de trend clair
- Marche indecis
Pourquoi c’est important
| Aspect | Impact |
|---|---|
| Performance | Une strategy peut gagner +20% en bull et perdre -30% en bear |
| Risque | Le drawdown maximal depend du regime |
| Allocation | L’optimale differe selon le regime (100% equity en bull vs 0% en bear) |
| Psychologie | Les investisseurs se comportent differemment selon le regime |
Non-stationnarite : derriere cette classification se cache le probleme fondamental du trading quantitatif : les series de prix ne sont pas stationnaires — leur esperance, leur volatilite et leur autocorrelation changent de regime. Un modele calibre sur une periode bull (un momentum, par exemple) transporte mal ses coefficients en bear. Detecter le regime, c’est segmenter la serie en sous-populations ou les statistiques restent stables, et adapter l’outil statistique a chaque sous-population — la difference entre un backtest qui suppose un marche unique et une strategie qui reconnait que le marche a des etats.
# Imports et configuration
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Optional
import warnings
warnings.filterwarnings('ignore')
# Configuration matplotlib
plt.style.use('seaborn-v0_8-darkgrid')
plt.rcParams['figure.figsize'] = (14, 6)
plt.rcParams['font.size'] = 10
print("Market Regime Detection Notebook")
print("="*60)
print("\nCe notebook explore la detection de regimes de marche")
print("et la construction de strategies adaptatives.")Market Regime Detection Notebook============================================================Ce notebook explore la detection de regimes de marcheet la construction de strategies adaptatives.
Mode d’emploi : Ce notebook a deux parties : 1. Sections analyse/ML (pandas, numpy, matplotlib) : executables en Jupyter local 2. Sections integration QC (classes
QCAlgorithm) : code de reference a copier dansmain.pyde votre projet QC LabLes cellules QCAlgorithm sont marquees
# [REFERENCE QC]et ne sont pas executables localement.
Introduction : Pourquoi detecter les regimes de marche ?
Le problème fondamental
Les marches financiers ne sont pas stationnaires - ils traversent différents regimes avec des caracteriques distinctes :
| Regime | Caractéristiques | Stratégies efficaces |
|---|---|---|
| Bull | Trend haussier, volatilite faible | Momentum, trend following |
| Bear | Trend baissier, volatilite elevee | Defensif, cash, or |
| Sideways | Range, pas de trend clair | Mean-reversion, oscillateurs |
L’edge du RegimeSwitching
La plupart des stratégies fonctionnent bien dans UN regime mais echouent dans les autres :
- Momentum : Performe en bull, massacre en bear
- Mean-reversion : Fonctionne en sideways, sous-performe en trend
- Buy & Hold : Excellent en bull, desastreux en bear
Solution : Adapter la stratégie au regime actuel du marche.