← Retour aux articles
March 7, 2026
5 min de lecture

Parité backtest-live : pourquoi votre bot trade différemment du backtest

#algotrading
#backtest
#live trading
#backtest-live parity
#execution
#NautilusTrader
🎯
Part 8 of 9 · Collection
Backtesting Without Fooling Yourself

Vous avez fait tourner une stratégie dans un backtest. Sharpe 2,1, MaxDD -8 %, PnL +67 %. Vous avez lancé le bot. Un mois plus tard, vous comparez : mêmes signaux, même période — mais le PnL en direct est inférieur de 40 %. Le drawdown est une fois et demie plus profond. Deux trades sur dix n'ont pas été exécutés du tout.

Ce n'est pas un bug. C'est la divergence backtest-live — un écart systématique entre les résultats du backtest et le trading réel. Tout le monde y est confronté. La seule question est de savoir si vous en avez conscience et si vous pouvez la maîtriser.

Cet article propose une taxonomie complète des divergences, des modèles architecturaux pour les minimiser, et une checklist pratique pour surveiller la parité en production.

Le syndrome du « ça marchait dans le backtest »

Divergence backtest vs live trading — courbe d'equity idéale par rapport à des résultats réels volatils

Tout algotrader traverse ce cycle :

  1. Écriture d'une stratégie dans un notebook Jupyter
  2. Backtest sur un CSV historique — les résultats sont excellents
  3. Réécriture de la logique sous forme de bot (souvent dans un autre langage ou framework)
  4. Lancement — les résultats ne correspondent pas
  5. Recherche d'un bug, sans en trouver — « le marché a changé »

Le problème n'est pas le marché. Le problème est que le backtest et le bot sont deux produits logiciels différents qui modélisent la même réalité de manière différente. Les divergences sont inévitables, mais elles peuvent être systématisées et minimisées.

Taxonomie des divergences

Taxonomie des divergences backtest-live

Toutes les sources de divergence se répartissent en quatre catégories. Pour chacune — une note de sévérité (de 1 à 5) et une contribution typique à la divergence de PnL.

1. Divergences de données (sévérité : 3/5)

Les données que voit le backtest et celles que voit le bot en temps réel ne sont pas identiques.

Horodatage. Les exchanges livrent les bougies avec des règles différentes d'attribution de l'horodatage. Un exchange marque la bougie avec le début de la période, un autre avec la fin. Une API REST peut renvoyer une bougie avec un délai de 1 à 3 secondes après la clôture réelle. Le backtest travaille avec des horodatages « idéaux » issus du fichier historique.

Agrégation OHLCV. Les données historiques sont souvent agrégées par le fournisseur différemment de la manière dont l'exchange le fait en temps réel. La différence se situe au dernier chiffre — mais avec des signaux à seuil (croisement de moyennes mobiles, cassure de niveau), cela détermine si la stratégie entre en position ou non.

Trous et données manquantes. Les données historiques sont généralement propres — les bougies manquantes sont comblées par interpolation. En temps réel, un WebSocket peut se couper, et le bot manque 30 secondes de données.

Contribution typique à la divergence de PnL : 2-5 % du PnL annuel.

2. Divergences d'exécution (sévérité : 5/5)

Divergences d'exécution des ordres — visualisation du slippage du carnet d'ordres, de la latence et des exécutions partielles

La classe de divergences la plus dangereuse. Le backtest simule l'exécution parfaitement — la réalité est loin d'être idéale.

Slippage. Le backtest remplit l'ordre au prix de clôture (ou au prix du signal). En réalité, un ordre au marché est exécuté au meilleur bid/ask plus un slippage qui dépend du volume et de la liquidité. Pour une position de 10 000 $ sur un altcoin à liquidité moyenne, le slippage peut être de 0,05 à 0,3 %.

Formule du slippage cumulé sur NN trades :

Slippagetotal=i=1Nsizei×si\text{Slippage}_{total} = \sum_{i=1}^{N} \text{size}_i \times s_i

sis_i est le slippage du ii-ème trade, dépendant de la profondeur du carnet d'ordres :

