Saltar a contenido

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 variante NT_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 t se ejecuta en open[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; continueNotImplementedError)

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 (+ variante native).
  • Las features se calculan sobre f64 sin cuantizar; la ejecución, sobre barras cuantizadas a tick.
  • capabilities.json acredita 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

  1. Para P01 en NVDA 1d, lanza run_case con VTA_CPU_LEDGER y con NT_INTENT_REPLAY y compara n_fills. ¿Por qué deben coincidir?
  2. ¿Qué etapa del runner absorbería el coste de construir 631.734 quotes sintéticas? Busca el nombre del campo de tiempos.
  3. En capabilities.json, cuenta cuántas filas de nautilus tienen supports_streaming: true.
  4. 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?