NautilusTrader frente a VectorTA¶
El banco de pruebas existe para comparar dos formas muy distintas de calcular un backtest: un motor event-driven que simula el mercado evento a evento (NautilusTrader) y una biblioteca vectorizada que calcula indicadores sobre arrays completos (VectorTA). Este capítulo explica qué es cada uno, en qué es bueno, dónde tropieza y qué se descubrió ejecutando los binarios, no leyendo su documentación.
Qué vas a aprender¶
- Qué son NautilusTrader 1.231.0 y VectorTA 0.2.8 y qué filosofía sigue cada uno.
- Cuándo conviene cada motor y qué límites tiene.
- El hallazgo central: cómo conseguir un fill next-open en Nautilus.
- Por qué la cuenta es MARGIN.
- Qué pasa con el AVX2 de VectorTA (no acelera).
- Las semillas de calentamiento de cada indicador y el tratamiento de los NaN de entrada.
1. Dos filosofías¶
flowchart TB
subgraph VEC["VectorTA — vectorizado"]
A1[array close completo] --> A2["ema(close, 20)"] --> A3[array ema completo]
end
subgraph EVT["NautilusTrader — event-driven"]
B1[Bar t] --> B2[on_bar: actualizar indicador] --> B3[decidir] --> B4[orden] --> B5[matching engine] --> B6[OrderFilled]
B6 --> B1
end
VectorTA 0.2.8 (vector-ta==0.2.8) |
NautilusTrader 1.231.0 (nautilus-trader==1.231.0) |
|
|---|---|---|
| Qué es | Biblioteca Rust con enlaces Python; el catálogo del proyecto le cuenta 346 indicadores con carril VectorTA | Plataforma de trading con motor de backtest y live, núcleo Rust/Cython |
| Modelo | Función sobre array: f(close) → array |
Eventos: barras, quotes, órdenes, fills, posiciones, cuenta |
| Carriles | batch (ema, ema_batch) y stream (EmaStream.update) |
Indicador incremental (update_raw/handle_bar) dentro de una estrategia |
| Ejecución | No tiene: el proyecto le añade su propio ledger | Tiene matching engine, venue, cuenta, comisiones |
| Fortaleza | Velocidad bruta en indicadores; barridos de parámetros | Realismo del flujo: órdenes, cuentas, eventos, mismo código en live |
| Límite | Sin ejecución; variantes de fórmula propias; NaN silencioso | Coste por evento; semillas propias; instrumentación costosa |
Analogía
VectorTA es una calculadora científica: le das la columna entera y te devuelve la columna resultado. Nautilus es un simulador de vuelo: cada instante pasa por los instrumentos, el piloto decide y el avión responde. La calculadora es rapidísima; el simulador reproduce mejor lo que pasaría de verdad.
El proyecto no elige uno: fija un contrato común (datos, features, política, ejecución, contabilidad) y mide cada motor contra él.
2. Hallazgo 1: el fill next-open en Nautilus¶
El contrato temporal del proyecto (SIM-S) es sencillo de enunciar: la decisión tomada al cierre
de la barra t se ejecuta a la apertura de la barra t+1. Ni en el cierre de t (miraría el
futuro) ni en la apertura de t+2 (doble desplazamiento).
Conseguirlo en Nautilus no es obvio. La sonda docs/nt_probe.py probó cuatro configuraciones con
un fixture mínimo: barra 0 con cierre 100 y barra 1 con apertura 110.
| Configuración | Precio de fill | Veredicto |
|---|---|---|
bar_execution=True, sin latencia |
100 (cierre de la barra de decisión) | No es next-open |
bar_execution=True, LatencyModel(1ns) |
111 (cierre de la barra siguiente) | No es next-open |
bar_execution=False, quotes de apertura, orden en on_bar |
sin fill (no hay libro al enviar) | Inválido |
bar_execution=False, quote sintética a open+1ns, orden en on_quote_tick |
110 | Diseño canónico SIM-S |
sequenceDiagram
participant E as Motor Nautilus
participant S as Estrategia SIM-S
E->>S: on_bar(t) — cierre 100
S->>S: decide objetivo (+1), guarda intención
E->>S: on_quote_tick(open[t+1] + 1 ns)<br/>bid = ask = 110 (k = 0)
S->>E: orden de mercado por el delta
E->>S: OrderFilled a 110 ✔
Detalles del diseño (en finazbench/adapters/nt_strategy_base.py y nt_common.py):
- Para cada barra
i ≥ 1se emite unaQuoteTickenmarket_open_ns[i] + 1 nsconbid = open − k·tickyask = open + k·tick, siendok = half_spread_ticks + slippage_ticks. Conk = 0el fill es el open exacto. - El desplazamiento de 1 ns es solo una clave de desempate de reproducción
(
replay_ts_shift_ns=1, declarado enrun_metrics); no llega a la tabla de fills, cuyoevent_tsesmarket_open_nsexacto. - La barra 0 no lleva quote: ninguna decisión previa puede ejecutarse en su apertura.
El caso negativo se mantiene vivo
tests/adapters/test_next_open_probe.py afirma explícitamente que bar_execution=True liquida
a 100, para que nadie lo reintroduzca «porque parece más simple».
3. Hallazgo 2: cuenta MARGIN y OMS NETTING¶
| Ajuste | Valor | Motivo |
|---|---|---|
AccountType |
MARGIN |
Para permitir cortos. No se habilitan cortos sobre una cuenta CASH que no los admite para luego compararla con un ledger que sí. |
OmsType |
NETTING |
Una posición neta por instrumento: eso significa un objetivo en {−1, 0, +1}. Con HEDGING una reversión abriría dos posiciones opuestas. |
bar_execution |
False |
Las barras informativas no reescriben el libro. |
fee_model |
HalfEvenFeeModel (por defecto) |
Nautilus redondea la comisión half-up; el oráculo, half-even (ver capítulo del ledger). |
| logging | ERROR, bypass_logging=True |
Un log por barra falsea el caudal de eventos. |
Variante long-only
NtIntentReplayAdapter(account_type=AccountType.CASH) es posible, pero debe declararse en
ambos motores y los objetivos deben quedarse en {0, +1}. No se usa en la fase A.
4. Hallazgo 3: el AVX2 de VectorTA no acelera¶
El wheel de PyPI vector-ta 0.2.8 solo trae el kernel escalar: kernel="avx2" o "avx512"
existen como valores aceptados pero lanzan «not compiled in this build». Que un símbolo exista no
prueba que funcione (DEV-02).
El proyecto construyó su propio wheel (finaz/bench:0.2.8-avx2-x86-64-v3, misma versión 0.2.8)
para medirlo. El resultado de WP-08b, con la sonda ABBA en el mismo proceso sobre 1 000 000 de
barras:
| Carga | Escalar (ms) | AVX2 (ms) | Speedup |
|---|---|---|---|
| sma single | 1,621 | 1,623 | 0,999 |
| ema single | 2,510 | 2,498 | 1,005 |
| rsi single | 3,844 | 3,858 | 0,996 |
| atr single | 3,779 | 3,791 | 0,997 |
| ema_grid (32 periodos) | 244,875 | 282,995 | 0,865 |
| rsi_grid (32 periodos) | 335,238 | 317,343 | 1,056 |
Conclusión: single ≈ 1,00 y batch 0,87–1,06. Los indicadores recursivos (EMA, RMA, RSI) tienen una dependencia barra a barra que ninguna instrucción vectorial rompe. Se detalla en el capítulo de benchmarks.
5. Hallazgo 4: semillas de calentamiento¶
Cada motor decide a su manera cuándo un indicador empieza a ser válido y con qué valor arranca. Es la fuente número uno de divergencias.
| Indicador | Canónico (first_valid) |
VectorTA batch | VectorTA stream | Nautilus |
|---|---|---|---|---|
sma(20) |
19 | 19 | 19 | 19 |
ema(20) |
19, semilla SMA | valores desde 0 (media corrida, vta_running_mean_masked), coincide desde 19; la máscara la pone el proyecto |
NaN hasta 19, semilla SMA | 19 (period_minus_1, máscara del proyecto), semilla = primer valor (nt_seed_first_value): el indicador nativo arranca en el primer cierre, no en la SMA |
rsi(14) |
14 (Wilder) | 14 | 14 | 13, escala 0..1 (nt_init_period_minus_1_scaled_0_1) |
atr(14) |
13 (TR0 = H0−L0) |
13 | 13 | 13, semilla distinta (nt_seed_first_value_sma_inner) |
adx(14) (dmi) |
26 | candidata del wheel adx/di: 27 (VARIANT, no usada; medida en docs/FEATURES.md). La fila vectorta/dmi de capabilities.json es custom_reference y observa 26 |
AdxStream: candidata, no usada |
no disponible: la fila nautilus/dmi de capabilities.json tiene available: false (1.231.0 no ofrece ADX nativo; DirectionalMovement no calcula ADX) |
Dos clases de diferencia
- Semilla distinta, converge (
ema,rmaen Nautilus; el repositorio incluye tambiénatryrsi): la diferencia decae exponencialmente, de 1e-1 en la barra 19 a menos de 1e-9 a las 5 000 barras (docs/FEATURES.md). Ojo: elrside Nautilus con suma_typepor defecto usa media exponencial y no converge (ver Ejemplos numéricos); y el propioprovider_nautilus.pydice queatrno converge con elma_typepor defecto. - Definición distinta, no converge nunca (
bbandsde Nautilus centrada en el precio típico;donchianinclusivo): ninguna tolerancia lo arregla.
El RSI de Nautilus devuelve 0..1; el proveedor del proyecto lo multiplica por 100 (conversión de unidad, no ajuste numérico). Sin eso, un umbral de 30 nunca se cruzaría.
6. Hallazgo 5: los NaN de entrada¶
VectorTA ignora un NaN sin avisar
Si la serie de entrada contiene un NaN, vector_ta devuelve un número plausible y falso. Por
eso los tres proveedores del proyecto (canonical, vectorta, nautilus) rechazan NaN e
infinitos en la entrada con un ValueError que nombra la columna y el índice. Hay un test que
lo acredita: test_parity.py::test_vectorta_ignoraria_el_nan_sin_la_comprobacion.
import numpy as np
from finazbench.features.registry import build_spec
spec = build_spec("ema", period=20) # único constructor legítimo
close = np.array([10.0, np.nan, 10.2])
# provider.compute(bars, spec) → ValueError: columna 'close', índice 1
7. Otros detalles verificados en el binario¶
Construir 632k barras de Nautilus: qué vía es la rápida
| Vía | 100k barras |
|---|---|
Bar.from_raw_arrays_to_list (elegida) |
0,059 s |
Bucle Python Bar(...) |
0,140 s |
BarDataWranglerV2.from_pandas + Bar.from_pyo3_list |
0,330 s |
La firma de QuoteTick.from_raw_arrays_to_list sugiere enteros, pero el binario exige
float64; y los arrays deben ser escribibles (los de Polars zero-copy no lo son).
El PortfolioAnalyzer cuadrático
Nautilus acumula cada trade con pd.concat de una Series de un elemento: coste cuadrático.
Con 1 000 fills sobre 50 000 barras, simulate pasa de 2,43 s a 5,77 s. El proyecto lo
neutraliza (portfolio_analyzer: "disabled", declarado) porque solo alimenta estadísticas
nativas que el banco no usa; un test comprueba que fills y equity no cambian ni un céntimo.
Precisión de 128 bits
El build usa FIXED_PRECISION = 16: un tamaño de 100 000 unidades en crudo es 10²¹ y desborda
int64. Por eso no hay vía «raw entera» para las quotes.
Resumen¶
- VectorTA calcula indicadores sobre arrays (rápido, sin ejecución); Nautilus simula eventos (realista, más caro).
- El fill next-open solo es correcto con
bar_execution=False+QuoteTicksintética aopen+1ns - orden en
on_quote_tick. - Cuenta MARGIN con OMS NETTING para cortos y reversiones en una sola posición.
- El AVX2 del build propio de VectorTA 0.2.8 no acelera estas rutas.
- Las semillas difieren (
ema,rsi14 vs 13,adx26 vs 27): el proyecto fija una canónica y declara cada variante. - Los NaN de entrada se rechazan siempre, porque VectorTA los ignoraría en silencio.
Para practicar¶
- Ejecuta mentalmente la sonda: barras con cierres
[100, 111, 120]y aperturas[100, 110, 120], orden de compra en la barra 0. ¿A qué precio se llena en cada una de las cuatro configuraciones? - ¿Por qué una EMA con semilla «primer valor» y otra con semilla SMA acaban coincidiendo, y una Bollinger centrada en el precio típico nunca coincide con la del cierre?
- Explica por qué una reversión de +1 a −1 con OMS HEDGING no sería comparable con el ledger.
- Busca en
runs/env/capabilities.jsonla fila dersipara Nautilus y anota suobserved_first_validy su variante.