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 consha256. - 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-20y2026-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¶
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 silabel_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 ventanaFIXED,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
TrainingSpeccongelado, y los códigosFUTURE_COVARIATE_LEAKAGE/CAPS_EXCEEDED/QUANTILE_CROSSINGson locales, pendientes de ADR. - La imagen
:s4aún no está enrelease-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.jsoncon sha256, stdoutARTEFACTO <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
UNKNOWNdeclarada.
Para practicar¶
- Escribe la petición de spool para un job
optconjob_idpractica-opt-001. ¿Qué pasa si lanzas el job sin petición? - Con el artefacto de
g5-opt-001, comprueba a mano que los pesos de mínima varianza suman 1. - Explica por qué
finaz_allocationno puede devolver unPortfolioTargety qué paquete sí. - En el artefacto de G7, ¿qué exógena es
FUTURE_KNOWN_EXOGy por qué necesitaavailable_at? - Calcula el skill de pinball de G7 a partir de los dos valores de la tabla
(pista:
1 − modelo / baseline). - Busca en un resultado de Chronos o TimesFM el campo
training_provenancey explica, en dos frases, por qué su valor impide llamar «PIT» a un backtest con ese modelo.