← Terug naar artikelen
March 7, 2026
5 min leestijd

Backtest-live pariteit: waarom je bot anders handelt dan in de backtest

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

Je hebt een strategie door een backtest gehaald. Sharpe 2,1, MaxDD -8%, PnL +67%. Je hebt de bot gelanceerd. Een maand later vergelijk je: dezelfde signalen, dezelfde periode — maar de live PnL is 40% lager. De drawdown is anderhalf keer zo diep. Twee van de tien trades werden helemaal niet uitgevoerd.

Dit is geen bug. Dit is backtest-live divergentie — een systematische afwijking tussen backtestresultaten en echte trading. Iedereen heeft ermee te maken. De enige vraag is of je het weet en of je het onder controle kunt houden.

Dit artikel geeft een volledige taxonomie van de afwijkingen, architectuurpatronen om ze te minimaliseren, en een praktische checklist voor het bewaken van pariteit in productie.

Het "het werkte in de backtest"-syndroom

Backtest vs live trading divergentie — ideale equity-curve versus echte volatiele resultaten

Elke algotrader doorloopt deze cyclus:

  1. Een strategie geschreven in een Jupyter-notebook
  2. Een backtest uitgevoerd op historische CSV — de resultaten zijn geweldig
  3. De logica herschreven als een bot (vaak in een andere taal of framework)
  4. Gelanceerd — de resultaten komen niet overeen
  5. Naar een bug gezocht, geen gevonden — "de markt is veranderd"

Het probleem is niet de markt. Het probleem is dat de backtest en de bot twee verschillende softwareproducten zijn die dezelfde werkelijkheid anders modelleren. Afwijkingen zijn onvermijdelijk, maar ze kunnen worden gesystematiseerd en geminimaliseerd.

Taxonomie van afwijkingen

Taxonomie van backtest-live afwijkingen

Alle bronnen van afwijking vallen in vier categorieën. Voor elke — een ernstclassificatie (van 1 tot 5) en een typische bijdrage aan de PnL-afwijking.

1. Data-afwijkingen (ernst: 3/5)

De data die de backtest ziet en de data die de bot in real time ziet, zijn niet hetzelfde.

Timestamps. Exchanges leveren candles met verschillende regels voor timestamp-toewijzing. De ene exchange markeert de candle met het begin van de periode, een andere met het einde. Een REST-API kan een candle 1-3 seconden na de daadwerkelijke close retourneren. De backtest werkt met "ideale" timestamps uit het historische bestand.

OHLCV-aggregatie. Historische data worden door de provider vaak anders geaggregeerd dan de exchange dat in real time doet. Het verschil zit in het laatste cijfer — maar bij drempelsignalen (MA-crossover, level breakout) bepaalt dit of de strategie een positie inneemt of niet.

Gaten en ontbrekende data. Historische data zijn meestal schoon — ontbrekende candles worden opgevuld door interpolatie. In real time kan een WebSocket wegvallen, en mist de bot 30 seconden aan data.

Typische bijdrage aan PnL-afwijking: 2-5% van de jaarlijkse PnL.

2. Uitvoeringsafwijkingen (ernst: 5/5)

Afwijkingen in orderuitvoering — orderboek slippage, latentie en gedeeltelijke fills gevisualiseerd

De gevaarlijkste klasse van afwijkingen. De backtest simuleert de uitvoering perfect — de werkelijkheid is verre van ideaal.

Slippage. De backtest vult de order tegen de slotkoers (of de signaalkoers). In werkelijkheid wordt een marktorder uitgevoerd tegen de beste bid/ask plus slippage die afhankelijk is van volume en liquiditeit. Voor een positie van $10K in een altcoin met gemiddelde liquiditeit kan de slippage 0,05-0,3% bedragen.

Formule voor cumulatieve slippage over NN trades:

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

waarbij sis_i de slippage van de ii-de trade is, afhankelijk van de orderboekdiepte:

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

Latentie. Vanaf het moment waarop een signaal wordt gegenereerd tot de orderuitvoering verstrijkt er tijd: signaalberekening (1-50 ms), verzending van het verzoek (10-200 ms), matching op de exchange (1-10 ms). In de backtest is latentie = 0. Live kan de prijs bewegen.

Gedeeltelijke fills. De backtest gaat ervan uit dat 100% van de order direct wordt gevuld. In werkelijkheid kan een limietorder gedeeltelijk worden gevuld — of helemaal niet als de prijs omkeert. Bij een marktorder in een illiquide markt "glijdt" de order door meerdere orderboekniveaus.