sisizeiLiquidity(ti)×ks_i \approx \frac{\text{size}_i}{\text{Liquidity}(t_i)} \times k

Latence. Entre le moment où un signal est généré et l'exécution de l'ordre, du temps s'écoule : calcul du signal (1-50 ms), transmission de la requête (10-200 ms), matching sur l'exchange (1-10 ms). Dans le backtest, la latence = 0. En direct, le prix peut bouger.

Exécutions partielles. Le backtest suppose que 100 % de l'ordre est rempli instantanément. En réalité, un ordre limite peut être partiellement rempli — ou pas rempli du tout si le prix s'inverse. Pour un ordre au marché sur un marché illiquide, l'ordre « glisse » à travers plusieurs niveaux du carnet d'ordres.

Priorité de file d'attente. Un ordre limite placé au meilleur prix bid ne sera pas exécuté immédiatement — il se met en file d'attente derrière tous les ordres déjà placés à ce niveau. Un backtest qui considère « prix touché = ordre exécuté » surestime systématiquement le taux d'exécution.

Contribution typique à la divergence de PnL : 10-30 % du PnL annuel.

3. Divergences de logique (sévérité : 4/5)

Ce sont des divergences dans le code même de la stratégie entre le backtest et le bot.

Bases de code séparées. L'anti-pattern classique : backtests/strategy_a.py et bot/strategy_a.py — deux fichiers séparés qui « font la même chose ». Après trois mois de modifications, ils divergent inévitablement. Quelqu'un a ajouté un filtre dans le backtest et a oublié de le répliquer dans le bot. Ou l'inverse — un bug a été corrigé dans le bot mais est resté dans le backtest.

Frameworks différents. Backtest sur pandas avec des opérations vectorisées, bot sur asyncio avec une logique événementielle. Même avec une stratégie identique, les cas limites sont traités différemment : arrondi, ordre de vérification des conditions, gestion des NaN.

Gestion d'état. Le backtest est généralement sans état — il itère sur un tableau de données. Le bot est avec état — il stocke les positions, les soldes, l'historique des ordres. Redémarrage du bot, perte d'état, désynchronisation avec l'exchange — tout cela constitue des sources de divergence.

Contribution typique à la divergence de PnL : 5-20 % du PnL annuel.

4. Divergences de coûts (sévérité : 3/5)

Divergences dans la modélisation des coûts de trading.

Funding rates. La plupart des backtests de futures perpétuels ne tiennent pas du tout compte des funding rates. Avec un levier de 10x et un taux moyen de 0,01 % toutes les 8 heures, cela donne 0.01%×3×365×10=109.5%0.01\% \times 3 \times 365 \times 10 = 109.5\% par an — plus que le PnL de la plupart des stratégies. Une analyse détaillée figure dans l'article Les funding rates tuent votre levier.

Commissions. Les commissions maker/taker sont généralement modélisées mais souvent avec le mauvais taux. Les paliers VIP, les remises BNB, les rebates — tout cela affecte le résultat final.

Spread. Un backtest basé sur des bougies ne voit pas le spread bid-ask. Sur une bougie d'1 minute, la clôture = 3000, mais en réalité bid = 2999,5 et ask = 3000,5. Chaque trade « coûte » la moitié du spread.

Contribution typique à la divergence de PnL : 5-15 % du PnL annuel.

Effet cumulatif

Les quatre catégories agissent simultanément et, en règle générale, dans une seule direction — contre le trader :

PnLlivePnLbacktestΔdataΔexecutionΔlogicΔcosts\text{PnL}_{live} \approx \text{PnL}_{backtest} - \Delta_{data} - \Delta_{execution} - \Delta_{logic} - \Delta_{costs}

Une divergence totale de 20 à 50 % par rapport au PnL du backtest est normale pour un système non raffiné. Avec du levier, l'effet est multiplié.

Modèles architecturaux pour la parité

