Research QuantBook: ML Classification (RandomForest)

Objectif

Entraîner et évaluer un modèle de classification ML (RandomForest) pour prédire la direction du marché (hausse/baisse) à J+1.

Workflow

  1. Charger les données SPY via QuantBook
  2. Générer les features techniques (RSI, EMA, MACD, returns, volatilité)
  3. Entraîner le modèle RandomForest
  4. Évaluer les performances (accuracy, precision, recall)
  5. Sauvegarder le modèle dans ObjectStore pour déploiement

Performance de référence

Le modèle doit atteindre une accuracy > 55% pour être meilleur qu’une stratégie aléatoire.

Prérequis

  • Environnement Lean Research
  • scikit-learn pour le modèle ML
  • Durée estimée: ~10 minutes
# Setup QuantBook
from AlgorithmImports import *
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
import joblib
import json
warnings.filterwarnings('ignore')

plt.style.use('seaborn-v0_8-darkgrid')
plt.rcParams['figure.figsize'] = (14, 5)

qb = QuantBook()
print("QuantBook initialisé.")
QuantBook initialisé.

1. Chargement des données

On charge les données SPY pour la période d’entraînement (2015-2022) et de test (2022-2026).

# Ajouter SPY
symbol = qb.add_equity("SPY", Resolution.DAILY).symbol

# Charger l'historique (2015-2026)
start = datetime(2015, 1, 1)
end = datetime(2026, 1, 1)

history = qb.history(symbol, start, end, Resolution.DAILY)
# qb.history(symbol, ...) retourne un DataFrame avec index (Symbol, datetime)
# On droplevel pour obtenir un index datetime simple
if isinstance(history.index, pd.MultiIndex):
    history = history.droplevel(0)
print(f"Données chargées: {len(history)} lignes")
print(f"Période: {history.index[0].date()} à {history.index[-1].date()}")
Données chargées: 2766 lignes
Période: 2015-01-02 à 2025-12-31

Extraction de la série de prix de clôture depuis l’historique et affichage du nombre de barres et du premier prix pour ML-Classification.

# Extraire les prix de clôture
closes = history['close']
print(f"Prix: {len(closes)} jours de trading")
print(f"Premier prix: ${closes.iloc[0]:.2f}")
print(f"Dernier prix: ${closes.iloc[-1]:.2f}")
Prix: 2766 jours de trading
Premier prix: $182.10
Dernier prix: $396.33

2. Génération des features

On génère les features techniques utilisés par le modèle: - RSI (14) - EMA (10, 20, 50) - MACD (12, 26, 9) - Returns (1d, 5d, 10d) - Prix/EMA ratios - Volatilité (10d, 20d)

def compute_rsi(prices, period=14):
    """Calcule le RSI."""
    delta = prices.diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
    rs = gain / loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

def compute_ema(prices, period):
    """Calcule l'EMA."""
    return prices.ewm(span=period, adjust=False).mean()

def compute_macd(prices, fast=12, slow=26, signal=9):
    """Calcule le MACD."""
    ema_fast = compute_ema(prices, fast)
    ema_slow = compute_ema(prices, slow)
    macd_line = ema_fast - ema_slow
    macd_signal = macd_line.ewm(span=signal, adjust=False).mean()
    macd_histogram = macd_line - macd_signal
    return macd_line, macd_signal, macd_histogram

print("Fonctions d'indicateurs définies.")
Fonctions d'indicateurs définies.

L’oscillateur RSI(14) est calculé sur les prix de clôture pour générer les signaux de surachat/survente de ML-Classification.

# Calculer tous les indicateurs
df = pd.DataFrame(index=closes.index)
df['close'] = closes

# RSI
df['rsi_14'] = compute_rsi(closes, 14)

# EMA
df['ema_10'] = compute_ema(closes, 10)
df['ema_20'] = compute_ema(closes, 20)
df['ema_50'] = compute_ema(closes, 50)

# MACD
df['macd_line'], df['macd_signal'], df['macd_histogram'] = compute_macd(closes)

# Returns
df['returns_1d'] = closes.pct_change(1)
df['returns_5d'] = closes.pct_change(5)
df['returns_10d'] = closes.pct_change(10)

# Prix/EMA ratios
df['price_ema10_ratio'] = closes / df['ema_10']
df['price_ema20_ratio'] = closes / df['ema_20']
df['price_ema50_ratio'] = closes / df['ema_50']

# Volatilité
df['volatility_10d'] = df['returns_1d'].rolling(10).std()
df['volatility_20d'] = df['returns_1d'].rolling(20).std()

# Target: direction du marché à J+1 (1 = hausse, 0 = baisse)
df['target'] = (df['returns_1d'].shift(-1) > 0).astype(int)

# Supprimer les NaN
df_clean = df.dropna()

