Skip to content
Level 3 · AFL DeveloperLessonPart 08 · page 5 of 935 min
35Minutes
8AFL functions
7Sources
StandardRequires
AFL functions taught here8

The Array Model: The Most Important Lesson in This Course

Here is the sentence this entire course is built on. One line of AFL performs its calculation once for every bar on the chart, and gives you back all of the answers.

Not one answer. All of them. When you wrote Average = MA( Close, 50 ); in the project lesson, you did not compute a moving average. You computed several thousand moving averages — one as it stood at each bar in the history — and stored the whole set in Average.

If that idea is already comfortable, this lesson will still be worth thirty-five minutes, because AFL’s specific version of it has details that bite. If it is not comfortable yet, read this lesson twice. Every formula in the remaining twenty-eight parts of the course assumes it, and no amount of syntax knowledge compensates for getting it wrong.

The comparison AmiBroker’s own documentation reaches for

Section titled “The comparison AmiBroker’s own documentation reaches for”

The official guide compares AFL to a spreadsheet, and the comparison is exact enough to lean on.

In a spreadsheet you do not write a program that walks down a column. You type one formula into one cell — =(B2+C2)/2 — and fill it down. The formula is written once and evaluated once per row, and what you get back is a whole new column. Nobody thinks of that as a loop. Nobody worries about which row is processed first. You think about the relationship between columns, and the spreadsheet takes care of applying it everywhere.

AFL is that, with the fill-down already done for you. MidPrice = ( High + Low ) / 2; is the formula; the whole column is the result; and there is no fill-down step because AFL has no concept of a single row to begin with.

Every value in an AFL formula has one of two shapes, and almost every confusion in this part of the course comes from mistaking one for the other.

A scalar is a single number. 50 is a scalar. MaPeriod after MaPeriod = 50; is a scalar. BarCount is a scalar. So is anything a function returns when its documentation says it returns NUMBER.

An array is an ordered list of numbers — one per bar — with a value for every bar the formula can see. Close is an array. MA( Close, 50 ) is an array. Close > MA( Close, 50 ) is an array. So is anything a function returns when its documentation says it returns ARRAY.

That word “ARRAY” in capitals on every page of the AFL Function Reference is not decoration. It is the single most useful piece of information on the page, and getting into the habit of reading it will save you more time than anything else in this part.

Your database stores exactly six arrays per symbol: open, high, low, close, volume and open interest. That is all. Everything else — every moving average, every oscillator, every condition you will ever write — is calculated on demand from those six and then thrown away.

You refer to them by name, and AFL accepts both the long and the short form. Because identifiers are case-insensitive, Close, close and C are all the same thing.

Long name Short name What it holds
Open O the opening price of each bar
High H the highest price traded in each bar
Low L the lowest price traded in each bar
Close C the closing price of each bar
Volume V the volume traded in each bar
OpenInt OI open interest, where the instrument has any
Avg (none) ( High + Low + Close ) / 3, the so-called typical price

Avg is the exception in two ways: it is derived rather than stored, and it has no abbreviation. The rest are the raw material.

Here are ten bars of a real symbol, laid out as arrays. These are the numbers from AmiBroker’s own worked example, and every diagram in this lesson uses them, so you can carry your understanding from one picture to the next.

Four of the six stored arrays, ten bars

Bar 0 is the oldest bar and bar 9 is the newest. Each column is one bar; each row is one array.
Bar0123456789
Open1.231.241.211.261.241.291.331.321.351.37
High1.241.271.251.291.251.291.351.351.371.29
Low1.201.211.191.201.211.241.301.281.311.27
Close1.231.261.241.281.251.251.311.301.321.28
Bar 0 is the oldest bar and bar 9 is the newest. Each column is one bar; each row is one array. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Notice the numbering, because it is a habit that has to be established early. Bar 0 is the oldest bar. The newest bar is the one with the highest number. Time runs left to right in these diagrams and top to bottom in the column picture. Users arriving from other platforms routinely assume the reverse and then write formulas that are backwards in time.

Now watch what an operator does. Take MidPrice = ( High + Low ) / 2;.

AFL does not interpret that expression once per bar. It takes the whole High array and the whole Low array and adds them in a single compiled step, producing a temporary array. Then it divides every element of that temporary array by two, also in a single step. The result is assigned to MidPrice.

( High + Low ) / 2, step by step

