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
- The strategy decides on a closed bar. Nothing it decides can trade inside the bar it was decided on.
- By default (
execution: next_open) the decision reaches the venue at the next bar's open, which is when a live run would place it. - A market order fills at the next open's touch (the ask for a buy, the bid for a sell) plus the slippage you set.
- The book a candle is quoted with has a half-spread of 1 basis point around each price, so a buy always pays a little more than the mid and a sell receives a little less.
Walking the bar both ways
Inside each bar the engine walks the price along both possible paths:
- open → low → high → close
- 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.
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:
- liquidation
- stop-loss
- take-profit
- 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.
Stops and targets
- A long position's stop and target trigger on the bid, a short's on the ask — the side you would actually close on.
- Exits are taker orders: they pay the taker fee and your slippage.
- A take-profit placed as a resting limit (
signal_closeexecution) fills at its own price and pays the maker fee.
Resting limit orders
- A resting limit order fills only when the price trades through it, not when it merely touches it. On a touch, part of the queue in front of you may not have traded; the engine assumes yours did not.
- A post-only (maker) order that would cross the book at the open is rejected, as the exchange rejects it. The
report counts these in
post_only_rejects. - A resting entry is cancelled if it has not filled after the strategy's bar (and at least one minute), the way a
live run cancels a leftover entry when it is flat. The report counts these in
entries_expired.
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
- Above 1× leverage the account is liquidated when its equity falls to the maintenance margin
(
maintenance_margin_rate, 0.4% by default). - A liquidation settles at the bankruptcy price: the account loses its whole balance, as an isolated position does on the exchange. There is no partial recovery that the exchange would not give you.
- An entry the account cannot margin at the time is skipped and counted in
entries_skipped_no_margin. A strategy whose declared size the account could never margin is an error, not a result.
Funding
Perpetual futures pay or charge funding every few hours. The backtest replays the market's real funding history:
- A settlement is charged to the position held at that time, on its size at the open price of the bar it falls in.
- When a settlement falls inside a bar, the engine does not know whether the position was still open at that instant. A charge is always applied to the position held at the bar's open. A payment to you is applied only if the position is still held at the bar's close.
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:
- be finer than the bars and divide them evenly;
- run forward in time;
- cover every bar the backtest trades;
- belong to the same market (a strategy that reads other markets cannot use them).
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.