Skip to content
Level 3 · AFL DeveloperChallengePart 09 · page 7 of 735 min
35Minutes
14AFL functions
7Sources
StandardRequires
AFL functions taught here14

Challenge: State or Event?

Six formulas follow. Every one of them compiles. Every one of them runs without a warning. Every one of them was written by somebody who understood what they wanted and expressed it incorrectly, and every one produces a symptom that people routinely misdiagnose as a problem with AmiBroker, with their data, or with their computer.

Your job is to say what each one actually computes, why the reported symptom follows from that, and what the correct formula is. The point is not to spot typos. There are none. The point is to practise the only skill that reliably finds this class of bug: measuring what an array contains instead of reasoning about what it ought to contain.

Work in this order, and resist the temptation to scroll.

  1. Read all six problems. Do not look at the hints.
  2. For each one, write down — on paper, in a comment, anywhere — what you think the array actually contains and why the symptom follows.
  3. Run the diagnosis procedure below on the ones you are unsure about, using the audit tool supplied here on real data.
  4. Only then read the hints, which are staged: the first is a nudge, the second names the area, the third is close to the answer.
  5. Read the worked solutions last, including the ones you got right — the root cause is often not the thing that first catches the eye.

Give yourself the full thirty-five minutes. Four of the six can be identified without running anything, if you read carefully.

This is the procedure, in the order that finds problems fastest.

Turn the formula back into an English sentence, then compare that sentence with what the author says they wanted. Words like when, crosses, breaks and the moment describe events. Words like while, during, as long as and is above describe states. A mismatch between the sentence and the code is found here more often than anywhere else, and it costs nothing.

For every Boolean array in the formula, get two numbers: how many bars it is true, and the length of its longest unbroken run of true bars.

  • Longest run of 1 → the array is event-shaped.
  • Longest run in the tens or hundreds → the array is a state.
  • True on zero bars → the condition is impossible, or the warm-up swallowed it.

Compare those numbers with what the rule was supposed to do. A trigger that is true on a third of all bars is not a trigger. A market filter that is true on 0.2% of bars is not a filter.

ValueWhen, HighestSince, LowestSince, HighestSinceBars, LowestSinceBars and SumSince all take the condition first. Every one of them compiles with the arguments reversed. The tell is that the output is in the wrong units: ones and zeros where you expected prices, or prices where you expected a count.

Cross( a, b ) and Cross( b, a ) are different questions, not a question and its negation.

Step 4 — Check every window and every sign

Section titled “Step 4 — Check every window and every sign”

For each Ref(): is the period negative? A positive period reads the future.

For each HHV, LLV or Sum: does the window include the current bar? It does — that is documented — so a comparison of the current bar against its own window is either impossible or trivial.

For each anchored measure: is the anchor an event, or is it a rolling window pretending to be one?

Step 5 — Ask what will consume the array

Section titled “Step 5 — Ask what will consume the array”

The same defective array behaves differently in different parts of AmiBroker. A state-shaped Buy floods a scan, a chart and an alert, while the default backtest mode quietly discards the redundant entries and produces a report that looks reasonable. So “the backtest looks fine” is not evidence that the array is right, and “the scan is broken” is often evidence that it is not.

Answer Step 2 of the procedure without writing a new formula each time: paste in any pair of rules and get back their true-bar counts, their longest unbroken runs and the size of their warm-up gaps.

Complete runnable AFL