Modèle 1 : Shared Core (extraction d'un noyau commun)

Architecture Shared Core — un seul module de stratégie alimentant à la fois les moteurs de backtest et de trading en direct

L'idée : extraire le noyau de la stratégie — génération de signaux et logique d'exécution — dans un module séparé utilisé à la fois par le backtest et le bot. Seule l'infrastructure environnante diffère : la source de données et le mécanisme de soumission des ordres.

┌─────────────────────────────────────┐
│         strategy_core.py            │
│  ┌─────────────┐ ┌───────────────┐  │
│  │ SignalEngine │ │ OrderManager  │  │
│  └──────┬──────┘ └──────┬────────┘  │
│         │               │           │
│    generate_signal()  create_order()│
└─────────┬───────────────┬───────────┘
          │               │
    ┌─────┴─────┐   ┌─────┴──────┐
    │ Backtest   │   │ Live       │
    │ DataFeed   │   │ DataFeed   │
    │ FillModel  │   │ Exchange   │
    └────────────┘   └────────────┘

from dataclasses import dataclass
from typing import Optional
import numpy as np

@dataclass
class Signal:
    side: str          # 'long' | 'short'
    entry_price: float
    sl_price: float
    tp_price: float
    size: float
    timestamp: int

@dataclass
class OrderRequest:
    side: str
    order_type: str    # 'market' | 'limit'
    price: float
    size: float

class StrategyCore:
    """
    Strategy core. Identical code for backtest and live.
    Depends only on data, not on infrastructure.
    """
    def __init__(self, params: dict):
        self.fast_period = params.get('fast_ma', 20)
        self.slow_period = params.get('slow_ma', 50)
        self.sl_pct = params.get('sl_pct', 0.02)
        self.tp_pct = params.get('tp_pct', 0.04)
        self.position: Optional[Signal] = None
        self._closes: list[float] = []

    def on_candle(self, timestamp: int, o: float, h: float,
                  l: float, c: float, v: float) -> Optional[OrderRequest]:
        """
        Process a new candle. Returns an OrderRequest or None.
        This method is called identically from the backtest and the bot.
        """
        self._closes.append(c)

        if len(self._closes) < self.slow_period:
            return None

        fast_ma = np.mean(self._closes[-self.fast_period:])
        slow_ma = np.mean(self._closes[-self.slow_period:])

        if self.position is not None:
            exit_order = self._check_exit(h, l, c)
            if exit_order:
                self.position = None
                return exit_order

        if self.position is None:
            if fast_ma > slow_ma and self._prev_fast_ma <= self._prev_slow_ma:
                self.position = Signal(
                    side='long', entry_price=c,
                    sl_price=c * (1 - self.sl_pct),
                    tp_price=c * (1 + self.tp_pct),
                    size=1.0, timestamp=timestamp,
                )
                return OrderRequest('buy', 'market', c, 1.0)

        self._prev_fast_ma = fast_ma
        self._prev_slow_ma = slow_ma
        return None

    def _check_exit(self, high: float, low: float,
                    close: float) -> Optional[OrderRequest]:
        pos = self.position
        if pos.side == 'long':
            if low <= pos.sl_price:
                return OrderRequest('sell', 'market', pos.sl_price, pos.size)
            if high >= pos.tp_price:
                return OrderRequest('sell', 'market', pos.tp_price, pos.size)
        return None

Désormais, le backtest et le bot utilisent le même StrategyCore :


from strategy_core import StrategyCore

def run_backtest(candles, params, fill_model):
    core = StrategyCore(params)
    trades = []

    for candle in candles:
        order = core.on_candle(
            candle['timestamp'], candle['open'], candle['high'],
            candle['low'], candle['close'], candle['volume'],
        )
        if order:
            fill_price = fill_model.simulate_fill(order, candle)
            trades.append({'price': fill_price, 'side': order.side})

    return trades

from strategy_core import StrategyCore

async def run_live(exchange, symbol, params):
    core = StrategyCore(params)

    async for candle in exchange.stream_candles(symbol, '1m'):
        order = core.on_candle(
            candle['timestamp'], candle['open'], candle['high'],
            candle['low'], candle['close'], candle['volume'],
        )
        if order:
            await exchange.place_order(symbol, order.side,
                                       order.order_type, order.size)

La règle clé : StrategyCore ne sait pas d'où viennent les données ni où les ordres sont envoyés. Il reçoit des OHLCV et renvoie une OrderRequest. Tout le reste relève de la responsabilité de la couche d'infrastructure.

Modèle 2 : Unification événementielle (approche NautilusTrader)

Architecture de trading événementielle avec pipeline d'événements en cascade — données de marché, signaux, ordres, exécutions

NautilusTrader met en œuvre la parité via un NautilusKernel unifié — un moteur natif en Rust avec un noyau événementiel déterministe et une résolution à la nanoseconde. La même implémentation de stratégie fonctionne aussi bien en backtest qu'en trading en direct.

L'architecture repose sur le modèle ports et adaptateurs (architecture hexagonale) :

┌──────────────────────────────────┐
│        NautilusKernel            │
│  ┌───────────┐  ┌─────────────┐  │
│  │ Strategy   │  │ RiskEngine  │  │
│  │ (Python)   │  │ (Rust)      │  │
│  └─────┬─────┘  └──────┬──────┘  │
│        │               │         │
│  ┌─────┴───────────────┴──────┐  │
│  │      Message Bus (Rust)    │  │
│  └─────┬───────────────┬──────┘  │
└────────┼───────────────┼─────────┘
         │               │
   ┌─────┴─────┐   ┌─────┴──────┐
   │ Backtest   │   │ Live       │
   │ Adapter    │   │ Adapter    │
   │ FillModel  │   │ Exchange   │
   │ (L2 book)  │   │ Gateway    │
   └────────────┘   └────────────┘

Avantages :

  • Replay déterministe. Les événements sont traités dans un ordre strictement défini — le résultat du backtest est reproductible bit à bit.
  • FillModel personnalisé. Simulation du carnet d'ordres L2 pour chaque exécution — le slippage est simulé en fonction de la profondeur réelle du carnet d'ordres.
  • Performance. Jusqu'à 5 millions de lignes/sec, traitement de données qui ne tiennent pas en RAM.
  • Redis + PostgreSQL. Cache et bus de messages via Redis, persistance via PostgreSQL — infrastructure identique pour le backtest et le direct.

Modèle 3 : Strategy Interface (approche Freqtrade)

Freqtrade utilise une interface unifiée IStrategy : la même classe de stratégie fonctionne à la fois en backtest et en direct. La seule différence est la couche de persistance.


class IStrategy:
    """Unified interface — the implementation does not know if this is a backtest or live."""

    def populate_indicators(self, dataframe, metadata):
        """Compute indicators."""
        dataframe['fast_ma'] = dataframe['close'].rolling(20).mean()
        dataframe['slow_ma'] = dataframe['close'].rolling(50).mean()
        return dataframe

    def populate_entry_trend(self, dataframe, metadata):
        """Determine entry signals."""
        dataframe.loc[
            (dataframe['fast_ma'] > dataframe['slow_ma']) &
            (dataframe['fast_ma'].shift(1) <= dataframe['slow_ma'].shift(1)),
            'enter_long'
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe, metadata):
        """Determine exit signals."""
        dataframe.loc[
            (dataframe['fast_ma'] < dataframe['slow_ma']),
            'exit_long'
        ] = 1
        return dataframe

Freqtrade offre en plus :

  • Hyperopt via Optuna — optimisation des paramètres de la stratégie
  • --timeframe-detail — descente vers une échelle de temps plus fine pour affiner les fills (similaire au drill-down adaptatif)

Comparaison des modèles

Shared Core Événementiel (NautilusTrader) Strategy Interface (Freqtrade)
Complexité d'implémentation Faible Élevée Moyenne
Niveau de parité Moyen Maximal Élevé
Simulation de fills FillModel séparé Carnet d'ordres L2 --timeframe-detail
Langage du noyau Python Rust + Python Python
Adapté pour Moteurs personnalisés Trading institutionnel Démarrage rapide

Précision de la simulation de fill

Niveaux de précision de la simulation de fill

La simulation de fill est la principale source de divergence d'exécution. Trois niveaux de précision :

Niveau 1 : Naïf (fill au prix de clôture)

fill_price = candle['close']

Erreur : ne tient compte ni du slippage, ni du spread, ni des exécutions partielles. Surestime systématiquement le PnL.

Niveau 2 : Modèle de slippage

def simulate_fill(order, candle, slippage_bps=5):
    """Fill with slippage."""
    base_price = candle['close']
    slip = base_price * slippage_bps / 10000

    if order.side == 'buy':
        return base_price + slip  # Buy at a higher price
    else:
        return base_price - slip  # Sell at a lower price

Erreur : un slippage fixe ne tient pas compte de la liquidité ni de la taille de l'ordre. Meilleur que le naïf, mais reste un modèle grossier.

Niveau 3 : Drill-down adaptatif avec des données 1s/100ms

La meilleure option : utiliser des données réelles à granularité fine pour déterminer précisément l'ordre d'exécution SL/TP. Décrit en détail dans l'article Drill-down adaptatif : backtesting à granularité variable.

class RealisticFillModel:
    """
    Combined fill model: slippage + spread + volume impact.
    """
    def __init__(self, avg_spread_bps=3, impact_coeff=0.1):
        self.avg_spread_bps = avg_spread_bps
        self.impact_coeff = impact_coeff

    def simulate_fill(self, order, candle, order_size_usd):
        base_price = candle['close']

        spread_cost = base_price * self.avg_spread_bps / 20000

        candle_volume_usd = candle['volume'] * candle['close']
        participation_rate = order_size_usd / max(candle_volume_usd, 1)
        impact = base_price * self.impact_coeff * np.sqrt(participation_rate)

        if order.side == 'buy':
            return base_price + spread_cost + impact
        else:
            return base_price - spread_cost - impact

Formule d'impact de marché (modèle Almgren-Chriss simplifié) :

Δp=σkVorderVmarket\Delta p = \sigma \cdot k \cdot \sqrt{\frac{V_{order}}{V_{market}}}

σ\sigma est la volatilité, kk le coefficient d'impact, VorderV_{order} le volume de l'ordre, et VmarketV_{market} le volume de marché pour la période.

Checklist pratique de parité

Checklist holographique de validation de la parité organisée par catégorie — données, exécution, timing, frais

Avant de lancer le bot en direct, vérifiez chaque point :

Code :

  • La stratégie utilise un shared core (un seul module pour backtest et live)
  • Pas de duplication de la logique de signal à deux endroits
  • Des tests unitaires vérifient des sorties identiques du noyau pour des entrées identiques
  • L'ordre de vérification des conditions est identique (SL avant TP ? TP avant SL ?)

Données :

  • Le format d'horodatage est identique (UTC, même fournisseur)
  • L'agrégation OHLCV suit les mêmes règles
  • La gestion des bougies manquantes est identique
  • Pas de look-ahead bias — le backtest ne regarde pas dans le futur

Exécution :

  • Le modèle de slippage est calibré sur des données réelles
  • Les exécutions partielles sont modélisées (ou au moins estimées de façon pessimiste)
  • Les ordres limites disposent d'un modèle de priorité de file d'attente
  • La latence est prise en compte (délai de 100 à 500 ms entre signal et fill)

Coûts :

  • Les commissions maker/taker sont incluses avec le taux actuel
  • Les funding rates sont prises en compte pour les futures perpétuels
  • Le spread est modélisé (au moins la moyenne)

Infrastructure :

  • Persistance d'état : le bot récupère les positions après un redémarrage
  • Logique de reconnexion : le WebSocket se reconnecte sans perte de données
  • Logging : tous les ordres et fills sont journalisés pour l'analyse post-mortem

Surveillance de la divergence en production

La parité n'est pas une vérification ponctuelle mais un processus continu. Après le lancement du bot, les divergences doivent être suivies en temps réel.

Mode shadow (paper trading)

Mode de trading shadow — données de marché en direct et ordres simulés s'exécutant en parallèle

Faites tourner le bot en parallèle du backtest sur les mêmes données. Le bot génère des signaux mais n'envoie pas d'ordres — il se contente de journaliser. Simultanément, le backtest traite les mêmes données. Comparez :

class DivergenceMonitor:
    """
    Compares backtest and live bot signals in real time.
    """
    def __init__(self, tolerance_pct=0.5):
        self.tolerance = tolerance_pct / 100
        self.divergences = []

    def compare_signal(self, backtest_signal, live_signal, timestamp):
        """Compare backtest and live signals."""
        if backtest_signal is None and live_signal is None:
            return  # Both silent — OK

        if (backtest_signal is None) != (live_signal is None):
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'signal_mismatch',
                'backtest': backtest_signal,
                'live': live_signal,
                'severity': 'HIGH',
            })
            return

        price_diff = abs(
            backtest_signal.entry_price - live_signal.entry_price
        ) / backtest_signal.entry_price

        if price_diff > self.tolerance:
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'price_divergence',
                'diff_pct': price_diff * 100,
                'severity': 'MEDIUM',
            })

    def compare_fill(self, backtest_fill, live_fill, timestamp):
        """Compare execution."""
        if backtest_fill and live_fill:
            slippage = (live_fill['price'] - backtest_fill['price']
                        ) / backtest_fill['price']
            self.divergences.append({
                'timestamp': timestamp,
                'type': 'fill_divergence',
                'slippage_bps': slippage * 10000,
                'severity': 'LOW' if abs(slippage) < 0.001 else 'MEDIUM',
            })

    def report(self):
        """Weekly divergence report."""
        from collections import Counter
        severity_counts = Counter(d['severity'] for d in self.divergences)
        return {
            'total_divergences': len(self.divergences),
            'by_severity': dict(severity_counts),
            'avg_slippage_bps': np.mean([
                d['slippage_bps'] for d in self.divergences
                if d['type'] == 'fill_divergence'
            ]) if any(d['type'] == 'fill_divergence'
                      for d in self.divergences) else 0,
        }

