Skip to content
Level 3 · AFL DeveloperProjectPart 11 · page 5 of 560 min
60Minutes
23AFL functions
8Sources
StandardRequires
AFL functions taught here23

Project: Your Personal AFL Utility Library

Build the include file that every later part of this course draws on: a small, documented, versioned library of helpers, installed where an AmiBroker upgrade cannot overwrite it, with a companion test formula that calls every function and checks the answers it can check.

The library is not the interesting deliverable. The conventions are. By the end you will have decided — and written down — how your functions are named, what happens when one cannot answer, where parameters live, how a caller knows which version is loaded, and how you find out that you broke something.

The failure mode of a personal library is not that it is too small. It is that it grows into a second, undocumented copy of AmiBroker with your own names on it.

What belongs in a library, and what does not

  1. IN — conventions you want applied identically everywhereSafe division, warm-up guards, normalisation, formatting. Change once, every formula follows.
  2. IN — definitions you want to be able to point atWhat "liquid" means, what "uptrend" means. Named, so a study can cite its own definitions.
  3. OUT — anything that calls Param()Parameters belong to the formula being used, not to the library. A library that owns a slider fights every caller.
  4. OUT — trading rulesBuy/Sell logic in an include file is a rule you will eventually forget you are running.
  5. OUT — anything AFL already doesA wrapper around MA() that adds nothing is a name to remember for no benefit.
The test is ownership: a library holds decisions you want to make once. Everything specific to one study stays in that study.

The library built here has ten functions. That is deliberately few. A library you can hold in your head is a library you will actually use.

These are decisions, not laws. Adopt them or replace them — but write down whichever you choose, because the value comes from consistency, not from the specific choice.

Every public name starts with Lib. AFL has one global namespace. A library function called Regime() will one day collide with a variable called Regime in a formula somebody else wrote. A prefix makes collision impossible by construction and makes it obvious, at the call site, which names came from the library.

Every variable a function assigns is declared local. Without local, an assignment inside a function is visible outside it. Two library functions using a variable named Result would then interfere in ways that depend on call order. This is the single most important rule in the file.

No function calls Param(). Parameters belong to the formula. A library that creates sliders puts controls in the Parameters dialog of every chart that includes it, whether that chart uses the function or not.

A function that cannot answer returns Null, not a plausible number — except where the caller is a screen, where the documented decision is to return False. Each function’s header says which policy it follows and why.

Periods are passed as numbers, not arrays. Passing an array where a number is expected raises Error 6 rather than producing a quiet wrong answer, and that is the outcome we want.

Every function has a header block giving purpose, inputs, return type, assumptions, warm-up behaviour and notes. If you cannot write the assumptions line, the function is not ready.

Complete runnable AFL

