Skip to content
Level 3 · AFL DeveloperProjectPart 10 · page 5 of 845 min
45Minutes
17AFL functions
8Sources
StandardRequires
AFL functions taught here17

Project: Configurable Moving-Average Indicator

AmiBroker ships with a moving average. So does every other package, and so does every website that has ever published a chart. This project builds one anyway, because the version you build answers a question the built-in one cannot: how much does the choice of averaging method actually change what I am looking at?

Produce a single chart formula that:

  • plots a moving average of a user-selected price field;
  • offers six averaging methods — simple, exponential, weighted, double exponential, triple exponential and Wilder’s — selected from a drop-down rather than by editing code;
  • optionally colours the line by its own slope;
  • optionally plots a second average of the same type at a different period, and marks the bars where the two cross;
  • refuses to draw any of them until the underlying calculation has seen a full window of data;
  • reports its own numbers in the title.

The last two points are what make it worth keeping rather than a toy.

The four lessons of this part. From earlier parts you need IIf(), Ref(), Cross() and ExRem(), and you should have met user-defined functions in Part 8. Any database with a few years of daily bars will do; nothing here needs a live feed or the Professional edition.

This is the design decision that separates the tool from the exercise, so it is worth establishing before the code.

MA( Close, 3 ) is documented as Null on the first two bars and only produces a number from bar 2 onward. That is correct and helpful: the chart simply starts later, and the reader is never shown an average of one and a half bars.

DEMA does not behave that way. Its official page states that DEMA[0] is initialised with the first value of the input array, so it returns a number on bar 0 — a “200-period double exponential average” that has seen exactly one price. EMA is different again: it is seeded from a simple moving average of equivalent length. And the same page notes that a hand-rolled 2*EMA - EMA(EMA) and the built-in DEMA do not converge until roughly 6*len bars from the start of the data.

What the first bars look like, by method

MA tells you when it is not ready. DEMA does not. Blanking the first `periods` bars makes every method admit it.
Bar01234
Close10.011.012.011.013.0
MA( Close, 3 )NullNull11.011.312.0
DEMA( Close, 3 )numbernumbernumbernumbernumber
Blanked in this projectNullNullNullvaluevalue
MA tells you when it is not ready. DEMA does not. Blanking the first `periods` bars makes every method admit it. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Complete runnable AFL

