← Voltar aos artigos
March 7, 2026
5 min read

Paridade backtest-live: por que seu bot opera diferente do backtest

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

Você rodou uma estratégia em um backtest. Sharpe 2,1, MaxDD -8%, PnL +67%. Você lançou o bot. Um mês depois você compara: os mesmos sinais, o mesmo período — mas o PnL ao vivo é 40% menor. O drawdown é uma vez e meia mais profundo. Duas em cada dez operações simplesmente não foram executadas.

Isso não é um bug. Isso é divergência backtest-live — uma discrepância sistemática entre os resultados do backtest e o trading real. Todo mundo tem esse problema. A única questão é se você sabe disso e se consegue controlá-lo.

Este artigo apresenta uma taxonomia completa das divergências, padrões arquiteturais para minimizá-las e uma checklist prática para monitorar a paridade em produção.

A síndrome do "funcionou no backtest"

Divergência backtest vs live trading — curva de equity ideal versus resultados reais voláteis

Todo algotrader passa por esse ciclo:

  1. Escreveu uma estratégia em um notebook Jupyter
  2. Rodou um backtest em CSV histórico — os resultados são ótimos
  3. Reescreveu a lógica como um bot (frequentemente em outra linguagem ou framework)
  4. Lançou — os resultados não coincidem
  5. Procurou um bug, não encontrou nenhum — "o mercado mudou"

O problema não é o mercado. O problema é que o backtest e o bot são dois produtos de software diferentes que modelam a mesma realidade de maneiras distintas. Divergências são inevitáveis, mas podem ser sistematizadas e minimizadas.

Taxonomia das divergências

Taxonomia das divergências backtest-live

Todas as fontes de divergência se enquadram em quatro categorias. Para cada uma — uma classificação de severidade (de 1 a 5) e uma contribuição típica para a divergência de PnL.

1. Divergências de dados (severidade: 3/5)

Os dados que o backtest vê e os dados que o bot vê em tempo real não são a mesma coisa.

Timestamps. As exchanges entregam candles com regras diferentes de atribuição de timestamp. Uma exchange marca o candle com o início do período, outra com o final. Uma API REST pode retornar um candle com atraso de 1-3 segundos após o fechamento real. O backtest trabalha com timestamps "ideais" do arquivo histórico.

Agregação OHLCV. Os dados históricos costumam ser agregados pelo provedor de forma diferente da que a exchange usa em tempo real. A diferença está no último dígito — mas com sinais de limiar (cruzamento de médias móveis, rompimento de nível) isso determina se a estratégia entra em uma posição ou não.

Lacunas e dados faltantes. Os dados históricos costumam estar limpos — candles faltantes são preenchidos por interpolação. Em tempo real, um WebSocket pode cair, e o bot perde 30 segundos de dados.

Contribuição típica para a divergência de PnL: 2-5% do PnL anual.

2. Divergências de execução (severidade: 5/5)

Divergências na execução de ordens — visualização de slippage do livro de ofertas, latência e execuções parciais

A classe mais perigosa de divergências. O backtest simula a execução perfeitamente — a realidade está longe de ser ideal.

Slippage. O backtest preenche a ordem ao preço de fechamento (ou ao preço do sinal). Na realidade, uma ordem a mercado é executada no melhor bid/ask mais um slippage que depende do volume e da liquidez. Para uma posição de $10K em uma altcoin de liquidez média, o slippage pode ser de 0,05-0,3%.

Fórmula para slippage acumulado em NN operações:

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

onde sis_i é o slippage da ii-ésima operação, dependendo da profundidade do livro de ofertas:

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

Latência. Do momento em que um sinal é gerado até a execução da ordem, passa-se tempo: cálculo do sinal (1-50 ms), transmissão da requisição (10-200 ms), matching na exchange (1-10 ms). No backtest, a latência = 0. Ao vivo, o preço pode se mover.

Execuções parciais. O backtest assume que 100% da ordem é preenchida instantaneamente. Na realidade, uma ordem limite pode ser parcialmente preenchida — ou não ser preenchida se o preço reverter. Para uma ordem a mercado em um mercado ilíquido, a ordem "desliza" por vários níveis do livro de ofertas.

