Saltar a contenido

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 los archive_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]

Tubería de datos del CSV al runtime

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:

  1. Materialización Parquet determinista: columnas fijas, orden fijo, sin diccionario ni estadísticas (dos materializaciones dan los mismos bytes).
  2. CAS (content-addressed storage, finaz_storage): el fichero se guarda en sha256/aa/resto; su nombre es su hash.
  3. Sellado: el manifiesto pasa a state=SEALED con su manifest_hash.
  4. 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_ns del canónico = cierre de la barra.
  • Es la cota más temprana honesta, no tiempo real.
  • Cada fila lleva la bandera HISTORICAL_IMPORT y el dominio temporal real.historico.import.v1.
  • ingested_at es el reloj de pared de la carga (solo medida).
  • Certificación PIT: AS_KNOWN sobre 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:00Z el selector devuelve 50 355 filas, todas con known_at <= as_of y 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 en market_bars_v1.
  • open_at, session_id e ingested_at van vacíos en el Parquet del snapshot, con la bandera OPEN_AT_NOT_PERSISTED.
  • Instrumentos y membresías no registrados en PostgreSQL (finaz_instruments): el UniverseRef del snapshot se deriva por hash de la lista de ids.
  • calendar_hash es declarativo (no hay CalendarSnapshot real 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, CH finaz_te, Redpanda finaz-te-*, MinIO finaz-te-artifacts.
  • market_bars_v1 es un log de revisiones append-only; el selector PIT devuelve la máxima revisión con known_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 tiene manifest_hash sha256:606128e4….
  • Hay límites declarados: sin intradía fino, sin adj_close, sin instrumentos en PG.

Para practicar

  1. Explica con un ejemplo de dos revisiones por qué quedarse con «la última versión» introduciría look-ahead.
  2. En ingesta.json, busca la serie real.twelvedata.XNYS.KO.1h: ¿cuántas filas se rechazaron y con qué código?
  3. ¿Qué dos colisiones evitaría añadir una columna bar_spec a market_bars_v1?
  4. Si mañana re-ejecutas la ingesta sin cambios, ¿qué accion esperas ver por serie? ¿Y si alguien hubiese borrado un commit dejando las filas?