configurable-ma.afl
/*
* Configurable moving average - Part 10 project.
*
* What it does
* Plots one, and optionally two, moving averages of a price field you pick,
* using an averaging method you pick, with an optional slope colouring and
* optional markers on the bars where the two averages cross.
*
* Assumptions and limits
* - Chart pane formula. It produces no Buy/Sell signals and does no trading.
* - Warm-up bars are blanked on purpose. MA returns Null for its first
* periods-1 bars, but DEMA is seeded from the first data point and so
* returns numbers immediately; without blanking, the early values of the
* lag-reduced averages look valid when they are not.
* - A moving average is an average of past prices. Its level and its
* direction describe where price has been. They are not a forecast.
* - Averaging methods differ in how they are seeded, so two methods can
* disagree for hundreds of bars at the start of a symbol's history.
*/
_SECTION_BEGIN( "Configurable MA" );
MaTypeName = ParamList( "Average type",
"Simple|Exponential|Weighted|Double exponential|Triple exponential|Wilders",
0 );
Source = ParamField( "Price field", 3 );
MaPeriod = Param( "Periods", 50, 2, 400, 1, 10 );
MaTint = ParamColor( "Colour", colorCycle );
MaStyleBits = ParamStyle( "Style", styleLine | styleThick, maskDefault );
ColourBySlope = ParamToggle( "Colour by slope", "No|Yes", 0 );
RisingTint = ParamColor( "Rising colour", colorBrightGreen );
FallingTint = ParamColor( "Falling colour", colorRed );
SlopeBars = Param( "Slope measured over (bars)", 3, 1, 50, 1 );
ShowSecond = ParamToggle( "Second average", "No|Yes", 1 );
SecondPeriod = Param( "Second periods", 200, 2, 600, 1 );
SecondTint = ParamColor( "Second colour", colorBlueGrey );
MarkCrosses = ParamToggle( "Mark crosses", "No|Yes", 1 );
/*
* Returns the chosen average. ParamList hands back the label as text, so the
* choice is made by string comparison. Anything unrecognised falls back to the
* simple average rather than leaving the result Null and quietly emptying the
* chart. AFL requires the single return to be the last statement in the body.
*/
function AverageOf( DataSeries, Periods, TypeName )
{
local Result;
if( TypeName == "Exponential" ) Result = EMA( DataSeries, Periods );
else if( TypeName == "Weighted" ) Result = WMA( DataSeries, Periods );
else if( TypeName == "Double exponential" ) Result = DEMA( DataSeries, Periods );
else if( TypeName == "Triple exponential" ) Result = TEMA( DataSeries, Periods );
else if( TypeName == "Wilders" ) Result = Wilders( DataSeries, Periods );
else Result = MA( DataSeries, Periods );
return Result;
}
/*
* Hides the bars before the average has seen a full window of data, so that
* every method starts drawing at the same bar and none of them shows a value
* that is really an artefact of its own seeding.
*/
function BlankWarmUp( DataSeries, Periods )
{
local Result;
Result = IIf( BarIndex() >= Periods, DataSeries, Null );
return Result;
}
Primary = BlankWarmUp( AverageOf( Source, MaPeriod, MaTypeName ), MaPeriod );
Plot( Close, "Price", colorDefault,
styleCandle | styleNoTitle | GetPriceStyle() );
// A per-bar colour array turns one plot into a two-colour plot. The comparison
// is against the average's own value SlopeBars ago, not against price, so the
// colour reports the average's direction rather than the market's.
Slope = Primary - Ref( Primary, -SlopeBars );
if( ColourBySlope ) LineTint = IIf( Slope > 0, RisingTint, FallingTint );
else LineTint = MaTint;
Plot( Primary, MaTypeName + " " + NumToStr( MaPeriod, 1.0 ),
LineTint, MaStyleBits );
if( ShowSecond )
{
GraphXSpace = 8;
Secondary = BlankWarmUp( AverageOf( Source, SecondPeriod, MaTypeName ), SecondPeriod );
Plot( Secondary, MaTypeName + " " + NumToStr( SecondPeriod, 1.0 ),
SecondTint, styleLine );
if( MarkCrosses )
{
// ExRem keeps one marker per change of side when the two averages sit
// on top of each other and cross repeatedly.
CrossUp = ExRem( Cross( Primary, Secondary ), Cross( Secondary, Primary ) );
CrossDown = ExRem( Cross( Secondary, Primary ), Cross( Primary, Secondary ) );
// Anchored to the average itself, with a zero pixel offset, so the
// marker sits exactly on the crossing point.
PlotShapes( IIf( CrossUp, shapeSmallCircle, shapeNone ),
RisingTint, 0, Primary, 0 );
PlotShapes( IIf( CrossDown, shapeSmallCircle, shapeNone ),
FallingTint, 0, Primary, 0 );
}
}
_N( Title = StrFormat(
"%s %s(%g) = %g change over %g bars = %g (an average of past prices, not a forecast)",
Name(), MaTypeName, MaPeriod,
SelectedValue( Primary ), SlopeBars, SelectedValue( Slope ) ) );
_SECTION_END();

Download configurable-ma.afl121 lines

Every Param* call sits at the top, so the whole adjustable surface of the tool is visible in one screen. MaTypeName is a ParamList and therefore a string; Source is a ParamField and therefore an array; the toggles are numbers; the colours and the style are numbers. Those four different return types all appear within ten lines, which is why the previous lesson spent so long on them.

Source uses field 3 (Close) as its default rather than −1, because this formula is normally dropped on a price pane. Change it to −1 and the average will chain onto whatever indicator is already in the pane, which is how you build an average of an oscillator.

The user-defined function turns a text label into a series. Three details are deliberate.