course-library.afl
// course-library.afl
// ---------------------------------------------------------------------------
// The course utility library.
//
// Version : 1.00
// Written : Part 11 - Reusable AFL Programming
// Requires: AmiBroker 6.20 or later (StDev's Population argument)
//
// PURPOSE
// A small, deliberately narrow set of helpers that later parts of the course
// reuse: safe division, a warm-up guard, a liquidity measure, a normalised
// volatility measure, a z-score, a three-state trend regime, and two display
// formatters. Nothing here is a trading rule. Every function is a
// transformation of past data with its assumptions written down.
//
// INSTALLATION
// Save this file as course-library.afl in your own formula folder, for
// example C:\AmiBroker\Formulas\Custom\course-library.afl , and either
//
// #include "C:\AmiBroker\Formulas\Custom\course-library.afl" // single \
//
// or set Tools -> Preferences -> AFL -> the standard include path to that
// folder and then simply write
//
// #include <course-library.afl>
//
// Do not save it into the Formulas folder that ships with AmiBroker: the
// official documentation warns that formulas supplied with AmiBroker are
// overwritten by the next upgrade.
//
// CONVENTIONS
// - Every public name starts with Lib, so this file cannot collide with your
// own variables or with another library.
// - Every variable a function assigns is declared local. Nothing in this file
// reads or writes a global.
// - No function here calls Param(). Parameters belong to the formula that
// uses the library, not to the library.
// - A function that cannot answer returns Null rather than a plausible
// number, so the gap is visible on a chart instead of silently wrong.
// - Periods are passed as numbers. Passing an array where a number is
// expected raises Error 6 rather than producing a quiet wrong answer.
//
// CHANGELOG
// 1.00 First release. LibVersion, LibWarmupOK, LibSafeDivide,
// LibMedianTurnover, LibIsLiquid, LibNormATR, LibZScore, LibRegime,
// LibRegimeName, LibPercentText, LibMoneyText.
// ---------------------------------------------------------------------------
// The single executable statement in this file. It raises an AmiBroker error
// on any version older than 6.20 instead of letting StDev's third argument
// fail somewhere deep inside a formula that includes this library.
Version( 6.20 );
// ---------------------------------------------------------------------------
// LibVersion()
// Purpose : Report which release of this library is loaded.
// Inputs : none
// Returns : NUMBER
// Notes : Compare numerically, e.g. if( LibVersion() < 1.00 ) ...
// ---------------------------------------------------------------------------
function LibVersion()
{
return 1.00;
}
// ---------------------------------------------------------------------------
// LibWarmupOK( RequiredBars )
// Purpose : True from the bar on which an indicator needing RequiredBars of
// history can first have a real value.
// Inputs : RequiredBars - NUMBER of bars of history required
// Returns : ARRAY (Boolean, one value per bar)
// Notes : BarIndex() is zero-based, so bar RequiredBars is the first bar
// with RequiredBars earlier bars behind it. A negative request is
// clamped to zero rather than silently inverting the test.
// ---------------------------------------------------------------------------
function LibWarmupOK( RequiredBars )
{
local Needed;
local Result;
Needed = RequiredBars;
if( Needed < 0 ) Needed = 0;
Result = BarIndex() >= Needed;
return Result;
}
// ---------------------------------------------------------------------------
// LibSafeDivide( Numerator, Denominator, Fallback )
// Purpose : Division that yields a value you chose wherever the result is
// not a finite number.
// Inputs : Numerator - ARRAY or NUMBER
// Denominator - ARRAY or NUMBER
// Fallback - ARRAY or NUMBER used where the quotient is Null,
// NaN or infinite. Pass Null to leave a visible gap.
// Returns : ARRAY
// Notes : IIf evaluates both branches, so the division still happens on
// every bar; only the result is discarded. This is the pattern the
// official Nz() page gives as the long-hand form of Nz().
// ---------------------------------------------------------------------------
function LibSafeDivide( Numerator, Denominator, Fallback )
{
local Quotient;
local Result;
Quotient = Numerator / Denominator;
Result = IIf( IsFinite( Quotient ), Quotient, Fallback );
return Result;
}
// ---------------------------------------------------------------------------
// LibMedianTurnover( LookbackPeriod )
// Purpose : Typical money changing hands per bar, as a median rather than a
// mean, so one enormous day does not carry the estimate.
// Inputs : LookbackPeriod - NUMBER of bars in the median window
// Returns : ARRAY, in the currency the symbol is quoted in
// Assumes : Volume is share volume, not contracts or notional. For futures
// and for symbols quoted in cents this number is not money.
// Warm-up : Null until LookbackPeriod bars are available.
// ---------------------------------------------------------------------------
function LibMedianTurnover( LookbackPeriod )
{
local BarTurnover;
local Result;
BarTurnover = Close * Volume;
Result = Median( BarTurnover, LookbackPeriod );
return Result;
}
// ---------------------------------------------------------------------------
// LibIsLiquid( LookbackPeriod, MinimumTurnover )
// Purpose : A first-stage screening filter: is there normally enough money
// traded in this symbol for a position to be enterable and
// exitable at something close to the printed price?
// Inputs : LookbackPeriod - NUMBER of bars for the median
// MinimumTurnover - NUMBER, the threshold in quote currency
// Returns : ARRAY (Boolean)
// Notes : Warm-up bars return False, not Null. That is a decision, not an
// accident: for a screen, "unknown" and "excluded" should behave
// the same way, and a Null here would propagate into every
// condition it is combined with.
// ---------------------------------------------------------------------------
function LibIsLiquid( LookbackPeriod, MinimumTurnover )
{
local Typical;
local Result;
Typical = LibMedianTurnover( LookbackPeriod );
Result = IIf( LibWarmupOK( LookbackPeriod ), Typical >= MinimumTurnover, False );
return Result;
}
// ---------------------------------------------------------------------------
// LibNormATR( ATRPeriod )
// Purpose : Average True Range expressed as a percentage of price, so that a
// 300-currency-unit share and a 3-currency-unit share can be
// compared on the same axis.
// Inputs : ATRPeriod - NUMBER of bars for Wilder's average
// Returns : ARRAY, percent
// Notes : Null wherever Close is zero or missing, so a broken quote leaves
// a gap in the plot instead of a spike.
// ---------------------------------------------------------------------------
function LibNormATR( ATRPeriod )
{
local AverageRange;
local Result;
AverageRange = ATR( ATRPeriod );
Result = 100 * LibSafeDivide( AverageRange, Close, Null );
return Result;
}
// ---------------------------------------------------------------------------
// LibZScore( InputArray, LookbackPeriod )
// Purpose : How unusual the current value of an array is, measured in
// standard deviations of its own recent history.
// Inputs : InputArray - ARRAY to normalise
// LookbackPeriod - NUMBER of bars in the window
// Returns : ARRAY
// Notes : StDev's third argument is passed explicitly. The documented
// default is the population standard deviation, which is what a
// spreadsheet calls STDEV.P, not STDEV. Where the window is flat
// the standard deviation is zero and the result is Null.
// ---------------------------------------------------------------------------
function LibZScore( InputArray, LookbackPeriod )
{
local Mean;
local Spread;
local Result;
Mean = MA( InputArray, LookbackPeriod );
Spread = StDev( InputArray, LookbackPeriod, True );
Result = LibSafeDivide( InputArray - Mean, Spread, Null );
return Result;
}
// ---------------------------------------------------------------------------
// LibRegime( FastPeriod, SlowPeriod )
// Purpose : Classify each bar into one of three states, so that a strategy
// can be gated on market conditions instead of being run blind.
// Inputs : FastPeriod - NUMBER, short moving-average length
// SlowPeriod - NUMBER, long moving-average length
// Returns : ARRAY of +1 (uptrend), 0 (neither), -1 (downtrend),
// Null during warm-up.
// Definition: uptrend = Close above the slow average AND fast above slow
// downtrend = Close below the slow average AND fast below slow
// neither = anything else
// Notes : This is a definition, not a fact about markets. Change the
// periods and the classification changes with them. Nothing here
// claims that an uptrend continues.
// ---------------------------------------------------------------------------
function LibRegime( FastPeriod, SlowPeriod )
{
local FastAverage;
local SlowAverage;
local UpCondition;
local DownCondition;
local Ready;
local Result;
FastAverage = MA( Close, FastPeriod );
SlowAverage = MA( Close, SlowPeriod );
UpCondition = Close > SlowAverage AND FastAverage > SlowAverage;
DownCondition = Close < SlowAverage AND FastAverage < SlowAverage;
Ready = LibWarmupOK( SlowPeriod );
Result = IIf( Ready,
IIf( UpCondition, 1, IIf( DownCondition, -1, 0 ) ),
Null );
return Result;
}
// ---------------------------------------------------------------------------
// LibRegimeName( RegimeArray )
// Purpose : Turn one bar of LibRegime output into text for a chart title or
// a commentary.
// Inputs : RegimeArray - ARRAY produced by LibRegime()
// Returns : STRING describing the SELECTED bar only
// Notes : Display only. It reads a single bar, so it must never be used to
// build a signal. IsNull is tested before the value is read,
// because SelectedValue of a Null bar cannot be told apart from a
// genuine zero.
// ---------------------------------------------------------------------------
function LibRegimeName( RegimeArray )
{
local Unknown;
local Code;
local Result;
Unknown = SelectedValue( IsNull( RegimeArray ) );
Code = SelectedValue( RegimeArray );
Result = "neutral";
if( Unknown )
{
Result = "not enough history";
}
else
{
if( Code > 0 ) Result = "uptrend";
if( Code < 0 ) Result = "downtrend";
}
return Result;
}
// ---------------------------------------------------------------------------
// LibPercentText( ValueArray )
// Purpose : Format a percentage for display with two decimal places.
// Inputs : ValueArray - ARRAY or NUMBER already expressed in percent
// Returns : STRING, e.g. "2.41%"
// Notes : Given an array, NumToStr formats the selected value. Display
// only.
// ---------------------------------------------------------------------------
function LibPercentText( ValueArray )
{
local Result;
Result = NumToStr( ValueArray, 1.2 ) + "%";
return Result;
}
// ---------------------------------------------------------------------------
// LibMoneyText( ValueArray )
// Purpose : Format a currency amount for display, with thousands separators
// and no decimals.
// Inputs : ValueArray - ARRAY or NUMBER
// Returns : STRING, e.g. "12,480,000"
// Notes : The thousands separator itself is a Windows regional setting,
// configured in AmiBroker under Tools -> Preferences -> Misc.
// ---------------------------------------------------------------------------
function LibMoneyText( ValueArray )
{
local Result;
Result = NumToStr( ValueArray, 1.0, True );
return Result;
}
// End of course-library.afl