Two whole-array operations. The middle row exists only during evaluation and is then discarded.
Bar0123456789
High1.241.271.251.291.251.291.351.351.371.29
Low1.201.211.191.201.211.241.301.281.311.27
High + Lowtemporary2.442.482.442.492.462.532.652.632.682.46
( High + Low ) / 21.221.241.221.2451.231.2651.3251.3151.341.23
Two whole-array operations. The middle row exists only during evaluation and is then discarded. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Three things to take from that picture.

Operations are element-wise. Bar 0 is added to bar 0, bar 1 to bar 1, and so on. Nothing in High + Low ever mixes bars. When you want a value from a different bar you have to say so explicitly, with Ref(), and Part 9 is largely about the functions that do that.

Intermediate arrays are real. The High + Low row is not a fiction of the diagram; it is an actual temporary array that AmiBroker builds and then releases. This is why array code is fast, and it is also why a loop that rebuilds the same array on every iteration is catastrophically slow — we come back to that below.

Lengths always match. Both inputs have ten elements, so the output has ten. All arrays in one formula run have the same length. There is no such thing as a short array in AFL.

You have already written MA( Close, MaPeriod ) where MaPeriod was a plain number, and Close - Average where both sides were arrays. What about mixing them?

The official wording is that when a number is combined with an array, the number “works as if it spanned all array elements”. A single number is stretched to the full length of the array and then the operation proceeds element-wise as usual. This is called scalar promotion, and it is what makes expressions like Close * 1.02 or Volume > 100000 work.

Here it is with a number that came from the data. BeginValue() takes an array and returns the single value at the start of the selected range — one number, not a column.

An array compared with one number

The single number behaves as though it were repeated across every bar, and the comparison then proceeds element-wise.
Bar0123456789
Open1.231.241.211.261.241.291.331.321.351.37
BeginValue( Open )one number1.241.241.241.241.241.241.241.241.241.24
Close1.221.261.231.281.251.251.311.301.321.28
Close <= BeginValue( Open )1010000000
The single number behaves as though it were repeated across every bar, and the comparison then proceeds element-wise. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Four functions collapse an array to a single number, and they are worth knowing now because they are the usual source of an accidental scalar:

  • LastValue( array ) — the value at the very last bar.
  • SelectedValue( array ) — the value at the bar the vertical selection line is on.
  • BeginValue( array ) and EndValue( array ) — the values at the start and end of the marked From-To range.

Arrays built from arrays built from arrays

Section titled “Arrays built from arrays built from arrays”

Real formulas are chains. Each line takes one or more arrays and produces another, and the whole chain is evaluated column by column. Here is AmiBroker’s own worked example, which takes five lines to build a pair of signal arrays:

Fragment — not a complete formula

Cond1 = Close < MA( Close, 3 );
Cond2 = Volume > Ref( Volume, -1 );
Buy = Cond1 AND Cond2;
Sell = High > 1.30;

Five arrays, each derived from the ones above it

Every row is an array of the same length. The values in this table are AmiBroker's own, from the Understanding how AFL works chapter.
Bar0123456789
Close1.231.261.241.281.251.251.311.301.321.28
Volume831030215325283414325666784755567493456
Ref( Volume, -1 )shiftedNull83103021532528341432566678475556749
MA( Close, 3 )NullNull1.2431.2601.2571.2601.2701.2871.3101.300
Cond1 = Close < MA( Close, 3 )NullNull10110001
Cond2 = Volume > Ref( Volume, -1 )Null010011010
Buy = Cond1 AND Cond2NullNull10010000
Sell = High > 1.300000001110
Every row is an array of the same length. The values in this table are AmiBroker's own, from the Understanding how AFL works chapter. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Spend a minute on that picture. It contains, in miniature, most of what a trading system is.

Ref( Volume, -1 ) is the volume array shifted one bar forward in time, so that each bar can see the previous bar’s volume alongside its own. Bar 0 has no previous bar, so its value is empty. MA( Close, 3 ) needs three closes before it can produce anything, so its first two values are empty. Those empty values are Null, and they are the subject of lesson eight — notice already that Cond1 and Cond2 are Null where their inputs were Null, and that Buy inherits the emptiness from both. It is not false there. It is unknown.

Sell = High > 1.30; shows scalar promotion again: an array on the left, one number on the right, one true-or-false answer per bar.

And Buy and Sell are just arrays. There is nothing magic about their names except that the Analysis window knows to look for them. Everything you do for the rest of this course is building arrays and giving some of them names AmiBroker recognises.

