Skip to content
Level 3 · AFL DeveloperLessonPart 36 · page 5 of 628 min
28Minutes
13AFL functions
7Sources
StandardRequires
AFL functions taught here13

Advanced Debugging and Tracing

Part 8 covered the errors that stop a formula. This lesson is about the harder problem: a formula that runs, produces numbers, and produces the wrong ones. There is no error message to read and no line to jump to. What you have instead is a debugger with specific capabilities and specific blind spots, two tracing functions, and — the part that actually solves most of these — a method that does not depend on either.

The integrated visual debugger arrived in AmiBroker 6.10. It runs from the Formula Editor: Debug → Go, or F5. The other three shortcuts are F9 to toggle a breakpoint, F10 to step over, F11 to step into.

The difference between stepping over and stepping into shows up only at a call to a user-defined function: Step Into stops at the first line inside it, Step Over runs it as one step. Built-in functions are opaque either way.

Where breakpoints work, and where they do not

Section titled “Where breakpoints work, and where they do not”

The list is documented and it is exhaustive. Breakpoints work on:

  • regular statements ending in a semicolon — for a multi-line statement, put it on the first line
  • for loops, while loops
  • do-while loops — place it on the while clause, because the do line is a no-op, or on the first statement inside the block
  • if statements, return statements, switch/case statements, break statements

Anywhere else the breakpoint never triggers. The editor refuses to place one on an empty line, a // comment line, or a line containing nothing but a brace.

The documented trick for a conditional breakpoint is to build the condition into the code:

Fragment — not a complete formula

if( reset > 0 AND param > 4 )
{
cs = 0; // put the breakpoint on THIS line - it stops only when the condition holds
}

Window → Watch opens the Watch window. Double-click an empty row to add a name, drag a variable or expression from the editor, press DELETE to remove one. The expression evaluator handles arithmetic, parentheses, array subscripts with variable indexes such as Close[ i + 2 ], matrix subscripts such as mat[ i + 1 ][ j ], and mixed types like "Value is = " + Close[ i + 2 ].

Changed values are highlighted with a yellow background; scalars turn green when they increase and red when they decrease. The Arrays tab renders array contents as an exploration-like table with bar numbers and timestamps — but only the first 20 arrays from the Watch window are reported, so a Watch list of thirty arrays silently shows you twenty.

The context trap. Since version 6.10 the debugger runs code in the actionBacktest context. Any breakpoint inside if( Status("action") == actionIndicator ) or == actionPortfolio will therefore never be hit. You can temporarily disable the check to get inside — but the guide adds that “you still cannot access certain objects because they are simply not available”, and names the specific case: GetBacktesterObject() returns an empty object anywhere outside the second phase of a portfolio backtest in the New Analysis window. The custom backtester genuinely cannot be stepped through this way.

The 200-bar default. Tools → Preferences has a Debugger page whose “Limit BarCount to” setting defaults to 200 bars. Code that assumes a large BarCount fails under the debugger with a subscript error while working fine on a chart. That is not a bug in the debugger; it is your formula’s assumption being caught, which is one of the more useful things the debugger does for free.

The same page controls whether the debugger uses the base time interval — the default, “because there can be no chart open at all” — or the current chart interval; whether the Arrays tab auto-scrolls to the first changed item; and whether breakpoints, bookmarks and watches are saved beside the formula in a .dbg file. That file is deleted automatically when nothing is left to save.

Three functions print text, and they do not go to the same place.

Call Goes to Since
_TRACE( "string" ) The Win32 OutputDebugString API — so a system debug viewer — and AmiBroker’s own Window → Log 4.40
_TRACEF( "format", arg1, ... ) The same, with printf-style formatting 6.0
printf( "format", ... ) Commentary, Interpretation, or the debugger’s Output window 4.50

To read _TRACE output outside AmiBroker you need a system debug viewer; Microsoft’s free DebugView is the one the documentation names. Inside AmiBroker, Window → Log shows it, and there is a documented way to clear that window from code:

Fragment — not a complete formula

_TRACE( "!CLEAR!" ); // empties the internal Log window

Put that at the top of a formula you are tracing and everything you then read belongs to the current run. Without it you will eventually diagnose a problem from last run’s output.

