Skip to content
Level 3 · AFL DeveloperLessonPart 13 · page 4 of 530 min
30Minutes
8AFL functions
5Sources
StandardRequires
AFL functions taught here8

StaticVarGenerateRanks() and Top-N Selection

You now have a way to leave one number per symbol somewhere every execution can see it. StaticVarGenerateRanks() is the function that reads that whole family of numbers, sorts them across symbols bar by bar, and writes the resulting positions back as a new family of static variables.

It is a well-behaved function with an unusually high number of ways to use it wrongly and get plausible output. This lesson goes through the signature, the naming rules, the direction question, and — most importantly — a procedure that will tell you whether the ranking in front of you is correct.

The AFL Function Reference page prints its own SYNTAX line as:

StaticVarGenarateRanks( "outputprefix", "inputprefix", topranks, tiemode )

The real call, then, is:

Fragment — not a complete formula

StaticVarGenerateRanks( "outputprefix", "inputprefix", topranks, tiemode );
Argument Meaning
outputprefix String prepended to the generated rank variables
inputprefix String identifying the score variables to read
topranks 0 for normal mode; positive for top-N; negative for bottom-N
tiemode How ties are numbered. Documented values: 1234 and 1224

It returns nothing. It was introduced in AmiBroker 5.70.

inputprefix is not a list of variables — it is a pattern. AmiBroker searches every static variable whose name begins with that prefix and assumes the remaining part of the name is a stock symbol. So with an input prefix of "P13Score", a variable named P13ScoreMSFT contributes MSFT’s score, and a stray variable named P13ScoreTemp would contribute the score of a symbol called “Temp”.

One ranking call, from scores to positions

  1. P13ScoreAAAarray of scores
  2. P13ScoreBBBarray of scores
  3. Sort across symbols, bar by barranks start at ONE
  4. P13RankP13ScoreAAAarray of ranks
  5. P13RankP13ScoreBBBarray of ranks
Output names concatenate BOTH prefixes and then the symbol. Every output is an array, one rank per bar.

With topranks set to 0, the output variable for each symbol is named outputprefix + inputprefix + symbol. This is the single detail people get wrong most often, because the name is longer than you expect. After

Fragment — not a complete formula

StaticVarGenerateRanks( "Rank", "P13Score", 0, 1224 );

MSFT’s rank array is in RankP13ScoreMSFT, not in RankMSFT. Read it back with the same concatenation you used to build it.

Choose the two prefixes so the output family cannot be mistaken for an input family. "Rank" plus "P13Score" gives outputs beginning RankP13Score, which does not begin with P13Score, so a second call is safe. Had the input prefix been "P13", the outputs would have begun RankP13... — still safe — but an input prefix of "Rank" with an output prefix of "Rank" would feed the function its own results.

With topranks greater than zero you get top-N mode; with topranks less than zero, bottom-N mode. The outputs are named outputprefix + inputprefix + N where N is 1, 2, 3 and so on, and — this is the trap — they hold indexes, not scores and not tickers. With an output prefix of Top and an input prefix of ROC, TopROC1 holds the index of the top-rated value.

To turn an index into a ticker, the function also writes a companion variable named outputprefix + inputprefix + "Symbols" holding a comma-separated list, so an index of 1 means the first entry in TopROCSymbols. StaticVarGetRankedSymbols( "outputprefix", "inputprefix", datetime ) does that lookup for you and returns a comma-separated string for one date and time. The Ranking chapter describes what comes back as ranked symbols while the underlying companion variable is described as holding variable names; check what your build returns on the first run and strip the prefix if you see one.

Ranks count from one. The documentation states this twice on the same page, which is usually a sign that people get it wrong.

There is no direction argument. The documentation states that ranks start at one and describes positive topranks as selecting top-ranked values; taken with the chapter’s opening description of a sorted list whose first entry is the best performer, the straightforward reading is that in normal mode rank 1 holds the highest score. That is what this course assumes.

It is also, precisely, an inference. Confirming it in your own build takes two minutes and the procedure is below, so do that rather than take it on trust. If your check disagrees, the fix is not an argument you have missed — there isn’t one. You reverse the order by negating the score before you store it:

Fragment — not a complete formula

// rank smallest-first instead of largest-first
StaticVarSet( InputPrefix + Sym, -SymbolScore );

That is worth knowing for its own sake. Ranking by lowest volatility, or by smallest spread, is a perfectly ordinary requirement, and negation is how you express it.

An Exploration that ranks every symbol of the Analysis window’s universe on a momentum score, reports each symbol’s own rank, and reports enough extra information to show whether the ranking is sound.

Complete runnable AFL

universe-ranks.afl
// universe-ranks.afl
// Part 13 - StaticVarGenerateRanks() and Top-N Selection
//
// Turns one score per symbol into one rank per symbol per bar across the whole
// universe, and reports enough extra information to prove the ranking is sane
// rather than merely plausible.
//
// How to run it: Analysis window, Apply to = Filter (a watch list) or All,
// Range = 1 recent bar, then Explore.
//
// Assumptions declared up front:
// - Daily bars. The ranking is computed on the bar grid of whichever symbol
// AmiBroker happens to process first, so a first symbol with a short or
// gappy history limits the whole ranking. The lesson explains the fix.
// - StaticVarGenerateRanks is expensive. It is called ONCE, inside the
// Status("stocknum") == 0 block, exactly as the documentation insists.
// - The output family is named outputprefix + inputprefix + symbol, so with
// the prefixes below MSFT's rank lives in "P13RankP13ScoreMSFT".
// - Note the official function-reference page misspells the name in its
// SYNTAX line ("StaticVarGenarateRanks"). The callable name is
// StaticVarGenerateRanks, spelled as it is below.
InputPrefix = "P13Score";
OutputPrefix = "P13Rank";
MaxRankName = "P13HighestRank";
ScoredName = "P13ScoredCount";
ScorePeriod = Param( "Momentum lookback (bars)", 126, 20, 500, 1 );
TopCount = Param( "Report top N", 20, 1, 200, 1 );
TieToggle = ParamToggle( "Tie mode", "1224 shared ranks|1234 consecutive ranks", 0 );
DirToggle = ParamToggle( "Rank direction", "Highest score first|Lowest score first", 0 );
if( TieToggle ) TieMode = 1234; else TieMode = 1224;
ApplySetting = GetOption( "ApplyTo" );
if( ApplySetting == 2 )
{
WatchListNum = GetOption( "FilterIncludeWatchlist" );
SymbolList = CategoryGetSymbols( categoryWatchlist, WatchListNum );
}
else if( ApplySetting == 0 )
{
SymbolList = CategoryGetSymbols( categoryAll, 0 );
}
else
{
Error( "Set Apply to = All symbols, or Filter with an Include watch list." );
}
// ------------------------------------------------------------------- PASS 1
if( Status( "stocknum" ) == 0 )
{
// Remove the inputs AND the previous run's outputs. The asterisk is not
// decoration: without it nothing is removed at all.
StaticVarRemove( InputPrefix + "*" );
StaticVarRemove( OutputPrefix + InputPrefix + "*" );
StaticVarRemove( MaxRankName );
StaticVarRemove( ScoredName );
for( i = 0; ( Sym = StrExtract( SymbolList, i ) ) != ""; i++ )
{
if( SetForeign( Sym ) )
{
SymbolScore = ROC( Close, ScorePeriod );
RestorePriceArrays();
// Ranking direction is not an argument of the ranking function.
// Negating the score is how you turn "highest first" into
// "lowest first" - there is no third option.
if( DirToggle ) SymbolScore = -SymbolScore;
StaticVarSet( InputPrefix + Sym, SymbolScore );
}
}
// Called exactly once. topranks = 0 selects normal ranking mode, in which
// every ranked symbol gets its own output array.
StaticVarGenerateRanks( OutputPrefix, InputPrefix, 0, TieMode );
// -------------------------------------------------------- self-check
// Read every rank back and record two numbers the reader can compare:
// how many symbols had a real score on each bar, and the highest rank
// the function actually issued. If the second is larger than the first,
// something that was not a score has been ranked.
HighestRank = 0;
ScoredSymbols = 0;
for( i = 0; ( Sym = StrExtract( SymbolList, i ) ) != ""; i++ )
{
ThisScore = StaticVarGet( InputPrefix + Sym );
ThisRank = StaticVarGet( OutputPrefix + InputPrefix + Sym );
HighestRank = Max( HighestRank, Nz( ThisRank ) );
ScoredSymbols = ScoredSymbols + ( NOT IsNull( ThisScore ) );
}
StaticVarSet( MaxRankName, HighestRank );
StaticVarSet( ScoredName, ScoredSymbols );
}
// ------------------------------------------------------------------- PASS 2
OwnScore = StaticVarGet( InputPrefix + Name() );
OwnRank = StaticVarGet( OutputPrefix + InputPrefix + Name() );
RankIssued = StaticVarGet( MaxRankName );
Scored = StaticVarGet( ScoredName );
// Rank 1 is the first rank AmiBroker issues; ranks count from ONE, not zero.
RankPercent = 100 * OwnRank / Max( Scored, 1 );
Filter = NOT IsNull( OwnRank );
AddColumn( OwnScore, "Score", 1.2 );
AddColumn( OwnRank, "Rank", 1.0 );
AddColumn( RankPercent, "Rank as % of field", 1.1 );
AddColumn( Scored, "Symbols scored", 1.0 );
AddColumn( RankIssued, "Highest rank issued", 1.0 );
AddTextColumn( WriteIf( OwnRank <= TopCount, "selected", "" ),
"Top " + NumToStr( TopCount, 1.0 ) );
SetSortColumns( 4 );

