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.
Five assumptions every formula makes
Section titled “Five assumptions every formula makes”Write them down, because you are making all five whether or not you have thought about them.
- The parameters are sane. Somebody could pass a period of zero, or a negative one.
- The denominator is not zero. Volatility can be zero. Volume can be zero. Price cannot, usually, but a broken quote can be.
- There is enough history. Every lookback function is empty at the start of the array.
- There are enough bars in this run.
BarCountis not a property of the symbol. - The data is not missing.
Nullexists, propagates, and does so silently.
The rest of the lesson takes them one at a time.
Guard 1: validate parameters
Section titled “Guard 1: validate parameters”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.
An assertion procedure
Section titled “An assertion procedure”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.
Assert the AmiBroker version too
Section titled “Assert the AmiBroker version too”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.
Guard 2: division
Section titled “Guard 2: division”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 ) |
Guard 3: warm-up
Section titled “Guard 3: warm-up”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
| Bar | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
Close | 10.0 | 10.2 | 10.1 | 10.4 | 10.9 | 11.0 | 10.7 | 11.2 |
MA(Close,4)3 leading Nulls | — | — | — | 10.18 | 10.40 | 10.60 | 10.75 | 10.95 |
Ready (BarIndex >= 4) | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
Guarded answerbar 3 discarded too | — | — | — | — | 0.50 | 0.40 | -0.05 | 0.25 |
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.
Guard 4: bar count
Section titled “Guard 4: bar count”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.
Guard 5: handle Null explicitly
Section titled “Guard 5: handle Null explicitly”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.
Putting all five together
Section titled “Putting all five together”Complete runnable 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();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.
Failing loudly: a hierarchy
Section titled “Failing loudly: a hierarchy”Not every problem deserves the same response. In descending order of severity:
- Refuse to run.
Version( x )for a version dependency. There is no point producing output that is known to be wrong. - Produce nothing, visibly. Return
Nulland let the plot show a gap. Best for indicators, because the gap is self-documenting. - Produce a chosen fallback and say so.
Nz( q, 50 )plus a title note. Appropriate when downstream code cannot cope with Null. - 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. - 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
Sources for this lesson
9 verified · checked 2026-08-31
- 01AFL Function Reference — IsFiniteamibroker.com/guide/afl/isfinite.html2026-08-31
- 02AFL Function Reference — Nzamibroker.com/guide/afl/nz.html2026-08-31
- 03AFL Function Reference — IsNullamibroker.com/guide/afl/isnull.html2026-08-31
- 04AFL Function Reference — NullCountamibroker.com/guide/afl/nullcount.html2026-08-31
- 05AFL Function Reference — BarIndex§ New in 5.30: BarIndex() now returns values always starting from zeroamibroker.com/guide/afl/barindex.html2026-08-31
- 06AFL Function Reference — Versionamibroker.com/guide/afl/version.html2026-08-31
- 07AFL Function Reference — _TRACEamibroker.com/guide/afl/_trace.html2026-08-31
- 08AmiBroker User's Guide — Common Coding Mistakes in AFLamibroker.com/guide/a_mistakes.html2026-08-31
- 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.