Skip to content
Level 4 · Trading System ResearcherLessonPart 28 · page 1 of 830 min
30Minutes
7AFL functions
5Sources
StandardRequires
AFL functions taught here7

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

  1. Your formula runsOnce per symbol. Buy, Sell, Short, Cover, the four price arrays, PositionSize and PositionScore are collected.
  2. Delays are appliedInside the backtester, after your formula has finished: Buy = Ref( Buy, -buydelay ), and the same for the other three.
  3. Redundant signals are removedIn the default mode, entries between an entry and its matching exit are stripped exactly as ExRem() would strip them.
  4. Signals are ranked and listedPer bar, top-ranked entries first, then scale and exit signals.
  5. The walkBar by bar, the engine enters, exits and marks positions to market against the price arrays.

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.

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.

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.

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.

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

Invented prices, used to show the transformations. Two trades result: in at 10.50 on bar 3, out at 10.30 on bar 6, and a second entry at 11.20 on bar 8 that is still open when the range ends.
Bar12345678
Open10.1010.2010.5010.8010.5010.3010.6011.20
Close10.0010.4010.9010.6010.2010.8011.4011.10
Buy (raw)01100010
Sell (raw)00001001
Buy after ExRembar 3 removed01000010
Sell after ExRem00001001
Buy after delay 100100001
Sell after delay 1bar 8 shifted off the end00000100
Fill at Open10.5010.3011.20
Invented prices, used to show the transformations. Two trades result: in at 10.50 on bar 3, out at 10.30 on bar 6, and a second entry at 11.20 on bar 8 that is still open when the range ends. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

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.

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.

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.

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 opened
Buy = Cross( High, BuyStop ); // the bar traded through it
BuyPrice = Max( BuyStop, Low ); // fill at the level, or at the Low if it gapped

Max( 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.

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.

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.

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.

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
// 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 account
SetOption( "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.

Download signals-to-trades.afl78 lines

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
// 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 rules
Trend = 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 output
Filter = 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 );

Download signal-audit.afl76 lines

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

Question 1. A formula sets BuyPrice = Open, SellPrice = Close and calls SetTradeDelays( 1, 1, 1, 1 ). A Buy signal is true on Tuesday. When and at what price is the fill simulated?
SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open;
Buy = Cross( Close, MA( Close, 50 ) );
Show the answer and why

Answer: Wednesday, at Wednesday's open

The delay shifts the signal to Wednesday. Price arrays are never shifted, so the engine reads BuyPrice where the shifted signal landed — Wednesday’s value of the Open array.

Question 2. Which of these actually tell AmiBroker that a same-bar entry happened before a same-bar exit? Select all that apply.
Show the answer and why

Answer: SetOption( "AllowSameBarExit", True ) with HoldMinBars at 0

Only AllowSameBarExit and HoldMinBars control the interpretation. Prices carry no timing, and the order of statements in a formula is irrelevant because all of it is evaluated before the backtester runs. HoldMinBars of 1 gives the opposite ordering: exit before entry.

Question 3. A system generates 900 Buy signals in a Scan but the backtest reports 120 trades. Which explanation is documented behaviour rather than a defect?
Show the answer and why

Answer: Redundant entries between an entry and its matching exit are removed, and a skipped entry suppresses every later one until an exit arrives

That is exactly what backtestRegular does, and it is why a skipped trade cascades. backtestRegularRaw keeps every entry signal and is the documented alternative.

Question 4. True or false: assigning only Buy and Sell produces a stop-and-reverse system that is always in the market.
Show the answer and why

Answer: False

It produces a long-only test. Short trades are simulated only if Short and Cover are assigned. Stop-and-reverse is the explicit idiom Short = Sell; Cover = Buy;.

Question 5. Why does BuyPrice = Max( BuyStop, Low ) appear in the documented stop-order idiom instead of BuyPrice = BuyStop?
Buy      = Cross( High, BuyStop );
BuyPrice = Max( BuyStop, Low );
Show the answer and why

Answer: Because a bar that gapped above the level never offered it, so the Low is the closest fill a daily bar can justify

It is an honesty guard. AmiBroker would clamp a price outside the bar range anyway, but writing the guard makes the assumption visible in the formula instead of hidden in the engine.

Sources for this lesson

5 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — Back-testing your trading ideasamibroker.com/guide/h_backtest.html2026-08-31
  2. 02AmiBroker User's Guide — Portfolio-level backtesting§ Resolving same-bar, same-symbol signal conflictsamibroker.com/guide/h_portfolio.html2026-08-31
  3. 03AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
  4. 04AFL Function Reference — SetBacktestModeamibroker.com/guide/afl/setbacktestmode.html2026-08-31
  5. 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.