Le cimetière des backtests parfaits
Vous avez développé une stratégie momentum qui génère un Sharpe de 2.5 en backtest sur 2019-2021. Vous la déployez en production début 2022. Six mois plus tard, votre drawdown atteint -35%. Que s'est-il passé ?
Le marché a changé de régime. La stratégie qui fonctionnait parfaitement en bull market (taux bas, liquidité abondante, momentum fort) s'est effondrée en bear market (hausse des taux, resserrement quantitatif, rotation violente). Ce n'est pas un bug — c'est le problème fondamental de toute approche statique.
73%
Stratégies statiques qui sous-performent OOS
2-3 ans
Durée moyenne d'un régime
4
Régimes de marché distincts
+40%
Gain CAGR avec adaptation
Les 4 régimes de marché
Toute la littérature académique converge vers quatre macro-régimes qui gouvernent les marchés financiers. Chaque régime modifie radicalement les caractéristiques statistiques des rendements :
Risk-On (Bull)
VIX < 18, courbe des taux positive, spreads crédit serrés. Momentum et growth dominent. CAGR typique : +15-25%.
Risk-Off (Bear)
VIX > 28, spreads crédit écartés, fuite vers la qualité. Value et défensifs surperforment. DD typique : -20-40%.
Transition
VIX 18-28, signaux mixtes, rotation sectorielle rapide. Volatilité des corrélations élevée. Le plus dangereux.
Recovery
Sortie de bear market, VIX en baisse, breadth en expansion. Small caps et cycliques explosent. Fenêtre étroite.
Les indicateurs de détection
Détecter le régime actuel requiert un faisceau d'indicateurs convergents. Aucun indicateur seul n'est suffisant — c'est leur combinaison qui génère un signal fiable :
| Indicateur | Risk-On | Risk-Off | Transition | Recovery |
| VIX (niveau) | < 16 | > 28 | 18-28 | 28 → 18 (baisse) |
| VIX Term Structure | Contango fort | Backwardation | Flat | Contango naissant |
| Yield Curve (10Y-2Y) | > +50bps | Inversée | Flat ± 25bps | Re-pentification |
| Credit Spreads (HY-IG) | < 300bps | > 500bps | 300-500bps | > 500 → 300 |
| Momentum Breadth | > 70% au-dessus SMA200 | < 30% | 30-70% | < 30 → 50%+ |
| Put/Call Ratio | < 0.7 (complaisance) | > 1.2 (panique) | 0.7-1.0 | > 1.0 → 0.8 |
| Fed Funds Rate | Stable ou en baisse | En hausse rapide | Pic/Plateau | Premières baisses |
| ISM Manufacturing | > 55 (expansion) | < 48 (contraction) | 48-52 | < 48 → 50+ |
Impact du régime sur les stratégies
Chaque famille de stratégie possède un régime d'or où elle excelle et un régime mortel où elle se fait détruire. Ignorer cela, c'est jouer à la roulette russe :
| Stratégie | Risk-On | Risk-Off | Transition | Recovery |
| Momentum cross-sectionnel | Excellent (+) | Catastrophique (crash) | Moyen | Excellent (+) |
| Mean Reversion | Faible | Bon | Excellent (+) | Faible |
| Value/Quality | Sous-performe | Défensif (ok) | Bon | Excellent (+) |
| Trend Following | Très bon | Très bon | Whipsaws | Moyen |
| Carry/Yield | Excellent (+) | Catastrophique | Moyen | Bon |
| Vol Selling | Excellent (+) | Cataclysmique | Risqué | Bon |
| Defensive/Min-Vol | Sous-performe | Excellent (+) | Bon | Sous-performe |
Pourquoi l'adaptativité est non-négociable
Sur la période 2000-2025, un portefeuille momentum statique a généré un CAGR de ~12% avec un max drawdown de -55% (2008-2009). Le même portefeuille avec un overlay de détection de régime qui réduit l'exposition en Risk-Off a généré un CAGR de ~18% avec un max drawdown de -22%. Même rendement annualisé supérieur, mais surtout un ratio rendement/risque multiplié par 3. La différence : survivre aux crashs permet de composer à plein régime pendant les phases haussières.
Le graphique ci-dessus illustre les transitions de régime depuis 2018. Observez comment les phases Risk-Off (rouges) correspondent systématiquement aux drawdowns majeurs du S&P 500. La phase de Transition (jaune) précédant chaque Risk-Off est la fenêtre critique pour réduire l'exposition.
Les états cachés du marché
Les Hidden Markov Models (HMM) sont l'outil de référence académique pour la détection de régime de marché. Le principe est élégant : le marché se trouve à chaque instant dans un état caché (le régime) que nous ne pouvons pas observer directement, mais qui génère des observations (rendements, volatilité, volume) dont les propriétés statistiques diffèrent selon l'état.
L'analogie du casino
Imaginez un casino avec deux dés truqués : un dé "favorable" (moyenne +0.5% par jour) et un dé "défavorable" (moyenne -0.2% par jour). Le croupier alterne entre les deux dés de manière aléatoire, mais vous ne voyez pas quel dé est utilisé — vous ne voyez que les résultats. Le HMM tente de deviner, à partir de la séquence des résultats observés, quel dé était utilisé à chaque instant et quelles sont les probabilités de transition entre les dés. Appliqué aux marchés : les "dés" sont les régimes, les "résultats" sont les rendements quotidiens.
Les composantes d'un HMM
Un HMM est défini par cinq éléments fondamentaux. Maîtrisez chacun d'eux pour comprendre ce que le modèle fait réellement :
N
Nombre d'états
2 (bull/bear), 3 (bull/bear/sideways) ou 4 états. 3 est le sweet spot en pratique.
A
Matrice de transition
Probabilités de passer d'un état à l'autre. P(bull→bear) ~ 2-5% par jour.
B
Distributions d'émission
Distribution des observations dans chaque état. Souvent gaussienne (moyenne, variance).
π
Distribution initiale
Probabilité d'être dans chaque état au temps t=0. Souvent uniforme ou estimée.
Implémentation : HMM à 3 états avec hmmlearn
Voici l'implémentation complète d'un détecteur de régime HMM à 3 états. Ce code est production-ready et peut être branché directement sur votre pipeline de données :
Python — regime_hmm.py
import numpy as np
import pandas as pd
from hmmlearn.hmm import GaussianHMM
from sklearn.preprocessing import StandardScaler
import warnings
warnings.filterwarnings('ignore')
class RegimeDetector:
"""
Détecteur de régime basé sur un HMM gaussien à 3 états.
États : 0=Bull (Risk-On), 1=Bear (Risk-Off), 2=Sideways (Transition)
Features utilisées :
- Rendements quotidiens (momentum court terme)
- Volatilité réalisée 21j (risque)
- Volume relatif (activité/liquidité)
- Corrélation actions/obligations 21j (risk appetite)
"""
def __init__(self, n_states=3, lookback=252,
n_iter=200, random_state=42):
self.n_states = n_states
self.lookback = lookback
self.n_iter = n_iter
self.random_state = random_state
self.model = None
self.scaler = StandardScaler()
self.state_mapping = {}
def _prepare_features(self, prices_df):
"""
Calcule les features multi-dimensionnelles à partir
d'un DataFrame avec colonnes: SPY, TLT, volume_spy
"""
features = pd.DataFrame(index=prices_df.index)
features['returns'] = np.log(
prices_df['SPY'] / prices_df['SPY'].shift(1)
)
features['realized_vol'] = (
features['returns']
.rolling(21)
.std() * np.sqrt(252)
)
features['rel_volume'] = (
prices_df['volume_spy'] /
prices_df['volume_spy'].rolling(50).mean()
)
features['spy_tlt_corr'] = (
prices_df['SPY'].pct_change()
.rolling(21)
.corr(prices_df['TLT'].pct_change())
)
features['momentum_20d'] = (
prices_df['SPY'].pct_change(20)
)
return features.dropna()
def _identify_states(self, features, states):
"""
Mappe les états HMM numériques (0,1,2) aux régimes
sémantiques (Bull, Bear, Sideways) en se basant sur
la moyenne des rendements dans chaque état.
"""
state_returns = {}
for s in range(self.n_states):
mask = states == s
state_returns[s] = features['returns'][mask].mean()
sorted_states = sorted(
state_returns.keys(),
key=lambda x: state_returns[x]
)
self.state_mapping = {
sorted_states[0]: 'Bear',
sorted_states[1]: 'Sideways',
sorted_states[2]: 'Bull',
}
return self.state_mapping
def fit(self, prices_df):
"""Entraîne le HMM sur les données historiques."""
features = self._prepare_features(prices_df)
feature_cols = [
'returns', 'realized_vol',
'rel_volume', 'spy_tlt_corr',
'momentum_20d'
]
X = self.scaler.fit_transform(features[feature_cols])
self.model = GaussianHMM(
n_components=self.n_states,
covariance_type='full',
n_iter=self.n_iter,
random_state=self.random_state,
tol=1e-4,
verbose=False,
init_params='stmc',
)
self.model.fit(X)
states = self.model.predict(X)
self._identify_states(features, states)
for state_id, regime in self.state_mapping.items():
mask = states == state_id
avg_ret = features['returns'][mask].mean() * 252
avg_vol = features['realized_vol'][mask].mean()
pct = mask.sum() / len(mask) * 100
print(f" {regime:8s}: ret={avg_ret:+.1f}%, "
f"vol={avg_vol:.1f}%, freq={pct:.0f}%")
return self
def predict_regime(self, prices_df):
"""
Prédit le régime actuel + probabilités de chaque état.
Retourne (régime, probabilités_dict)
"""
features = self._prepare_features(prices_df)
feature_cols = [
'returns', 'realized_vol',
'rel_volume', 'spy_tlt_corr',
'momentum_20d'
]
X = self.scaler.transform(features[feature_cols])
log_prob, posteriors = self.model.score_samples(X)
current_probs = posteriors[-1]
current_state = np.argmax(current_probs)
current_regime = self.state_mapping[current_state]
probs_dict = {
self.state_mapping[i]: round(p, 4)
for i, p in enumerate(current_probs)
}
return current_regime, probs_dict
def get_transition_matrix(self):
"""Retourne la matrice de transition lisible."""
if self.model is None:
raise ValueError("Modèle non entraîné")
labels = [self.state_mapping[i]
for i in range(self.n_states)]
return pd.DataFrame(
self.model.transmat_,
index=labels, columns=labels
).round(4)
Résultats typiques d'un HMM à 3 états
Après entraînement sur 10 ans de données S&P 500 (2014-2024), le HMM identifie trois régimes clairement distincts :
| Caractéristique | Bull (Risk-On) | Sideways (Transition) | Bear (Risk-Off) |
| Rendement annualisé | +22.4% | +3.8% | -28.6% |
| Volatilité annualisée | 11.2% | 16.5% | 31.8% |
| Sharpe Ratio | 2.00 | 0.23 | -0.90 |
| Durée médiane | 145 jours | 42 jours | 28 jours |
| Fréquence | 58% | 28% | 14% |
| P(rester dans l'état) | 98.2% | 95.1% | 93.7% |
| Corrélation SPY/TLT | -0.35 | -0.15 | +0.42 |
Insight clé : La corrélation SPY/TLT est un des indicateurs les plus puissants. En Risk-On, les actions et obligations sont décorrélées (diversification fonctionne). En Risk-Off, elles deviennent positivement corrélées — les deux baissent ensemble (2022). Ce changement de corrélation est un signal précurseur de transition de régime.
Limites et pièges du HMM
Les 5 pièges des HMM en finance
- Hindsight bias : L'algorithme de Viterbi utilise toute la séquence pour décoder — en temps réel, utilisez le Forward algorithm (filtrage) qui n'utilise que le passé.
- Nombre d'états : BIC/AIC peuvent aider à choisir N, mais en pratique N=3 est robuste. N>4 overfit systématiquement.
- Label switching : À chaque ré-entraînement, l'état 0 peut devenir l'état 2. Toujours re-mapper les états par rendement moyen.
- Stationnarité : Le HMM suppose des paramètres stationnaires. Ré-entraînez tous les 6-12 mois avec une fenêtre glissante.
- Faux signaux en Transition : Le HMM oscille entre Bull et Bear en période de Transition. Appliquez un filtre de persistance (rester N jours dans un état avant de le confirmer).
Filtre de persistance et lissage
En production, les probabilités brutes du HMM oscillent trop. Voici comment les lisser et ajouter un filtre de persistance pour éviter les whipsaws :
Python — regime_filter.py
class RegimeFilter:
"""
Filtre de persistance pour les signaux de régime HMM.
Évite les oscillations en imposant une durée minimale
dans un état avant de confirmer la transition.
"""
def __init__(self, min_days=5, ema_span=10,
threshold=0.6):
self.min_days = min_days
self.ema_span = ema_span
self.threshold = threshold
self.current_regime = None
self.days_in_regime = 0
self.pending_regime = None
self.pending_days = 0
def smooth_probabilities(self, probs_series):
"""Lisse les probabilités avec une EMA."""
smoothed = {}
for regime in probs_series.columns:
smoothed[regime] = (
probs_series[regime]
.ewm(span=self.ema_span, adjust=False)
.mean()
)
return pd.DataFrame(smoothed)
def update(self, regime_probs):
"""
Met à jour le régime filtré.
regime_probs: dict {'Bull': 0.7, 'Sideways': 0.2, 'Bear': 0.1}
Returns: régime confirmé (str)
"""
raw_regime = max(regime_probs, key=regime_probs.get)
raw_prob = regime_probs[raw_regime]
if self.current_regime is None:
self.current_regime = raw_regime
self.days_in_regime = 1
return self.current_regime
if raw_regime == self.current_regime:
self.days_in_regime += 1
self.pending_regime = None
self.pending_days = 0
return self.current_regime
if raw_prob >= self.threshold:
if self.pending_regime == raw_regime:
self.pending_days += 1
else:
self.pending_regime = raw_regime
self.pending_days = 1
if self.pending_days >= self.min_days:
self.current_regime = raw_regime
self.days_in_regime = self.pending_days
self.pending_regime = None
self.pending_days = 0
else:
self.pending_regime = None
self.pending_days = 0
return self.current_regime
Le filtre de persistance réduit le nombre de transitions de régime de 40-60 par an (HMM brut) à 8-12 par an (filtré), éliminant 80% des faux signaux tout en conservant 95% de la valeur ajoutée de la détection.
Du facteur linéaire au modèle non-linéaire
Dans les parties précédentes, nous avons construit des signaux alpha basés sur des combinaisons linéaires de facteurs (momentum, value, quality). Le Machine Learning permet d'aller plus loin en capturant des interactions non-linéaires entre facteurs que le cerveau humain ne peut pas identifier.
Mais attention : le ML en finance n'est pas comme le ML en computer vision ou en NLP. Le ratio signal/bruit est infiniment plus faible (IC de 0.02-0.05 vs. 0.99 en classification d'images). Cela change radicalement l'approche.
0.02-0.05
IC typique des meilleurs modèles ML
50-100
Features alpha candidates
+3-8%
CAGR additionnel vs linéaire
Mensuel
Rebalancement optimal ML
Gradient Boosting (XGBoost / LightGBM) — Le champion
Pour les données tabulaires financières, le Gradient Boosting (XGBoost, LightGBM) domine systématiquement. Ce n'est pas un hasard : les données financières sont structurées, hétérogènes (mix de features continues et catégorielles), et le gradient boosting excelle précisément dans ce contexte.
Pourquoi pas le Deep Learning ?
Les réseaux de neurones profonds brillent quand les données sont massives, homogènes et à très haute dimension (images, texte, audio). En finance quantitative, les données sont relativement petites (quelques milliers d'observations mensuelles), hétérogènes et extrêmement bruitées. Le Deep Learning overfit presque systématiquement dans ce régime de données. Les arbres de gradient boosting sont supérieurs car ils gèrent naturellement les interactions non-linéaires, sont robustes aux outliers, et nécessitent beaucoup moins de données pour converger. Exception notable : les LSTM pour le time-series forecasting et les Transformers pour le NLP financier.
Python — ml_alpha_signal.py
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import TimeSeriesSplit
import optuna
import shap
class AlphaMLModel:
"""
Modèle de signal alpha basé sur LightGBM.
Target : quintile de rendement du mois suivant (1-5)
Features : 50 alpha factors (momentum, value, quality,
sentiment, technique)
Validation : Purged K-Fold pour éviter le data leakage
Tuning : Optuna (Bayesian Optimization)
"""
def __init__(self, n_splits=5, purge_days=21,
embargo_days=5):
self.n_splits = n_splits
self.purge_days = purge_days
self.embargo_days = embargo_days
self.model = None
self.feature_names = None
self.best_params = None
def _purged_kfold(self, X, n_splits,
purge_days, embargo_days):
"""
Purged K-Fold Cross-Validation.
Élimine les observations proches de la frontière
train/test pour éviter le data leakage temporel.
"""
n = len(X)
fold_size = n // n_splits
for i in range(n_splits):
test_start = i * fold_size
test_end = min((i + 1) * fold_size, n)
train_end = max(0, test_start - purge_days)
test_start_adj = min(n, test_end + embargo_days)
train_idx = list(range(0, train_end))
if test_start_adj < n:
train_idx += list(range(test_start_adj, n))
test_idx = list(range(test_start, test_end))
if len(train_idx) > 0 and len(test_idx) > 0:
yield np.array(train_idx), np.array(test_idx)
def _create_target(self, returns_df, horizon=21):
"""
Crée le target : quintile de rendement forward.
1 = worst performers, 5 = best performers
"""
fwd_returns = returns_df.shift(-horizon)
quintiles = fwd_returns.apply(
lambda x: pd.qcut(
x.rank(method='first'),
5, labels=[1,2,3,4,5]
),
axis=1
)
return quintiles
def _objective(self, trial, X, y):
"""Objectif Optuna pour le tuning bayésien."""
params = {
'objective': 'multiclass',
'num_class': 5,
'metric': 'multi_logloss',
'verbosity': -1,
'n_estimators': trial.suggest_int(
'n_estimators', 100, 1000
),
'max_depth': trial.suggest_int(
'max_depth', 3, 8
),
'learning_rate': trial.suggest_float(
'learning_rate', 0.01, 0.1, log=True
),
'num_leaves': trial.suggest_int(
'num_leaves', 8, 64
),
'min_child_samples': trial.suggest_int(
'min_child_samples', 20, 100
),
'subsample': trial.suggest_float(
'subsample', 0.6, 1.0
),
'colsample_bytree': trial.suggest_float(
'colsample_bytree', 0.5, 0.9
),
'reg_alpha': trial.suggest_float(
'reg_alpha', 1e-3, 10.0, log=True
),
'reg_lambda': trial.suggest_float(
'reg_lambda', 1e-3, 10.0, log=True
),
}
scores = []
for train_idx, val_idx in self._purged_kfold(
X, self.n_splits, self.purge_days, self.embargo_days
):
model = lgb.LGBMClassifier(**params)
model.fit(
X.iloc[train_idx], y.iloc[train_idx],
eval_set=[(X.iloc[val_idx], y.iloc[val_idx])],
callbacks=[lgb.early_stopping(50, verbose=False)]
)
pred_proba = model.predict_proba(X.iloc[val_idx])
expected_quintile = np.sum(
pred_proba * np.array([1,2,3,4,5]), axis=1
)
from scipy.stats import spearmanr
ic, _ = spearmanr(expected_quintile, y.iloc[val_idx])
scores.append(ic)
return np.mean(scores)
def tune_and_fit(self, X, y, n_trials=100):
"""
Optimise les hyperparamètres via Optuna puis
entraîne le modèle final.
"""
self.feature_names = X.columns.tolist()
study = optuna.create_study(
direction='maximize',
sampler=optuna.samplers.TPESampler(seed=42)
)
study.optimize(
lambda trial: self._objective(trial, X, y),
n_trials=n_trials,
show_progress_bar=True
)
self.best_params = study.best_params
self.best_params['objective'] = 'multiclass'
self.best_params['num_class'] = 5
self.best_params['verbosity'] = -1
print(f"Meilleur IC moyen: {study.best_value:.4f}")
print(f"Meilleurs paramètres: {self.best_params}")
self.model = lgb.LGBMClassifier(**self.best_params)
self.model.fit(X, y)
return self
def predict_alpha(self, X):
"""
Retourne un score alpha continu (1-5) pour chaque stock.
Score élevé = alpha attendu positif.
"""
pred_proba = self.model.predict_proba(X)
alpha_score = np.sum(
pred_proba * np.array([1,2,3,4,5]), axis=1
)
return alpha_score
def explain(self, X, top_n=20):
"""
SHAP feature importance — comprendre ce que
le modèle a appris.
"""
explainer = shap.TreeExplainer(self.model)
shap_values = explainer.shap_values(X)
importance = np.mean(
[np.abs(sv).mean(axis=0) for sv in shap_values],
axis=0
)
feat_imp = pd.DataFrame({
'feature': self.feature_names,
'importance': importance
}).sort_values('importance', ascending=False)
return feat_imp.head(top_n)
Random Forest — Plus robuste, moins performant
Le Random Forest est souvent le premier choix pour un débutant en ML finance, et pour cause : il est presque impossible à faire overfitter grâce au bagging et au subsampling de features. En contrepartie, il capture moins bien les interactions complexes que le gradient boosting.
Python — random_forest_alpha.py
from sklearn.ensemble import RandomForestClassifier
class RFAlphaModel:
"""Random Forest pour alpha signal — plus robuste."""
def __init__(self):
self.model = RandomForestClassifier(
n_estimators=500,
max_depth=6,
min_samples_leaf=50,
max_features='sqrt',
n_jobs=-1,
random_state=42,
class_weight='balanced'
)
def fit_predict(self, X_train, y_train, X_test):
self.model.fit(X_train, y_train)
proba = self.model.predict_proba(X_test)
alpha = np.sum(
proba * np.arange(1, 6), axis=1
)
return alpha
def feature_importance(self):
imp = pd.DataFrame({
'feature': self.model.feature_names_in_,
'importance': self.model.feature_importances_
})
return imp.sort_values('importance', ascending=False)
Ridge / Lasso — Linéaire mais puissant
Ne sous-estimez jamais les modèles linéaires. En finance, la Ridge Regression (L2) et le Lasso (L1) restent parmi les meilleurs modèles quand le nombre de features est élevé par rapport au nombre d'observations. Le Lasso a l'avantage de la sélection automatique de features (coefficients mis à zéro).
Python — linear_alpha.py
from sklearn.linear_model import RidgeCV, LassoCV
from sklearn.preprocessing import StandardScaler
class LinearAlphaModel:
"""Modèle linéaire régularisé pour alpha signal."""
def __init__(self, method='ridge'):
self.scaler = StandardScaler()
if method == 'ridge':
self.model = RidgeCV(
alphas=np.logspace(-3, 3, 20),
cv=5
)
else:
self.model = LassoCV(
alphas=np.logspace(-4, 1, 30),
cv=5,
max_iter=10000
)
def fit_predict(self, X_train, y_train, X_test):
X_tr = self.scaler.fit_transform(X_train)
X_te = self.scaler.transform(X_test)
self.model.fit(X_tr, y_train)
alpha = self.model.predict(X_te)
return alpha
def selected_features(self):
"""Features sélectionnées (Lasso: coeff != 0)."""
coefs = pd.Series(
self.model.coef_,
index=self.feature_names
)
return coefs[coefs != 0].sort_values(
key=lambda x: x.abs(), ascending=False
)
Ensemble — La sagesse des foules
En production, les meilleurs fonds quantitatifs ne s'appuient jamais sur un seul modèle. L'ensemble de modèles réduit la variance des prédictions et est plus robuste aux changements de régime. Voici l'approche :
Python — ensemble_alpha.py
class EnsembleAlpha:
"""
Ensemble de modèles alpha : moyenne pondérée.
Poids basés sur la performance récente (IC glissant).
"""
def __init__(self, models, ic_window=6):
"""
models: dict {'lgbm': LGBMModel, 'rf': RFModel,
'ridge': RidgeModel}
ic_window: nombre de mois pour calculer IC glissant
"""
self.models = models
self.ic_window = ic_window
self.weights = {k: 1.0/len(models) for k in models}
self.ic_history = {k: [] for k in models}
def predict(self, X):
"""Prédiction ensemble pondérée."""
predictions = {}
for name, model in self.models.items():
predictions[name] = model.predict_alpha(X)
ensemble = np.zeros(len(X))
total_weight = sum(self.weights.values())
for name, pred in predictions.items():
w = self.weights[name] / total_weight
ensemble += w * pred
return ensemble
def update_weights(self, X, y_actual):
"""
Met à jour les poids basés sur la performance
récente de chaque modèle.
"""
from scipy.stats import spearmanr
for name, model in self.models.items():
pred = model.predict_alpha(X)
ic, _ = spearmanr(pred, y_actual)
self.ic_history[name].append(ic)
recent = self.ic_history[name][-self.ic_window:]
decay = np.array([0.8**i for i in range(
len(recent)-1, -1, -1
)])
avg_ic = np.average(recent, weights=decay)
self.weights[name] = max(avg_ic, 0.01)
print("Poids mis à jour:")
total = sum(self.weights.values())
for name, w in self.weights.items():
print(f" {name}: {w/total:.1%}")
L'ensemble typique en production combine 3-5 modèles avec des horizons et des familles de facteurs différents. La diversification des modèles réduit le drawdown de 15-25% par rapport au meilleur modèle individuel.
| Modèle | IC moyen | Stabilité IC | Temps entraînement | Risque overfitting | Interprétabilité |
| LightGBM | 0.045 | Moyenne | Rapide (30s) | Moyen | SHAP disponible |
| XGBoost | 0.043 | Moyenne | Modéré (2min) | Moyen | SHAP disponible |
| Random Forest | 0.038 | Haute | Modéré (1min) | Faible | Feature importance |
| Ridge Regression | 0.032 | Très haute | Instantané | Très faible | Coefficients |
| Lasso | 0.030 | Très haute | Instantané | Très faible | Feature selection |
| Ensemble (3 modèles) | 0.052 | Haute | Somme | Faible | Moyenne pondérée |