Prioridade de fila. Uma ordem limite colocada no melhor preço bid não será executada imediatamente — ela entra na fila atrás de todas as ordens já colocadas naquele nível. Um backtest que considera "preço tocado = ordem executada" superestima sistematicamente a taxa de preenchimento.

Contribuição típica para a divergência de PnL: 10-30% do PnL anual.

3. Divergências de lógica (severidade: 4/5)

São divergências no próprio código da estratégia entre o backtest e o bot.

Bases de código separadas. O antipadrão clássico: backtests/strategy_a.py e bot/strategy_a.py — dois arquivos separados que "fazem a mesma coisa". Após três meses de edições, eles inevitavelmente divergem. Alguém adicionou um filtro no backtest e esqueceu de replicá-lo no bot. Ou o contrário — um bug foi corrigido no bot mas permaneceu no backtest.

Frameworks diferentes. Backtest em pandas com operações vetorizadas, bot em asyncio com lógica orientada a eventos. Mesmo com uma estratégia idêntica, casos extremos são tratados de forma diferente: arredondamento, ordem de verificação de condições, tratamento de NaN.

Gerenciamento de estado. O backtest geralmente é stateless — ele itera sobre um array de dados. O bot é stateful — armazena posições, saldos, histórico de ordens. Reinício do bot, perda de estado, dessincronização com a exchange — tudo isso são fontes de divergência.

Contribuição típica para a divergência de PnL: 5-20% do PnL anual.

4. Divergências de custos (severidade: 3/5)

Divergências na modelagem de custos de trading.

Funding rates. A maioria dos backtests de futuros perpétuos não considera as funding rates de forma alguma. Com alavancagem de 10x e uma taxa média de 0,01% a cada 8 horas, isso equivale a 0.01%×3×365×10=109.5%0.01\% \times 3 \times 365 \times 10 = 109.5\% ao ano — mais que o PnL da maioria das estratégias. Uma análise detalhada está no artigo As funding rates destroem sua alavancagem.

Comissões. As comissões maker/taker geralmente são modeladas, mas frequentemente com a taxa errada. Níveis VIP, descontos em BNB, rebates — tudo isso afeta o resultado final.

Spread. Um backtest baseado em candles não vê o spread bid-ask. Em um candle de 1 minuto, o fechamento = 3000, mas na realidade bid = 2999,5 e ask = 3000,5. Cada operação "custa" metade do spread.

Contribuição típica para a divergência de PnL: 5-15% do PnL anual.

Efeito cumulativo

As quatro categorias atuam simultaneamente e, em regra, em uma única direção — contra o trader:

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

Uma divergência total de 20-50% em relação ao PnL do backtest é normal para um sistema não refinado. Com alavancagem, o efeito é multiplicado.

Padrões arquiteturais para paridade

Padrão 1: Shared Core (extração de um núcleo comum)

Arquitetura Shared Core — um único módulo de estratégia alimentando tanto o motor de backtest quanto o de trading ao vivo

A ideia: extrair o núcleo da estratégia — geração de sinais e lógica de execução — para um módulo separado usado tanto pelo backtest quanto pelo bot. Apenas a infraestrutura ao redor difere: a fonte de dados e o mecanismo de envio de ordens.

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

Agora o backtest e o bot usam o mesmo 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)

A regra fundamental: StrategyCore não sabe de onde vêm os dados nem para onde as ordens são enviadas. Ele recebe OHLCV e retorna uma OrderRequest. Todo o resto é responsabilidade da camada de infraestrutura.

Padrão 2: Unificação orientada a eventos (abordagem NautilusTrader)

Arquitetura de trading orientada a eventos com pipeline de eventos em cascata — dados de mercado, sinais, ordens, execuções

O NautilusTrader implementa a paridade por meio de um NautilusKernel unificado — um motor nativo em Rust com um núcleo determinístico orientado a eventos e resolução em nanossegundos. A mesma implementação de estratégia funciona tanto no backtest quanto no trading ao vivo.

A arquitetura é construída sobre o padrão portas e adaptadores (arquitetura hexagonal):

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

