Skip to content
Level 5 · Real-Time AmiBroker UserProjectPart Capstone · page 4 of 955 min
55Minutes
23AFL functions
6Sources
StandardRequires
AFL functions taught here23

Component 3: Scanner

Report the symbols where an explicitly documented setup became true on the most recent completed bar — with the specification written in a form somebody else could implement and get the same list.

That last clause is the requirement. A scanner whose rules exist only in the author’s head is not a component of a research workstation; it is a habit.

Every scanner needs all five, in this order

  1. UniverseWhich instruments are eligible at all, and how that list was built — including whether it is point-in-time.
  2. LiquidityThe floor below which a candidate is not tradeable at your size. Measured on the decision bar.
  3. RegimeThe market-wide condition under which the setup is permitted. Read from a benchmark, not from the candidate.
  4. SetupThe state that makes an instrument a candidate. True over a span of bars.
  5. TriggerThe event that turns a candidate into a signal. True on one bar.

Two distinctions in that list carry most of the weight.

Setup versus trigger. A setup is a state — “the symbol is in an uptrend” is true for weeks. A trigger is an event — “the close crossed above the prior 50-bar high” is true on one bar. Confusing them produces a scan that returns the same forty symbols every day, which is a watch list wearing a scanner’s clothes.

Regime read from the benchmark, not the candidate. If each symbol is gated by its own trend, “regime” means something different in every row and you have simply added another setup condition. A single benchmark applied to every candidate on the same date is what makes it a market-wide gate.

Element Definition
Universe The watch list the Analysis window points at
Liquidity Median turnover over LiquidityPeriod bars ≥ MinTurnover, and close ≥ MinPrice, both measured on the decision bar
Regime The benchmark is above its own RegimePeriod-bar average
Setup The symbol’s close is above its own TrendPeriod-bar average, and the fast average is above the slow
Trigger The close crosses above the highest high of the previous BreakoutPeriod bars
Not included No exit, no stop, no position size. Those are Component 6.

Complete runnable AFL

scanner.afl
// scanner.afl
// Capstone Component 3 - Scanner
//
// GOAL
// Report the symbols where an explicitly documented setup became true on the
// most recent completed bar. Everything the setup depends on is named at the
// top of the file, so that somebody else could reproduce the list exactly.
//
// THE SPECIFICATION THIS FORMULA IMPLEMENTS
// Universe the watch list the Analysis window points at.
// Liquidity median turnover over LiquidityPeriod bars must clear
// MinTurnover, and the close must clear MinPrice. Both measured
// on the decision bar, so both use information that existed
// before any order.
// Regime the BENCHMARK - not the traded symbol - must be above its own
// RegimePeriod-bar average. A market-wide gate, applied
// identically to every candidate.
// Setup the traded symbol is in an uptrend: close above its own
// TrendPeriod-bar average, and the fast average above the slow.
// Trigger the close crosses above the highest high of the previous
// BreakoutPeriod bars. The look-back is shifted back one bar, so
// the bar cannot break out of a range containing its own high.
// Not a rule no exit, no stop, no size. Those live in Component 6.
//
// HOW TO RUN
// Analysis -> Apply to: your universe. Periodicity: Daily.
// Range: n last quotations, at least RequiredBars + 20. Press SCAN.
// Run it as EXPLORE instead to get the same rows with the supporting
// measurements attached, which is what you want while you are still
// deciding whether the specification says what you meant.
//
// ASSUMPTIONS
// Interval daily, split- and dividend-adjusted.
// Timing the scan reports the most recent COMPLETED bar. Acting on it
// means the next session at the earliest, which is exactly the
// delay Component 6's backtest assumes.
// Benchmark must exist in the database and share the calendar.
// Not modelled costs, fills, size, borrow, and the future.
SetBarsRequired( sbrAll, sbrAll );
BenchSymbol = ParamStr( "Benchmark symbol", "" );
RegimePeriod = Param( "Benchmark regime average", 200, 20, 400, 10 );
TrendPeriod = Param( "Symbol trend average", 200, 20, 400, 10 );
FastPeriod = Param( "Symbol fast average", 50, 5, 200, 5 );
BreakoutPeriod = Param( "Breakout look-back (bars)", 50, 5, 250, 5 );
LiquidityPeriod = Param( "Liquidity look-back (bars)", 50, 5, 400, 5 );
MinTurnover = Param( "Minimum median turnover", 2000000, 0, 100000000, 250000 );
MinPrice = Param( "Minimum close", 2, 0, 500, 0.5 );
RequiredBars = Max( Max( TrendPeriod, RegimePeriod ),
Max( BreakoutPeriod, LiquidityPeriod ) ) + 1;
Ready = BarIndex() >= RequiredBars;
// ------------------------------------------------------------- 1. regime
// Read from the benchmark, so every candidate is gated by the same market
// state on the same date. A per-symbol "regime" would mean something different
// in every row.
HaveBench = StrLen( BenchSymbol ) > 0;
if( HaveBench )
{
BenchClose = Foreign( BenchSymbol, "C" );
BenchOK = NOT IsNull( BenchClose ) AND BenchClose > 0;
RegimeOpen = BenchOK AND BenchClose > MA( BenchClose, RegimePeriod );
}
else
{
// No benchmark configured: the gate is open and the title says so. A
// silently absent filter is worse than a stated missing one.
BenchOK = False;
RegimeOpen = True;
}
// ---------------------------------------------------------- 2. liquidity
Turnover = Median( Close * Volume, LiquidityPeriod );
Liquid = IIf( Ready, Turnover >= MinTurnover AND Close >= MinPrice, False );
// -------------------------------------------------------------- 3. setup
TrendMa = MA( Close, TrendPeriod );
FastMa = MA( Close, FastPeriod );
Setup = Close > TrendMa AND FastMa > TrendMa;
// ------------------------------------------------------------ 4. trigger
BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );
Trigger = Cross( Close, BreakoutLevel );
// -------------------------------------------------------- 5. the signal
Buy = IIf( Ready, RegimeOpen AND Liquid AND Setup AND Trigger, False );
Sell = 0; // deliberately absent: this component reports candidates only
// ------------------------------------------------------------- 6. output
// In Scan mode AmiBroker reports the Buy signals directly. In Exploration mode
// the same condition produces a table with the evidence behind each row, which
// is how you check that the specification says what you meant.
Filter = Buy;
AddTextColumn( FullName(), "Name", 28 );
AddColumn( Close, "Close", 1.2 );
AddColumn( BreakoutLevel, "Level broken", 1.2 );
AddColumn( 100 * SafeDivide( Close - BreakoutLevel, BreakoutLevel, Null ),
"% above level", 1.2 );
AddColumn( 100 * SafeDivide( Close - TrendMa, TrendMa, Null ),
"% above trend MA", 1.1 );
AddColumn( SafeDivide( Close - TrendMa, ATR( 20 ), Null ),
"ATR from trend", 1.2 );
AddColumn( Turnover, "Median turnover", 1.0 );
AddColumn( SafeDivide( Volume, Median( Volume, LiquidityPeriod ), Null ),
"Rel volume", 1.2 );
AddColumn( DateTime(), "Bar", formatDateTimeISO );
SetSortColumns( 2 );
// Most liquid candidate first when you page through the charts.
AddSummaryRows( 16, 1.0 );