There is exactly one return, and it is the last statement in the body. AFL does not support early returns — the official documentation says a return statement must be placed at the very end of the function — so the if/else chain assigns to a local and the function returns it.

local Result; is not strictly required, because a name first assigned inside a function is local by default. Writing it makes the intent explicit and protects against a global of the same name existing elsewhere in a longer file.

The final else is a fallback to the simple average rather than an error. If the labels in the ParamList string and the labels in the comparisons ever drift apart, the tool draws something recognisable instead of an empty pane, and the title tells you which method it thinks it is using.

Fragment — not a complete formula

Result = IIf( BarIndex() >= Periods, DataSeries, Null );

BarIndex() returns the zero-based bar number, so this replaces the first Periods values with Null and leaves the rest alone. Every method now starts drawing on the same bar, and none of them shows a value derived from fewer bars than its own period.

This is a floor, not a guarantee of accuracy. EMA, DEMA, TEMA and Wilders are all recursive, so their values remain influenced by their seeding well past the first non-null bar. Blanking removes the most misleading part of the curve; it does not make the rest exact.

Fragment — not a complete formula

Slope = Primary - Ref( Primary, -SlopeBars );
LineTint = IIf( Slope > 0, RisingTint, FallingTint );

Ref( array, -n ) reads the value n bars ago, so Slope is the average’s own change over the lookback. The comparison produces a Boolean array, IIf() turns it into a colour array, and the colour array goes into Plot()’s third argument.

Note what is being coloured: the average’s direction, not price’s. Those are different statements, and conflating them is how a rising line gets read as a rising market when price has already turned.

The second average uses the same method, which is the point — comparing a 50-period and a 200-period simple average tells you about the period; comparing a 50-period simple and a 50-period triple exponential tells you about the method. Mixing both at once tells you neither.

ExRem() keeps one marker per change of side. The markers are anchored to Primary with a zero pixel offset, so each circle sits exactly on the crossing point rather than floating near it.

StrFormat() assembles the line with %s for the method name and %g for the numbers, and the whole assignment is wrapped in _N(). The trailing clause — “an average of past prices, not a forecast” — is in the tool rather than in the documentation on purpose. Tools get shared; documentation does not travel with them.

Apply the formula to the price pane of a liquid daily chart with at least three years of data.

A thick coloured line following price with a visible lag, a thinner grey line following it more slowly, and small circles where the two touch. Nothing is drawn in the first 50 bars of the loaded range for the fast line and nothing in the first 200 for the slow one, so the left edge of the chart shows price alone. The title reports the method, the period, the current value and the change over the slope lookback.

Switch Average type from Simple to Triple exponential. The line should hug price noticeably more closely and turn earlier at reversals — and it should also change direction more often in sideways stretches. That trade-off is the whole subject of moving-average choice, and now you can see both halves of it in one click.

A test that cannot fail is not a test. Each of these can.

  1. Degenerate period. Set Periods to 2. Every method should sit almost on top of price. If one of them does not, that method is not doing what you think it is.
  2. Method equivalence at the right edge. Set both averages to the same period with Second average on. The two lines must coincide exactly, because they are the same calculation. If they do not, the second call is not receiving the same arguments.
  3. Warm-up. Set Periods to 200 and look at the left edge. There must be no line at all for the first 200 bars. Then comment out the BlankWarmUp call for the primary average, choose Double exponential, and refresh: a line now appears from the very first bar. That line is the artefact the function exists to remove.
  4. Slope colouring. Set Slope measured over to 1 and watch the line change colour on almost every bar in a choppy stretch. Set it to 20 and watch the colour become stable. The parameter is a smoothing decision disguised as a display option, which is worth knowing before you trust the colour.
  5. Cross markers against the arithmetic. Pick one marked crossing. In the Data window, confirm that on the marked bar the fast value is above the slow one, and on the previous bar it was not. That is the definition of Cross(); anything else is a bug.
  6. Second pane independence. Open the same formula in a second pane with different parameters. Both should keep their own settings, because parameter values are stored per Chart ID.
  • The chart is empty. Usually the ParamList labels and the if comparisons have drifted apart, and every case fell through to the fallback — check the title, which reports the label the tool received. If it is genuinely empty, the period may exceed the number of loaded bars, in which case BlankWarmUp correctly blanks everything.
  • “Error 33” or a complaint about the function name. You cannot assign to a function’s own identifier to return a value, and you cannot define a function whose name is already used by a global variable earlier in the file. Rename one of them.
  • The colour never changes. Colour by slope is off, or SlopeBars is large enough that the slope rarely changes sign over the visible range.
  • Circles appear in clusters. ExRem() was removed or the two averages are nearly identical, so they cross repeatedly. That is real behaviour, not a fault — but it is why the markers are filtered.
  • Everything works on one chart and not on another. Parameters are per pane. Check you are adjusting the pane you are looking at.
  • The averages disagree wildly at the start of a symbol’s history. Expected. Recursive averages carry their seed for a long time; the official documentation puts convergence between a hand-rolled DEMA and the built-in one at roughly six times the period.

