Skip to content
Level 3 · AFL DeveloperLessonPart 08 · page 9 of 928 min
28Minutes
16AFL functions
6Sources
StandardRequires
AFL functions taught here16

Errors, Debugging and Sanity Checks

There are two kinds of broken formula, and they need completely different treatment.

The first kind will not run. AmiBroker refuses it, names a line, and you fix it. These are irritating and cheap.

The second kind runs perfectly, produces a chart, fills a trade list, and is wrong. Nothing warns you. This is the expensive kind, and the whole point of this lesson is that you cannot find it by staring at the formula harder. You find it by making the formula show you its intermediate values and then checking those against something you already know.

The Formula Editor has Tools → Check syntax (Ctrl+F5 in the default keyboard layout, and also available as a toolbar button). It compiles the formula and reports the first problem it finds, with a line number.

Two things about it are worth internalising early.

It checks the formula, not your idea. A formula that computes a 200-period average of the wrong array passes syntax checking with no complaint whatsoever.

And it runs the formula against whatever data the editor has to hand, which is not the full history. This matters because a formula that indexes into an array — Close[500], say — can pass here and fail on a symbol with fewer bars, or vice versa. Never treat “check syntax passed” as “the formula is correct”. Treat it as “the formula is spelled correctly”.

The exact wording belongs to your AmiBroker version, so match on the shape rather than the sentence.

“Error 30. Syntax error” and its relatives point at a line where AmiBroker stopped being able to parse. The real fault is very often on the line above — a missing semicolon means the parser reads two statements as one and only notices when the grammar breaks. When the named line looks fine, read the line before it.

“Variable … used without having been assigned” means exactly what it says, and the usual cause is a typo. AFL identifiers are case-insensitive, so MyPeriod and myperiod are the same variable — this is not the cause. MyPeriod and MyPeriodd are not, and that usually is.

“Function … called with too few / too many arguments” almost always means you have mis-remembered a signature. Do not guess at it: put the cursor on the function name and press F1, which opens that function’s page in the AFL Function Reference. Getting into this habit now saves you from an entire category of self-inflicted wound later.

This one is documented in the same official page, and it is worth reproducing because it produces a formula that runs, looks right, and does the wrong thing:

Fragment — not a complete formula

// WRONG. AND binds tighter than OR, so this reads as:
// ( Close > MA( Close, 10 ) ) OR ( bigMove AND Volume > MA( Volume, 10 ) )
Buy = Close > MA( Close, 10 ) OR Close == 1.1 * Ref( Close, -1 )
AND Volume > MA( Volume, 10 );

The author wanted the volume condition to apply to both branches. It applies to one.

Fragment — not a complete formula

// CORRECT. Parentheses say what was meant.
Buy = ( Close > MA( Close, 10 ) OR Close == 1.1 * Ref( Close, -1 ) )
AND Volume > MA( Volume, 10 );

The rule to adopt is not “learn the precedence table”. It is: when a condition mixes AND with OR, parenthesise it even when you do not have to. The cost is four characters. The benefit is that the next person to read it — which is you, in six months — cannot get it wrong.

if, while and for need a single true/false value. They cannot take an array, because there would be no way to decide what to do with an array that is true on some bars and false on others. This is documented explicitly.

Fragment — not a complete formula

// WRONG. Close > Open is an array of BarCount values.
if( Close > Open )
BarTint = colorGreen;
else
BarTint = colorRed;

Fragment — not a complete formula

// CORRECT. IIf() decides per bar, which is what was wanted.
BarTint = IIf( Close > Open, colorGreen, colorRed );

The mirror image of this mistake is using IIf() to build a string. IIf() returns an array of numbers; it cannot hold text. WriteIf() is the string version, and it returns one string built from the selected bar, not an array of strings.

Now the expensive kind. A formula runs; the output is plausible; it is wrong. There are four diagnostic tools, in increasing order of effort.