Wachtrijprioriteit. Een limietorder geplaatst tegen de beste bid-prijs wordt niet onmiddellijk gevuld — hij komt achteraan in de rij achter alle eerder op dat niveau geplaatste orders. Een backtest die "prijs geraakt = order gevuld" aanneemt, overschat systematisch de fill rate.

Typische bijdrage aan PnL-afwijking: 10-30% van de jaarlijkse PnL.

3. Logica-afwijkingen (ernst: 4/5)

Dit zijn afwijkingen in de strategiecode zelf tussen de backtest en de bot.

Gescheiden codebases. Het klassieke antipatroon: backtests/strategy_a.py en bot/strategy_a.py — twee afzonderlijke bestanden die "hetzelfde doen". Na drie maanden wijzigingen wijken ze onvermijdelijk af. Iemand heeft een filter toegevoegd in de backtest en vergat dit in de bot te repliceren. Of omgekeerd — een bug is opgelost in de bot maar bleef in de backtest bestaan.

Verschillende frameworks. Backtest op pandas met vectorized operations, bot op asyncio met event-driven logica. Zelfs bij een identieke strategie worden edge cases anders behandeld: afronding, volgorde van conditiecontroles, omgang met NaN.

Statebeheer. De backtest is meestal stateless — hij itereert over een dataarray. De bot is stateful — hij bewaart posities, saldi, orderhistorie. Herstart van de bot, verlies van state, desynchronisatie met de exchange — dit zijn allemaal bronnen van afwijking.

Typische bijdrage aan PnL-afwijking: 5-20% van de jaarlijkse PnL.

4. Kostenafwijkingen (ernst: 3/5)

Afwijkingen in de modellering van handelskosten.

Funding rates. De meeste backtests van perpetual futures houden helemaal geen rekening met funding rates. Bij 10x hefboom en een gemiddeld tarief van 0,01% per 8 uur is dat 0.01%×3×365×10=109.5%0.01\% \times 3 \times 365 \times 10 = 109.5\% per jaar — meer dan de PnL van de meeste strategieën. Een gedetailleerde analyse staat in het artikel Funding rates verpesten je hefboom.

Commissies. Maker/taker-commissies worden meestal gemodelleerd, maar vaak met het verkeerde tarief. VIP-niveaus, BNB-kortingen, rebates — dit alles beïnvloedt het uiteindelijke resultaat.

Spread. Een candle-gebaseerde backtest ziet de bid-ask spread niet. Bij een 1-minuut candle is close = 3000, maar in werkelijkheid is bid = 2999,5 en ask = 3000,5. Elke trade "kost" de helft van de spread.

Typische bijdrage aan PnL-afwijking: 5-15% van de jaarlijkse PnL.

Cumulatief effect

Alle vier categorieën werken gelijktijdig en gewoonlijk in één richting — tegen de trader:

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

Een totale afwijking van 20-50% ten opzichte van de backtest-PnL is normaal voor een niet-verfijnd systeem. Met hefboom wordt het effect vermenigvuldigd.

Architectuurpatronen voor pariteit

Patroon 1: Shared Core (extractie van een gemeenschappelijke kern)

Shared Core-architectuur — één strategiemodule die zowel backtest- als live trading-engines aandrijft

Het idee: de strategiekern — signaalgeneratie en uitvoeringslogica — uit te trekken in een aparte module die zowel door de backtest als door de bot wordt gebruikt. Alleen de omringende infrastructuur verschilt: de databron en het mechanisme voor het indienen van orders.

┌─────────────────────────────────────┐
│         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

Nu gebruiken de backtest en de bot dezelfde 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)

De kernregel: StrategyCore weet niet waar de data vandaan komen of waar orders naartoe worden gestuurd. Hij ontvangt OHLCV en geeft een OrderRequest terug. Al het overige is de verantwoordelijkheid van de infrastructuurlaag.

Patroon 2: Event-driven unificatie (NautilusTrader-aanpak)

Event-driven trading-architectuur met cascaderende event-pipeline — marktdata, signalen, orders, fills

NautilusTrader implementeert pariteit via een uniforme NautilusKernel — een Rust-native engine met een deterministische, event-driven kern en nanoseconde-resolutie. Dezelfde strategie-implementatie werkt zowel in de backtest als in live trading.

De architectuur is gebouwd op het ports-and-adapters-patroon (hexagonale architectuur):

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

