← Volver a los artículos
March 7, 2026
5 min de lectura

Paridad backtest-live: por qué tu bot opera diferente al backtest

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

Ejecutaste una estrategia en un backtest. Sharpe 2.1, MaxDD -8%, PnL +67%. Lanzaste el bot. Un mes después comparas: las mismas señales, el mismo periodo — pero el PnL en vivo es 40% menor. El drawdown es una vez y media más profundo. Dos de cada diez operaciones no se ejecutaron en absoluto.

Esto no es un bug. Esto es divergencia backtest-live — una discrepancia sistemática entre los resultados del backtest y el trading real. Todos la tienen. La única pregunta es si sabes de ella y si puedes controlarla.

Este artículo ofrece una taxonomía completa de las divergencias, patrones arquitectónicos para minimizarlas y una lista de verificación práctica para monitorear la paridad en producción.

El síndrome del "funcionó en el backtest"

Divergencia backtest vs live trading — curva de equity ideal frente a resultados reales volátiles

Todo algotrader pasa por este ciclo:

  1. Escribió una estrategia en un notebook de Jupyter
  2. Corrió un backtest sobre CSV histórico — los resultados son excelentes
  3. Reescribió la lógica como un bot (a menudo en otro lenguaje o framework)
  4. Lo lanzó — los resultados no coinciden
  5. Buscó un bug, no lo encontró — "el mercado cambió"

El problema no es el mercado. El problema es que el backtest y el bot son dos productos de software distintos que modelan la misma realidad de manera diferente. Las divergencias son inevitables, pero pueden sistematizarse y minimizarse.

Taxonomía de las divergencias

Taxonomía de las divergencias backtest-live

Todas las fuentes de divergencia se agrupan en cuatro categorías. Para cada una — una calificación de severidad (de 1 a 5) y una contribución típica a la divergencia del PnL.

1. Divergencias de datos (severidad: 3/5)

Los datos que ve el backtest y los datos que ve el bot en tiempo real no son lo mismo.

Marcas de tiempo. Los exchanges entregan velas con reglas distintas para la asignación de la marca de tiempo. Un exchange marca la vela con el inicio del periodo, otro con el final. Una API REST puede devolver una vela con un retraso de 1-3 segundos tras el cierre real. El backtest trabaja con marcas de tiempo "ideales" del archivo histórico.

Agregación de OHLCV. Los datos históricos suelen ser agregados por el proveedor de forma distinta a como lo hace el exchange en tiempo real. La diferencia está en el último dígito — pero con señales de umbral (cruce de medias móviles, ruptura de nivel) esto determina si la estrategia entra en una posición o no.

Huecos y datos faltantes. Los datos históricos suelen estar limpios — las velas faltantes se rellenan por interpolación. En tiempo real, un WebSocket puede caerse y el bot pierde 30 segundos de datos.

Contribución típica a la divergencia del PnL: 2-5% del PnL anual.

2. Divergencias de ejecución (severidad: 5/5)

Divergencias en la ejecución de órdenes — visualización del slippage del libro de órdenes, la latencia y las ejecuciones parciales

La clase más peligrosa de divergencias. El backtest simula la ejecución a la perfección — la realidad está lejos de ser ideal.

Slippage. El backtest llena la orden al precio de cierre (o al precio de la señal). En realidad, una orden de mercado se ejecuta al mejor bid/ask más un slippage que depende del volumen y la liquidez. Para una posición de $10K en una altcoin de liquidez media, el slippage puede ser de 0.05-0.3%.

Fórmula para el slippage acumulado en NN operaciones:

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

donde sis_i es el slippage de la operación ii-ésima, que depende de la profundidad del libro de órdenes:

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

Latencia. Desde el momento en que se genera una señal hasta la ejecución de la orden, pasa tiempo: cálculo de la señal (1-50 ms), transmisión de la solicitud (10-200 ms), emparejamiento en el exchange (1-10 ms). En el backtest, la latencia = 0. En vivo, el precio puede moverse.

Ejecuciones parciales. El backtest asume que el 100% de la orden se llena instantáneamente. En realidad, una orden límite puede ejecutarse parcialmente — o no ejecutarse en absoluto si el precio se revierte. Para una orden de mercado en un mercado ilíquido, la orden "se desliza" a través de varios niveles del libro de órdenes.

