Skip to content
Level 3 · AFL DeveloperLessonPart 11 · page 4 of 528 min
28Minutes
20AFL functions
9Sources
StandardRequires
AFL functions taught here20

Defensive AFL: Guarding Against Bad Input and Bad Data

A formula that crashes has told you something. A formula that produces a number has told you nothing at all until you know the number could not have been produced by accident.

This lesson is about the second case, because it is the one that costs money. Every technique here has the same shape: identify an assumption the formula is making, and arrange for a broken assumption to be visible — as a gap in a plot, a line in the Log window, or a refusal to run — rather than as a plausible value nobody questions.

Write them down, because you are making all five whether or not you have thought about them.

  1. The parameters are sane. Somebody could pass a period of zero, or a negative one.
  2. The denominator is not zero. Volatility can be zero. Volume can be zero. Price cannot, usually, but a broken quote can be.
  3. There is enough history. Every lookback function is empty at the start of the array.
  4. There are enough bars in this run. BarCount is not a property of the symbol.
  5. The data is not missing. Null exists, propagates, and does so silently.

The rest of the lesson takes them one at a time.

Param() constrains the slider in the Parameters dialog, and that is real protection while your formula is used as intended. It is not protection at all once somebody copies your function into their own formula and calls it with a literal.

Fragment — not a complete formula

// The slider cannot produce a period below 2. A hand-edited copy can.
AveragePeriod = Param( "Average period", 50, 2, 400, 1 );

A validated parameter is one where the formula itself states the requirement. The cheapest way to state it, and the one that survives being copied, is an assertion.

AFL has no assert. It is four lines to write one:

Fragment — not a complete formula

procedure AssertTrue( ConditionValue, MessageText )
{
if( NOT ConditionValue )
{
_TRACE( "ASSERTION FAILED: " + MessageText );
}
}

Two design decisions in there are worth defending.

It is a procedure, not a function, because it returns nothing. AFL distinguishes the two, and using the right one documents intent.

And it takes a number, not an array — deliberately. if requires a single value, so passing an array raises Error 6, “Condition in IF/WHILE/FOR must be Numeric or Boolean”. That looks like a limitation and is actually the feature: an assertion that silently tested only the selected bar would be worse than no assertion, because it would pass on the bar you happen to be looking at and stay silent about the other four thousand.

Version( minrequired ) returns the running version number and, when given an argument, raises an error message when the formula runs on an older release. One line at the top of a formula converts “this silently behaves differently on 5.20” into “this refuses to run on 5.20 and says why”.

Fragment — not a complete formula

// BarIndex() has returned values starting from zero even under QuickAFL
// since 5.30, and the warm-up guard below depends on that.
Version( 5.30 );

Use it whenever your formula depends on documented behaviour that changed in a known release. Do not sprinkle it everywhere: a version assertion you cannot justify from the documentation is noise.

Division is where a formula most often produces a number that is not a number.

Fragment — not a complete formula

Stretch = ( Close - MA( Close, 50 ) ) / ATR( 14 );

ATR( 14 ) is zero for any symbol that has not moved at all over the window — a halted stock, an illiquid one that printed the same price fourteen times, a synthetic series. The quotient becomes infinite, and infinity plots as a spike that looks exactly like a discovery.

Two documented tools handle this, and they are not interchangeable.

IsFinite( x ) returns non-zero when x is not infinite. Use it when you want the guard visible in the code and the fallback under your control.

Nz( x, valueifnull = 0 ) converts Null, NaN and infinity to zero or to a value you choose. The official page gives the equivalence directly: Nz( (H-L)/(C-L) ) is the short form of IIf( IsFinite( (H-L)/(C-L) ), (H-L)/(C-L), 0 ).

The choice between them is a choice about what a missing answer should look like:

You want Write
A visible gap in the plot IIf( IsFinite( q ), q, Null )
Zero, because zero is meaningful here Nz( q )
A specific neutral value Nz( q, 50 ) or IIf( IsFinite( q ), q, 50 )
The screen to exclude the symbol IIf( IsFinite( q ), q >= Threshold, False )

Every lookback function has a period at the start of the array where it cannot have a real answer. MA( Close, 50 ) has no fiftieth prior bar on bar 3.

The way to state “I am only willing to answer from here” is BarIndex(), which returns a zero-based bar number — always from zero, even under QuickAFL, since version 5.30. That last clause is why the version assertion above exists.

