Saltar a contenido

Backtesting causal

Qué vas a aprender

  • Qué es un backtest y cuáles son sus trampas clásicas (lookahead).
  • La regla de ejecución del banco: decisión en el cierre, ejecución en la apertura siguiente (next-open).
  • Qué es point-in-time (PIT) y el reloj known_at.
  • Cómo se modelan los costes, los cortos con cuenta MARGIN y las barras no operables.
  • La diferencia entre los carriles de ejecución SIM-S y SIM-R.

Un backtest es una máquina del tiempo… con reglas

Un backtest simula qué habría pasado si una estrategia se hubiera ejecutado en el pasado. La trampa es obvia pero sutil: en el pasado ya conocemos el futuro, y es facilísimo usarlo sin querer. A ese error se le llama lookahead (mirar adelante).

Comparación de un backtest causal y otro con lookahead sobre la misma serie

Figura 1. La misma serie operada de forma causal (izquierda) y con lookahead (derecha): un solo índice de desfase regala rendimiento imposible.

Ejemplos de lookahead

  • Decidir comprar con el close de hoy y ejecutar al open de hoy (que fue antes).
  • Unir una feature diaria a las barras intradía por fecha: el cierre del día D quedaría pegado a la barra de las 09:31 del día D, seis horas y media antes de que exista. «No sería un backtest optimista, sería un backtest imposible» (docs/DATOS.md).
  • Usar datos revisados después (una cifra corregida semanas más tarde) como si se hubieran conocido en su día.

La regla next-open

El contrato SIM-S fija una regla sencilla y verificable:

Se decide con la información disponible al cierre de la barra t y se ejecuta en la apertura de la barra t+1.

Decisión en el cierre de t y ejecución en la apertura de t+1 con QuoteTick sintética

Figura 2. La decisión se toma al cierre de t y se llena en el open de t+1; en Nautilus, con una QuoteTick sintética a open+1 ns y la orden enviada en on_quote_tick.

sequenceDiagram
    participant M as Mercado
    participant F as Features
    participant P as Política
    participant L as Ledger
    M->>F: cierre de la barra t (data_available_ns)
    F->>P: features válidas hasta t
    P->>L: objetivo target[t] ∈ {-1, 0, +1}
    Note over L: esperar a la siguiente apertura operable
    M->>L: open de la barra t+1
    L->>L: fill: quantity[t+1] = target[t]

En el ledger SIM-S (versión NumPy) esto se escribe literalmente quantity[t] = target[t-1] en las barras operables. Es sencillo, auditable y exacto: sin redondeos de coma flotante (el dinero va en enteros, en unidades mínimas).

Barras no operables (D-25)

Una barra de subasta o de halt existe en los datos pero no admite fill (is_tradable = False). Regla común a los tres carriles: el cambio pendiente se ejecuta en la primera apertura operable posterior. Mientras tanto la política congela el objetivo (no lo pone a 0: poner 0 sería decidir cerrar). Por eso fill_index - signal_index > 1 es legítimo.

Point-in-time (PIT) y known_at

Point-in-time significa: para cada instante de la simulación, usar solo la versión del dato que se conocía en ese instante. En el banco de backtesting se consigue con data_available_ns; en la capa de datos de la plataforma, con una tabla de revisiones append-only (market_bars_v1 en ClickHouse) y un reloj explícito:

Campo Significado
known_at Cuándo se supo esta revisión del dato
as_of Instante desde el que se mira

Revisiones con known_at y consulta con as_of

Figura 3. Una consulta point-in-time solo ve las revisiones con known_at ≤ as_of; la corrección de las 11:00 no existe para una consulta de las 10:00.

Regla PIT de la capa de datos: para cada (instrumento, ts) se toma la máxima revisión con known_at <= as_of; las filas «conocidas en el futuro» se excluyen, y un hueco es una anomalía, nunca un festivo implícito.

flowchart LR
    R1["rev 1<br/>known_at 10:00"] --> Q{"as_of = 10:30"}
    R2["rev 2<br/>known_at 11:00"] --> Q
    Q -->|known_at ≤ as_of| U["se usa rev 1"]
    Q -.->|future-known| X["rev 2 excluida"]

Costes

Un backtest sin costes miente a favor de las estrategias que operan mucho. El contrato de ejecución declara:

Parámetro Qué es
fee_rate Comisión proporcional al nocional (0,0005 = 5 bps)
half_spread_ticks Medio diferencial compra-venta, en ticks, aplicado al precio
slippage_ticks Deslizamiento adverso adicional, en ticks

Descomposición de un fill en los escenarios zero y 5bps

Figura 4. El mismo fill con coste cero y con 5 bps: el nocional no cambia, la comisión se redondea half-even en enteros y el slippage es cero.

Precio de ejecución: fill_price = next_open + sign(delta_q) · (half_spread_ticks + slippage_ticks) · tick_size. El spread se cobra una vez, en el precio. Escenarios con nombre: zero y hypothetical_5bps (fee_rate = 0,0005).

Cifras reales

P01 sobre NVDA 1d 2024 con 5 bps: 5 fills, costs_total 23,4 y final_equity 99.976,6 partiendo de 100.000, idéntico en VectorTA y Nautilus.

La comisión se redondea half-even (empate al par) con aritmética entera pura, para que la paridad no dependa del último bit de un float64 (docs/POLITICAS_Y_LEDGER.md §2).

Cortos y cuenta MARGIN

Con objetivo -1 la estrategia vende en corto. Una cuenta cash no admite cortos, así que el venue de Nautilus se configura con AccountType.MARGIN: comparar una cuenta cash con un ledger que sí permite cortos sería comparar peras con manzanas.

Lo que no se modela

El coste de préstamo de títulos no se modela (borrow_cost_modeled: false) y el adjclose de Yahoo no es point-in-time. Son reservas que viajan en run_metrics.warnings: los resultados de cartera no son evidencia económica.

SIM-S frente a SIM-R

SIM-S (simple, implementado) SIM-R (realista, no implementado)
Ejecución next-open idealizado Latencia, libro de órdenes
Fills Completos Parciales
Latencia de dato Cero (supuesto declarado) Medida
Estado Único carril disponible 501 LANE_UNSUPPORTED

Por qué SIM-S basta para comparar motores

El objetivo del banco es comparar motores con el mismo contrato, no predecir el PnL real. SIM-S es exacto y auditable; SIM-R quedó fuera de alcance y se rechaza de forma explícita.

Resumen

  • El enemigo es el lookahead; la defensa es un contrato causal verificable.
  • Regla next-open: decisión en el cierre de t, fill en la apertura operable siguiente.
  • PIT: solo la revisión conocida en ese instante (known_at <= as_of).
  • Costes explícitos (bps, spread, slippage), dinero en enteros y redondeo half-even.
  • Cortos solo con cuenta MARGIN; préstamo no modelado y declarado.
  • SIM-S implementado; SIM-R rechazado con 501.

Para practicar

  1. Una política decide +1 en el cierre del viernes; el lunes es festivo. ¿Cuándo se ejecuta?
  2. Calcula la comisión de comprar 100 acciones a 50,00 USD con 5 bps.
  3. ¿Por qué unir por fecha una feature diaria a barras de 5 min es lookahead?