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).
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
closede hoy y ejecutar alopende 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
ty se ejecuta en la apertura de la barrat+1.
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 |
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 |
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¶
- Una política decide
+1en el cierre del viernes; el lunes es festivo. ¿Cuándo se ejecuta? - Calcula la comisión de comprar 100 acciones a 50,00 USD con 5 bps.
- ¿Por qué unir por fecha una feature diaria a barras de 5 min es lookahead?