Vantagens:

  • Replay determinístico. Os eventos são processados em uma ordem estritamente definida — o resultado do backtest é reproduzível bit a bit.
  • FillModel personalizado. Simulação do livro de ofertas L2 para cada execução — o slippage é simulado com base na profundidade real do livro de ofertas.
  • Desempenho. Até 5 milhões de linhas/seg, processando dados que não cabem na RAM.
  • Redis + PostgreSQL. Cache e message bus via Redis, persistência via PostgreSQL — infraestrutura idêntica para backtest e live.

Padrão 3: Strategy Interface (abordagem Freqtrade)

O Freqtrade usa uma interface unificada IStrategy: a mesma classe de estratégia funciona tanto no backtest quanto ao vivo. A única diferença é a camada de persistência.


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

O Freqtrade também oferece:

  • Hyperopt via Optuna — otimização dos parâmetros da estratégia
  • --timeframe-detail — aprofundamento para um timeframe mais fino para refinar os fills (semelhante ao drill-down adaptativo)

Comparação dos padrões

Shared Core Orientado a eventos (NautilusTrader) Strategy Interface (Freqtrade)
Complexidade de implementação Baixa Alta Média
Nível de paridade Médio Máximo Alto
Simulação de fills FillModel separado Livro de ofertas L2 --timeframe-detail
Linguagem do núcleo Python Rust + Python Python
Indicado para Motores personalizados Trading institucional Início rápido

Precisão da simulação de fills

Níveis de precisão da simulação de fills

A simulação de fills é a principal fonte de divergência de execução. Três níveis de precisão:

Nível 1: Ingênuo (fill ao preço de fechamento)

fill_price = candle['close']

Erro: não considera slippage, spread ou execuções parciais. Superestima sistematicamente o PnL.

Nível 2: Modelo 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

Erro: um slippage fixo não considera liquidez nem o tamanho da ordem. Melhor que o ingênuo, mas ainda um modelo grosseiro.

Nível 3: Drill-down adaptativo com dados de 1s/100ms

A melhor opção: usar dados reais de granularidade fina para determinar com precisão a ordem de preenchimento de SL/TP. Descrito em detalhes no artigo Drill-down adaptativo: backtesting com granularidade variável.

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

Fórmula de impacto de mercado (modelo simplificado de Almgren-Chriss):

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

onde σ\sigma é a volatilidade, kk é o coeficiente de impacto, VorderV_{order} é o volume da ordem e VmarketV_{market} é o volume do mercado no período.

Checklist prática de paridade

Checklist holográfica de validação de paridade organizada por categoria — dados, execução, tempo, taxas

Antes de lançar o bot ao vivo, verifique cada item:

Código:

  • A estratégia usa um shared core (um módulo para backtest e live)
  • Sem duplicação da lógica de sinal em dois lugares
  • Testes unitários verificam saídas idênticas do núcleo para entradas idênticas
  • A ordem de verificação das condições é idêntica (SL antes do TP? TP antes do SL?)

Dados:

  • O formato de timestamp é idêntico (UTC, mesmo provedor)
  • A agregação OHLCV usa as mesmas regras
  • O tratamento de candles faltantes é idêntico
  • Sem look-ahead bias — o backtest não espia o futuro

Execução:

  • O modelo de slippage é calibrado com dados reais
  • As execuções parciais são modeladas (ou pelo menos estimadas de forma pessimista)
  • As ordens limite têm um modelo de prioridade de fila
  • A latência é considerada (atraso de 100-500 ms do sinal ao fill)

Custos:

  • As comissões maker/taker estão incluídas com a taxa atual
  • As funding rates são consideradas em futuros perpétuos
  • O spread é modelado (pelo menos a média)

Infraestrutura:

  • Persistência de estado: o bot recupera posições após reinício
  • Lógica de reconexão: o WebSocket se reconecta sem perda de dados
  • Logging: todas as ordens e execuções são registradas para análise post-mortem

Monitoramento da divergência em produção

Paridade não é uma verificação pontual, mas um processo contínuo. Após o lançamento do bot, as divergências devem ser rastreadas em tempo real.

Modo shadow (paper trading)

Modo de trading shadow — dados de mercado ao vivo e ordens simuladas rodando em paralelo