Download course-library.afl313 lines

Function Returns Why it exists
LibVersion() number So a caller can check what it got. Compare numerically.
LibWarmupOK( n ) Boolean array The BarIndex() >= n idiom, written once, with the negative-input case handled.
LibSafeDivide( n, d, fallback ) array Division whose result you chose when it cannot be finite.
LibMedianTurnover( n ) array Typical money traded per bar, as a median so one huge day cannot carry it.
LibIsLiquid( n, min ) Boolean array The first-stage screening gate, with the Null policy already decided.
LibNormATR( n ) array, percent ATR made comparable across instruments.
LibZScore( array, n ) array How unusual a value is against its own recent history.
LibRegime( fast, slow ) array of −1/0/+1 A named, citable definition of market state.
LibRegimeName( array ) string Display only — reads one bar.
LibPercentText / LibMoneyText strings Formatting decisions made once.

Version( 6.20 ); is the only executable statement at file scope. It is there because LibZScore() passes StDev’s third argument, and that argument was added in 6.20. Without the assertion, an older AmiBroker would fail somewhere deep inside a caller’s formula with an error that named the wrong file.

StDev( InputArray, LookbackPeriod, True ) passes Population explicitly even though True is the documented default. The official page is precise about what that means: StDev( a, n, True ) matches Excel’s STDEV.P, and StDev( a, n, False ) matches Excel’s STDEV. A z-score is a statement about a distribution, and which of the two you used changes the number. Writing the argument makes the choice reviewable instead of inherited.