print(f"Features générées: {len(df_clean)} lignes (après suppression des NaN)")
print(f"Colonnes: {list(df_clean.columns)}")
Features générées: 1565 lignes (après suppression des NaN)
Colonnes: ['close', 'rsi_14', 'ema_10', 'ema_20', 'ema_50', 'macd_line', 'macd_signal', 'macd_histogram', 'returns_1d', 'returns_5d', 'returns_10d', 'price_ema10_ratio', 'price_ema20_ratio', 'price_ema50_ratio', 'volatility_10d', 'volatility_20d', 'target']

Interprétation: Features

Les features capturent différents aspects du marché: - RSI: Surachat/survente (0-100) - EMA: Tendance (prix > EMA = haussier) - MACD: Momentum (histogram > 0 = haussier) - Returns: Performance passée - Volatilité: Risque du marché

3. Préparation des données d’entraînement

Séparation train/test (80/20) pour évaluer la généralisation du modèle.

# Colonnes de features
feature_cols = [
    'rsi_14', 'ema_10', 'ema_20', 'ema_50',
    'macd_line', 'macd_signal', 'macd_histogram',
    'returns_1d', 'returns_5d', 'returns_10d',
    'price_ema10_ratio', 'price_ema20_ratio', 'price_ema50_ratio',
    'volatility_10d', 'volatility_20d'
]

# Préparer X et y
X = df_clean[feature_cols].values
y = df_clean['target'].values

# Split train/test (80/20)
split_idx = int(len(X) * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]

print(f"Train set: {len(X_train)} échantillons")
print(f"Test set: {len(X_test)} échantillons")
print(f"\nDistribution des classes:")
print(f"  Train: {np.mean(y_train):.1%} hausse")
print(f"  Test:  {np.mean(y_test):.1%} hausse")
Train set: 1252 échantillons
Test set: 313 échantillons

Distribution des classes:
  Train: 54.5% hausse
  Test:  54.3% hausse

4. Entraînement du modèle RandomForest

# Créer et entraîner le modèle
model = RandomForestClassifier(
    n_estimators=100,
    max_depth=10,
    min_samples_split=20,
    min_samples_leaf=10,
    random_state=42,
    n_jobs=-1
)

model.fit(X_train, y_train)
print("Modèle RandomForest entraîné.")
Modèle RandomForest entraîné.

Application du modèle entraîné pour générer des prédictions sur les données de test.

# Prédictions
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)

# Probabilités
y_test_proba = model.predict_proba(X_test)

# Métriques
train_accuracy = accuracy_score(y_train, y_train_pred)
test_accuracy = accuracy_score(y_test, y_test_pred)

print(f"Accuracy Train: {train_accuracy:.2%}")
print(f"Accuracy Test:  {test_accuracy:.2%}")
print(f"\nRapport de classification (Test):")
print(classification_report(y_test, y_test_pred, target_names=['Baisse', 'Hausse']))
Accuracy Train: 84.66%
Accuracy Test:  53.67%

Rapport de classification (Test):
              precision    recall  f1-score   support

      Baisse       0.47      0.10      0.17       143
      Hausse       0.54      0.90      0.68       170

    accuracy                           0.54       313
   macro avg       0.51      0.50      0.42       313
weighted avg       0.51      0.54      0.45       313

Interprétation: Performance du modèle

Un accuracy > 55% est nécessaire pour battre une stratégie aléatoire. Si l’accuracy train >> accuracy test, le modèle overfit.

5. Matrice de confusion et feature importance

# Matrice de confusion
cm = confusion_matrix(y_test, y_test_pred)
print("Matrice de confusion (Test):")
print(f"                Prédit Baisse    Prédit Hausse")
print(f"Réel Baisse      {cm[0,0]:>6}           {cm[0,1]:>6}")
print(f"Réel Hausse      {cm[1,0]:>6}           {cm[1,1]:>6}")

# Feature importance
importance = pd.DataFrame({
    'feature': feature_cols,
    'importance': model.feature_importances_
}).sort_values('importance', ascending=False)

print(f"\nTop 10 Features:")
print(importance.head(10).to_string(index=False))
Matrice de confusion (Test):
                Prédit Baisse    Prédit Hausse
Réel Baisse          15              128
Réel Hausse          17              153

Top 10 Features:
          feature  importance
       returns_1d    0.073496
price_ema50_ratio    0.069142
   volatility_10d    0.068910
       returns_5d    0.068618
           rsi_14    0.068358
           ema_50    0.068321
      macd_signal    0.067909
   volatility_20d    0.066841
           ema_10    0.066566
   macd_histogram    0.065369

6. Visualisation des résultats

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Feature importance
ax = axes[0]
importance.head(10).plot(kind='barh', x='feature', y='importance', ax=ax)
ax.set_title('Feature Importance', fontsize=12, fontweight='bold')
ax.set_xlabel('Importance')
ax.invert_yaxis()
ax.grid(True, alpha=0.3)