Escalation ladder for a formula that runs but is wrong

  1. Count somethingHow many bars is the condition true on? Zero and BarCount are both alarming answers.
  2. Plot the intermediatesNot just the final signal — the average, the ratio, the raw indicator. Bugs are visible in the middle of a calculation and invisible at the end.
  3. Trace it_TRACE and _TRACEF write to Window -> Log. Cheap, always available, works in every Analysis mode.
  4. Run it under the debuggerBreakpoints, single stepping, the Watch window and its Arrays tab. Reach for this when you need to watch a loop.

The fastest sanity check in AFL is a count. Before you look at performance, ask how often your condition is even true:

Fragment — not a complete formula

Setup = Close > MA( Close, 50 ) AND RSI( 14 ) > 50;
SetupBars = LastValue( Cum( Nz( Setup ) ) );
_TRACEF( "Setup true on %g of %g bars", SetupBars, BarCount );

Nz() is there because Cum() propagates Null: a single empty value at the front of the array would otherwise make the running total empty for the entire rest of the array. That is itself a bug people spend an hour on.

Interpret the count immediately. Zero means a condition never fires — usually a filter that contradicts itself, or a warm-up problem. BarCount means a condition is always true — usually the = versus == error, or a comparison against the wrong array. Anything between is not proof of correctness, but at least it is not proof of failure.

A chart formula gives you a free debugger, and almost nobody uses it. If Buy is never true, do not stare at the Buy line. Plot the pieces:

Fragment — not a complete formula

// Not the signal — the ingredients of the signal.
Plot( RSI( 14 ), "RSI", colorOrange, styleLine | styleOwnScale | styleNoLabel );
Plot( Setup, "Setup", colorGreen, styleArea | styleOwnScale | styleNoLabel );

The moment you can see that RSI is flat at 50 for the whole chart, or that Setup is a solid green block, you know where the fault is. Reading the shape of an intermediate array is faster than reasoning about it.

_TRACE( "string" ) writes a line of text. _TRACEF( "format", args… ) does the same with printf-style formatting, so you can print numbers without string-concatenation gymnastics. Both write to the system debug viewer and to AmiBroker’s own Log window, which you open with Window → Log. You do not need any external tool to use them.

One documented convenience is worth knowing on day one: _TRACE( "!CLEAR!" ); clears the internal Log window. Put it at the top of the formula and every run starts from an empty log, so you can never mistake the previous run’s output for this one’s.

Fragment — not a complete formula

_TRACE( "!CLEAR!" );
_TRACEF( "BarCount = %g, first bar = %s", BarCount, DateTimeToStr( DateTime()[0] ) );

For anything involving a loop, the visual debugger earns its keep. In the Formula Editor, Debug → Go (F5) runs under the debugger, F9 toggles a breakpoint, F10 steps over a line and F11 steps into a user-defined function. Hovering over a variable shows its type and value. Window → Watch opens the Watch window, and its Arrays tab shows watched arrays as a table with bar numbers and timestamps — which is the single most useful view in AmiBroker for understanding what an array actually contains.

Four documented details that will otherwise waste your afternoon:

  • BarCount under the debugger defaults to 200 bars. It is a preference — Tools → Preferences, the Debugger page, “Limit BarCount to”. A formula that needs 250 bars of warm-up will look completely empty under the debugger until you raise it.
  • Breakpoints only bind to certain lines: regular statements ending in a semicolon, for, while, do-while (on the while clause), if, return, switch/case and break. A breakpoint on a blank line, a comment or a lone brace will not trigger. If your red circle is never hit, check the line type before you conclude the code is not running.
  • As of version 6.10 the debugger runs the code in the actionBacktest context. Code guarded by if( Status( "action" ) == actionIndicator ) will not execute, and neither will a breakpoint inside it.
  • GetBacktesterObject() returns an empty object outside the second phase of a portfolio backtest, so custom-backtester code cannot be fully debugged this way. Part 36 says more.

This formula is deliberately ordinary — a moving average, an RSI and a condition combining them. What makes it worth reading is that every step announces itself.