Fragment — not a complete formula

RequiredBars = 50;
Ready = BarIndex() >= RequiredBars;
Answer = IIf( Ready, Something, Null );

NullCount( array, mode = 1 ) then lets the formula report its own warm-up: mode 1 counts consecutive Nulls at the start of the array. Printing that number in a title turns an invisible property into a visible one.

What a guarded calculation actually contains

What a guarded calculation actually contains
Bar01234567
Close10.010.210.110.410.911.010.711.2
MA(Close,4)3 leading Nulls10.1810.4010.6010.7510.95
Ready (BarIndex >= 4)00001111
Guarded answerbar 3 discarded too0.500.40-0.050.25
Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Note that the guarded answer discards bar 3 even though MA() produced a value there. That is deliberate: the fourth bar’s average is arithmetically correct but rests on the whole available history, which is a different quantity from a 4-bar average of a long series. Requiring RequiredBars full bars behind you is the conservative reading, and conservative is what “defensive” means.

BarCount is the number of bars delivered to this execution of this formula. It is not a property of the symbol. It changes with:

  • the symbol and its listing history
  • the chart’s zoom level
  • the Analysis window’s date range
  • QuickAFL, which may hand the formula far fewer bars than the database holds
  • the debugger’s “Limit BarCount to” preference, which defaults to 200

Any formula that indexes an array with a literal — Close[100] — or that assumes a minimum history is making a claim that can be false. Test it:

Fragment — not a complete formula

HaveEnoughBars = BarCount > RequiredBars;
AssertTrue( HaveEnoughBars, "chart has fewer bars than this indicator needs" );

And say so on screen, not only in the log. A blank pane with no explanation is indistinguishable from a broken formula; a blank pane whose title says “NOT ENOUGH BARS LOADED” is a diagnosis.

Null propagates. Any arithmetic involving Null is Null, and any comparison involving Null is false. That second rule is the dangerous one, because “false” is a perfectly ordinary answer that nothing flags.

Fragment — not a complete formula

// If Turnover is Null during warm-up, this is False - the same answer it
// would give for a genuinely illiquid symbol. Two different situations,
// one indistinguishable result.
Liquid = Turnover >= 5000000;

The fix is not to remove the Null. It is to decide, in writing, what a Null should mean for this condition, and then to say it:

Fragment — not a complete formula

// For a screen, "unknown" and "excluded" should behave identically, and
// that is a decision worth writing down rather than inheriting by accident.
Liquid = IIf( Ready, Turnover >= 5000000, False );

IsNull( x ) tests it directly, and is what you need when Null and zero must be told apart — SelectedValue() of a Null bar cannot be distinguished from a genuine zero by looking at the number.

Complete runnable AFL