Prioridad de cola. Una orden límite colocada al mejor precio bid no se ejecutará de inmediato — se pone en cola detrás de todas las órdenes colocadas previamente en ese nivel. Un backtest que considera "precio tocado = orden ejecutada" sobreestima sistemáticamente la tasa de ejecución.

Contribución típica a la divergencia del PnL: 10-30% del PnL anual.

3. Divergencias de lógica (severidad: 4/5)

Son divergencias en el propio código de la estrategia entre el backtest y el bot.

Bases de código separadas. El antipatrón clásico: backtests/strategy_a.py y bot/strategy_a.py — dos archivos separados que "hacen lo mismo". Tras tres meses de ediciones, inevitablemente divergen. Alguien añadió un filtro en el backtest y olvidó replicarlo en el bot. O al revés — se corrigió un bug en el bot pero permaneció en el backtest.

Frameworks distintos. Backtest en pandas con operaciones vectorizadas, bot en asyncio con lógica orientada a eventos. Incluso con una estrategia idéntica, los casos límite se manejan de manera diferente: redondeo, orden de verificación de condiciones, manejo de NaN.

Gestión de estado. El backtest suele ser sin estado — itera sobre un array de datos. El bot mantiene estado — almacena posiciones, saldos, historial de órdenes. El reinicio del bot, la pérdida de estado, la desincronización con el exchange — todos son fuentes de divergencia.

Contribución típica a la divergencia del PnL: 5-20% del PnL anual.

4. Divergencias de costos (severidad: 3/5)

Divergencias en el modelado de los costos de trading.

Funding rates. La mayoría de los backtests de futuros perpetuos no tienen en cuenta las funding rates en absoluto. Con apalancamiento 10x y una tasa promedio de 0.01% cada 8 horas, esto equivale a 0.01%×3×365×10=109.5%0.01\% \times 3 \times 365 \times 10 = 109.5\% anual — más que el PnL de la mayoría de las estrategias. Un análisis detallado está en el artículo Las funding rates matan tu apalancamiento.

Comisiones. Las comisiones maker/taker suelen modelarse, pero a menudo con la tasa incorrecta. Los niveles VIP, los descuentos por BNB, los rebates — todo esto afecta el resultado final.

Spread. Un backtest basado en velas no ve el spread bid-ask. En una vela de 1 minuto, el cierre = 3000, pero en realidad bid = 2999.5 y ask = 3000.5. Cada operación "cuesta" la mitad del spread.

Contribución típica a la divergencia del PnL: 5-15% del PnL anual.

Efecto acumulativo

Las cuatro categorías actúan simultáneamente y, por lo general, en una sola dirección — en contra del trader:

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

Una divergencia total del 20-50% respecto al PnL del backtest es normal para un sistema sin refinar. Con apalancamiento, el efecto se multiplica.

Patrones arquitectónicos para la paridad

Patrón 1: Shared Core (extracción de un núcleo común)

Arquitectura Shared Core — un único módulo de estrategia impulsando tanto el motor de backtest como el de trading en vivo

La idea: extraer el núcleo de la estrategia — la generación de señales y la lógica de ejecución — en un módulo separado utilizado tanto por el backtest como por el bot. Solo difiere la infraestructura circundante: la fuente de datos y el mecanismo de envío de órdenes.

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

Ahora el backtest y el bot usan el mismo 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 regla clave: StrategyCore no sabe de dónde vienen los datos ni adónde se envían las órdenes. Recibe OHLCV y devuelve un OrderRequest. Todo lo demás es responsabilidad de la capa de infraestructura.

Patrón 2: Unificación orientada a eventos (enfoque NautilusTrader)

Arquitectura de trading orientada a eventos con pipeline de eventos en cascada — datos de mercado, señales, órdenes, ejecuciones

NautilusTrader implementa la paridad mediante un NautilusKernel unificado — un motor nativo en Rust con un núcleo determinista orientado a eventos y resolución de nanosegundos. La misma implementación de la estrategia funciona tanto en el backtest como en el trading en vivo.

La arquitectura se construye sobre el patrón puertos y adaptadores (arquitectura hexagonal):

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

