Position Sizing: Basé sur la probabilité de hausse (confidence)
Performance de référence
Sharpe ~1.2 (2023-2024) avec filtres optimisés.
Hypothèses à tester
Seuils de confiance: (0.54/0.38), (0.56/0.40), (0.58/0.42)
Seuil volatilité: 50%, 60%, 70%
Stop-loss: 6%, 8%, 10%
Prérequis
Environnement Lean Research
Données BTCUSDT horaires/journalières
Durée estimée: ~15 minutes
Note : Ce notebook utilise une approche simplifiée pour la recherche. Le modèle complet utilise ObjectStore pour la persistance.
# Setup QuantBookfrom AlgorithmImports import*import numpy as npimport pandas as pdimport matplotlib.pyplot as pltimport warningsfrom sklearn.ensemble import RandomForestClassifierwarnings.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 BTCUSDT pour la période 2019-2026 (training + backtest).
# BTCUSDT sur Binancebtc = qb.add_crypto("BTCUSDT", Resolution.DAILY, Market.BINANCE)# Période étendue pour multi-regimestart = datetime(2019, 1, 1)end = datetime(2026, 1, 1)history = qb.history(btc.symbol, start, end, Resolution.DAILY)print(f"Données chargées: {len(history)} lignes")
Données chargées: 2557 lignes
Extraction des séries close, high et low de BTCUSD et affichage des statistiques de prix (initial, final, rendement total) pour BTC-ML.
# Preparer les donneesiflen(history) ==0: closes =None highs =None lows =Noneprint("WARNING: No BTCUSDT data returned from QC Cloud.")print("Crypto daily data may not be available in this environment.")print("All analysis cells will be skipped.")else:# c.935 MultiIndex guard: qb.history single-symbol returns (symbol,time) index -> drop symbol levelifisinstance(history.index, pd.MultiIndex): history = history.reset_index(level=0, drop=True) closes = history['close'] highs = history['high'] lows = history['low']print(f"Periode: {closes.index[0].date()} a {closes.index[-1].date()}")print(f"Donnees: {len(closes)} jours")print(f"\nStatistiques BTC:")print(f" Prix initial: ${closes.iloc[0]:.2f}")print(f" Prix final: ${closes.iloc[-1]:.2f}")print(f" Return total: {(closes.iloc[-1]/closes.iloc[0] -1):.1%}")
Periode: 2019-01-02 a 2026-01-01
Donnees: 2557 jours
Statistiques BTC:
Prix initial: $3797.14
Prix final: $87648.22
Return total: 2208.3%
2. Calcul des features ML
def compute_features(closes, highs, lows, i, sma_period=20, rsi_period=14, ema_periods=[10, 20, 50, 200], adx_period=14, atr_period=14, vol_period=20):""" Calcule les features ML pour l'index i. IMPORTANT: Utilise uniquement les donnees jusqu'a i-1 (walk-forward). """if i <max(ema_periods) +2:returnNone# Features historiques (jusqu'a i-1) daily_ret = (closes.iloc[i-1] - closes.iloc[i-2]) / closes.iloc[i-2]# SMA sma_20 = closes.iloc[i-sma_period:i].mean()# RSI deltas = closes.iloc[i-rsi_period:i].diff().dropna() gains = deltas.where(deltas >0, 0).mean() losses = (-deltas.where(deltas <0, 0)).mean() rs = gains /max(losses, 1e-10) rsi_14 =100- (100/ (1+ rs))# EMAs emas = {}for period in ema_periods: multiplier =2.0/ (period +1) ema = closes.iloc[i-period:i].iloc[0]for val in closes.iloc[i-period:i][1:]: ema = (val - ema) * multiplier + ema emas[period] = ema# ADX (simplifie - ATR mean) atr_vals = []for j inrange(max(0, i-adx_period), i): tr =max(highs.iloc[j] - lows.iloc[j], abs(highs.iloc[j] - closes.iloc[j-1]) if j >0else0,abs(lows.iloc[j] - closes.iloc[j-1]) if j >0else0) atr_vals.append(tr) adx_val = np.mean(atr_vals) if atr_vals else25.0# ATR atr_val = np.mean(atr_vals) if atr_vals else closes.iloc[i-1] *0.02# Volatilite annualisee vol_20 = closes.iloc[i-vol_period:i].std() / closes.iloc[i-vol_period:i].mean() * np.sqrt(252) if i >= vol_period else0return [sma_20, rsi_14, daily_ret, emas[10], emas[20], emas[50], emas[200], adx_val, atr_val, vol_20]# Test de la fonctionif closes isNone:print("BTC data not available — skipping feature computation.")print("Run this notebook on QC Cloud with crypto data access.")else: test_features = compute_features(closes, highs, lows, 250)if test_features: feature_names = ['SMA20', 'RSI14', 'DailyRet', 'EMA10', 'EMA20', 'EMA50', 'EMA200', 'ADX', 'ATR', 'Vol20']print("Features a i=250:")for name, val inzip(feature_names, test_features):print(f" {name}: {val:.2f}")
Simulation de la stratégie avec: - Seuils de confiance pour entry/exit - Filtres (trend, RSI, ADX, volatilité) - Stop-loss et take-profit
def backtest_ml_btc(closes, highs, lows, model, confidence_long=0.56, confidence_exit=0.40, vol_threshold=0.60, stop_loss=0.08, take_profit=0.15, start_idx=None):""" Backtest BTC ML strategy (OOS-only). start_idx defaults to train_end_idx (the model's train/OOS boundary, set in the data-prep cell) so the backtest evaluates ONLY the out-of-sample window and is never contaminated by training data. """# OOS start: align with the model's train_end_idx (fallback 250)if start_idx isNone: start_idx = train_end_idx if'train_end_idx'inglobals() else250 start_idx =max(start_idx, 250) # keep features warmup portfolio_values = [1.0] invested =False entry_price =None warmup = start_idx # alias for index alignment below# Stats trades =0 stop_losses =0 take_profits =0 signal_exits =0for i inrange(warmup, len(closes)): current_price = closes.iloc[i]# Calcul features features = compute_features(closes, highs, lows, i)if features isNone: portfolio_values.append(portfolio_values[-1])continue# Extraire indicateurs pour filtres sma_20, rsi_14, daily_ret, ema_10, ema_20, ema_50, ema_200, adx_val, atr_val, vol_20 = features# Prediction proba = model.predict_proba([features])[0] confidence_up = proba[1] port_return =0.0if invested and entry_price isnotNone: pnl_pct = (current_price - entry_price) / entry_price# Check stop-lossif pnl_pct <=-stop_loss: invested =False entry_price =None stop_losses +=1 port_return = pnl_pct# Check take-profitelif pnl_pct >= take_profit: invested =False entry_price =None take_profits +=1 port_return = pnl_pct# Check signal exitelif confidence_up < confidence_exit: invested =False entry_price =None signal_exits +=1 port_return = pnl_pctelse:# Hold - mark-to-market port_return = daily_ret * (current_price / closes.iloc[i-1])# Entry signalelifnot invested:# FILTRE VOLATILITEif vol_20 > vol_threshold: portfolio_values.append(portfolio_values[-1])continue# FILTRE TREND: Prix > EMA200if current_price < ema_200: portfolio_values.append(portfolio_values[-1])continue# FILTRE RSI: RSI > 40 (éviter survente)if rsi_14 <40: portfolio_values.append(portfolio_values[-1])continue# FILTRE ADX: ADX > 20 (éviter absence de trend)if adx_val <20: portfolio_values.append(portfolio_values[-1])continue# Signal d'entréeif confidence_up > confidence_long: invested =True entry_price = current_price trades +=1 portfolio_values.append(portfolio_values[-1] * (1+ port_return))# Métriques returns = np.diff(portfolio_values) / np.array(portfolio_values[:-1]) cum_returns = pd.Series(portfolio_values[1:], index=closes.index[warmup:]) total_ret = (portfolio_values[-1] / portfolio_values[0]) -1 years =len(returns) /365 cagr = (1+ total_ret) ** (1/ years) -1if years >0else0 vol = np.std(returns) * np.sqrt(365) iflen(returns) >1else0 sharpe = (cagr -0.03) / vol if vol >0.001else0 running_max = cum_returns.expanding().max() drawdown = (cum_returns - running_max) / running_max max_dd = drawdown.min()return {'cum': cum_returns,'sharpe': sharpe,'cagr': cagr,'max_dd': max_dd,'vol': vol,'trades': trades,'stop_losses': stop_losses,'take_profits': take_profits,'signal_exits': signal_exits }print("Fonction de backtest définie.")
Fonction de backtest définie.
5. Test des seuils de confiance
if closes isNoneor model isNone:print("BTC data or model not available — skipping confidence threshold analysis.")else:# Test differents seuils de confiance confidence_configs = [ ((0.54, 0.38), "Conf54/38"), ((0.56, 0.40), "Conf56/40"), ((0.58, 0.42), "Conf58/42"), ]print(f"{'Config':<12}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}{'Trades':>8}")print("-"*52) conf_results = {}for (long, exit), name in confidence_configs: r = backtest_ml_btc(closes, highs, lows, model, confidence_long=long, confidence_exit=exit) conf_results[name] = rprint(f"{name:<12}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}{r['trades']:>8}") best_conf =max(conf_results.items(), key=lambda x: x[1]['sharpe'])print(f"\nMeilleure config: {best_conf[0]} (Sharpe={best_conf[1]['sharpe']:.3f})")
if closes isNoneor model isNone:print("BTC data or model not available — skipping volatility threshold analysis.")else:# Test différents seuils de volatilité vol_thresholds = [0.50, 0.60, 0.70]print(f"{'Seuil Vol':<12}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}{'Trades':>8}")print("-"*48) vol_results = {}for threshold in vol_thresholds: r = backtest_ml_btc(closes, highs, lows, model, vol_threshold=threshold) vol_results[f"{threshold*100:.0f}%"] = rprint(f"{threshold*100:.0f}%{'':<9}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}{r['trades']:>8}") best_vol =max(vol_results.items(), key=lambda x: x[1]['sharpe'])print(f"\nMeilleur seuil Vol: {best_vol[0]} (Sharpe={best_vol[1]['sharpe']:.3f})")
if closes isNoneor model isNone:print("BTC data or model not available — skipping stop-loss analysis.")else:# Test différents stop-loss sl_values = [0.06, 0.08, 0.10]print(f"{'Stop-Loss':<12}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}{'SL Hits':>10}")print("-"*54) sl_results = {}for sl in sl_values: r = backtest_ml_btc(closes, highs, lows, model, stop_loss=sl) sl_results[f"{sl*100:.0f}%"] = rprint(f"{sl*100:.0f}%{'':<9}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}{r['stop_losses']:>10}") best_sl =max(sl_results.items(), key=lambda x: x[1]['sharpe'])print(f"\nMeilleur Stop-Loss: {best_sl[0]} (Sharpe={best_sl[1]['sharpe']:.3f})")
Interprétation : modèle ML tabulaire sous-performe le Buy & Hold sur la fenêtre OOS (résultat honnête)
Contexte d’exécution. Ce quantbook a été ré-exécuté via le moteur de recherche QC (image Docker quantconnect/research:latest, vrai QuantBook()) avec de vraies données BTCUSDT Binance journalières (2557 jours, 2019-01-02 → 2026-01-01, $3797 → $87648, +2208 %). Sorties C.2 réelles, aucune fabrication (Stop & Repair).
Fenêtre out-of-sample (correctif c.940). Le modèle est entraîné sur 2019-2022 (train_end_idx = 2022-12-31). Le backtest évalue uniquement la fenêtre OOS 2023-2026 : la stratégie et la ligne de référence Buy & Hold démarrent toutes deux à train_end_idx, alignées sur la même fenêtre, pour exclure toute contamination in-sample. (Version antérieure : le backtest bouclait sur la série entière 2019-2026 incluant le train window — corrigé suite à un audit coord.)
Résultat (fenêtre OOS 2023-2026). Le classifieur tabulaire (10 features SMA/RSI/EMA/ADX/ATR/Vol, 1209 samples d’entraînement, 65.3 % d’accuracy en train) utilisé comme signal de trading avec seuils de confiance élevés (Conf56/40) produit 1 seul trade sur la fenêtre OOS (un take-profit). Il sous-performe massivement le Buy & Hold :
Stratégie
CAGR
Sharpe
MaxDD
BTC ML Enhanced
9.2 %
0.542
−8.3 %
BTC B&H (2023-2026)
73.8 %
1.530
−32.0 %
Pourquoi le signal se déclenche presque jamais. Les seuils de confiance stricts (Conf56/40) combinés aux quatre filtres de risque (volatilité, prix > EMA200, RSI > 40, ADX > 20) excluent la quasi-totalité des jours OOS. Sur la phase haussière 2023-2026 du BTC (où le B&H fait +73.8 % CAGR), ne trader qu’une seule fois fait rater l’essentiel de la dynamique. La précision de direction à 65 % en entraînement ne se traduit pas en edge de trading une fois la rareté des signaux prise en compte.
Leçon pédagogique (limite honnête). Ce n’est pas un bug : un classifieur tabulaire sur features techniques journalières, à seuils de confiance élevés, ne bat pas le Buy & Hold sur un actif en bull market soutenu. Le seul avantage est la réduction de drawdown (−8.3 % vs −32.0 % sur la fenêtre OOS), au prix d’un rendement divisé par ~8. Verdict : NO BEATS — résultat négatif documenté comme livrable, conformément à la barre honnête des rungs du curriculum (cf. ML-Training-Pipeline/scripts/results/).
9. Visualisation des résultats
if closes isNoneor model isNone:print("BTC data or model not available — skipping visualization.")else: fig, axes = plt.subplots(1, 2, figsize=(16, 5))# Gauche: Confidence comparison ax = axes[0]for name, r in conf_results.items(): ax.plot(r['cum'].values, label=f"{name} (S={r['sharpe']:.2f})", linewidth=1.5) ax.plot(btc_values.values, label='BTC B&H', linestyle='--', alpha=0.5) ax.set_title('Seuils de Confiance', fontsize=12, fontweight='bold') ax.set_ylabel('Valeur du portefeuille') ax.legend(fontsize=8) ax.grid(True, alpha=0.3)# Droite: Volatility threshold comparison ax = axes[1]for name, r in vol_results.items(): ax.plot(r['cum'].values, label=f"Vol {name} (S={r['sharpe']:.2f})", linewidth=1.5) ax.plot(btc_values.values, label='BTC B&H', linestyle='--', alpha=0.5) ax.set_title('Filtre Volatilité', fontsize=12, fontweight='bold') ax.set_ylabel('Valeur du portefeuille') ax.legend(fontsize=8) ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('btc_ml_analysis.png', dpi=150, bbox_inches='tight') plt.show()print("Graphique sauvegardé.")
Graphique sauvegardé.
10. Conclusions et recommandations
Résumé
Métrique
Meilleure config
Confiance
(à remplir)
Seuil Vol
(à remplir)
Stop-Loss
(à remplir)
Sharpe
(à remplir)
CAGR
(à remplir)
Verdict
Si Sharpe > 1.0: Déployer avec les paramètres optimaux
Points forts BTC ML Enhanced
Multi-filtres: Trend + RSI + ADX + Volatilité
Position sizing: Basé sur la probabilité de hausse
Risk management: Stop-loss + take-profit stricts
Adaptatif: Le modèle ML capture des patterns non-linéaires
Limitations
Surapprentissage potentiel: Le modèle peut overfitter sur le training set
Dépendance aux features: Les indicateurs techniques peuvent perdre en efficacité
Complexité: Nécessite retraining périodique (tous les 60 jours)
Prochaines étapes
Déployer sur QC cloud avec les paramètres optimaux
Tester d’autres modèles (XGBoost, LSTM)
Optimiser les features avec feature importance analysis