LibMedianTurnover() uses Median() rather than MA() because turnover is heavily skewed — one index-rebalance day can be twenty times a normal day, and a mean carries that day forward for the whole window. Note the documented subtlety: Median() returns the lower median for an even period. Percentile( array, period, 50 ) averages the two middle values instead, at the cost of speed.

LibIsLiquid() returns False during warm-up, not Null. The header says so, and says why: for a screening gate, “we do not know yet” and “excluded” should behave identically, and a Null here would propagate into every condition it is combined with. That is the kind of decision that must be written down, because the alternative is defensible too — and a reader six months from now needs to know which one you picked.

  1. Choose a folder that upgrades cannot touch. Something like C:\AmiBroker\Formulas\Custom\. The User’s Guide warns that formulas supplied with AmiBroker will be overwritten by the next upgrade, so never put your own work in the folders that ship with the program.

  2. Save the file as course-library.afl in that folder.

  3. Set the standard include path under Tools → Preferences → AFL to the same folder.

  4. Include it with the angle-bracket form, which looks the bare file name up in that path:

    Fragment — not a complete formula

    #include <course-library.afl>

    If you would rather not set the preference, use the quoted form with a full path — and note the documented oddity that #include takes single backslashes, unlike every other string in AFL:

    Fragment — not a complete formula

    #include "C:\AmiBroker\Formulas\Custom\course-library.afl"
  5. Run the test formula below. Do not skip this step; the whole point of having a test is that installation is where things go wrong.