BarCount is a single number: how many bars the arrays hold in this particular execution. Valid positions in an array run from 0 to BarCount - 1.

The important word in that sentence is this particular execution. BarCount does not change while a formula runs, but it changes freely between runs:

  • new bars arrive, so a daily database grows by one most days;
  • you change the chart’s zoom, and AmiBroker may calculate only the visible portion;
  • you switch to a symbol with a different amount of history;
  • you run the formula in the Analysis window over a date range rather than all quotations;
  • AmiBroker syntax-checks your formula, using at most a couple of hundred recent bars;
  • the debugger runs it, defaulting to 200 bars.

The official Knowledge Base article on this is emphatic: a formula “should be written so it is able to execute without errors with BarCount as small as 1 (ONE)”. Hard-coding a bar count, or assuming there are at least N bars, produces a formula that works on your test chart and fails somewhere else.

There is a companion you will meet constantly, and confusing the two is mistake number six on AmiBroker’s official list:

  • BarCount is a NUMBER — the count of bars.
  • BarIndex() is an ARRAY — the zero-based number of each bar: 0, 1, 2, 3, and so on.

BarIndex() is genuinely useful. BarIndex() >= 200 is a Boolean array that is false over the first two hundred bars and true afterwards, which is how lesson eight builds a warm-up guard. But you cannot use it to control a loop, because a loop needs a single number to compare against and BarIndex() is a whole column.

BarCount is one number; BarIndex() is an array

BarCount is shown filled across the row only to make the shape difference visible. It is a single value, not a column.
Bar0123456789
BarIndex()0123456789
BarCountone number10101010101010101010
BarIndex() >= 50000011111
BarCount is shown filled across the row only to make the shape difference visible. It is a single value, not a column.

Occasionally you do want one specific element, and the subscript operator [ ] provides it:

Fragment — not a complete formula

FirstClose = Close[ 0 ]; // the oldest bar in the current array
LatestClose = Close[ BarCount - 1 ]; // the newest bar

Both of those are scalars. Two cautions come with the syntax. Going outside 0 to BarCount - 1 is Error 10, and a Null used as a subscript is Error 51. And Close[ 0 ] is not necessarily the first bar in your database — under AmiBroker’s QuickAFL optimisation it is the first bar of the array currently in use, which may be far more recent. Part 36 covers QuickAFL in detail; for now, treat Close[ 0 ] as “the start of what I can see” rather than “the start of history”.

Every language you may have met before would express the midpoint calculation as a loop: for each bar, take the high, take the low, add them, halve them, store the answer. AFL can do that too, and the syntax exists precisely because some calculations need it.

Fragment — not a complete formula

// This works. It is also the wrong tool for this job.
for( i = 0; i < BarCount; i++ )
{
MidPrice[ i ] = ( High[ i ] + Low[ i ] ) / 2;
}

The official performance chapter measures the difference and does not mince words: “Poor formula coding is the foremost reason for slowdown”, and loops are between ten and fifty times slower than the equivalent array code. Its published measurement is for exactly the calculation above:

Bars Loop version Array version Ratio
350,000 100 ms 2 ms 50x
300 0.1 ms 0.01 ms 10x

Two milliseconds against a hundred sounds trivial until you multiply it. An exploration over two thousand symbols, or an optimisation running the formula five thousand times, turns a fifty-fold penalty into the difference between a coffee break and an afternoon.

The reason is structural rather than mysterious. ( High + Low ) / 2 performs two operations at compiled speed over a whole block of memory. The loop version performs BarCount iterations of interpreted code, with the indexing, the loop test and the increment paid for on every one of them.

Loops are not forbidden, and lesson seven says exactly when they are justified: when a bar’s value depends on a value that the same calculation produced for the previous bar, and no built-in function expresses the relationship. That is a real category — it is why AMA() and the recursive averages exist — but it is much smaller than beginners assume. Nearly every “for each bar, if this then that” you can describe in English has a direct array expression.

The habit to build is translation. When you catch yourself thinking “for each bar, I want to…”, stop and ask what column of numbers you actually want. Most of the time it already has a name.

