Skip to content

Concepts

Temporal safety

A backtest is only honest if every row was knowable when the decision was made. The SDK enforces that in one place per surface: mr.align guards composed joins, and the temporal toolkit audits any frame that carries a knowledge_time.

from mostlyright.core import KnowledgeView, TimePoint
from mostlyright.core.temporal import LeakageDetector, assert_no_leakage
  • TimePoint: a UTC-aware timestamp wrapper (DST-proof, nanosecond-truncation aware). Cutoffs are TimePoints, not strings; passing a raw string raises a TypeError rather than guessing a timezone.
  • KnowledgeView(df, as_of): the silent filter. Construction validates the frame (it requires a knowledge_time column; SchemaValidationError otherwise), and .dataframe() returns a defensive copy of the rows where knowledge_time <= as_of.
  • assert_no_leakage / LeakageDetector: the loud audit. Same question, but raising with the offending rows named instead of filtering them out.

Executed example on real econ data:

from datetime import date
from mostlyright import econ
from mostlyright.core import KnowledgeView, TimePoint
s = econ.series("cpi", date(2024, 1, 1), date(2025, 6, 1))
view = KnowledgeView(s, TimePoint("2025-04-01T00:00:00Z"))
len(s), len(view.dataframe())
# (5, 3): the March and April vintages had not been published by the cutoff

Frames built by weather.pairs() / mr.align do not carry knowledge_time per row. They carry the spine’s decision_time and label_available_time, because the guard already ran: align enforces source.knowledge_time <= spine.decision_time for every source at join time and raises a typed mr.LeakageError (naming the source id, the offending rows, both timestamps, and a fix) when violated.

So the division of labor is:

FrameTemporal columnsGuard
Source frames (econ vintages, forecast runs)knowledge_timeKnowledgeView / assert_no_leakage
Composed frames (pairs, align output)decision_time, label_available_timeEnforced inside align

Not when a thing happened; when it became knowable. An observation’s knowledge time tracks its report. A forecast’s is its issuance. An econ number’s is its vintage date, and revisions are new rows with new knowledge times, which is why econ vintages never leak. Two timestamps for the same fact is the entire trick: event_time says what period a row describes, knowledge_time says when you could have known it.

A leakage-safe frame does not make an evaluation honest by itself. The pattern that does:

  1. Split by time, never randomly. Train on a window, score strictly after it, roll forward.
  2. Recompute features inside each fold. A rolling mean computed once over the full history leaks the future into early folds even though every row is individually legitimate.
  3. Audit the fold, not the vibe. Run assert_no_leakage on knowledge-time-bearing sources at each fold’s cutoff; for composed frames, the decision_time column is the fold boundary to respect.
  4. Group when you generalize. Cross-station claims need station-grouped folds; the quickstart’s training-table section shows the trap and the fix with real numbers.

Rolling and derived features remain deliberately user-land (pandas/polars own them; the SDK’s transforms helpers exited with 1.18, see the upgrade guide): the SDK guards joins and audits knowledge, and refuses to pretend a DSL could guard your feature engineering for you.