signal-audit-tool.afl
// signal-audit-tool.afl
// Part 9 - Challenge: State or Event?
//
// A diagnosis instrument, not a trading system. Paste the rules under test into
// the marked block and it answers the questions that separate a state bug from
// an event bug:
//
// How many bars is each array true?
// What is the longest unbroken run of true bars?
// Does the array ever become true at all?
// How many bars at the left edge are empty rather than false?
//
// The decisive column is the longest unbroken run. A Buy array whose longest run
// is 1 is event-shaped. A Buy array whose longest run is 60 is state-shaped. Two
// of the six broken formulas in this challenge are identified by that column
// alone, and two more by the "ever true" column.
//
// Assumptions:
// - Replace only the marked block. Everything below it is measurement.
// - Counts cover the bars delivered to this run; SetBarsRequired( sbrAll )
// asks for the whole history.
// - Run length is measured as "bars since the array was last false". On a
// symbol whose Buy array is true from the very first delivered bar, that
// count has never been reset, so treat a run length equal to the bar count
// as "always true" rather than as a precise measurement.
// - The tool describes the arrays. It has no opinion on whether the rule is
// a good idea.
SetBarsRequired( sbrAll );
// ===========================================================================
// The formula under test - replace this block
// ===========================================================================
Average = MA( Close, 50 );
Buy = Close > Average;
Sell = Close < Average;
// ===========================================================================
// Measurement - leave this alone
// ===========================================================================
// IsTrue() maps Null to 0 and any non-zero value to 1, so a warm-up Null cannot
// poison the counts the way a raw AND would.
BuyTrue = IsTrue( Buy );
SellTrue = IsTrue( Sell );
// Bars since the array was last false. On a false bar this is zero, so the
// running maximum of it is the longest unbroken true run so far.
BuyRun = BarsSince( NOT BuyTrue );
SellRun = BarsSince( NOT SellTrue );
BarsInRange = Cum( 1 );
BuyBars = Cum( BuyTrue );
SellBars = Cum( SellTrue );
LongestBuyRun = Highest( BuyRun );
LongestSellRun = Highest( SellRun );
// How much of the left edge is empty rather than false? A long leading Null run
// means a warm-up dependency you may not have noticed.
BuyNulls = NullCount( Buy, 1 );
SellNulls = NullCount( Sell, 1 );
Filter = Status( "lastbarinrange" );
AddColumn( BarsInRange, "Bars in range", 1.0 );
AddColumn( BuyBars, "Bars where Buy is true", 1.0 );
AddColumn( LongestBuyRun, "Longest unbroken Buy run", 1.0 );
AddColumn( SellBars, "Bars where Sell is true", 1.0 );
AddColumn( LongestSellRun, "Longest unbroken Sell run", 1.0 );
AddColumn( BuyNulls, "Leading Nulls in Buy", 1.0 );
AddColumn( SellNulls, "Leading Nulls in Sell", 1.0 );
AddColumn( 100 * BuyBars / BarsInRange, "Buy true, % of bars", 1.2 );

Download signal-audit-tool.afl75 lines

The block at the top is the only part you edit. Everything below it measures.

IsTrue() converts each array to strict ones and zeros so that warm-up empties count as false instead of propagating. Cum() of that gives the total number of true bars. The run-length trick is the interesting part: BarsSince( NOT BuyTrue ) is the number of bars since the array was last false, which is zero on every false bar and climbs through each true run — so the running maximum of it, Highest( BuyRun ), is the longest unbroken run so far. NullCount( Buy, 1 ) reports how much of the left edge is empty rather than false, which is how a long warm-up dependency announces itself.

Filter = Status( "lastbarinrange" ) reduces the report to one row per symbol, and SetBarsRequired( sbrAll ) asks for the whole history so the totals mean what they say.

  • IsTrue( ARRAY ) — maps Null to 0, non-zero to 1.
  • BarsSince( ARRAY ) — used here on the negation of a condition, to measure run length rather than event age.
  • Highest( ARRAY ) — the running all-history maximum, with no window; exactly right for “the longest run so far”.
  • NullCount( array, mode ) — the length of the leading empty run.
  • Status( "lastbarinrange" ) — one row per symbol.

Point it at two arrays whose answers you already know. Replace the block with Buy = Close > 0; and Sell = Close < 0;. The buy count should equal the bar count, the longest buy run should equal the bar count, the sell count should be zero — and the longest sell run should be zero too, which incidentally tells you what BarsSince returns when its condition is true on every bar.

  • Leaving Filter = 1. One row per bar per symbol, which is unreadable.
  • Reading the run length on an array that is true from the very first delivered bar. The count has never been reset, so treat a run equal to the bar count as “always true” rather than as a measurement.
  • Forgetting that the numbers describe the delivered range, not the database.

Add a column for the number of bars on which Buy and Sell are both true. Two rules that are supposed to be opposites should never overlap, and an overlap is a fast explanation for ExRem and Flip behaving oddly.

What the author wanted. “Mark the chart where the fifty-bar average moves above the two-hundred-bar average, and where it moves back below. Then run it as a daily scan to see which symbols crossed today.”

What they wrote.

Fragment — not a complete formula

Trend = MA( Close, 50 ) > MA( Close, 200 );
Buy = Trend;
Sell = NOT Trend;
PlotShapes( Buy * shapeUpArrow, colorGreen, 0, Low, -20 );
PlotShapes( Sell * shapeDownArrow, colorRed, 0, High, 20 );

