Skip to content
Level 5 · Real-Time AmiBroker UserProjectPart Capstone · page 3 of 960 min
60Minutes
22AFL functions
7Sources
StandardRequires
AFL functions taught here22

Component 2: Stock Exploration

One table row per symbol, on the last bar of the range, carrying every measurement the rest of the capstone depends on. It is the instrument panel, not the decision — nothing in it is a signal.

Column What it measures Why it is there
Symbol, Name, As of Identification So a saved table can be dated and reproduced
Close Price level The scale everything else is normalised against
Median turnover Liquidity Whether a position could be established at all
Trend (−1/0/1) Trend state A defined classification, not a discovery
ATR from trend Distance from the average, in volatility units Comparable across instruments
Momentum % Plain change over a lookback No smoothing, so the name is the definition
vs benchmark % Relative strength Whether the move was the instrument or the market
ATR % of price Volatility, normalised Comparable across instruments
Volatility percentile Volatility against its own history Comparable across time
Rel volume Today’s volume against its own median Whether today was busier than usual
Rank score One ordering number Because a table you cannot sort is a list

Complete runnable AFL

stock-exploration.afl
// stock-exploration.afl
// Capstone Component 2 - Stock Exploration
//
// GOAL
// One table row per symbol on the last bar of the range, carrying every
// measurement the rest of the capstone depends on: liquidity, trend state,
// distance from trend, momentum, volatility, relative volume, relative
// strength against a benchmark, and a rank.
//
// It is the instrument panel, not the decision. Nothing in it is a signal.
//
// HOW TO RUN
// Analysis -> Apply to: your universe. Periodicity: Daily.
// Range: n last quotations, at least RequiredBars + 50. Press EXPLORE.
// Sort by the rank column and read the top and the bottom.
//
// ASSUMPTIONS - carry these into the capstone report
// Interval daily, split- and dividend-adjusted bars.
// Universe the watch list the Analysis window points at, exactly as it
// stands today. If it was built from a current membership
// list it contains survivorship bias and this table cannot
// remove it.
// Benchmark the symbol named below. It must exist and share the
// universe's trading calendar.
// Turnover Close * Volume, which is money only when Volume is share
// volume. Not true for futures or for cent-quoted symbols.
// Warm-up every symbol with fewer than RequiredBars bars is excluded
// rather than shown with partial values.
// Not modelled costs, slippage, position size, borrow, or anything about
// what happens next.
// Running totals and percentile ranks need every loaded bar, not the QuickAFL
// subset.
SetBarsRequired( sbrAll, sbrAll );
BenchSymbol = ParamStr( "Benchmark symbol", "" );
TrendPeriod = Param( "Trend average (bars)", 200, 20, 400, 10 );
FastPeriod = Param( "Fast average (bars)", 50, 5, 200, 5 );
MomPeriod = Param( "Momentum look-back (bars)", 63, 5, 400, 1 );
AtrPeriod = Param( "ATR period", 20, 2, 100, 1 );
VolPeriod = Param( "Volume average (bars)", 50, 5, 400, 5 );
RankPeriod = Param( "Volatility percentile look-back", 252, 20, 1000, 1 );
MinTurnover = Param( "Minimum median turnover", 2000000, 0, 100000000, 250000 );
MinPrice = Param( "Minimum close", 2, 0, 500, 0.5 );
RequiredBars = Max( Max( TrendPeriod, RankPeriod ), MomPeriod ) + 1;
Ready = BarIndex() >= RequiredBars;
// ------------------------------------------------------------- liquidity
// Median rather than mean: one index-rebalance day can be twenty times a
// normal day, and a mean carries that day for the whole window.
Turnover = Median( Close * Volume, VolPeriod );
Liquid = IIf( Ready, Turnover >= MinTurnover AND Close >= MinPrice, False );
// ----------------------------------------------------------------- trend
TrendMa = MA( Close, TrendPeriod );
FastMa = MA( Close, FastPeriod );
// Three states, defined rather than discovered. Change the periods and the
// classification changes with them.
UpTrend = Close > TrendMa AND FastMa > TrendMa;
DownTrend = Close < TrendMa AND FastMa < TrendMa;
TrendCode = IIf( Ready, IIf( UpTrend, 1, IIf( DownTrend, -1, 0 ) ), Null );
Volatility = ATR( AtrPeriod );
Stretch = SafeDivide( Close - TrendMa, Volatility, Null );
// -------------------------------------------------------------- momentum
// Plain percentage change over the look-back. No smoothing, so the number
// means exactly what its name says.
Momentum = ROC( Close, MomPeriod );
// ------------------------------------------------------------ volatility
AtrPercent = 100 * SafeDivide( Volatility, Close, Null );
VolRank = IIf( Ready, PercentRank( AtrPercent, RankPeriod ), Null );
// --------------------------------------------------------- relative volume
// Today's volume against its own recent median. Above 1 means busier than
// usual for this instrument; it says nothing about direction.
RelVolume = SafeDivide( Volume, Median( Volume, VolPeriod ), Null );
// ------------------------------------------------------- relative strength
// The ratio of the symbol's move to the benchmark's move over the same window.
// A missing benchmark leaves the column empty rather than substituting zero.
HaveBench = StrLen( BenchSymbol ) > 0;
if( HaveBench )
{
BenchClose = Foreign( BenchSymbol, "C" );
BenchMove = ROC( BenchClose, MomPeriod );
RelStrength = IIf( IsNull( BenchClose ), Null, Momentum - BenchMove );
}
else
{
RelStrength = Null;
}
// ------------------------------------------------------------------ rank
// A single ordering number, built from two standardised inputs so that neither
// can dominate purely because of its units. This is a CHOICE: preferring high
// momentum and low volatility is one coherent preference among many, and a
// different one will select different symbols.
MomScore = PercentRank( Momentum, RankPeriod );
RankBase = IIf( Ready, 0.7 * MomScore + 0.3 * ( 100 - VolRank ), Null );
// ---------------------------------------------------------------- output
Filter = Status( "lastbarinrange" ) AND Liquid AND NOT IsNull( RankBase );
// Default Ticker and Date/Time columns are switched off so that column
// numbers are fixed and are ours to control. Remember that this renumbering
// also shifts every column number used by SetSortColumns and AddSummaryRows.
SetOption( "NoDefaultColumns", True );
AddTextColumn( Name(), "Symbol", 12 ); // 1
AddTextColumn( FullName(), "Name", 28 ); // 2
AddColumn( DateTime(), "As of", formatDateTimeISO ); // 3
AddColumn( Close, "Close", 1.2 ); // 4
AddColumn( Turnover, "Median turnover", 1.0 ); // 5
AddColumn( TrendCode, "Trend (-1/0/1)", 1.0 ); // 6
AddColumn( Stretch, "ATR from trend", 1.2 ); // 7
AddColumn( Momentum, "Momentum %", 1.2 ); // 8
AddColumn( RelStrength, "vs benchmark %", 1.2 ); // 9
AddColumn( AtrPercent, "ATR % of price", 1.2 ); // 10
AddColumn( VolRank, "Volatility pctile", 1.0 ); // 11
AddColumn( RelVolume, "Rel volume", 1.2 ); // 12
AddColumn( RankBase, "Rank score", 1.1 ); // 13
// Highest rank score first.
SetSortColumns( -13 );
// COUNT and AVERAGE for the measurement columns only. The average weights
// every symbol equally, which is what you want for a cross-sectional snapshot
// and is NOT what you want for pooling observations over time.
AddSummaryRows( 2 | 16, 1.2, 7, 8, 9, 10, 11, 12, 13 );

