15 min read ·
How to Keep the Historical Clock From Cheating Your Backtest
Use strict-prior inputs when equal-time order is unknown; audit candle labels, backward joins, execution timing, settlement leakage, and fills.

If you cannot establish the order of records sharing a timestamp, require inputs to be strictly earlier than the decision.
That rule catches many prediction market backtest timestamp errors, but not all of them. A chronologically sorted dataset can still contain later revisions, future settlement data, unsafe joins, synthetic records, or prices that were never executable.
Start with the causal rule: no input may arrive after the decision
A timestamp is not simply “the time.” It can describe when an interval ended, when an exchange emitted an event, when a collector received it, when a value became available to the strategy, or when an order could first interact with the market.
At minimum, distinguish these clocks:
- Candle period end: when the interval summarized by a bar finishes.
- Exchange event time: when the venue associates an event with the market stream.
- Collector capture time: when an external system receives or stores that event.
- Availability time: when the strategy could actually observe the record.
- Decision time: when the strategy freezes its inputs and chooses an action.
- Order-activation time: decision time plus modeled processing and transmission latency.
- Fill observation time: the recorded market state used to determine a simulated fill.
- Settlement time: when the final outcome or payoff becomes available.
Write the governing invariant as code, not commentary:
for every input used by decision d:
assert input.availability_time <= d.decision_time
If equal-time ordering is unknown:
assert input.availability_time < d.decision_time
The strict-prior policy is conservative. It may exclude legitimately observable records if the exchange supplies a reliable sequence proving that they arrived before the decision. Conversely, allowing every equal-time record is unsafe when no ordering evidence exists.
A practical audit has five layers:
- Ingestion boundaries: Did range semantics, timestamp units, or pagination duplicate or omit records?
- Event ordering: Are events replayed in their real causal order, especially when timestamps tie?
- Feature availability: Were only historically published, unrevised inputs used?
- Execution timing: Could the order have reached the quoted market state?
- Settlement joining: Were outcomes isolated until trading had ended?
Start with a short diagnostic:
- Decode several raw timestamps into human-readable dates.
- Confirm which timestamp field is being used and what it represents.
- Verify the unit instead of inferring it from the integer’s data type.
- Sort by validated event time and authoritative sequence, where available.
- Inspect records sharing identical timestamps.
- Search joins for observations later than their associated decisions.
- Lag every signal by one observation and compare the changed decisions.
- Trace several fills from raw feature records through decision, activation, book selection, and execution.
Chronological processing is necessary but insufficient. A row dated noon may contain a revision loaded hours later. A market row may already include its final outcome. A candle close is not known before the interval ends. A quote may be stale, too small for the simulated order, or no longer tradable when the order arrives.
Automated look-ahead checks can help. Freqtrade’s tool, for example, compares a full backtest with sliced runs to find indicators, entries, or exits that change when future rows are unavailable. Its documentation also identifies potential false positives and false negatives, so a clean result is diagnostic evidence rather than proof of temporal correctness (Freqtrade lookahead-analysis documentation).
Worked example: Kalshi candles are selected by their ending timestamps
Suppose a one-minute candle has an end_period_ts corresponding to 14:01. That candle summarizes a period completed at 14:01. It is not information available at 14:00.
The invalid replay looks like this:
14:00 Strategy receives candle labeled 14:01
14:00 Strategy calculates signal from completed OHLC values
14:00 Backtest assigns a fill
14:01 Candle actually finishes
This exposes the following minute’s completed information before the minute has finished. The error can affect both the signal and the assumed fill: the model may use the bar’s close, high, or low and then pretend to trade at an earlier price from the same interval.
The minimum valid timeline is:
14:01 Candle period completes
14:01 or later Candle becomes observable
after that Strategy calculates its signal
after latency Order becomes active
then First eligible market state may produce a fill
A signal calculated from the completed candle cannot receive a 14:00 fill. Even a fill stamped 14:01 requires an execution model establishing when the candle became observable, when the decision completed, and which market state followed order activation.
Kalshi documents start_ts and end_ts as required Unix timestamps and selects candles according to their ending timestamps: at or after start_ts and at or before end_ts. The endpoint identifies each candle with end_period_ts, separates yes_bid, yes_ask, and trade-price fields, and supports intervals of 1, 60, and 1440 minutes (Kalshi market-candlestick documentation).
Because both boundaries are inclusive, adjacent overlapping requests can duplicate a candle:
Request A: [14:00, 15:00] -> includes candle ending 15:00
Request B: [15:00, 16:00] -> includes candle ending 15:00
Do not solve this by dropping an arbitrary first or last row. Use a stable identity and convert the external inclusive convention into an internal nonoverlapping convention:
for each requested range:
response = fetch_inclusive(start_ts, end_ts)
for candle in response:
validate_timestamp_unit(candle.end_period_ts)
key = (
candle.market_ticker,
candle.period_interval,
candle.end_period_ts
)
upsert_by_key(key, candle)
sort by market_ticker, period_interval, end_period_ts
assert monotonic_end_times_within_each_series()
publish internal slices using [start, end)
The documentation calls the fields Unix timestamps but does not explicitly establish, in the cited material, whether a particular request value should be interpreted as seconds or milliseconds. Decode sample values and confirm that they fall within the requested market’s active period. Reject implausible dates instead of allowing a library to interpret the wrong magnitude silently.
There is another ingestion trap. With include_latest_before_start=true, Kalshi may prepend a synthetic context candle projected to the first period boundary after the requested start. Its OHLC values are null, while previous_price is carried from the most recent real candle before the range. Mark this record synthetic=true; it may provide context where appropriate, but it must not trigger a signal, order, or fill (Kalshi market-candlestick documentation).
Build one canonical timeline without pretending every clock is equivalent
Normalization should preserve information, not replace it. Keep the original field and unit alongside every converted value.
A useful canonical event schema is:
| Field | Purpose |
|---|---|
market_id |
Stable venue and contract identity |
record_type |
Trade, quote, book update, candle, lifecycle event, or resolution |
raw_timestamp / raw_unit |
Exact source value and interpreted unit |
normalized_utc_time |
Common representation used for comparison |
event_time |
Time assigned to the event by the venue or source |
capture_time |
Time the collector received or stored it |
availability_time |
Earliest time the strategy may use it |
sequence_id |
Authoritative ordering key, if available |
revision_status |
Original, corrected, superseded, or final |
source_endpoint |
Route or file that supplied the record |
synthetic_row |
Whether the source constructed the record |
UTC normalization makes comparisons easier, but it cannot repair a timestamp that was mislabeled as UTC, converted from the wrong local zone, shifted incorrectly for daylight saving, delayed before publication, or produced by a bad clock. A polished ISO 8601 string can still encode the wrong instant.
For market replay, prefer a validated exchange event timestamp over collector capture time or database row order. Capture time includes network and processing delay; row order may reflect batching or storage behavior. But exchange event time still does not prove immediate public availability, accurate clock synchronization, or correct delivery order.
When several events have the same timestamp, use an exchange sequence field only if its documented semantics preserve event order. PredictionMarketBench provides one implementation example: its replay design uses a unified UTC timeline and sequence numbers to resolve timestamp ties (PredictionMarketBench preprint).
Without an authoritative sequence, define a deterministic fallback—such as source-file order followed by record type and stable ID—so reruns remain reproducible. Do not describe that fallback as the true historical order. Mark the tie unresolved, and if alternate plausible orderings change a fill, report the resulting range of outcomes.
Precision is not accuracy. A millisecond timestamp can still come from an unsynchronized clock, arrive late, or tie with several causally ordered events.
Use backward as-of joins and explicit availability times
An unrestricted nearest-row join can select the future:
Decision: 12:00:00
Earlier snapshot: 11:59:50
Later snapshot: 12:00:02
The later snapshot is numerically closer, but it did not exist at the decision time. The safe match is the latest eligible record at or before the decision—or strictly before it when equal-time order is unproven.
sort decisions by decision_time
sort observations by availability_time
matches = asof_join(
left=decisions,
right=observations,
left_on=decision_time,
right_on=availability_time,
direction="backward",
allow_exact_matches=EQUAL_TIME_POLICY,
tolerance=MARKET_SPECIFIC_STALENESS_LIMIT
)
for match in matches:
if match exists:
assert match.availability_time <= match.decision_time
allow_exact_matches must reflect a documented policy. If the matching engine or feed supplies sequence data proving that the observation preceded the decision, equality can be permissible. Otherwise, strict-prior matching is safer.
The finite tolerance is equally important. Without it, a backward join will keep selecting progressively older observations.
External data needs at least two clocks as well. A match played on Saturday, a poll conducted on Monday, an economic period ending in June, or a weather observation nominally assigned to 10:00 does not prove that its value was published at that nominal time. Join on historical publication availability and preserve revision versions where applicable.
Completed-bar features should normally be lagged. Same-bar use is acceptable only if the system can demonstrate that the required data were observed and processed before the modeled order reached its execution state.
Missing data should remain missing. Do not:
- turn missing expected-goals, weather, or polling values into measured zero;
- backfill a gap using a later observation;
- carry a quote indefinitely without a staleness limit;
- merge duplicate and revised records without retaining their status.
Maintain separate labels and counts for missing, measured zero, duplicate, stale, synthetic, revised, and out-of-order observations. Those categories imply different remedies and should not collapse into a generic null-handling step.
Timestamp correctness does not make a candle price executable
Fixing the clock determines what the strategy could have known. It does not determine what price or quantity the strategy could have traded.
Kalshi’s candle response separates yes_bid, yes_ask, and trade-price statistics. Those fields are not interchangeable, and aggregated OHLC values do not reveal the within-period sequence of quote changes and trades. A candle’s high and low may have occurred in either order; displayed bid or ask statistics do not establish that sufficient liquidity remained when the simulated order arrived.
Use an explicit execution timeline:
decision time
+ modeled computation/network latency
= order-activation time
-> first eligible recorded book at or after activation
-> side-appropriate price
-> depth consumed level by level
-> full, partial, or failed fill
For a buy, consume available asks. For a sale, consume available bids. A midpoint may be useful for marking a position, but it is not a default executable price. Historical-book simulations should process snapshots chronologically, avoid consulting the next snapshot for the current decision, and constrain fills to the recorded side and depth (PolyTest backtesting guide).
Where historical order books exist, walk displayed depth:
remaining = requested_quantity
for level in eligible_book.side:
filled_here = min(remaining, level.displayed_quantity)
record_fill(level.price, filled_here)
remaining -= filled_here
if remaining > 0:
record_unfilled_remainder(remaining)
Even that remains a model. Displayed depth can be canceled, other orders may have queue priority, liquidity can change between snapshots, and the simulated order itself may affect the market. Event-driven benchmark designs can model maker/taker behavior and queueing, but the quality of the result still depends on the underlying event stream and ordering data (PredictionMarketBench preprint). State explicitly whether queue position, cancellations, market impact, and between-snapshot changes are omitted.
Keep execution adjustments separate:
- spread crossing;
- venue fees;
- slippage;
- decision and transmission latency;
- available depth;
- partial fills;
- failed entries and exits.
Otherwise, a single before-and-after performance comparison may wrongly attribute every change to timestamp correction.
An anonymous Reddit post titled “Football market data is messier than I expected,” observed on September 6, 2026, mentions mismatched team names, missing expected-goals values, stale quotes, and backtests affected by fees, spreads, depth, and failed exits. It supplies no underlying dataset, methodology, or measured prevalence, so it is an anecdotal warning about possible failure modes—not evidence of industry-wide rates (Reddit anecdote).
The operative rule is narrow: a candle or displayed quote is evidence of a recorded market state, not proof of execution at the assumed price and size.
Keep settlement data and Kalshi retrieval cutoffs out of the trading clock
Resolution data can leak the answer even when every row appears chronologically sorted. Common routes include:
- joining the final outcome onto every market row before generating features;
- retaining settlement timestamps or final prices in model inputs;
- filling earlier missing fields with values taken from a final market record;
- allowing a model to query a currently resolved market page during historical replay.
Store distinct record types for trades, order-book states, lifecycle events, and resolutions. Settlement information should become accessible only after the tradable price window closes, and then only for payoff accounting or evaluation—not for earlier decisions. The same isolation principle applies to final prices, later headlines, and any other information unavailable at the historical decision time (DepthFeed backtesting guide).
Retrieval routing is a separate source of missing or duplicated history. Kalshi publishes different historical cutoff fields for settled markets, fills, orders, and positions:
market_settled_tsgoverns older settled markets and candlesticks;trades_created_tsgoverns older fills;orders_updated_tsgoverns older canceled or fully executed orders;market_positions_last_updated_tsgoverns archived settled positions.
These are not one interchangeable “historical cutoff.” Kalshi directs affected record types to their corresponding historical routes, while active resting orders remain available through the portfolio orders route (Kalshi historical-cutoff documentation).
When combining live and historical endpoints, reconcile the seam:
- Compare record counts around each record type’s boundary.
- Compare stable IDs for overlap.
- Record minimum and maximum timestamps from both routes.
- Deduplicate exact repeated records.
- Investigate unexplained temporal gaps.
- Verify that endpoint selection follows the record’s applicable cutoff field.
Applying the market-settlement cutoff to fills, orders, and positions can send requests to the wrong route or produce an incomplete historical tape.
Turn timestamp assumptions into executable tests
A defensible backtest should fail loudly on adversarial fixtures.
| Failure mode | Fixture | Expected assertion | Likely consequence |
|---|---|---|---|
| Wrong magnitude or date | Timestamp decodes outside market era | Reject input | Entire feed shifted |
| End label treated as start | 14:01 candle exposed at 14:00 | Block availability | One-bar look-ahead |
| Inclusive duplicate | Adjacent ranges contain the 15:00 candle | Keep one stable-key record | Double-counted signal or volume |
| Nonmonotonic rows | Later row precedes earlier event | Sort or reject and count | Incorrect state replay |
| Same time, distinct sequences | Tied events have ordered IDs | Follow validated sequence | Wrong depth or priority |
| Missing sequence | Material tied events lack order | Mark unresolved; test alternatives | Ambiguous fill |
| Future nearest match | 12:00 decision matches 12:00:02 | Reject future match | External-data leakage |
| Stale book | Last book exceeds tolerance | Reject fill explicitly | Unrealistic execution |
| Synthetic context candle | Null OHLC with carried price | Cannot trigger trade | Fabricated signal |
| Early settlement exposure | Outcome appears before trading ends | Deny feature access | Direct answer leakage |
Encode the core invariants:
decoded_time is within expected market era
normalized_time is monotonic after valid sequencing
input_availability_time <= decision_time
backward_join_time <= decision_time
execution_book_time >= order_activation_time
synthetic_row cannot trigger signal or fill
settlement_fields are inaccessible during trading
Then run policy sensitivity tests:
- Strict-prior replay: equal-time records are ineligible.
- Inclusive replay: equal-time records are eligible only under a documented ordering rule.
- Alternate tie orderings: unresolved equal-time events are replayed in plausible sequences.
- One-observation delay: every strategy signal is shifted once.
- Full versus sliced runs: indicators and trades are recomputed after future rows are removed.
- Bar versus event replay: where event data exist, compare results to expose within-bar ambiguity.
Report how many signals, entries, exits, and fills change. Do not select the policy that happens to produce the strongest return.
Sliced reruns may flag harmless differences caused by order mechanics, and they may miss leakage in signals that never trigger. A one-step delay can expose suspicious dependence on the current bar without identifying every defect. Event replay can reveal ambiguity hidden by candles, but its numerical effect must be measured rather than assumed.
Finally, manually trace a sample of trades:
raw feature records
-> normalized and availability timestamps
-> feature calculation
-> decision timestamp
-> modeled latency
-> order activation
-> selected historical book
-> bid/ask and depth consumed
-> fees and slippage
-> partial or failed quantity
-> settlement payoff
A trade that cannot survive this trace should not appear as an unexplained dot on an equity curve.
Publish an audit trail, not just a performance chart
Reproducibility requires enough information to reconstruct every decision against the exact historical tape.
Save run-level metadata:
- requested start and end times;
- timezone and daylight-saving policy;
- equal-time eligibility policy;
- staleness tolerance by feed or market;
- latency model;
- fee schedule;
- random seed;
- configuration hash;
- code commit;
- model version;
- run timestamp.
Save record- and action-level metadata:
- raw timestamp and unit;
- normalized event, capture, and availability times;
- source endpoint;
- sequence ID;
- revision and synthetic flags;
- decision and order-activation times;
- selected book and fill times;
- side, price, and quantity;
- spread, fees, slippage, and latency;
- unfilled quantity;
- rejection reason.
Hash or version the exact feature tapes, lifecycle streams, and order-book files. Otherwise, a later vendor correction or ingestion rerun can silently change a supposedly reproducible result.
Retain observations that performance summaries tend to hide: passes, rejected actions, partial fills, failed exits, missing-market episodes, delisted contracts, and markets that closed early. Their absence can create a cleaner story than the historical record supports.
A compact audit report can look like this:
Run identity
- configuration hash:
- code commit:
- input-tape hash:
- order-book hash:
Timestamp policy
- timezone:
- equal-time rule:
- staleness tolerance:
- latency model:
Integrity counts
- duplicate records removed:
- nonmonotonic records:
- stale matches rejected:
- synthetic candles excluded:
- future joins blocked:
- unresolved timestamp ties:
- missing observations:
- endpoint-routing gaps:
Sensitivity
- signals changed under strict-prior policy:
- fills changed under alternate tie ordering:
- trades changed after one-observation delay:
- differences between full and sliced reruns:
- differences between bar and event replay:
Execution
- partial fills:
- failed entries:
- failed exits:
- total unfilled quantity:
A credible prediction-market backtest makes its historical clock inspectable. Define what every timestamp means, convert inclusive API responses into nonoverlapping internal data, preserve causal order where possible, block future joins, isolate settlement records, and connect each decision to the first eligible executable market state. When timing or ordering remains unknowable, label the uncertainty and test plausible alternatives instead of silently choosing the favorable interpretation.
Present timestamp-policy sensitivity and execution-model sensitivity separately from strategy performance. This procedure evaluates measurement integrity; consistent with Yes No Odds’ informational scope, it is not trading advice.