Backtest Python reproductible sans clé API
Lancez un backtest Python reproductible avec une installation figée et des données d’exemple déterministes, puis mesurez ce qu’une seule courbe de performance masque.
Un backtest reproductible en Python est un backtest qu’un tiers peut relancer sur une machine vierge et retrouver exactement vos chiffres, sans compte ni clé API pour l’en empêcher. La plupart des tutoriels échouent dès la première ligne : un téléchargement en temps réel fournit au lecteur suivant un historique de prix légèrement différent de celui fourni à l’auteur. Ce guide fixe un moteur open source, quantjourney-bt en version 0.12.4, exécute son exemple intégré sans aucun identifiant, puis utilise des données de marché réelles pour montrer ce qu’une seule exécution propre ne permet toujours pas de déterminer.
Qu’est-ce qui rend un backtest reproductible ?
La reproductibilité a ici un sens précis et vérifiable : une deuxième personne exécute une seule commande sur une machine vierge et retrouve vos chiffres à la décimale près. Deux éléments courants la compromettent.
Le premier est le code. Une bibliothèque en version 0.x n’offre aucune garantie de compatibilité entre versions mineures. Un renommage, une valeur par défaut modifiée ou une colonne réordonnée peuvent laisser votre script s’exécuter tout en produisant discrètement un autre résultat.
Le second est la data. Un tutoriel dont la première ligne télécharge des données en temps réel n’a jamais été reproductible. Les fournisseurs révisent l’historique, ajustent les cours pour tenir compte des splits et complètent les données manquantes. Le même script affiche alors des chiffres différents un mois plus tard. Après coup, vous ne pouvez pas distinguer une modification du code d’une modification de la data.
Le principe à retenir de ce projet consiste à associer une version fixe du moteur à un petit dataset intégré, inclus directement dans le package.
Épingler l’installation : pip install quantjourney-bt==0.12.4
quantjourney-bt est le backtester QuantJourney, distribué sous licence Apache 2.0 et nécessitant Python 3.11 ou une version ultérieure. La version 0.12.4 a été publiée le 21 juillet 2026. C’est la version utilisée par toutes les commandes ci-dessous, comme indiqué dans la documentation d’août 2026.
Travaillez dans un environnement isolé. python3 -m venv .venv en crée un, source .venv/bin/activate l’active et python -m pip install -U pip met à jour l’installateur à l’intérieur de cet environnement. Utilisez ensuite la version exacte : pip install quantjourney-bt==0.12.4.
Le projet documente la forme sans version verrouillée, pip install quantjourney-bt. La partie ==0.12.4 relève de votre responsabilité. Avec une version 0.x, elle est particulièrement importante. Écrivez cette contrainte là où la personne suivante pourra la trouver : pip freeze > requirements.txt enregistre toutes les dépendances résolues, y compris celles que vous n’avez jamais spécifiées.
Deux extras facultatifs sont disponibles. pip install "quantjourney-bt[wf]" ajoute Optuna pour les exemples de walk-forward et d’optimisation. pip install "quantjourney-bt[data]" ajoute une solution de repli fondée sur yfinance, utilisée pour les benchmarks.
La licence Apache-2.0 est permissive. Vous pouvez utiliser et modifier le code à des fins commerciales. Vous devez conserver les fichiers de licence et de mentions légales lors de toute redistribution. Les contributeurs accordent aussi explicitement des droits relatifs aux brevets.
Comment exécuter l’exemple SMA fourni sans clé API
Le dépôt fournit un script de lancement avec cinquante stratégies d’exemple exécutables, réparties entre une approche fondée sur les pondérations et une approche fondée sur les ordres. Cinq workflows walk-forward en font partie. ./strategy.sh --list affiche le catalogue. ./strategy.sh example_weights_01_sma_daily --check importe une seule stratégie et n’accède à aucune donnée. C’est la vérification la plus rapide pour confirmer qu’une installation fonctionne.
L’exécution de démonstration tient sur une ligne : ./strategy.sh example_weights_01_sma_daily --sample-data --output /tmp/qj-sample
L’option --sample-data porte l’essentiel du message. Le projet décrit ainsi le dataset utilisé :
Le dataset d’exemple est volontairement réduit et reproductible. Il permet de vérifier l’installation, de générer des rapports et de suivre le fonctionnement du moteur sans créer de compte.
Source : README de quantjourney-bt, version 0.12.4, consulté le 6 août 2026.
L’exécution écrit un répertoire au lieu d’afficher un résultat dans la console : summary.txt et summary.json, un metrics.csv, un equity_curve.csv à côté de son equity_curve.png, un dashboard.html, un dossier plots/ et un run_metadata.json qui enregistre la configuration de l’exécution. C’est le fichier que la plupart des utilisateurs négligent. C’est aussi celui qui permet de rendre un résultat auditable un an plus tard.
Interprétez les métriques obtenues avec prudence. Le dataset fourni est réduit et illustratif. Le ratio de Sharpe et le drawdown maximal affichés dans summary.txt décrivent un fichier d’exemple. Ils ne prouvent rien sur une stratégie. Les considérer comme un résultat constitue la première erreur possible.
L’exécution établit néanmoins des points utiles : l’installation fonctionne et l’ensemble du flux du moteur, du signal aux pondérations cibles puis à la reconstitution de la valeur du portefeuille, produit ses artefacts sur votre machine sans aucun identifiant. Une voie avec authentification vers le propre service de données du projet existe pour effectuer des backtests sur un historique réel. Cette voie est documentée. Ce guide s’arrête ici, à l’étape qui ne nécessite rien de personne.
Ce qu’une seule courbe de performance in-sample ne permet pas de conclure
L’exécution sur l’échantillon produit une courbe de performance. Voici ce que cette courbe ne permet pas de démontrer, sur des données de marché réelles plutôt que sur un fichier de démonstration.
Le panneau ci-dessous reprend le principe de la stratégie d’exemple : une moyenne mobile à 20 séances croise une moyenne mobile à 50 séances. Il l’applique à SPY et présente séparément chaque année civile de 2017 à 2025. La position pour chaque séance est déterminée par le cours de clôture de la séance précédente. La règle ne négocie donc jamais sur la base d’une donnée qui n’était pas encore disponible.
Le SQL exact derrière chaque chiffre
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
countIf(long_today != long_prior) AS crossover_count
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearUne seule règle inchangée, mesurée 9 fois séparément. Lisez d’abord les deux colonnes de pourcentage. En 2017, la règle a terminé l’année à 16%, contre 19.4% pour une détention de SPY sur la même période. En 2025, ces deux mêmes colonnes affichent 10.4% et 16.4%. Le code est identique dans les deux lignes. Seule la fenêtre a changé.
La colonne des croisements montre à quel point les éléments observés sont limités. 4 changements de position sur 2025 signifient qu’une année complète de courbe de performance repose sur une poignée de décisions. C’est un échantillon très réduit pour tirer une conclusion.
La même règle se comporte-t-elle de la même façon sur d’autres valeurs ?
Modifier la fenêtre temporelle est une façon d’interroger une courbe unique. Modifier l’univers en est une autre. Le panneau ci-dessous conserve les paramètres et applique la même règle à cinq valeurs liquides sur les cinq années civiles 2021 à 2025.
Le SQL exact derrière chaque chiffre
WITH daily AS
(
SELECT
ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'KO')
AND window_start >= '2020-07-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY ticker, d
),
averaged AS
(
SELECT
ticker,
d,
px,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (PARTITION BY ticker ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
ticker,
d,
px,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (PARTITION BY ticker ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
ticker AS symbol,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS rule_pct,
round((exp(sum(log(px / px_prior))) - 1) * 100, 1) AS hold_pct,
round(avg(long_prior) * 100, 0) AS days_long_pct
FROM positioned
WHERE px_prior > 0
AND d >= toDate('2021-01-01')
GROUP BY symbol
ORDER BY rule_pct DESCQQQ se situe en haut du panneau, à 40.2%, tandis que la ligne du bas, KO, ressort à 3.8%. La colonne days_long_pct indique la part de la fenêtre pendant laquelle chaque version a détenu une position, quelle qu’elle soit ; cette proportion s’élève à 67% pour la première ligne. Un seul jeu de paramètres, cinq univers et un spread suffisamment large pour que choisir le gagnant a posteriori ne dise rien de la performance d’une exécution que vous n’avez pas encore réalisée.
Il ne s’agit en aucun cas d’une recommandation de trader un crossover. Le crossover sert de référence pour mesurer le backtest, et c’est le backtest que nous mesurons.
Où le look-ahead bias s’introduit dans un backtest fondé sur des poids
Un moteur fondé sur des poids transforme un signal en poids cibles, simule les exécutions sur la base de ces poids, puis reconstitue la valeur du portefeuille à partir des positions obtenues. L’erreur se dissimule dans la liaison entre le signal et le poids. Si le poids du jour est calculé à partir de la clôture du jour, puis génère le rendement du jour, le backtest a opéré sur la base d’informations qui n’existaient pas encore au moment où l’ordre aurait été exécuté. C’est du look-ahead bias, et cela ne génère aucune erreur. Tout semble simplement meilleur.
Le panneau ci-dessous exécute les deux versions d’une même règle sur le même historique de SPY.
Le SQL exact derrière chaque chiffre
WITH daily AS
(
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
toFloat64(argMax(close, window_start)) AS px
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= '2016-01-01 00:00:00'
AND window_start < '2026-01-01 05:00:00'
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
+ toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
GROUP BY d
),
averaged AS
(
SELECT
d,
px,
avg(px) OVER (ORDER BY d ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS fast_ma,
avg(px) OVER (ORDER BY d ROWS BETWEEN 49 PRECEDING AND CURRENT ROW) AS slow_ma,
row_number() OVER (ORDER BY d) AS session_no
FROM daily
),
positioned AS
(
SELECT
d,
px,
if(session_no >= 50 AND fast_ma > slow_ma, 1, 0) AS long_today,
lagInFrame(if(session_no >= 50 AND fast_ma > slow_ma, 1, 0), 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS long_prior,
lagInFrame(px, 1)
OVER (ORDER BY d ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS px_prior
FROM averaged
)
SELECT
toYear(d) AS year,
round((exp(sum(log(if(long_prior = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS next_bar_pct,
round((exp(sum(log(if(long_today = 1, px / px_prior, 1.0)))) - 1) * 100, 1) AS same_bar_pct,
round(abs(exp(sum(log(if(long_today = 1, px / px_prior, 1.0))))
- exp(sum(log(if(long_prior = 1, px / px_prior, 1.0))))) * 100, 1) AS gap_pp
FROM positioned
WHERE px_prior > 0
AND toYear(d) >= 2017
GROUP BY year
ORDER BY yearDans 2017, la version fondée sur la séance précédente affichait 16%, contre 17.1% pour la version fondée sur la même séance, soit un écart de 1.1 points de pourcentage. Dans 2025, l’écart entre les deux mesures atteignait 1.6 points de pourcentage. Une seule de ces colonnes peut être produite par une machine qui ne connaissait pas déjà le niveau de clôture de la séance. Leur différence relève uniquement de la comptabilité, sans idée, sans compétence et sans trade sous-jacent.
Ce moteur indique lui-même comment il traite la question du calendrier, ce qui vaut mieux qu’une simple promesse :
Pour les exécutions à l’ouverture, le slippage sensible à la fourchette ne tient compte que de la barre précédente entièrement clôturée, et la capacité liée au volume est estimée à partir d’observations décalées ; le moteur n’utilise pas le plus haut, le plus bas, la clôture ni le volume total de la séance en cours.
Source : README de quantjourney-bt, version 0.12.4, consulté le 6 août 2026.
Une hypothèse documentée peut être vérifiée à partir du code source que vous avez déjà installé. Une hypothèse non documentée reste une supposition.
Pourquoi le module walk-forward supplémentaire est utile
Les exemples walk-forward, de WF01 à WF05, sont fournis avec le module supplémentaire [wf] et sa dépendance à Optuna. La méthode walk-forward ajuste les paramètres sur une période de l’historique, puis les évalue sur la période suivante. Elle fait ensuite avancer les deux périodes et répète l’opération. Les variantes rolling et expanding se distinguent par le traitement de la fenêtre d’ajustement : dans le premier cas, les données les plus anciennes sont retirées à mesure que la fenêtre avance. Un autre exemple ajoute un purge et un embargo à chaque frontière. Les observations les plus proches de la jonction sont supprimées. Ainsi, les données utilisées pour l’ajustement ne peuvent pas contaminer la période sur laquelle le modèle est évalué.
Aucune de ces méthodes ne transforme une idée faible en stratégie viable. Elle remplace un chiffre unique par une distribution de chiffres que vous pouvez analyser et remettre en question. C’est l’essentiel de l’amélioration. L’étape suivante ne consiste toujours pas à investir de l’argent réel : le paper trading avant l’argent réel mesure ce qu’un backtest ne peut pas voir structurellement. Il vérifie notamment si vos ordres sont exécutés à un prix proche de celui retenu par le simulateur. Les disjoncteurs pour les robots de trading encadrent, eux, le comportement de votre code le jour où il ne fonctionne pas comme prévu. Pour les statistiques qui sous-tendent l’ensemble, nos notes sur le livre open source sur le trading quantitatif vont plus loin.
FAQ
Peut-on backtester une stratégie sans clé API ?
Oui. quantjourney-bt fournit un jeu de données d’exemple intégré derrière un indicateur --sample-data. Ses stratégies d’exemple s’exécutent avec ce jeu de données, sans compte ni identifiants. Le jeu de données est réduit et purement illustratif. Considérez donc ce test comme une vérification de l’installation et du pipeline, et non comme une preuve de la pertinence d’une stratégie.
Pourquoi figer la version d’un package Python de backtesting ?
Un package en version 0.x n’offre aucune garantie de compatibilité entre les versions mineures. La modification d’une valeur par défaut ou le changement de nom d’une métrique peut passer inaperçu. Le fait de figer la version avec pip install quantjourney-bt==0.12.4 et de consigner l’environnement dans un fichier requirements permet de reproduire l’an prochain un résultat obtenu aujourd’hui avec le moteur qui l’a produit.
Sous quelle licence quantjourney-bt est-il distribué ?
Apache License 2.0. Cette licence autorise l’utilisation commerciale et les modifications. Elle impose de conserver les fichiers de licence et de mentions légales avec toute redistribution. Elle prévoit également une concession explicite de droits de brevet de la part des contributeurs. La version 0.12.4 a été publiée le 21 juillet 2026 et nécessite Python 3.11 ou une version ultérieure.
Un résultat de backtest solide signifie-t-il que la stratégie fonctionne ?
Non. Un backtest constitue une seule mesure, sur une seule période et sur un seul univers. Les panneaux ci-dessus montrent qu’une même règle inchangée peut produire des résultats annuels très différents sur un ticker et des résultats très différents sur cinq valeurs. C’est précisément l’écart que la validation walk-forward et les tests out-of-sample cherchent à mettre en évidence.
Méthode de calcul des panneaux ci-dessus
Les clôtures quotidiennes correspondent au dernier print de la séance régulière pour chaque date. Elles sont relevées selon l’heure de New York entre 9 h 30 et 16 h 00. Cette méthode traite correctement les séances écourtées sans coder en dur la durée d’une séance. La moyenne rapide porte sur 20 séances et la moyenne lente sur 50 séances. Toutes deux sont des moyennes simples. Les 49 premières séances de chaque série constituent la période de chauffe et aucune position n’y est détenue. Les chiffres annuels capitalisent la variation de clôture à clôture de chaque séance pendant laquelle la règle était long. La colonne hold capitalise, à titre de comparaison, chaque séance de la même année. Les cinq valeurs de la coupe transversale ont été sélectionnées pour disposer d’historiques continus et ne présenter aucun split sur la période. La série des cours de clôture ne nécessite donc aucun ajustement. Les périodes sont fixes et situées dans le passé. Ces panneaux renvoient donc les mêmes chiffres à chaque nouvelle génération.
Chaque panneau présenté ici contient la requête SQL exacte utilisée en dessous. Les chiffres de cette page peuvent ainsi être recalculés aussi facilement que l’installation dont la version est figée. Pour mesurer une règle sur votre propre période avant d’écrire le moindre code de backtest, formulez la question en anglais courant sur le terminal Strasmore.