Formatting has its own rules, inherited from printf. “For numbers always use %f, %e or %g formatting, %d or %x will not work because there are no integers in AFL.” A literal percent sign is %%. Since 6.10 the format string is validated, and a mismatch between specifiers and arguments raises Error 61. The %s specifier for strings works only from 6.20.

That validation has a practical consequence: never pass data as the format string. If a variable might contain a %, write printf( "%s\n", Data ); rather than printf( Data );.

StrFormat uses the same rules but returns the string instead of printing it, which is what you want when the destination is a Title or a column.

When a hundred-line formula gives a wrong number, the instinct is to read it. Reading finds typos. It does not find wrong assumptions, because you will re-read the assumption the same way you wrote it.

Bisection does. It is mechanical, it does not require you to be clever, and it converges in about seven steps on a 100-line formula.

Bisecting a wrong result

  1. 1. Make the fault reproducibleOne symbol, one interval, one date. Fix the range so QuickAFL cannot change the answer between runs.
  2. 2. State the expected valueWrite down what the number should be at one specific bar, and how you know. If you cannot, that is the actual problem.
  3. 3. Halve the formulaComment out the second half. Plot or trace the last surviving intermediate value. Is it already wrong?
  4. 4. Recurse into the half that is wrongIf the intermediate is correct the fault is downstream; if not, upstream. Repeat.
  5. 5. Stop at one expressionYou now have a single line whose inputs are right and whose output is wrong. That is a small enough question to answer.

Three things make it work in AFL specifically.

Every intermediate is plottable. An AFL variable is one number per bar, so any intermediate can be sent to a chart pane with Plot and read off at a chosen bar. You do not need the debugger to see it.

Warm-up shows up as gaps. Nulls are drawn as gaps rather than zeros, so a plotted intermediate makes its warm-up period visible immediately — and warm-up is a common cause of a result that is wrong only near the left edge of a test.

The Analysis window’s Exploration mode is a table of intermediates. Filter = 1; plus one AddColumn per intermediate gives you every step of the calculation, bar by bar, for the period you are suspicious of. For array code this is frequently faster than stepping.

Some formulas are too tangled to bisect usefully, and some are correct-looking in every intermediate but wrong overall. For those, build a harness: implement the same quantity a second time, by a different route, and compare the two automatically.

Complete runnable AFL

