Saltar a contenido

Paridad entre motores

«Los dos motores dan lo mismo» es una frase que en este proyecto tiene una definición precisa, capas, tolerancias registradas y un informe que dice dónde empieza la diferencia cuando la hay. Este capítulo explica esa definición, cómo se lee la matriz de 960 casos y cómo interpretar un informe de divergencia.

Qué vas a aprender

  • Qué es paridad y por qué se compara en cuatro capas ordenadas.
  • Las tolerancias registradas en finazbench/features/compare.py y por qué cada una tiene motivo.
  • Qué es la primera divergencia con contexto ±5.
  • Cómo leer runs/parity/summary.json: 960 casos, 672 PASS, 60 PARITY_FAIL, 228 UNSUPPORTED.
  • La paridad de cartera Q01–Q03 (9 PASS).
  • Cómo leer un informe de caso runs/parity/<caso>.md.

1. La idea: comparar por capas y quedarse con la primera que falla

flowchart LR
    F[1. features<br/>tolerancia por primitiva] -->|PASS| S[2. signals<br/>exacto]
    S -->|PASS| X[3. fills<br/>exacto]
    X -->|PASS| E[4. equity<br/>exacto en unidades mínimas]
    F -.->|FAIL| NC1[resto: NOT_COMPARED]
    S -.->|FAIL| NC2[resto: NOT_COMPARED]
    X -.->|FAIL| NC3[resto: NOT_COMPARED]

Paridad entre dos motores comparada por capas

Figura 1. La misma entrada en dos motores; se compara capa a capa y se informa la primera divergencia (el resto queda NOT_COMPARED).

Capa Modo Qué compara
features tolerancia por primitiva valores de los indicadores y máscaras de validez exactas
signals exacto target por barra desde el first_decision_index
fills exacto signal_index, fill_index, event_ts_ns, side, qty, price_ticks, commission_minor
equity exacto en unidades mínimas equity_base tras la misma cuantización

Se informa la primera capa que falla; las posteriores quedan NOT_COMPARED, que no es lo mismo que PASS. Una divergencia de equity que viene de una de señal no es información nueva: lo informativo es dónde empieza.

Analogía: la avería en cadena

Si una fábrica entrega coches con la pintura mal, no sirve de nada inspeccionar el ensamblaje final: hay que encontrar la primera estación donde la pieza salió distinta. La paridad por capas es esa inspección estación por estación.

No se comparan los order_id internos (se exige igualdad de significado) ni el desplazamiento de 1 ns de reproducción.


2. Tolerancias registradas

Los indicadores son números en coma flotante y dos implementaciones correctas pueden diferir en el último bit. Pero cada tolerancia más laxa que la por defecto tiene su motivo escrito y medido: una tolerancia sin motivo es una divergencia escondida.

Primitiva rtol atol Motivo (resumido de compare.py::TOLERANCES)
por defecto (ema, rma, rsi, tr, atr, hl2, supertrend…) 1e-10 1e-12 Propuesta de la guía técnica §5.4 para f64
sma 1e-10 1e-9 Suma corrida de VectorTA: 1,8e-9 absoluto sobre NVDA 1min (632k barras)
stddev 1e-5 1e-7 VectorTA usa E[x²]−E[x]² y pierde ~6 dígitos con precios de 236 y desviaciones < 0,01. Medido 4,7e-6 relativo
bbands 1e-5 1e-7 Hereda la de stddev, propagada por k
bandwidth 1e-5 1e-7 Hereda de bbands; medido 2,9e-10 absoluto en NVDA 1min
rolling_quantile 1e-5 1e-7 Error acotado por su entrada (bandwidth)
donchian 0 0 Máximo y mínimo de valores existentes: igualdad exacta o hay un bug

Regla que no se negocia

Ninguna tolerancia se afloja para poner un test en verde. Si hace falta, se registra en compare.py con el motivo y la medida (así se hizo con bandwidth y rolling_quantile).


3. La primera divergencia, con contexto ±5

compare.py::Divergence no dice solo «falla»: dice desde qué índice, con qué valores y con ±5 barras de contexto de ambos lados. Eso permite distinguir:

  • error de semilla: la diferencia es máxima al principio y decae;
  • error de fórmula: la diferencia persiste o crece.
primera divergencia (value) en 'ema', índice 19: 45.3789999 vs 45.3564330
  idx        izquierda           derecha              diff
  19       45.3789999           45.3564330          +2.257e-02 <--
  20       45.4000475           45.3796299          +2.042e-02
  21       45.3676621           45.3491890          +1.847e-02

(La diferencia decae: firma de semilla.)


4. La matriz: runs/parity/summary.json

960 casos = 20 estrategias (P01–P20) × 6 series (NVDA 1d, 1h, 5min, 1min; KO 1d, 1h) × 2 escenarios de coste (zero, 5bps) × 4 comparaciones. Es una medida de desarrollo (is_benchmark: false): valida paridad, no rendimiento.

Comparación PASS PARITY_FAIL UNSUPPORTED
VTA_CPU_LEDGER vs NT_INTENT_REPLAY 224 0 16
VTA_CPU_LEDGER vs NT_FEATURES 224 0 16
VTA_CPU_LEDGER vs NT_ONLINE(vectorta) 224 0 16
VTA_CPU_LEDGER vs NT_ONLINE(native) 0 60 180
Total 672 60 228
pie title 960 casos de paridad
    "PASS" : 672
    "PARITY_FAIL (solo nativa)" : 60
    "UNSUPPORTED con motivo" : 228

