Strasmore Research
Educación Matt ConnorPor Matt Connor · data as of August 16, 2026 · refreshed weekly

h5i-db: datos point-in-time para backtests

Los datos point-in-time evitan el look-ahead bias en backtests. Conoce cómo h5i-db versiona cada escritura y qué revela el filing lag sobre historiales desactualizados.

Los datos point-in-time registran lo que contenía un conjunto de datos en una fecha pasada. En un backtest, permiten distinguir un resultado defendible de otro que incorporó silenciosamente cifras del futuro. h5i-db es una base de datos open source de series temporales, desarrollada en Rust y con una API de Python. Almacena cada escritura como una versión numerada y permite que cualquier lectura se fije en una versión anterior. A continuación se muestra el look-ahead bias que este mecanismo bloquea, medido con datos reales de reportes financieros, seguido de un escenario que puede ejecutarse en una laptop.

¿Qué son los datos point-in-time en un backtest?

Cada dato de mercado tiene dos marcas de tiempo. El tiempo del evento indica cuándo ocurrió. El tiempo de llegada indica cuándo pasó a estar disponible para cualquier persona ajena a la firma que lo reportó. Un informe trimestral de posiciones describe las tenencias del último día del trimestre, pero se hace público semanas después. Por tanto, un modelo que solo cruza los datos por tiempo del evento recibe información que nadie tenía en ese momento.

La diferencia se puede medir. Los gestores institucionales presentan el Form 13F después del cierre de cada trimestre. El panel siguiente mide los días transcurridos entre el trimestre que describe cada filing y la fecha en que se presentó.

ConsultaTiempo que tardan las presentaciones de tenencias 13F en hacerse públicas
El SQL exacto detrás de cada cifra
WITH per_filing AS
(
    SELECT
        accession_number,
        any(toDate(parseDateTimeBestEffortOrNull(toString(period)))) AS period_end,
        any(toDate(filing_date))                                     AS filed_on
    FROM global_markets.stocks_13f_filings
    WHERE filing_date >= '2023-01-01'
    GROUP BY accession_number
)
SELECT
    toString(period_end)                                 AS period_end_date,
    countDistinct(accession_number)                      AS filings_count,
    round(avg(dateDiff('day', period_end, filed_on)), 1) AS avg_days_to_public
FROM per_filing
WHERE period_end IS NOT NULL
  AND filed_on >= period_end
  AND filed_on <= period_end + 400
GROUP BY period_end
HAVING filings_count >= 100
ORDER BY period_end
Run this yourself

Para el trimestre terminado en 2026-06-30, los filings de 10688 llegaron, en promedio, 34.1 días después del periodo que cubrían. El panel repite esta medición para 16 trimestres. Un gestor también puede modificar un informe mucho después de haberlo presentado. Por eso, el registro que describe una fecha pasada sigue cambiando después de que esa fecha ya transcurrió.

El sesgo de anticipación es un problema de almacenamiento

Nuestra guía sobre el sesgo de anticipación en backtesting trata las filtraciones como una cuestión de disciplina: aplicar un rezago a cada variable y respetar las fechas de publicación. La disciplina funciona hasta que alguien la olvida, y el fallo pasa inadvertido. Un backtest con datos filtrados muestra un Sharpe ratio mejor, sin generar ningún error.

El almacenamiento point-in-time traslada la garantía a una capa inferior. Cuando el marco de datos que recibe una estrategia proviene de una lectura fijada a una versión, una fila escrita después de esa versión no puede aparecer en él, independientemente de lo que haga después el código de la estrategia. La comprobación deja de ser una revisión de código y pasa a ser una propiedad de la lectura.

Los dividendos muestran la brecha temporal desde el otro lado. Primero se anuncia un dividendo en efectivo y después llega la fecha ex-dividendo. Una tabla cargada hoy contiene ambas fechas para cada pago, incluidos los que todavía no se habían anunciado en la fecha simulada.