Complete runnable AFL

course-library-test.afl
// course-library-test.afl
// ---------------------------------------------------------------------------
// Self-test and demonstration for course-library.afl version 1.00.
//
// WHAT IT DOES
// Calls every function in the library, checks the ones whose answer is known
// in advance, and reports pass or fail for each check. Run it after you
// install the library, and again after you change anything in it.
//
// HOW TO RUN IT
// As a chart formula : the checks appear in the title, the library's arrays
// are plotted, and the Log window (Window -> Log) holds
// one line per check.
// As an Exploration : Analysis -> Explore produces one row per bar with the
// library's arrays as columns.
//
// PREREQUISITE
// course-library.afl must be installed and reachable by the #include below.
// If AmiBroker reports Error 42, the path is wrong - fix the path, do not
// comment the include out.
//
// ASSUMPTIONS
// - Daily bars of an ordinary share, with at least 150 bars of history.
// - Volume is share volume, so Close * Volume is money.
// ---------------------------------------------------------------------------
#include <course-library.afl>
// Alternative when you have not set a standard include path. Note the SINGLE
// backslashes: #include is the one place in AFL where they are not doubled.
// #include "C:\AmiBroker\Formulas\Custom\course-library.afl"
_SECTION_BEGIN( "Library self-test" );
// --- Settings. Deliberately named, never buried inside an expression. -------
TurnoverPeriod = 100;
MinimumTurnover = 5000000;
VolatilityPeriod = 14;
FastPeriod = 20;
SlowPeriod = 100;
ZScorePeriod = 100;
// --- Exercise every function in the library. --------------------------------
Turnover = LibMedianTurnover( TurnoverPeriod );
Liquid = LibIsLiquid( TurnoverPeriod, MinimumTurnover );
Volatility = LibNormATR( VolatilityPeriod );
Stretch = LibZScore( Close, ZScorePeriod );
Regime = LibRegime( FastPeriod, SlowPeriod );
Ready = LibWarmupOK( SlowPeriod );
BarRangePct = 100 * LibSafeDivide( High - Low, Close, Null );
RegimeText = LibRegimeName( Regime );
VolatilityText = LibPercentText( Volatility );
TurnoverText = LibMoneyText( Turnover );
// --- Checks whose answer is known before the formula runs. ------------------
// Each check is a single number: 1 for pass, 0 for fail.
HistoryOK = BarCount > SlowPeriod + 5;
VersionOK = LibVersion() >= 1.00;
// A zero denominator must produce the fallback, not an infinity.
DivideByZeroOK = LastValue( LibSafeDivide( 1, 0, -999 ) ) == -999;
// A good denominator must produce the ordinary quotient.
DivideOK = LastValue( LibSafeDivide( 10, 4, -999 ) ) == 2.5;
// Requiring no history is true immediately; requiring the whole chart is never
// true, so bar zero must be false.
WarmupNone = LibWarmupOK( 0 );
WarmupAll = LibWarmupOK( BarCount );
WarmupStartOK = WarmupNone[ 0 ] == 1;
WarmupBlockOK = WarmupAll[ 0 ] == 0;
// The regime code may only ever be -1, 0 or +1 where it is not Null.
RegimeFilled = Nz( Regime, 0 );
RegimeStrayed = Cum( RegimeFilled > 1 OR RegimeFilled < -1 );
RegimeRangeOK = LastValue( RegimeStrayed ) == 0;
// The liquidity flag is a screening gate, so it must never be Null.
LiquidNulls = Cum( IsNull( Liquid ) );
LiquidNullOK = LastValue( LiquidNulls ) == 0;
// Normalised volatility is a range over a price: negative values are impossible.
VolatilityFilled = Nz( Volatility, 0 );
VolatilityNegative = Cum( VolatilityFilled < 0 );
VolatilitySignOK = LastValue( VolatilityNegative ) == 0;
// A z-score may be Null during warm-up or where the window is flat, but a
// value that exists must be a finite number.
StretchBroken = Cum( NOT IsNull( Stretch ) AND NOT IsFinite( Stretch ) );
StretchFiniteOK = LastValue( StretchBroken ) == 0;
// The two display helpers must return real text.
RegimeTextOK = StrLen( RegimeText ) > 0;
NumberTextOK = StrLen( VolatilityText ) > 0 AND StrLen( TurnoverText ) > 0;
CheckCount = 10;
PassCount = VersionOK + DivideByZeroOK + DivideOK + WarmupStartOK +
WarmupBlockOK + RegimeRangeOK + LiquidNullOK +
VolatilitySignOK + StretchFiniteOK + RegimeTextOK;
// --- Report every check to the Log window (Window -> Log). ------------------
_TRACE( "!CLEAR!" );
_TRACE( "course-library self-test on " + Name() );
_TRACE( " enough history : " + WriteIf( HistoryOK, "yes", "NO - results below are unreliable" ) );
_TRACE( " version >= 1.00 : " + WriteIf( VersionOK, "PASS", "FAIL" ) );
_TRACE( " divide by zero : " + WriteIf( DivideByZeroOK, "PASS", "FAIL" ) );
_TRACE( " ordinary division : " + WriteIf( DivideOK, "PASS", "FAIL" ) );
_TRACE( " warm-up starts open : " + WriteIf( WarmupStartOK, "PASS", "FAIL" ) );
_TRACE( " warm-up blocks early: " + WriteIf( WarmupBlockOK, "PASS", "FAIL" ) );
_TRACE( " regime codes in -1..1: " + WriteIf( RegimeRangeOK, "PASS", "FAIL" ) );
_TRACE( " liquidity never Null: " + WriteIf( LiquidNullOK, "PASS", "FAIL" ) );
_TRACE( " volatility >= 0 : " + WriteIf( VolatilitySignOK, "PASS", "FAIL" ) );
_TRACE( " z-score finite : " + WriteIf( StretchFiniteOK, "PASS", "FAIL" ) );
_TRACE( " text helpers : " + WriteIf( RegimeTextOK AND NumberTextOK, "PASS", "FAIL" ) );
// --- Output. Charts get plots and a title; the Analysis window gets a table. -
if( Status( "action" ) == actionIndicator )
{
Plot( Close, "Close", colorDefault, styleCandle );
// The regime ribbon sits along the bottom of the price pane. Colour alone
// never carries the meaning here: the title states the regime in words.
RibbonColour = IIf( IsNull( Regime ), colorLightGrey,
IIf( Regime > 0, colorPaleGreen,
IIf( Regime < 0, colorRose, colorLightYellow ) ) );
Plot( 1, "Regime", RibbonColour, styleArea | styleOwnScale | styleNoLabel, 0, 100 );
Plot( Volatility, "ATR % of price", colorBlue, styleLine | styleOwnScale );
Plot( Stretch, "Close z-score", colorOrange, styleLine | styleOwnScale );
_N( Title =
Name() + " - library self-test - " +
StrFormat( "%g of %g checks passed\n", PassCount, CheckCount ) +
"Regime at selected bar: " + RegimeText + "\n" +
"ATR as percent of price: " + VolatilityText + "\n" +
"Median turnover: " + TurnoverText +
WriteIf( Liquid, " (passes the liquidity filter)", " (fails the liquidity filter)" ) + "\n" +
WriteIf( HistoryOK, "", "WARNING: not enough bars loaded for a meaningful test" ) );
}
else
{
// One row per bar, from the first bar the slow average can be trusted.
Filter = Ready;
AddColumn( Close, "Close", 1.2 );
AddColumn( Turnover, "Median turnover", 1.0 );
AddColumn( Liquid, "Liquid", 1.0 );
AddColumn( Volatility, "ATR %", 1.2 );
AddColumn( Stretch, "Close z-score", 1.2 );
AddColumn( Regime, "Regime code", 1.0 );
AddColumn( BarRangePct, "Bar range %", 1.2 );
AddColumn( PassCount, "Checks passed", 1.0 );
}
_SECTION_END();

