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.pyy 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]
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.
stddeven P04,supertrenden 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:
- Estado y capa:
PARITY_FAILenfeatures→ el problema está en el indicador, no en la ejecución. - 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. - Barra: 19 =
period − 1→ primer índice válido → firma de semilla. - Contexto ±5: mira si la diferencia decae (semilla) o persiste (definición).
- 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 (
features→signals→fills→equity); se informa la primera que falla. - Las tolerancias viven en
compare.pycon motivo medido;donchianexige 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¶
- Abre
runs/parity/summary.jsony verifica que los 16 UNSUPPORTED de un carril comparable son P12 y P13 en 1d y 1h. - 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?
- Propón una tolerancia para una primitiva nueva y escribe el «motivo medido» que exigiría
compare.py. - ¿Por qué un speedup entre
VTA_CPU_LEDGERyNT_ONLINE(native)de P01 no se puede publicar aunque ambos tiempos sean correctos?