ConsultaDías entre el anuncio de dividendos y su fecha ex, por mes
El SQL exacto detrás de cada cifra
SELECT
    toString(toStartOfMonth(ex_dividend_date))                         AS month,
    round(avg(dateDiff('day', declaration_date, ex_dividend_date)), 1) AS avg_days_announced_ahead,
    countDistinct(ticker)                                              AS payers_count
FROM global_markets.stocks_dividends
WHERE ex_dividend_date >= toStartOfMonth(today() - 730)
  AND ex_dividend_date <  toStartOfMonth(today())
  AND declaration_date >= '1990-01-01'
  AND declaration_date <= ex_dividend_date
GROUP BY month
ORDER BY month
Run this yourself

En el mes que comienza 2026-07-01, los anuncios se produjeron en promedio 88.2 días antes de la fecha ex-dividendo, y el panel cubre 24 meses de la misma medición. Si se lee una tabla moderna de dividendos para una fecha simulada dentro de esa brecha, el pago ya aparece allí, varias semanas antes de que existiera el anuncio.

El historial almacenado se reescribe

La llegada tardía es un modo de falla. La reformulación es el otro. Las acciones corporativas reescriben precios que ya se habían registrado: después de un split four-for-one, cada precio anterior de una serie ajustada se divide entre cuatro, y la serie descargada hoy ya no coincide con la cinta que observó un trader. Nuestra nota sobre historial de precios ajustado por splits desarrolla el cálculo. Lo importante aquí es la frecuencia.

ConsultaSplits de acciones efectivos cada trimestre, directos e inversos
El SQL exacto detrás de cada cifra
SELECT
    toString(toStartOfQuarter(execution_date))    AS quarter_start_date,
    countDistinctIf(id, split_to > split_from)    AS forward_splits,
    countDistinctIf(id, split_to < split_from)    AS reverse_splits
FROM global_markets.stocks_splits
WHERE execution_date >= toStartOfQuarter(today() - 1460)
  AND execution_date <  toStartOfQuarter(today())
  AND split_from > 0
  AND split_to   > 0
GROUP BY quarter_start_date
ORDER BY quarter_start_date
Run this yourself

En el trimestre que comenzó en 2026-04-01, entraron en vigor 131 splits forward y 303 splits reverse. Cada uno reformula un historial de precios que un pipeline de investigación quizá ya haya almacenado en caché. El almacenamiento con versiones no evita la reformulación. Registra el nuevo estado como una nueva versión y mantiene legible el anterior. Eso es lo que convierte un resultado obsoleto en uno reproducible.

Las marcas de tiempo de las noticias esconden la misma trampa, en menor escala.

ConsultaHora de publicación de titulares del mercado, según la hora de Nueva York
El SQL exacto detrás de cada cifra
SELECT
    formatDateTime(toTimeZone(published_utc, 'America/New_York'), '%H:00') AS et_hour,
    countDistinct(id)                                                     AS articles
FROM global_markets.stocks_news
WHERE published_utc >= today() - 90
GROUP BY et_hour
ORDER BY et_hour
Run this yourself

Los titulares se publican durante todo el día, a lo largo de las 24 horas de la jornada de Nueva York. La hora de las 09:00 concentró 790 artículos durante los 90 días anteriores, mientras que la hora de las 20:00 concentró 404. Asignar un titular vespertino al cierre de las 16:00 de ese día entrega a una estrategia que opera al cierre varias horas de información futura.

Un escenario puntual que puede ejecutar