Métriques du dashboard

Métrique Formule Seuil d'alerte
Taux de correspondance des signaux matchestotal signals\frac{\text{matches}}{\text{total signals}} < 95 %
Slippage moyen 1Nsi\frac{1}{N}\sum s_i (bps) > 10 bps
Taux d'exécution filledsent\frac{\text{filled}}{\text{sent}} < 90 %
Divergence de PnL PnLlivePnLbtPnLbt\frac{PnL_{live} - PnL_{bt}}{PnL_{bt}} > 20 %
Latence p99 99e centile signal-à-fill > 500 ms

Calibration du modèle de slippage

Calibration du modèle de slippage — profondeur du carnet d'ordres avec courbe d'impact de prix montrant les fills attendus vs réels

Après avoir accumulé des données pendant 2 à 4 semaines, vous pouvez calibrer le modèle de slippage du backtest sur des données réelles :

def calibrate_slippage(live_fills: list[dict]) -> dict:
    """
    Calibrate slippage model using real fills.

    live_fills: [{'expected_price': ..., 'actual_price': ..., 'size_usd': ..., 'volume_usd': ...}]
    """
    slippages = []
    participation_rates = []

    for fill in live_fills:
        slip = abs(fill['actual_price'] - fill['expected_price']
                   ) / fill['expected_price']
        part = fill['size_usd'] / max(fill['volume_usd'], 1)
        slippages.append(slip)
        participation_rates.append(part)

    slippages = np.array(slippages)
    participation_rates = np.array(participation_rates)

    from scipy.optimize import curve_fit

    def model(x, k, base):
        return k * np.sqrt(x) + base

    popt, _ = curve_fit(model, participation_rates, slippages,
                        p0=[0.1, 0.0001])

    return {
        'impact_coeff': popt[0],
        'base_slippage': popt[1],
        'mean_slippage_bps': np.mean(slippages) * 10000,
        'p95_slippage_bps': np.percentile(slippages, 95) * 10000,
    }

