From Rules to AFL
The gap between a rule and a trade is one bar wide, and almost every unrealistic backtest in existence lives in that gap. Your rule is computed from a closing price. The closing price is known only once the session has ended. Any simulation that then buys at that same close has placed an order using information that did not exist when the order would have had to be sent.
This lesson covers the AFL surface that closes the gap: the four signal variables, the four
trade price arrays, and SetTradeDelays(). It is a small surface — five or six lines — and
getting it right is worth more than any indicator you will ever add.
The four signal variables
Section titled “The four signal variables”AmiBroker’s backtester reads four reserved array variables. They are variables, not
functions: there is no Buy() page in the AFL Function Reference, because Buy is something
you assign to. The User’s Guide defines them as follows.
| Variable | Meaning |
|---|---|
Buy |
Enter a long position |
Sell |
Close a long position |
Short |
Enter a short position (short sell) |
Cover |
Close a short position (buy to cover) |
Two consequences follow immediately, and both catch people out.
Sell does not open a short. It closes a long. Before version 3.59 the same two variables
served both directions; now they are separate. If you assign only Buy and Sell, you have
specified a long-only system, and the backtester will simulate long trades only. That is a
perfectly good thing to want — it is what the project in this part does — but it should be a
choice you made rather than an omission you did not notice.
A stop-and-reverse system is written explicitly. The guide’s own idiom for “always in the market” is to assign the exit of one direction as the entry of the other:
Fragment — not a complete formula
Short = Sell;Cover = Buy;Each of the four is an array with one value per bar. A non-zero value on a bar is a signal on that bar. And here is the part that governs everything else in this lesson:
Redundant signals
Section titled “Redundant signals”If your entry condition stays true for eleven consecutive bars, Buy contains eleven 1s. In
AmiBroker’s default backtest mode, backtestRegular, redundant entry signals occurring after
the first entry and before the matching exit are removed exactly the way ExRem() removes
them. The other documented modes — backtestRegularRaw and its relatives — keep them, and
Part 28 explains when you would want that.
Because the removal happens inside the engine, a chart of your formula shows eleven arrows
while the report shows one trade. Applying ExRem() yourself makes the two agree:
Fragment — not a complete formula
Buy = ExRem( Buy, Sell );Sell = ExRem( Sell, Buy );This does not change the trades in the default mode. It changes what you can see, which is worth a line.
The four trade price arrays
Section titled “The four trade price arrays”BuyPrice, SellPrice, ShortPrice and CoverPrice say at what price each kind of order is
simulated as filled. AmiBroker pre-fills all four from the trade price fields in the Analysis
settings, so assigning them is optional — and, exactly because it is optional, the default is
frequently something nobody chose.
Fragment — not a complete formula
BuyPrice = Open;SellPrice = Open;Three documented behaviours matter here.
Price-bound checking. During backtesting AmiBroker checks whether the value you assigned
fits inside the bar’s high-low range, and adjusts it if not: up to the high if your value was
above the high, down to the low if it was below the low. This is what makes the guide’s
simulated-stop-order idiom work, and it is also why a slippage adjustment can be silently
truncated on a bar that opens at its own extreme. You can switch the checking off with
SetOption( "PriceBoundChecking", False ), which you should almost never do, because the
result is a fill at a price the instrument never traded at.
The price arrays are not shifted by trade delays. Only the four signal arrays are. This is the single most important sentence in the lesson and the next section is about it.
TickSize does not round these arrays. Per the guide, tick size affects only exits
generated by the built-in stops; the backtester assumes the prices you supply already respect
the instrument’s tick size.
SetTradeDelays(), exactly
Section titled “SetTradeDelays(), exactly”Fragment — not a complete formula
SetTradeDelays( buydelay, selldelay, shortdelay, coverdelay );Four arguments, in that order, all required. The function returns nothing and overrides the trade delays configured on the Settings page.
The AFL Function Reference is unusually explicit about the mechanism. Trade delays literally apply this, inside the backtester, after your formula has been executed but before trade simulation starts:
Fragment — not a complete formula
Buy = Ref( Buy, -buydelay );Sell = Ref( Sell, -selldelay );Short = Ref( Short, -shortdelay );Cover = Ref( Cover, -coverdelay );Ref( array, -1 ) returns the value from one bar earlier, so a signal that was on bar t
now sits on bar t+1. The documentation states that this is functionally equivalent to
putting those four lines at the end of your own formula.
What that produces
Section titled “What that produces”SetTradeDelays( 1, 1, 1, 1 ) on a buy signal
| Bar | Mon | Tue | Wed | Thu |
|---|---|---|---|---|
Close | 49.6 | 50.4 | 51.1 | 50.8 |
Open | 49.5 | 49.8 | 50.9 | 51.0 |
Buy (your formula) | 0 | 1 | 0 | 0 |
Buy (after delay, inside the engine) | 0 | 0 | 1 | 0 |
BuyPrice = Open (never shifted) | 49.5 | 49.8 | 50.9 | 51.0 |
The fill price is whatever BuyPrice holds on the bar the shifted signal lands on. That
is why BuyPrice = Open with a one-bar delay means “next bar’s open”, even though nothing in
the assignment mentions delay: the delay moved the signal, and the signal picks its price from
the bar it now sits on.
Signal on close, trade on next open
Section titled “Signal on close, trade on next open”This is the standard end-of-day configuration, and it is standard because it is the only one that corresponds to something a person with a daily database could actually do.
One trading decision, laid out in real time
In AFL that is three lines:
Fragment — not a complete formula
SetTradeDelays( 1, 1, 1, 1 );BuyPrice = Open;SellPrice = Open;What it assumes, and what you are therefore claiming when you publish a result from it:
- You could download and process the day’s data after the close and before the next open.
- Your order reached the market in time for the opening auction.
- You were filled in full, at the printed opening price, with no slippage. (That last one is the subject of the next lesson, and it is not true.)
- The overnight gap is taken as it comes. This is a feature: gap risk is real, and a fill-at-close backtest hides it entirely.
Why the delay is not optional
Section titled “Why the delay is not optional”Strictly, SetTradeDelays() is optional in the sense that the compiler does not require it —
if you leave it out, the backtester uses whatever the Settings page holds, which may be
whatever the last person to open that dialog left there. That is a good reason to write it
into every system formula regardless: a formula that states its own delays is reproducible on
somebody else’s installation, and one that does not is not.
The substantive reason is different. Consider the zero-delay, fill-at-close configuration:
Fragment — not a complete formula
// A rule computed FROM the close, filled AT the close, with no delay.SetTradeDelays( 0, 0, 0, 0 );Buy = Cross( Close, MA( Close, 50 ) );BuyPrice = Close;To take that trade, you would have to know the closing price before the close in order to compute the signal, and then trade at that same close. The information required to act arrives at the moment acting becomes impossible. It is not a small approximation; it is a circular one, and it flatters results systematically, because the very bars on which a rule fires are bars on which price moved.
There are configurations with zero delay that are defensible, and it is worth being precise rather than superstitious about it:
| Configuration | Defensible? | Why |
|---|---|---|
| Signal from bar t’s close, delay 1, fill at bar t+1’s open | Yes | Everything the signal uses precedes everything the fill touches |
| Signal from bar t-1’s data only, delay 0, fill at bar t’s open | Yes | The signal was computable before bar t began; the delay is in the formula rather than in the setting |
| Signal from bar t’s close, delay 0, fill at bar t’s close | No | Requires knowing the close before it exists |
| Signal from bar t’s close, delay 0, fill at bar t’s open | No | Worse: it fills at a price that had already printed hours before the signal |
| Stop or limit level set from bar t-1, delay 0, fill at that level on bar t | Yes, with care | The order rests in the market; the price array holds a level, not an observed close, and price-bound checking keeps it inside the bar |
Trading on the close is possible in real life — market-on-close orders exist. But then the signal must be computable before the close, which means computing it from the previous bar’s data or from an intraday snapshot taken before the auction. That is a different formula, not a different setting, and Part 23 deals with the intraday case.
A minimal complete system
Section titled “A minimal complete system”Assemble the smallest formula that is a system rather than an indicator: it enters, it exits, it says at what price, and it says how long after the signal. Nothing else. Getting this to run and produce a trade list you can reconcile by hand is the milestone; realism comes next lesson.
Complete formula
Section titled “Complete formula”Complete runnable AFL
// minimal-system.afl// Part 27 - From Rules to AFL//// The smallest formula that is a trading system rather than an indicator. It// says when to enter, when to exit, at what price a trade is simulated, and how// many bars after the signal the trade happens. Nothing else.//// It is deliberately incomplete as RESEARCH. There is no position sizing, no// commission, no slippage and no liquidity floor, so any figure it produces is// a description of a frictionless market that does not exist. Those pieces are// added in execution-assumptions.afl. Do not quote a number from this file.//// EXECUTION ASSUMPTIONS// Interval Daily bars, split- and dividend-adjusted.// Signal computed On the close of bar t, from data up to and including bar t.// Order placed After the close of bar t.// Fill The opening print of bar t+1, in full, at the printed open.// Delays SetTradeDelays( 1, 1, 1, 1 ) - one bar on all four signals.// Direction Long only. Short and Cover are left unassigned, so the// backtester simulates long trades only.// Costs NONE MODELLED.// Liquidity NOT CHECKED. The formula will happily "trade" a symbol// whose entire daily volume is smaller than one order.//// How to run it:// Formula Editor -> paste -> Tools -> Send to Analysis// Apply to: Current symbol, or a small watch list// Range: All quotations// Settings: Periodicity = Daily// Then press Backtest.
MaPeriod = 100;
TrendLine = MA( Close, MaPeriod );
// Entry and exit are EVENTS, not states. Cross() is true only on the bar the// relationship changed. Writing Buy = Close > TrendLine instead would be true// on every bar of an advance, which is a different rule producing different// trades - see the ambiguity discussion in the previous lesson.Buy = Cross( Close, TrendLine );Sell = Cross( TrendLine, Close );
// One bar of delay on every signal. SetTradeDelays shifts Buy/Sell/Short/Cover// inside the backtester after this formula has run. It shifts nothing else -// the price arrays below are NOT shifted - so BuyPrice = Open means "the open// of whichever bar the shifted signal lands on", which is the bar after the// signal bar. That is the whole point.SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open;SellPrice = Open;
// Chart section. Applying this same formula to a chart pane draws the rule and// the bars it fires on, which is how you check the logic by eye before you let// a report tell you anything. Note that the arrows sit on the SIGNAL bars, not// on the fill bars - the delay lives inside the backtester, not in the arrays._SECTION_BEGIN( "Minimal system" );
Plot( Close, "Close", colorDefault, styleCandle );Plot( TrendLine, "MA(" + MaPeriod + ")", colorBlue, styleLine | styleThick );
PlotShapes( shapeUpArrow * Buy, colorGreen, 0, Low, -20 );PlotShapes( shapeDownArrow * Sell, colorRed, 0, High, 20 );
_SECTION_END();How it works
Section titled “How it works”Four sections, in the order they have to happen.
The rule section computes one trend line and two events from it. Cross() gives 1 only on
the bar the relationship changed, which makes Buy and Sell events rather than states — the
distinction the previous lesson spent a table on.
The timing section is the three lines this lesson exists for. SetTradeDelays( 1, 1, 1, 1 )
shifts all four signal arrays by one bar inside the engine; BuyPrice and SellPrice are set
to Open, and because they are not shifted, they resolve to the open of the bar the shifted
signal landed on.
Short and Cover are left unassigned, which is what makes this long-only. There is a
comment saying so, because an unassigned variable is invisible and a reader cannot tell an
omission from a decision.
The chart section plots the rule and the signal bars. This is not decoration: being able to apply the same file to a chart and see the arrows is how you check the logic against something visible before you let a report talk to you.
Key functions
Section titled “Key functions”Cross( ARRAY1, ARRAY2 )— returns 1 on the barARRAY1crosses aboveARRAY2, 0 otherwise. For the downward cross, swap the arguments; there is no separate function.SetTradeDelays( buydelay, selldelay, shortdelay, coverdelay )— all four arguments required, applied inside the backtester asRef( Buy, -buydelay )and so on.Ref( array, period )— the value fromperiodbars away. Negative looks back, positive looks forward. A positive shift on anything the rule uses is look-ahead bias.PlotShapes( shape, color, layer, yposition, offset )— draws arrows. Multiplying a shape constant by a Boolean array plots it only on the bars where the array is true, becauseshapeof zero plots nothing.
Expected result
Section titled “Expected result”Applied to a chart pane, a candlestick chart with a blue moving average and alternating green and red arrows at the crossings. Applied in the Analysis window with Apply to set to Current symbol, Range set to All quotations and Periodicity Daily, pressing Backtest produces a trade list in which:
- entries alternate strictly with exits — never two entries in a row;
- every trade’s entry date is one bar after a green arrow on the chart;
- every entry price equals that bar’s opening price, to the cent.
Test it
Section titled “Test it”Run signal-fill-audit.afl on the same symbol and the same range:
Complete runnable AFL
// signal-fill-audit.afl// Part 27 - From Rules to AFL//// An Exploration that shows, side by side, the bar a signal was generated on// and the bar the backtester will actually trade on when SetTradeDelays( 1, ... )// is in force. Run it on the same symbol and the same range as your backtest and// reconcile a handful of rows against the trade list by hand.//// WHY THIS EXISTS// Trade delays are applied inside the backtester, after your formula has// finished. Nothing on your chart moves, and nothing in the Buy array moves.// So the only way to see the delay is to reproduce it yourself and compare.// That is exactly what the FillDate/FillPrice columns below do: they apply// Ref( ..., -1 ) - the documented mechanism of SetTradeDelays - and show you// the result next to the untouched signal.//// ASSUMPTIONS// Interval Daily bars, split- and dividend-adjusted.// Delay One bar, matching SetTradeDelays( 1, 1, 1, 1 ).// Fill price The opening print of the bar after the signal bar.// Costs Not modelled here. This page audits timing, nothing else.//// How to run it:// Formula Editor -> paste -> Tools -> Send to Analysis// Apply to: Current symbol// Range: All quotations, or the same range as the backtest you are checking// Then press Explore.
MaPeriod = 100;Delay = 1; // must match the first argument of SetTradeDelays in the system
TrendLine = MA( Close, MaPeriod );
SignalBuy = Cross( Close, TrendLine );SignalSell = Cross( TrendLine, Close );
// What the backtester does to the signal arrays, reproduced in the open so it// can be inspected: a signal on bar t is acted upon on bar t + Delay.DelayedBuy = Ref( SignalBuy, -Delay );DelayedSell = Ref( SignalSell, -Delay );
// On a bar where a delayed signal lands, these two answer "what did the rule// see?" and "what would we have paid?". They are different bars on purpose.SignalBarClose = Ref( Close, -Delay );SignalBarDate = Ref( DateTime(), -Delay );
Filter = DelayedBuy OR DelayedSell;
// Two 1/0 columns rather than one text column: AddTextColumn is documented as// taking a single string for the whole exploration, so it cannot label rows// that differ bar by bar.AddColumn( DelayedBuy, "Buy fills here", 1.0 );AddColumn( DelayedSell, "Sell fills here", 1.0 );AddColumn( SignalBarDate, "Signal bar", formatDateTimeISO );AddColumn( SignalBarClose, "Signal bar close", 1.2 );AddColumn( DateTime(), "Fill bar", formatDateTimeISO );AddColumn( Open, "Fill bar open", 1.2 );
// The gap between what the rule saw and what you would have paid. A large,// systematically negative number for buys is the overnight gap risk that a// signal-on-close, fill-on-next-open system carries and a fill-at-close// backtest quietly pretends away.AddColumn( 100 * ( Open - SignalBarClose ) / SignalBarClose, "Gap %", 1.2 );It reproduces the delay in the open, using the same Ref( ..., -1 ) the engine uses
internally, and prints the signal bar’s date and close next to the fill bar’s date and open.
Take three rows at random and find the corresponding trades in the backtest trade list. If the
dates and prices match, your delay is doing what you think. If the trade list shows entries on
the signal date, your formula is not applying the delay — check that SetTradeDelays() is
actually in the file and has not been shadowed by a Settings value.
The Gap % column is worth a minute of attention on its own. It is the overnight move between the price your rule saw and the price you would have paid, and its average over many signals is a cost that no commission setting captures.
Common errors
Section titled “Common errors”| Symptom | Cause |
|---|---|
| Entries on the same bar as the arrow | No delay in force. Trading a close computed from that close. |
| Every trade lasts exactly one bar | Sell is true on the same bars as Buy — usually a state condition where an event was intended. |
| Far more signals than trades | Redundant entries, removed by the default mode. Apply ExRem() to make the chart agree with the report. |
| Entry prices are not the bar’s open | BuyPrice never assigned, so the Settings trade price is in force. |
| An entry price does not match the printed open | Price-bound checking clamped it into the high-low range. |
No short trades despite a Short rule |
Cover never assigned; both are needed. |
Extension
Section titled “Extension”Change one thing and watch what happens: set the delays to SetTradeDelays( 0, 0, 0, 0 ),
leave BuyPrice = Open, and run the same test. Every trade now fills at the open of the bar
whose close produced the signal — a price that printed hours before the rule could have been
evaluated. Compare the two trade lists side by side. The difference between them, on any
reasonable data, is the size of the error people make without noticing.
Buy, Sell, Short and Cover are arrays you assign, not functions. Sell closes a long
and does not open a short; a long-only system is one where Short and Cover are simply
never assigned. BuyPrice and its three siblings set the fill price and are pre-filled from
the Settings dialog, so a formula that does not assign them inherits a decision you did not
make. AmiBroker clamps those prices into each bar’s high-low range.
SetTradeDelays( buydelay, selldelay, shortdelay, coverdelay ) shifts the four signal arrays
by applying Ref( array, -delay ) inside the engine after your formula has run, and shifts
nothing else — not the price arrays, not the position size, not the score. Signal on close and
trade on the next open is three lines, and it is the only end-of-day configuration in which
every input to the decision precedes every consequence of it.
Next: everything the fill price does not tell you — slippage, commission, liquidity limits and gaps through levels — assembled into an assumptions block you attach to every system you build from here on.
Check your understanding
Sources for this lesson
6 verified · checked 2026-08-31
- 01AmiBroker User's Guide — Back-testing your trading ideas§ Reserved variable names; Controlling trade priceamibroker.com/guide/h_backtest.html2026-08-31
- 02AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
- 03AFL Function Reference — Refamibroker.com/guide/afl/ref.html2026-08-31
- 04AFL Function Reference — Crossamibroker.com/guide/afl/cross.html2026-08-31
- 05AmiBroker User's Guide — Portfolio-level backtestingamibroker.com/guide/h_portfolio.html2026-08-31
- 06AFL 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.