Download universe-ranks.afl121 lines

The shape is the two-pass pattern from the previous lesson with the ranking call inserted between the passes. Pass one removes both the input family and the previous run’s output family, loops the universe writing one score per symbol, and then calls StaticVarGenerateRanks() exactly once. The direction toggle negates the score before storing, which is the only mechanism there is for reversing the order.

After the ranking call, a second loop reads every rank straight back out of the store. That loop is the self-check: it records the highest rank the function issued and how many symbols had a non-Null score, and stores both. Pass two then reports each symbol’s score, its rank, its rank as a percentage of the field, and those two diagnostic numbers.

If the highest rank issued is larger than the number of symbols scored, something that was not a score has been given a position. That is a real possibility, because the documentation does not state how Null input values are treated, and it is exactly the kind of thing that should be visible in the output rather than assumed away.

  • StaticVarGenerateRanks( "outputprefix", "inputprefix", topranks, tiemode ) — the ranking call; topranks = 0 selects normal mode.
  • StaticVarGet( "varname", align = True ) — reads back both the scores and the ranks.
  • Max( ARRAY1, ARRAY2 ) — used to track the highest rank issued across the loop, and to keep the percentage division defined when nothing has been scored yet.
  • ParamToggle( "name", "values", defaultval = 0 ) — supplies the tie mode and the direction without editing the formula.

One row per ranked symbol, sorted by rank ascending, so rank 1 is the top row. The “Symbols scored” and “Highest rank issued” columns hold the same value on every row. On a healthy run those two numbers are equal, or the highest rank is slightly smaller when the tie mode is 1224 and ties have compressed the top of the range.

This is the procedure worth doing once properly, because it settles the direction question and the correctness question together.

  1. Build a watch list of six symbols and nothing else.
  2. Set the Analysis range to one recent bar and run the Exploration.
  3. Copy the Score and Rank columns into a spreadsheet.
  4. Sort your copy by Score, descending. Number the rows 1 to 6.
  5. Compare your numbering with the Rank column.