Todo lo descrito aquí se limita al paquete de Python. El proyecto también incluye una herramienta de línea de comandos en Rust, pero este recorrido no necesita esa instalación aparte. Los datos de ejemplo se generan localmente, sin descargas.

  1. Instale la versión fijada: pip install 'h5i-db==0.1.6', publicada el 4 de agosto de 2026. Requiere Python 3.9 o una versión posterior e incluye pyarrow>=14. Hay wheels precompilados para Linux en x86-64 y arm64, además de macOS con Apple silicon y Windows en x86-64.
  2. Describa los datos una sola vez, después de import pyarrow as pa y import pyarrow.parquet as pq: schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]).
  3. Escriba dos filas inventadas en un archivo local con pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet'), donde d1 y d2 sean objetos datetime con zona horaria.
  4. Cree la base de datos y la tabla, y asigne un nombre a la columna de tiempo: db = h5i_db.Database('pit.db', create=True) y luego db.create_table('prices', schema, time_column='ts').
  5. Ingrese el archivo con una clave: db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). La llamada devuelve el commit que realizó.
  6. Ejecute de nuevo esa misma línea. El proyecto documenta que una repetición con la misma clave encuentra el commit que ya produjo y lo devuelve con "segments_added": 0, en lugar de escribir las filas por segunda vez. Imprima db.versions('prices') a ambos lados del reintento y compruebe que la lista de versiones no cambia.
  7. Ingrese un segundo día con idempotency_key='load-day2' y luego consulte ambos días: db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas().
  8. Lea la tabla tal como estaba antes de incorporar el segundo día: db.read('prices', version=1). El mismo método acepta los argumentos as_of= y snapshot= para realizar la misma tarea.

El paso 6 es el punto central. Un append duplicado no genera ningún error. Desde ese momento, la tabla queda incorrecta y todas las ejecuciones posteriores heredan el daño de forma silenciosa. El paso 8 es la ventaja principal: un cálculo realizado en marzo puede volver a calcularse en agosto a partir de la misma versión fijada. Esa es la propiedad que defendemos a nivel del framework en nuestro análisis sobre un backtest reproducible.

Lo que afirma el proyecto y lo que comprobamos

El README comienza con un benchmark:

más de 4.5× rápido que DuckDB y Polars en agregaciones móviles de OHLCV+VWAP sobre 20M de filas

Esa cifra corresponde a la medición del propio proyecto y está citada del README de h5i-db, consultado en agosto de 2026. No la ejecutamos y nada de lo anterior depende de ella.

Aquí, la madurez importa más que la velocidad. El repositorio tenía 29 estrellas al momento de redactar este texto y estaba en la versión 0.1.6, bajo la licencia Apache-2.0. Esa combinación implica un grupo pequeño de mantenedores y una API que todavía puede cambiar entre versiones menores. No existe un historial público extenso que muestre el funcionamiento del motor bajo carga. Fijar la versión exacta y conservar los archivos parquet que alimentaron la base de datos permite volver atrás si una versión cambia su comportamiento. Mantener una copia propia de los datos sin procesar responde al mismo principio que protege frente a un proveedor que modifique el historial a nuestras espaldas: el tema que recorre el sesgo de supervivencia en los datos bursátiles.

Preguntas frecuentes

¿Qué son los datos point-in-time?

Los datos point-in-time son un conjunto de datos almacenado con las marcas de tiempo en las que cada hecho pasó a ser conocido. Así, una consulta puede reconstruir qué información estaba disponible en cualquier fecha pasada. Una tabla simple de «último valor» no puede hacerlo, porque sobrescribe el pasado con las cifras corregidas de hoy.

¿El almacenamiento versionado elimina el sesgo de anticipación?

No. El versionado corrige una forma de filtración de información: aquella en la que una ejecución lee valores escritos después del momento de decisión simulado. La construcción de variables todavía puede filtrar información de otras maneras. Por ejemplo, al escalar una muestra con estadísticas calculadas sobre todo su historial.

¿Qué hace una clave de idempotencia durante la ingesta?

Identifica una escritura para que un reintento se reconozca como la misma operación. Así, un cargador puede volver a ejecutarse después de una interrupción sin añadir las mismas filas dos veces. Ese fallo puede dejar una tabla incorrecta sin que el problema sea evidente.

¿Está h5i-db listo para producción?

En el momento de redactar este artículo, h5i-db se encuentra en la versión 0.1.6, tiene 29 estrellas en GitHub y está bajo la licencia Apache-2.0. Un software inicial de ese tamaño puede experimentar cambios frecuentes en su API y tiene un historial público todavía limitado. Fijar la versión y conservar una copia propia de los archivos fuente permite revertir la evaluación.


Cada panel anterior incluye el SQL que lo generó. Abra uno para consultar cómo se calculó la cifra. Las mismas preguntas pueden formularse en inglés sencillo en la terminal de Strasmore.