formula-test-harness.afl
// formula-test-harness.afl
// Part 36 - Advanced Debugging and Tracing
//
// A harness that checks one AFL expression against an independent
// implementation of the same thing, and reports the worst disagreement.
//
// THE SUBJECT UNDER TEST
// AmiBroker's "Understanding how AFL works" tutorial gives the explicit
// recursion that AMA( array, factor ) is equivalent to:
//
// y[i] = factor * x[i] + ( 1 - factor ) * y[i-1]
//
// If the two agree to within floating-point tolerance, the loop is a
// trustworthy reference implementation. The point is not the result - it is
// the shape. Reuse this shape on code of your own that has no built-in twin,
// with the reference implementation written from the definition rather than
// copied from the code being tested.
//
// ASSUMPTIONS
// - Daily end-of-day bars. No feed, no Professional edition.
// - AFL numbers are 32-bit floats, so "equal" means "agrees to within
// Tolerance". Never test two floating-point results for exact equality.
// - QuickAFL is switched off here on purpose: a recursive average started
// from a different first bar is a different calculation, and the harness
// would report that as a failure of the formula rather than of the range.
_SECTION_BEGIN("Formula Test Harness");
// sbrAll for both arguments requires all past and future bars, which turns
// QuickAFL off for this formula.
SetBarsRequired( sbrAll, sbrAll );
Factor = Param( "Smoothing factor", 0.1, 0.01, 0.5, 0.01 );
Tolerance = Param( "Tolerance", 0.0001, 0.000001, 0.01, 0.000001 );
TraceBars = Param( "Trace first N bars", 5, 0, 50, 1 );
// The documented way to empty the internal Log window before a run, so what
// you read afterwards belongs to this execution and not the previous one.
_TRACE( "!CLEAR!" );
// --- Implementation A: the built-in ----------------------------------------
GetPerformanceCounter( True ); // reset the counter to zero
Builtin = AMA( Close, Factor );
TimeA = GetPerformanceCounter( True ); // read elapsed, and reset again
// --- Implementation B: the explicit recursion ------------------------------
// Copying Close allocates an array of the right length; every element from
// bar 0 upwards is overwritten below.
Manual = Close;
Manual[ 0 ] = Close[ 0 ]; // seed: bar 0 has no predecessor
for( i = 1; i < BarCount; i++ )
{
Manual[ i ] = Factor * Close[ i ] + ( 1 - Factor ) * Manual[ i - 1 ];
// Tracing inside a bar loop is the fastest way to make a formula slow.
// Guard it, and keep the guard in the code rather than commenting the
// trace out and back in.
if( i <= TraceBars )
_TRACEF( "bar %g close %g builtin %g manual %g",
i, Close[ i ], Builtin[ i ], Manual[ i ] );
}
TimeB = GetPerformanceCounter( True );
// --- Compare ----------------------------------------------------------------
Difference = abs( Builtin - Manual );
// Highest() is a running maximum, so its last element is the worst
// disagreement anywhere in the array.
RunningWorst = Highest( Difference );
Worst = RunningWorst[ BarCount - 1 ];
Agrees = Worst <= Tolerance;
_TRACEF( "array %g ms, loop %g ms, worst difference %g over %g bars",
TimeA, TimeB, Worst, BarCount );
Plot( Builtin, "AMA built-in", colorBlue, styleLine | styleThick );
Plot( Manual, "AMA explicit loop", colorRed, styleDashed );
Plot( Difference, "|difference|", colorOrange, styleLine | styleOwnScale );
Title =
"Worst absolute difference: " + NumToStr( Worst, 1.8 ) + "\n" +
"Tolerance: " + NumToStr( Tolerance, 1.8 ) + "\n" +
"Verdict: " + WriteIf( Agrees, "AGREES", "DISAGREES" ) + "\n" +
"Array time / loop time: " + NumToStr( TimeA, 1.3 ) + " ms / "
+ NumToStr( TimeB, 1.3 ) + " ms\n" +
"Bars compared: " + NumToStr( BarCount, 1.0 );
_SECTION_END();

Download formula-test-harness.afl91 lines

The subject under test is AMA( Close, Factor ). AmiBroker’s “Understanding how AFL works” tutorial documents the explicit recursion it corresponds to:

Pseudocode — not valid AFL

y[i] = factor * x[i] + (1 - factor) * y[i-1]

The harness computes the built-in version, computes the loop version, subtracts them, takes the absolute difference, and finds the largest disagreement anywhere in the array. It then prints a verdict against a tolerance you can set, times both implementations, and plots all three series so a localised disagreement is visible rather than merely summarised.

SetBarsRequired( sbrAll, sbrAll ) is at the top on purpose. A recursive average started at a different first bar is a different calculation, so leaving QuickAFL on would make the harness report a failure that belongs to the range, not to the formula.

Highest( ARRAY ) is a running maximum — the highest value seen since the first bar present — so its last element is the maximum over the whole array. That is the standard idiom for “the worst case anywhere”.

GetPerformanceCounter( bReset = False ) returns milliseconds. Called with True it reports elapsed time since the last reset and resets, which is why it appears three times: once to zero it, then once after each implementation.

_TRACEF( "format", ... ) is _TRACE and StrFormat combined.

A pane with two nearly identical lines, a difference series on its own scale that should sit at or very near zero, and a title reporting the worst absolute difference, the tolerance, a verdict and both timings. The Log window shows the first few bars side by side and one summary line.

Expect the loop to be substantially slower than the built-in. The guide’s measured figures for a simple array expression are 10× at 300 bars and 50× at 350,000.

The harness needs testing more than the formula does. Break it deliberately:

  1. Change the seed line to Manual[ 0 ] = 0;. The difference should become large near the left edge and shrink to the right, because the recursion forgets its start.
  2. Change the loop to start at i = 0. It should raise a subscript error, because Manual[ -1 ] does not exist — the loop-off-by-one that Part 8 warned about.
  3. Set the tolerance to 0.000001. The verdict may flip to DISAGREES purely because AFL numbers are 32-bit floats. That is the harness telling you the truth about precision, not a defect.