If they match, rank 1 is the highest score in your build and the function is doing what you think. If they are reversed, negate the score. If they match for five rows and not the sixth, look at that symbol’s score: it is almost certainly Null, and you have just discovered how your build ranks missing values.

  • Every rank is Null. The read-back name is wrong. It must be outputprefix + inputprefix + symbol, both prefixes, in that order.
  • Ranks go far higher than the number of symbols. A previous run’s scores are still in memory. Check that the removal call ends with an asterisk, and check it in the inspector formula from the previous lesson.
  • The ranking is stale after you change the score period. Pass one only runs on the first symbol; if you changed a parameter but the Analysis did not re-run from the start, you are reading the old ranks.
  • Two symbols hold rank 1 unexpectedly. Tie mode 1224 numbers ties with an equal rank. Switch to 1234 if you need a strict order.

Add a column for the symbol’s rank twenty bars ago — Ref( OwnRank, -20 ) — and a column for the change. A ranking that reshuffles completely every month is telling you something about the score you chose.

If you already have ranks as arrays, the top N is a comparison:

Fragment — not a complete formula

IsTopN = OwnRank <= 20;

This is usually what you want inside an Exploration, because it keeps the score, the rank and the selection in one table where they can be checked against each other. Remember that in tie mode 1224 this can return more than twenty rows.

The dedicated mode is for the other question: not “where does this symbol stand?” but “who is at the top right now?”. It builds the tables of indexes described above and the companion symbol list, and StaticVarGetRankedSymbols() reads them for one bar.

A chart pane that names the strongest and weakest symbols of a small universe at the bar you have selected, and plots the current symbol’s own rank.

Complete runnable AFL

top-ranked-symbols.afl
// top-ranked-symbols.afl
// Part 13 - StaticVarGenerateRanks() and Top-N Selection
//
// A chart pane that names the strongest and weakest symbols of a small
// universe at the bar you click on. It follows the documented one-pass
// indicator pattern: clear, score, generate, read - all in one execution.
//
// Assumptions declared up front:
// - This re-ranks the entire list on EVERY chart redraw. The documentation
// measures the ranking function at roughly 20 ms per 15,000 bars and 7
// symbols, so keep the list short. Twenty symbols is comfortable; five
// hundred is not, and belongs in an Analysis run instead.
// - Top and bottom mode store INDEXES into a companion list, not scores and
// not tickers. StaticVarGetRankedSymbols does that lookup for you.
// - Ranks count from ONE.
// - The list is taken from a watch list unless you type symbols yourself.
_SECTION_BEGIN( "Top ranked symbols" );
InputPrefix = "P13ChartScore";
NormalPrefix = "P13ChartRank";
TopPrefix = "P13ChartTop";
BottomPrefix = "P13ChartBottom";
TypedList = ParamStr( "Symbols (comma separated, blank = watch list)", "" );
WatchListNum = Param( "Watch list number", 0, 0, 200, 1 );
ScorePeriod = Param( "Momentum lookback (bars)", 126, 20, 500, 1 );
HowMany = Param( "How many at each end", 3, 1, 20, 1 );
if( TypedList == "" )
SymbolList = CategoryGetSymbols( categoryWatchlist, WatchListNum );
else
SymbolList = TypedList;
// Clear first. Every one of these names is shared with every other formula in
// this AmiBroker, so a stale entry is not a local problem.
StaticVarRemove( InputPrefix + "*" );
StaticVarRemove( NormalPrefix + InputPrefix + "*" );
StaticVarRemove( TopPrefix + InputPrefix + "*" );
StaticVarRemove( BottomPrefix + InputPrefix + "*" );
for( i = 0; ( Sym = StrExtract( SymbolList, i ) ) != ""; i++ )
{
if( SetForeign( Sym ) )
{
SymbolScore = ROC( Close, ScorePeriod );
RestorePriceArrays();
StaticVarSet( InputPrefix + Sym, SymbolScore );
}
}
// Three calls, three modes. Normal mode gives every symbol its own rank array;
// positive topranks builds the top-N table; negative topranks builds the
// bottom-N table.
StaticVarGenerateRanks( NormalPrefix, InputPrefix, 0, 1224 );
StaticVarGenerateRanks( TopPrefix, InputPrefix, HowMany, 1224 );
StaticVarGenerateRanks( BottomPrefix, InputPrefix, -HowMany, 1224 );
OwnRank = StaticVarGet( NormalPrefix + InputPrefix + Name() );
Plot( OwnRank, "Rank of " + Name(), colorBlue, styleLine | styleThick );
// The ranked-symbol lookup is per date/time, not per array, so it answers for
// the bar you have selected on the chart and no other.
SelectedDate = SelectedValue( DateTime() );
Title =
"Universe: " + NumToStr( StrCount( SymbolList, "," ) + 1, 1.0 ) + " symbols " +
"lookback " + NumToStr( ScorePeriod, 1.0 ) + " bars" + "\n" +
Name() + " rank: " + WriteVal( OwnRank, 1.0 ) + "\n" +
"Strongest: " + StaticVarGetRankedSymbols( TopPrefix, InputPrefix, SelectedDate ) + "\n" +
"Weakest: " + StaticVarGetRankedSymbols( BottomPrefix, InputPrefix, SelectedDate );
_SECTION_END();