Complete runnable AFL

debug-harness.afl
// debug-harness.afl
// Part 8 - Errors, Debugging and Sanity Checks
//
// Purpose: a small, ordinary calculation instrumented so that every step can
// be SEEN rather than guessed at. The pattern - trace the shape of
// the data, plot the intermediates, never assume a bar count -
// transfers to formulas of any size.
// Assumes: any symbol and interval. Open Window -> Log before applying, so
// the trace output has somewhere to appear.
// Warning: a chart formula re-runs on every redraw, so tracing from a chart
// produces a lot of output. Remove the trace lines, or comment them
// out, once the formula behaves.
_SECTION_BEGIN( "Debug Harness" );
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------
MaPeriod = 20;
RsiPeriod = 14;
// ---------------------------------------------------------------------------
// Instrumentation
// ---------------------------------------------------------------------------
_TRACE( "!CLEAR!" ); // wipe the Log window so this run is not read as the last one
_TRACEF( "Run started. BarCount = %g", BarCount );
// BarCount is not a constant of the universe. Syntax checking uses at most a
// couple of hundred recent bars, the debugger defaults to 200, and a zoomed
// chart can be shorter still. Say so out loud instead of assuming.
if( BarCount < MaPeriod )
{
_TRACEF( "Only %g bars available - MA(%g) will be empty everywhere.",
BarCount, MaPeriod );
}
// ---------------------------------------------------------------------------
// The calculation under inspection
// ---------------------------------------------------------------------------
Average = MA( Close, MaPeriod );
Momentum = RSI( RsiPeriod );
_TRACEF( "MA(%g): %g empty bars at the front.", MaPeriod, NullCount( Average, 1 ) );
_TRACEF( "RSI(%g): %g empty bars at the front.", RsiPeriod, NullCount( Momentum, 1 ) );
Setup = Close > Average AND Momentum > 50;
// A count is the fastest sanity check there is. Zero and BarCount are both
// suspicious answers; anything in between at least deserves a look.
SetupBars = LastValue( Cum( Nz( Setup ) ) );
_TRACEF( "Setup is true on %g of %g bars.", SetupBars, BarCount );
// ---------------------------------------------------------------------------
// Drawing - including the intermediates
// ---------------------------------------------------------------------------
Plot( Close, "Close", colorDefault, styleCandle );
Plot( Average, "MA(" + MaPeriod + ")", colorBlue, styleLine | styleThick );
// Plotting an intermediate array finds bugs that staring at the final answer
// never will: if RSI is flat, or Setup is on for every bar, you see it here.
Plot( Momentum, "RSI(" + RsiPeriod + ")", colorOrange,
styleLine | styleOwnScale | styleNoLabel );
Plot( Setup, "Setup", colorGreen,
styleArea | styleOwnScale | styleNoLabel );
Title = StrFormat(
"{{NAME}} {{DATE}} Close %g MA %g RSI %.1f Setup %g (%g setup bars)",
Close, Average, Momentum, Setup, SetupBars );
_SECTION_END();

Download debug-harness.afl74 lines

It clears the log, prints BarCount, warns if there are not enough bars for the average it is about to compute, reports how many leading Null values each indicator has using NullCount( array, 1 ), counts the bars on which the setup is true, and then plots both intermediates alongside price rather than only the final answer.

None of that is clever. All of it is the difference between “the formula doesn’t work” and “the RSI is Null for the first 14 bars and my Cum() was swallowing the whole array”.

