Look-ahead bias occurs when a backtesting strategy reads data that would not have been available at the point in simulated time when the read occurs. In practice this means a strategy making a trading decision at, say, 09:00 on January 15 accidentally reading a position snapshot that was written at 11:30 on January 16 — data from the future. The result is a backtest that appears profitable under conditions that are impossible to replicate in live trading.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/theonetrade/backtest-kit-redis-postgres-pgpool-docker/llms.txt
Use this file to discover all available pages before exploring further.
How backtest-kit 9.0+ Addresses It
Starting with backtest-kit 9.0, every adapter write method accepts awhen: Date argument carrying the logical simulation timestamp at which the write occurs. For adapters that affect signal-affecting state — Risk, Partial, Breakeven, Recent, State, Session, Memory, and Interval — this timestamp is stored in the database alongside the payload. backtest-kit’s internal read filter uses the stored when value to verify that no read returns a row whose when is greater than the current simulation time.
The adapters that do not carry a when column are the ones where temporal ordering is not meaningful:
- Candle — OHLCV data is inherently ordered by
timestamp(the market data column), not by write time. - Signal, Schedule, Strategy, Storage, Notification, Log — administrative or append-only records where look-ahead bias is not applicable.
- Measure — intentionally exempt because it caches LLM/API responses, where temporal simulation ordering is not applicable.
The when Column: bigint + epochTransformer
The when column is stored in PostgreSQL as bigint (epoch milliseconds). This avoids timezone ambiguities and precision loss that can affect timestamptz columns at millisecond resolution. The pg Node.js driver returns bigint columns as strings, so a ValueTransformer is needed to keep the JS-visible value a plain number.
to function is a no-op — TypeORM passes the JS number directly to the pg driver, which serializes it as a PostgreSQL bigint literal. The from function converts the string that pg returns back into a number. This is applied transparently whenever TypeORM reads a when column.
State Schema: Full Entity Definition
StateModel is a representative example of a when-carrying entity. Its unique index is on (signalId, bucketName), and when is declared with epochTransformer:
payload column holds the domain-specific StateData object as jsonb. The when value is always stored as the millisecond epoch integer — e.g. 1737014400000 for 2026-01-16T12:00:00Z.
StateDbService: Storing when.getTime()
StateDbService.upsert converts the Date argument to milliseconds via when.getTime() before passing it to the query builder. This conversion is done inline in the values(...) call, so the atomic INSERT … ON CONFLICT … DO UPDATE … RETURNING * writes both the payload and the simulation timestamp in the same statement:
when is included in the orUpdate column list alongside payload. If a later simulation step writes a new state for the same (signalId, bucketName) context, both the payload and the simulation timestamp are updated atomically. This means the stored when always reflects the most recent logical write time.
Which Adapters Carry the when Column
Carry 'when'
Risk — active risk positions snapshotPartial — profit/loss levels per signalBreakeven — breakeven-reached flagRecent — last public signal per contextState — per-signal state bucketsSession — one session per running strategyMemory — per-signal memory store (soft-delete)Interval — once-per-interval markers (soft-delete)
Exempt from 'when'
Candle — market data ordered by
timestampSignal — live signal state, no temporal filterSchedule — pending scheduled signalStrategy — persistent strategy stateStorage — closed/opened signal logNotification — event notificationsLog — strategy log entriesMeasure — LLM response cache (intentionally exempt)Measure is intentionally exempt from look-ahead bias protection. It caches LLM or external API responses, which are keyed by content (a deterministic
(bucket, entryKey) pair), not by simulation time. Applying a when filter to an LLM response cache would cause cache misses on every backtest replay, defeating its purpose.The Interval Schema Example
IntervalModel follows the same when pattern, adding a removed: boolean column for soft-delete support:
IntervalDbService.upsert is called with a when: Date, the value is converted inline: when: when.getTime(). The orUpdate list includes ["payload", "removed", "when"], so the simulation timestamp is always updated alongside the payload on conflict.
How the Filter Works End-to-End
Strategy calls writeIntervalData(data, key, when)
The adapt layer receives the simulation timestamp as a
Date object and passes it to ioc.intervalDbService.upsert(bucket, key, data, when).Epoch milliseconds stored atomically
IntervalDbService.upsert calls when.getTime() and includes the result in the INSERT … ON CONFLICT … DO UPDATE … RETURNING * statement. Both the payload and the when bigint are committed in one round-trip.Read returns the stored when value
IntervalDbService.findByKey returns the full IIntervalRow, which includes when as a number (thanks to epochTransformer.from). backtest-kit reads this value and checks it against current_simulation_time.