Liens avec d'autres outils

La parité backtest-live n'est pas une tâche isolée. Elle recoupe d'autres outils de la série « Backtests sans illusions » :

  • Drill-down adaptatif — améliore la précision de la simulation de fill, un composant clé de la parité d'exécution.
  • Funding rates — si le backtest ne modélise pas le funding, la parité est impossible avec un levier > 3x.
  • Cache Parquet — des échelles de temps et indicateurs précalculés garantissent que le backtest voit les mêmes données que le bot. L'émulation de RunningCandleBuffer = mise à jour en temps réel.
  • Polars vs Pandas — en passant de pandas (backtest) à Polars (live), il faut s'assurer que les résultats numériques concordent.
  • Walk-Forward — le walk-forward sur des données out-of-sample montre comment la stratégie se dégrade — ce qui est plus proche du direct qu'un backtest in-sample.

Recommandations

  1. Le shared core est obligatoire. Une base de code unique pour la génération de signaux est l'exigence minimale pour la parité. Deux fichiers avec une logique identique garantissent une divergence en un mois.

  2. Calibrez le modèle de fill. Un slippage fixe de 5 bps vaut mieux que rien. Un modèle de slippage calibré sur des données réelles est nettement meilleur.

  3. Utilisez le mode shadow pendant les 2 à 4 premières semaines. Ne tradez pas avec de l'argent réel tant que le taux de correspondance des signaux n'atteint pas 95 % ou plus.

  4. Modélisez les funding rates. Pour les futures perpétuels, ce n'est pas optionnel — c'est obligatoire. Le funding peut absorber tout le PnL avec un levier > 5x.

  5. Journalisez tout. Chaque signal, chaque ordre, chaque fill — avec horodatage. Sans logs, l'analyse post-mortem est impossible.

  6. Automatisez la comparaison. Un rapport hebdomadaire du DivergenceMonitor devrait arriver automatiquement. N'attendez pas que le PnL devienne négatif.

  7. Backtest pessimiste par défaut. Il vaut mieux sous-estimer les attentes dans le backtest et être agréablement surpris en direct que l'inverse. Le modèle de slippage doit être conservateur.

