Static Variables for Cross-Sectional Work
There is a moment in every AmiBroker user’s development where they try to write something
like OtherScore = Score of "XYZ"; and discover that AFL has no way to express it. This
lesson explains why that gap is deliberate, and what AmiBroker provides in its place.
By the end you should be able to write a formula whose first pass computes something for every symbol in a universe, whose second pass reads those values back per symbol, and which leaves no debris behind for the next run to trip over.
Why one execution cannot see another’s results
Section titled “Why one execution cannot see another’s results”An AFL formula runs against one symbol. In the Analysis window, AmiBroker runs it once per symbol, and since version 5.50 it runs many of those executions in parallel: the documented model is one thread per operation per symbol, capped at 2 threads per Analysis window on the Standard edition and 32 on Professional, and never more than the number of logical processors Windows reports.
What one Analysis run actually is
- Analysis windowOne operation: Scan, Exploration, Backtest
- Thread pool2 threads (Standard) or up to 32 (Professional)edition
- One AFL execution per symbolIts own arrays, its own variables
- Result rowsCollected and displayed afterwards
Each of those executions has its own copy of every variable. They begin at unpredictable times, they finish at unpredictable times, and when one finishes its variables are gone. An ordinary AFL variable therefore cannot carry anything from one symbol’s run to another’s; there is nothing wrong with your code when this fails, and no argument you can pass that changes it.
Foreign() and SetForeign() do not close the gap either. They read another symbol’s
stored data — its open, high, low, close, volume — not another execution’s computed
results. You can of course recompute a peer’s score inside your own execution, and for a
handful of peers that is fine. For a universe it is not: scoring 500 symbols by having
each of the 500 executions rebuild all 500 scores is 250,000 foreign reads to produce 500
numbers, and the multi-threading chapter notes that every cross-symbol access takes a
global lock, so those reads do not even parallelise well.
What is needed is a place outside any single execution where a value can be left for another execution to pick up. That is exactly what a static variable is.
The store AmiBroker provides
Section titled “The store AmiBroker provides”A static variable is a named slot in a key-value store that belongs to the running AmiBroker process. The official pages describe it as having static duration — allocated when the program begins and deallocated when the program ends — and say plainly that static variables allow values to be shared between various formulas.
Fragment — not a complete formula
StaticVarSet( "MyScore" + Name(), ROC( Close, 126 ) );Back = StaticVarGet( "MyScore" + Name() );The documented signatures are:
StaticVarSet( "varname", value, persistent = False, compressionMode = cmDefault )— returns 1 on success and 0 on failure. Scalars and strings have been supported since 4.60, arrays since 5.30, matrices since 6.10.StaticVarGet( "varname", align = True )— returns the stored value, orNullif the name does not exist. (The official page’s own syntax line has misplaced quote characters; the call is as written here.)
Four properties matter more than the rest.
They outlive your formula. A value written by a chart pane is readable by an Exploration, a Scan, a backtest or another chart, in the same AmiBroker session. Nothing clears them when your formula ends.
A missing name reads back as Null, not zero. Wrap reads in Nz() whenever you
intend to accumulate, or a single missing symbol will blank an entire running total.
StaticVarGetText() behaves differently and returns an empty string for a missing name,
so the existence test for text is a comparison with "", not IsNull().
Arrays cost memory that is never reclaimed on its own. The StaticVarSet page states
that a static array consumes 8 bytes per bar and that the memory is not released until
AmiBroker closes or you call StaticVarRemove(). Five hundred symbols of five thousand
daily bars is roughly 20 MB, per score, per run.
Read once, write once. The documentation is explicit that static arrays are a little slower than ordinary AFL variables and recommends pulling a value into a normal variable, doing all the arithmetic there, and writing it back at the end. Reading a static variable inside a per-bar loop is the classic way to make a fast formula slow.
There is a second alignment question worth knowing before it bites. StaticVarGet()’s
align argument defaults to True, which synchronises the stored values to the reading
symbol’s own timestamps. The documentation advises against align = False unless you
know exactly what you are doing, notes that it is incompatible with compression, and adds
that the speed saving is negligible anyway. Leave it alone. Also read values back in the
same interval you wrote them in: the pages state that cross-interval reads perform
padding and compression automatically but by a different route from Foreign(), so a
daily-written variable read on an intraday chart shows essentially flat lines for each
day.
The persistent argument
Section titled “The persistent argument”The third argument of StaticVarSet() is persistent, defaulting to False. When it is
True, the variable is written to PersistVars.bin when AmiBroker closes and reloaded
automatically on the next startup, preserving values between application runs. The feature
arrived in 5.80. The prose on the same page calls the argument persist while the syntax
line calls it persistent; it is one argument.
Persistent variables can additionally be auto-saved on a timer with
SetOption( "StaticVarAutoSave", interval ), where the interval is in seconds; 0
disables it, and from 6.90 an interval of -1 performs a single one-shot save. The
documentation warns that writing many static variables to disk takes time and blocks
all static variable access while it happens — it calls saving every second a bad idea
and 60 seconds fine.
The two-pass pattern
Section titled “The two-pass pattern”The pattern that makes cross-sectional work possible has three steps in a fixed order: write every symbol’s score, do the cross-symbol operation once, then read back per symbol. In the Analysis window the first two steps must happen exactly once, before any other symbol’s execution starts, and AmiBroker provides a documented way to arrange that.
Fragment — not a complete formula
if( Status( "stocknum" ) == 0 ){ // runs once, on the first symbol, on one thread}The multi-threading chapter states that AmiBroker detects this statement, runs the very
first symbol in one thread only, waits for completion, and only then launches all the
other threads. That is what makes writes inside the block safe without any locking of
your own. The chapter also states a caveat with no exceptions: this statement must not
be placed inside an #include file.
Build the smallest formula that demonstrates the pattern: score every symbol in the Analysis window’s own universe, compute one number that depends on all of them, and let every symbol read that number back.
Complete formula
Section titled “Complete formula”Complete runnable AFL
// two-pass-momentum-scores.afl// Part 13 - Static Variables for Cross-Sectional Work//// The smallest honest example of the two-pass workflow. Pass 1 runs once, on// the very first symbol only, and writes one static array per symbol plus two// universe-wide arrays. Pass 2 runs for every symbol and reads them back.//// Nothing is ranked here on purpose. The single point being demonstrated is// that a formula executing on one symbol can see a number that was computed// from every symbol - which plain AFL cannot do.//// 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, one trading calendar across the universe. Where calendars// differ, StaticVarGet's default align = True re-aligns stored values to// the reading symbol's timestamps, which is what we want here.// - Static variable names live in one namespace shared by every formula in// the running AmiBroker. The prefix below is long and specific on purpose.// - Everything this formula writes, it removes first. Nothing is persistent:// a score written yesterday is a wrong answer today.
InputPrefix = "P13Score"; // one static array per symbolMeanName = "P13UniverseMean"; // one array shared by the whole universeCountName = "P13UniverseCount";
ScorePeriod = Param( "Momentum lookback (bars)", 126, 20, 500, 1 );
// Read the Analysis window's own universe setting, so the list we compute over// and the list we report on can never drift apart. GetOption is the native,// thread-safe way to do this.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// AmiBroker detects Status("stocknum") == 0 and runs the very first symbol on// one thread alone, waiting for it to finish before launching the others. That// is what makes these writes safe without any locking of our own.if( Status( "stocknum" ) == 0 ){ // DO NOT forget the asterisk. Without it nothing is removed, and last // run's scores quietly join this run's numbers. StaticVarRemove( InputPrefix + "*" ); StaticVarRemove( MeanName ); StaticVarRemove( CountName );
ScoreTotal = 0; ScoreCount = 0;
for( i = 0; ( Sym = StrExtract( SymbolList, i ) ) != ""; i++ ) { // A symbol that is not in the database leaves the price arrays alone // and returns 0, so skipping it here avoids scoring the wrong data. if( SetForeign( Sym ) ) { SymbolScore = ROC( Close, ScorePeriod ); RestorePriceArrays();
StaticVarSet( InputPrefix + Sym, SymbolScore );
// Nz converts the warm-up Nulls to zero so that one short-history // symbol cannot blank the running total for every bar. HasScore = NOT IsNull( SymbolScore ); ScoreTotal = ScoreTotal + Nz( SymbolScore ); ScoreCount = ScoreCount + HasScore; } }
// Max( ScoreCount, 1 ) keeps the division defined on bars where no symbol // had enough history yet. StaticVarSet( MeanName, ScoreTotal / Max( ScoreCount, 1 ) ); StaticVarSet( CountName, ScoreCount );}
// ------------------------------------------------------------------- PASS 2// Read once into ordinary AFL variables, as the documentation recommends, and// do all the arithmetic on those.OwnScore = StaticVarGet( InputPrefix + Name() );UniverseMean = StaticVarGet( MeanName );UniverseCount = StaticVarGet( CountName );
// A missing static variable reads back as Null, not as zero. Anything with no// score was either skipped in pass 1 or is not in the universe at all.Filter = NOT IsNull( OwnScore );
AddColumn( OwnScore, "Score %", 1.2 );AddColumn( UniverseMean, "Universe mean %", 1.2 );AddColumn( OwnScore - UniverseMean, "Score - mean", 1.2 );AddColumn( UniverseCount, "Symbols scored", 1.0 );
SetSortColumns( -5 );How it works
Section titled “How it works”The formula reads the universe from the Analysis window itself rather than from a
hard-coded list. GetOption( "ApplyTo" ) returns 0 for all symbols, 1 for the current
symbol and 2 for the filter setting; when it is 2, GetOption( "FilterIncludeWatchlist" )
gives the watch list number, and CategoryGetSymbols() turns that into a comma-separated
string. Reading the settings natively rather than through OLE is what the multi-threading
chapter insists on, and it also means the list you compute over cannot drift away from
the list you report on.
Pass one sits inside the Status( "stocknum" ) == 0 guard. It removes everything it is
about to write, loops the symbol list, and for each symbol calls SetForeign(), computes
the score, restores the price arrays and stores the score under a name built from the
prefix and the ticker. It also accumulates a running total and a running count so that,
after the loop, it can store the universe mean — a number no single symbol could have
computed.
Pass two runs for every symbol, including the first. It reads its own score and the two
universe-wide arrays into ordinary variables, and reports them. The Filter line uses the
fact that a missing name reads back as Null to exclude anything that was never scored.
Key functions
Section titled “Key functions”Status( "stocknum" )— the ordinal number of the symbol currently being analysed;== 0is the documented “do this once per Analysis run” guard.GetOption( "ApplyTo" )andGetOption( "FilterIncludeWatchlist" )— the native, thread-friendly way to read the Analysis window’s own settings.CategoryGetSymbols( category, index, mode = 0 )— returns a comma-separated symbol list;categoryAll(5.50 and later) means every symbol in the database.StrExtract( list, item, separator = ',' )— pulls item n out of that list, and returns an empty string when it runs off the end, which is what terminates the loop.Nz( x, valueifnull = 0 )— convertsNull,NaNand infinities to zero.
Expected result
Section titled “Expected result”One row per symbol, with the symbol’s own score, the universe mean of all the scores, the difference between them, and how many symbols contributed. The mean column holds the same value on every row — that is the proof that the cross-symbol step worked. The count column tells you how many symbols actually had enough history on that bar.
Test it
Section titled “Test it”Run the Exploration on a watch list of five or six symbols with the range set to one recent bar. Copy the individual score column into a spreadsheet, average it by hand, and compare with the mean column. They should agree to the displayed precision. Then delete one symbol from the watch list and re-run: the count falls by one and the mean moves. If the mean does not move, pass one did not re-run, and the next section explains why.
Common errors
Section titled “Common errors”- Every row shows the same score as well as the same mean.
RestorePriceArrays()was missed, so everything after the firstSetForeign()computed on foreign data. - The mean is unchanged after you edit the watch list. Something removed the guard, or the removal call lost its wildcard, so yesterday’s variables are still in memory and still being read.
- An error about Apply To. The formula deliberately refuses to run with Apply To set to “current symbol”, because then the list and the run would describe different things.
- Scores are
Nullfor symbols you know have data. Check the lookback against the Analysis range: a 126-bar rate of change needs 126 prior bars inside the range.
Extension
Section titled “Extension”Add a second universe-wide statistic — the standard deviation of the scores across symbols — and report each symbol’s score as a z-score. You will need a second pass over the stored scores after the mean is known, which is a useful rehearsal for the ranking loop in the next lesson.
Naming schemes and collisions
Section titled “Naming schemes and collisions”Static variable names live in one flat namespace shared by every formula, chart pane and
Analysis window in the running AmiBroker. There is no per-formula scope, no per-database
scope documented in the User’s Guide, and no warning when two formulas pick the same name.
Two unrelated formulas that both use "values" will overwrite each other’s data, and the
symptom is not an error — it is a plausible-looking wrong answer.
Three rules keep this manageable.
Use a long, specific prefix. "P13Score" is defensible; "score" is not. This matters
doubly for ranking, because the ranking function matches input variables by “starts with”
and treats whatever follows as a ticker.
Never let one prefix be the prefix of another. If you have "val" and "values",
anything scanning for "val" will swallow the "values" family too and read nonsense
ticker names out of the remainder.
Prefer names AmiBroker builds for you where it can. The static keyword, enabled by
#pragma enable_static_decl (prefix), lets you declare identifiers that are backed by
static variables and automatically prefixed to avoid clashes. The prefix may contain the
runtime tokens {chartid}, {interval} and {symbol}, which are substituted at run
time and give you per-chart, per-interval or per-symbol namespaces for free. The
documentation recommends prefixing declared statics with an underscore so they stand out,
and notes an important limit: a declared static is read once at the declaration and
written once when the formula finishes, so it is not a live channel between threads
during a run. For the two-pass pattern, the function-based API is the right tool.
Cleaning up between runs
Section titled “Cleaning up between runs”Nothing removes a static variable for you. StaticVarRemove( "variablename" ) is the only
documented way to free the memory short of closing AmiBroker, and since 5.30 it accepts
wildcards: * matches any number of characters including zero, and ? matches exactly
one.
Fragment — not a complete formula
// delete static variables - DO NOT forget the asterisk (wildcard) at the endStaticVarRemove( "P13Score*" );That comment is AmiBroker’s own, from its ranking example, and it is shouted for a reason.
StaticVarRemove( "P13Score" ) removes exactly one variable named P13Score, which
probably does not exist, and leaves every P13ScoreAAPL, P13ScoreMSFT and the rest of
last run’s data sitting in memory to be silently included in this run’s answer.
Put the removal inside the Status( "stocknum" ) == 0 block so it happens once, before
the writes, rather than once per symbol — which would delete the work of the symbols that
have already run.
A diagnostic worth keeping
Section titled “A diagnostic worth keeping”Have a chart pane that answers, at a glance, what is still resident in the static namespace and how much memory it is using.
Complete formula
Section titled “Complete formula”Complete runnable AFL
// static-var-inspector.afl// Part 13 - Static Variables for Cross-Sectional Work//// A diagnostic to keep in a chart pane while you develop cross-sectional// formulas. It answers the two questions that explain most "impossible"// ranking results: what is still resident in memory, and how much of it.//// Assumptions declared up front:// - StaticVarInfo( pattern, "count" ) needs AmiBroker 6.90 or newer. The// other three fields work from 5.60. The running version is printed so a// missing count is not mistaken for a count of zero.// - This formula only READS. It never removes anything, because a wildcard// removal typed into the wrong pane destroys state belonging to other// formulas, chart panes and Analysis windows in the same AmiBroker.
_SECTION_BEGIN( "Static variable inspector" );
NamePattern = ParamStr( "Name pattern (wildcards allowed)", "P13*" );
TotalCount = StaticVarCount();MatchingList = StaticVarInfo( NamePattern, "list" );MatchedBytes = StaticVarInfo( NamePattern, "totalmemory" );AllBytes = StaticVarInfo( "*", "totalmemory" );
Plot( Close, "Close", colorDefault, styleCandle );
Title = "Static variables resident in this AmiBroker process" + "\n" + "AmiBroker version: " + NumToStr( Version(), 1.2 ) + "\n" + "Total static variables: " + NumToStr( TotalCount, 1.0 ) + "\n" + "Memory, all variables: " + NumToStr( AllBytes / 1024, 1.0 ) + " kB" + "\n" + "Memory, names matching the pattern: " + NumToStr( MatchedBytes / 1024, 1.0 ) + " kB" + "\n" + "Names matching the pattern: " + MatchingList;
_SECTION_END();How it works
Section titled “How it works”It reads and reports, and does nothing else. StaticVarCount() gives the global total
with no filtering argument. StaticVarInfo() takes a name or a wildcard pattern plus a
field: "list" returns the matching names, "memory" the bytes excluding the names
themselves, "totalmemory" the bytes including them, and "count" the number of matches.
The version is printed alongside because "count" requires AmiBroker 6.90 or newer, and a
blank is easy to misread as a zero.
Key functions
Section titled “Key functions”StaticVarCount()— total number of static variables in memory.StaticVarInfo( "varname", "field" )— introspection over the namespace; only the four documented field strings exist.Version( minrequired = 0 )— returns the AmiBroker version as a number.
Expected result
Section titled “Expected result”A title block listing the total count, the memory used by everything, the memory used by the names matching your pattern, and those names. Run the two-pass formula, then look here: you should see one variable per symbol plus the two universe-wide ones.
Test it
Section titled “Test it”Note the count. Run the two-pass Exploration on a watch list of ten symbols. The count should rise by twelve. Change the pattern to your prefix and confirm the listed names are exactly the ones you expect, with no leftovers from an earlier prefix.
Common errors
Section titled “Common errors”- The count never falls. Nothing removes static variables automatically. If a formula you have stopped using left ten thousand arrays behind, they are still there.
- A blank version or count.
"count"needs 6.90 or newer. - The list is enormous. That is the finding, not a bug. It usually means a formula is writing per-symbol variables without a matching removal.
Extension
Section titled “Extension”Add a parameter for a second pattern and report both counts side by side, so you can watch an input family and its generated output family at the same time — which is exactly what the next lesson produces.
Multi-threading: what you must do, and what you need not
Section titled “Multi-threading: what you must do, and what you need not”The multi-threading chapter is precise about this, and it is less onerous than people expect.
A single StaticVarSet or StaticVarGet call is atomic. It reads or writes an entire
array in one indivisible operation; no other thread will see a half-updated array.
Groups of calls are not atomic. If the correctness of your logic depends on several static variables being updated together, nothing protects that region for you.
The rule that removes the problem is one writer, many readers. Do all writing inside
Status( "stocknum" ) == 0, and let every other thread only read. No synchronisation is
then needed at all — the guide says so in those terms.
Two functions exist for the cases that rule does not cover.
StaticVarAdd( "name", value, keepAll = True, persistent = False ) performs an atomic
read-add-write and is the documented multi-threading-safe replacement for
AddToComposite(); note that it converts Null values to zero, and that unlike
AddToComposite() it does not skip group 253, so composite symbols must be excluded by
hand. StaticVarCompareExchange( "varname", exchange, comperand ) is an atomic
compare-and-swap on a scalar numeric static variable, and it is the primitive the
documentation uses to build a semaphore.
AFL executions are isolated by design, which is what makes scanning fast and
cross-sectional work impossible without help. Static variables are the help: a
process-wide store that outlives every execution, costs 8 bytes per bar per array, and is
cleaned up only when you clean it up. The workflow is always the same shape — write once
under Status( "stocknum" ) == 0, do the cross-symbol operation once, read back per
symbol — and the three things that go wrong are a missing wildcard, a prefix that is too
short, and persistence left switched on. The next lesson replaces the hand-rolled
universe statistic with AmiBroker’s own ranking function.
Check your understanding
Sources for this lesson
10 verified · checked 2026-08-31
- 01AFL Function Reference - StaticVarSetamibroker.com/guide/afl/staticvarset.html2026-08-31
- 02AFL Function Reference - StaticVarGetamibroker.com/guide/afl/staticvarget.html2026-08-31
- 03AFL Function Reference - StaticVarRemoveamibroker.com/guide/afl/staticvarremove.html2026-08-31
- 04AFL Function Reference - StaticVarInfoamibroker.com/guide/afl/staticvarinfo.html2026-08-31
- 05AFL Function Reference - StaticVarCountamibroker.com/guide/afl/staticvarcount.html2026-08-31
- 06AFL Function Reference - StaticVarAddamibroker.com/guide/afl/staticvaradd.html2026-08-31
- 07AFL Function Reference - StaticVarCompareExchangeamibroker.com/guide/afl/staticvarcompareexchange.html2026-08-31
- 08AFL Function Reference - SetOption§ StaticVarAutoSaveamibroker.com/guide/afl/setoption.html2026-08-31
- 09AmiBroker keyword reference - staticamibroker.com/guide/keyword/static.html2026-08-31
- 10AmiBroker User's Guide - Efficient use of multithreadingamibroker.com/guide/h_multithreading.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.