Ventajas:

  • Repetición determinista. Los eventos se procesan en un orden estrictamente definido — el resultado del backtest es reproducible bit a bit.
  • FillModel personalizado. Simulación del libro de órdenes L2 para cada ejecución — el slippage se simula según la profundidad real del libro de órdenes.
  • Rendimiento. Hasta 5 millones de filas/seg, procesando datos que no caben en RAM.
  • Redis + PostgreSQL. Caché y bus de mensajes vía Redis, persistencia vía PostgreSQL — infraestructura idéntica para backtest y live.

Patrón 3: Strategy Interface (enfoque Freqtrade)

Freqtrade utiliza una interfaz unificada IStrategy: la misma clase de estrategia funciona tanto en el backtest como en vivo. La única diferencia es la capa de persistencia.


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 también ofrece:

  • Hyperopt vía Optuna — optimización de los parámetros de la estrategia
  • --timeframe-detail — profundización a un marco temporal más fino para refinar los fills (similar al drill-down adaptativo)

Comparación de patrones

Shared Core Orientado a eventos (NautilusTrader) Strategy Interface (Freqtrade)
Complejidad de implementación Baja Alta Media
Nivel de paridad Media Máxima Alta
Simulación de fills FillModel separado Libro de órdenes L2 --timeframe-detail
Lenguaje del núcleo Python Rust + Python Python
Adecuado para Motores personalizados Trading institucional Inicio rápido

Precisión de la simulación de fills

Niveles de precisión de la simulación de fills

La simulación de fills es la principal fuente de divergencia de ejecución. Tres niveles de precisión:

Nivel 1: Ingenuo (ejecución al precio de cierre)

fill_price = candle['close']

Error: no considera el slippage, el spread ni las ejecuciones parciales. Sobreestima sistemáticamente el PnL.

Nivel 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

Error: un slippage fijo no considera la liquidez ni el tamaño de la orden. Mejor que el ingenuo, pero sigue siendo un modelo tosco.

Nivel 3: Drill-down adaptativo con datos de 1s/100ms

La mejor opción: usar datos reales de granularidad fina para determinar con precisión el orden de ejecución de SL/TP. Descrito en detalle en el artículo Drill-down adaptativo: backtesting con granularidad 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

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

donde σ\sigma es la volatilidad, kk es el coeficiente de impacto, VorderV_{order} es el volumen de la orden y VmarketV_{market} es el volumen del mercado para el periodo.

Lista de verificación práctica de paridad

Lista de verificación holográfica de validación de paridad organizada por categoría — datos, ejecución, tiempo, comisiones

Antes de lanzar el bot en vivo, verifica cada punto:

Código:

  • La estrategia usa un shared core (un módulo para backtest y live)
  • Sin duplicación de la lógica de señales en dos lugares
  • Pruebas unitarias verifican salidas idénticas del núcleo para entradas idénticas
  • El orden de verificación de condiciones es idéntico (¿SL antes de TP? ¿TP antes de SL?)

Datos:

  • El formato de marca de tiempo es idéntico (UTC, mismo proveedor)
  • La agregación de OHLCV usa las mismas reglas
  • El manejo de velas faltantes es idéntico
  • No hay look-ahead bias — el backtest no espía el futuro

Ejecución:

  • El modelo de slippage está calibrado con datos reales
  • Las ejecuciones parciales están modeladas (o al menos estimadas de forma pesimista)
  • Las órdenes límite tienen un modelo de prioridad de cola
  • La latencia se tiene en cuenta (retraso de 100-500 ms de la señal al fill)

Costos:

  • Las comisiones maker/taker están incluidas con la tasa actual
  • Las funding rates se tienen en cuenta con futuros perpetuos
  • El spread está modelado (al menos el promedio)

Infraestructura:

  • Persistencia de estado: el bot recupera las posiciones tras un reinicio
  • Lógica de reconexión: el WebSocket se reconecta sin pérdida de datos
  • Logging: todas las órdenes y ejecuciones se registran para el análisis post-mortem

Monitoreo de la divergencia en producción

La paridad no es una verificación única sino un proceso continuo. Tras lanzar el bot, las divergencias deben rastrearse en tiempo real.

Modo sombra (paper trading)

Modo de trading sombra — datos de mercado en vivo y órdenes simuladas ejecutándose en paralelo

