Component 3: Scanner
Requirements
Section titled “Requirements”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.
The five things that must be documented
Section titled “The five things that must be documented”Every scanner needs all five, in this order
- UniverseWhich instruments are eligible at all, and how that list was built — including whether it is point-in-time.
- LiquidityThe floor below which a candidate is not tradeable at your size. Measured on the decision bar.
- RegimeThe market-wide condition under which the setup is permitted. Read from a benchmark, not from the candidate.
- SetupThe state that makes an instrument a candidate. True over a span of bars.
- 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.
The specification this formula implements
Section titled “The specification this formula implements”| 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. |
The complete formula
Section titled “The complete formula”Complete runnable 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. liquidityTurnover = Median( Close * Volume, LiquidityPeriod );Liquid = IIf( Ready, Turnover >= MinTurnover AND Close >= MinPrice, False );
// -------------------------------------------------------------- 3. setupTrendMa = MA( Close, TrendPeriod );FastMa = MA( Close, FastPeriod );Setup = Close > TrendMa AND FastMa > TrendMa;
// ------------------------------------------------------------ 4. triggerBreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );Trigger = Cross( Close, BreakoutLevel );
// -------------------------------------------------------- 5. the signalBuy = 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 );How it works
Section titled “How it works”Everything is decidable before the order
Section titled “Everything is decidable before the order”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.
Cross() rather than a comparison
Section titled “Cross() rather than a comparison”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 barsThis 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.
One file, two modes
Section titled “One file, two modes”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.
Expected result
Section titled “Expected result”Validation
Section titled “Validation”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.
Common errors
Section titled “Common errors”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.
Extensions
Section titled “Extensions”-
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.
-
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. -
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. -
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.
What to record for the report
Section titled “What to record for the report”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
Sources for this lesson
6 verified · checked 2026-08-31
- 01AmiBroker User's Guide — New Analysis windowamibroker.com/guide/h_newanalysis.html2026-08-31
- 02AFL Function Reference — Crossamibroker.com/guide/afl/cross.html2026-08-31
- 03AFL Function Reference — HHVamibroker.com/guide/afl/hhv.html2026-08-31
- 04AFL Function Reference — Foreignamibroker.com/guide/afl/foreign.html2026-08-31
- 05AmiBroker User's Guide — Explorationamibroker.com/guide/h_exploration.html2026-08-31
- 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.