Skip to content
Level 3 · AFL DeveloperLessonPart 11 · page 1 of 530 min
30Minutes
8AFL functions
5Sources
StandardRequires
AFL functions taught here8

User-Defined Functions and Procedures

Count the number of times you have typed some version of this:

Fragment — not a complete formula

Turnover = Close * Volume;
Liquid = MA( Turnover, 100 ) > 5000000;

If the answer is more than one, you have already met the problem this lesson solves. Not because repetition is untidy, but because the second copy is where the definition starts to drift. One formula uses a 100-bar average, another a 50-bar one, a third switches to Median after you read something persuasive, and now your screener and your backtest disagree about which shares are tradeable - silently, because each file looks correct on its own.

A function is the mechanism AFL gives you for having one definition instead of several. By the end of this lesson you will be able to write one, know exactly which of AFL’s rules about them are documented and which are folklore, and document a function so that you can use it a year later without reading its body.

A function definition starts with the function keyword, then the name, then a parenthesised list of arguments, then the body in braces:

Fragment — not a complete formula

function MedianTurnover( LookbackPeriod )
{
local BarTurnover;
local Result;
BarTurnover = Close * Volume;
Result = Median( BarTurnover, LookbackPeriod );
return Result;
}

Three rules, all documented, all of which cost beginners time:

The definition must appear before the call. AFL has no forward declarations. If you call MedianTurnover on line 4 and define it on line 40, you get a syntax error, not a helpful message about ordering. In practice this means definitions go at the top of the file, which is exactly where an include file will later put them for you.

The body is always braced, even for a one-line function. There is no brace-free short form.

function and procedure are currently interchangeable. The User’s Guide states that AmiBroker treats them the same today - both accept a return value and neither requires one - but that the rules “might get enforced” in future. It advises using function when you return a value and procedure when you do not. Follow that advice, because it costs nothing now and it documents your intent to the next reader.

A procedure is a function that exists for its effect rather than its answer. Drawing is the obvious case:

Fragment — not a complete formula

procedure PlotRangeRatio( AveragePeriod, LineColour )
{
local Ratio;
local LegendText;
Ratio = 100 * ( High - Low ) / MA( High - Low, AveragePeriod );
LegendText = "Range vs " + NumToStr( AveragePeriod, 1.0 ) + "-bar average, %";
Plot( Ratio, LegendText, LineColour, styleLine );
}

Calling it three times with three different periods draws three lines from one description of what a line is. The alternative - three near-identical Plot calls with the period and the legend text typed out each time - is where a legend eventually says 20 above a line computed with 50.

The User’s Guide is explicit: arguments are passed by value, and formal parameters “behave like local variables”. The official example passes a global z into a function that assigns to its own parameter, and shows that z is unchanged afterwards.

This has a practical consequence worth stating plainly. A function cannot hand a result back by modifying one of its arguments. Whatever you assign to a parameter inside the body affects only the function’s private copy. There is a documented way to return a second value - the global keyword, covered in the next lesson - and that is the mechanism to reach for, not argument mutation.

return goes at the end, and only at the end

Section titled “return goes at the end, and only at the end”

The User’s Guide says it in one sentence: “Note that currently, a return statement must be placed at the very end of the function.” There is no early return in AFL.

If you are used to guard clauses, this is the habit that has to change. Code that would naturally be written as an early exit becomes a single result variable that later branches may overwrite:

Fragment — not a complete formula

function ClampedPeriod( RequestedPeriod )
{
local Result;
Result = RequestedPeriod;
if( Result < 2 ) Result = 2;
if( Result > 400 ) Result = 400;
return Result;
}

The single-result-variable shape is not a workaround so much as a discipline. It forces every path through the function to end in the same place, which makes the function easier to reason about and much easier to test.

Read the AFL Function Reference and you will constantly see syntax lines like Nz( x, valueifnull = 0 ) or RSI( periods = 14 ). It is natural to assume you can write the same thing in your own functions.