Voordelen:

  • Deterministische replay. Events worden verwerkt in een strikt vastgelegde volgorde — het backtest-resultaat is bit-voor-bit reproduceerbaar.
  • Custom FillModel. L2-orderboeksimulatie voor elke uitvoering — slippage wordt gesimuleerd op basis van reële orderboekdiepte.
  • Prestaties. Tot 5 miljoen rijen/sec, verwerking van data die niet in het RAM passen.
  • Redis + PostgreSQL. Cache en message bus via Redis, persistentie via PostgreSQL — identieke infrastructuur voor backtest en live.

Patroon 3: Strategy Interface (Freqtrade-aanpak)

Freqtrade gebruikt een uniforme IStrategy-interface: dezelfde strategieklasse werkt zowel in de backtest als live. Het enige verschil is de persistentielaag.


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 biedt daarnaast:

  • Hyperopt via Optuna — optimalisatie van strategieparameters
  • --timeframe-detail — drill-down naar een fijnmaziger tijdsbestek voor fill-verfijning (vergelijkbaar met adaptieve drill-down)

Vergelijking van patronen

Shared Core Event-driven (NautilusTrader) Strategy Interface (Freqtrade)
Implementatiecomplexiteit Laag Hoog Gemiddeld
Pariteitsniveau Gemiddeld Maximaal Hoog
Fill-simulatie Aparte FillModel L2-orderboek --timeframe-detail
Kerntaal Python Rust + Python Python
Geschikt voor Custom engines Institutionele trading Snelle start

Nauwkeurigheid van fill-simulatie

Nauwkeurigheidsniveaus van fill-simulatie

Fill-simulatie is de belangrijkste bron van uitvoeringsafwijking. Drie nauwkeurigheidsniveaus:

Niveau 1: Naïef (fill tegen slotkoers)

fill_price = candle['close']

Fout: houdt geen rekening met slippage, spread of gedeeltelijke fills. Overschat de PnL systematisch.

Niveau 2: Slippagemodel

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

Fout: een vaste slippage houdt geen rekening met liquiditeit en ordergrootte. Beter dan naïef, maar nog steeds een grof model.

Niveau 3: Adaptieve drill-down met 1s/100ms-data

De beste optie: echte fijnmazige data gebruiken voor een nauwkeurige bepaling van de SL/TP-fillvolgorde. Uitgebreid beschreven in het artikel Adaptieve drill-down: backtesten met variabele granulariteit.

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 voor market impact (vereenvoudigd Almgren-Chriss-model):

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

waarbij σ\sigma de volatiliteit is, kk de impact-coëfficiënt, VorderV_{order} het ordervolume, en VmarketV_{market} het marktvolume voor de periode.

Praktische pariteitschecklist

Holografische checklist voor pariteitsvalidatie, georganiseerd per categorie — data, uitvoering, timing, kosten

Controleer voordat je de bot live lanceert elk punt:

Code:

  • De strategie gebruikt een shared core (één module voor backtest en live)
  • Geen duplicatie van signaallogica op twee plaatsen
  • Unit tests verifiëren identieke kernoutputs voor identieke inputs
  • De volgorde van conditiecontroles is identiek (SL vóór TP? TP vóór SL?)

Data:

  • Het timestampformaat is identiek (UTC, dezelfde provider)
  • OHLCV-aggregatie gebruikt dezelfde regels
  • De omgang met ontbrekende candles is identiek
  • Geen look-ahead bias — de backtest kijkt niet vooruit in de tijd

Uitvoering:

  • Het slippagemodel is gekalibreerd op reële data
  • Gedeeltelijke fills worden gemodelleerd (of ten minste pessimistisch geschat)
  • Limietorders hebben een model voor wachtrijprioriteit
  • Latentie wordt meegenomen (100-500 ms vertraging van signaal tot fill)

Kosten:

  • Maker/taker-commissies zijn opgenomen met het huidige tarief
  • Funding rates worden meegenomen bij perpetual futures
  • De spread wordt gemodelleerd (op zijn minst het gemiddelde)

Infrastructuur:

  • Statepersistentie: de bot herstelt posities na een herstart
  • Reconnectielogica: WebSocket verbindt opnieuw zonder dataverlies
  • Logging: alle orders en fills worden gelogd voor post-mortemanalyse

Afwijking monitoren in productie

Pariteit is geen eenmalige controle maar een continu proces. Na het lanceren van de bot moeten afwijkingen in real time worden gevolgd.

Shadow mode (paper trading)

Shadow trading-modus — live marktdata en gesimuleerde orders die parallel draaien

Draai de bot parallel aan de backtest op dezelfde data. De bot genereert signalen maar stuurt geen orders — hij logt alleen. Tegelijkertijd verwerkt de backtest dezelfde data. Vergelijk:

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,
        }

Dashboardmetrieken