Download stock-exploration.afl134 lines

Two kinds of normalisation, used deliberately

Section titled “Two kinds of normalisation, used deliberately”

Almost every column in this table exists to make different instruments comparable, and there are exactly two ways to do it.

Against the instrument’s own priceATR % of price, ATR from trend. Divides out the price level, so a £4 share and a £400 share can be read on one axis.

Against the instrument’s own historyVolatility percentile, Rel volume, and the momentum component of the rank. Uses PercentRank or a median, so the number says “unusual for this symbol” rather than “large in absolute terms”.

Confusing the two is the most common source of a screen that quietly selects one kind of instrument. A raw ATR column, for instance, sorts your universe by price level.

Median rather than mean, for both liquidity and volume

Section titled “Median rather than mean, for both liquidity and volume”

Fragment — not a complete formula

Turnover = Median( Close * Volume, VolPeriod );
RelVolume = SafeDivide( Volume, Median( Volume, VolPeriod ), Null );

Turnover and volume are heavily skewed: one index-rebalance day can be twenty times a normal day, and a mean carries that day for the whole window. The median does not.

Note the documented subtlety: Median() returns the lower median when the period is even. Percentile( array, period, 50 ) averages the two middle values instead, at the cost of speed. For a screening filter the difference is immaterial; for a published statistic, say which you used.