The AFL Basics page explains where that notation comes from. The equal sign, it says, “also indicates the default value for a parameter (see built-in function description)” - it is the notation the documentation uses when describing built-in functions. The page on user-defined functions never mentions optional arguments or default values at all.

Two patterns give you the convenience without the guess. Both are ordinary AFL.

A sentinel value. Agree, and document, that a particular argument value means “use the house default”:

Fragment — not a complete formula

function RangeRatio( AveragePeriod )
{
local Period;
local Result;
// Zero or negative means "use the documented default".
Period = AveragePeriod;
if( Period <= 0 ) Period = 20;
Result = MA( High - Low, Period );
return Result;
}

A thin wrapper. Define the full function, then a second one that supplies the common arguments:

Fragment — not a complete formula

function LiquidAtLeast( LookbackPeriod, MinimumTurnover )
{
local Result;
Result = Median( Close * Volume, LookbackPeriod ) >= MinimumTurnover;
return Result;
}
function LiquidByHouseRule()
{
local Result;
Result = LiquidAtLeast( 100, 5000000 );
return Result;
}

The wrapper costs three lines and buys something the sentinel does not: the house rule now has a name, and a single place to change it.

AFL functions are not typed. What a function returns depends entirely on what its body computes, and the same function will return an array or a single number depending on what you feed it and what you do with it.

RangeRatio above returns an array, because everything it touches is an array:

RangeRatio( 3 ) evaluated bar by bar

One value per bar in, one value per bar out. The Null bars are the moving average's warm-up, and they propagate straight through the division.
Bar12345
High - Low1.001.200.802.401.00
MA(High - Low, 3)NullNull1.001.471.40
RangeRatio(3), %NullNull80.0163.671.4
One value per bar in, one value per bar out. The Null bars are the moving average's warm-up, and they propagate straight through the division.

A function that ends with LastValue( ... ) or SelectedValue( ... ), by contrast, returns a single number, and that difference decides where you are allowed to use the result. An if() condition must be a number: feeding it an array raises Error 6, “Condition in IF/WHILE/FOR must be Numeric or Boolean”.

So the return type belongs in the documentation of every function you write, in capitals, next to the name. ARRAY, NUMBER or STRING - the reader should never have to work it out from the body.

Recursion. The User’s Guide states that a function calling itself “is NOT supported as of now”. A recursive definition must be rewritten as a loop or as an array operation.

Return by assigning to the function’s own name. Writing function Test( x ) { Test = 2 * x; } produces Error 33, “Identifier already in use”. The return statement is the only mechanism.

Share a name with an earlier global variable. If Trend is used as a variable and you later define function Trend(), you get Error 34. This is one of several reasons that every function in a shared library should carry a prefix that nothing else uses.

A function you cannot use without reading its body has not saved you anything. The convention this course uses, and which the library in this part follows, records five things above every definition:

Field What it answers
Purpose Why would anyone call this?
Inputs What is each argument, and is it a number or an array?
Returns ARRAY, NUMBER or STRING - and in what units?
Warm-up From which bar is the answer real?
Notes The decisions and assumptions a caller could be surprised by

Names matter as much as the comment. RangeRatio says what comes back; Calc2 does not. Argument names carry units: AveragePeriod is bars, MinimumTurnover is money. When a function returns a percentage rather than a fraction, say so in the name or the comment, because that particular ambiguity has caused more wrong charts than any other.

Putting it together: a normalised range indicator

Section titled “Putting it together: a normalised range indicator”

Raw bar range is in price units, so a 2.00 range means something different on a share trading at 8 and a share trading at 800, and something different again on the same share five years and one stock split ago. Dividing the bar’s range by a rolling average of its own range removes both problems: the result is a percentage where 100 means “an ordinary bar for this instrument, lately”.

We want that measurement at three averaging lengths on one pane, computed from a single definition.

Complete runnable AFL