Download course-library-test.afl157 lines

Each check reduces to a single number: 1 for pass, 0 for fail. They fall into three groups.

Checks with a known answer. LibSafeDivide( 1, 0, -999 ) must be exactly −999, and LibSafeDivide( 10, 4, -999 ) must be exactly 2.5. There is no data dependence here at all — these two either work or the function is broken.

Checks on the boundary. LibWarmupOK( 0 ) must be true on bar 0, and LibWarmupOK( BarCount ) must be false on bar 0. Those are the two ends of the guard’s behaviour, and they are where an off-by-one lives.

Invariant checks over the whole array. The regime code must never leave the set {−1, 0, +1}; the liquidity flag must never be Null; normalised ATR is a range divided by a price and so can never be negative; a z-score that exists must be finite. Each one is written as LastValue( Cum( ViolationCondition ) ) == 0 — “this never happened on any bar” — which is the array-to-number reduction from the defensive-AFL lesson, used in anger.

The formula ends with if( Status( "action" ) == actionIndicator ), and the two branches exist because the two contexts answer different questions.

As a chart formula, you get the plots and a title reporting how many checks passed. This is the installation smoke test: apply it to a chart, read “10 of 10 checks passed”, and you know the library is loaded and working.

As an Exploration, you get one row per bar with every library array as a column. This is the debugging view, and it is how you would investigate a failing invariant — the table shows you which bars violated it.

