vike
Backtesting

Staleness

The opt-in stale-price wait discipline — a market order that would fill against a forward-filled or otherwise stale price waits for a fresh print instead, with three rules, an event-time clock, and a counter that says when it bit.

A repeated last price is not a tradeable price. crates/vike-fills/src/staleness.rs opens with the case: on sparse feeds — Polymarket especially, where an outcome token can go minutes without a trade — a resampled series emits flat, forward-filled bars, and a market order that fills against one manufactures edge that never existed. The discipline this module supplies makes such an order wait for a fresh print.

It is enabled only by EngineParams::max_price_staleness_ms. None, the default, means the module is never consulted and every fill lane is byte-identical to the pre-feature engine; the header lists the parity and golden suites that all run with None. A run profile's [engine] table carries no key for it — the knob is set on EngineParams by a caller building the engine directly.

The three rules

The header states them once, and each is one function.

  1. What counts as a fresh print — is_fresh_print. An event carrying genuine current price evidence: volume > 0.0 (a real transaction happened in this event or period) or a live two-sided quote (bid and ask both present — resting liquidity you can actually cross). A zero-volume, quote-less bar is precisely the fill-forward shape, and is the thing this discipline exists to refuse. A one-sided quote is not crossable and is not a print.
  2. Which orders defer — defers. Market and MarketClose only. Those resolve at "whatever the current price is", so a stale price silently becomes their fill price. Limit, LimitClose, Stop, Trailing and the protective-stop lane are price-conditional: a repeated stale price cannot spuriously satisfy a condition it did not already satisfy, and gating a protective stop would strand live risk. They pass through untouched.
  3. When a price is stale — is_stale. The age of the symbol's last fresh print, measured against the event clock, strictly exceeds the bound. Age 0 — this very event is a print — therefore always fills. A symbol that has not printed at all yet is stale by definition: there is no evidence of a tradeable price, so its market orders keep waiting.

Because the comparison is strict, Some(0) is the tightest useful setting — fill only on an event that is itself a fresh print. A negative bound is clamped to 0 rather than taken literally: read literally, age > −1 would make even age 0 stale and nothing could ever fill, a silent total freeze from a plausible typo. A negative age — an out-of-order event stamped before the last print — is never stale.

What a deferral is

A deferred order is not cancelled and not rejected, and it is never lost. It stays in pending exactly as it was and is retried on the very next event for that symbol, so it fills on the next fresh print, at that print's price. The only way a deferred order stops resting is the way any resting order does: the strategy cancels it, or an opt-in resolution or expiry lane retires it.

SimBroker::stale_deferrals counts how often a deferral happened, and it is mirrored onto BacktestResult::stale_deferrals by both run and run_ticks, so callers that drop the engine — the harness and the backtest bin — can read it too. The result carries it for one reason: "zero trades because the tape never printed" must stay distinguishable from "zero trades because the strategy never traded". The zero-trade analyzer turns a non-zero count into a named cause, stale-price, whose detail says the tape never printed fresh data within the bound and may be forward-filled or zero-volume across the window.

A strategy can also ask before it sends. SimBroker::price_is_stale(symbol) is the read-only surface — a maker can check it before sending a taker order it knows would be deferred. It answers false when the discipline is off and on an unknown symbol, and on the bar path it measures against that symbol's own current bar timestamp rather than the engine clock, so that on a ragged multi-symbol tape the predicate agrees with the engine's actual deferral decision.

The clock is event time

Every age is event_ts − last_print_ts, both drawn from the tape. Nothing reads a system clock, so a replay of the same tape yields the same deferrals on any machine at any speed — the determinism the golden and parity suites depend on.

Which tier it actually bites

The target is the bar tier

Forward-filled and no-trade bars are real on StrategyEngine::run, including the cash-gated lane: a resampled sparse series emits flat zero-volume bars, and without this knob a market order fills against them. Freshness is recorded once per symbol per step from that step's coarse bar, immediately before the fill phase, so a bar that is itself a print has age 0 and fills on the spot.

On the granular sub-bar lane each sub-bar is a first-class print: it is recorded before its own fill pass and the age is measured at the sub-bar's timestamp, because recording only the coarse bar while measuring at sub-bar time would make every later sub-bar of a fully fresh step spuriously stale. A fully printing granular step never defers; a quiet stretch within a step does, which is the point of the finer tier.

The tick tier is inert by construction, and the header says that is correct: each fill event there is the symbol's own just-arrived tick, recorded as a print immediately before the fill phase, so the age is always 0. Freshness there is decided from the tick kind — a Tick::Trade counts as a print regardless of its reported size, since some venues emit zero-size index prints that would project to zero volume with no quote. The tick path already waits for fresh data structurally, because an order rests until the symbol's next tick arrives; the gate is still wired into it so the policy has one definition rather than two. Book events never reach the price/fill path at all, so there is nothing to gate.

The knob composes with the other bar-lane knobs. crates/vike-backtest/tests/fills/knob_composition.rs arms impact, staleness and variation-margin settlement on one run and proves each stays individually observable while the others are on — staleness there pushes a fill one bar out, onto a bar whose open is a different price.

On this page