h5i-db: dados point-in-time para backtests
Dados point-in-time evitam look-ahead bias no backtest. Veja como o h5i-db versiona cada gravação e o que o atraso de filings revela sobre histórico desatualizado.
Dados point-in-time registram o que um conjunto de dados continha em uma data passada. Em um backtest, eles distinguem um resultado defensável de outro que usou silenciosamente números que só estariam disponíveis no dia seguinte. O h5i-db é um banco de dados open-source de séries temporais recente, escrito em Rust e com API em Python. Ele armazena cada gravação como uma versão numerada e permite fixar qualquer leitura em uma versão anterior. A seguir, mostramos o look-ahead bias que esse mecanismo bloqueia, medido com dados reais de documentos regulatórios, e depois um cenário que pode ser executado em um laptop.
O que são dados point-in-time em um backtest?
Cada informação de mercado tem dois timestamps. O tempo do evento é quando o fato ocorreu. O tempo de chegada é quando ele se tornou conhecido por qualquer pessoa fora da empresa que o reportou. Um relatório trimestral de posições descreve os ativos detidos no último dia de um trimestre e chega ao público semanas depois. Por isso, um modelo que faz o vínculo apenas pelo tempo do evento recebe informações que ninguém tinha naquele momento.
Essa diferença pode ser medida. Gestores institucionais apresentam o Form 13F depois do encerramento de cada trimestre. O painel abaixo mede o número de dias entre o trimestre descrito pelo filing e o dia em que ele foi apresentado.
O SQL exato por trás de cada número
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_endNo trimestre encerrado em 2026-06-30, os filings de 10688 chegaram, em média, 34.1 dias depois do período a que se referiam. O painel repete essa medição ao longo de 16 trimestres. Um gestor também pode alterar um relatório muito tempo depois de apresentá-lo. Assim, o registro que descreve uma data passada continua mudando mesmo depois de essa data ter passado.
O look-ahead bias é um problema de armazenamento
Nosso guia sobre look-ahead bias em backtests trata o vazamento de dados como uma questão de disciplina: aplicar defasagem a cada variável e respeitar as datas de publicação. A disciplina funciona até alguém se esquecer dela, e a falha passa despercebida. Um backtest com vazamento apresenta um índice de Sharpe melhor, sem gerar qualquer erro.
O armazenamento point-in-time leva essa garantia para uma camada inferior. Quando o quadro entregue a uma estratégia vem de uma leitura fixada em uma versão, uma linha gravada depois dessa versão não pode aparecer nele, independentemente do que o código da estratégia faça em seguida. A verificação deixa de ser uma revisão de código e passa a ser uma propriedade da leitura.
Os dividendos mostram a defasagem temporal pelo outro lado. Primeiro, o dividendo em dinheiro é declarado; depois, ocorre a data ex-dividendo. Uma tabela carregada hoje traz as duas datas de cada pagamento, inclusive daqueles que ainda não haviam sido anunciados na data simulada.
O SQL exato por trás de cada número
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 monthNo mês iniciado em 2026-07-01, as declarações ocorreram, em média, 88.2 dias antes da data ex-dividendo, e o painel abrange 24 meses da mesma medição. Ao ler uma tabela moderna de dividendos em uma data simulada dentro desse intervalo, o pagamento já estará registrado, semanas antes de o anúncio existir.
Histórico armazenado é reescrito
A chegada tardia é um modo de falha. A revisão é o outro. Eventos societários reescrevem preços que já foram registrados: após um desdobramento de 4 por 1, cada preço anterior em uma série ajustada é dividido por quatro, e a série baixada hoje deixa de corresponder ao tape que um trader acompanhou. Nossa nota sobre histórico de preços ajustado por desdobramentos detalha os cálculos. O que importa aqui é a frequência.
O SQL exato por trás de cada número
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_dateNo trimestre iniciado em 2026-04-01, entraram em vigor 131 desdobramentos e 303 grupamentos. Cada um reescreve um histórico de preços que um pipeline de pesquisa pode já ter armazenado em cache. O armazenamento com controle de versões não impede a revisão. Ele registra o novo estado como uma nova versão e mantém o anterior legível. É isso que transforma um resultado desatualizado em um resultado reproduzível.
Os timestamps das notícias apresentam a mesma armadilha em escala menor.
O SQL exato por trás de cada número
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_hourAs manchetes são publicadas continuamente, ao longo das 24 horas do dia em Nova York. A hora das 09:00 concentrou 790 artigos nos 90 dias anteriores, enquanto a hora das 20:00 concentrou 404. Atribuir uma manchete do período noturno ao fechamento das 16:00 daquele dia dá a uma estratégia que negocia no fechamento várias horas de informação privilegiada pelo futuro.
Um cenário pontual que pode ser executado
Tudo aqui se limita ao pacote Python. O projeto também distribui uma ferramenta de linha de comando em Rust, instalada separadamente, mas ela não é necessária neste passo a passo. Os dados de exemplo são gerados localmente, sem download.
- Instale a versão fixada:
pip install 'h5i-db==0.1.6', publicada em 4 de agosto de 2026. Ela requer Python 3.9 ou posterior e incluipyarrow>=14. Há wheels pré-compiladas para Linux em x86-64 e arm64, além de macOS com Apple silicon e Windows em x86-64. - Descreva os dados uma única vez, depois de
import pyarrow as paeimport pyarrow.parquet as pq:schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]). - Escreva duas linhas inventadas em um arquivo local com
pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet'), em qued1ed2são datetimes cientes do fuso horário. - Crie o banco de dados e a tabela, nomeando a coluna de tempo:
db = h5i_db.Database('pit.db', create=True)e depoisdb.create_table('prices', schema, time_column='ts'). - Faça a ingestão do arquivo sob uma chave:
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). A chamada retorna o commit realizado. - Execute exatamente essa linha novamente. O projeto documenta que uma repetição com a mesma chave encontra o commit que já produziu e o retorna com
"segments_added": 0, em vez de gravar as linhas uma segunda vez. Imprimadb.versions('prices')antes e depois do retry e observe a lista de versões permanecer inalterada. - Faça a ingestão de um segundo dia usando
idempotency_key='load-day2'e consulte os dois dias:db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas(). - Leia a tabela como ela estava antes da entrada do segundo dia:
db.read('prices', version=1). O mesmo método aceita os argumentosas_of=esnapshot=para a mesma finalidade.
O passo 6 merece atenção. Um append duplicado não gera erro. A partir desse momento, ele deixa a tabela incorreta, e todas as execuções seguintes herdam o problema silenciosamente. O passo 8 é o principal benefício: um número calculado em março pode ser recalculado em agosto a partir da mesma versão fixada. Essa é a propriedade defendida pelo nosso texto sobre um backtest reproduzível no nível do framework.
O que o projeto afirma e o que verificámos
O README começa com um benchmark:
mais de 4,5 vezes mais rápido que DuckDB e Polars em agregações OHLCV+VWAP sobre 20 milhões de linhas
Esse número é uma medição do próprio projeto, citada no README do h5i-db, conforme consultado em agosto de 2026. Não executámos o benchmark, e nada do que foi dito acima depende dele.
Aqui, a maturidade é mais importante que a velocidade. O repositório tinha 29 estrelas no momento da redação e estava na versão 0.1.6, sob a licença Apache-2.0. Essa combinação indica uma equipa pequena de manutenção e uma API que ainda pode mudar entre versões de correção. Não existe um histórico público longo que mostre o motor a funcionar sob carga. Fixar a versão exata e manter os ficheiros parquet que alimentaram a base de dados cria uma forma de recuar se uma versão alterar o comportamento. Manter a sua própria cópia dos dados brutos é o mesmo hábito que protege contra um fornecedor reescrever o histórico sem o seu conhecimento, tema presente em viés de sobrevivência nos dados de ações.
Perguntas frequentes
O que são dados point-in-time?
Dados point-in-time são armazenados com os timestamps em que cada fato se tornou conhecido. Assim, uma consulta pode reconstruir o que estava disponível em qualquer data passada. Uma tabela simples com o “último valor” não permite isso, pois substitui os dados históricos pelos números corrigidos de hoje.
O armazenamento versionado elimina o viés de look-ahead?
Não. O versionamento corrige uma forma de vazamento: aquela em que uma execução lê valores gravados depois do momento da decisão simulada. A construção de features ainda pode vazar dados de outras formas. Um exemplo é dimensionar uma amostra usando estatísticas calculadas sobre todo o seu histórico.
O que uma chave de idempotência faz durante a ingestão?
Ela identifica uma gravação para que uma nova tentativa seja reconhecida como a mesma gravação. Assim, o carregador pode ser executado novamente após uma falha sem inserir as mesmas linhas duas vezes. Essa duplicação é o tipo de falha que deixa uma tabela incorreta sem qualquer aviso.
O h5i-db está pronto para produção?
No momento da redação, ele estava na versão 0.1.6, com 29 estrelas no GitHub e sob a licença Apache-2.0. Um software inicial desse porte pode sofrer mudanças frequentes na API e ainda tem um histórico público limitado. Fixar a versão e manter uma cópia própria dos arquivos-fonte são medidas que tornam a avaliação reversível.
Cada painel acima vem acompanhado do SQL que o produziu. Abra um deles para ver como o número foi calculado. As mesmas perguntas podem ser feitas em inglês simples no terminal Strasmore.