# Distribution des probabilités
ax = axes[1]
ax.hist(y_test_proba[:, 1], bins=50, alpha=0.7, edgecolor='black')
ax.axvline(0.5, color='red', linestyle='--', label='Seuil 50%')
ax.set_title('Distribution des probabilités de hausse', fontsize=12, fontweight='bold')
ax.set_xlabel('Probabilité de hausse')
ax.set_ylabel('Fréquence')
ax.legend()
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('ml_classification_results.png', dpi=150, bbox_inches='tight')
plt.show()
print("Graphique sauvegardé.")

Graphique sauvegardé.

7. Backtest de la stratégie ML

Simulation de la stratégie utilisant les prédictions du modèle.

def backtest_ml_strategy(df_clean, y_pred, y_proba, 
                         long_threshold=0.55, short_threshold=0.45):
    """Backtest de la stratégie ML."""
    returns = df_clean['returns_1d'].values
    
    portfolio_values = [1.0]
    position = 0  # 0 = cash, 1 = long
    
    for i in range(len(y_pred)):
        # Décision de trading
        confidence = y_proba[i, 1]  # Probabilité de hausse
        
        if confidence > long_threshold:
            position = 1
        elif confidence < short_threshold:
            position = 0
        
        # Calcul du return
        port_return = position * returns[i]
        portfolio_values.append(portfolio_values[-1] * (1 + port_return))
    
    # Métriques
    port_returns = np.diff(portfolio_values) / np.array(portfolio_values[:-1])
    cum_returns = pd.Series(portfolio_values[1:])
    
    total_ret = (portfolio_values[-1] / portfolio_values[0]) - 1
    years = len(port_returns) / 252
    cagr = (1 + total_ret) ** (1 / years) - 1 if years > 0 else 0
    vol = np.std(port_returns) * np.sqrt(252)
    sharpe = (cagr - 0.03) / vol if vol > 0.001 else 0
    
    running_max = cum_returns.expanding().max()
    drawdown = (cum_returns - running_max) / running_max
    max_dd = drawdown.min()
    
    # Buy & Hold SPY — returns est un numpy array, cumprod retourne un array
    spy_values = np.cumprod(1 + returns)
    spy_total_ret = spy_values[-1] - 1
    
    return {
        'cum': cum_returns,
        'sharpe': sharpe,
        'cagr': cagr,
        'max_dd': max_dd,
        'vol': vol,
        'spy_cum': pd.Series(spy_values)
    }

# Backtest sur le test set
result = backtest_ml_strategy(df_clean.iloc[-len(y_test):], y_test_pred, y_test_proba)

print(f"\nStratégie ML (Test set):")
print(f"  Sharpe: {result['sharpe']:.3f}")
print(f"  CAGR:   {result['cagr']:.1%}")
print(f"  Max DD: {result['max_dd']:.1%}")
print(f"\nBuy & Hold SPY (Test set):")
spy_cagr = (result['spy_cum'].iloc[-1] ** (252/len(result['spy_cum']))) - 1
print(f"  CAGR:   {spy_cagr:.1%}")

Stratégie ML (Test set):
  Sharpe: 0.035
  CAGR:   4.1%
  Max DD: -33.7%

Buy & Hold SPY (Test set):
  CAGR:   17.6%

8. Sauvegarde du modèle dans ObjectStore

Le modèle sera sauvegardé pour être utilisé par l’algorithme de trading.

# Sauvegarder le modèle
import pickle

MODEL_KEY = "classification_model"
CONFIG_KEY = "classification_config"

# Sérialiser le modèle avec pickle (joblib.dumps n'existe pas)
model_bytes = pickle.dumps(model)
qb.object_store.save_bytes(MODEL_KEY, model_bytes)

# Sauvegarder la configuration
config = {
    'feature_cols': feature_cols,
    'accuracy_train': float(train_accuracy),
    'accuracy_test': float(test_accuracy),
    'long_threshold': 0.55,
    'short_threshold': 0.45,
    'train_period': '2015-2022',
    'test_period': '2022-2026'
}
config_bytes = json.dumps(config).encode('utf-8')
qb.object_store.save_bytes(CONFIG_KEY, config_bytes)

print(f"Modèle sauvegardé dans ObjectStore:")
print(f"  - {MODEL_KEY}")
print(f"  - {CONFIG_KEY}")
Modèle sauvegardé dans ObjectStore:
  - classification_model
  - classification_config

9. Conclusions et recommandations

Résumé

Métrique Valeur
Accuracy Train (à remplir)
Accuracy Test (à remplir)
Sharpe Stratégie (à remplir)
CAGR Stratégie (à remplir)

Verdict

Si accuracy test > 55% et Sharpe > 0.5: Déployer Sinon: Améliorer (features, hyperparamètres, ou changer d’approche)

Prochaines étapes

  1. Déployer l’algorithme ML-Classification
  2. Backtester sur QC cloud avec le modèle chargé
  3. Tester d’autres modèles (XGBoost, LSTM)
  4. Optimiser les seuils de confiance (long/short thresholds)
Retour au sommet