normalised-range-indicator.afl
// normalised-range-indicator.afl
// Part 11 - User-Defined Functions and Procedures
//
// GOAL
// Show the same measurement - how large today's bar is compared with its own
// recent average range - at three different averaging lengths, from ONE
// definition of the measurement. Change the definition once and all three
// lines change with it.
//
// WHY IT IS USEFUL
// Raw range is in price units, so it cannot be compared between symbols or
// across a long history in which price has doubled. Dividing by a rolling
// average of the same quantity removes both problems. A reading of 100 means
// "an average-sized bar"; 250 means "two and a half times the usual range".
//
// ASSUMPTIONS
// - Any instrument, any interval.
// - The result says nothing about direction, and nothing about what happens
// next. It is a description of one bar relative to its recent neighbours.
_SECTION_BEGIN( "Normalised range" );
// ---------------------------------------------------------------------------
// RangeRatio( AveragePeriod )
// Purpose : bar range as a percentage of its own rolling average range.
// Inputs : AveragePeriod - NUMBER of bars. Pass 0 or a negative number to
// ask for the house default of 20.
// Returns : ARRAY, percent. Null where the average is not yet available or
// is zero.
// ---------------------------------------------------------------------------
function RangeRatio( AveragePeriod )
{
local Period;
local BarRange;
local TypicalRange;
local Quotient;
local Result;
// AFL publishes no syntax for default argument values in user-defined
// functions, so the "default" is an agreed sentinel that the function
// recognises and documents. The caller always passes something.
Period = AveragePeriod;
if( Period <= 0 ) Period = 20;
BarRange = High - Low;
TypicalRange = MA( BarRange, Period );
// A symbol that did not move at all has a zero average range. Dividing by
// it produces an infinity, which would draw a spike off the top of the
// pane. Null draws nothing, which is the honest answer.
Quotient = BarRange / TypicalRange;
Result = IIf( IsFinite( Quotient ), 100 * Quotient, Null );
return Result;
}
// ---------------------------------------------------------------------------
// PlotRangeRatio( AveragePeriod, LineColour )
// A procedure: it draws something and returns nothing. Everything it needs
// arrives as an argument, so it never reads a global.
// ---------------------------------------------------------------------------
procedure PlotRangeRatio( AveragePeriod, LineColour )
{
local Ratio;
local LegendText;
Ratio = RangeRatio( AveragePeriod );
LegendText = "Range vs " + NumToStr( AveragePeriod, 1.0 ) + "-bar average, %";
Plot( Ratio, LegendText, LineColour, styleLine );
}
ShortPeriod = Param( "Short average", 10, 2, 200, 1 );
MediumPeriod = Param( "Medium average", 20, 2, 200, 1 );
LongPeriod = Param( "Long average", 60, 2, 200, 1 );
PlotRangeRatio( ShortPeriod, colorBlue );
PlotRangeRatio( MediumPeriod, colorOrange );
PlotRangeRatio( LongPeriod, colorDarkGreen );
// The 100 line is where a bar is exactly as large as its recent average.
PlotGrid( 100, colorLightGrey );
SelectedRatio = RangeRatio( MediumPeriod );
_N( Title =
Name() + " - " + Interval( 2 ) + " - normalised bar range\n" +
WriteIf( IsNull( SelectedRatio ),
"No reading at the selected bar: the rolling average is not available yet.",
"Selected bar is " + NumToStr( SelectedRatio, 1.1 ) + "% of its " +
NumToStr( MediumPeriod, 1.0 ) + "-bar average range." ) );
_SECTION_END();

Download normalised-range-indicator.afl93 lines

The formula has three sections. RangeRatio is the definition of the measurement: it validates its own period argument through the documented sentinel, computes the bar range and its rolling average, divides, and converts a non-finite result into Null. PlotRangeRatio is a procedure that turns a period and a colour into a labelled line, so the legend text can never disagree with the period used. The rest of the formula is three Param calls and three procedure calls.