Symptom. The chart shows a continuous green band under roughly half the bars and a continuous red band above the rest; individual arrows are indistinguishable. The daily scan reports almost every symbol in the watch list, every single day. The author has checked the scan settings three times.

What the author wanted. “Enter when price crosses above its twenty-bar average, but only in a rising market and only with above-average volume.”

What they wrote.

Fragment — not a complete formula

Buy = Cross( Close, MA( Close, 20 ) )
AND Cross( MA( Close, 50 ), MA( Close, 200 ) )
AND Cross( Volume, MA( Volume, 50 ) );
Sell = Cross( MA( Close, 20 ), Close );

Symptom. The backtest report is empty. Not “few trades” — zero, across five hundred symbols and twenty years. The author has widened the date range, changed the universe and reinstalled AmiBroker.

Problem 3 — The breakout that never breaks out

Section titled “Problem 3 — The breakout that never breaks out”

What the author wanted. “Buy when the close breaks above the highest high of the previous twenty bars, on at least twice the average volume.”

What they wrote.

Fragment — not a complete formula

Buy = Close > HHV( Close, 20 )
AND Volume > 2 * MA( Volume, 50 );
Sell = Close < LLV( Close, 20 );

Symptom. Zero signals. The author concluded that the volume filter was too strict, relaxed it to 1.2 *, then removed it entirely. Still zero signals. They now believe their data has no breakouts in it.

Problem 4 — The trailing stop that moves on its own

Section titled “Problem 4 — The trailing stop that moves on its own”

What the author wanted. “Exit when price falls two average true ranges below the highest high reached since the entry.”

What they wrote.

Fragment — not a complete formula

Buy = Cross( Close, MA( Close, 50 ) );
StopLevel = HHV( High, 20 ) - 2 * ATR( 14 );
Sell = Close < StopLevel;

Symptom. The backtest produces trades, so nothing looks obviously wrong. But plotting StopLevel shows it moving up and down before any trade exists, and on several trades the level falls while the position is open. Two trades exit on a bar where price made a new high.

Problem 5 — The entry price in the thousands of per cent

Section titled “Problem 5 — The entry price in the thousands of per cent”

What the author wanted. “Record the close at each entry signal and show the open profit as a percentage.”

What they wrote.

Fragment — not a complete formula

Buy = Cross( Close, MA( Close, 50 ) );
EntryPrice = ValueWhen( Close, Buy );
ProfitPct = 100 * ( Close - EntryPrice ) / EntryPrice;
Filter = Buy;
AddColumn( EntryPrice, "Entry price", 1.2 );
AddColumn( ProfitPct, "Open profit %", 1.2 );

Symptom. The entry-price column contains only 0.00 and 1.00. The profit column contains enormous numbers and empty cells. The author suspects a currency or decimal-places setting.

What the author wanted. “Buy when twenty-bar momentum is positive and price is above its fifty-bar average; sell when momentum turns negative.”

What they wrote.

Fragment — not a complete formula

Momentum = Close - Ref( Close, 20 );
Buy = Momentum > 0 AND Close > MA( Close, 50 );
Sell = Momentum < 0;

Symptom. The equity curve is almost a straight line. The maximum drawdown is under two per cent. The win rate is above eighty per cent. The author is delighted and is about to trade it.

Each problem gets three hints of increasing directness. Take the smallest number you need.

  1. How many bars is Trend true on a ten-year chart? Roughly, not exactly.
  2. The author’s sentence contains the word “moves”. What kind of array does that word describe?
  3. Trend is a state. The chart and the scan are both reporting it faithfully.
  1. Estimate how often each of the three Cross() calls is true, per year.
  2. Now estimate how often all three are true on the same bar.
  3. Two of the three conditions were meant as context, not as triggers.
  1. Re-read the documented definition of HHV’s periods argument. There is a parenthesis in it that decides this problem.
  2. Ask whether Close > HHV( Close, 20 ) can be true for any value of any close, on any symbol, ever.
  3. The Sell rule has the same defect, in the other direction — which is why removing the volume filter changed nothing.
  1. On bar 3 of a trade, which bars does HHV( High, 20 ) look at?
  2. How many of those bars happened before the entry?
  3. The author’s sentence says “since the entry”. Which function anchors at a condition rather than sliding a window?
  1. What are the units of the entry-price column? What should they be?
  2. ValueWhen has a documented argument order. Check it.
  3. Close is non-zero on every bar, so as a condition it is true on every bar.
  1. Which functions in this formula can read a bar that had not printed yet?
  2. There is exactly one, and it appears once.
  3. Read the sign of the second argument to Ref(), then read the documented meaning of a positive period.

