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.
The shape of a definition
Section titled “The shape of a definition”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.
Procedures: when nothing comes back
Section titled “Procedures: when nothing comes back”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.
Arguments arrive as copies
Section titled “Arguments arrive as copies”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.
Arguments have no documented defaults
Section titled “Arguments have no documented defaults”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.
Arrays in, arrays out - usually
Section titled “Arrays in, arrays out - usually”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
| Bar | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
High - Low | 1.00 | 1.20 | 0.80 | 2.40 | 1.00 |
MA(High - Low, 3) | Null | Null | 1.00 | 1.47 | 1.40 |
RangeRatio(3), % | Null | Null | 80.0 | 163.6 | 71.4 |
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.
Three things AFL functions cannot do
Section titled “Three things AFL functions cannot do”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.
Naming and documenting
Section titled “Naming and documenting”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 formula
Section titled “Complete formula”Complete runnable 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();How it works
Section titled “How it works”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.
Key functions
Section titled “Key functions”IsFinite( x )returns 1 wherexis a real number and 0 where it is Null, NaN or infinite. TheNzpage gives exactly thisIIf( IsFinite( ... ), ... )pattern as the long-hand form ofNz(), and we use the long-hand form here because we wantNullrather than zero as the fallback.NumToStr( value, format )turns a number into text; the1.0format 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.
Expected result
Section titled “Expected result”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.
Test it
Section titled “Test it”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.
Common errors
Section titled “Common errors”| 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. |
Extension
Section titled “Extension”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
Sources for this lesson
5 verified · checked 2026-08-31
- 01AmiBroker User's Guide - User functions/procedures. Local/global scopeamibroker.com/guide/a_userfunctions.html2026-08-31
- 02AmiBroker User's Guide - AFL Basics§ Lexical elements - the equal signamibroker.com/guide/a_language.html2026-08-31
- 03AmiBroker User's Guide - Error and warning messagesamibroker.com/guide/errors2026-08-31
- 04AFL Function Reference - Nzamibroker.com/guide/afl/nz.html2026-08-31
- 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.