Change return 1.00; in LibVersion() to return 1.01;, save the library, and re-run the test without touching the test formula. The title must change. If it does not, you are running a cached or duplicated copy — press Apply in the Formula Editor, or check that you do not have a second course-library.afl earlier in the search path.

Temporarily delete the local Quotient; line from LibSafeDivide(). Then, in the test formula, add Quotient = 12345; before the first library call and print Quotient in the title after it. Without local, the library will have overwritten your variable. Put the declaration back. This is the bug the naming convention alone cannot prevent, and seeing it once is worth more than reading about it three times.

Find a symbol in your database with a flat patch — a suspended stock, or any thin symbol. Add a column for 100 * LibSafeDivide( High - Low, Close, Null ) and look at the flat bars. You should see empty cells, not zeros and not infinities.

If you have access to an older AmiBroker, run the library on it and confirm the error. If you do not, change Version( 6.20 ) to a version number higher than the one you are running — the error message should name the requirement immediately, before anything else executes.

Error 42 on #include. The path is wrong or the standard include path is not set. See installation, step 3.

“Variable used without having been assigned” inside a library function. You referenced a global that the caller happens not to define. Library functions must take everything they need as arguments; reaching out to a global is what makes an include file unusable in the second formula that tries it.

A caller’s variable changes value unexpectedly. A missing local in a library function. Search the file for every assignment inside a function body and confirm each name appears in a local declaration.

Two functions in the library disagree about warm-up. They are computing Ready separately instead of both calling LibWarmupOK(). That is precisely the duplication a library exists to remove.

The self-test passes but a real formula gives odd numbers. Check the assumptions lines. The most likely culprit is LibMedianTurnover() on an instrument where Volume is not share volume.

Everything breaks after you edit the library. Good — that is the test doing its job. Run the self-test after every library change, before you use it for anything.

  1. Add LibDrawdown( EquityArray ) returning the percentage below the running peak. It is three lines with HHV(), and it is the measure Part 34 leans on hardest. Write the header first: what does it return during warm-up?

  2. Add LibRelativeStrength( BenchmarkTicker, LookbackPeriod ) using Foreign(). This one introduces a dependency on the database containing a specific symbol, so it needs a guard — and deciding what it returns when the benchmark is missing is a genuine design decision.

  3. Split the library in two once it exceeds about fifteen functions: course-library.afl for general helpers and a second file for whatever domain grew. Use #include_once in both so a formula including both does not define anything twice.

  4. Add a changelog discipline. Every change bumps LibVersion() and adds a CHANGELOG line. When a study you ran six months ago disagrees with the same study today, the version number is the only thing that will tell you why.

  5. Write a second test formula that is expected to fail, calling functions with deliberately bad arguments, and confirm each one fails the way its header promises. Testing the failure path is what separates a test suite from a demonstration.