Read these even for the problems you solved. The root cause is frequently one level below the thing that first looks wrong.

Root cause. Trend is a state. Buy = Trend assigns that state to a signal array, so Buy is true on every bar of every stretch where the fifty-bar average is above the two-hundred-bar average — on a ten-year daily chart, plausibly a thousand bars or more.

Why the symptom follows. PlotShapes() draws a shape on every true bar, so a thousand true bars produce a thousand arrows, which read as a band. A Scan reports every bar that passes, so every symbol currently in an uptrend appears every day. Nothing is misconfigured; both tools are reporting the array accurately.

The fix.

Fragment — not a complete formula

Buy = Cross( MA( Close, 50 ), MA( Close, 200 ) );
Sell = Cross( MA( Close, 200 ), MA( Close, 50 ) );

Why it works. Cross() is true only on the bar the relationship changed, which is what “moves above” means. Note that the Sell line swaps the arguments rather than negating anything — Cross( b, a ) is a different array, not NOT Cross( a, b ).

What else to check. If the author genuinely wants the state as well — for shading, or as a filter on some other rule — keep it in its own variable: TrendUp = MA( Close, 50 ) > MA( Close, 200 ); and use it where a state belongs. ExRem( Trend, NOT Trend ) would also produce one signal per run, but it is the wrong tool here: the rule was always meant to be an event, and expressing it as one is clearer than thinning a state after the fact.

Root cause. Three events ANDed together. A twenty-bar crossing might happen ten times a year; a fifty-over-two-hundred crossing perhaps twice a decade; a volume crossing dozens of times a year. Requiring all three on the same bar makes the rule almost impossible to satisfy, and across the tested universe it was satisfied zero times.

Why the symptom follows. An empty Buy array produces no trades, so the backtest report is genuinely empty rather than broken. Widening the date range cannot help, because the problem is a conjunction of rare moments rather than a shortage of data.

The fix.

Fragment — not a complete formula

TrendUp = MA( Close, 50 ) > MA( Close, 200 ); // STATE: market context
VolumeOK = Volume > MA( Volume, 50 ); // STATE: liquidity context
Trigger = Cross( Close, MA( Close, 20 ) ); // EVENT: the moment
Buy = Trigger AND TrendUp AND VolumeOK;
Sell = Cross( MA( Close, 20 ), Close );

Why it works. One event supplies the timing; the two states supply the context. Read as a sentence: at the moment price crosses its twenty-bar average, while the longer averages are in the right order and while volume is above average.

What else to check. Run the audit tool on TrendUp and VolumeOK before trusting them. If either is true on almost every bar it is not filtering anything; if either is true on almost no bars, it will suppress the trigger almost entirely. The occurrence-rate measurement from earlier in this part is the right instrument.

Solution 3 — The breakout that never breaks out

Section titled “Solution 3 — The breakout that never breaks out”

Root cause. The periods argument of HHV is documented to include the current day. HHV( Close, 20 ) on any bar is therefore the maximum of twenty values, one of which is that bar’s own close. A number cannot be strictly greater than the maximum of a set containing it, so Close > HHV( Close, 20 ) is false on every bar of every symbol.

Why the symptom follows. The volume filter was never the constraint. Buy was already an array of zeros before the AND, so relaxing or removing the second condition could not change anything. The Sell rule has the mirror-image defect — Close < LLV( Close, 20 ) is likewise never true — which is why the system had no exits either.

The fix.

Fragment — not a complete formula

PriorHigh = Ref( HHV( High, 20 ), -1 ); // the window that ended last bar
PriorLow = Ref( LLV( Low, 20 ), -1 );
Buy = Cross( Close, PriorHigh ) AND Volume > 2 * MA( Volume, 50 );
Sell = Cross( PriorLow, Close );

Why it works. Shifting the window back one bar gives a level that was already fixed before the current bar opened, so the comparison is between the current close and a set that does not contain it. Cross() then makes the breakout an event rather than a state, which stops the rule reporting every bar of an extended run above the old high.

What else to check. Decide explicitly whether you want a closing breakout (Cross( Close, PriorHigh )) or an intrabar one (High > PriorHigh), and write the choice down. They are different rules, and the intrabar version carries a fill assumption the closing version does not.