Conclusion

Niveaux de maturité des systèmes de trading — du backtesting basique à la production complète

La parité backtest-live n'est pas une propriété d'un système mais un processus. La parité parfaite n'existe pas : un backtest est par définition un modèle de la réalité, et un modèle simplifie toujours. Mais la différence entre « le modèle diverge de 5 % » et « le modèle diverge de 50 % » est déterminée par l'architecture.

Trois niveaux de maturité :

  1. Basique. Shared core, slippage fixe, commissions. Divergence : 10-20 %.
  2. Avancé. Architecture événementielle, drill-down adaptatif, modèle de funding, mode shadow. Divergence : 5-10 %.
  3. Institutionnel. Simulation du carnet d'ordres L2, modèle d'impact calibré, monitoring de divergence en temps réel. Divergence : 2-5 %.

Votre tâche consiste à déterminer à quel niveau vous vous situez et à comprendre quelle divergence vous jugez acceptable pour votre taille de position et votre levier.


Liens utiles

  1. NautilusTrader — High-Performance Algorithmic Trading Platform
  2. Freqtrade — Free, open source crypto trading bot
  3. Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
  4. Lopez de Prado — Advances in Financial Machine Learning, Chapter 12: Backtesting
  5. Ernest Chan — Quantitative Trading: How to Build Your Own Algorithmic Trading Business
  6. Hexagonal Architecture (Ports and Adapters) — Alistair Cockburn
  7. Optuna — Hyperparameter Optimization Framework

Citation

@article{soloviov2026backtestliveparity,
  author = {Soloviov, Eugen},
  title = {Backtest-live parity: why your bot trades differently from the backtest},
  year = {2026},
  url = {https://marketmaker.cc/ru/blog/post/backtest-live-parity},
  description = {Complete taxonomy of divergences between backtesting and live trading: from slippage and partial fills to codebase desynchronization. Architectural patterns for achieving parity and a production monitoring checklist.}
}
blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Gardez une longueur d'avance sur le marché

Abonnez-vous à notre newsletter pour des insights exclusifs sur le trading IA, des analyses de marché et des mises à jour de la plateforme.

Nous respectons votre vie privée. Désabonnement possible à tout moment.