Download top-ranked-symbols.afl74 lines

Everything happens in one execution, which is the documented indicator pattern: clear, score, generate, read. Three ranking calls are made — normal mode for the plotted rank line, positive topranks for the strongest, negative for the weakest — and each writes its own family of variables, which is why all three families are removed at the start.

The lookup is per date and time rather than per bar index, so the formula takes SelectedValue( DateTime() ) and asks for the ranked list at that instant. Click a different bar and the title changes.

  • StaticVarGetRankedSymbols( "outputprefix", "inputprefix", datetime ) — returns the comma-separated ranked list for one date and time. The prefixes must match the pair passed to the ranking call exactly.
  • SelectedValue( ARRAY ) — the value of an array at the selected bar.
  • DateTime() — the date and time array for the current symbol.
  • StrCount( "string", "substring" ) — counts separators, used here to report how many symbols are in the list.

A line showing this symbol’s rank within the list, and a title naming the three strongest and three weakest at the selected bar. Clicking through bars moves both.

Set the list to five symbols you can chart individually. Note the strongest name in the title, then chart that symbol and check that its rank line reads 1 on the same bar. Repeat for the weakest.

  • The title is empty. The ranked-symbol lookup only has anything to return after a ranking call with a non-zero topranks, and the two prefixes must match that call.
  • The chart is slow. This re-ranks the entire list on every redraw. The documentation measures the ranking call at roughly 20 ms per 15,000 bars and 7 symbols; scale that up and the cost is obvious. Keep chart-based ranking to a short list.
  • The rank line is flat at 1. The watch list number points at a list with one symbol in it.

Add a second pane driven by the same prefixes but a different score — turnover, say — and compare who is at the top of each. Two rankings that agree are telling you the two scores are measuring the same thing.

Failure modes when the first pass is incomplete

Section titled “Failure modes when the first pass is incomplete”

The ranking is only ever as good as the family of scores it was handed, and a first pass can be incomplete in several ways that produce no error at all.

  • Stale variables from an earlier run. The wildcard was missing, or the prefix changed. Symbols that are no longer in your universe keep their old scores and keep taking up positions in the ranking.
  • Symbols that were skipped. SetForeign() returns 0 for a ticker that is not in the database and leaves the price arrays untouched. If the return value is not checked, the loop stores the previous symbol’s score under the missing symbol’s name.
  • Null scores from insufficient history. A 252-bar rate of change is undefined until there are 252 prior bars. How the ranking function treats Null input is not documented, so do not depend on it: either exclude the symbol by not writing a variable for it, or make the emptiness visible with a self-check column, as the formula above does.
  • A short bar range. A static array stores only the bars currently in use, so a narrow Analysis range produces a short ranking. The ranks will still be correct for the bars covered, and simply absent before them.
  • The first symbol’s calendar. Everything computed inside one execution is aligned to that execution’s own symbol. Pass one runs on whichever symbol is first, so a first symbol with a short history or data holes limits the ranking for every symbol. Where this matters, use the Analysis setting “Pad and align all data to reference symbol”, which the manual notes is off by default, and pick a reference symbol with complete history.
  • The universe and the run disagreeing. If the loop reads a hard-coded list while the Analysis runs on a different watch list, half your rows will have no rank and you will spend an afternoon looking at the ranking function. Read the universe from GetOption(), as both formulas here do.