Solution 4 — The trailing stop that moves on its own

Section titled “Solution 4 — The trailing stop that moves on its own”

Root cause. HHV( High, 20 ) is a rolling window, not an anchor. On bar 3 of a trade it covers the entry bar, the two bars after it, and seventeen bars from before the entry — bars that have nothing to do with the position. As the trade progresses, old bars drop out of the window, so the “highest high” can fall, and with it the stop level.

Why the symptom follows. The level moves before any trade exists because the window exists on every bar regardless of positions. It falls during a trade whenever a high from before the entry rolls out of the twenty-bar window. And a trade can exit on a bar that made a new high, because the stop is computed from a window whose composition changed on that bar.

The fix.

Fragment — not a complete formula

Buy = Cross( Close, MA( Close, 50 ) );
PeakSinceEntry = HighestSince( Buy, High ); // condition first
StopLevel = PeakSinceEntry - 2 * ATR( 14 );
Sell = Close < StopLevel;

Why it works. HighestSince( EXPRESSION, ARRAY ) measures from the most recent bar on which the condition was true, so the anchor is the entry and the peak can only rise while the trade is open. Note the condition-first argument order, which is Step 3 of the diagnosis procedure.

What else to check. Before the first Buy has ever occurred, the return value of HighestSince is not stated in the official documentation — guard it with Cum( Buy ) > 0 if the level feeds anything other than a plot. And for a real system, AmiBroker’s own ApplyStop() is the proper mechanism for trailing stops: it is evaluated by the backtester with the correct intrabar semantics, which a hand-built Sell rule computed on closes cannot reproduce. Part 28 covers it.

Note also that Sell here is state-shaped — true on every bar below the stop. In the default backtest mode that is harmless, because the first matching exit closes the trade and the rest are ignored. In a scan or on a chart it would produce the same wall of markers as Problem 1.

Solution 5 — The entry price in the thousands of per cent

Section titled “Solution 5 — The entry price in the thousands of per cent”

Root cause. The arguments to ValueWhen are reversed. The documented signature is ValueWhen( EXPRESSION, ARRAY, n = 1 ) — condition first, array to sample second. The formula passes Close as the condition and Buy as the array, so it computes “the value of Buy at the most recent bar on which Close was non-zero”. Since the close is non-zero on every bar, that is simply Buy itself.

Why the symptom follows. EntryPrice therefore contains ones and zeros, which is what the column shows. The profit calculation divides by it, giving a division by zero on the zero bars — hence the empty cells — and a percentage computed against a denominator of 1 on the rest, which is why the numbers are enormous. Nothing to do with currency or decimal settings.

The fix.

Fragment — not a complete formula

Buy = Cross( Close, MA( Close, 50 ) );
EntryPrice = ValueWhen( Buy, Close );
ProfitPct = 100 * ( Close - EntryPrice ) / EntryPrice;

Why it works. With the condition first, ValueWhen samples the close on each signal bar and holds it until the next signal — the sample-and-hold shelf from the ValueWhen lesson.

What else to check. Two things. First, Filter = Buy means the report shows only the signal bars, where the open profit is by definition zero; if the author wanted to watch the profit evolve they need a different filter. Second, if the system enters at the next bar’s open rather than at the signal bar’s close, the honest entry price is:

Fragment — not a complete formula

TradeBar = Ref( Buy, -1 ); // the bar after the signal
EntryPrice = ValueWhen( TradeBar, Open );

And for anything that feeds a real evaluation, the backtester’s own trade prices — not a hand-built array — are the authority.

Root cause. Ref( Close, 20 ) has a positive period. The official documentation states that a positive period references periods in the future, and its own example annotates the positive case as looking up the future. So Momentum = Close - Ref( Close, 20 ) is the current close minus the close twenty bars later — a quantity nobody could have known.

Why the symptom follows. Momentum > 0 means “price is lower now than it will be in twenty bars”, so the rule buys only when it already knows price rises. Momentum < 0 exits only when it already knows price falls. A near-straight equity curve, a tiny drawdown and a very high win rate are the expected signature of look-ahead bias, not evidence of a good idea.

The fix.

Fragment — not a complete formula

Momentum = Close - Ref( Close, -20 ); // minus: twenty bars AGO
Buy = Momentum > 0 AND Close > MA( Close, 50 );
Sell = Momentum < 0;

