Articles ·

How a Metrand backtest fills orders inside a bar

A candle tells you four prices: open, high, low and close. It does not tell you the order in which the high and the low were reached, how long the price stayed at each level, or whether your limit order was actually filled when the price merely touched it. Every backtest has to answer those questions somehow. Most engines answer them in the way that flatters the strategy. The Metrand engine answers them in the way that costs it, and says so in the report.

This page describes the rules exactly. They apply to every backtest on the platform: a single backtest, a comparison of versions, a split into periods, a basket of markets, the parameter optimizer, and the API.

Decisions and orders

Walking the bar both ways

Inside each bar the engine walks the price along both possible paths:

  1. open → low → high → close
  2. open → high → low → close

It runs your position and orders along each path on a separate copy of the account. Then it keeps the path that leaves the account worse at the close, valued as if the position were closed there at the touch with its exit fee. When the closes tie, it keeps the path whose equity fell lower on the way, then the one with the lower high.

If the two paths ended differently — for example, one hit the take-profit and the other the stop — the trade is marked either way (ambiguous_bar: true), and the report counts these trades in ambiguous_bar_exits. You see exactly which results depend on the order the candle hides.

One candle and a long position with a stop below and a target above the open, walked both ways: low first, the stop fills; high first, the target fills. The low-first walk is kept.
One bar, two walks. Walked low first, the long is stopped out; walked high first, it takes profit. The engine keeps the walk that leaves the account worse at the close and marks the trade “either way”.

The maximum drawdown is taken as the worse of the two paths, even when the kept path is the other one. A long held through a bar that went up 10 and then down 20 is reported as having fallen 20, not 10.

Events inside a path

Along a path the engine meets price levels in the order the price reaches them. When several levels are reached at the same price, they resolve in this order, which is the pessimistic one:

  1. liquidation
  2. stop-loss
  3. take-profit
  4. fills of resting orders

A level the bar opened beyond (a gap) fires at the open price, not at the level: a stop that gapped is filled where the market actually was.

Three bars above a long position’s stop, then a bar that opens below it: the stop fills at that bar’s open, not at the stop level.
A bar that opens beyond a stop fills it at the open, where the market was, not at the level.

Stops and targets

Resting limit orders

A resting buy limit order: a bar whose low only touches the limit leaves it unfilled; a bar that trades below the limit fills it at the limit price.
A touch is not a fill: a resting limit fills only when the price trades through it.

Grids and martingale ladders

A ladder of orders (decision.grid) is placed as one plan: the first leg at market or as a maker order, the other rungs as resting limits spaced by spacing_bps, each scale times the size of the one before. A stop that lies between two rungs fires before the deeper rung fills, because the price reaches it first.

Leverage and liquidation

Funding

Perpetual futures pay or charge funding every few hours. The backtest replays the market's real funding history:

Finer candles under the bars

Finer candles (detail) can go under the bars the strategy decides on — for example 1-minute candles under 15-minute bars. The strategy still decides on the 15-minute bars, but the account is marked on every minute, so the engine knows which extreme came first instead of assuming the worse one. A backtest on the platform fetches them itself: the finest of the market's own candles that fit, one-minute candles under a month of hourly bars and coarser ones under longer windows (which and why). Sent through the API, the finer candles must:

Anything else is refused with an error rather than replayed with a silent gap.

An example from our own measurements: a martingale ladder on BCHUSDT, January to June 2021, on 15-minute bars. An earlier version of the engine, which marked each bar in one fixed order, liquidated it in the crash of 19 May. Walked both ways on the 15-minute bars, 25 of its exits were "either way". Marked on 1-minute candles, only 2 stayed ambiguous, and the minutes show the ladder came through May without a liquidation. The 15-minute result is the conservative bound; the minutes tell what happened.

What the report tells you

Every backtest result carries the terms it was measured on (report.config): fees, slippage, leverage, maintenance margin, execution mode, how long a resting entry waits, the candle span the account was marked on, the lot rules, and whether funding was applied. Alongside the P&L you get the counts that show where the rules above decided the result: ambiguous_bar_exits, post_only_rejects, entries_expired, entries_skipped_no_margin and liquidations.

The same rules apply to every request, from the web app and from the API alike.