Component 2: Stock Exploration
Requirements
Section titled “Requirements”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.
Required columns
Section titled “Required columns”| 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 |
The complete formula
Section titled “The complete formula”Complete runnable 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 );
// ----------------------------------------------------------------- trendTrendMa = 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 );
// ------------------------------------------------------------ volatilityAtrPercent = 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 );
// ---------------------------------------------------------------- outputFilter = 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 ); // 1AddTextColumn( FullName(), "Name", 28 ); // 2AddColumn( DateTime(), "As of", formatDateTimeISO ); // 3AddColumn( Close, "Close", 1.2 ); // 4AddColumn( Turnover, "Median turnover", 1.0 ); // 5AddColumn( TrendCode, "Trend (-1/0/1)", 1.0 ); // 6AddColumn( Stretch, "ATR from trend", 1.2 ); // 7AddColumn( Momentum, "Momentum %", 1.2 ); // 8AddColumn( RelStrength, "vs benchmark %", 1.2 ); // 9AddColumn( AtrPercent, "ATR % of price", 1.2 ); // 10AddColumn( VolRank, "Volatility pctile", 1.0 ); // 11AddColumn( RelVolume, "Rel volume", 1.2 ); // 12AddColumn( 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 );How it works
Section titled “How it works”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 price — ATR % 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 history — Volatility 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.
The trend classification is a definition
Section titled “The trend classification is a definition”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.
The rank is a choice you are making
Section titled “The rank is a choice you are making”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.
Warm-up is handled by exclusion
Section titled “Warm-up is handled by exclusion”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.
Column numbering
Section titled “Column numbering”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.
Formatting and sorting
Section titled “Formatting and sorting”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.
Expected result
Section titled “Expected result”Validation
Section titled “Validation”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.
Common errors
Section titled “Common errors”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.
Extensions
Section titled “Extensions”-
Add a sector or industry column with
SectorID()orIndustryID()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. -
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.
-
Add
AddRankColumn()so the table carries an explicit rank position rather than only a score. Note that multipleSetSortColumnscalls interact with it — read the reference before assuming. -
Journal the run with the
Status( "stocknum" ) == 0technique from Part 12, so every exported table has a matching record of the settings that produced it.
What to record for the report
Section titled “What to record for the report”- 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
Sources for this lesson
7 verified · checked 2026-08-31
- 01AmiBroker User's Guide — Explorationamibroker.com/guide/h_exploration.html2026-08-31
- 02AFL Function Reference — Median§ LOWER median is returned when period is evenamibroker.com/guide/afl/median.html2026-08-31
- 03AFL Function Reference — PercentRankamibroker.com/guide/afl/percentrank.html2026-08-31
- 04AFL Function Reference — SetSortColumnsamibroker.com/guide/afl/setsortcolumns.html2026-08-31
- 05AFL Function Reference — AddSummaryRowsamibroker.com/guide/afl/addsummaryrows.html2026-08-31
- 06AFL Function Reference — GetOption§ NoDefaultColumnsamibroker.com/guide/afl/getoption.html2026-08-31
- 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.