What you were about to loop for The array expression
For each bar, is the close above the 50-bar average? Close > MA( Close, 50 )
For each bar, what was the close one bar earlier? Ref( Close, -1 )
For each bar, how far is the close above its average, in percent? 100 * ( Close - MA( Close, 50 ) ) / MA( Close, 50 )
For each bar, pick the high in an up bar and the low in a down bar IIf( Close > Open, High, Low )
For each bar, is it both trending and liquid? Trend AND Liquid
For each bar, what bar number is this? BarIndex()

Three rules of thumb make the translation easier.

Name every step. A chain of five short named arrays is easier to read, easier to comment and — crucially — easier to debug than one long expression, because you can plot any step and see where the answer stopped being right.

Say the shape out loud. Before writing a line, decide whether the result is a column or a number. If you cannot say which, look up the function; the reference states it in capital letters at the top of every page.

Be suspicious of any function that returns NUMBER. Not because they are bad, but because mixing one into a per-bar rule by accident is the quiet failure described above. LastValue() in a Buy line deserves a comment justifying itself.

Reading tables convinces nobody. This short formula puts four arrays on one chart and prints the value of each at whichever bar you click, so you can watch a whole row of numbers change together.

Complete runnable AFL

see-the-arrays.afl
// see-the-arrays.afl
// Part 8 - The Array Model
//
// Purpose: make the array model visible. Every line below produces a whole
// column of numbers - one value for every bar - and the title
// reads those columns back at whichever bar you select.
// Assumes: any symbol, any interval. A deliberately short average period is
// used so the empty warm-up bars are easy to find.
// Apply: Formula Editor -> Apply indicator, then click along the chart and
// watch every number in the title change together.
_SECTION_BEGIN( "See The Arrays" );
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------
AvgPeriod = 3; // small on purpose: the warm-up is then only two bars long
// ---------------------------------------------------------------------------
// Four arrays, built one from another
// ---------------------------------------------------------------------------
// High and Low are built-in arrays. Adding them produces a temporary array of
// the same length; dividing that by 2 produces another. One line, two whole
// array operations, no loop anywhere.
MidPrice = ( High + Low ) / 2;
// MA() reads one array and returns another of exactly the same length.
Average = MA( Close, AvgPeriod );
// Element-wise subtraction: bar 0 minus bar 0, bar 1 minus bar 1, and so on.
// Where Average is empty the difference is empty too - that is Null spreading.
Gap = Close - Average;
// ---------------------------------------------------------------------------
// Drawing
// ---------------------------------------------------------------------------
Plot( Close, "Close", colorDefault, styleCandle );
Plot( MidPrice, "(High+Low)/2", colorOrange, styleLine );
Plot( Average, "MA(Close," + AvgPeriod + ")", colorBlue, styleLine | styleThick );
// Gap is drawn on its own scale so that a small difference in price does not
// have to share an axis with the price itself.
Plot( Gap, "Close - MA", colorGrey40, styleHistogram | styleOwnScale | styleNoLabel );
// ---------------------------------------------------------------------------
// Reading the arrays back
// ---------------------------------------------------------------------------
// BarCount is a single number: how many bars the arrays hold.
// BarIndex() is an array: the zero-based number of each bar.
// SelectedValue() collapses an array to the one value at the selected bar.
Title = StrFormat(
"{{NAME}} {{DATE}} bar %g of %g Close %g (H+L)/2 %g MA %g Close-MA %g",
SelectedValue( BarIndex() ), BarCount, Close, MidPrice, Average, Gap );
_SECTION_END();

Download see-the-arrays.afl59 lines

How it works. Three calculation lines, each producing a whole array: MidPrice from two built-in arrays, Average from one built-in array and a period, and Gap from one built-in and one derived array. Four Plot() calls draw them; the last uses styleOwnScale so that a difference measured in fractions of a currency unit does not have to share an axis with the price itself. The Title line then reads one value out of each array — at the bar you have selected — plus two scalars: BarCount, and SelectedValue( BarIndex() ), which is the number of the selected bar.

Key functions. SelectedValue( array ) collapses an array to the value at the selected bar. BarIndex() returns the per-bar number as an array. BarCount is not a function at all but a number the language supplies. StrFormat() substitutes values into a template, using the selected value whenever it is handed an array.

Test it. Pick any bar in the middle of the chart and check (H+L)/2 by hand from the high and low shown in AmiBroker’s own data tooltip — they must agree exactly. Then check that the bar number of the last bar is BarCount - 1, not BarCount. If either fails, you are not looking at the bar you think you are.