Download scanner.afl115 lines

Fragment — not a complete formula

BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );
Trigger = Cross( Close, BreakoutLevel );

The lookback is shifted back one bar, so a bar cannot break out of a range that includes its own high. Without the shift the test is nearly impossible to satisfy, because today’s high is almost always at least today’s close — and the failure is silent: you simply get very few signals and assume the setup is rare.

The liquidity floor is measured on the decision bar for the same reason. A floor applied to the fill bar would use information from after the decision.

Fragment — not a complete formula

Trigger = Cross( Close, BreakoutLevel ); // an event: true on one bar
// NOT: Close > BreakoutLevel; // a state: true for a run of bars

This is the setup/trigger distinction in code. Cross() fires on the bar where the relationship first became true. The comparison stays true while price remains above the level, which is how a scanner ends up reporting the same names for a fortnight.

The missing-benchmark case is stated, not hidden

Section titled “The missing-benchmark case is stated, not hidden”

Fragment — not a complete formula

else
{
// No benchmark configured: the gate is open and the title says so. A
// silently absent filter is worse than a stated missing one.
BenchOK = False;
RegimeOpen = True;
}

Running without a benchmark is a legitimate thing to do while you are developing. Running without a benchmark and believing you have a regime filter is not. The formula makes the difference visible.

Fragment — not a complete formula

Filter = Buy;

In Scan mode AmiBroker reports the Buy signals directly. In Exploration mode the same condition produces a table with the evidence behind each row: the level that was broken, how far above it the close is, the distance from the trend average in ATR units, the turnover and the relative volume.

Use Explore while you are still deciding whether the specification says what you meant. Use Scan once it does.

Confirm the setup/trigger distinction empirically. Change Cross( Close, BreakoutLevel ) to Close > BreakoutLevel and re-run over the same range. The signal count should rise sharply and the same names should repeat on consecutive days. Change it back. Seeing the difference once fixes the concept permanently.

Confirm the regime gate binds. Run the scan twice over a period containing a sustained decline — once as written, once with RegimeOpen = True forced. The difference is what your gate removed. If there is no difference, the gate is not connected.

Hand-check one signal completely. Open the symbol’s chart on the signal date and verify all four conditions: the benchmark was above its average that day, the symbol’s close was above its trend average, the close crossed above the prior 50-bar high, and the turnover cleared the floor.