defensive-stretch-indicator.afl
// defensive-stretch-indicator.afl
// Part 11 - Defensive AFL: Guarding Against Bad Input and Bad Data
//
// GOAL
// Measure how far price has travelled from its own moving average, expressed
// in units of the Average True Range so that the number means the same thing
// on a 3-unit share and a 300-unit share. Then guard every step, so that the
// indicator either shows a value you can trust or shows nothing at all and
// says why.
//
// THE DEFENSIVE CLAIM
// A plot that is blank where the data cannot support an answer is worth more
// than a plot that is complete and quietly wrong. Everything below exists to
// make the second outcome impossible.
//
// ASSUMPTIONS
// - Any instrument, any interval.
// - Distance from an average describes the past. It does not imply reversion,
// continuation, or anything else about the next bar.
// BarIndex() has returned values starting from zero even under QuickAFL since
// version 5.30, and the warm-up guard below depends on that. Saying so here
// turns a subtle wrong answer on an old build into a plain error message.
Version( 5.30 );
_SECTION_BEGIN( "Defensive stretch" );
// ---------------------------------------------------------------------------
// AssertTrue( ConditionValue, MessageText )
// Purpose : Record a broken assumption in the Log window instead of letting
// it travel silently into the plot.
// Inputs : ConditionValue - NUMBER, 1 when the assumption holds
// MessageText - STRING describing what should have been true
// Returns : nothing (it is a procedure)
// Notes : if() requires a number, so passing an array here raises Error 6.
// That is intentional: an assertion that quietly tests only one bar
// would be worse than no assertion.
// ---------------------------------------------------------------------------
procedure AssertTrue( ConditionValue, MessageText )
{
if( NOT ConditionValue )
{
_TRACE( "ASSERTION FAILED: " + MessageText );
}
}
AveragePeriod = Param( "Average period", 50, 2, 400, 1 );
VolatilityPeriod = Param( "ATR period", 14, 1, 200, 1 );
// The longest window decides how much history the indicator needs. Max() is
// documented as returning an ARRAY, and an array cannot be used in an if(), so
// the comparison is written out rather than delegated to it.
RequiredBars = AveragePeriod;
if( VolatilityPeriod > RequiredBars ) RequiredBars = VolatilityPeriod;
RequiredBars = RequiredBars + 1;
// Guard 1: the inputs themselves. Param() constrains the slider, but nothing
// stops a copy of this formula being edited by hand.
AssertTrue( AveragePeriod >= 2, "average period must be at least 2 bars" );
AssertTrue( VolatilityPeriod >= 1, "ATR period must be at least 1 bar" );
// Guard 2: is there enough history to answer at all? BarCount is the number of
// bars delivered to THIS execution, which changes with zoom and with QuickAFL,
// so it must be tested every run rather than assumed.
HaveEnoughBars = BarCount > RequiredBars;
AssertTrue( HaveEnoughBars, "chart has fewer bars than this indicator needs" );
Average = MA( Close, AveragePeriod );
AverageRange = ATR( VolatilityPeriod );
// Guard 3: warm-up. Neither MA nor ATR has a usable value over its first bars,
// and Null propagates through every comparison that follows, so the formula
// states explicitly where it is willing to answer.
Ready = BarIndex() >= RequiredBars;
// Guard 4: division. A symbol that has not moved for a whole ATR window has an
// average true range of zero. The quotient would then be infinite, which draws
// a spike that looks like a discovery.
Distance = Close - Average;
Quotient = Distance / AverageRange;
Stretch = IIf( Ready AND IsFinite( Quotient ), Quotient, Null );
// Guard 5: report the warm-up rather than hiding it. NullCount returns how many
// consecutive Null bars sit at the start of the array.
LeadingNulls = NullCount( Stretch );
Plot( Close, "Close", colorDefault, styleCandle );
Plot( Average, "Average", colorBlueGrey, styleLine );
// The stretch line is drawn on its own scale over the price pane, so its
// height is not comparable with price. The reading that matters is stated in
// words in the title, because a position on an unlabelled scale is not
// information everyone can read.
Plot( Stretch, "Stretch, ATR units", colorOrange, styleLine | styleOwnScale );
SelectedStretch = SelectedValue( Stretch );
SelectedIsNull = SelectedValue( IsNull( Stretch ) );
_N( Title =
Name() + " - " + Interval( 2 ) + " - distance from the " +
NumToStr( AveragePeriod, 1.0 ) + "-bar average, in ATR(" +
NumToStr( VolatilityPeriod, 1.0 ) + ") units\n" +
WriteIf( HaveEnoughBars, "",
"NOT ENOUGH BARS LOADED - this pane is deliberately blank\n" ) +
StrFormat( "Bars loaded: %g bars needed: %g warm-up bars discarded: %g\n",
BarCount, RequiredBars, LeadingNulls ) +
WriteIf( SelectedIsNull,
"Selected bar: no reading. Either warm-up, or the average range was zero.",
"Selected bar: " + NumToStr( SelectedStretch, 1.2 ) +
" ATR units from the average." ) );
_SECTION_END();

Download defensive-stretch-indicator.afl112 lines

The calculation is trivial: how far is price from its own moving average, measured in ATR units. Everything else in the file is a guard, and each one is labelled with the number it corresponds to above.

Read the title block last. It reports bars loaded, bars needed and warm-up bars discarded on every run, and it states in words when the pane is blank on purpose. That is the part most formulas omit, and it is the part that stops somebody — including you, later — from concluding that a blank pane means the symbol has no data.

Not every problem deserves the same response. In descending order of severity:

  1. Refuse to run. Version( x ) for a version dependency. There is no point producing output that is known to be wrong.
  2. Produce nothing, visibly. Return Null and let the plot show a gap. Best for indicators, because the gap is self-documenting.
  3. Produce a chosen fallback and say so. Nz( q, 50 ) plus a title note. Appropriate when downstream code cannot cope with Null.
  4. Produce the answer and log a warning. AssertTrue(). For assumptions that are usually true and whose violation is worth knowing about but not worth stopping for.
  5. Exclude the symbol. In a screen, IIf( Ready, Condition, False ). The symbol simply does not appear.

