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 visual debugger
Section titled “The visual debugger”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
forloops,whileloopsdo-whileloops — place it on thewhileclause, because thedoline is a no-op, or on the first statement inside the blockifstatements,returnstatements,switch/casestatements,breakstatements
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}Watches, and what they cannot do
Section titled “Watches, and what they cannot do”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.
Two traps that waste an afternoon
Section titled “Two traps that waste an afternoon”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.
Tracing: _TRACE, _TRACEF and printf
Section titled “Tracing: _TRACE, _TRACEF and printf”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 windowPut 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.
Bisecting a formula
Section titled “Bisecting a formula”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. Make the fault reproducibleOne symbol, one interval, one date. Fix the range so QuickAFL cannot change the answer between runs.
- 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. Halve the formulaComment out the second half. Plot or trace the last surviving intermediate value. Is it already wrong?
- 4. Recurse into the half that is wrongIf the intermediate is correct the fault is downstream; if not, upstream. Repeat.
- 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.
A test harness
Section titled “A test harness”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.
The complete formula
Section titled “The complete formula”Complete runnable 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 zeroBuiltin = 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();How it works
Section titled “How it works”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.
Key functions
Section titled “Key functions”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.
Expected result
Section titled “Expected result”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.
Test it
Section titled “Test it”The harness needs testing more than the formula does. Break it deliberately:
- 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. - Change the loop to start at
i = 0. It should raise a subscript error, becauseManual[ -1 ]does not exist — the loop-off-by-one that Part 8 warned about. - 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.
Common errors
Section titled “Common errors”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.
Extension
Section titled “Extension”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.
Verifying against a hand calculation
Section titled “Verifying against a hand calculation”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.
What changed
Section titled “What changed”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
Sources for this lesson
7 verified · checked 2026-08-31
- 01AmiBroker User's Guide — How to use AFL debuggeramibroker.com/guide/h_debugger.html2026-08-31
- 02AFL Function Reference — _TRACEamibroker.com/guide/afl/_trace.html2026-08-31
- 03AFL Function Reference — _TRACEFamibroker.com/guide/afl/_tracef.html2026-08-31
- 04AFL Function Reference — printfamibroker.com/guide/afl/printf.html2026-08-31
- 05AmiBroker User's Guide — Understanding how AFL works§ Variable-period exponential averageamibroker.com/guide/h_understandafl.html2026-08-31
- 06AFL Function Reference — GetPerformanceCounteramibroker.com/guide/afl/getperformancecounter.html2026-08-31
- 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.