Fragment — not a complete formula

UpTrend = Close > TrendMa AND FastMa > TrendMa;
DownTrend = Close < TrendMa AND FastMa < TrendMa;
TrendCode = IIf( Ready, IIf( UpTrend, 1, IIf( DownTrend, -1, 0 ) ), Null );

Three states, and they are defined here rather than discovered. Change the periods and the classification changes with them. Nothing about this says an uptrend continues; it says what the word means in this table, so that every row uses it the same way.

Fragment — not a complete formula

MomScore = PercentRank( Momentum, RankPeriod );
RankBase = IIf( Ready, 0.7 * MomScore + 0.3 * ( 100 - VolRank ), Null );

Both inputs are percentiles, so neither can dominate merely because of its units — a raw momentum percentage added to a raw ATR percentage would be arithmetic on incommensurable quantities.

The weights are a preference: “mostly prefer strong momentum, and among equals prefer the calmer instrument.” That is one coherent preference among many, and a different one will select different symbols. Write your choice and your reasoning in the research log; Component 9 asks about it.

Fragment — not a complete formula

Ready = BarIndex() >= RequiredBars;
Filter = Status( "lastbarinrange" ) AND Liquid AND NOT IsNull( RankBase );

Symbols without enough history do not appear at all, rather than appearing with partial values.

SetOption( "NoDefaultColumns", True ) removes the automatic Ticker and Date/Time columns so that column positions are fixed and yours. Every column is then numbered in a trailing comment, because SetSortColumns() and AddSummaryRows()’s onlycols argument both address columns by one-based number — and inserting a column silently shifts every one of them.

SetSortColumns( -13 ) sorts descending by rank score: negative means descending, and 13 is the rank column’s position.

AddSummaryRows( 2 | 16, 1.2, 7, 8, 9, 10, 11, 12, 13 ) requests AVERAGE (2) and COUNT (16) for the measurement columns only, to two decimal places. Summary rows appear at the top of the result list.

Hand-check one row completely. Pick a symbol, open its chart, and verify every column against what you can read there: close, moving averages, the 63-bar change, the ATR. All eleven measurements should reconcile.

Check the benchmark column on a day the market moved a lot. On such a day most symbols’ vs benchmark % should be small even though their Momentum % is large, because both moved. If the two columns are nearly identical, the benchmark is not being read — check the symbol name.

Check that the exclusions are the ones you intended. Temporarily change Filter to Status( "lastbarinrange" ) alone and compare the row count. The difference is what your liquidity floor and warm-up removed; look at a few of them and confirm you meant to remove them.

Check the sort. The top row by rank score should have high momentum and low-to-middling volatility. If the top rows are the most volatile symbols, the sign on the volatility term is wrong.

Save the table with its date. Export it and name the file with the run date. Component 9 asks for one saved output, and a table you cannot date is a table you cannot reproduce.

Almost no rows. Either the liquidity floor is too high for your universe, or RequiredBars exceeds the loaded history. The COUNT row and a temporary Filter relaxation distinguish them in thirty seconds.

The sort is on the wrong column. NoDefaultColumns was toggled without renumbering, so every column moved by two. This is the single most common exploration bug.

vs benchmark % is empty for every row. BenchSymbol is blank or misspelled. The formula returns Null rather than substituting zero, which is what makes the failure visible.

Rank scores are all near 50. Both percentile inputs are near their midpoints because RankPeriod exceeds the loaded history, so PercentRank never has a full window.

Turnover looks implausible. Close * Volume is money only when Volume is share volume. It is not for futures, not for contracts, and not for symbols quoted in cents. Check what your data vendor supplies before believing a liquidity filter.