A displaced average. Add a Param for a horizontal shift and pass it to Plot()’s XShift slot. Then answer the question that matters: does shifting the line right make the average look better because it is better, or because you have hidden the lag by moving the picture? The shift is documented as visual only — the underlying array is unchanged — so anything that looks improved is looking improved for cosmetic reasons.

An envelope. Add a percentage band above and below the average using two more plots. Compare a fixed-percentage band with one scaled by ATR( 20 ) and decide which is more stable across instruments with different volatilities.

Method disagreement as a series. Plot the difference between the simple and the triple exponential average of the same period, on its own scale, and look at when the two methods disagree most. The answer is usually “at turns”, which is precisely when the choice of method matters and precisely when it is hardest to evaluate.

You have a moving-average tool you would actually keep, and — more valuable — you have seen that “moving average” names a family whose members disagree, that some of them do not tell you when they are not ready, and that the fix is three lines of code you now write by habit.

Check your understanding

Question 1. Why does this project blank the first `Periods` bars of every average?
Show the answer and why

Answer: Because DEMA is seeded from the first data point and returns numbers immediately, so its warm-up values look valid when they are not

MA is documented as Null for its first periods-1 bars, but DEMA[0] is initialised with the first value of the input array. Blanking makes every method start at the same, defensible bar.

Question 2. What does this line produce?
LineTint = IIf( Primary > Ref( Primary, -3 ), colorGreen, colorRed );
Show the answer and why

Answer: One colour per bar, based on whether the average is higher than it was three bars ago

IIf over an array condition produces an array - here one colour value per bar. Note it compares the average with its own past, not with price.

Question 3. Where must the return statement go in an AFL user-defined function?
Show the answer and why

Answer: At the very end of the function body

The official documentation states that a return statement must currently be placed at the very end of the function. Early returns are not supported, which is why the if/else chain assigns to a local variable.

Question 4. The two averages are set to the same period and the same method, but the lines differ. What is the most likely cause?
Show the answer and why

Answer: One of the two calls is not receiving the same arguments as the other

Identical inputs to identical code must give identical output. A visible difference means the inputs are not identical - typically a different period, price field or method reaching the second call.

Sources for this lesson

8 verified · checked 2026-08-31

  1. 01AFL Function Reference — MAamibroker.com/guide/afl/ma.html2026-08-31
  2. 02AFL Function Reference — EMAamibroker.com/guide/afl/ema.html2026-08-31
  3. 03AFL Function Reference — DEMAamibroker.com/guide/afl/dema.html2026-08-31
  4. 04AFL Function Reference — TEMAamibroker.com/guide/afl/tema.html2026-08-31
  5. 05AFL Function Reference — Wildersamibroker.com/guide/afl/wilders.html2026-08-31
  6. 06AFL Function Reference — WMAamibroker.com/guide/afl/wma.html2026-08-31
  7. 07AmiBroker User's Guide — Understanding how AFL works§ Array processing and warm-upamibroker.com/guide/h_understandafl.html2026-08-31
  8. 08AmiBroker User's Guide — Drag-and-drop indicator buildingamibroker.com/guide/h_dragdrop.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.