Capa de datos de la plataforma¶
El banco de backtesting lee los datos de ficheros Parquet del repositorio (data/canonical). La capa de datos de la plataforma los
lleva a servicios compartidos (PostgreSQL, ClickHouse, Redpanda y MinIO) con tres
garantías nuevas: nada se sobrescribe (log de revisiones), se puede preguntar «¿qué se
sabía en tal fecha?» (selectores point-in-time) y cada conjunto usado queda sellado por su
hash (snapshots en almacenamiento direccionado por contenido). Este capítulo explica esas tres
ideas y la primera carga de datos reales, del 2026-09-22.
Qué vas a aprender¶
- Qué servicio guarda qué y en qué namespace propio vive la plataforma.
- Qué es un log de revisiones append-only (
market_bars_v1) y losarchive_commits. - Cómo funciona un selector PIT (
known_at <= as_of). - Qué es un snapshot sellado en CAS y cómo se verifica por su
manifest_hash. - Qué se ingirió el 2026-09-22 (141 551 barras, 44 series), con qué política y con qué límites.
1. La idea: del fichero al registro auditable¶
flowchart LR
CAN[(data/canonical<br/>Parquet canónico)] -->|ingestar_canonico.py| CH[(ClickHouse finaz_te<br/>market_bars_v1<br/>append-only)]
CH --> AC[archive_commits<br/>data_hash por serie]
CH -->|selector PIT<br/>known_at ≤ as_of| SN[Snapshot<br/>Parquet determinista]
SN -->|sellado SEALED| CAS[(CAS sha256/aa/…<br/>volumen de artefactos)]
CAS --> PG[(PostgreSQL<br/>finaz_trading_engine<br/>publicaciones)]
PG --> RR[ResultRef]
Figura 1. Del fichero del proveedor a los arrays del motor: cada paso es verificable por hash.
Analogía: el libro de contabilidad
Un contable nunca borra un asiento: si se equivoca, añade un asiento de corrección. Al final
puede reconstruir el saldo a cualquier fecha sumando los asientos conocidos hasta entonces.
market_bars_v1 funciona igual con las barras.
2. Namespaces propios en infraestructura compartida¶
La plataforma no duplica los servicios del proyecto finaz (red finaz-net): los reutiliza por DNS
interno en namespaces propios, creados el 2026-09-18/19.
| Servicio | Interno | Namespace propio | Para qué |
|---|---|---|---|
| PostgreSQL | finaz-postgres:5432 |
base finaz_trading_engine, rol finaz_te_app |
Control: admisiones, leases, outbox, publicaciones inmutables, checkpoints, artefactos, snapshots |
| ClickHouse | finaz-clickhouse:8123/9000 |
base finaz_te |
Histórico: market_bars_v1, archive_commits, staging de features |
| Redpanda | finaz-redpanda:9092 |
prefijo de topics finaz-te- |
Log durable y simulación (replay, stream) |
| MinIO S3 | finaz-minio:9000 |
bucket finaz-te-artifacts |
Artefactos (además del volumen finaz-te-artifacts) |
Las credenciales viven en un .env fuera de git y en ~/finaz-secrets-v2/ en el servidor
(permisos 600). El manual no las reproduce.
Topics de Redpanda
finaz-te-market.normalized.v1, finaz-te-control.events.v1, finaz-te-quarantine.v1 y
finaz-te-sim.fixture.replay.faults.normalized.v1. Productor con acks=all; consumidor con
commit manual y auto_offset_reset=none: si se pierde un offset a mitad de corrida el error es
TRANSPORT_OFFSET_PERDIDO, jamás un reinicio silencioso.
3. El log de revisiones market_bars_v1¶
Cada fila de market_bars_v1 lleva: instrumento, ts_ns, OHLCV, revision, known_at,
payload_hash y tombstone. Sin UPDATE jamás: una corrección es una fila nueva con revisión
mayor; un borrado lógico es un tombstone.
| Tabla ClickHouse | Contenido |
|---|---|
market_bars_v1 |
Log de revisiones append-only |
archive_commits |
Un commit por serie archivada, con su data_hash |
batch_features_v1 |
Staging del worker batch (append-only; filtrar por result_id) |
3.1 El selector PIT¶
Regla: para cada (instrumento, ts), quedarse con la máxima revisión con
known_at <= as_of. Las filas conocidas en el futuro se excluyen. Un hueco es una anomalía,
nunca un festivo implícito.
sequenceDiagram
participant Q as Consulta (as_of = 10:00)
participant L as market_bars_v1
L-->>Q: barra X, revision 1, known_at 09:00 ✔
L-->>Q: barra X, revision 2, known_at 11:00 ✘ (futuro)
Note over Q: resultado: revision 1<br/>lo que se sabía a las 10:00
La consulta real la genera sql_pit_barras("finaz_te") en packages/finaz_data/selectors/pit.py (parámetros de ClickHouse, sin concatenar identificadores):
SELECT instrument_id, ts_ns,
argMax((revision, open, high, low, close, volume,
known_at, source, event_id, tombstone, payload_hash),
revision) AS elegido
FROM finaz_te.market_bars_v1
WHERE known_at <= {as_of:DateTime64(9)}
AND ts_ns >= {inicio:Int64} AND ts_ns < {fin:Int64}
GROUP BY instrument_id, ts_ns
HAVING elegido.10 = 0
Fíjate en el HAVING: el tombstone (campo 10 de la tupla) se filtra después de elegir la última revisión. Si se filtrara en el WHERE, un evento borrado «resucitaría» su revisión anterior. La misma regla, en Python, está en seleccionar_pit del mismo módulo.
Por qué no basta con «la última versión»
Si un proveedor corrige hoy un precio de 2020 y tú preguntas qué se sabía en 2021, quedarte con la última versión introduce información del futuro. El selector PIT es lo que evita ese look-ahead de revisiones.
4. Snapshots sellados en CAS¶
Un snapshot congela el resultado de un selector PIT:
- Materialización Parquet determinista: columnas fijas, orden fijo, sin diccionario ni estadísticas (dos materializaciones dan los mismos bytes).
- CAS (content-addressed storage,
finaz_storage): el fichero se guarda ensha256/aa/resto; su nombre es su hash. - Sellado: el manifiesto pasa a
state=SEALEDcon sumanifest_hash. - Publicación en PostgreSQL y emisión de un
ResultRef.
stateDiagram-v2
[*] --> Materializado: selector PIT + Parquet determinista
Materializado --> EnCAS: escribir en sha256/aa/…
EnCAS --> SEALED: manifiesto + manifest_hash
SEALED --> Publicado: fila en PG + ResultRef
SEALED --> SEALED: re-materializar ⇒ mismo hash
Verificar sin confiar
Como el nombre del artefacto es su hash, cualquiera puede recalcular el SHA-256 del Parquet y
compararlo con la URI artifact://sha256/…. Si no coincide, el artefacto está corrupto o no es
el que dice ser (CONTENT_HASH_MISMATCH).
5. La primera carga de datos reales (2026-09-22)¶
Evidencia en qa_reports/v2/runs/2026-09-22/datos/ (ingesta.json, ch_totales.json,
snapshot_manifest.json, run1..3.log) y en docs/v2/DATOS.md, sección «Datos reales ingeridos».
5.1 Qué hay en ClickHouse¶
| Fuente | Series | Marco | Rango (cierre UTC) | Aceptadas | Rechazadas |
|---|---|---|---|---|---|
| yahoo | 40 (34 XNYS, 3 XMAD, 3 FX) | 1d | 2015-01-02 – 2026-09-16 | 117 814 | 330 |
| twelvedata | NVDA, KO | 1d | 2020-03-24 – 2026-09-15 | 2 973 | 11 |
| twelvedata | NVDA, KO | 1h | 2020-03-24 – 2026-09-15 | 20 764 | 44 |
Total market_bars_v1: 141 551 filas, 44 series. archive_commits: 44 commits que suman
exactamente 141 551 filas.
Identidad de serie: instrument_id = real.<fuente>.<mercado>.<activo>.<tf> (p. ej.
real.yahoo.XNYS.AMZN.1d). El sufijo de marco es necesario porque market_bars_v1 no tiene
columna de timeframe y la clave PIT es (instrument_id, ts_ns): sin él, una barra 1h y otra 1d
con el mismo cierre colisionarían (deuda de esquema declarada).
5.2 La política known_at para históricos: HISTORICAL_IMPORT_CLOSE¶
El instante real en que el proveedor publicó cada barra no se conoce. La política:
available_at/known_at=data_available_nsdel canónico = cierre de la barra.- Es la cota más temprana honesta, no tiempo real.
- Cada fila lleva la bandera
HISTORICAL_IMPORTy el dominio temporalreal.historico.import.v1. ingested_ates el reloj de pared de la carga (solo medida).- Certificación PIT:
AS_KNOWNsobre esa política, no evidencia del proveedor.
5.3 Limpieza: rechazo, nunca corrección¶
| Código | Qué rechaza | Ejemplos |
|---|---|---|
SYNTHETIC_ROW |
filas is_synthetic del canónico |
EURUSD 86, USDJPY 189, GBPEUR 55, twelvedata 55 |
DUPLICATE_TS |
timestamps duplicados | — |
| (normalizador AG-008) | OHLC incoherente, no finito, unidades | ninguna fila no sintética falló en esta carga |
El volumen 0 y las barras de domingo de FX se aceptan (son válidos según el contrato).
Diferencia con el Parquet canónico
En el Parquet canónico (clean.v1) una barra con OHLC roto se repara y marca; en la ingesta a
ClickHouse la barra marcada como sintética se rechaza. La capa de datos prefiere no tener la
barra a tener una barra que nadie observó.
5.4 El snapshot sellado¶
| Campo | Valor |
|---|---|
snapshot_id |
snap.real.yahoo.1d.1789599600000000000 |
| Contenido | 40 series Yahoo 1d, 117 814 filas |
as_of |
último cierre = 2026-09-16T23:00Z |
state |
SEALED |
| Parquet | artifact://sha256/ba31371199e0c51007c5bc007b226d5a99005409a78e16c7cfc5e86724d47d63 (6 711 941 bytes) |
manifest_hash |
sha256:606128e45d37a461d78fd45fbfba443ab20db400419d69987990071afe1b2898 |
| Selector | revision_policy: AS_KNOWN, adjustment_policy: RAW, quality_policy: MASKED_RESEARCH |
- Determinismo: dos materializaciones por ejecución y dos ejecuciones (la run2 falló antes del
snapshot por permisos; la run3 terminó) dan el mismo Parquet y el mismo
manifest_hash. - Demostración PIT: con
as_of = 2020-01-01T00:00Zel selector devuelve 50 355 filas, todas conknown_at <= as_ofy ningún cierre posterior.
5.5 Paridad con el Parquet canónico (AMZN 1d)¶
2 942 barras en el Parquet canónico y 2 942 vía selector PIT desde ClickHouse; mismas claves;
0 diferencias en open/high/low/close/volume (igualdad exacta de float64). adj_close no es
comparable porque no se persiste en ClickHouse.
6. Límites declarados (qué NO se ingirió)¶
Límites de la carga del 2026-09-22
- Intradía de Twelve Data 1min, 2min, 5min, 15min y 30min y
1d_long(1999–2026): no ingeridos. adj_close/adj_factor: sin columna enmarket_bars_v1.open_at,session_ideingested_atvan vacíos en el Parquet del snapshot, con la banderaOPEN_AT_NOT_PERSISTED.- Instrumentos y membresías no registrados en PostgreSQL (
finaz_instruments): elUniverseRefdel snapshot se deriva por hash de la lista de ids. calendar_hashes declarativo (no hayCalendarSnapshotreal registrado).- Tamaño en disco (
system.parts) no medible con el rol de lectura.
7. Re-ejecutar la ingesta¶
La ingesta es idempotente: si el commit de una serie ya existe, la serie se omite
(OMITIDA_COMMIT_EXISTENTE); si hay filas sin commit, aborta sin duplicar.
docker run --rm --network finaz-net -e PYTHONPATH=/src/packages \
-v "$PWD:/src:ro" -v "$PWD/<salida>:/out" \
-v ~/FinazTradingEngine/data/canonical:/data/canonical:ro \
-v finaz-trading-engine_finaz-te-artifacts:/artifacts \
-v ~/finaz-secrets-v2/roles:/run/secrets-roles:ro -w /src \
--entrypoint python finaz-te-io:s2 deployment/v2/ops/datos/ingestar_canonico.py --out /out
El contenedor corre como uid 10000 (dueño del volumen de artefactos): <salida> debe ser
escribible por ese uid.
Copias de seguridad
Los backups (PostgreSQL, export de ClickHouse, volúmenes, RPO/RTO medidos) se explican en la parte de operación del manual (parte VIII). Aquí solo interesa saber que existen y que los modelos no se respaldan porque se re-descargan por revisión fijada.
Resumen¶
- La capa de datos usa namespaces propios: PG
finaz_trading_engine, CHfinaz_te, Redpandafinaz-te-*, MinIOfinaz-te-artifacts. market_bars_v1es un log de revisiones append-only; el selector PIT devuelve la máxima revisión conknown_at <= as_of.- Los snapshots se materializan de forma determinista, se guardan en CAS y se sellan por
su
manifest_hash. - El 2026-09-22 se ingirieron 141 551 barras de 44 series con la política
HISTORICAL_IMPORT_CLOSE; el snapshot Yahoo 1d tienemanifest_hash sha256:606128e4…. - Hay límites declarados: sin intradía fino, sin
adj_close, sin instrumentos en PG.
Para practicar¶
- Explica con un ejemplo de dos revisiones por qué quedarse con «la última versión» introduciría look-ahead.
- En
ingesta.json, busca la seriereal.twelvedata.XNYS.KO.1h: ¿cuántas filas se rechazaron y con qué código? - ¿Qué dos colisiones evitaría añadir una columna
bar_specamarket_bars_v1? - Si mañana re-ejecutas la ingesta sin cambios, ¿qué
accionesperas ver por serie? ¿Y si alguien hubiese borrado un commit dejando las filas?