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?
Objective
Section titled “Objective”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.
Prerequisites
Section titled “Prerequisites”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.
Why blanking the warm-up matters
Section titled “Why blanking the warm-up matters”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
| Bar | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
Close | 10.0 | 11.0 | 12.0 | 11.0 | 13.0 |
MA( Close, 3 ) | Null | Null | 11.0 | 11.3 | 12.0 |
DEMA( Close, 3 ) | number | number | number | number | number |
Blanked in this project | Null | Null | Null | value | value |
The complete formula
Section titled “The complete formula”Complete runnable 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();How it works
Section titled “How it works”The configurable surface
Section titled “The configurable surface”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.
AverageOf() — one switch, six methods
Section titled “AverageOf() — one switch, six methods”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.
BlankWarmUp() — the honesty function
Section titled “BlankWarmUp() — the honesty function”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.
Colour by slope
Section titled “Colour by slope”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 and the crosses
Section titled “The second average and the crosses”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.
The title
Section titled “The title”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.
What you should see
Section titled “What you should see”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.
Test it
Section titled “Test it”A test that cannot fail is not a test. Each of these can.
- 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.
- 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.
- 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
BlankWarmUpcall 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. - 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.
- 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. - 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.
Common errors
Section titled “Common errors”- The chart is empty. Usually the
ParamListlabels and theifcomparisons 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 caseBlankWarmUpcorrectly 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 slopeis off, orSlopeBarsis 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.
Extensions
Section titled “Extensions”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.
What changed
Section titled “What changed”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
Sources for this lesson
8 verified · checked 2026-08-31
- 01AFL Function Reference — MAamibroker.com/guide/afl/ma.html2026-08-31
- 02AFL Function Reference — EMAamibroker.com/guide/afl/ema.html2026-08-31
- 03AFL Function Reference — DEMAamibroker.com/guide/afl/dema.html2026-08-31
- 04AFL Function Reference — TEMAamibroker.com/guide/afl/tema.html2026-08-31
- 05AFL Function Reference — Wildersamibroker.com/guide/afl/wilders.html2026-08-31
- 06AFL Function Reference — WMAamibroker.com/guide/afl/wma.html2026-08-31
- 07AmiBroker User's Guide — Understanding how AFL works§ Array processing and warm-upamibroker.com/guide/h_understandafl.html2026-08-31
- 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.