You have an include file, but more importantly you have a set of written conventions: a name prefix, a local rule, a no-Param() rule, an explicit fallback policy per function, and a version number that a caller can check. You have a self-test that reduces array invariants to pass/fail numbers, and you have run it. From here on, the course’s later parts can say “use LibIsLiquid()” and mean something specific — a definition you own, wrote down, and can change in one place.

Check your understanding

Question 1. Why does every library function declare its working variables with local?
function LibSafeDivide( Numerator, Denominator, Fallback )
{
    local Quotient;
    local Result;
    ...
}
Show the answer and why

Answer: Without it, an assignment inside a function is visible outside it, so two functions using the same variable name interfere depending on call order

AFL has one global namespace and function-body assignments escape by default. This is the failure mode that a naming convention cannot prevent, because it involves names the caller never sees.

Question 2. LibIsLiquid() returns False during its warm-up rather than Null. Which statements about that choice are correct? Select all that apply.
Show the answer and why

Answer: It means "not enough history" and "known illiquid" produce the same screening outcome, It prevents a Null from propagating into every condition the flag is combined with, It is a decision that has to be documented, because the opposite choice is also defensible

For a screen, collapsing "unknown" into "excluded" is a reasonable and conservative choice — but it does destroy information, and a study that wanted to count how many symbols were merely too young would need the Null. That is why the header states the policy explicitly.

Question 3. Why does LibZScore() pass StDev's third argument explicitly when True is already the default?
Spread = StDev( InputArray, LookbackPeriod, True );
Show the answer and why

Answer: Because StDev( a, n, True ) is population standard deviation — Excel's STDEV.P — and which one you used changes the number, so the choice should be visible

The documentation states the equivalence: True matches STDEV.P, False matches STDEV. A z-score is a statement about a distribution, so the estimator matters. Writing the argument makes the choice reviewable rather than inherited.

Question 4. Which of these belongs in a personal AFL library?
Show the answer and why

Answer: A safe-division helper whose fallback behaviour you want applied identically everywhere

A library holds decisions you want to make once. Trading rules hidden in an include file are rules you will forget you are running; Param() belongs to the calling formula; and a pass-through wrapper is a name to remember for no benefit.

Question 5. The self-test writes its invariant checks as LastValue( Cum( ViolationCondition ) ) == 0. What does that pattern express?
RegimeStrayed = Cum( RegimeFilled > 1 OR RegimeFilled < -1 );
RegimeRangeOK = LastValue( RegimeStrayed ) == 0;
Show the answer and why

Answer: The violation never occurred on any bar in the array

Cum() accumulates the Boolean across all bars and LastValue() reads the total, so zero means it never happened anywhere. Reducing an array to a single number is required before an assertion can use it, and choosing which reduction is choosing what the assertion means.

Sources for this lesson

8 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — User-defined functionsamibroker.com/guide/a_userfunctions.html2026-08-31
  2. 02AFL Function Reference — "#include"amibroker.com/guide/afl/_include.html2026-08-31
  3. 03AFL Function Reference — Versionamibroker.com/guide/afl/version.html2026-08-31
  4. 04AFL Function Reference — StDev§ AmiBroker 6.20 adds 3rd argument Population = Trueamibroker.com/guide/afl/stdev.html2026-08-31
  5. 05AFL Function Reference — Medianamibroker.com/guide/afl/median.html2026-08-31
  6. 06AFL Function Reference — NumToStr§ Third parameter separatoramibroker.com/guide/afl/numtostr.html2026-08-31
  7. 07AFL Function Reference — IsFiniteamibroker.com/guide/afl/isfinite.html2026-08-31
  8. 08AmiBroker User's Guide — Charts, sheets and layouts§ Portable chart files and include filesamibroker.com/guide/h_sheets.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.