Different results on consecutive runs with no edits. The date range is “n last quotations” and new data arrived, or QuickAFL is trimming the arrays. SetBarsRequired( sbrAll, sbrAll ) handles the second; only recording the range handles the first.

  1. Add a sector or industry column with SectorID() or IndustryID() and their category names, then check whether your top-ranked names are concentrated in one sector. If they are, your rank is partly a sector bet — which is worth knowing before Component 6.

  2. Add a second rank with different weights — say 0.3 momentum and 0.7 low volatility — and compare the top twenty of each. The overlap tells you how much your ranking choice actually matters, and it is usually less overlap than people expect.

  3. Add AddRankColumn() so the table carries an explicit rank position rather than only a score. Note that multiple SetSortColumns calls interact with it — read the reference before assuming.

  4. Journal the run with the Status( "stocknum" ) == 0 technique from Part 12, so every exported table has a matching record of the settings that produced it.

  • The universe, and how it was built — including whether it is a point-in-time list.
  • The benchmark symbol.
  • Every lookback period, and why you chose it.
  • The rank weights, and what preference they encode.
  • One exported table, dated.

Check your understanding

Question 1. Why are both inputs to the rank score percentiles rather than raw values?
MomScore = PercentRank( Momentum, RankPeriod );
RankBase = 0.7 * MomScore + 0.3 * ( 100 - VolRank );
Show the answer and why

Answer: Because adding a raw momentum percentage to a raw ATR percentage is arithmetic on incommensurable quantities — one would dominate purely because of its units

Putting both on a common 0–100 scale is what makes the weights mean what they say. With raw values, "0.7 momentum and 0.3 volatility" would describe the code but not the behaviour.

Question 2. Excluding symbols with insufficient history is defensible here but a bug in a backtest. Why?
Show the answer and why

Answer: A snapshot asks what is currently tradeable; a study asks what happened. Excluding short-history symbols from a study removes everything that listed late and everything that stopped trading

The same line means different things depending on what the output is for. In a study, warm-up belongs per bar — a BarIndex() guard — rather than per symbol, because "short history" correlates with exactly the outcomes you must not delete.

Question 3. Your sort is on the wrong column after you added a new AddColumn call. What happened?
Show the answer and why

Answer: Column numbers are one-based positions, so inserting a column shifts every subsequent number that SetSortColumns and AddSummaryRows refer to

This is why every column in the formula carries its number in a trailing comment. Toggling NoDefaultColumns does the same thing on a larger scale, shifting everything by two.

Question 4. Which normalisations make a symbol comparable against its OWN history rather than against other symbols? Select all that apply.
Show the answer and why

Answer: Volatility percentile via PercentRank, Relative volume against the median volume, The momentum percentile component of the rank

ATR as a percentage of price divides out the price level, which makes it comparable across instruments. The other three compare a value against that instrument's own recent distribution, which is a different question — and mixing the two up is how a screen quietly selects one kind of instrument.

Question 5. The AVERAGE summary row weights every symbol equally. When is that wrong?
Show the answer and why

Answer: When pooling observations over time, where a symbol contributing 400 rows should not count the same as one contributing a single row

For a cross-sectional snapshot, equal weighting is exactly what you want: the typical symbol today. For a longitudinal study the same row silently answers a different question, and correcting it requires exporting the table and weighting by each symbol's own count.

Sources for this lesson

7 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — Explorationamibroker.com/guide/h_exploration.html2026-08-31
  2. 02AFL Function Reference — Median§ LOWER median is returned when period is evenamibroker.com/guide/afl/median.html2026-08-31
  3. 03AFL Function Reference — PercentRankamibroker.com/guide/afl/percentrank.html2026-08-31
  4. 04AFL Function Reference — SetSortColumnsamibroker.com/guide/afl/setsortcolumns.html2026-08-31
  5. 05AFL Function Reference — AddSummaryRowsamibroker.com/guide/afl/addsummaryrows.html2026-08-31
  6. 06AFL Function Reference — GetOption§ NoDefaultColumnsamibroker.com/guide/afl/getoption.html2026-08-31
  7. 07AFL Function Reference — Status§ lastbarinrangeamibroker.com/guide/afl/status.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.