Check the shift. Temporarily remove the Ref( ..., -1 ) from the breakout level and re-run. The signal count should collapse to nearly zero. That is the confirmation that the shift is doing what the comment says.

Check the last bar. Run the scan with the range ending on a date you can verify by eye, and confirm the reported bar is the last completed one. Acting on a signal means the next session at the earliest, which is exactly the delay Component 6’s backtest assumes.

The same names every day. A state is being used where an event belongs. Look for > where Cross() was intended.

No signals at all, ever. Usually one of three things: the breakout lookback is not shifted; the regime gate is inverted; or RequiredBars exceeds the loaded history so Ready is never true. Test each by temporarily disabling it.

Signals on symbols you could never trade. The liquidity floor is too low, or Close * Volume is not money for those instruments. Add the turnover column and look at the smallest values.

The regime column is empty. The benchmark symbol is missing or misspelled. Foreign() returns an empty array and the comparison silently becomes false, which is why the formula tracks BenchOK separately.

Different signals in Scan and Explore. They should be identical — Filter = Buy guarantees it. If they differ, you have two versions of the file.

Signals disappear after a data update. The last bar changed. That is correct behaviour, and it is why the scan reports the last completed bar rather than the forming one.

  1. Add a second trigger and compare. A cross above the prior 50-bar high, versus a close above the prior 20-bar high with today’s volume above its median. Run both over the same range and count the overlap. Two triggers that fire on the same names are one trigger.

  2. Add a “days since setup began” column using BarsSince(). A breakout on the third day of an uptrend and one on the ninetieth day are arguably different events, and this is the cheapest way to find out whether your universe thinks so.

  3. Chain the scan with the exploration using #pragma sequence( scan, explore ) from Part 12, so one button gives you both the candidate list and the evidence.

  4. Write the specification as a standalone document — universe, liquidity, regime, setup, trigger — and hand it to somebody who can code, without the formula. If what they write returns a different list, your specification is incomplete, and finding out where is the whole exercise.

The five-element specification, in words, plus:

  • How the universe was built.
  • The benchmark symbol and its regime period.
  • The date of the scan and the range it covered.
  • One exported Explore table with the evidence columns.

Component 9 asks you to state the setup precisely enough that a reader could reproduce it. This is where that answer comes from.

Check your understanding

Question 1. What is the difference between a setup and a trigger, and what happens if you confuse them?
Show the answer and why

Answer: A setup is a state true over a span of bars; a trigger is an event true on one bar. Using a state where a trigger belongs makes the scanner return the same names every day

Cross() fires once, on the bar the relationship first became true. The comparison Close > Level stays true while price remains above it, which turns a scanner into a watch list. This is the state-versus-event distinction from Part 9, applied.

Question 2. Why is the regime read from a benchmark rather than from each candidate?
Show the answer and why

Answer: Because a single benchmark classifies every candidate the same way on the same date — a per-symbol version is just another setup condition wearing the word "regime"

A market-wide gate is a claim about the environment, applied identically to everything in it. If each symbol supplies its own definition, nothing market-wide is being tested and you have added a second trend filter.

Question 3. You remove the Ref( ..., -1 ) from the breakout level. What should happen and why?
BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );
Trigger       = Cross( Close, BreakoutLevel );
Show the answer and why

Answer: The signal count collapses to nearly zero, because the unshifted window includes today's high and today's close is almost never above today's high

Running this deliberately is the validation step, because the failure is otherwise silent: you get very few signals and conclude the setup is rare rather than that the level is wrong.

Question 4. Which statements about running the file in Scan versus Explore mode are correct? Select all that apply.
Show the answer and why

Answer: Filter = Buy makes both modes report the identical set of symbols, Scan gives a list of names; Explore gives the same names with the evidence that put them there, Explore is the output to attach to the research report

Trade delays are implemented only by the backtester — SetTradeDelays() does nothing in Scan, Exploration or Indicator modes. The other three are the reason the file is written to serve both modes from one condition.

Question 5. The scanner returns candidates every single day over a five-year range. What should you check first?
Show the answer and why

Answer: Whether the trigger is an event or a state, and whether the regime gate is actually reaching the Buy expression

A one-bar trigger gated by a market-wide regime should be silent for long stretches. Daily signals over five years means either a state where an event belongs, or a gate that is computed and then never used — both of which are a one-line check.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — New Analysis windowamibroker.com/guide/h_newanalysis.html2026-08-31
  2. 02AFL Function Reference — Crossamibroker.com/guide/afl/cross.html2026-08-31
  3. 03AFL Function Reference — HHVamibroker.com/guide/afl/hhv.html2026-08-31
  4. 04AFL Function Reference — Foreignamibroker.com/guide/afl/foreign.html2026-08-31
  5. 05AmiBroker User's Guide — Explorationamibroker.com/guide/h_exploration.html2026-08-31
  6. 06AFL Function Reference — Medianamibroker.com/guide/afl/median.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.