QUANT_API
UTCSign inGet a key ↗
METHOD · 7 min read

Leak-Free Backtesting: How to Kill Look-Ahead Bias

Leak-free backtesting starts with point-in-time data. Learn how look-ahead and revision leaks kill live performance and how to resolve features at ts <= as_of.

A backtest is a promise about the future written in the language of the past. The single most common reason that promise is broken is look-ahead bias: somewhere in your pipeline, a number that was not yet knowable at the decision time leaked into the row that traded on it. The equity curve looks beautiful in research and dies the moment it meets a live order book. This guide defines the leaks precisely, then shows how leak-free backtesting is enforced at the data layer rather than hoped for in your code.

What point-in-time data actually means

Point-in-time means every value you read for a decision at time as_of is computed strictly from observations with ts <= as_of. Nothing from the future bleeds backwards. It sounds obvious, but in practice the failure modes are subtle. A funding rate is stamped at the moment it settles, not the moment the window it covers began. An aggregated order-flow imbalance over a five-minute bar is only complete once that bar closes, so reading it at the bar's open is a one-bar look-ahead. Resampling, forward-filling, and joining on a naive timestamp all quietly import information that a live system would not have had.

The leak-free rule is one line: for a decision at as_of, every feature value must be a function only of data with ts <= as_of. If a single column violates that, the whole backtest is fiction.

The three leaks that kill live performance

  • Look-ahead leak — a value that references the future relative to the decision time. The classic case is using a bar's closing statistics to make a decision at its open, or joining a feature on its settlement timestamp instead of its publication timestamp.
  • Revision leak — you backtest against the current version of a value, but at the time the value was first published it was different. Data gets corrected, restated, and back-filled. If your history reflects the corrected number rather than the originally observed one, you are trading on knowledge that did not exist yet.
  • Survivorship bias — in crypto, assets and venues come and go: tokens get delisted, perp markets are deprecated, exchanges fail. A universe assembled from today's survivors silently deletes everything that blew up, flattering any strategy that was implicitly long quality or liquidity.

All three share a root cause: the dataset answers "what is true now?" when a backtest needs "what was knowable then?" Those are different questions, and most data pipelines only answer the first.

Resolving a feature matrix at your timestamps

QUANT_API closes the gap by making point-in-time the default, not an opt-in. Call POST /v1/features/historical with a list of your own decision timestamps and a list of features, and you get back a matrix where each cell is resolved strictly from data with ts <= as_of for that row. The same resolver runs for GET /v1/features/live, which simply resolves "now" — so the value you would have read live is the value the historical call reproduces. There is no separate research path that quietly sees more than production does.

Features are addressed as category.signal@window[:transform]. There are 161 signals across BTC, ETH, SOL, XRP and BNB, 14 windows from 1s to 24h, and 9 transforms including zscore, pctrank and ewma. So flow.ofi@5m:zscore is the z-scored five-minute order-flow imbalance, and every one of those values in your matrix obeys the same ts <= as_of discipline. Browse the full inventory in the catalog.

bash
curl -s -X POST https://api.quant-api.dev/v1/features/historical \
  -H "Authorization: Bearer fk_live_<secret>" \
  -H "Content-Type: application/json" \
  -d '{
    "asset": "BTC",
    "as_of": [
      "2026-05-01T00:00:00Z",
      "2026-05-01T00:05:00Z",
      "2026-05-01T00:10:00Z"
    ],
    "features": [
      "flow.ofi@5m:zscore",
      "flow.vpin@15m:level",
      "liquidations.liq_pressure_index@30m:level",
      "funding.funding_dispersion@1h:delta",
      "positioning.lsr_top@15m:pctrank",
      "orderbook.book_imbalance@1m:level",
      "regime.regime_score@1h:level"
    ]
  }'

Each returned row is anchored to a timestamp you supplied, and every feature in it was computed only from data available at or before that timestamp. Feed that matrix into your model and the look-ahead leak is gone by construction — not because you remembered to shift a column, but because the resolver never had the future to give you. For a single feature over a continuous interval, GET /v1/features/series gives the same guarantee in time-series form.

Honest backtests and the span_days field

When you run POST /v1/backtest, the response reports span_days: the actual length of history the test covered. This matters because backtest overfitting thrives in the dark. A strategy with a dozen tuned parameters validated on a short window is a curve fit, not an edge, and a tiny span makes that fit look stronger than it is. Surfacing span_days keeps you honest about how much evidence you really have — a short span is a caveat, not a headline. There are no fabricated performance figures anywhere in QUANT_API, because the whole point is to let your own backtest tell you the truth.

Leak-free data does not guarantee a profitable strategy. It guarantees something more valuable: that the backtest result and the live result are measuring the same thing. When they finally diverge, you will know the cause was your model, your costs, or the regime — not a timestamp that lied. See the docs for the full request schema, or work through other guides on building features that survive contact with production.

Start free — 14-day Signal trial, no card
KEEP READING
Trading the post-liquidation-cascade reversalOrder Flow Imbalance (OFI): Why Aggressive Flow Leads PriceFunding Rates as a Directional Signal: Reading Crowd Positioning Without Look-Ahead