Note what is not in the procedure: it does not know the house default, does not read a global, and does not decide which periods to draw. Everything it needs is in its argument list, which is what makes it safe to move into an include file later.

  • IsFinite( x ) returns 1 where x is a real number and 0 where it is Null, NaN or infinite. The Nz page gives exactly this IIf( IsFinite( ... ), ... ) pattern as the long-hand form of Nz(), and we use the long-hand form here because we want Null rather than zero as the fallback.
  • NumToStr( value, format ) turns a number into text; the 1.0 format means no decimal places. Given an array it formats the selected bar’s value.
  • PlotGrid( level, colour ) draws a horizontal reference line at a value on the current pane’s scale.

Three lines in one pane, oscillating around the 100 grid line, with the shortest average producing the most volatile line. The first bars of each line are missing: that is the moving average’s warm-up, and the longer the period the longer the gap. Hovering the title shows the medium-period reading for the selected bar in words.

Set all three parameters to the same value. The three lines must lie exactly on top of each other - if they do not, the procedure is using something other than its argument. Then set one parameter to 2 and check that the resulting line spends roughly half its time above 100 and half below, which is what a ratio to a two-bar average has to do. Finally, look at an instrument with a long flat stretch, or set the period longer than the chart’s history, and confirm you get a gap rather than a spike.

Symptom Cause
Syntax error on the first PlotRangeRatio call The procedure is defined below the call. Definitions come first.
One line spikes to an enormous value The divide-by-zero guard was removed, and a flat window produced an infinity.
Legend says one period, line looks like another The legend text was built outside the procedure from a different variable.
Error 6 when you add an if An array reached a condition that requires a number.

Add a fourth argument to PlotRangeRatio that switches the line style, and use it to draw the long-period line thicker. Then try moving RangeRatio and PlotRangeRatio into a separate file and including it - which is exactly the subject of two lessons from now, and worth attempting before you read it.

A function in AFL is the only mechanism you have for saying something once. The language supports the shape you expect with three restrictions that matter: definitions precede calls, return sits at the end, and recursion is not available. Arguments are copies, so a function’s answer must come back through return or through a declared global. Default argument values are documented for built-in functions only, so your own functions use a sentinel or a wrapper instead of a guess.

What changed is smaller than a new feature and larger than a new habit: from here on, when you find yourself typing a calculation for the second time, the correct response is to stop and name it.

Check your understanding

Question 1. A formula defines `function Slope( InputArray, Periods )` on line 30 and calls it on line 12. What happens?
Show the answer and why

Answer: It fails, because a function definition must precede the call

The User’s Guide states that a function definition must precede the call to the function. AFL has no forward declarations, so ordering inside the file is part of the syntax.

Question 2. What does this function return?
function Doubled( InputValue )
{
    InputValue = InputValue * 2;
}
Show the answer and why

Answer: Nothing, and the caller’s variable is unchanged

There is no return statement, so nothing comes back. Arguments are passed by value, so assigning to the parameter changes only the function’s private copy - the caller’s variable is untouched.

Question 3. Which of these are documented behaviours of user-defined AFL functions? Select all that apply.
Show the answer and why

Answer: return must be the last statement in the function, Arguments are passed by value

The User’s Guide documents the end-placement of return and by-value argument passing, and states that recursion is not supported. The `= value` notation appears only in the description of built-in functions, never as user-function syntax.

Question 4. A library function ends with `return LastValue( Result );` instead of `return Result;`. What has changed for callers?
Show the answer and why

Answer: The function now returns one number, so it can be used in an if() but no longer carries per-bar values

LastValue collapses an array to a single number. That makes the result legal in an if() condition, but it also throws away every bar except the last - and the documentation warns that reading the last bar from an earlier bar is a way to look into the future.

Sources for this lesson

5 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide - User functions/procedures. Local/global scopeamibroker.com/guide/a_userfunctions.html2026-08-31
  2. 02AmiBroker User's Guide - AFL Basics§ Lexical elements - the equal signamibroker.com/guide/a_language.html2026-08-31
  3. 03AmiBroker User's Guide - Error and warning messagesamibroker.com/guide/errors2026-08-31
  4. 04AFL Function Reference - Nzamibroker.com/guide/afl/nz.html2026-08-31
  5. 05AFL Function Reference - Paramamibroker.com/guide/afl/param.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.