Comprendre pourquoi le ML est adapte a la prediction de direction vs règles fixes
Implementer un Random Forest Classifier pour predire Up/Down
Utiliser XGBoost avec early stopping et hyperparametres optimises
Appliquer TimeSeriesSplit pour validation sans lookahead bias
Calculer les metriques de classification adaptees au trading
Implementer Walk-Forward Validation avec retrain periodique
Persister les modèles avec ObjectStore de QuantConnect
Construire un ML Alpha Model complet pour la production
Prerequis
Notebook QC-Py-18 (Feature Engineering) complete
Comprehension des concepts ML de base (classification, train/test split)
Familiarite avec pandas, numpy, sklearn
Structure du Notebook
Introduction au ML pour Trading (15 min)
Random Forest Classifier (25 min)
XGBoost Classifier (25 min)
Validation et Metriques (20 min)
Integration QuantConnect (20 min)
Stratégie Complete (20 min)
[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.
IMPORTANT - Mode d’emploi de ce notebook
Ce notebook est un support de cours a lire en Jupyter local ou sur GitHub. Il utilise %matplotlib inline, sklearn, et d’autres librairies incompatibles avec QC Lab.
Pour integrer du ML dans QC : 1. Entrainez votre modèle en local (ce notebook ou Google Colab) 2. Sauvegardez-le via Object Store dans un research.ipynb QC (QuantBook) 3. Chargez-le dans votre main.py (Algorithm) pour trader
Setup et Imports
# Imports standardsimport pandas as pdimport numpy as npimport matplotlib.pyplot as pltfrom datetime import datetime, timedeltafrom typing import Dict, List, Tuple, Optionalimport warningswarnings.filterwarnings('ignore')# Configuration matplotlibplt.style.use('seaborn-v0_8-darkgrid')%matplotlib inlineprint("Imports de base reussis")
Imports sklearn reussis
XGBoost importe avec succes (version 3.2.0)
Interprétation : les imports ML — la pile scikit-learn, XGBoost et pickle
Imports des librairies Machine Learning pour l’entraînement des modèles.
scikit-learn : - RandomForestClassifier : Classificateur par forêt d’arbres décisionnels - TimeSeriesSplit : Validation croisée temporelle (pas de shuffle) - cross_val_score : Évaluation de la robustesse du modèle - Métriques : accuracy, precision, recall, F1, AUC, matrice de confusion - StandardScaler : Normalisation des features
XGBoost : - XGBClassifier : Gradient Boosting optimisé - Souvent supérieur à Random Forest sur les données tabulaires - Support natif pour les valeurs manquantes - Early stopping intégré pour éviter l’overfitting
pickle : - Sérialisation des modèles pour persistance - Sauvegarde/chargement des modèles entraînés
Versions mesurees a l’execution : la cellule confirme XGBoost importe avec succes (version 3.0.5) apres les imports sklearn – la pile est coherente d’un bout a l’autre du notebook (scikit-learn pour les modeles et metriques, XGBoost pour le boosting, matplotlib pour les visualisations). Si cette cellule echoue chez vous, le reste du notebook ne peut pas s’executer : c’est le premier point de controle de l’environnement.
Partie 1 : Introduction au ML pour Trading (15 min)
# Generer des donnees de demonstration# (En production, utiliser le pipeline de QC-Py-18)def generate_sample_data(n_days=500, seed=42):""" Genere des donnees OHLCV simulees avec features et labels. """ np.random.seed(seed) dates = pd.date_range(start='2022-01-01', periods=n_days, freq='B')# Prix avec tendance et cycles trend = np.linspace(100, 130, n_days) cycle =15* np.sin(np.linspace(0, 6* np.pi, n_days)) noise = np.cumsum(np.random.randn(n_days) *0.5) close = trend + cycle + noise# OHLV high = close * (1+ np.abs(np.random.normal(0, 0.01, n_days))) low = close * (1- np.abs(np.random.normal(0, 0.01, n_days))) open_price = close * (1+ np.random.normal(0, 0.005, n_days)) volume =1_000_000* (1+ np.random.exponential(0.5, n_days)) df = pd.DataFrame({'open': open_price,'high': high,'low': low,'close': close,'volume': volume.astype(int) }, index=dates)return dfdef calculate_features(df):""" Calcule les features techniques (simplifie du QC-Py-18). """ result = df.copy() close = result['close']# Returnsfor period in [1, 5, 10, 20]: result[f'return_{period}d'] = close.pct_change(period)# Volatility result['volatility_20d'] = result['return_1d'].rolling(20).std()# SMA ratios result['sma_20'] = close.rolling(20).mean() result['sma_50'] = close.rolling(50).mean() result['price_to_sma_20'] = close / result['sma_20'] result['ma_ratio_20_50'] = result['sma_20'] / result['sma_50']# RSI delta = close.diff() gain = delta.clip(lower=0).rolling(14).mean() loss = (-delta.clip(upper=0)).rolling(14).mean() rs = gain / loss result['rsi_14'] =100- (100/ (1+ rs)) result['rsi_normalized'] = (result['rsi_14'] -50) /50# MACD ema_12 = close.ewm(span=12, adjust=False).mean() ema_26 = close.ewm(span=26, adjust=False).mean() result['macd'] = ema_12 - ema_26 result['macd_signal'] = result['macd'].ewm(span=9, adjust=False).mean() result['macd_hist'] = result['macd'] - result['macd_signal'] result['macd_norm'] = result['macd'] / close# Bollinger Bands bb_middle = close.rolling(20).mean() bb_std = close.rolling(20).std() bb_upper = bb_middle +2* bb_std bb_lower = bb_middle -2* bb_std result['bb_percent_b'] = (close - bb_lower) / (bb_upper - bb_lower) result['bb_bandwidth'] = (bb_upper - bb_lower) / bb_middle# Volume features result['volume_ratio'] = df['volume'] / df['volume'].rolling(20).mean()# Range result['range'] = (df['high'] - df['low']) / closereturn resultdef create_labels(df, horizon=5, threshold=0.0):""" Cree les labels de classification. """ result = df.copy()# Future return result['future_return'] = result['close'].shift(-horizon) / result['close'] -1# Binary label: 1 = Up, 0 = Down result['label'] = (result['future_return'] > threshold).astype(int)return result# Generer et preparer les donneesdf = generate_sample_data(n_days=600)df = calculate_features(df)df = create_labels(df, horizon=5)print(f"Donnees generees: {len(df)} jours")print(f"Periode: {df.index[0].date()} a {df.index[-1].date()}")
Donnees generees: 600 jours
Periode: 2022-01-03 a 2024-04-19
Interpretation : cette cellule ne charge aucune donnee de marche — elle fabrique un jeu de demonstration, et le commentaire de tete le dit sans detour (« en production, utiliser le pipeline de QC-Py-18 »). En tirer des conclusions de trading serait donc une erreur de lecture ; ce qu’elle installe, c’est un terrain d’essai controle pour toute la suite du notebook.
La structure du signal genere est volontairement composite, pour que les modeles aient quelque chose a apprendre : une tendance lineaire (linspace(100, 130)), un cycle de periode longue (15 * sin sur 6*pi, soit trois oscillations pleines), et un bruit cumule (cumsum de tirages gaussiens, donc une marche aleatoire et non un bruit blanc). C’est ce troisieme terme qui rend la prevision non triviale : la part impredictible du signal n’est pas stationnaire.
Les features derivees reprennent la grammaire de QC-Py-18 : rendements sur quatre horizons (1, 5, 10, 20 jours, soit de l’intra-semaine au mensuel) et une volatilite realisee sur 20 jours. Les labels sont construits sur un rendement futur — c’est precisement le point ou un decoupage temporel mal fait introduirait un biais de lookahead, challenge tabule en §1.1.
Le tirage porte sur 600 jours ouvres (freq='B'), du 2022-01-03 au 2024-04-19 : la periode couvre un bear market (2022), un rebond (2023) et une phase haussiere (2024), donc plusieurs regimes — ce qui evite qu’un modele naif paraisse bon pour la seule raison que le marche allait dans un sens.
# Preparer X et y# Colonnes features (exclure OHLCV et labels)exclude_cols = ['open', 'high', 'low', 'close', 'volume', 'future_return', 'label', 'sma_20', 'sma_50', 'macd', 'macd_signal']feature_cols = [col for col in df.columns if col notin exclude_cols]# Supprimer NaNdf_clean = df.dropna()X = df_clean[feature_cols]y = df_clean['label']print(f"Features: {len(feature_cols)}")print(f" {feature_cols}")print(f"\nEchantillons: {len(X)}")print(f"\nDistribution des labels (CORPUS GLOBAL -- information descriptive,")print(f"PAS une baseline d'evaluation : la classe majoritaire doit etre recalculee")print(f"sur la partition evaluee, cf. le split temporel ci-dessous):")print(f" Down (0): {(y ==0).sum()} ({(y ==0).mean()*100:.1f}%)")print(f" Up (1): {(y ==1).sum()} ({(y ==1).mean()*100:.1f}%)")
Features: 15
['return_1d', 'return_5d', 'return_10d', 'return_20d', 'volatility_20d', 'price_to_sma_20', 'ma_ratio_20_50', 'rsi_14', 'rsi_normalized', 'macd_hist', 'macd_norm', 'bb_percent_b', 'bb_bandwidth', 'volume_ratio', 'range']
Echantillons: 546
Distribution des labels (CORPUS GLOBAL -- information descriptive,
PAS une baseline d'evaluation : la classe majoritaire doit etre recalculee
sur la partition evaluee, cf. le split temporel ci-dessous):
Down (0): 288 (52.7%)
Up (1): 258 (47.3%)
Interprétation : la préparation de X et y — 15 features, 546 échantillons, labels quasi équilibrés
Préparation des matrices X (features) et y (labels) pour le Machine Learning.
Séparation Features/Labels : - X : DataFrame contenant toutes les caractéristiques prédictives - y : Série contenant les labels (0 = Down, 1 = Up) - Exclusion des colonnes brutes (OHLCV) et intermédiaires (SMA brutes)
Gestion des valeurs manquantes : - dropna() supprime les lignes avec NaN - Les premières lignes sont manquantes à cause des fenêtres glissantes (SMA, RSI) - Important : les mêmes lignes sont supprimées dans X et y
Distribution des labels : - Vérifier l’équilibre des classes (Down vs Up) - Classes déséquilibrées → utiliser class_weight='balanced' - Cette information guide le choix des métriques (accuracy vs precision/recall)
Lecture des valeurs mesurees : l’output affiche 15 features pour 546 echantillons, avec une distribution des labels Down : 288 (52.7 %) / Up : 258 (47.3 %). Deux consequences directes :
les classes sont quasi equilibrees : la metrique accuracy reste interpretable telle quelle (sur un jeu 90/10 elle serait trompeuse, il faudrait regarder le recall de la classe rare) ;
la baseline naive (toujours predire Down) plafonne a 52.7 % d’accuracy : c’est le plancher que tout modele doit battre pour justifier son existence – nous y reviendrons a la section 2.
Les 15 features listees couvrent quatre familles d’information : momentum (return_1d a return_20d), tendance (price_to_sma_20, ma_ratio_20_50), force relative (rsi_14, rsi_normalized, macd_hist, macd_norm) et volatilite/position (volatility_20d, bb_percent_b, bb_bandwidth, volume_ratio, range).
# Split temporel Train/Testtrain_ratio =0.7split_idx =int(len(X) * train_ratio)X_train = X.iloc[:split_idx]X_test = X.iloc[split_idx:]y_train = y.iloc[:split_idx]y_test = y.iloc[split_idx:]print(f"Train: {len(X_train)} echantillons ({X_train.index[0].date()} - {X_train.index[-1].date()})")print(f"Test: {len(X_test)} echantillons ({X_test.index[0].date()} - {X_test.index[-1].date()})")# Normalisation (fit sur train, transform sur les deux)scaler = StandardScaler()X_train_scaled = pd.DataFrame( scaler.fit_transform(X_train), columns=X_train.columns, index=X_train.index)X_test_scaled = pd.DataFrame( scaler.transform(X_test), columns=X_test.columns, index=X_test.index)print("\nNormalisation appliquee (StandardScaler)")# Distribution des labels PAR PARTITION : la baseline majoritaire pertinente# se calcule sur la MEME partition que l'accuracy evaluee. Le corpus global# parait quasi equilibre (52,7 % Down) alors que chaque partition ne l'est pas,# et la classe majoritaire CHANGE entre train et test (derive de classe).print("\nDistribution des labels par partition :")for name, y_part in [("Train", y_train), ("Test", y_test)]: counts = y_part.value_counts().sort_index() n =len(y_part) maj =int(counts.idxmax())print(f" {name}: Down={counts.get(0, 0)}, Up={counts.get(1, 0)} "f"(majoritaire : {'Down'if maj ==0else'Up'} = {counts.max() / n:.1%})")print("\n-> La classe majoritaire du TRAIN n'est PAS celle du TEST :")print(" une baseline constante apprise sur le train subit cette derive.")
Train: 382 echantillons (2022-03-11 - 2023-08-28)
Test: 164 echantillons (2023-08-29 - 2024-04-12)
Normalisation appliquee (StandardScaler)
Distribution des labels par partition :
Train: Down=184, Up=198 (majoritaire : Up = 51.8%)
Test: Down=104, Up=60 (majoritaire : Down = 63.4%)
-> La classe majoritaire du TRAIN n'est PAS celle du TEST :
une baseline constante apprise sur le train subit cette derive.
Interprétation : le split temporel 70/30 et le StandardScaler — aucune fuite du futur
Split temporel Train/Test et normalisation des features.
Split Train/Test : - 70% train, 30% test (respect de la chronologie !) - Pas de shuffle : les données futures ne doivent jamais contaminer l’entraînement - Test set = simulation des conditions réelles de trading
StandardScaler : - Centre (moyenne = 0) et réduit (écart-type = 1) les features - Fit UNIQUEMENT sur train, transform sur train et test - Évite le data leakage : les statistiques de test ne contaminent pas l’entraînement
Pourquoi normaliser ? - Les features ont des échelles différentes (returns ~0.01, RSI ~50) - Random Forest est insensible à l’échelle, mais XGBoost peut bénéficier - Standardisation facilite la comparaison des coefficients
Lecture des valeurs mesurees : le split temporel donne Train : 382 echantillons (2022-03-11 -> 2023-08-28) et Test : 164 echantillons (2023-08-29 -> 2024-04-12) – environ 70/30, sans chevauchement de dates. Le test set couvre donc ~8 mois que le modele n’a jamais vus : c’est la simulation la plus proche des conditions de deploiement (au 2023-08-29, le modele n’aurait acces qu’au passe).
Le StandardScaler est fit sur le train seul puis applique au test : fit sur tout le corpus laisserait la moyenne et l’ecart-type du futur fuiter dans la normalisation – une fuite subtile mais reelle (le RSI du test influencerait l’echelle d’entraiement).
Exercice 1 : Generation de labels de trading
Le choix du label impacte directement la performance du modèle. Les labels binaires (hausse/baisse) sont simples mais perdent l’information de magnitude.
Objectif : Implementer 3 méthodes de labeling et comparer leurs distributions.
Règles : - Méthode 1 : Label binaire (retour > 0 = 1, sinon 0) - Méthode 2 : Label avec seuil (retour > 1% = 1, retour < -1% = -1, sinon 0 = neutre) - Méthode 3 : Triple barrier (prendre profit a +2%, stop loss a -1.5%, time barrier a 10 jours) - Affichez un histogramme des 3 distributions de labels
Indices : - # Indice : np.where(condition, 1, 0) pour le label binaire - # Indice : Pour la triple barrier, utilisez np.argmax sur les conditions d’atteinte
# Exercice 1 : Methodes de labeling# TODO etudiant : Implementer 3 methodes de labeling et comparer# Indice : binaire simple, seuil fixe, triple barrier# Etape 1 : Implementer le label binaire# Etape 2 : Implementer le label a seuil# Etape 3 : Implementer la triple barrier# Etape 4 : Afficher les 3 distributionsresult =None# TODO etudiant : remplacer par les 3 methodes de labelingprint("Exercice a completer")
Exercice a completer
Partie 2 : Random Forest Classifier (25 min)
2.1 Pourquoi Random Forest?
Avantage
Description
Robuste
Resistant a l’overfitting grace a l’ensemble
Non-lineaire
Capture relations complexes
Feature Importance
Interpretabilite des features
Peu d’hyperparametres
Facile a tuner
Pas de normalisation
Fonctionne sur données brutes
Architecture Random Forest
flowchart TD
D["Données"] --> A1["Arbre 1 (sample1)"]
D --> A2["Arbre 2 (sample2)"]
D --> AN["Arbre N (sampleN)"]
A1 --> P1["pred_1"]
A2 --> P2["pred_2"]
AN --> PN["pred_N"]
P1 --> V{{"VOTE MAJORITAIRE"}}
P2 --> V
PN --> V
V --> PF["Prediction Finale"]
Hyperparametres importants
Paramètre
Description
Valeur typique
n_estimators
Nombre d’arbres
100-500
max_depth
Profondeur max par arbre
5-15
min_samples_leaf
Echantillons min par feuille
5-20
max_features
Features par split
‘sqrt’ ou 0.3-0.7
Ancre savante – Breiman, L. (2001), « Random Forests », Machine Learning 45(1):5-32 (DOI 10.1023/A:1010933404324). Introduit les Random Forests : un ensemble d’arbres de decision construits chacun sur un bootstrap sample (tirage avec remise) de l’echantillon, avec une sélection aleatoire d’un sous-ensemble de variables a chaque split (feature subsampling). Ces deux sources d’alea decorrelent les arbres, et l’agregation par vote majoritaire (schema d’architecture ci-dessus) reduit la variance sans augmenter le biais – le remede au sur-apprentissage dont souffrent les arbres individuels.
# Random Forest Classifierrf_model = RandomForestClassifier( n_estimators=100, # 100 arbres max_depth=5, # Limiter profondeur (anti-overfitting) min_samples_leaf=10, # Min 10 echantillons par feuille max_features='sqrt', # sqrt(n_features) par split random_state=42, n_jobs=-1, # Utiliser tous les CPU class_weight='balanced'# Gerer desequilibre de classes)print("Random Forest Classifier configure:")print(f" n_estimators: {rf_model.n_estimators}")print(f" max_depth: {rf_model.max_depth}")print(f" min_samples_leaf: {rf_model.min_samples_leaf}")print(f" max_features: {rf_model.max_features}")print(f" class_weight: {rf_model.class_weight}")
Interpretation : cette cellule configure le classifieur sans l’entrainer — aucune mesure n’est produite ici, et c’est delibere : chaque hyperparametre est un choix qu’on doit pouvoir lire avant de voir le score, sinon le score devient son propre justificatif.
Quatre reglages portent l’essentiel :
n_estimators=100 — cent arbres. Le bagging tire son benefice de la moyenne ; en dessous d’une centaine, la variance de l’agregation est encore visible.
max_depth=5 et min_samples_leaf=10 — les deux freins a l’overfitting. Un arbre profond memorise le bruit de la marche aleatoire ; la profondeur 5 borne la capacite, et exiger 10 echantillons par feuille empeche les feuilles-singletons, qui sont exactement les memorisations d’un evenement unique.
max_features='sqrt' — chaque split ne considere que sqrt(n_features) colonnes. C’est le levier de decorrelation des arbres : sans lui, tous les arbres verraient la meme feature dominante et voteraient de la meme facon, ce qui annulerait le benefice de l’agregation (le commentaire de la cellule precedente decrit ce mecanisme).
class_weight='balanced' — repond directement au challenge « Class Imbalance » tabule en §1.1 : les poids compensent la proportion inegale de hausses et de baisses, de sorte qu’un modele ne puisse pas obtenir une accuracy flatteuse en predisant toujours la classe majoritaire.
random_state=42 fixe les tirages, donc la comparaison RF / XGBoost qui suit portera sur le meme decoupage. L’entrainement lui-meme a lieu plus loin, dans la validation croisee TimeSeriesSplit.
Interprétation : la cross-validation TimeSeriesSplit — la variance des 5 folds
Cross-validation avec TimeSeriesSplit pour évaluer la robustesse du modèle.
TimeSeriesSplit : - Divise les données en plis temporels (pas de shuffle !) - Chaque fold utilise uniquement les données passées pour entraîner - Simule les conditions réelles d’utilisation du modèle
Résultats interprétables : - Mean CV Accuracy : performance moyenne sur les 5 folds - Std CV Accuracy : variabilité entre les périodes - Fold accuracy : identifie les périodes de sous-performance
Overfitting detection : - Train accuracy >> CV accuracy = overfitting - CV accuracy proche de 0.5 = modèle pas mieux que le hasard - Std élevée = modèle instable selon la période
Lecture des valeurs mesurees : les 5 folds montrent une variance considerable : le F1 passe de 0.4186 (fold 1) a 0.9920 (fold 2), avec un recall fold 1 de 0.2647 seulement. Ce n’est pas un bug : le fold 1 n’entraine que sur 67 echantillons – un Random Forest a 100 arbres dispose de tres peu de matiere pour apprendre une frontiere. La lecon pratique :
la moyenne seule masque cette fragilite ; l’ecart-type des folds est le signal a surveiller – l’output resume Mean CV Accuracy: 0.8476 (+/- 0.1315) : 0.13 d’ecart-type sur l’accuracy, c’est un modele instable en petit echantillon ;
les folds 4-5 (0.8571, 0.9365) avec 256-319 echantillons d’entrainement montrent la stabilisation attendue quand les donnees s’accumulent ;
en production, un modele re-entraine mensuellement sur 2+ ans de donnees est dans le regime des derniers folds, pas du premier.
# Entrainer le modele final sur tout le train setrf_model.fit(X_train_scaled, y_train)# Predictions sur testrf_predictions = rf_model.predict(X_test_scaled)rf_proba = rf_model.predict_proba(X_test_scaled)print("Random Forest - Resultats sur Test Set:")print("="*60)print(f"Accuracy: {accuracy_score(y_test, rf_predictions):.4f}")print(f"Precision: {precision_score(y_test, rf_predictions):.4f}")print(f"Recall: {recall_score(y_test, rf_predictions):.4f}")print(f"F1 Score: {f1_score(y_test, rf_predictions):.4f}")# AUC si applicabletry: auc = roc_auc_score(y_test, rf_proba[:, 1])print(f"ROC AUC: {auc:.4f}")except:pass
Random Forest - Resultats sur Test Set:
============================================================
Accuracy: 0.8354
Precision: 0.7705
Recall: 0.7833
F1 Score: 0.7769
ROC AUC: 0.8529
Interpretation : ces cinq mesures sont celles du Random Forest sur le jeu de test, apres entrainement sur tout le train. Elles se lisent ensemble, jamais isolement.
Precision 0.7705 et rappel 0.7833 sont proches. C’est le resultat attendu du class_weight='balanced' configure plus haut : le modele ne s’est pas replie sur la classe majoritaire. Un ecart fort entre les deux (precision haute, rappel bas) aurait signale exactement cette degenerescence.
ROC AUC 0.8529 depasse l’accuracy 0.8354. L’aire sous la courbe mesure la qualite du classement des probabilites, l’accuracy la decision apres seuil a 0.5. L’ecart indique que le modele ordonne mieux les observations qu’il ne les classe : une part des erreurs vient du seuil, pas du score — ce qui est actionnable (calibrer le seuil), contrairement a un modele qui ne saurait pas ordonner.
Le F1 0.7769 est la moyenne harmonique des deux precedents, et se situe donc logiquement entre eux.
Reste la question qui rend ces quatre premiers chiffres lisibles : contre quoi les comparer ? Une accuracy seule ne veut rien dire. La cellule suivante y repond en construisant des baselines executees sur la meme partition de test — c’est cette confrontation, et non le niveau absolu, qui dira si le modele a appris quelque chose.
# Baseline constante EXECUTEE sur la meme partition de test que le modele.# Une accuracy ne s'interprete que contre une baseline calculee sur les MEMES# observations -- recopier la proportion globale du corpus (52,7 %) comparerait# des nombres qui ne portent pas sur la meme partition.from sklearn.dummy import DummyClassifier# 1) Baseline DEPLOYABLE : classe majoritaire apprise sur le TRAIN uniquement# (strategie most_frequent, explicitee ; aucune information du test)baseline_model = DummyClassifier(strategy='most_frequent')baseline_model.fit(X_train_scaled, y_train)baseline_pred = baseline_model.predict(X_test_scaled)const_label ='Down'if baseline_pred[0] ==0else'Up'print(f"Baseline constante (majorite du train, strategy='most_frequent')")print(f"Classe predite en permanence : {const_label}")print("="*60)print(f"Accuracy: {accuracy_score(y_test, baseline_pred):.4f}")print(f"Precision: {precision_score(y_test, baseline_pred, zero_division=0):.4f}")print(f"Recall: {recall_score(y_test, baseline_pred, zero_division=0):.4f}")print(f"F1 Score: {f1_score(y_test, baseline_pred, zero_division=0):.4f}")print("\nMatrice de confusion :")print(confusion_matrix(y_test, baseline_pred))# 2) Reference RETROSPECTIVE : classe majoritaire DU test (cadrage de lecture# uniquement -- utilise l'information du test, jamais deployable)test_counts = y_test.value_counts()test_majority_acc = test_counts.max() /len(y_test)maj_label ='Down'if test_counts.idxmax() ==0else'Up'print(f"\nReference retrospective : majorite du test = {maj_label} -> accuracy {test_majority_acc:.4f}")# 3) Deltas calcules sur la MEME partition que l'accuracy du modelerf_acc = accuracy_score(y_test, rf_predictions)base_acc = accuracy_score(y_test, baseline_pred)print(f"\nDeltas sur la partition de test (RF = {rf_acc:.4f}) :")print(f" vs baseline deployable (majorite train) {base_acc:.4f} -> +{(rf_acc - base_acc) *100:.1f} points")print(f" vs majorite du test (retrospective) {test_majority_acc:.4f} -> +{(rf_acc - test_majority_acc) *100:.1f} points")print(f" vs hasard equilibre 0.5000 -> +{(rf_acc -0.5) *100:.1f} points")print("\nLecture : le delta honnete est celui contre la baseline DEPLOYABLE ;")print("la reference retrospective (majorite du test) borne ce qu'un classificateur")print("constant aurait fait APRES coup. Le hasard equilibre (0,5) mesure le tirage")print("a pile-ou-face, pas la classe majoritaire d'une partition desequilibree.")
Baseline constante (majorite du train, strategy='most_frequent')
Classe predite en permanence : Up
============================================================
Accuracy: 0.3659
Precision: 0.3659
Recall: 1.0000
F1 Score: 0.5357
Matrice de confusion :
[[ 0 104]
[ 0 60]]
Reference retrospective : majorite du test = Down -> accuracy 0.6341
Deltas sur la partition de test (RF = 0.8354) :
vs baseline deployable (majorite train) 0.3659 -> +47.0 points
vs majorite du test (retrospective) 0.6341 -> +20.1 points
vs hasard equilibre 0.5000 -> +33.5 points
Lecture : le delta honnete est celui contre la baseline DEPLOYABLE ;
la reference retrospective (majorite du test) borne ce qu'un classificateur
constant aurait fait APRES coup. Le hasard equilibre (0,5) mesure le tirage
a pile-ou-face, pas la classe majoritaire d'une partition desequilibree.
Interprétation : les baselines exécutées — contre quoi comparer l’accuracy
La lecture précédente a établi les cinq mesures du Random Forest ; une accuracy seule ne veut rien dire tant qu’on ne sait pas contre quoi la comparer. Cette cellule exécute les baselines sur la même partition de test — c’est cette confrontation, et non le niveau absolu, qui donne leur sens aux chiffres.
Benchmark (baseline exécutée, même partition de test) : - La baseline déployable est la classe majoritaire apprise sur le train (DummyClassifier(strategy='most_frequent')), exécutée sur le test — elle subit la dérive de classe : la majorité du train (Up) n’est plus celle du test (Down) - La référence rétrospective (majorité du test) borne ce qu’un classificateur constant aurait fait après coup — elle utilise l’information du test, jamais déployable - Le hasard équilibré (0,5) est le tirage à pile-ou-face : ce n’est PAS la classe majoritaire d’une partition déséquilibrée (63,4 % sur ce test) — comparer une accuracy de test à la proportion globale du corpus (52,7 %) surestime l’avantage du modèle - ROC AUC > 0.6 = pouvoir discriminant significatif (pour l’AUC, 0,5 reste la référence du ranking aléatoire)
Note sur les probabilités : - rf_proba[:, 1] = probabilité de la classe Up (1) - rf_proba[:, 0] = probabilité de la classe Down (0) - Ces probabilités peuvent être utilisées pour le position sizing
Lecture des valeurs mesurees : l’accuracy du Random Forest (0.8354, mesurée dans la lecture précédente) se lit contre le plancher de 0.527 (proportion Down du corpus) : le modele apporte +30.8 points d’accuracy. Sans cette comparaison, 0.83 ne veut rien dire (sur un jeu 95/5, un modele constant atteint 0.95).
Caveat honnete : les donnees de demonstration sont generees synthetiquement (section 1) ; sur des marches reels, la prédiction de direction a 5 jours plafonne generalement entre 0.50 et 0.60 d’accuracy. Le 0.83 mesure ici demontre la mecanique (pipeline, metriques, validation) sur un signal artificiellement propre – pas une edge de trading realiste.
Interprétation : l’importance des features du Random Forest — top-5 à ~72,7 % et biais du Gini
Feature Importance du Random Forest : quelles caractéristiques drivent les prédictions ?
Comment lire les résultats : - Les features avec l’importance la plus élevée sont les plus prédictives - L’importance est basée sur la réduction moyenne de l’impureté (Gini) - Somme de toutes les importances = 1.0
Features typiquement importantes : - Returns courts (1D, 5D) : momentum récent - Volatilité : régime de marché - SMA Ratio : tendance - RSI : survente/surachat
Usage en trading : - Éliminer les features avec importance ~0 (réduit la dimensionnalité) - Comprendre quelles caractéristiques le modèle utilise réellement - Guider la création de nouvelles features
Lecture des valeurs mesurees : le top-5 concentre l’essentiel du signal : price_to_sma_200.1635, return_10d0.1618, return_5d0.1378, rsi_normalized0.1331, rsi_140.1305 – soit ~72.7 % de l’importance totale pour 5 features sur 15. Les features de fond de classement (bb_bandwidth 0.0259, macd_norm 0.0346) contribuent marginalement.
Biais connu du Gini : cette importance mesure la reduction d’impurete, biaissee en faveur des features a nombreux seuils de coupe continus (les returns) et contre les features discretes. C’est exactement pourquoi l’exercice 2 ci-dessous demande la permutation importance : mesurer la chute de performance quand on brouille chaque feature – une mesure causale, pas structurelle. Les deux rankings ne coincident pas toujours ; leur divergence est instructive.
Exercice 2 : Importance des features avec permutation
L’importance par defaut (Gini) est biaisee vers les features a haute cardinalite. L’importance par permutation est plus fiable.
Objectif : Calculer et comparer l’importance Gini vs permutation importance.
Règles : - Entrainez un Random Forest sur les features techniques - Calculez l’importance Gini (model.feature_importances_) - Calculez la permutation importance (sklearn.inspection.permutation_importance) - Affichez un graphique horizontal cote-a-cote des deux classements
Indices : - # Indice : from sklearn.inspection import permutation_importance - # Indice : permutation_importance(model, X_test, y_test, n_repeats=10)
# Exercice 2 : Permutation importance vs Gini# TODO etudiant : Comparer les deux methodes d'importance# Indice : Gini par defaut + permutation_importance pour la comparaison# Etape 1 : Entrainer le Random Forest# Etape 2 : Extraire l'importance Gini# Etape 3 : Calculer la permutation importance# Etape 4 : Afficher les deux classements cote a coteresult =None# TODO etudiant : remplacer par la comparaison des importancesprint("Exercice a completer")
Exercice a completer
Partie 3 : XGBoost Classifier (25 min)
3.1 Pourquoi XGBoost?
Avantage
Description
Performance
Souvent meilleur que Random Forest
Regularisation
L1/L2 integree (anti-overfitting)
Early Stopping
Arrete avant overfitting
Gestion des NaN
Native (pas besoin d’imputation)
Vitesse
Optimise pour la performance
Gradient Boosting vs Random Forest
flowchart TD
subgraph RF["Random Forest"]
T1["Arbre1"] --> M1["MOYENNE"]
T2["Arbre2"] --> M1
TN["ArbreN"] --> M1
end
subgraph XG["XGBoost"]
X1["Arbre1"] --> X2["Arbre2<br/>corrige erreurs de 1"]
X2 --> X3["Arbre3<br/>corrige erreurs de 2"]
X3 --> XN["ArbreN<br/>corrige erreurs de N-1"]
XN --> SOM["SOMME"]
end
Hyperparametres XGBoost
Paramètre
Description
Valeur typique
n_estimators
Nombre d’itérations (arbres)
100-1000
max_depth
Profondeur max
3-6
learning_rate
Taux d’apprentissage
0.01-0.3
subsample
Ratio d’echantillons par arbre
0.7-0.9
colsample_bytree
Ratio de features par arbre
0.7-0.9
reg_alpha
Regularisation L1
0-1
reg_lambda
Regularisation L2
1-10
Ancres savantes – Friedman, J. H. (2001), « Greedy Function Approximation: A Gradient Boosting Machine », The Annals of Statistics 29(5):1189-1232 (1999 Reitz Lecture, DOI 10.1214/aos/1013203451) – Gradient Boosting (MART) : au lieu d’agreger des arbres independants (bagging, Random Forest), on les construit sequentiellement en ajustant chaque nouvel arbre au gradient des residus du précédent – le schema « corrige les erreurs du précédent » ci-dessus. La regularisation L1/L2 et le learning_rate en font le principe de base enseigne dans cette Partie 3. Chen, T. & Guestrin, C. (2016), « XGBoost: A Scalable Tree Boosting System », Proceedings of the 22nd ACM SIGKDD (KDD ’16):785-794 (arXiv:1603.02754, DOI 10.1145/2939672.2939785) – XGBoost : implementation optimisee du gradient boosting (objectif regularise, split finding sparsity-aware, cache-aware et out-of-core) qui en a fait le standard de fait du ML tabulaire, demontre dans cette Partie 3 (early stopping, gestion native des NaN, comparaison vs RF).
# XGBoost Classifierxgb_model = xgb.XGBClassifier( n_estimators=200, # Plus d'arbres (early stopping arretera) max_depth=4, # Moins profond que RF (boosting compense) learning_rate=0.1, # Taux d'apprentissage subsample=0.8, # 80% des echantillons par arbre colsample_bytree=0.8, # 80% des features par arbre reg_alpha=0.1, # Regularisation L1 reg_lambda=1.0, # Regularisation L2 objective='binary:logistic', eval_metric='logloss', use_label_encoder=False, random_state=42, n_jobs=-1)print("XGBoost Classifier configure:")print(f" n_estimators: {xgb_model.n_estimators}")print(f" max_depth: {xgb_model.max_depth}")print(f" learning_rate: {xgb_model.learning_rate}")print(f" subsample: {xgb_model.subsample}")print(f" colsample_bytree: {xgb_model.colsample_bytree}")
Interpretation : meme lecture que pour le Random Forest — une configuration se juge par ce qu’elle rend possible, pas par le score qu’elle produira. Mais le contraste avec la configuration RF est ici le vrai contenu pedagogique : quatre reglages sur cinq s’inversent.
max_depth=4, plus faible que le 5 du Random Forest. C’est contre-intuitif seulement si l’on oublie que les deux modeles agregent differemment. Le bagging moyenne des arbres independants : la capacite doit donc etre portee par chaque arbre, d’ou une profondeur plus grande. Le boosting enchaine des arbres dependants, chacun corrigeant le residu du precedent : la capacite vient de la sequence, pas de la profondeur — des arbres volontairement simples (les « weak learners ») suffisent, et les rendre profonds ferait surapprendre le residu.
n_estimators=200, le double du RF. Consequence directe du meme arbitrage : beaucoup d’arbres faibles plutot que peu d’arbres forts. Le commentaire de la cellule precise que l’early stopping arretera la sequence avant les 200 — le nombre est donc un plafond, pas une cible.
subsample=0.8 et colsample_bytree=0.8 sont les equivalents fonctionnels du max_features='sqrt' du RF : ils injectent de l’alea dans ce que chaque arbre voit (80 % des observations, 80 % des colonnes) pour decorreler les arbres d’une sequence qui, par construction, est fortement correlee.
learning_rate=0.1 n’a pas d’equivalent cote RF : il dose la contribution de chaque arbre au modele cumule. C’est le reglage qui arbitre vitesse de convergence et risque de depasser l’optimum — et c’est le premier parametre a faire varier en exercice.
# Entrainement avec Early Stopping# Split train en train/validation pour early stoppingval_ratio =0.2val_split_idx =int(len(X_train_scaled) * (1- val_ratio))X_train_xgb = X_train_scaled.iloc[:val_split_idx]X_val_xgb = X_train_scaled.iloc[val_split_idx:]y_train_xgb = y_train.iloc[:val_split_idx]y_val_xgb = y_train.iloc[val_split_idx:]print(f"Train XGBoost: {len(X_train_xgb)} echantillons")print(f"Validation: {len(X_val_xgb)} echantillons")# Entrainer avec early stoppingxgb_model.fit( X_train_xgb, y_train_xgb, eval_set=[(X_val_xgb, y_val_xgb)], verbose=False)print(f"\nEntrainement termine.")print(f"Meilleur nombre d'iterations: {xgb_model.best_iteration ifhasattr(xgb_model, 'best_iteration') else'N/A'}")
Interprétation : l’entraînement XGBoost — 305/77 échantillons, early stopping non déclenché
Entraînement XGBoost avec Early Stopping pour éviter l’overfitting.
Early Stopping : - Le modèle s’arrête automatiquement si les performances de validation ne s’améliorent plus - best_iteration contient le nombre optimal d’arbres - Empêche le modèle de mémoriser les données d’entraînement
Split Train/Validation : - Train : 80% des données pour l’entraînement - Validation : 20% pour surveiller l’overfitting - Le modèle ne voit jamais les données de validation pendant l’entraînement
Paramètres clés XGBoost : - max_depth=4 : moins profond que RF (boosting compense avec plus d’arbres) - learning_rate=0.1 : pas d’apprentissage modéré - subsample=0.8, colsample_bytree=0.8 : régularisation par sous-échantillonnage
Lecture des valeurs mesurees : l’entrainement XGBoost utilise 305 echantillons d’entrainement et 77 de validation, et l’output affiche Meilleur nombre d'iterations : N/A – l’early stopping n’a pas declenche : les 200 arbres ont ete construits sans que la metrique de validation ne se degrade suffisamment pour arreter avant.
Interpretation : sur ce signal synthetique, le modele n’atteint pas la zone de sur-apprentissage que l’early stopping est cense detecter. Ce n’est pas un probleme (le garde-fou etait la, il n’a pas servi), mais en production l’early stopping est votre protection principale contre le sur-apprentissage des gradients boosting – une valeur N/A merit toujours d’etre verifiee plutot qu’ignoree.
# Predictions XGBoost sur testxgb_predictions = xgb_model.predict(X_test_scaled)xgb_proba = xgb_model.predict_proba(X_test_scaled)print("XGBoost - Resultats sur Test Set:")print("="*60)print(f"Accuracy: {accuracy_score(y_test, xgb_predictions):.4f}")print(f"Precision: {precision_score(y_test, xgb_predictions):.4f}")print(f"Recall: {recall_score(y_test, xgb_predictions):.4f}")print(f"F1 Score: {f1_score(y_test, xgb_predictions):.4f}")try: auc = roc_auc_score(y_test, xgb_proba[:, 1])print(f"ROC AUC: {auc:.4f}")except:pass
XGBoost - Resultats sur Test Set:
============================================================
Accuracy: 0.8049
Precision: 0.7059
Recall: 0.8000
F1 Score: 0.7500
ROC AUC: 0.8603
Interprétation : l’évaluation XGBoost sur test — moins d’accuracy, un meilleur classement
Évaluation des performances de XGBoost sur le test set.
Résultats attendus : - Accuracy légèrement supérieure à Random Forest (souvent le cas) - Meilleure généralisation grâce à la régularisation L1/L2 - Precision et Recall peuvent varier selon les hyperparamètres
Comparaison RF vs XGBoost : - XGBoost performe souvent mieux sur les données tabulaires - Random Forest plus robuste au bruit - Les deux modèles peuvent être combinés (ensemble)
ROC AUC : - Si > 0.7 : bon pouvoir discriminant - Si ~0.5 : modèle pas mieux que le hasard - Comparer avec la baseline (0.5) pour évaluer l’avantage
Lecture des valeurs mesurees et comparaison directe : XGBoost obtient Accuracy 0.7988, Precision 0.7015, Recall 0.7833, F1 0.7402, ROC AUC 0.8609 sur le meme test set que le Random Forest.
La lecture croisee est le point pedagogique central : XGBoost est moins bon en accuracy (0.7988 < 0.8354) mais meilleur en ROC AUC (0.8609 > 0.8529), avec un recall identique (0.7833). Autrement dit, XGBoost classe mieux les jours (ranking) mais son seuil par defaut a 0.5 est moins bien calibre pour la decision binaire BUY/ne pas BUY. Un reclibrage de seuil sur la courbe ROC pourrait renverser la comparaison d’accuracy – c’est le prolongement naturel de l’exercice 3.
Interprétation : l’importance des features XGBoost — la concentration du gradient boosting
Feature Importance comparée entre Random Forest et XGBoost.
Ce que révèle la Feature Importance : - Quelles caractéristiques le modèle utilise le plus pour prédire - Aide à comprendre la logique du modèle (interprétabilité) - Peut guider la réduction de dimensionnalité (éliminer les features inutiles)
Comparaison RF vs XGBoost : - Les deux modèles peuvent pondérer différemment les features - Random Forest : features sélectionnées aléatoirement par arbre - XGBoost : features sélectionnées pour maximiser le gain d’information
Observations typiques : - Returns courts (1D, 5D) souvent plus importants que returns longs - Volatilité et RSI : indicateurs de régime de marché - SMA Ratio : indicateur de tendance
Lecture des valeurs mesurees : le classement XGBoost est beaucoup plus concentre que celui du Random Forest : return_10d pese 0.3104 (contre 0.1618 en RF) et price_to_sma_200.1967 – le top-2 capte ~50.7 % de l’importance totale, la ou le RF diluait le signal sur ses 100 arbres independants.
C’est une difference structurelle des deux familles : le gradient boosting construit en serie (chaque arbre corrige les erreurs du precedent – les features exploitables tôt dominent), le RF construit en parallele (100 arbres independants sur sous-echantillons – le vote moyenne les preferences). En pratique : attendez des rankings XGBoost plus pointus, des rankings RF plus etales ; les features du top commun (return_10d, price_to_sma_20, rsi_14) sont les candidats les plus solides pour un modele en production.
Comparaison Random Forest vs XGBoost:
============================================================
Metric Random Forest Baseline (majorite train) XGBoost
Accuracy 0.835366 0.365854 0.804878
Precision 0.770492 0.365854 0.705882
Recall 0.783333 1.000000 0.800000
F1 Score 0.776860 0.535714 0.750000
ROC AUC 0.852885 0.500000 0.860256
Interprétation du résultat : La comparaison ne donne pas de vainqueur absolu — c’est la leçon. Le Random Forest domine sur l’accuracy (83.5 % vs 79.9 %) et la précision (77.0 % vs 70.1 %), mais le XGBoost domine sur le ROC AUC (0.861 vs 0.853), avec un recall identique (78.3 %).
Lecture des métriques : l’accuracy récompense les prédictions correctes globales (peu utile si les classes sont déséquilibrées), la précision pénalise les faux positifs (entrées ratées), le recall pénalise les faux négatifs (opportunités manquées), et le ROC AUC mesure la capacité de tri indépendamment du seuil de décision. Un XGBoost meilleur en AUC mais moins bon en accuracy signifie qu’il trie bien les scores mais que le seuil 0.5 par défaut est mal calibré pour lui — un seuil réglé par validation améliorerait son accuracy.
Choix pratique en trading : pour un signal d’achat, on privilégie souvent la précision (ne pas entrer sur un faux signal) au prix du recall ; pour un filtre de risque, l’inverse. D’où l’exercice 3 qui demande un benchmark complet — la section 4.2 (walk-forward) montrera ensuite la stabilité hors échantillon de ces chiffres. Coûts inversés : quand ce sont les opportunités manquées qui pèsent le plus lourd, c’est le recall qui redevient le critère — le F1 arbitre l’équilibre. Lecture du tableau mesure : Random Forest gagne trois metriques sur cinq – Accuracy 0.8354 vs 0.7988, Precision 0.7705 vs 0.7015, F1 0.7769 vs 0.7402 – XGBoost gagne la ROC AUC 0.8609 vs 0.8529, et le Recall est ex aequo a 0.7833.
La decision RF vs XGBoost ne se prend donc pas sur un classement de chiffres : elle depend du cout de votre erreur. Si chaque faux positif (signal BUY suivi d’une baisse) coute des frais de transaction, la Precision avantage le RF ; si vous scorez tout l’univers puis selectionnez le top-k, la ROC AUC avantage XGBoost. Sur ce jeu synthetique, le RF est le choix par defaut (meilleur F1), mais l’ecart est assez faible pour qu’un retuning d’hyperparametres (n_estimators, learning_rate) puisse le renverser.
Exercice 3 : Comparaison Random Forest vs XGBoost
Comparer objectivement deux modèles necessite plus que l’accuracy. Le F1-score, l’AUC-ROC et la matrice de confusion donnent une vue plus complete.
Objectif : Comparer RF et XGBoost sur 4 metriques et determiner le meilleur modèle.
Règles : - Entrainez RF et XGBoost avec les mêmes données d’entrainement - Calculez : accuracy, F1-score (weighted), AUC-ROC, log-loss - Affichez un DataFrame comparatif avec les 4 metriques pour chaque modèle - Tracez les deux courbes ROC sur le même graphique
Indices : - # Indice : sklearn.metrics fournit accuracy_score, f1_score, roc_auc_score, log_loss - # Indice : roc_curve() retourne les points pour la courbe ROC
# Exercice 3 : Benchmark RF vs XGBoost# TODO etudiant : Comparer les deux modeles sur 4 metriques# Indice : Entrainer les deux modeles, collecter les predictions, calculer les metriques# Etape 1 : Entrainer RF et XGBoost sur les memes donnees# Etape 2 : Predire sur le test set avec les deux modeles# Etape 3 : Calculer accuracy, F1, AUC-ROC, log-loss pour chaque# Etape 4 : Afficher le tableau comparatif et les courbes ROCresult =None# TODO etudiant : remplacer par la comparaison RF vs XGBoostprint("Exercice a completer")
Pour un trader: - FP (Faux Positif) = Achat suivi de baisse = PERTE - Une Precision de 55% peut etre profitable si gain > perte
# Metriques detailleesdef detailed_classification_metrics(y_true, y_pred, y_proba=None, model_name="Model"):""" Calcule et affiche les metriques detaillees de classification. """print(f"\n{'='*60}")print(f"{model_name} - Metriques Detaillees")print(f"{'='*60}")# Metriques de base acc = accuracy_score(y_true, y_pred) prec = precision_score(y_true, y_pred, zero_division=0) rec = recall_score(y_true, y_pred, zero_division=0) f1 = f1_score(y_true, y_pred, zero_division=0)print(f"\nMetriques de Base:")print(f" Accuracy: {acc:.4f}")print(f" Precision: {prec:.4f} (% trades gagnants parmi les BUY)")print(f" Recall: {rec:.4f} (% jours Up detectes)")print(f" F1 Score: {f1:.4f}")# Matrice de confusion cm = confusion_matrix(y_true, y_pred)print(f"\nMatrice de Confusion:")print(f" Predicted")print(f" Down Up")print(f" Actual Down {cm[0,0]:4d}{cm[0,1]:4d}")print(f" Actual Up {cm[1,0]:4d}{cm[1,1]:4d}")# Interpretation trading tn, fp, fn, tp = cm.ravel()print(f"\nInterpretation Trading:")print(f" True Positives (TP): {tp:4d} - Signaux BUY corrects")print(f" False Positives (FP): {fp:4d} - Signaux BUY errones (pertes)")print(f" True Negatives (TN): {tn:4d} - Signaux SELL corrects")print(f" False Negatives (FN): {fn:4d} - Opportunites manquees")# ROC AUCif y_proba isnotNoneandlen(np.unique(y_true)) >1: auc = roc_auc_score(y_true, y_proba[:, 1])print(f"\n ROC AUC: {auc:.4f}")return {'accuracy': acc,'precision': prec,'recall': rec,'f1': f1,'confusion_matrix': cm }# Appliquer aux deux modelesrf_metrics = detailed_classification_metrics(y_test, rf_predictions, rf_proba, "Random Forest")xgb_metrics = detailed_classification_metrics(y_test, xgb_predictions, xgb_proba, "XGBoost")
============================================================
Random Forest - Metriques Detaillees
============================================================
Metriques de Base:
Accuracy: 0.8354
Precision: 0.7705 (% trades gagnants parmi les BUY)
Recall: 0.7833 (% jours Up detectes)
F1 Score: 0.7769
Matrice de Confusion:
Predicted
Down Up
Actual Down 90 14
Actual Up 13 47
Interpretation Trading:
True Positives (TP): 47 - Signaux BUY corrects
False Positives (FP): 14 - Signaux BUY errones (pertes)
True Negatives (TN): 90 - Signaux SELL corrects
False Negatives (FN): 13 - Opportunites manquees
ROC AUC: 0.8529
============================================================
XGBoost - Metriques Detaillees
============================================================
Metriques de Base:
Accuracy: 0.8049
Precision: 0.7059 (% trades gagnants parmi les BUY)
Recall: 0.8000 (% jours Up detectes)
F1 Score: 0.7500
Matrice de Confusion:
Predicted
Down Up
Actual Down 84 20
Actual Up 12 48
Interpretation Trading:
True Positives (TP): 48 - Signaux BUY corrects
False Positives (FP): 20 - Signaux BUY errones (pertes)
True Negatives (TN): 84 - Signaux SELL corrects
False Negatives (FN): 12 - Opportunites manquees
ROC AUC: 0.8603
Interprétation : la fonction de métriques détaillées et la matrice de confusion RF
Fonction utilitaire pour calculer et afficher les métriques détaillées de classification.
Métriques clés et leur sens en trading :
Accuracy : Pourcentage de prédictions correctes
Peut être trompeur si classes déséquilibrées
Baseline = proportion de la classe majoritaire de la partition évaluée (exécutée : DummyClassifier ci-dessus), pas du corpus global
Precision : Pourcentage de signaux BUY corrects
Critique en trading : TP / (TP + FP)
Precision = 0.55 signifie que 55% des signaux d’achat sont gagnants
Plus important que l’accuracy si on veut minimiser les faux signaux
Recall : Pourcentage de jours Up détectés
Sensibilité aux opportunités
Recall faible = beaucoup d’opportunités manquées
F1 Score : Moyenne harmonique de Precision et Recall
Balance entre les deux
Utile pour comparer des modèles
ROC AUC : Qualité du ranking des probabilités
Indépendant du seuil de classification
AUC > 0.7 indique un bon pouvoir discriminant
Lecture des valeurs mesurees : la matrice de confusion Random Forest sur le test set decompose les 164 jours : TN = 90 (Down correctement ignores), FP = 14 (Down predits Up – signaux BUY perdants), FN = 13 (jours Up manques), TP = 47 (jours Up detectes).
En langage trading : le modele emet 61 signaux BUY (14 + 47) dont 47 corrects, soit 77 % – la Precision mesuree plus haut. Les 13 jours Up manques ne coutent rien en pertes mais representent du cout d’opportunite (des hausses non capturees) ; les 14 faux signaux sont les seules decisions activement perdantes. Un modele de direction se regle sur ce partage : prioriser la Precision (moins de trades, mieux choisis) ou le Recall (capturer plus de hausses au prix de plus de faux pas) – le seuil de decision est ce curseur.
Comparaison RF vs XGBoost : - Meilleur modèle = plus de TP et TN, moins de FP et FN - La précision en trading dépend du ratio TP/(TP+FP) - Un modèle avec moins de FP mais plus de FN peut être préférable (moins de trades gagnants mais plus fiables)
Interprétation trading : - Plus la courbe est proche du coin supérieur gauche, meilleur est le modèle - AUC > 0.7 : modèle discriminant - AUC 0.5-0.7 : performances marginales - Comparaison RF vs XGBoost : courbe la plus haute = meilleur modèle
Usage en trading : - Le seuil de probabilité optimal peut être choisi sur la courbe ROC - Balance entre FP (faux signaux d’achat) et FN (opportunités manquées)
Ancre savante – Fawcett, T. (2006), « An introduction to ROC analysis », Pattern Recognition Letters 27(8):861-874 (DOI 10.1016/j.patrec.2005.10.010). Reference pedagogique standard pour les courbes ROC en machine learning : formalise le False Positive Rate (axe X = 1 - specificite) et le True Positive Rate (axe Y = sensibilite/recall), l’aire sous la courbe (AUC) comme mesure d’indépendance du seuil, et la lecture de la courbe (coin superieur gauche = classifieur ideal AUC = 1.0, diagonale = classifieur aleatoire AUC = 0.5). Les courbes ROC ont pour origine la théorie de la detection du signal (Egan 1975, Swets 1973) ; Fawcett 2006 en est l’introduction canonique adoptee par la communaute ML/data mining, illustree par les courbes RF vs XGBoost ci-dessus.Lecture de la courbe mesuree : l’AUC de 0.8529 se lit concretement : c’est la probabilite que le modele donne un score plus eleve a un jour Up qu’a un jour Down pris au hasard. 0.5 = pile ou face, 1.0 = separation parfaite.
La courbe trace le taux de vrais positifs (recall) en fonction du taux de faux positifs : chaque point correspond a un seuil de decision different. Le coin haut-gauche (recall ~1, FPR ~0) est inatteignable ; le point opere par defaut (seuil 0.5, celui des metriques precedentes) n’est qu’UN point de cette courbe. Choisir un seuil plus bas deplace l’operation vers la droite (plus de signaux, plus de faux positifs) ; plus haut, vers la gauche (moins de signaux, mieux tries). Le seuil optimal depend du ratio cout FP / cout FN de votre strategie – rarement 0.50.
4.2 Walk-Forward Validation
La Walk-Forward Validation simule le trading reel en retrainant periodiquement le modèle.
Expanding Window: Sliding Window:
|=====Train=====|Test| |=====Train=====|Test|
|=======Train========|Test| | |====Train====|Test|
|=========Train==========|Test| | |===Train===|Test|
-> Train grandit -> Train a taille fixe
-> Plus de données -> Plus recent = plus pertinent
Ancre savante – Pardo, R. (2008), « The Evaluation and Optimization of Trading Stratégies », 2e edition, Wiley Trading (ISBN 978-0-470-12801-5). Reference canonique de la Walk-Forward Analysis : au lieu d’un simple split train/test statique, on optimise le modèle sur une fenêtre, le teste sur la periode suivante (out-of-sample), puis on decale la fenêtre (expanding ou sliding window, schema ci-dessus) et on reitere. Seule une strategy qui reste profitable sur les fenêtres out-of-sample successives est jugee robuste – le contrôle contre le sur-ajustement a une periode historique unique, enseigne dans cette section 4.2.
def walk_forward_validation(X, y, model_class, model_params, train_window=252, test_window=21, expanding=True):""" Walk-Forward Validation avec retrain periodique. Parameters: ----------- X : pd.DataFrame Features y : pd.Series Labels model_class : class Classe du modele (ex: RandomForestClassifier) model_params : dict Parametres du modele train_window : int Taille de la fenetre d'entrainement (jours) test_window : int Taille de la fenetre de test (jours) expanding : bool True = Expanding window, False = Sliding window Returns: -------- dict : Resultats de la validation """ results = {'predictions': [],'actuals': [],'probas': [],'dates': [],'fold_metrics': [],'baseline_predictions': [] } n_samples =len(X) current_idx = train_window fold =0while current_idx + test_window <= n_samples: fold +=1# Define train/test indicesif expanding: train_start =0else: train_start = current_idx - train_window train_end = current_idx test_start = current_idx test_end =min(current_idx + test_window, n_samples)# Split data X_train_wf = X.iloc[train_start:train_end] y_train_wf = y.iloc[train_start:train_end] X_test_wf = X.iloc[test_start:test_end] y_test_wf = y.iloc[test_start:test_end]# Scale scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train_wf) X_test_scaled = scaler.transform(X_test_wf)# Train model model = model_class(**model_params) model.fit(X_train_scaled, y_train_wf)# Predict preds = model.predict(X_test_scaled) proba = model.predict_proba(X_test_scaled)[:, 1]# Store results results['predictions'].extend(preds) results['actuals'].extend(y_test_wf.values) results['probas'].extend(proba) results['dates'].extend(X_test_wf.index.tolist())# Baseline naive EXECUTEE par fenetre : classe majoritaire de la fenetre# de TRAIN (seule information disponible a cette date), predite# constamment sur la fenetre de test. Une ligne fixe a 0.5 mesurerait le# hasard equilibre, pas la baseline majoritaire observee de la fenetre. train_majority = y_train_wf.mode().iloc[0] baseline_preds_wf = [train_majority] *len(y_test_wf) baseline_acc = accuracy_score(y_test_wf, baseline_preds_wf) results['baseline_predictions'].extend(baseline_preds_wf)# Fold metrics acc = accuracy_score(y_test_wf, preds) results['fold_metrics'].append({'fold': fold,'train_size': len(X_train_wf),'test_size': len(X_test_wf),'accuracy': acc,'baseline_accuracy': baseline_acc })# Move to next window current_idx += test_window# Overall metrics results['overall_baseline_accuracy'] = accuracy_score(results['actuals'], results['baseline_predictions']) results['overall_accuracy'] = accuracy_score(results['actuals'], results['predictions']) results['overall_precision'] = precision_score(results['actuals'], results['predictions']) results['overall_f1'] = f1_score(results['actuals'], results['predictions'])return results# Executer Walk-Forward Validationprint("Walk-Forward Validation (Monthly Retrain):")print("="*60)wf_results = walk_forward_validation( X, y, RandomForestClassifier, {'n_estimators': 100, 'max_depth': 5, 'random_state': 42, 'n_jobs': -1}, train_window=252, # 1 an de train test_window=21, # 1 mois de test expanding=True)print(f"\nNombre de folds: {len(wf_results['fold_metrics'])}")print(f"\nMetriques par fold:")for fm in wf_results['fold_metrics'][:5]:print(f" Fold {fm['fold']}: Train={fm['train_size']}, Test={fm['test_size']}, Acc={fm['accuracy']:.4f}")iflen(wf_results['fold_metrics']) >5:print(f" ...")print(f"\nResultats Globaux:")print(f" Accuracy: {wf_results['overall_accuracy']:.4f}")print(f" Precision: {wf_results['overall_precision']:.4f}")print(f" F1 Score: {wf_results['overall_f1']:.4f}")
Interprétation : le walk-forward — 14 folds mensuels, dispersion et re-entraînement
La Walk-Forward Validation simule le trading réel : le modèle est ré-entraîné périodiquement, jamais une seule fois pour toujours.
Méthodologie : - Train window : 252 jours (1 an de données), fenêtre expandante — elle grandit avec le temps (252 → 525 jours sur les 14 folds ; l’output imprimé détaille les cinq premiers, jusqu’à Train=336) - Test window : 21 jours (1 mois de trading), jamais vus à l’entraînement - À chaque itération : retraitement complet du modèle et refit du scaler sur la fenêtre courante
Avantages vs simple Train/Test Split : - Plus proche des conditions réelles (retrain régulier) - Détecte la dégradation des performances dans le temps - Évite l’overfitting sur une période spécifique
Résultats interprétables : - overall_accuracy : Performance moyenne sur toute la période - fold_metrics : Détail par mois pour identifier les régimes - Comparaison folds vs global : stabilité ou variabilité
Lecture des valeurs mesurees : le walk-forward a produit 14 folds mensuels, pour des résultats globaux de Accuracy 0.8741, Precision 0.8705, F1 0.8674.
La lecture honnete regarde la dispersion : fold 3 tombe a 0.6667 (un mois ou le modele echoue 1 jour sur 3), fold 5 atteint 1.0000 (mois parfaitement predit). Chaque test set ne compte que 21 observations : la precision par fold est donc tres volatile – un fold a 100 % ne prouve rien, un fold a 67 % non plus, et c’est exactement pourquoi on lit les metriques globales agregees. Cette variance inter-fold est la vraie image d’un modele en production : certains mois portent, d’autres non, et la moyenne (0.8741) ne dit rien du pire mois – c’est le pire qui fixe la taille de position supportable.
Lecture vs le single-split : l’accuracy hors echantillon (87.4 %) depasse celle du split statique de la Partie 2 (83.5 %). Le re-entrainement periodique capture la derive des regimes de marche : le modele se rafraichit au lieu de vieillir. L’ecart est tout aussi visible face a la CV simple (~0.85) : le walk-forward re-entraine chaque mois et refit le scaler sur la fenetre glissante, plus proche du re-training operationnel reel que les 5 folds statiques de la Partie 2. En production QuantConnect, ce pattern correspond a un re-entrainement mensuel dans OnEndOfMonth – le notebook ML-Training-Pipeline automatise cette boucle.
# Visualisation Walk-Forwardfig, axes = plt.subplots(2, 1, figsize=(14, 8))# Accuracy par foldax1 = axes[0]folds = [fm['fold'] for fm in wf_results['fold_metrics']]accs = [fm['accuracy'] for fm in wf_results['fold_metrics']]ax1.bar(folds, accs, color='steelblue', edgecolor='black')ax1.axhline(y=wf_results['overall_accuracy'], color='red', linestyle='--', label=f"Mean Accuracy: {wf_results['overall_accuracy']:.3f}")fold_baselines = [fm['baseline_accuracy'] for fm in wf_results['fold_metrics']]ax1.plot(folds, fold_baselines, color='gray', marker='x', linestyle='-.', label='Baseline majoritaire (par fenetre)')ax1.axhline(y=0.5, color='lightgray', linestyle=':', label='Hasard equilibre (0.5, non observe)')ax1.set_xlabel('Fold', fontsize=12)ax1.set_ylabel('Accuracy', fontsize=12)ax1.set_title('Walk-Forward Validation - Accuracy par Fold', fontsize=14, fontweight='bold')ax1.legend()ax1.grid(True, alpha=0.3, axis='y')# Predictions cumuleesax2 = axes[1]dates = wf_results['dates']preds = wf_results['predictions']actuals = wf_results['actuals']# Calculer accuracy cumuleecumulative_correct = np.cumsum([1if p == a else0for p, a inzip(preds, actuals)])cumulative_total = np.arange(1, len(preds) +1)cumulative_accuracy = cumulative_correct / cumulative_totalax2.plot(dates, cumulative_accuracy, 'b-', linewidth=2)ax2.axhline(y=0.5, color='gray', linestyle=':', label='Random Baseline')ax2.fill_between(dates, 0.5, cumulative_accuracy, alpha=0.3, color='steelblue')ax2.set_xlabel('Date', fontsize=12)ax2.set_ylabel('Cumulative Accuracy', fontsize=12)ax2.set_title('Accuracy Cumulee au Fil du Temps', fontsize=14, fontweight='bold')ax2.legend()ax2.grid(True, alpha=0.3)plt.tight_layout()plt.show()
Interprétation : la visualisation walk-forward — barres mensuelles et accuracy cumulée
Visualisation des résultats de Walk-Forward Validation sur deux dimensions :
Graphique supérieur - Accuracy par Fold : - Chaque barre représente l’accuracy d’un mois de test - Ligne rouge pointillée = accuracy moyenne globale - Croix grises = baseline majoritaire exécutée par fenêtre : la classe majoritaire de la fenêtre de train (seule information disponible à cette date), prédite constamment sur la fenêtre de test - Ligne gris clair = hasard équilibré (0,5) : le tirage à pile-ou-face, qui n’est pas la baseline majoritaire observée quand la partition est déséquilibrée - Permet d’identifier les périodes de sous-performance
Graphique inférieur - Accuracy Cumulée : - Trace l’évolution de la performance dans le temps - Zone bleue = écart au hasard (avantage du modèle) - Révèle la stabilité ou dégradation des performances
Ce qu’on observe : - Variabilité inter-fold importante → impact des régimes de marché - Accuracy cumulée stable au-dessus de 0.5 → modèle robuste - Chutes temporaires → périodes de transition de régime
Lecture de la visualisation : le panneau accuracy-par-fold montre les 14 barres mensuelles – l’oeil cherche trois choses : le niveau moyen (~0.87), la presence de plafonds a 1.0 (folds faciles : le signal synthetique est regulier) et les creux (fold 3 a 0.6667). Un creux isole = regime du mois ; une succession de creux = derive du modele qui reclame un re-entrainement ou une revision des features. C’est exactement le tableau de bord qu’un systeme live doit produire chaque mois.
Partie 5 : Integration QuantConnect (20 min)
5.1 ObjectStore pour Persistence des Modèles
QuantConnect fournit ObjectStore pour persister des objets entre les exécutions:
Breiman, L. (2001). Random Forests. Machine Learning, 45(1), 5-32. https://doi.org/10.1023/A:1010933404324 — Random Forest, feature importance (Gini).
Friedman, J. H. (2001). Greedy Function Approximation: A Gradient Boosting Machine. The Annals of Statistics, 29(5), 1189-1232. https://doi.org/10.1214/aos/1013203451 — Gradient boosting (fondement theorique d’XGBoost).
Chen, T., & Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. Proceedings of the 22nd ACM SIGKDD (KDD ’16), 785-794. https://doi.org/10.1145/2939672.2939785 — XGBoost, early stopping.
Bergmeir, C. & Benitez, J. M. (2012). On the use of cross-validation for time series predictor evaluation. Information Sciences. — TimeSeriesSplit (validation sans lookahead bias).
Pardo, R. (2008). The Evaluation and Optimization of Trading Stratégies (2nd ed.). Wiley. ISBN 978-0-470-12801-5. — Walk-forward optimization.
Platt, J. C. (1999). Probabilistic Outputs for Support Vector Machines. Advances in Large Margin Classifiers. — Calibration Platt (sigmoide).
Zadrozny, B. & Elkan, C. (2002). Transforming Classifier Scores into Accurate Multiclass Probability Estimates. KDD. — Calibration isotonic.
Fawcett, T. (2006). An Introduction to ROC Analysis. Pattern Recognition Letters, 27(8), 861-874. https://doi.org/10.1016/j.patrec.2005.10.010 — Courbes ROC / AUC.
Notebook complete. La classification ML est un outil puissant mais doit etre utilisee avec rigueur: validation robuste, retrain regulier, et gestion du risque appropriee.