Why it works. A negative period reads backwards, so every input to the rule was available at the time the decision had to be made.

What else to check. The corrected rule is still state-shaped in both directions, which is fine for a default-mode backtest and wrong for a scan, a chart or an alert. And a plausible result from the corrected version deserves the same scepticism the implausible one should have received: Part 30 lists the other routes to look-ahead bias, several of which leave no plus sign to find.

Five of the six defects are one of two things: an array of the wrong shape — a state where an event was needed, or the reverse — or an array of the right shape computed from the wrong bars. The sixth, the reversed ValueWhen, is an array of the wrong units, which the audit tool exposes as clearly as the others.

None of them is a subtle language feature. All of them are visible in about ninety seconds of measurement:

  • Count the true bars and the longest run. Wrong shape shows up here.
  • Look at the units of every derived column. Reversed arguments show up here.
  • Read the sign of every Ref() and the window of every HHV, LLV and Sum. Wrong bars show up here.

That is the whole method, and it generalises far beyond this part. Part 12’s challenge on empty scans and Part 30’s collection of broken backtests are the same skill applied to different symptoms.

A formula that compiles has told you nothing about whether it is correct. AFL has no way to distinguish a state from an event, an anchored measure from a rolling one, or a backward shift from a forward one — all of them are arrays of numbers, and all of them are legal in every position.

What replaces the compiler is a habit: describe the rule in words, measure every Boolean array, check the argument order of the condition-first functions, check every sign and every window, and ask what part of AmiBroker is going to read the result. Six formulas that defeated their authors gave way to that procedure in this page, and the procedure is worth more than any of the six individual answers.

Check your understanding

Question 1. You inherit a formula and want to know quickly whether its Buy array is a state or a set of events. What do you measure?
Show the answer and why

Answer: The number of true bars and the longest unbroken run of true bars

A longest run of 1 means event-shaped; a long run means a state. Neither requires you to read or understand the rule that produced the array, which is exactly why it is the first measurement to take.

Question 2. Which pair of symptoms points most strongly at look-ahead bias rather than at a state-versus-event mistake?
Show the answer and why

Answer: A near-straight equity curve with a very small drawdown

A wall of rows is a state used as a signal. An empty report is usually events ANDed together or an impossible comparison. One trade per symbol suggests an ExRem whose resetting array never fires. A suspiciously smooth equity curve is the signature of a rule that already knows what happens next.

Question 3. Why did removing the volume filter in Problem 3 change nothing?
Show the answer and why

Answer: The first condition was already false on every bar, so the AND could never be true whatever the second condition did

Close > HHV( Close, 20 ) compares the close with a window that includes it, so it is false everywhere. An AND with a false operand is false regardless of the other side - which is why loosening the other side was wasted effort.

Question 4. A hand-built trailing stop should be anchored at the entry. Which construction does that?
Show the answer and why

Answer: HighestSince( Buy, High ) - 2 * ATR( 14 )

HighestSince measures from the most recent bar on which the condition was true, so the anchor is the entry. HHV slides a window that includes pre-entry bars, Highest measures from the first delivered bar, and the last option has its arguments reversed.

Question 5. A colleague says "the backtest is fine, so the Buy array must be correct". What is wrong with that reasoning?
Show the answer and why

Answer: The default backtest mode discards redundant entry signals, so a state-shaped Buy can still produce a reasonable-looking report

The User's Guide describes the default mode as ignoring entry signals that arrive while a position is open, by the same process ExRem performs. That masks a state-shaped Buy in the backtest while leaving scans, charts and alerts broken.

Sources for this lesson

7 verified · checked 2026-08-31

  1. 01AFL Function Reference — Crossamibroker.com/guide/afl/cross.html2026-08-31
  2. 02AFL Function Reference — Refamibroker.com/guide/afl/ref.html2026-08-31
  3. 03AFL Function Reference — HHVamibroker.com/guide/afl/hhv.html2026-08-31
  4. 04AFL Function Reference — ValueWhenamibroker.com/guide/afl/valuewhen.html2026-08-31
  5. 05AFL Function Reference — HighestSinceamibroker.com/guide/afl/highestsince.html2026-08-31
  6. 06AFL Function Reference — ExRemamibroker.com/guide/afl/exrem.html2026-08-31
  7. 07AmiBroker User's Guide — Portfolio-level backtesting§ Backtest modesamibroker.com/guide/h_portfolio.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.