Testing two floating-point results for exact equality. AFL treats every number as a 32-bit float; “equal” always means “within a tolerance you chose deliberately”.

Leaving the trace ungated. _TRACEF on every bar of a 20-year daily database is 5,000 lines, and on intraday data it is far worse.

Generalise it. Replace the pair of implementations with two user-defined functions, Subject() and Reference(), and leave everything else alone. You then have a harness you can point at any formula whose definition you can state twice.

The last resort is also the most reliable, and it is worth doing once for any calculation you intend to rely on.

Take five or six bars. Not the whole history — five. Read the actual values off the chart, or better, dump them with an exploration so there is no transcription error. Compute the quantity by hand or in a spreadsheet, from its definition, without looking at your AFL. Then compare.

AmiBroker’s own tutorial does exactly this: it works MA( Close, 3 ) numerically bar by bar, showing Null, Null, 1.243, ..., and then shows the equivalent explicit loop. Two things usually fall out of the exercise, and they are the two that cause most quiet errors.

The first is warm-up. The tutorial’s table shows Cond1 = Close < MA( Close, 3 ) as Null, Null, 1, 0, ... — the warm-up bars are Null, not false. Anything counting those bars with Cum() or Sum() gets a different answer from the one you expected.

The second is off-by-one. A 20-bar average at bar n includes bar n, so it spans n−19 to n. Whether your hand calculation agrees is exactly the sort of thing that is invisible in code and obvious in a table of six numbers.

You now know what the debugger can do — breakpoints on a specific list of statement kinds, watches on identifiers and expressions but never on function calls, an Arrays tab limited to twenty arrays — and the two settings that will otherwise confuse you: the actionBacktest context and the 200-bar limit. You know which of the three printing functions goes where, and that %d does not exist in a language with no integers.

More durably, you have a method that does not depend on any of that: make it reproducible, state the expected value, bisect, and when bisection is not enough, build a harness whose reference implementation comes from the definition rather than from the code.

Check your understanding

Question 1. A breakpoint inside `if( Status("action") == actionIndicator ) { ... }` never triggers under the debugger. Why?
Show the answer and why

Answer: Since 6.10 the debugger runs code in the actionBacktest context, so the condition is false

The debugger executes with actionBacktest. You can temporarily remove the check to get inside, but some objects — GetBacktesterObject() in particular — are genuinely unavailable outside their real context.

Question 2. Which of these can the Watch window evaluate? Select all that apply.
Show the answer and why

Answer: Close[ i + 2 ], (High + Low) / 2, mat[ i + 1 ][ j ]

Arithmetic, parentheses, array subscripts with expression indexes and matrix subscripts are all supported. Function calls are not — only variable identifiers, which is a reason to name intermediates rather than nest calls.

Question 3. Why does the harness call `SetBarsRequired( sbrAll, sbrAll );` before computing anything?
Show the answer and why

Answer: Because a recursive average started at a different first bar is a different calculation, so QuickAFL would cause a false failure

AMA is recursive, which is one of the four documented categories where QuickAFL may change results. With QuickAFL on, the harness would report a disagreement caused by the range rather than by the formula.

Question 4. Your reference implementation and the formula under test agree exactly. When is that weak evidence?
Show the answer and why

Answer: When you wrote the reference by adapting the code under test

Two implementations that share an origin also share its mistakes. The reference has to be derived independently — from the definition, a spreadsheet, or a hand calculation — for agreement to mean anything.

Sources for this lesson

7 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — How to use AFL debuggeramibroker.com/guide/h_debugger.html2026-08-31
  2. 02AFL Function Reference — _TRACEamibroker.com/guide/afl/_trace.html2026-08-31
  3. 03AFL Function Reference — _TRACEFamibroker.com/guide/afl/_tracef.html2026-08-31
  4. 04AFL Function Reference — printfamibroker.com/guide/afl/printf.html2026-08-31
  5. 05AmiBroker User's Guide — Understanding how AFL works§ Variable-period exponential averageamibroker.com/guide/h_understandafl.html2026-08-31
  6. 06AFL Function Reference — GetPerformanceCounteramibroker.com/guide/afl/getperformancecounter.html2026-08-31
  7. 07AmiBroker User's Guide — Common coding mistakesamibroker.com/guide/a_mistakes.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.