What is not on this list is producing a plausible number with no indication that anything was wrong. That is the outcome every guard here exists to prevent.

Defensive AFL is not about adding checks everywhere. It is about naming the five assumptions every formula makes — sane parameters, a non-zero denominator, enough history, enough bars in this run, present data — and then choosing, for each one, what a violation should look like on screen. Version() refuses. Null leaves a visible gap. Nz() substitutes a value you chose. IsFinite() catches the infinities that division produces. BarIndex() states where you are willing to answer, and NullCount() reports how much you discarded. _TRACE records what you would otherwise never learn. The one option that is never acceptable is the plausible wrong number.

Check your understanding

Question 1. A screening filter reads Liquid = Turnover >= 5000000; where Turnover is Null during its warm-up. What actually happens on the warm-up bars?
Show the answer and why

Answer: The comparison is false, which is the same answer an illiquid symbol gives — the two situations become indistinguishable

A comparison involving Null is false. That is a perfectly ordinary answer, which is exactly the problem: nothing distinguishes "not enough history to know" from "known to be illiquid". Writing IIf( Ready, Turnover >= Threshold, False ) makes the decision explicit rather than accidental.

Question 2. Which of these are documented behaviours of Nz()? Select all that apply.
Show the answer and why

Answer: It converts Null, NaN and Infinity, Its second argument defaults to zero, Since AmiBroker 6.90 the valueifnull argument may itself be an array

Nz( x, valueifnull = 0 ) converts Null/NaN/Infinity and, since 6.90, accepts an array as the replacement. It cannot prevent an operation — the value is computed first and then replaced, which is why the official page presents it as shorthand for an IIf( IsFinite( ... ) ) expression.

Question 3. Why is AssertTrue() written to take a number rather than an array?
procedure AssertTrue( ConditionValue, MessageText )
{
    if( NOT ConditionValue ) _TRACE( "ASSERTION FAILED: " + MessageText );
}
Show the answer and why

Answer: Because if() requires a single value, so passing an array raises Error 6 rather than silently testing one bar

The restriction is the feature. An assertion that quietly collapsed an array to one bar would pass on the bar you are looking at and stay silent about the rest. Error 6 forces you to state which summary of the array you actually mean — usually via LastValue( Cum( ... ) ).

Question 4. A formula works on your daily chart and produces nothing when run in the debugger. Which explanation should you check first?
Show the answer and why

Answer: The debugger limits BarCount to 200 bars by default, which may be fewer than the formula's warm-up requires

The documented default for "Limit BarCount to" in Tools -> Preferences -> Debugger is 200 bars. A formula needing 252 bars of warm-up is legitimately empty under those conditions. (The debugger runs with actionBacktest, not actionIndicator — that is a different documented gotcha.)

Question 5. True or false: guarding a division with IIf( D != 0, N / D, 0 ) stops AmiBroker from performing the division on bars where D is zero.
Show the answer and why

Answer: False

IIf() evaluates both branches for every bar and then selects between the results. The division still happens; only its result is discarded. This is why guards are written in terms of the result — IsFinite( q ) — rather than in terms of avoiding the operation.

Sources for this lesson

9 verified · checked 2026-08-31

  1. 01AFL Function Reference — IsFiniteamibroker.com/guide/afl/isfinite.html2026-08-31
  2. 02AFL Function Reference — Nzamibroker.com/guide/afl/nz.html2026-08-31
  3. 03AFL Function Reference — IsNullamibroker.com/guide/afl/isnull.html2026-08-31
  4. 04AFL Function Reference — NullCountamibroker.com/guide/afl/nullcount.html2026-08-31
  5. 05AFL Function Reference — BarIndex§ New in 5.30: BarIndex() now returns values always starting from zeroamibroker.com/guide/afl/barindex.html2026-08-31
  6. 06AFL Function Reference — Versionamibroker.com/guide/afl/version.html2026-08-31
  7. 07AFL Function Reference — _TRACEamibroker.com/guide/afl/_trace.html2026-08-31
  8. 08AmiBroker User's Guide — Common Coding Mistakes in AFLamibroker.com/guide/a_mistakes.html2026-08-31
  9. 09AmiBroker User's Guide — Understanding how AFL worksamibroker.com/guide/h_understandafl.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.