Backtester Basics: Signals and Trade Prices
By the end of this lesson you will be able to look at a trade in AmiBroker’s trade list and say, without guessing, which bar produced the signal, which bar the fill happened on, and which array supplied the price. That sounds like a small skill. It is the one that separates people who can debug a backtest from people who can only re-run it.
Four arrays, and not one of them knows what time it is
Section titled “Four arrays, and not one of them knows what time it is”A backtest formula communicates with the backtester through four reserved arrays. Buy
opens a long position. Sell closes a long position. Short opens a short position.
Cover closes a short position. They are arrays like every other array in AFL: one value
per bar, non-zero meaning true.
Two consequences follow immediately, and both surprise people.
Sell does not open a short. A formula that assigns only Buy and Sell is a long-only
test, and short trades are simply not simulated. AmiBroker has had separate rules for the
two directions since version 3.59; before that, Buy and Sell did double duty and every
system was a stop-and-reverse. You can still get that behaviour deliberately, and the User’s
Guide gives the idiom:
Fragment — not a complete formula
// Always in the market: every long exit is also a short entry.Short = Sell;Cover = Buy;The second consequence is the important one. The signal arrays carry no timing
information whatsoever. A 1 on bar 40 of the Buy array says “an entry belongs to bar
40”. It does not say when during bar 40, and there is no way to encode that in the array,
because the array has one slot per bar and it is already used.
What the backtester does with those arrays
Section titled “What the backtester does with those arrays”The engine runs in two phases, and knowing which phase you are in explains a great deal later. In the first phase your formula is executed once per symbol, and AmiBroker collects the signal arrays, the price arrays, the position-size array and the score array. In the second phase it walks forward bar by bar and signal by signal, and actually executes trades.
From your formula to a row in the trade list
- Your formula runsOnce per symbol. Buy, Sell, Short, Cover, the four price arrays, PositionSize and PositionScore are collected.
- Delays are appliedInside the backtester, after your formula has finished: Buy = Ref( Buy, -buydelay ), and the same for the other three.
- Redundant signals are removedIn the default mode, entries between an entry and its matching exit are stripped exactly as ExRem() would strip them.
- Signals are ranked and listedPer bar, top-ranked entries first, then scale and exit signals.
- The walkBar by bar, the engine enters, exits and marks positions to market against the price arrays.
Trade delays, in detail
Section titled “Trade delays, in detail”SetTradeDelays( buydelay, selldelay, shortdelay, coverdelay ) takes four arguments, all
required, and overrides the delays configured in Settings. What it does is documented
literally, and it is worth memorising because it explains every delay-related surprise:
Fragment — not a complete formula
// What the backtester does internally, after your formula has run.Buy = Ref( Buy, -buydelay );Sell = Ref( Sell, -selldelay );Short = Ref( Short, -shortdelay );Cover = Ref( Cover, -coverdelay );That is the whole mechanism. It is functionally identical to putting those four lines at the end of your own formula.
What is not shifted
Section titled “What is not shifted”Nothing else moves. Not the price arrays. Not PositionSize. Not PositionScore. The
documentation states this plainly, and it has a sharp practical edge: if your position sizing
or your ranking reads values out of the Buy array and you also use a non-zero delay, you
must shift those yourself, because they will still be lined up with the undelayed signals.
It also means the price arrays are read on the bar the shifted signal lands on. With
SetTradeDelays( 1, 1, 1, 1 ) and BuyPrice = Open, a signal on Tuesday’s close fills at
Wednesday’s open — not at Tuesday’s open. The signal moved; the price array stayed put and
was evaluated where the signal arrived.
Why the delay is not optional
Section titled “Why the delay is not optional”A rule computed from a bar’s close cannot be acted on inside that bar, because the close is the last thing that happens. Backtesting a close-signal at the same bar’s close is not aggressive modelling; it is a formula that trades on information it did not yet have. The lesson on look-ahead bias in Part 30 takes this apart properly. For now: a system whose signals come from closes needs a delay of at least one bar, or its results are describing a market where you can act on a price before it prints.
There is one legitimate exception, and it is narrow: a rule that depends only on a level being touched, filled by a resting order, rather than on a bar’s summary statistic. That pattern is built with price arrays rather than delays, and it is covered below.
The signal that falls off the end
Section titled “The signal that falls off the end”Shifting an array forward has an edge case. A Sell on the very last bar of the range,
shifted by one, lands on a bar that does not exist and is lost. That is not a bug, and in a
portfolio backtest it does not lose you the trade — the engine closes any position still open
at the end of the range at the closing price, and includes it in the statistics. But it does
mean the final trade in the list may have an exit that your rules did not generate. Check
before you interpret the last trade.
Walking one symbol, bar by bar
Section titled “Walking one symbol, bar by bar”Here is the whole chain on eight bars. The prices are invented for the illustration — they are not market data and nothing should be inferred from them.
Raw signals, kept signals, delayed signals, and the price that fills
| Bar | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
Open | 10.10 | 10.20 | 10.50 | 10.80 | 10.50 | 10.30 | 10.60 | 11.20 |
Close | 10.00 | 10.40 | 10.90 | 10.60 | 10.20 | 10.80 | 11.40 | 11.10 |
Buy (raw) | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 |
Sell (raw) | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
Buy after ExRembar 3 removed | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 |
Sell after ExRem | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
Buy after delay 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
Sell after delay 1bar 8 shifted off the end | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
Fill at Open | — | — | 10.50 | — | — | 10.30 | — | 11.20 |
Read the last two rows against the first. The trade list will say the entry happened on bar 3 at 10.50, and a reader who has not internalised the delay will look for something interesting about bar 3 and find nothing. The interesting thing happened on bar 2.
The four price arrays
Section titled “The four price arrays”BuyPrice, SellPrice, ShortPrice and CoverPrice define what you are simulated to have
paid or received. AmiBroker pre-fills all four from the Trade price fields in Settings, so
assigning them is optional. Assign them anyway. A formula that states its own fill price
cannot be silently invalidated by a settings dialog that somebody else changed, and the
formula is the thing you keep.
Price-bound checking
Section titled “Price-bound checking”During backtesting AmiBroker checks each assigned price against the bar’s High–Low range and
adjusts it silently: a value above the High becomes the High, a value below the Low
becomes the Low. This is usually protective — it stops you filling at a price the bar never
saw — but it also quietly removes slippage you deliberately added on bars that opened at an
extreme. SetOption( "PriceBoundChecking", False ) disables it, which is almost never what
you want in an honest test.
Simulating a resting stop order
Section titled “Simulating a resting stop order”Clamping is what makes the documented stop-order idiom work. If you want to model an order resting above the market, you need the fill to be the trigger level, not the close:
Fragment — not a complete formula
BuyStop = Ref( HHV( High, 20 ), -1 ); // a level known before the bar openedBuy = Cross( High, BuyStop ); // the bar traded through itBuyPrice = Max( BuyStop, Low ); // fill at the level, or at the Low if it gappedMax( BuyStop, Low ) is the honest half of that pattern: if the bar gapped straight past
your level and opened above it, the level was never available, and taking the Low is the
nearest thing the daily bar can offer. The stops lesson later in this part is entirely about
how much this kind of assumption is worth.
Same-bar entries and exits
Section titled “Same-bar entries and exits”It is completely normal for a symbol to have an entry and an exit signal on the same bar. Since neither the signals nor the prices carry timing, AmiBroker cannot infer which came first, so you tell it — with two options, in three documented combinations.
AllowSameBarExit |
HoldMinBars |
What the engine does |
|---|---|---|
| False | any | Only one signal per symbol is acted on per bar. If flat, the entry is taken (Buy beats Short); if long, the Sell is taken. Then it moves to the next bar. |
| True | 0 (default) | Both are used, entry before exit. Single-bar trades, flat between bars. |
| True | 1 or more | Both are used, exit before entry. Trades span at least overnight. |
In a portfolio run these conflicts are resolved on each symbol first, and only the survivors go forward to be ranked against the other symbols.
Long-only and long-and-short
Section titled “Long-only and long-and-short”To test both directions you assign all four arrays. The User’s Guide’s own two-sided example uses one indicator with two thresholds:
Fragment — not a complete formula
Buy = Cross( CCI(), 100 );Sell = Cross( 100, CCI() );
Short = Cross( -100, CCI() );Cover = Cross( CCI(), -100 );Between −100 and +100 this system holds nothing, which is a design decision worth noticing: being flat is a position too.
Three engine behaviours govern how the two sides interact. When you are flat and both an
entry Buy and an entry Short fire on the same bar for the same symbol, Buy takes
precedence. SetOption( "ReverseSignalForcesExit", True ) is the default, and it means a
Short closes an open long even without a Sell; turning it off makes the engine ignore
Short during a long trade and Buy during a short trade. And a short test is not free
merely because the simulator allows it — borrow availability, borrow cost and short-sale
rules are real and are not modelled by any of this.
Redundant signals and the default mode
Section titled “Redundant signals and the default mode”Buy will often be true on several consecutive bars. In the default mode,
backtestRegular, AmiBroker removes the “extra” entries — the ones that come after an entry
and before its matching exit — in exactly the way ExRem() removes them. You do not need to
call SetBacktestMode() at all to get this.
There is a consequence that generates more confused forum posts than almost anything else in
the backtester. If a trade is not entered on the first entry signal — because it ranked
too low, or the cash was gone, or the position limit was reached — then every later entry
signal in that block is ignored too, until a matching exit signal arrives. The system appears
to take a fraction of its own signals. The documented cure is backtestRegularRaw, which
keeps every entry signal and acts on any that is ranked highly enough and affordable, while
still allowing only one open position per symbol.
The formulas
Section titled “The formulas”Two files. The first is a minimal system whose only purpose is to make the arrays visible; the second is the audit tool you will reach for whenever a trade list surprises you.
Complete runnable AFL
// signals-to-trades.afl// Part 28 - Backtester Basics: Signals and Trade Prices//// A deliberately minimal system whose only job is to make the four signal// arrays and the four price arrays visible. Run it as a BACKTEST in the// Analysis window with "Apply to" set to Current symbol and Periodicity Daily.//// ASSUMPTIONS - every one of these is a choice, and every one moves the answer://// Fill price Next bar's open, taken exactly. No slippage, no spread and no// partial fill are modelled here; the costs lesson adds them.// Delays One bar on all four signals. A rule computed from a bar's// close cannot be acted on until the next bar has begun.// Commission Whatever is currently in Settings. Set it on purpose before// you read any number this formula produces.// Sizing 100% of portfolio equity in one position, one symbol at a// time. This is a single-symbol illustration, not a portfolio.// Liquidity Not modelled at all. The simulator will fill any size.// Data Whatever your database holds, with whatever survivorship,// split and dividend treatment it happens to have.//// This simulates a rule set over history that has already happened. It is not// a forecast, and nothing here should be read as an expected return.
// ------------------------------------------------------------ 1. the accountSetOption( "InitialEquity", 100000 );SetOption( "MaxOpenPositions", 1 );SetOption( "AllowPositionShrinking", True );
// Put all available equity into the single open position. Anything less leaves// cash idle, earning the annual interest rate set in Settings.PositionSize = -100;
// ------------------------------------------------------------- 2. the timing// SetTradeDelays does exactly one thing, documented: inside the backtester,// after this formula has run, it applies Ref( Buy, -buydelay ) and the same to// the other three arrays. Nothing else is shifted - not the price arrays, not// PositionSize, not PositionScore.SetTradeDelays( 1, 1, 1, 1 );
// ------------------------------------------------------------- 3. the prices// AmiBroker pre-fills all four price arrays from the Trade price fields in// Settings, so assigning them is optional. Assign them anyway: a formula that// states its own fill price cannot be invalidated by someone else's dialog.BuyPrice = Open;SellPrice = Open;ShortPrice = Open;CoverPrice = Open;
// ---------------------------------------------------------- 4. the two rules// The AmiBroker tutorial's own example system, used here because it is the// smallest thing that produces both entries and exits. It is not a// recommendation, and it has not been tested for anything.MaPeriod = 45;Trend = EMA( Close, MaPeriod );
Buy = Cross( Close, Trend );Sell = Cross( Trend, Close );
// ----------------------------------------------------- 5. the optional short// Short trades are simulated only if Short and Cover are actually assigned.// Set TradeShortSide to 1 to test the symmetric short rules; leave it at 0 for// a long-only test. Note that shorting is not free, not always permitted, and// not modelled here beyond the price and the commission.TradeShortSide = 0;
if( TradeShortSide ){ Short = Cross( Trend, Close ); Cover = Cross( Close, Trend );}
// -------------------------------------------------------- 6. what to look at// Run the backtest, then open the trade list. For the first trade, check three// things against the chart: the entry date is ONE bar after the bar on which// Buy became true, the entry price equals that bar's open, and the exit obeys// the same rule. If any of the three is not what you expect, the delay or the// price array is not doing what you assumed.Run it as a Backtest with Apply to: Current symbol, then open the trade list and check
three things on the first trade: the entry date is one bar after the bar on which Buy
became true, the entry price equals that bar’s open, and the exit obeys the same rule. If
any of the three is not what you expected, the delay or the price array is not doing what
you assumed, and everything downstream is built on that misunderstanding.
Complete runnable AFL
// signal-audit.afl// Part 28 - Backtester Basics: Signals and Trade Prices//// Run this as an EXPLORATION, not a backtest, with "Apply to" set to Current// symbol and Range set to a few hundred recent bars. It prints, bar by bar,// every transformation the backtester will apply to your signal arrays before// it simulates a single trade, so you can see the trades coming before you// read a report that summarises them.//// WHY AN EXPLORATION. Only the backtester implements trade delays. In// Exploration, Scan and Indicator modes SetTradeDelays() does nothing at all,// which is exactly why the shifted columns below are computed with Ref()// explicitly rather than by calling SetTradeDelays and hoping.//// ASSUMPTIONS. The rules and the delay below must be kept identical to the// backtest formula you are auditing, by hand. Nothing enforces that for you.
BuyDelay = 1; // must match SetTradeDelays( BuyDelay, SellDelay, ... )SellDelay = 1;MaPeriod = 45;
// ------------------------------------------------------------ 1. the raw rulesTrend = EMA( Close, MaPeriod );RawBuy = Cross( Close, Trend );RawSell = Cross( Trend, Close );
// -------------------------------------------- 2. what backtestRegular keeps// The default backtest mode removes "extra" entry signals - the ones that come// after an entry and before its matching exit - exactly the way ExRem() does.// backtestRegularRaw would keep them all.KeptBuy = ExRem( RawBuy, RawSell );KeptSell = ExRem( RawSell, RawBuy );
// ----------------------------------------------- 3. what the delay does next// The documented mechanism is literally Ref( Buy, -buydelay ) applied inside// the backtester. Because the same shift is applied to both arrays here, it// does not matter whether you picture the removal or the shift happening// first. With DIFFERENT buy and sell delays it would matter, and this audit// would no longer be exact.ActedBuy = Ref( KeptBuy, -BuyDelay );ActedSell = Ref( KeptSell, -SellDelay );
// --------------------------------------------------- 4. the price that fills// The price arrays are NOT shifted by trade delays. The fill happens on the// bar the shifted signal lands on, at that bar's value of the price array.FillPrice = Open;
// ------------------------------------------------------ 5. the conflict flag// Buy and Sell true on the same bar is not an error and not rare. How it is// resolved is decided by AllowSameBarExit and HoldMinBars, never by the price// arrays, which carry no timing information whatsoever.SameBarConflict = RawBuy AND RawSell;
// ------------------------------------------------------------- 6. the outputFilter = RawBuy OR RawSell OR ActedBuy OR ActedSell;
AddColumn( Close, "Close", 1.2 );AddColumn( Open, "Open", 1.2 );AddColumn( Trend, "EMA", 1.2 );
AddColumn( RawBuy, "Raw Buy", 1.0 );AddColumn( RawSell, "Raw Sell", 1.0 );AddColumn( KeptBuy, "Kept Buy", 1.0 );AddColumn( KeptSell, "Kept Sell", 1.0 );AddColumn( ActedBuy, "Acted Buy", 1.0 );AddColumn( ActedSell, "Acted Sell", 1.0 );
AddColumn( IIf( ActedBuy OR ActedSell, FillPrice, Null ), "Fill price", 1.2 );AddColumn( SameBarConflict, "Same-bar conflict", 1.0 );
// Running counts, so you can compare "signals my formula produced" with// "signals the backtester will actually act on". A large gap between the two// is the usual explanation for a system that seems to ignore most of its own// entries.AddColumn( Cum( RawBuy ), "Raw buys so far", 1.0 );AddColumn( Cum( ActedBuy ), "Acted buys so far", 1.0 );This one runs as an Exploration, and the reason is itself a fact worth knowing: only the
backtester implements trade delays. In Exploration, Scan and Indicator modes,
SetTradeDelays() does nothing at all. So the audit computes the shift with Ref()
explicitly rather than calling the function and hoping.
The backtester sees four Boolean arrays and four price arrays, none of which carries any
information about when inside a bar something happened. Trade delays shift the signal arrays
by Ref() and shift nothing else, which is why the fill price is read on the bar the shifted
signal lands on. Assigned prices are silently clamped into the bar’s High–Low range. Same-bar
conflicts are resolved by AllowSameBarExit and HoldMinBars, never by the prices. Short
trades happen only if you assign Short and Cover. And in the default backtest mode, a
skipped entry suppresses every later entry until the matching exit.
Check your understanding
Sources for this lesson
5 verified · checked 2026-08-31
- 01AmiBroker User's Guide — Back-testing your trading ideasamibroker.com/guide/h_backtest.html2026-08-31
- 02AmiBroker User's Guide — Portfolio-level backtesting§ Resolving same-bar, same-symbol signal conflictsamibroker.com/guide/h_portfolio.html2026-08-31
- 03AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
- 04AFL Function Reference — SetBacktestModeamibroker.com/guide/afl/setbacktestmode.html2026-08-31
- 05AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.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.