The canonical timeframe vocabulary: a string label, its width in seconds, and the boundary every real candle of that width sits on.
Why alignment is part of the vocabulary
A candle is not just "OHLC at a time" — it is OHLC for a specific bucket. A
1d bar means midnight-to-midnight UTC; a 4h bar means one of the six fixed
4-hour windows in a day. Every real source honours that: venue candle
endpoints return bucket-start timestamps, and any honest aggregation over them
rolls up on bucket boundaries.
That makes alignment a cheap, total test of authenticity, and it caught a
live data-integrity bug on 2026-08-06. The Gemini adapter, when its historical
path returned too little data, synthesised candles at now - i * granularity
— arbitrary sub-second timestamps carrying prices from a hardcoded table. The
poison is visible at a glance once you know to look:
2026-08-06T00:00:00Z close 64698.60 <- real
2026-08-04T16:01:33.654710Z close 42912.10 <- fabricated (42_500 base)Both were tagged timeframe: "1d", both fed backtests and the shadow gate.
Nothing downstream could tell them apart, because nothing downstream checked
the one property a fabricated bar cannot fake without also being right.
So this module ships aligned?/2, and a consumer storing candles should enforce
it on the write path — a misaligned candle rejected rather than stored — and filter
on the read path, so rows written before the guard existed never reach a backtest.
Core cannot enforce that itself: it owns no storage. It owns the test.
Weekly and monthly are deliberately absent from boundary/2
A 1w bar's boundary depends on which weekday the venue starts its week, and
1M is not a fixed number of seconds at all. Rather than encode a guess that
would reject real data, both are absent, and callers treat "no boundary rule"
as "cannot check" rather than "invalid".
Summary
Functions
Whether datetime sits exactly on a timeframe bucket boundary.
The start of the bucket datetime falls in, or the input when unknown.
Every timeframe with a known width, shortest first.
Width of a timeframe in seconds, or :error for one we do not model.
Functions
@spec aligned?(DateTime.t(), String.t()) :: boolean()
Whether datetime sits exactly on a timeframe bucket boundary.
Unknown timeframes return true — "we have no rule" must not read as
"invalid", or a venue serving a width we do not model would have all of its
real data rejected.
@spec boundary(DateTime.t(), String.t()) :: DateTime.t()
The start of the bucket datetime falls in, or the input when unknown.
@spec known() :: [String.t()]
Every timeframe with a known width, shortest first.
@spec seconds(String.t()) :: {:ok, pos_integer()} | :error
Width of a timeframe in seconds, or :error for one we do not model.
:error rather than a default: a wrong width silently mis-buckets every
candle it touches, which is exactly the class of failure a fallback hides.