Unrealistic Fills, Costs and Liquidity
Every fault in this lesson has the same shape: the backtest awards you a price, or a size, that the market never actually offered. None of them produces an error message. All of them make the result better. And they compound — a system with three of these is not 30% too optimistic, it is describing a market that does not exist.
Fault 1: filling at a price that was not available
Section titled “Fault 1: filling at a price that was not available”The same-bar close fill
Section titled “The same-bar close fill”The most common and the most expensive:
Fragment — not a complete formula
// WRONG. The condition is computed FROM the close, and the fill is AT the// close, on the same bar. You acted on a number before it existed.SetTradeDelays( 0, 0, 0, 0 );BuyPrice = Close;Buy = Close > Ref( HHV( High, 50 ), -1 );The close is the last thing that happens in a bar. A rule computed from it cannot be acted on inside it. This is look-ahead bias in its most ordinary form, and it is so common because it is the default if you never think about delays.
The same-bar open fill, which is subtler and worse
Section titled “The same-bar open fill, which is subtler and worse”Fragment — not a complete formula
// ALSO WRONG, and harder to spot. The decision uses the close; the fill uses// the open of the SAME bar - a price that printed hours earlier.SetTradeDelays( 0, 0, 0, 0 );BuyPrice = Open;This one is worse than the close fill, because on average it hands you a positive edge before the trade even begins: you are systematically buying at the start of a bar you already know ended higher.
The audit formula later in this lesson prints exactly that quantity, as “Same-bar leak %”. On a breakout rule it is frequently the entire result.
The fix, and how to check it
Section titled “The fix, and how to check it”Fragment — not a complete formula
SetTradeDelays( 1, 1, 1, 1 );BuyPrice = Open; // the NEXT bar's open, because the signal movedThen verify it on a real trade. Open the trade list, pick a trade, find the signal bar on the chart, and confirm the entry date is the following bar and the price is that bar’s open. Not “confirm the setting is 1” — confirm the trade.
Fault 2: limit orders that would not have filled
Section titled “Fault 2: limit orders that would not have filled”A backtest can express “buy at the level” trivially. Reality cannot.
Fragment — not a complete formula
// A resting order below the market. Did it fill?BuyLimit = Ref( Close, -1 ) * 0.98;Buy = Low <= BuyLimit;BuyPrice = BuyLimit;The bar traded at or below your level, so the backtest fills you there. Two things it has not accounted for:
A gap through the level. If the bar opened below your limit, no resting order at that level
was ever available — the market was already past it. The honest floor is Max( BuyLimit, Low ),
or better, IIf( Open < BuyLimit, Open, BuyLimit ) for a buy limit, which fills at the open when
the market gapped through.
Queue position. Price touching your level does not mean your order filled. If the level was traded once and then the market moved away, the fills went to orders that were already in the queue. A backtest has no concept of the queue at all.
Fault 3: gaps through stops
Section titled “Fault 3: gaps through stops”This is the same problem on the exit side, and it is the one that destroys risk models.
ApplyStop’s fourth argument, ExitAtStop, decides it:
| Value | What the backtest does on a bar that gapped through your stop |
|---|---|
| 1 | Fills you at the stop level — a price the market never offered |
| 2 | Recognises the trigger, exits on the next bar at the trade price |
| 0 | Only checks the trade price, so may miss the trigger entirely |
The stops lesson covers this in full, including the
exploration that measures how often it happens on your own data. The reason it belongs here too is
what it does to risk: a system whose maximum loss per trade is enforced by ExitAtStop = 1 has
no maximum loss at all in reality. Every gap-down is unbounded, and the backtest has hidden every
one of them.
Fault 4: ignoring the spread
Section titled “Fault 4: ignoring the spread”The quoted price is not one price. You buy at the ask and sell at the bid, so you cross half the spread on entry and half on exit — before any commission and before any market impact.
On a liquid large-cap this is small. On the instruments where backtests look most exciting — thin names with large percentage moves — the spread can be several percent, and a system whose average winner is 3% is not a system at all.
Spread belongs in the price arrays, not the commission, for the reasons in the costs lesson.
Fault 5: positions larger than what traded
Section titled “Fault 5: positions larger than what traded”This is the one people find hardest to believe until they measure it.
A £100,000 account with ten equal slots wants £10,000 per position. On a symbol whose average daily turnover is £120,000, that is 8% of everything that changed hands. You are not a participant in that market; you are the market.
And it gets worse as the backtest compounds. Percent-of-equity sizing means positions grow with the account, so a system that quadruples its equity is, by the end, requesting positions four times larger in the same thin names.
Two ways to constrain it
Section titled “Two ways to constrain it”AmiBroker’s own setting. Settings → Portfolio has “Limit trade size as % of entry bar volume”, documented as preventing entries greater than a given percentage of the entry bar’s volume:
For example, if backtesting daily data, and today’s volume for [a] thinly traded stock is 177,000 shares, setting this to 10% will limit the maximum trade size to 17,700 shares (10% of total daily volume). This prevents ‘affecting the market’ by huge orders.
Two documented cautions come with it. Instruments without volume data — mutual funds, for example — will not be enterable at all unless you set the field to zero or tick “Disable trade size limit when bar volume is zero”. And it works on the entry bar’s volume, a single bar, which is noisy; an average-turnover constraint in the formula is steadier.
A constraint in the formula, which is what the next lesson’s participation-capped sizing does, and which has the advantage of being visible in the file rather than in a dialog.
Measuring your own assumptions
Section titled “Measuring your own assumptions”Two formulas. The first makes every execution assumption a parameter so you can see what each one is worth; the second audits individual signals.
A cost-sensitivity system
Section titled “A cost-sensitivity system”Complete runnable AFL
// cost-sensitivity-backtest.afl// Part 30 - Unrealistic Fills, Costs and Liquidity//// PURPOSE// A deliberately ordinary long-only breakout system whose only interesting// feature is that every execution assumption is a named parameter. Run it// once with the costs at zero and once with them at a pessimistic setting,// and read the difference. The difference is the part of the result that was// never yours.//// ============================ ASSUMPTIONS =============================// Universe whatever the Analysis window is applied to, with whatever// survivorship problems that list carries. State it in your// research log; the formula cannot know it.// Periodicity Daily.// Decision bar the bar on which the breakout condition becomes true,// evaluated on that bar's CLOSE.// Fill the NEXT bar's Open, because SetTradeDelays is 1 on all// four signals and BuyPrice/SellPrice are built from Open.// Slippage SlippagePct is added to every entry price and subtracted// from every exit price, in percent. This is a stand-in for// half-spread plus market impact. It is a guess. Make it a// pessimistic guess.// Commission percent of trade value, both legs, via CommissionMode 1.// Liquidity a symbol is only tradable on a signal bar if its 50-bar// average turnover, measured up to and including that bar,// clears MinTurnover, and its close clears MinPrice.// Participation NOT modelled here. Set "Limit trade size as % of entry bar// volume" on the Settings -> Portfolio tab, or use the// companion formula participation-capped-sizing.afl.// Stops max-loss stop with ExitAtStop = 2: the High-Low range is// checked, and the exit happens on the NEXT bar at the// regular trade price. That models "the stop was breached, I// found out, I got out at the next open" - not "I was filled// at exactly my stop price". Confirm the actual exit prices// in the trade list before you trust them.// Interest set the annual interest rate to 0 in Settings unless you// really do intend to credit idle cash.// ======================================================================//// This formula produces a simulation of a rule over history. It is not a// forecast, and no result it produces is an expectation of future returns.
SetOption( "InitialEquity", Param( "Initial equity", 100000, 10000, 1000000, 10000 ) );
BreakoutPeriod = Param( "Breakout lookback (bars)", 50, 10, 250, 5 );ExitPeriod = Param( "Exit lookback (bars)", 20, 5, 100, 5 );TrendPeriod = Param( "Trend filter MA (bars)", 200, 20, 300, 10 );StopLossPct = Param( "Max loss stop (%)", 8, 1, 30, 0.5 );PosQty = Param( "Max open positions", 10, 1, 50, 1 );
// --- the three assumptions the lesson is about -----------------------CommissionPct = Param( "Commission per trade (%)", 0.10, 0, 1.00, 0.01 );SlippagePct = Param( "Slippage per fill (%)", 0.15, 0, 2.00, 0.01 );MinTurnover = Param( "Min 50-bar turnover", 2000000, 0, 50000000, 250000 );MinPrice = Param( "Min close price", 5, 0, 100, 1 );
// ---------------------------------------------------------------------// 1. Portfolio and cost settings, set from the formula so that the report// and the formula can never disagree about what was assumed.// ---------------------------------------------------------------------SetOption( "MaxOpenPositions", PosQty );SetOption( "AllowPositionShrinking", True );SetOption( "CommissionMode", 1 ); // 1 = percent of trade valueSetOption( "CommissionAmount", CommissionPct );SetOption( "ActivateStopsImmediately", True ); // entries are on the open
PositionSize = -100 / PosQty; // equal weight across open slots
// ---------------------------------------------------------------------// 2. Execution model. Delays of 1 bar mean the backtester internally shifts// Buy/Sell/Short/Cover by one bar, so a signal decided on today's close is// acted on tomorrow. The price arrays are NOT shifted by the delay setting,// which is why they are written in terms of the fill bar's own Open.// ---------------------------------------------------------------------SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open * ( 1 + SlippagePct / 100 );SellPrice = Open * ( 1 - SlippagePct / 100 );ShortPrice = Open * ( 1 - SlippagePct / 100 );CoverPrice = Open * ( 1 + SlippagePct / 100 );
// AmiBroker clamps any assigned trade price into the bar's High-Low range.// A slipped open that lands outside the bar therefore fills at High or Low// rather than at the impossible price - which is the behaviour we want.
// ---------------------------------------------------------------------// 3. The rule itself. Every input is known at the close of the decision bar:// the breakout level uses bars strictly BEFORE the current one, so a bar// cannot break out of a range that includes its own high.// ---------------------------------------------------------------------Turnover = MA( Close * Volume, 50 );Tradable = Turnover >= MinTurnover AND Close >= MinPrice AND Volume > 0;
BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );ExitLevel = Ref( LLV( Low, ExitPeriod ), -1 );Trend = Close > MA( Close, TrendPeriod );
Buy = Close > BreakoutLevel AND Trend AND Tradable;Sell = Close < ExitLevel;
// Rank competing entries by liquidity, so that when more symbols signal than// there are slots, the more tradable ones are preferred. PositionScore is// ranked on its ABSOLUTE value by default, so keep it strictly positive.PositionScore = Turnover;
// ---------------------------------------------------------------------// 4. Risk exit. ExitAtStop = 2 checks the High-Low range but executes on the// NEXT bar at the regular trade price, so a gap through the stop is paid// for at the gapped price rather than at the level you nominated.// ---------------------------------------------------------------------ApplyStop( stopTypeLoss, stopModePercent, StopLossPct, 2 );
// ---------------------------------------------------------------------// 5. Chart view, so the same file can be inspected visually.// ---------------------------------------------------------------------_SECTION_BEGIN( "Cost sensitivity view" );Plot( Close, "Close", colorDefault, styleCandle );Plot( BreakoutLevel, "Breakout level", colorGreen, styleLine );Plot( ExitLevel, "Exit level", colorRed, styleLine );PlotShapes( Buy * shapeUpArrow, colorGreen, 0, Low );PlotShapes( Sell * shapeDownArrow, colorRed, 0, High );_SECTION_END();An ordinary long-only breakout system whose only interesting feature is that commission, slippage, the liquidity floor and the minimum price are all named parameters.
Run it twice: once with CommissionPct and SlippagePct at zero, once at a pessimistic
setting. The difference between the two results is the part of the result that was never yours.
Note the entry condition:
Fragment — not a complete formula
BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );Buy = Close > BreakoutLevel AND Trend AND Tradable;The lookback is shifted so that a bar cannot break out of a range that includes its own high. That is not a cost issue, but it belongs in the same audit: every input to the decision must be known at the close of the decision bar.
A per-signal audit
Section titled “A per-signal audit”Complete runnable AFL
// signal-execution-audit.afl// Part 30 - Challenge Collection: Six Broken Backtests//// PURPOSE// A backtest report tells you what the engine decided. It does not tell you// whether those decisions were physically possible. This exploration prints// one row per entry signal with the numbers you need to answer that// question: which price each execution assumption would have used, how much// free money a same-bar fill would have handed you, how many shares the// sizing rule wants, and how that compares with what actually traded that// day.//// Point it at the same universe and the same date range as the backtest you// are auditing, and change the rule in section 2 to match the one you are// checking.//// HOW TO RUN// Analysis -> Apply to: the universe under audit. Periodicity: Daily.// Range: the same range as the backtest. Press EXPLORE.// Sort by "Participation % of bar volume" descending and look at the top.//// ============================ ASSUMPTIONS =============================// Account size stated, not read from a live equity curve. The share// counts below are what an equal-weight allocation of that// stated account would buy. A compounding backtest will want// larger positions than this as equity grows, so treat these// figures as the optimistic case.// Fill prices three of them are printed side by side so you can see what// the choice is worth: this bar's Open, this bar's Close,// and the next bar's Open.// Costs the round-trip estimate is commission on both legs plus// two slippage crossings. It is an assumption, not a// measurement.// Ref(Open, 1) reads the bar after the signal. That is legitimate in an// AUDIT, which is looking backwards at what happened next.// The identical expression inside a Buy rule is a bug.// ======================================================================
SetBarsRequired( -2, -2 );
AccountSize = Param( "Assumed account size", 100000, 10000, 10000000, 10000 );PosQty = Param( "Max open positions", 10, 1, 50, 1 );CommissionPct = Param( "Commission per trade (%)", 0.10, 0, 1.00, 0.01 );SlippagePct = Param( "Slippage per fill (%)", 0.15, 0, 2.00, 0.01 );StopPct = Param( "Max loss stop (%)", 8, 1, 30, 0.5 );
// ---------------------------------------------------------------------// 1. Reference measurements// ---------------------------------------------------------------------Turnover = MA( Close * Volume, 50 );
// ---------------------------------------------------------------------// 2. THE RULE UNDER AUDIT. Replace this with the entry condition of whatever// backtest you are checking. Everything below is generic.// ---------------------------------------------------------------------BreakoutPeriod = Param( "Breakout lookback (bars)", 50, 10, 250, 5 );EntrySignal = Close > Ref( HHV( High, BreakoutPeriod ), -1 ) AND Volume > 0;
// ---------------------------------------------------------------------// 3. What each execution assumption would have paid// ---------------------------------------------------------------------FillSameBarOpen = Open; // delay 0, trade price OpenFillSameBarClose = Close; // delay 0, trade price CloseFillNextBarOpen = Ref( Open, 1 ); // delay 1, trade price Open
// The gap between deciding on the Close and being filled at that same bar's// Open. Positive means the leak paid you before the trade began.SameBarEdgePct = 100 * SafeDivide( Close - Open, Open, 0 );
// What the honest assumption costs relative to the leaky one, on this bar.NextOpenGapPct = 100 * SafeDivide( FillNextBarOpen - Close, Close, Null );
// ---------------------------------------------------------------------// 4. Size, and whether the market could have absorbed it// ---------------------------------------------------------------------IntendedDollars = AccountSize / PosQty;FillPrice = FillNextBarOpen * ( 1 + SlippagePct / 100 );IntendedShares = SafeDivide( IntendedDollars, FillPrice, Null );
EntryBarVolume = Ref( Volume, 1 ); // the bar the fill happens onParticipationBar = 100 * SafeDivide( IntendedShares, EntryBarVolume, Null );ParticipationAvg = 100 * SafeDivide( IntendedDollars, Turnover, Null );
// ---------------------------------------------------------------------// 5. Cost and stop realism// ---------------------------------------------------------------------RoundTripCostPct = 2 * CommissionPct + 2 * SlippagePct;
StopLevel = FillPrice * ( 1 - StopPct / 100 );
// Did the next bar open below where the stop would have been placed? If so, a// backtest that exits "at the stop level" is claiming a fill at a price that// was never available on that bar.GapThroughStop = Ref( Open, 1 ) < StopLevel;
// ---------------------------------------------------------------------// 6. Output: one row per entry signal// ---------------------------------------------------------------------Filter = EntrySignal AND Status( "barinrange" );
AddColumn( DateTime(), "Signal date", formatDateTime );AddColumn( Close, "Decision close", 1.3 );
AddColumn( FillSameBarOpen, "Fill: same-bar open", 1.3 );AddColumn( FillSameBarClose, "Fill: same-bar close", 1.3 );AddColumn( FillNextBarOpen, "Fill: next-bar open", 1.3 );AddColumn( SameBarEdgePct, "Same-bar leak %", 1.2 );AddColumn( NextOpenGapPct, "Overnight gap %", 1.2 );
AddColumn( IntendedDollars, "Intended position", 1.0 );AddColumn( IntendedShares, "Intended shares", 1.0 );AddColumn( EntryBarVolume, "Entry bar volume", 1.0 );AddColumn( ParticipationBar, "Participation % of bar volume", 1.2 );AddColumn( ParticipationAvg, "Participation % of avg turnover", 1.2 );
AddColumn( RoundTripCostPct, "Assumed round-trip cost %", 1.2 );AddColumn( StopLevel, "Stop level", 1.3 );AddColumn( GapThroughStop, "Gapped through stop", 1.0 );
AddColumn( Turnover, "50-bar avg turnover", 1.0 );
// COUNT and AVERAGE rows: the average same-bar leak is the single most useful// number on this table.AddSummaryRows( 2 | 16, 1.2 );Run this as an Exploration over the same universe and range as the backtest you are checking, having replaced the rule in section 2 with your own entry condition. It prints one row per entry signal with:
- what three different fill assumptions would each have paid
- Same-bar leak % — how much free money a same-bar close-to-open fill would have handed you
- the overnight gap between the decision close and the next open
- the intended share count, and that count as a percentage of the entry bar’s volume
- the same as a percentage of 50-bar average turnover
- whether the next bar opened below where the stop would have sat
A realistic-fill checklist
Section titled “A realistic-fill checklist”Run this against any backtest before you believe it.
- Delays. Are they at least 1 on all four signals? Confirmed on an actual trade, not just in the settings?
- Fill price. Is the price array read on the bar the shifted signal lands on, and is it a price that existed on that bar?
- Same-bar leak. Does any decision use a bar’s close and fill within that same bar?
- Limit fills. If you fill at a level, what happens when the bar gapped through it? Is queue position acknowledged as unmodelled?
- Stops. Is
ExitAtStop2 rather than 1 on daily data? If 1, have you measured gap frequency on your universe? - Spread. Is it in the price arrays, in the correct direction on all four?
- Commission. Set, on both legs, at a figure you can point to in a broker schedule?
- Participation. What is the largest position, as a percentage of that bar’s volume and of average turnover? Do you know, or are you guessing?
- Compounding. Does the position size grow with equity, and if so, is the participation constraint growing with it?
- Sensitivity. At what cost level does the conclusion change?
Five faults, all silent, all favourable. Filling inside the bar that produced the decision hands
you an edge before the trade starts. Limit fills assume you were at the front of a queue that is
not modelled, and are wrong one-directionally. ExitAtStop = 1 removes gaps from your risk model
entirely. Spread is real, belongs in the price, and is widest when your rules most want to trade.
And a position larger than a meaningful fraction of what actually traded is not a position — it is
a fiction that compounds as the backtest’s equity grows. Every one of these can be measured on your
own data, and measuring them is cheaper than finding out later.
Check your understanding
Sources for this lesson
6 verified · checked 2026-09-01
- 01AmiBroker User's Guide — Settings window§ Limit trade size as % of entry bar volumeamibroker.com/guide/w_settings.html2026-09-01
- 02AFL Function Reference — SetOption§ PriceBoundChecking, CommissionModeamibroker.com/guide/afl/setoption.html2026-08-31
- 03AFL Function Reference — ApplyStop§ ExitAtStopamibroker.com/guide/afl/applystop.html2026-08-31
- 04AmiBroker User's Guide — Portfolio-level backtestingamibroker.com/guide/h_portfolio.html2026-08-31
- 05AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
- 06AFL Function Reference — SafeDivideamibroker.com/guide/afl/safedivide.html2026-08-31
Every technical claim on this page was checked against the official AmiBroker documentation on the date shown. Where the course disagrees with folklore, the source is how you can tell which one to trust.