Project: Your Personal AFL Utility Library
Objective
Section titled “Objective”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.
Prerequisites
Section titled “Prerequisites”- User-defined functions
- Variable scope —
localmatters enormously here - Include files and building a library —
the
#includemechanics and where AmiBroker looks - Defensive AFL — the fallback policy this library implements
Scope: what goes in and what stays out
Section titled “Scope: what goes in and what stays out”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
- IN — conventions you want applied identically everywhereSafe division, warm-up guards, normalisation, formatting. Change once, every formula follows.
- IN — definitions you want to be able to point atWhat "liquid" means, what "uptrend" means. Named, so a study can cite its own definitions.
- 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.
- OUT — trading rulesBuy/Sell logic in an include file is a rule you will eventually forget you are running.
- OUT — anything AFL already doesA wrapper around MA() that adds nothing is a name to remember for no benefit.
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.
The conventions, stated before the code
Section titled “The conventions, stated before the code”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.
The library
Section titled “The library”Complete runnable 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.aflWhat each function is for
Section titled “What each function is for”| 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. |
Four details worth reading closely
Section titled “Four details worth reading closely”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.
Installation
Section titled “Installation”-
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. -
Save the file as
course-library.aflin that folder. -
Set the standard include path under Tools → Preferences → AFL to the same folder.
-
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
#includetakes single backslashes, unlike every other string in AFL:Fragment — not a complete formula
#include "C:\AmiBroker\Formulas\Custom\course-library.afl" -
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.
The test formula
Section titled “The test formula”Complete runnable 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();How the checks work
Section titled “How the checks work”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.
Running it both ways
Section titled “Running it both ways”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.
Test it
Section titled “Test it”Verify the include is real
Section titled “Verify the include is real”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.
Verify local is doing its job
Section titled “Verify local is doing its job”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.
Verify the division guard on real data
Section titled “Verify the division guard on real data”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.
Verify the version assertion
Section titled “Verify the version assertion”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.
Common errors
Section titled “Common errors”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.
Extensions
Section titled “Extensions”-
Add
LibDrawdown( EquityArray )returning the percentage below the running peak. It is three lines withHHV(), and it is the measure Part 34 leans on hardest. Write the header first: what does it return during warm-up? -
Add
LibRelativeStrength( BenchmarkTicker, LookbackPeriod )usingForeign(). 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. -
Split the library in two once it exceeds about fifteen functions:
course-library.aflfor general helpers and a second file for whatever domain grew. Use#include_oncein both so a formula including both does not define anything twice. -
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. -
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.
What changed
Section titled “What changed”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
Sources for this lesson
8 verified · checked 2026-08-31
- 01AmiBroker User's Guide — User-defined functionsamibroker.com/guide/a_userfunctions.html2026-08-31
- 02AFL Function Reference — "#include"amibroker.com/guide/afl/_include.html2026-08-31
- 03AFL Function Reference — Versionamibroker.com/guide/afl/version.html2026-08-31
- 04AFL Function Reference — StDev§ AmiBroker 6.20 adds 3rd argument Population = Trueamibroker.com/guide/afl/stdev.html2026-08-31
- 05AFL Function Reference — Medianamibroker.com/guide/afl/median.html2026-08-31
- 06AFL Function Reference — NumToStr§ Third parameter separatoramibroker.com/guide/afl/numtostr.html2026-08-31
- 07AFL Function Reference — IsFiniteamibroker.com/guide/afl/isfinite.html2026-08-31
- 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.