Saltar a contenido

Modelos: jobs S7 (G4–G8)

Qué vas a aprender

  • Qué es un job S7 y por qué los modelos se ejecutan como trabajos one-shot y no como servicios.
  • El protocolo del spool (finaz-te-jobs) y del artefacto con sha256.
  • Qué hace cada familia: quant (Ridge exacto, CatBoost challenger, ARIMA/ETS/GARCH), opt (Ledoit-Wolf, ERC/mínima varianza, veto del ConstraintValidator, CP-SAT para lotes), neural (NHITS real en :s4), chronos (Chronos-2) y timesfm25 (TimesFM 2.5).
  • Cómo se construyen labels con madurez y embargo, y los splits walk-forward.
  • Cómo lanzar un job y leer su artefacto, con la evidencia real de qa_reports/v2/runs/2026-09-20 y 2026-09-22.

Fuentes

packages/finaz_runtime/jobs/{runner,spool}.py, deployment/v2/README.md (sección S7), docs/v2/{MOTORES,GATES}.md, paquetes finaz_models_*, finaz_training, finaz_risk, finaz_allocation, finaz_constraints, finaz_discrete, y los artefactos reales qa_reports/v2/runs/2026-09-20/*.result.json y (en el servidor) qa_reports/v2/runs/2026-09-22/g7/.

Datos sintéticos

Todos los jobs de este capítulo trabajan con datos sintéticos deterministas (semilla fija). Sirven para demostrar que el adaptador real funciona de extremo a extremo con sus garantías (causalidad, provenance, artefacto verificable). No son evidencia de rendimiento predictivo en mercado.


1. ¿Por qué jobs y no servicios?

En la auditoría del 2026-09-19 (docs/v2/GAP_REPORT.md, hallazgo 5) se comprobó que los CMD de las imágenes de modelos apuntaban a módulos inexistentes (python -m finaz_quant…). En vez de crear módulos vacíos que «arrancan», se tomó una decisión honesta: los modelos corren como jobs S7.

  • Perfil propio en Compose (quant, opt, neural, chronos, timesfm25), restart: "no".
  • CMD real: python -m finaz_runtime.jobs.runner --rol <familia>.
  • Hacen una operación real con el adaptador real, escriben un artefacto verificable y salen.

Analogía: el laboratorio de análisis

Un servicio es una ventanilla abierta todo el día. Un job es una muestra que entregas al laboratorio: la procesan una vez, te dan un informe firmado (el artefacto con su hash) y el técnico se va. Para un entrenamiento o una inferencia puntual, no necesitas la ventanilla abierta.

El protocolo del spool

sequenceDiagram
    autonumber
    participant O as Operador
    participant SP as volumen finaz-te-jobs<br/>/spool/jobs
    participant J as job-{rol} (one-shot)
    participant AR as volumen finaz-te-artifacts<br/>/artifacts/jobs/{job_id}/
    O->>SP: escribe {job_id}.json {job_id, rol, operacion, params?}
    O->>J: docker compose --profile {rol} run --rm job-{rol}
    J->>SP: rename atómico → .claimed (un solo consumidor)
    J->>J: ejecuta la operación con el adaptador real
    J->>AR: artefacto.json (JSON canónico) + manifest.json (sha256)
    J->>SP: {job_id}.result.json, elimina el .claimed
    J-->>O: stdout: sólo «ARTEFACTO <sha256>», exit 0

Reglas del runner (finaz_runtime/jobs/runner.py y spool.py):

Regla Detalle
Reclamo atómico rename a .claimed: si dos runners del mismo rol compiten, el sistema de ficheros decide quién la ejecuta
Sin petición ejecuta la operación por defecto de su familia (self-check real, nunca «duerme vacío»)
Estado honesto PASS, INCOMPLETO, UNSUPPORTED o FAIL: nunca se inventa un PASS
Disciplina de salida stdout lleva sólo la línea ARTEFACTO <sha256>; todo lo demás va a stderr
Códigos 0 ok, 1 fallo operativo, 2 uso indebido
Aislamiento el runner de modelos no toca PG/CH/MinIO: cómputo puro sobre su imagen, volumen de modelos y spool

Operaciones por familia

Rol Operación por defecto Adaptadores reales Gate
quant ridge_catboost_stats Ridge + CatBoost + ARIMA/ETS (statsmodels) G4
opt erc_cpsat ERC + mínima varianza + CP-SAT (OR-Tools) G5, G6
neural torch_nhits Torch (roundtrip) + NHITS real en CPU vs naive estacional G7
chronos inferencia_chronos Chronos-2 con checkpoint SHA verificado G8
timesfm25 inferencia_timesfm25 TimesFM 2.5 con checkpoint SHA verificado G8

2. Cómo lanzar un job y leer su artefacto

cd ~/FinazTradingEngine
docker run --rm -v finaz-trading-engine_finaz-te-jobs:/spool alpine sh -c \
  'mkdir -p /spool/jobs; echo "{\"job_id\":\"mi-quant-001\",\"rol\":\"quant\",\"operacion\":\"ridge_catboost_stats\"}" > /spool/jobs/mi-quant-001.json'
docker compose -p finaz-trading-engine -f deployment/v2/compose.yaml \
  --profile quant run --rm job-quant
# stdout: ARTEFACTO <sha256>

Para neural, con la imagen de G7: FINAZ_NEURAL_IMAGE=finaz-te-neural:s4 delante del comando.

docker run --rm -v finaz-trading-engine_finaz-te-artifacts:/artifacts:ro alpine \
  cat /artifacts/jobs/mi-quant-001/manifest.json /artifacts/jobs/mi-quant-001/artefacto.json
import hashlib, json

manifest = json.load(open("manifest.json"))
datos = open("artefacto.json", "rb").read()
calculado = hashlib.sha256(datos).hexdigest()
esperado = manifest["artefacto_sha256"].split(":")[-1]
print("OK" if calculado == esperado else "HASH DISTINTO", manifest["estado"])

Requisito de pesos

Neural, chronos y timesfm25 necesitan los checkpoints en el volumen de modelos (/models/…), descargados una vez por el operador con verificación SHA. Los pesos nunca se «hornean» en la imagen. Sin pesos, el job no inventa: devuelve un estado no-PASS con motivo.

Anatomía de un manifest real

qa_reports/v2/runs/2026-09-20/s7-jobs/g4-quant-001/manifest.json:

{
  "artefacto_sha256": "sha256:sha256:02b68b05c83ac272e28820e7e398167f8464f25ca8d126edea5eccec65edf100",
  "bytes": 987,
  "estado": "PASS",
  "job_id": "g4-quant-001",
  "operacion": "ridge_catboost_stats",
  "rol": "quant",
  "ruta": "/artifacts/jobs/g4-quant-001/artefacto.json",
  "schema_version": "1.0.0"
}
¿Por qué sha256:sha256:?

Es un prefijo duplicado que aparece en los manifests reales (también en g7-neural-002). Nace en packages/finaz_runtime/jobs/runner.py, que antepone "sha256:" a un digest que ya lo trae. El hash que sigue es correcto; sólo sobra un prefijo. Al verificar, quédate con el último segmento tras : (como en el ejemplo de Python). Es una pequeña deuda de formato, no un problema de integridad.

Verificado a mano

Para este manual se recalculó el hash del artefacto de g4-quant-001: los 987 bytes que declara el manifest dan exactamente 02b68b05…edf100. El fichero artefacto.json tiene 988 bytes porque el propio runner lo escribe como JSON canónico más un salto de línea final, y el hash (y bytes) se calculan sobre el JSON canónico sin ese salto; si verificas esa copia, quítale el \n final (head -c 987 artefacto.json | sha256sum). El campo bytes del manifest te dice cuántos bytes cuentan.


3. Labels con madurez y embargo; walk-forward

Antes de entrenar nada hay que responder: ¿cuándo se conoce la etiqueta? Un retorno a 5 días del lunes no se conoce hasta el lunes siguiente. Usarlo antes es mirar el futuro.

finaz_training/labels (AG-018) construye etiquetas de retorno simple desde un TargetSpec congelado:

  • Cada etiqueta tiene label_available_at. Madurez: sólo es utilizable si label_available_at <= corte. Las inmaduras se excluyen (su resultado es desconocido).
  • Embargo (aplicar_embargo): se purga toda etiqueta cuya ventana [origen, disponible] toque la zona [inicio_embargo, fin_embargo).
  • Recalcular con el mismo corte da exactamente lo mismo (Q010).

finaz_training/validation hace splits cronológicos:

  • Intervalos [start, end) en tiempo de evento; políticas de ventana FIXED, ROLLING, EXPANDING.
  • Validación interna estrictamente antes del test externo; los hiperparámetros sólo miran la validación interna.
  • Hueco de purga + embargo en nanosegundos entre segmentos.
  • Nunca K-fold aleatorio por defecto.
tiempo de evento ──────────────────────────────────────────────────────────►
Fold 1: [ train ........................ ]░░[ validación ]░░[ test ]
Fold 2: [ train ................................ ]░░[ validación ]░░[ test ]
Fold 3: [ train ........................................ ]░░[ validación ]░░[ test ]
        ░░ = hueco de purga + embargo (ns)

Según docs/v2/MOTORES.md (AG-018): 55 de 60 etiquetas maduras al corte y 3 folds. Es una cifra de la tabla de motores: no hay fichero de evidencia en qa_reports/v2/runs/ que la registre (los tests tests_v2/temporal/ cubren madurez y walk-forward, pero no dejan ese recuento).

Analogía: corregir exámenes

No puedes usar la nota de un examen para decidir algo antes de que el profesor la haya publicado, aunque el examen ya se hiciera. available_at es la fecha de publicación de la nota; el embargo es no mirar los exámenes que se corrigen justo en la frontera entre dos cursos.


4. Quant (G4): Ridge, CatBoost y estadísticos

Pieza Paquete Qué tiene de especial
Ridge (baseline) finaz_models_tabular Implementado desde cero con NumPy (Cholesky): siempre disponible en CPU, recupera pesos conocidos
CatBoost (challenger) finaz_models_tabular Importación guardada: si no está, CatBoostNoDisponible; sin fallback silencioso a otro modelo
ARIMA / ETS / GARCH / AutoARIMA finaz_models_statistics statsmodels/arch/StatsForecast con puertas; intervalos honestos (una banda de volatilidad no es un intervalo de predicción)

Artefacto real g4-quant-001 (2026-09-20), recortado:

{
  "estado": "PASS", "operacion": "ridge_catboost_stats", "rol": "quant",
  "ridge": {"adapter_id": "finaz.tabular.ridge", "n_muestras": 200, "n_features": 4,
            "semilla": 7, "rmse": 0.01688, "desviacion_y": 4.61, "paridad_recarga": true,
            "artifact_sha256": "sha256:73ec7614…"},
  "catboost": {"adapter_id": "finaz.tabular.catboost", "driver_version": "1.2.8",
               "estado": "PASS", "rmse": 1.334, "hash_cbm": "sha256:8298468f…"},
  "estadisticos": {"arima": {"parametros": {"orden": [1, 0, 0]}, }, "ets": {"monotona": true, }},
  "supported": ["finaz.tabular.ridge"], "unsupported": []
}

Cómo leerlo: el RMSE del Ridge (0,017) frente a la desviación de y (4,6) dice que recupera la relación lineal sintética; paridad_recarga: true dice que el modelo guardado y recargado predice igual. CatBoost, como challenger, tiene su propio hash de artefacto (hash_cbm).


5. Opt (G5, G6): riesgo, cartera, veto y lotes

La cadena de cartera separa estrictamente responsabilidades:

flowchart LR
    A[AlphaEstimate] --> R[RiskEngine<br/>finaz_risk<br/>Ledoit-Wolf, VaR/ES]
    R --> AL[Allocation<br/>finaz_allocation<br/>ERC / min-var / max-div]
    AL -- PortfolioCandidate --> V{ConstraintValidator<br/>finaz_constraints}
    V -- aprobado --> PT[PortfolioTarget]
    V -- vetado --> H[HOLD<br/>sin target nuevo]
    PT --> D[Discreto<br/>finaz_discrete<br/>CP-SAT en subproceso]
    D -- DiscreteCandidate --> V2{ConstraintValidator}
    V2 -- aprobado --> DT[DiscreteTarget]
Pieza Qué hace Detalle honesto
finaz_risk (AG-022) Puerta dura sobre AlphaEstimate, Ledoit-Wolf propio, VaR/ES históricos Paridad con sklearn en tests
finaz_allocation (AG-024) Produce sólo PortfolioCandidate (ERC, mínima varianza, max-div) FEASIBLE ≠ OPTIMAL: se informa el estado real del solver (Clarabel vía CVXPY)
finaz_constraints (AG-023) Productor exclusivo de targets aprobados; veto independiente Anti-bypass probado (ApprovedTarget). Un HOLD no implica que la cartera actual sea segura
finaz_discrete (AG-025) Pesos → lotes enteros (largest-remainder, greedy, CP-SAT) CP-SAT corre en subproceso por un conflicto de librerías (dos libhighs: highspy vs OR-Tools)

Artefacto real g5-opt-001 (2026-09-20):

{
  "estado": "PASS", "motivo": "ERC+minvar+CP-SAT reales OPTIMAL",
  "erc":    {"estado_solver": "OPTIMAL", "pesos": [0.5, 0.5], "objetivo_5050": true,
             "solver_id": "finaz_allocation.erc_punto_fijo.v1"},
  "minvar": {"estado_solver": "OPTIMAL", "pesos": [0.3617, 0.2021, 0.4362],
             "solver_id": "finaz_allocation.min_var_cvxpy_clarabel.v1"},
  "cpsat":  {"estado_normalizado": "OPTIMAL", "lotes": [5, 1, 4], "objetivo": 10000.0,
             "prueba_optimalidad": true, "elapsed_ms": 1772.99}
}
  • ERC 50/50 es el caso analítico: con dos activos simétricos, la contribución al riesgo igual implica pesos iguales. Si el solver no diera 0,5/0,5, habría un fallo.
  • CP-SAT devuelve lotes enteros con prueba de optimalidad: por eso el mismo artefacto certifica G6 (discreto).

Analogía: el arquitecto y el inspector

Allocation es el arquitecto que dibuja la casa (candidato). ConstraintValidator es el inspector que firma la licencia (target aprobado). El arquitecto no puede firmarse su propia licencia, y el inspector no rediseña la casa: sólo aprueba o rechaza.


6. Neural (G7): NHITS real en :s4 desde el 2026-09-22

El 2026-09-20, el job neural era INCOMPLETO: Torch funcionaba (roundtrip PASS), pero NHITS se declaraba UNSUPPORTED con honestidad. El 2026-09-22, con la imagen finaz-te-neural:s4 (Torch 2.9.1+cpu, NeuralForecast 3.2.2, pandas 3.0.6), el job g7-neural-002 es PASS.

Lo que el adaptador garantiza

Garantía Cómo se comprobó en vivo
Puerta causal de exógenas Cada exógena declara rol: xh histórica, finde futura conocida (con available_at), sector estática. Una fuga de futuro → FUTURE_COVARIATE_LEAKAGE (negativo ejecutado en vivo)
Deadline Un plazo diminuto → TIMEOUT, sin resultados parciales (negativo en vivo). Presupuesto normal: 120 s
Baseline comparable Naive estacional (periodo 24) con cuantiles empíricos de residuos al mismo rezago
Artefacto sin pickle Pesos en NPZ (sin_pickle: true), recargados con paridad exacta
Cuantiles sin cruce Pérdida MQLoss con niveles 0,1/0,5/0,9; si se cruzan, se reparan con la política declarada REORDENAMIENTO_V1 (en este run: 0 filas reparadas)
Sólo CPU dispositivo: cpu

Los números del artefacto (datos sintéticos)

Configuración: ventana de entrada 96, horizonte 24, 2 pilas, ancho oculto 128, 200 pasos de entrenamiento, semilla 7, 936 muestras de entrenamiento y un tramo reservado de 24 pasos (936–960).

Métrica (tramo reservado) Baseline naive estacional NHITS Skill
MAE 1,4146 0,7090 0,50
Pinball 0,3699 0,2034 0,45

Cobertura observada de los cuantiles en esos 24 pasos: 0,1 → 8,3 %; 0,5 → 37,5 %; 0,9 → 91,7 %.

Recursos (g7-recursos.txt): ajuste de NHITS ~6,1 s, predicción 0,04 s, contenedor ~23 s de pared, pico de memoria del contenedor ~363 MiB (RSS máximo del proceso ~0,5 GiB). Suite neural en la imagen: 55/55.

Tamaño del modelo: 165 167 parámetros (n_parametros en el resultado y en g7-recursos.txt). El artefacto NPZ declara parametros_total: 165 170 en 18 tensores (valores guardados en el NPZ). La diferencia de 3 no está explicada en la evidencia; la cifra del modelo es n_parametros.

Lo que G7 NO certifica

  • Entrenado y evaluado sólo con datos sintéticos deterministas.
  • La cobertura de cuantiles es nominal, no una calibración certificada (24 puntos no bastan).
  • El spec del adaptador aún no está mapeado al TrainingSpec congelado, y los códigos FUTURE_COVARIATE_LEAKAGE / CAPS_EXCEEDED / QUANTILE_CROSSING son locales, pendientes de ADR.
  • La imagen :s4 aún no está en release-manifest.s2.json (su SBOM está en la carpeta de G7).

7. Foundation (G8): Chronos-2 y TimesFM 2.5

Los modelos foundation vienen preentrenados: no se entrenan aquí, se infieren. El riesgo es otro: ¿qué modelo exacto es, de dónde viene y con qué licencia? Por eso G8 exige identidad del checkpoint.

Chronos-2 (finaz_models_chronos, AG-027) TimesFM 2.5 (finaz_models_timesfm, AG-028)
Revisión pineada 29ec3766… 1d952420… (google/timesfm-2.5-200m-pytorch)
SHA del checkpoint sha256:ddcda3c7… sha256:2f776efe…
Vetos JAX XReg, JAX, TimesFM 3.0 (pin de generación 2.5)
Salida ForecastDistribution con point_kind: MEDIAN y cuantiles 0,1/0,5/0,9 ídem; punto == mediana

Extracto real de g8-chronos-001.result.json (2026-09-20):

"checkpoint": {"n_tensores": 170,
               "revision": "29ec3766d36d6f73f0696f85560a422f50e8498c",
               "sha256": "sha256:ddcda3c7508bf2528087723e98a20707cc04b7f370ae275a9fd88078ddba4f42"},
"distribucion": {"kind": "ForecastDistribution",
  "model": {"kind": "ModelRef", "training_provenance": "UNKNOWN", "training_cutoff": null, },
  "rows": [{"horizon_step": 1, "point": 98.586, "point_kind": "MEDIAN",
            "quantiles": [{"probability": 0.1, "value": 98.405},
                          {"probability": 0.5, "value": 98.586},
                          {"probability": 0.9, "value": 98.843}]}, ]}

training_provenance: UNKNOWN es honestidad, no un error

No sabemos con qué datos ni hasta qué fecha se entrenó el checkpoint público. El pack lo dice claro: sin evidencia histórica de disponibilidad, un backtest con foundation es COUNTERFACTUAL_RESEARCH, no un backtest PIT. Por eso training_cutoff es null en vez de una fecha inventada.


8. Mapa de evidencia

Job Fecha Estado Dónde
g4-quant-001 2026-09-20 PASS qa_reports/v2/runs/2026-09-20/g4-quant-001.result.json + s7-jobs/g4-quant-001/
g5-opt-001 2026-09-20 PASS …/2026-09-20/g5-opt-001.result.json
g7-neural-001 2026-09-20 INCOMPLETO (NHITS UNSUPPORTED honesto) …/2026-09-20/g7-neural-001.result.json
g7-neural-002 2026-09-22 PASS (:s4) …/2026-09-22/g7/ (result, manifest, artefacto, SBOM, JUnit, recursos)
g8-chronos-001 2026-09-20 PASS …/2026-09-20/g8-chronos-001.result.json
g8-timesfm-001 2026-09-20 PASS …/2026-09-20/g8-timesfm-001.result.json

Resumen

  • Los modelos corren como jobs S7 one-shot: petición en el spool finaz-te-jobs, reclamo atómico, una operación real, artefacto + manifest.json con sha256, stdout ARTEFACTO <sha256>.
  • Estados honestos: PASS, INCOMPLETO, UNSUPPORTED, FAIL; sin fallback silencioso.
  • Labels con madurez (available_at <= corte) y embargo; walk-forward cronológico, nunca K-fold aleatorio por defecto.
  • Quant: Ridge exacto + CatBoost challenger + ARIMA/ETS/GARCH. Opt: Ledoit-Wolf, ERC 50/50, mínima varianza, veto exclusivo del ConstraintValidator y CP-SAT con prueba de optimalidad.
  • Neural: NHITS real en CPU (:s4, 2026-09-22) con puerta causal, deadline, baseline naive estacional y artefacto sin pickle; skill MAE 0,50 sobre datos sintéticos.
  • Foundation: Chronos-2 y TimesFM 2.5 con revisión y SHA verificados; provenance UNKNOWN declarada.

Para practicar

  1. Escribe la petición de spool para un job opt con job_id practica-opt-001. ¿Qué pasa si lanzas el job sin petición?
  2. Con el artefacto de g5-opt-001, comprueba a mano que los pesos de mínima varianza suman 1.
  3. Explica por qué finaz_allocation no puede devolver un PortfolioTarget y qué paquete sí.
  4. En el artefacto de G7, ¿qué exógena es FUTURE_KNOWN_EXOG y por qué necesita available_at?
  5. Calcula el skill de pinball de G7 a partir de los dos valores de la tabla (pista: 1 − modelo / baseline).
  6. Busca en un resultado de Chronos o TimesFM el campo training_provenance y explica, en dos frases, por qué su valor impide llamar «PIT» a un backtest con ese modelo.