The Ranking chapter is unusually blunt on both points, and it is worth repeating in full because it saves people from two different mistakes.

On cost: the function is computationally and memory intensive, at roughly 20 ms per 15,000 bars and 7 symbols. Call it once per run, guarded by Status( "stocknum" ) == 0, or better still pre-compute the ranks in a separate Scan and use them later. Calling it per symbol collapses performance, because as well as being slow it must lock the shared static variable memory, so every other thread that touches a static variable waits for it.

On scope: the documentation states that this function is not intended to replace the backtester’s built-in ranking through PositionScore, and that whenever you can, you should use PositionScore because it is far faster and less memory-consuming for backtests with ranking. StaticVarGenerateRanks() is intended for explorations and indicators, and for cases PositionScore alone cannot express. Part 28 covers PositionScore where it belongs.

StaticVarGenerateRanks() reads a family of static variables matched by prefix, sorts their values across symbols bar by bar, and writes a new family whose names concatenate the output prefix, the input prefix and the symbol. Ranks start at one, ties are numbered by the documented modes 1234 or 1224, and direction is controlled by negating the score rather than by an argument. Top and bottom mode store indexes into a companion symbol list, which StaticVarGetRankedSymbols() will resolve for one bar.

None of that is hard. What is hard is knowing whether the numbers on screen are right, and the answer is the six-symbol hand check: sort a spreadsheet, compare, and only then build on top of it. The project applies all of this to a real relative strength tool.

Check your understanding

Question 1. You call StaticVarGenerateRanks("Rank", "P13Score", 0, 1224). Where does MSFT’s rank end up?
Show the answer and why

Answer: RankP13ScoreMSFT

In normal mode the output name is outputprefix + inputprefix + symbol. Both prefixes appear, in that order, before the ticker.

Question 2. Which statements about top-N mode are correct? Select all that apply.
Show the answer and why

Answer: A positive topranks selects top-ranked values and a negative one selects bottom-ranked values, The variables named outputprefix + inputprefix + N hold indexes, not tickers, A companion variable ending in "Symbols" maps those indexes to names

Per-symbol rank arrays come from normal mode, with topranks set to zero. Top and bottom mode build index tables plus the companion symbol list that StaticVarGetRankedSymbols reads.

Question 3. Your ranking of a 40-symbol watch list reports a highest rank issued of 63. What is the most likely cause?
Show the answer and why

Answer: Static variables from a previous run are still in memory and joined the ranking

The function ranks every static variable matching the input prefix. More positions than symbols means more matching variables than symbols - almost always a removal call that lost its asterisk or a prefix that changed.

Question 4. You need the twenty LOWEST-volatility symbols ranked first. How do you do it?
Show the answer and why

Answer: Negate the score before storing it

There is no direction argument. Negating the stored score reverses the order for every mode at once; SetSortColumns would only change the display, not the rank values your formula reads.

Question 5. Why does the documentation tell you to prefer PositionScore for backtesting with ranking?
Show the answer and why

Answer: PositionScore is described as far faster and less memory-consuming for that job

Both work in a backtest. The Ranking chapter states plainly that PositionScore is far faster and less memory-consuming, and that StaticVarGenerateRanks is intended for explorations and indicators and for cases PositionScore cannot express.

Sources for this lesson

5 verified · checked 2026-08-31

  1. 01AFL Function Reference - StaticVarGenerateRanksamibroker.com/guide/afl/staticvargenerateranks.html2026-08-31
  2. 02AFL Function Reference - StaticVarGetRankedSymbolsamibroker.com/guide/afl/staticvargetrankedsymbols.html2026-08-31
  3. 03AmiBroker User's Guide - Ranking functionalityamibroker.com/guide/h_ranking.html2026-08-31
  4. 04AFL Function Reference - StaticVarSetamibroker.com/guide/afl/staticvarset.html2026-08-31
  5. 05AFL Function Reference - StaticVarRemoveamibroker.com/guide/afl/staticvarremove.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.