Ejecuta el bot en paralelo con el backtest sobre los mismos datos. El bot genera señales pero no envía órdenes — solo registra. Simultáneamente, el backtest procesa los mismos datos. Compara:

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 del dashboard

Métrica Fórmula Umbral de alerta
Tasa de coincidencia de señales matchestotal signals\frac{\text{matches}}{\text{total signals}} < 95%
Slippage promedio 1Nsi\frac{1}{N}\sum s_i (bps) > 10 bps
Tasa de ejecución filledsent\frac{\text{filled}}{\text{sent}} < 90%
Divergencia de PnL PnLlivePnLbtPnLbt\frac{PnL_{live} - PnL_{bt}}{PnL_{bt}} > 20%
Latencia p99 Percentil 99 de señal a fill > 500 ms

Calibración del modelo de slippage

Calibración del modelo de slippage — profundidad del libro de órdenes con curva de impacto de precio mostrando fills esperados vs reales

Tras acumular datos durante 2-4 semanas, puedes calibrar el modelo de slippage del backtest con datos reales:

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

Conexiones con otras herramientas

La paridad backtest-live no es una tarea aislada. Se cruza con otras herramientas de la serie "Backtests sin ilusiones":

  • Drill-down adaptativo — mejora la precisión de la simulación de fills, un componente clave de la paridad de ejecución.
  • Funding rates — si el backtest no modela el funding, la paridad es imposible con apalancamiento > 3x.
  • Caché Parquet — los marcos temporales e indicadores precalculados garantizan que el backtest vea los mismos datos que el bot. La emulación de RunningCandleBuffer = actualización en tiempo real.
  • Polars vs Pandas — al cambiar de pandas (backtest) a Polars (live), hay que asegurarse de que los resultados numéricos coincidan.
  • Walk-Forward — el walk-forward sobre datos out-of-sample muestra cómo se degrada la estrategia — esto está más cerca del live que un backtest in-sample.

Recomendaciones

  1. El shared core es obligatorio. Una única base de código para la generación de señales es el requisito mínimo para la paridad. Dos archivos con lógica idéntica garantizan la divergencia en un mes.

  2. Calibra el modelo de fills. Un slippage fijo de 5 bps es mejor que nada. Un modelo de slippage calibrado con datos reales es considerablemente mejor.

  3. Usa el modo sombra durante las primeras 2-4 semanas. No operes con dinero real hasta que la tasa de coincidencia de señales alcance el 95%+.

  4. Modela las funding rates. Para los futuros perpetuos, esto no es opcional — es obligatorio. El funding puede consumir todo el PnL con apalancamiento > 5x.

  5. Registra todo. Cada señal, cada orden, cada ejecución — con marcas de tiempo. Sin logs, el análisis post-mortem es imposible.

  6. Automatiza la comparación. Un informe semanal de DivergenceMonitor debería llegar automáticamente. No esperes a que el PnL se vuelva negativo.

  7. Backtest pesimista por defecto. Es mejor subestimar las expectativas en el backtest y llevarse una sorpresa agradable en vivo que al revés. El modelo de slippage debe ser conservador.

Conclusión

Niveles de madurez de los sistemas de trading — desde el backtesting básico hasta la producción completa

La paridad backtest-live no es una propiedad de un sistema sino un proceso. La paridad perfecta no existe: un backtest es, por definición, un modelo de la realidad, y un modelo siempre simplifica. Pero la diferencia entre "el modelo difiere en un 5%" y "el modelo difiere en un 50%" la determina la arquitectura.

Tres niveles de madurez:

  1. Básico. Shared core, slippage fijo, comisiones. Divergencia: 10-20%.
  2. Avanzado. Arquitectura orientada a eventos, drill-down adaptativo, modelo de funding, modo sombra. Divergencia: 5-10%.
  3. Institucional. Simulación del libro de órdenes L2, modelo de impacto calibrado, monitoreo de divergencia en tiempo real. Divergencia: 2-5%.

Tu tarea es determinar en qué nivel te encuentras y comprender qué divergencia consideras aceptable para tu tamaño de posición y apalancamiento.


Enlaces útiles

  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

@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

Mantente a la vanguardia

Suscríbete a nuestro boletín para recibir información exclusiva sobre trading con IA, análisis de mercado y actualizaciones de la plataforma.

Respetamos tu privacidad. Puedes darte de baja en cualquier momento.