Common errors. If the grey histogram flattens the price into a line, the styleOwnScale flag was lost from the fourth Plot(). If the title shows the same numbers whichever bar you click, you have replaced SelectedValue() with LastValue() somewhere. If nothing appears at all, check that the pane you are watching is the selected one.

Extension. Add a line computing Range = High - Low; and plot it too, then satisfy yourself that Range is Gap-like in shape: a column of numbers, one per bar, derived from two other columns. Then try Ref( Close, -1 ) as a fifth row and confirm that its value at bar n equals Close at bar n − 1 — the shift that Part 9 builds on.

An AFL value is either a single number or a column of numbers with one entry per bar. The database supplies six such columns; everything else is derived from them on demand. Operators and functions work element-wise on whole columns at once, producing temporary columns along the way, and a plain number combined with a column behaves as though it had been repeated across every bar. Columns in one formula run all have the same length, BarCount, and that length is a property of the current execution rather than a fact about the symbol.

Loops exist and are occasionally necessary, but the array form of the same calculation is between ten and fifty times faster, and calling an array function inside a loop multiplies the work by the number of bars. The habit worth building is translation: when you think “for each bar…”, find the column instead.

The next lesson takes the most important special case — comparisons — and looks at it closely, because a comparison in AFL does not answer a question. It answers it once per bar.

Check your understanding

Question 1. After this line runs on a chart with 4,000 bars, what does Average contain?
Average = MA( Close, 50 );
Show the answer and why

Answer: An array of 4,000 values, each the 50-bar average as it stood at that bar

Every array in one formula run has the same length, so the result has 4,000 entries. The first 49 or so of them are empty rather than missing — the array is full length, but the early values are Null because a 50-bar average has nothing to average yet.

Question 2. Why is Buy = Close > LastValue( MA( Close, 50 ) ); a broken rule even though it compiles and produces signals?
Show the answer and why

Answer: It compares every bar in history against a single number taken from the end of the chart

LastValue() collapses the array to one number, which scalar promotion then stretches back across every bar. The rule judges a bar from 2011 against a value computed from data up to 2026. It is a look-ahead bug that produces no error message, which is exactly what makes it dangerous.

Question 3. Which of these are true about BarCount? Select all that apply.
Show the answer and why

Answer: It is a single number, not an array, It can differ between two runs of the same formula on the same symbol, The last valid array position is BarCount - 1

Zoom level, date range, syntax checking and the debugger can all change how many bars a formula sees. The official guidance is that a formula must survive BarCount as small as one. That is why hard-coded bar counts, and assumptions about minimum history, are a recurring source of Error 10.

Question 4. You need the midpoint of every bar. Which is the better implementation, and why?
Show the answer and why

Answer: The array expression ( High + Low ) / 2, because it is between ten and fifty times faster and expresses the same thing

AmiBroker's own performance chapter measures this exact calculation: on 350,000 bars the loop takes 100 ms against 2 ms for the array form. The array version is also shorter and has no index arithmetic to get wrong. Loops earn their place only when a bar's value depends on the previous bar's result and no built-in function expresses the relationship.

Question 5. In an AFL array, which bar is at position 0?
Show the answer and why

Answer: The oldest bar in the array

Indices are zero-based with bar 0 the oldest and BarCount - 1 the newest. The qualifier matters: under QuickAFL, position 0 is the oldest bar of the array currently in use, which is not necessarily the oldest bar in your database.

Sources for this lesson

7 verified · checked 2026-09-01

  1. 01AmiBroker User's Guide — Understanding how AFL worksamibroker.com/guide/h_understandafl.html2026-08-31
  2. 02AmiBroker User's Guide — AFL Reference Manual§ Built-in price arrays and the subscript operatoramibroker.com/guide/a_language.html2026-08-31
  3. 03AmiBroker User's Guide — Performance tuningamibroker.com/guide/x_performance.html2026-08-31
  4. 04AmiBroker User's Guide — Common Coding Mistakesamibroker.com/guide/a_mistakes.html2026-08-31
  5. 05AFL Function Reference — BarIndexamibroker.com/guide/afl/barindex.html2026-08-31
  6. 06AFL Function Reference — LastValueamibroker.com/guide/afl/lastvalue.html2026-08-31
  7. 07AmiBroker Knowledge Base — Do NOT make assumptions on number of barsamibroker.com/kb/2014/09/22/do-not-make-assumptions-on-number-of-bars2026-09-01

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.