Lectura:

  • 0 PARITY_FAIL en los tres carriles comparables. El ledger propio y los tres carriles de Nautilus producen los mismos fills y la misma equity, céntimo a céntimo, sobre datos reales.
  • Los 16 UNSUPPORTED de cada carril comparable son P12 y P13 en 1d y 1h: con los defaults del catálogo no aplican (1d no tiene «primeros M minutos»; en 1h el rango no es múltiplo de la barra).
  • Los 60 PARITY_FAIL son todos de la variante nativa y corresponden a P01, P02, P11, P16 y P18 (12 casos cada una): indicadores nativos con semilla o definición distinta, documentados.
  • Los 180 UNSUPPORTED nativos: las 15 estrategias restantes necesitan un indicador que Nautilus 1.231.0 no ofrece (p. ej. stddev en P04, supertrend en P06).

data_hash del canónico usado: e8b5fc3612fd944a692af21807706f665fb5bec4f1251d1652fbeaabea6595b0.

4.1 Primera divergencia de la variante nativa (fase A)

Estrategia Primitiva Barra Causa
P01 ema(20) 19 Semilla: Nautilus arranca con el primer valor (nt_seed_first_value)
P02 rsi(14) 14 Semilla del suavizado de Wilder (el ×100 ya está normalizado)
P04 stddev(20) UNSUPPORTED: no hay stddev nativo
P06 supertrend UNSUPPORTED: no hay supertrend nativo
P11 donchian 20–37 según serie Definición: el Donchian nativo incluye la barra actual

La barra de divergencia no depende del coste (es idéntica en zero y 5bps): confirma que la causa es el indicador, no la contabilidad. En fase B se añaden P16 (features, barra 19) y P18 (fills, barra 63).


5. Paridad de cartera: Q01–Q03

runs/parity/summary_weights.json, con --costs 5bps:

Estado Casos
PASS 9 (Q01, Q02, Q03 × tres carriles canónicos)
UNSUPPORTED 3 (carril nativo de panel, por diseño, D-14)
docker compose --profile dev run --rm -T dev python -m tools.parity_weights \
    --strategies Q01 Q02 Q03 --costs 5bps

6. Cómo leer un informe de caso

Cada caso deja runs/parity/<caso>.json y, si falla, runs/parity/<caso>.md. Extracto real de KO_1d_P01_zero_NT_ONLINE_native.md:

Estado: PARITY_FAIL
Primera capa que falla: features

| Capa     | Modo      | Veredicto    | Comparados | Primer índice malo |
| features | tolerance | FAIL         | 1338       | 19                 |
| signals  | exact     | NOT_COMPARED | 0          | —                  |
| fills    | exact     | NOT_COMPARED | 0          | —                  |
| equity   | quantized | NOT_COMPARED | 0          | —                  |

a_variant: vta_running_mean_masked   b_variant: nt_seed_first_value
primitive: ema   role: ema_fast   rtol: 1e-10   atol: 1e-12
max_abs_diff: 0.02256687926549006
Barra 19 · Campo ema_fast.ema · A: 45.378999900000004 · B: 45.356433020734514

Guía de lectura en cinco pasos:

  1. Estado y capa: PARITY_FAIL en features → el problema está en el indicador, no en la ejecución.
  2. Variantes declaradas: si a_variant ≠ b_variant, la divergencia no es un bug (§5.1), pero invalida cualquier speedup que se presente como caso idéntico.
  3. Barra: 19 = period − 1 → primer índice válido → firma de semilla.
  4. Contexto ±5: mira si la diferencia decae (semilla) o persiste (definición).
  5. Notas declaradas: cuantización aplicada, features sobre f64 sin cuantizar, variante nativa esperada.

Por qué la divergencia aparece en features y no en signals

Si al lado online se le atribuyeran las features batch, la capa 1 compararía la serie batch consigo misma, saldría PASS siempre, y la diferencia de semilla aparecería en las señales como si el indicador coincidiera (DEV-05-08). El informe es interpretable porque cada lado aporta sus features.


7. Reproducir

# la matriz entera (960 casos)
docker compose --profile dev run --rm -T dev python -m tools.parity_matrix
# el subconjunto rápido (1d, P01 y P02, zero)
docker compose --profile dev run --rm -T dev python -m tools.parity_matrix --fast
# un corte concreto
docker compose --profile dev run --rm -T dev python -m tools.parity_matrix \
    --assets NVDA --timeframes 1d 1h --strategies P01 --costs zero

Desde Python, run_parity_case(lado_a, lado_b) rechaza dos lados que no describan el mismo caso (distinto activo, marco, estrategia, coste o recorte) y comparte los artefactos (barras, features, política) para que una diferencia de lectura no se disfrace de divergencia de ejecución.


Resumen

  • Paridad = cuatro capas en orden (featuressignalsfillsequity); se informa la primera que falla.
  • Las tolerancias viven en compare.py con motivo medido; donchian exige igualdad exacta.
  • 960 casos: 672 PASS, 60 PARITY_FAIL (todos de la variante nativa) y 228 UNSUPPORTED con motivo; 0 fallos en los tres carriles comparables.
  • Cartera Q01–Q03: 9 PASS.
  • Un informe de caso se lee por capa, variantes, barra y contexto ±5.

Para practicar

  1. Abre runs/parity/summary.json y verifica que los 16 UNSUPPORTED de un carril comparable son P12 y P13 en 1d y 1h.
  2. En el informe de P11 nativo, ¿por qué la barra de divergencia depende de la serie (20 en NVDA 5min, 37 en NVDA 1min) y la de P01 no?
  3. Propón una tolerancia para una primitiva nueva y escribe el «motivo medido» que exigiría compare.py.
  4. ¿Por qué un speedup entre VTA_CPU_LEDGER y NT_ONLINE(native) de P01 no se puede publicar aunque ambos tiempos sean correctos?