Metriek Formule Waarschuwingsdrempel
Signaalovereenkomstpercentage matchestotal signals\frac{\text{matches}}{\text{total signals}} < 95%
Gemiddelde slippage 1Nsi\frac{1}{N}\sum s_i (bps) > 10 bps
Fill rate filledsent\frac{\text{filled}}{\text{sent}} < 90%
PnL-afwijking PnLlivePnLbtPnLbt\frac{PnL_{live} - PnL_{bt}}{PnL_{bt}} > 20%
Latentie p99 99e percentiel signaal-tot-fill > 500 ms

Kalibratie van het slippagemodel

Kalibratie van het slippagemodel — orderboekdiepte met price impact-curve die verwachte versus werkelijke fills toont

Nadat je 2-4 weken data hebt verzameld, kun je het slippagemodel van de backtest kalibreren op reële data:

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,
    }

Verbanden met andere tools

Backtest-live pariteit is geen geïsoleerde taak. Het overlapt met andere tools uit de serie "Backtests zonder illusies":

  • Adaptieve drill-down — verbetert de nauwkeurigheid van fill-simulatie, een sleutelonderdeel van uitvoeringspariteit.
  • Funding rates — als de backtest geen funding modelleert, is pariteit onmogelijk bij een hefboom > 3x.
  • Parquet-cache — vooraf berekende tijdsbestekken en indicatoren zorgen ervoor dat de backtest dezelfde data ziet als de bot. RunningCandleBuffer-emulatie = real-time bijwerken.
  • Polars vs Pandas — bij de overstap van pandas (backtest) naar Polars (live) moet je ervoor zorgen dat de numerieke resultaten overeenkomen.
  • Walk-Forward — walk-forward op out-of-sample data laat zien hoe de strategie degradeert — dit ligt dichter bij live dan een in-sample backtest.

Aanbevelingen

  1. Shared core is verplicht. Eén codebasis voor signaalgeneratie is de minimale vereiste voor pariteit. Twee bestanden met identieke logica garanderen afwijking binnen een maand.

  2. Kalibreer het fill-model. Een vaste slippage van 5 bps is beter dan niets. Een op reële data gekalibreerd slippagemodel is aanzienlijk beter.

  3. Gebruik shadow mode voor de eerste 2-4 weken. Handel niet met echt geld totdat het signaalovereenkomstpercentage 95%+ bereikt.

  4. Modelleer funding rates. Voor perpetual futures is dit niet optioneel — het is verplicht. Funding kan bij een hefboom > 5x de hele PnL opslokken.

  5. Log alles. Elk signaal, elke order, elke fill — met timestamps. Zonder logs is post-mortemanalyse onmogelijk.

  6. Automatiseer de vergelijking. Een wekelijks DivergenceMonitor-rapport zou automatisch moeten binnenkomen. Wacht niet tot de PnL negatief wordt.

  7. Standaard pessimistische backtest. Het is beter om de verwachtingen in de backtest te onderschatten en aangenaam verrast te worden live dan andersom. Het slippagemodel moet conservatief zijn.

Conclusie

Volwassenheidsniveaus van handelssystemen — van basaal backtesten tot volledige productie

Backtest-live pariteit is geen eigenschap van een systeem maar een proces. Perfecte pariteit bestaat niet: een backtest is per definitie een model van de werkelijkheid, en een model vereenvoudigt altijd. Maar het verschil tussen "het model wijkt 5% af" en "het model wijkt 50% af" wordt bepaald door de architectuur.

Drie volwassenheidsniveaus:

  1. Basaal. Shared core, vaste slippage, commissies. Afwijking: 10-20%.
  2. Geavanceerd. Event-driven architectuur, adaptieve drill-down, fundingmodel, shadow mode. Afwijking: 5-10%.
  3. Institutioneel. L2-orderboeksimulatie, gekalibreerd impactmodel, real-time afwijkingsmonitoring. Afwijking: 2-5%.

Jouw taak is te bepalen op welk niveau je zit en te begrijpen welke afwijking je acceptabel acht voor jouw positiegrootte en hefboom.


Nuttige links

  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

Citatie

@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.}
}
Disclaimer: De informatie in dit artikel is uitsluitend bedoeld voor educatieve en informatieve doeleinden en vormt geen financieel, beleggings- of handelsadvies. Het handelen in cryptovaluta brengt een aanzienlijk risico op verlies met zich mee.

Auteurs

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

Blijf de markt voor

Abonneer je op onze nieuwsbrief voor exclusieve AI-handelsinzichten, marktanalyses en platformupdates.

We respecteren je privacy. Je kunt je op elk moment afmelden.