Carriles SIM-S y adaptadores¶
Comparar dos motores solo tiene sentido si ambos resuelven el mismo problema con el mismo contrato. El proyecto lo consigue con cuatro adaptadores (carriles) que comparten datos, política y reglas de ejecución, y que difieren solo en qué parte del trabajo hace cada motor. Así, una diferencia de tiempo o de resultado se puede atribuir a una etapa concreta.
Qué vas a aprender¶
- Qué es el carril SIM-S y su contrato temporal.
- Los cuatro adaptadores:
VTA_CPU_LEDGER,NT_INTENT_REPLAY,NT_FEATURES,NT_ONLINE(y la varianteNT_ONLINE(native)). - Qué pregunta responde cada uno y qué entra en su cronómetro.
- Cómo se leen la matriz de capacidades (
runs/env/capabilities.json) y el orden de etapas del runner. - Cómo elegir motor y carril para una tarea.
1. SIM-S: el carril simple y determinista¶
SIM-S es el carril de simulación simple: sin latencia, sin libro, sin fills parciales, con fill a la apertura siguiente. Su contrato temporal cabe en una línea:
La decisión tomada al cierre de
tse ejecuta enopen[t+1].
flowchart LR
C["cierre[t]<br/>data_available_ns[t]"] --> D["política decide<br/>objetivo ∈ {−1, 0, +1}"]
D --> F["fill en open[t+1]<br/>± k·tick"]
F --> E["equity marcada<br/>a close[t+1]"]
Reglas del contrato que comparten todos los carriles:
| Regla | Detalle |
|---|---|
| Objetivo discreto | target ∈ {−1, 0, +1} lotes; la orden es el delta (+1 → −1 es −2) |
| Next-open | fill_index = signal_index + 1, salvo barra no operable (fill diferido, D-25) |
| Precio | fill_price = open + sign(delta)·k·tick, con k = half_spread + slippage en ticks |
| Costes | escenarios zero o 5bps (parámetros de ensayo, no tarifas reales) |
| Sin cierre automático | la posición final se valora a la última barra; auto_liquidate_at_end no está implementado |
| Reinicio | reset_flat (el único implementado; continue → NotImplementedError) |
SIM-R no existe (todavía)
El carril realista (latencia, libro, fills parciales) está fuera de alcance: la API lo rechaza con
501 LANE_UNSUPPORTED. SIM-S y SIM-R no se mezclan.
2. Los cuatro adaptadores¶
flowchart TB
BARS[BarArrays cuantizadas] --> V
BARS --> R
BARS --> F
BARS --> O
subgraph V[VTA_CPU_LEDGER]
V1[features batch VectorTA] --> V2[política batch] --> V3[ledger propio NumPy/Numba]
end
subgraph R[NT_INTENT_REPLAY]
R1[objetivos ya calculados] --> R2[motor Nautilus: solo ejecución]
end
subgraph F[NT_FEATURES]
F1[features ya calculadas] --> F2[política en on_bar] --> F3[ejecución Nautilus]
end
subgraph O[NT_ONLINE]
O1[indicadores incrementales<br/>streams VectorTA o nativos] --> O2[política en on_bar] --> O3[ejecución Nautilus]
end
| Adaptador | Qué entra en el cronómetro | Pregunta que responde |
|---|---|---|
VTA_CPU_LEDGER |
features batch, política y ledger propio | ¿Qué rendimiento ofrece un pipeline CPU especializado? |
NT_ONLINE |
ingesta, indicadores incrementales, política y ejecución | ¿Cuánto cuesta el flujo completo online/event-driven? |
NT_FEATURES |
features ya calculadas, política al recibir el evento y ejecución | ¿Cuánto queda si se comparte el cálculo pesado? |
NT_INTENT_REPLAY |
solo ejecución y contabilidad | ¿Cuánto cuesta exclusivamente reproducir decisiones? |
Analogía: la carrera de relevos
Imagina cuatro equipos que corren el mismo recorrido. En NT_INTENT_REPLAY Nautilus solo corre el
último tramo (ejecutar). En NT_FEATURES corre los dos últimos (decidir y ejecutar). En NT_ONLINE
corre los tres (calcular, decidir, ejecutar). VTA_CPU_LEDGER es el equipo rival que corre todo sin
Nautilus. Comparando tiempos por tramo sabes dónde se pierde el tiempo.
2.1 NT_ONLINE y su variante nativa¶
NT_ONLINE tiene un parámetro indicator_provider:
| Variante | Indicadores | Qué esperar |
|---|---|---|
NT_ONLINE(vectorta) (por defecto) |
streams de VectorTA (EmaStream, RsiStream…) o custom_reference incremental |
Reproduce la canónica: PASS |
NT_ONLINE(native) |
indicadores nativos de Nautilus | PARITY_FAIL donde el nativo tiene otra semilla o definición; UNSUPPORTED donde no existe |
La variante nativa se ejecuta y se publica aunque falle, porque el proyecto está obligado a declarar el proveedor por indicador y prohíbe presentar como nativo lo que no lo es.
2.2 Todos comparten la ejecución¶
Los tres carriles de Nautilus heredan de NautilusAdapterBase
(finazbench/adapters/nt_base_adapter.py): next-open, delta, reversión, recogida de fills y tablas
son de la base. Un adaptador nuevo solo aporta su decide(i, bar) -> int. Así, si dos carriles
difieren, la diferencia está en la decisión y no en el contrato de ejecución.
from finazbench.adapters.registry import get_adapter
adapter = get_adapter("NT_INTENT_REPLAY")(max_qty=2)
adapter.prepare(bars, instrument_meta, execution_contract)
adapter.replay(intents) # IntentArrays o array de {-1, 0, +1}
result = adapter.canonical_result() # CanonicalResult validado
adapter.reset("reset_flat") # conserva los datos nativos cargados
adapter.replay(otros_intents)
otro = adapter.canonical_result()
adapter.dispose()
| Fichero | Contenido |
|---|---|
nt_common.py |
instrumento, venue, objetos nativos, HalfEvenFeeModel, extracción de fills |
nt_strategy_base.py |
intención en on_bar, orden en on_quote_tick |
nt_base_adapter.py |
ciclo prepare → reset → replay → canonical_result |
nt_result.py |
las cuatro tablas (intents, fills, positions, equity) |
vta_cpu_ledger.py |
orquestador features → política → ledger |
registry.py |
get_adapter(adapter_id) con importación perezosa |
Carriles de cartera
Para Q01–Q03 existen los hermanos multi-activo nt_weight_replay.py, nt_weight_features.py y
nt_weight_online.py, con orden de eventos (replay_ts, phase, asset_id, seq). El carril nativo de
panel es UNSUPPORTED por diseño (D-14).
3. El orden de etapas del runner¶
finazbench/runner/single.py::run_case recorre siempre el mismo orden (docs/RUNNER.md):
load_bars(f64 crudo) read_decode_ns
└─ validate clean_validate_ns
└─ features SOBRE EL f64 SIN CUANTIZAR feature_build_ns
└─ política → intents weights_and_policy_ns
└─ quantize_bars_for_execution clean_validate_ns
└─ adaptador (ledger o Nautilus) convert/load/reset/simulate
└─ CanonicalResult metrics_ns
La cuantización va en medio, y a propósito
Cuantizar antes de los indicadores cambiaría las señales (una EMA sobre precios redondeados no
es la EMA del dato). Cuantizar después del adaptador no serviría: es el adaptador quien necesita
precios en rejilla. Las dos ramas reciben exactamente el mismo BarArrays cuantizado.
from finazbench.runner.single import CaseSpec, run_case
spec = CaseSpec(asset_id="NVDA", timeframe="1d", strategy_id="P01",
cost_scenario="zero", adapter_id="VTA_CPU_LEDGER")
r = run_case(spec)
print(r.status, r.result.run_metrics["n_fills"])
print(r.timings.as_dict()) # las etapas, en ns de pared
Campo de CaseSpec |
Valores |
|---|---|
adapter_id |
VTA_CPU_LEDGER, NT_INTENT_REPLAY, NT_FEATURES, NT_ONLINE |
feature_provider |
vectorta (defecto), nautilus, canonical |
feature_path |
scalar o batch (entra en la firma: sma_batch no es bit a bit igual a sma) |
indicator_provider |
solo NT_ONLINE: vectorta (defecto) o native |
cost_scenario |
zero o 5bps |
4. La matriz de capacidades¶
finazbench/features/capabilities.py construye runs/env/capabilities.json por introspección más
una sonda de ejecución de 100 puntos: que un símbolo exista no prueba que funcione.
- 102 filas = 34 primitivas de fase A+B × 3 proveedores (
canonical,vectorta,nautilus). - Cada fila guarda lo declarado y lo observado (
observed_first_valid,observed_variant,observed_kernel,stream_first_valid). Si difieren, se conservan las dos. supports_streaming = stream_ok AND stream_parity_ok: un stream que corre pero no reproduce su batch no cuenta.
| Proveedor | Primitivas con streaming acreditado |
|---|---|
vectorta |
34 de 34 |
canonical |
0 (batch por diseño) |
nautilus |
7 (las de fase A que ofrece) |
Extracto real de dos filas:
{"primitive": "rsi", "provider": "nautilus",
"observed_first_valid": {"rsi": 13},
"observed_variant": "nt_init_period_minus_1_scaled_0_1",
"supports_streaming": true, "native_or_custom": "native"}
{"primitive": "dmi", "provider": "vectorta",
"observed_first_valid": {"plus_di": 13, "minus_di": 13, "adx": 26},
"observed_variant": "custom_reference_numpy",
"supports_streaming": true, "native_or_custom": "custom_reference"}
native frente a custom_reference
custom_reference significa «cálculo propio del proyecto servido bajo ese proveedor». Se marca
explícitamente para no atribuir a VectorTA un tiempo que es de NumPy. Ejemplo: supertrend en el
proveedor vectorta tarda 764 ms sobre 632k barras, casi lo mismo que el canónico (759 ms), porque
es el mismo código.
5. ¿Qué motor y qué carril elijo?¶
flowchart TD
Q{¿Qué quieres saber?} -->|Rendimiento máximo en CPU<br/>o barrer muchos parámetros| A[VTA_CPU_LEDGER]
Q -->|Coste puro de ejecución<br/>en Nautilus| B[NT_INTENT_REPLAY]
Q -->|Coste de decidir dentro del motor<br/>con features compartidas| C[NT_FEATURES]
Q -->|Flujo completo como en live| D[NT_ONLINE vectorta]
Q -->|Cómo se comportan los indicadores<br/>propios de Nautilus| E[NT_ONLINE native<br/>esperar PARITY_FAIL documentado]
| Situación | Recomendación |
|---|---|
| Barrido de miles de candidatos | VTA_CPU_LEDGER: en B02 1min completo P01 tarda 487 ms frente a 32,5 s de NT_FEATURES (~67×) |
| Validar que el ledger propio es correcto | Paridad VTA_CPU_LEDGER vs NT_INTENT_REPLAY (misma entrada, ejecución distinta) |
| Preparar una estrategia para un motor event-driven | NT_ONLINE(vectorta) y comprobar paridad con el batch |
| Estrategia con indicador sin equivalente nativo (Supertrend, stddev) | No uses native: quedará UNSUPPORTED con motivo |
Resumen¶
- SIM-S: fill en
open[t+1], objetivos{−1, 0, +1}, delta como orden, sin latencia ni libro. - Cuatro carriles que comparten contrato y difieren en qué calcula el motor:
VTA_CPU_LEDGER,NT_INTENT_REPLAY,NT_FEATURES,NT_ONLINE(+ variantenative). - Las features se calculan sobre f64 sin cuantizar; la ejecución, sobre barras cuantizadas a tick.
capabilities.jsonacredita capacidades por sonda y test (102 filas), no por nombre.- Elige el carril según la pregunta: rendimiento, coste de ejecución, coste de decisión o flujo completo.
Para practicar¶
- Para P01 en NVDA 1d, lanza
run_caseconVTA_CPU_LEDGERy conNT_INTENT_REPLAYy comparan_fills. ¿Por qué deben coincidir? - ¿Qué etapa del runner absorbería el coste de construir 631.734 quotes sintéticas? Busca el nombre del campo de tiempos.
- En
capabilities.json, cuenta cuántas filas denautilustienensupports_streaming: true. - Diseña (sin código) un quinto adaptador para un motor ficticio: ¿qué tres pasos de
docs/ADAPTADORES.md§11 tendrías que cumplir antes de medir su velocidad?