When a formula is wrong and you do not know why, work down this list. It is ordered by how often each step finds the fault.

  1. Does it run at all? Check syntax. Read the line above the reported one.
  2. Are = and == right everywhere? Search the formula for = inside IIf( and inside any comparison.
  3. Are AND/OR mixes parenthesised? If not, parenthesise them and see whether the answer changes. If it changes, one of the two readings was wrong.
  4. Count your conditions. Zero and BarCount are both bugs.
  5. How many bars of warm-up? NullCount( x, 1 ) for each indicator. Compare with the length of the array you are testing over.
  6. Plot the intermediates. Look at the shapes, not the numbers.
  7. Check the array lengths you assumed. BarCount is not a constant — it changes with the symbol, the interval, the date range, QuickAFL and the debugger’s own limit. Never hard-code a bar index you have not guarded.
  8. Trace the values at the boundaries. First bar, last bar, the bar where the answer first goes wrong.
  9. Only then use the debugger, and only if there is a loop or a function call chain that the previous eight steps could not resolve.

Syntax errors are cheap: read the line above the one AmiBroker names, and press F1 on any function whose signature you are not certain of. Silent errors are expensive, and there are only two ways to find them — make the formula report what it computed, or compare its answer with something you can verify by hand. Counting is the cheapest report, plotting intermediates is the fastest, _TRACE/_TRACEF into the Log window is the most flexible, and the debugger is what you reach for when a loop is involved. And “check syntax passed” means the formula is spelled correctly. It says nothing at all about whether it is right.

Check your understanding

Question 1. A formula runs without error but its Buy condition is true on every single bar. Which cause fits that symptom best?
Buy = IIf( Trend = 1, True, False );
Show the answer and why

Answer: A single = where == was meant, so the assignment result (non-zero) is tested every bar

Assignment inside an expression is legal AFL, so there is no error message. The expression evaluates to the assigned value, 1, which is true on every bar. This is the first mistake listed on AmiBroker's own Common Coding Mistakes page.

Question 2. Which of these will AmiBroker reject with an error rather than run silently? Select all that apply.
Show the answer and why

Answer: if( Close > Open ) BarTint = colorGreen; else BarTint = colorRed;, for( i = 0; i < BarIndex(); i++ ) { }, Label = IIf( Close > Open, "up", "down" );

if and for need a single value, and both Close > Open and BarIndex() are arrays. IIf() returns numbers, not strings, so the fourth is also invalid — WriteIf() is the string version. Only the IIf() colour assignment is correct, and it is the documented fix for the first.

Question 3. You set a breakpoint, press F5, and the debugger never stops there. Which explanations are documented behaviour? Select all that apply.
Show the answer and why

Answer: The breakpoint is on a comment line, a blank line or a lone brace, The code is inside if( Status( "action" ) == actionIndicator ), and the debugger runs in the actionBacktest context, The condition guarding that block is never true for the data being debugged

Breakpoints bind only to statements, loops, if, return, switch/case and break. Since 6.10 the debugger runs with actionBacktest, so indicator-only and portfolio-only blocks are skipped. There is no line-count limit.

Question 4. Why does this line use Nz() before Cum()?
SetupBars = LastValue( Cum( Nz( Setup ) ) );
Show the answer and why

Answer: Because Cum() propagates Null, so one empty leading value would make the running total empty for the whole rest of the array

A running total that meets a Null carries it forward. Nz() replaces empty values with zero so the count survives the warm-up period. Booleans are already numeric in AFL, so no conversion is needed.

Question 5. True or false: a formula that passes Check syntax has been verified to compute what you intended.
Show the answer and why

Answer: False

Check syntax verifies that the formula parses and runs. It has no knowledge of your intent, and it runs against a limited amount of data, so it can also miss bar-count problems that appear on a full history.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — Common Coding Mistakes in AFLamibroker.com/guide/a_mistakes.html2026-08-31
  2. 02AmiBroker User's Guide — How to use AFL debuggeramibroker.com/guide/h_debugger.html2026-08-31
  3. 03AFL Function Reference — _TRACEamibroker.com/guide/afl/_trace.html2026-08-31
  4. 04AFL Function Reference — _TRACEFamibroker.com/guide/afl/_tracef.html2026-08-31
  5. 05AFL Function Reference — NullCountamibroker.com/guide/afl/nullcount.html2026-08-31
  6. 06AmiBroker User's Guide — AFL language reference§ Operator precedenceamibroker.com/guide/a_language.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.