Rode o bot em paralelo com o backtest sobre os mesmos dados. O bot gera sinais mas não envia ordens — apenas registra. Simultaneamente, o backtest processa os mesmos dados. Compare:

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étricas do dashboard

Métrica Fórmula Limiar de alerta
Taxa de correspondência de sinais matchestotal signals\frac{\text{matches}}{\text{total signals}} < 95%
Slippage médio 1Nsi\frac{1}{N}\sum s_i (bps) > 10 bps
Taxa de preenchimento filledsent\frac{\text{filled}}{\text{sent}} < 90%
Divergência de PnL PnLlivePnLbtPnLbt\frac{PnL_{live} - PnL_{bt}}{PnL_{bt}} > 20%
Latência p99 99º percentil sinal-para-fill > 500 ms

Calibração do modelo de slippage

Calibração do modelo de slippage — profundidade do livro de ofertas com curva de impacto de preço mostrando fills esperados vs reais

Após acumular dados por 2-4 semanas, você pode calibrar o modelo de slippage do backtest com dados reais:

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

Conexões com outras ferramentas

A paridade backtest-live não é uma tarefa isolada. Ela se cruza com outras ferramentas da série "Backtests sem ilusões":

  • Drill-down adaptativo — melhora a precisão da simulação de fills, um componente-chave da paridade de execução.
  • Funding rates — se o backtest não modela o funding, a paridade é impossível com alavancagem > 3x.
  • Cache Parquet — timeframes e indicadores pré-calculados garantem que o backtest veja os mesmos dados que o bot. A emulação do RunningCandleBuffer = atualização em tempo real.
  • Polars vs Pandas — ao migrar de pandas (backtest) para Polars (live), é preciso garantir que os resultados numéricos coincidam.
  • Walk-Forward — o walk-forward em dados out-of-sample mostra como a estratégia se degrada — isso está mais próximo do live do que um backtest in-sample.

Recomendações

  1. Shared core é obrigatório. Uma única base de código para geração de sinais é o requisito mínimo para paridade. Dois arquivos com lógica idêntica garantem divergência em um mês.

  2. Calibre o modelo de fill. Um slippage fixo de 5 bps é melhor que nada. Um modelo de slippage calibrado com dados reais é significativamente melhor.

  3. Use o modo shadow nas primeiras 2-4 semanas. Não opere com dinheiro real até que a taxa de correspondência de sinais atinja 95%+.

  4. Modele as funding rates. Para futuros perpétuos, isso não é opcional — é obrigatório. O funding pode consumir todo o PnL com alavancagem > 5x.

  5. Registre tudo. Cada sinal, cada ordem, cada fill — com timestamps. Sem logs, a análise post-mortem é impossível.

  6. Automatize a comparação. Um relatório semanal do DivergenceMonitor deve chegar automaticamente. Não espere o PnL ficar negativo.

  7. Backtest pessimista por padrão. É melhor subestimar as expectativas no backtest e ser agradavelmente surpreendido ao vivo do que o contrário. O modelo de slippage deve ser conservador.

Conclusão

Níveis de maturidade dos sistemas de trading — do backtesting básico à produção completa

A paridade backtest-live não é uma propriedade de um sistema, mas um processo. A paridade perfeita não existe: um backtest é, por definição, um modelo da realidade, e um modelo sempre simplifica. Mas a diferença entre "o modelo diverge 5%" e "o modelo diverge 50%" é determinada pela arquitetura.

Três níveis de maturidade:

  1. Básico. Shared core, slippage fixo, comissões. Divergência: 10-20%.
  2. Avançado. Arquitetura orientada a eventos, drill-down adaptativo, modelo de funding, modo shadow. Divergência: 5-10%.
  3. Institucional. Simulação do livro de ofertas L2, modelo de impacto calibrado, monitoramento de divergência em tempo real. Divergência: 2-5%.

Sua tarefa é determinar em qual nível você está e entender qual divergência você considera aceitável para o tamanho da sua posição e alavancagem.


Links úteis

  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

Citação

@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

Fique à frente do mercado

Assine nossa newsletter para insights exclusivos sobre trading com IA, análises de mercado e atualizações da plataforma.

Respeitamos sua privacidade. Cancele a inscrição a qualquer momento.