Un projet complet de notebook de recherche, des données au rapport, avec une liste de contrôle de revue de code contre le biais d'anticipation et le surapprentissage
Organisez une étude de stratégie complète sous forme de projet reproductible, menez-la des données brutes à un résultat rédigé, et relisez le code avec une liste de contrôle qui repère les erreurs flatteuses pour les backtests.
Ce que vous allez apprendre
- Une organisation des dossiers et des notebooks qui rend une étude reproductible, par vous dans six mois ou par n'importe qui d'autre.
- L'ordre de travail : données, nettoyage, variables, signaux, backtest, métriques, vérification hors échantillon, rapport.
- Une liste de contrôle de revue de code, ligne par ligne, contre le biais d'anticipation, le biais du survivant et le surapprentissage.
- Comment rédiger honnêtement un résultat, y compris ce qui a été essayé et n'a pas fonctionné.
Explication
Les tests hors échantillon, les plateaux de paramètres et le coût des essais multiples ont été traités dans Trading systématique (leçons 6 et 7). Cette leçon porte sur l'ingénierie qui permet à ces idées de tenir : une structure de projet, un déroulé de travail fixe et une revue qu'une autre personne pourrait refaire.
Organisation du projet
project/data/raw/ # téléchargements, jamais modifiés à la maindata/clean/ # fichiers parquet produits par 01_data.ipynbnotebooks/ # 01_data, 02_features, 03_backtest, 04_reportsrc/ # indicators.py, engine.py, metrics.py (leçons 6, 8, 9)tests/ # test_metrics.py, test_engine.pyconfig.yaml # tickers, dates, paramètres, hypothèses de coûtsrequirements.txt # versions figées : pandas==2.x, numpy==…
Le code réutilisé vit dans src/ ; les notebooks se contentent d'appeler des fonctions et d'afficher les résultats. Figez les versions des paquets, car une mise à jour de pandas peut modifier un comportement par défaut (chaînes de fréquence, étiquettes de rééchantillonnage) et déplacer vos chiffres. Fixez la graine de tout élément aléatoire et indiquez dans le rapport la date de téléchargement des données.
Le déroulé de travail, dans l'ordre
- L'hypothèse d'abord. Une phrase de raisonnement économique et les règles exactes avant de toucher aux données (Trading systématique, leçon 2). Fixez dès maintenant la séparation entre échantillon d'apprentissage (in-sample) et échantillon de test (out-of-sample), et n'ouvrez pas le fichier hors échantillon avant l'étape 7.
- Données. Téléchargez, stockez les données brutes, documentez la source et la méthode d'ajustement (leçon 4). Une liste des « membres actuels de l'indice » testée sur dix ans en arrière constitue un biais du survivant.
- Nettoyer et valider. Trous, doublons, prix nuls, fuseaux horaires ; enregistrez dans
data/clean/. - Variables et signaux avec les fonctions testées de
src/; chaque variable à la barre t n'utilise que des données allant jusqu'à t. - Backtest avec le moteur de la leçon 8, modèle de coûts compris (Trading systématique, leçon 8).
- Métriques et graphiques (leçons 9 et 10), sur la période in-sample, avec une petite grille de paramètres, et non une recherche de la meilleure case.
- Exécution hors échantillon une seule fois, avec les paramètres figés. Consignez le résultat, quel qu'il soit.
- Rapport : hypothèse, données, règles, coûts, les deux tableaux de résultats, graphiques, nombre de variantes essayées, décision.
Liste de contrôle de revue de code
Relisez votre notebook comme s'il était celui de quelqu'un d'autre ; chaque ligne est une question fermée.
| Domaine | Vérification | Correction type |
|---|---|---|
| Timing | Chaque signal est-il décalé d'une barre avant de fixer une position ? | pos = signal.shift(1) |
| Timing | Les fenêtres glissantes utilisent-elles center=False (la valeur par défaut) et aucun bfill() ? |
Supprimer le centrage et les remplissages vers l'arrière |
| Timing | Les normalisations (z-scores, rangs, scalers) sont-elles ajustées uniquement sur des données passées ? | Ajustements cumulatifs ou glissants |
| Données | L'univers est-il défini tel qu'il était connu à chaque date ? | Listes de composants à date (point-in-time) ou réserve honnête |
| Données | Les prix ajustés servent-ils aux rendements et les prix non ajustés aux distances de stop, le cas échéant ? | Conserver les deux colonnes |
| Exécutions | Les gaps sont-ils exécutés à l'ouverture, et non au prix du stop ou du signal ? | min(open, stop) |
| Coûts | Les commissions, le spread et le slippage sont-ils appliqués par trade et par sens ? | Modèle de coûts dans la configuration |
| Surapprentissage | Combien de jeux de paramètres, de règles et d'univers ont été essayés ? Ce nombre figure-t-il dans le rapport ? | Journaliser chaque exécution |
| Surapprentissage | Des paramètres voisins donnent-ils des résultats similaires (un plateau) ? | Carte de chaleur de la grille |
| Surapprentissage | Les données hors échantillon ont-elles été utilisées plus d'une fois ? | Si oui, elles sont désormais in-sample ; trouvez de nouvelles données |
| Reproductibilité | « Restart kernel and run all » reproduit-il les chiffres du rapport ? | Corriger l'état caché et l'ordre des cellules |
Règle empirique pour la significativité : la statistique t d'un ratio de Sharpe annualisé vaut à peu près Sharpe × √années. Un Sharpe de 0.8 sur quatre ans donne 1.6, sous le seuil habituel d'environ 2, et cela avant même de tenir compte du nombre de variantes essayées ; parmi vingt essais, le meilleur devrait paraître bon par simple hasard.
Exemple commenté
Exemple hypothétique, tous les chiffres sont inventés. Un trader teste « acheter une cassure des 100 jours, sortir sur un stop suiveur de 3 × ATR » sur 30 actions de grande capitalisation, de 2014 à 2025, avec des coûts de 0.1 % par sens.
- Séparation. In-sample 2014–2021 (8 ans), out-of-sample 2022–2025 (4 ans), décidé avant la première exécution.
- Grille. Longueurs de cassure 50, 100, 150 × multiples d'ATR 2, 3, 4 = 9 variantes, journalisées. Le Sharpe in-sample va de 0.6 à 1.1, la meilleure case étant (100, 3) = 1.1 ; ses voisines affichent 0.9 et 1.0, un plateau, le choix est donc conservé.
- Revue. La liste de contrôle révèle que la colonne ATR utilisait
rolling(14).mean()sur l'étendue vraie du jour pour le stop du jour. Le moteur est modifié pour utiliser l'ATR de la barre précédente (leçon 8). Le Sharpe in-sample passe de 1.1 à 1.0 ; le drawdown maximal reste inchangé à −22 %. - Hors échantillon. Une seule exécution : Sharpe de 0.6, CAGR de 7.2 %, drawdown maximal de −19 %, 118 trades. Significativité : 0.6 × √4 = 1.2 ; le chiffre in-sample donnait 1.0 × √8 = 2.8. Le résultat hors échantillon est positif mais statistiquement faible à lui seul.
- Décision dans le rapport. « La règle a conservé environ 60 % de son Sharpe in-sample hors échantillon (0.6 / 1.0), sur un plateau, avec 9 variantes essayées et un bug d'anticipation corrigé. Les preuves sont modestes ; l'étape suivante est six mois de trading simulé (paper trading) avec le script de la leçon 12, et non du capital réel. » Frais modélisés ; impôts et coûts de financement exclus.
Erreur fréquente
Exercice pratique
Sur papier : relisez ce fragment de notebook et le résultat qui l'a suivi.
df['z'] = (df.close - df.close.mean()) / df.close.std()df['signal'] = df.z < -1.5df['pos'] = df.signal.astype(int)df['ret'] = df.close.pct_change() * df.pos
Sharpe in-sample de 2.4 sur 6 ans ; Sharpe hors échantillon de −0.3 sur 2 ans. (a) Identifiez deux problèmes d'anticipation dans le code. (b) Calculez les statistiques t approximatives des ratios de Sharpe in-sample et hors échantillon. (c) Réécrivez les quatre lignes pour qu'elles soient exemptes d'anticipation (décrivez les modifications si vous préférez la prose).
Afficher la solution
(a) Premièrement, df.close.mean() et df.close.std() utilisent tout l'échantillon, prix futurs compris, si bien que le z-score à n'importe quelle date « connaît » la moyenne future. Deuxièmement, pos n'est pas décalé : le rendement du jour est obtenu sur un signal calculé à partir de la clôture du jour.
(b) In-sample : 2.4 × √6 = 5.9 ; hors échantillon : −0.3 × √2 = −0.4. La valeur in-sample élevée était produite par les bugs, c'est pourquoi elle s'est effondrée.
(c) Utilisez une fenêtre glissante ou cumulative et un décalage :
m = df.close.rolling(250).mean(); s = df.close.rolling(250).std()df['z'] = (df.close - m) / sdf['signal'] = df.z < -1.5df['pos'] = df.signal.shift(1).fillna(False).astype(int)df['ret'] = df.close.pct_change() * df.pos
Limites : supprimer l'anticipation rend le test honnête, pas rentable ; la règle corrigée a encore besoin de coûts, d'un moteur avec stops et d'une nouvelle période hors échantillon.
Points clés à retenir
- Conservez le code réutilisable dans
src/avec des tests, figez les versions, et faites en sorte que « run all » reproduise le rapport. - Fixez l'hypothèse, les règles et la séparation in-sample / out-of-sample avant le premier backtest ; n'exécutez le test hors échantillon qu'une seule fois.
- Passez en revue le timing, les données, les exécutions, les coûts et le nombre d'essais à l'aide d'une liste de contrôle ; journalisez chaque variante essayée.
- Indiquez la significativité approximativement par Sharpe × √années, le nombre de variantes et la décision, y compris « preuves insuffisantes ».
Étape suivante
Ainsi s'achève Python pour traders et investisseurs ; le cours suivant, « Prop firms, comptes financés et trading en tant qu'activité professionnelle », examine les règles d'évaluation, les types de drawdown ainsi que les aspects pratiques et fiscaux allemands du trading exercé comme activité professionnelle, pour lesquels un processus testé et scripté comme celui que vous avez construit ici constitue le point de départ.
Auto-évaluation
Répondez aux questions, puis vérifiez vos réponses. Vous pouvez recommencer autant de fois que vous le souhaitez.
Auto-évaluation automatique pour vos propres révisions. Vos réponses sont vérifiées dans votre navigateur ; nous ne les voyons pas et ne les évaluons pas. Ce n'est ni un examen ni une qualification.
Votre progression
« Terminée » signifie seulement que vous avez marqué la leçon comme lue. Ce n'est pas un test de votre compréhension. La progression et les notes sont enregistrées uniquement dans ce navigateur, sur cet appareil — pas dans un compte.