Look-ahead bias is a common backtesting defect where a strategy inadvertently uses information that would not have been available at the simulated point in time — for example, reading tomorrow’s closing price when evaluating today’s entry signal. Even a single data point leaking across the time boundary can produce unrealistically optimistic results that collapse entirely in live trading.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-redis-mongo-docker/llms.txt
Use this file to discover all available pages before exploring further.
backtest-kit 9.0+ added systematic protection against this defect: every adapter write* method that stores signal-affecting state now accepts a when: Date argument representing the simulation timestamp at which the data was produced. The persistence layer stores when as an indexed numeric column (when: number, milliseconds since epoch) so the bias filter can later verify that any read only observes records whose when is ≤ the current simulation time.
Which Adapters Carry when
Not every adapter needs a when column. The rule is:
- Signal-affecting state — data that can influence whether, when, or how a position is opened or closed — must carry
when. - Infrastructure or LLM-response caches — data that is either time-independent or deliberately computed outside the simulation timeline — are exempt.
| Adapter | Schema field | Notes |
|---|---|---|
| Risk | when: Number | Position exposure per exchange, time-dependent |
| Partial | when: Number | Per-signal partial fill state, time-dependent |
| Breakeven | when: Number | Per-signal breakeven price, time-dependent |
| Recent | when: Number | Most-recent public signal row, time-dependent |
| State | when: Number | Arbitrary per-signal state bucket, time-dependent |
| Session | when: Number | Per-frame session data, time-dependent |
| Memory | when: Number | Per-signal memory entries, time-dependent |
| Interval | when: Number | Named interval trackers, time-dependent |
| Signal | (absent) | Identity record; bias is tracked via payload.when inside backtest-kit |
| Schedule | (absent) | Scheduling metadata; not directly signal-affecting |
| Measure | (absent) | LLM / API response cache; intentionally exempt (see below) |
| Candle | (absent) | Raw market data; immutable, bias is enforced by timestamp filter |
| Storage | (absent) | Global completed-signal archive; read-only in bias context |
| Notification | (absent) | UI notification log; not signal-affecting |
| Log | (absent) | Debug log entries; not signal-affecting |
Schema Example — StateSchema
StateSchema is representative of all time-tracked schemas. The when field is Number (ms since epoch), required, and indexed so the bias filter can issue range queries efficiently:
(signalId, bucketName) ensures at-most-one active state per signal/bucket pair, while the individual index: true on when enables range queries across the timeline.
The same structure appears in every time-tracked schema:
How when Is Stored
StateDbService.upsert converts the Date object to an integer before persisting it, matching the Number schema type:
Date.getTime()) is portable, sortable, and directly comparable — a range query { when: { $lte: simulationTime } } works without any date-parsing overhead.
Why Measure Is Intentionally Exempt
MeasureDbService.upsert accepts a _when: Date parameter (note the leading underscore) in its adapter interface but does not persist it to MongoDB:
Measure is designed as a deterministic response cache for expensive LLM calls or external API lookups. The same key always returns the same data regardless of simulation time — the response is not a function of the market state at a particular timestamp. Storing when for Measure would incorrectly imply that the data can go stale as the simulation clock advances, which is semantically wrong for this adapter. MeasureSchema therefore omits the when field entirely.