Skip to content
Level 3 · AFL DeveloperLessonPart 36 · page 2 of 630 min
30Minutes
13AFL functions
10Sources
StandardRequires
AFL functions taught here13

Static Variables in Depth

Part 13 used static variables as plumbing: write a score per symbol, generate ranks, read them back. This lesson is about what you were actually handling. A static variable is a slot in a single, flat, application-wide store that outlives every formula, that no window owns, that nothing clears for you, and that several threads may be reading while another writes. Each of those properties is a way to get a wrong answer quietly, and each has a documented pattern that avoids it.

Lifetime: the program run, not the formula

Section titled “Lifetime: the program run, not the formula”

The sentence repeated on all four core pages is precise, and every word in it is load bearing:

Static variable — the variable has static duration (it is allocated when the program begins and deallocated when the program ends) and initializes it to Null unless another value is specified. Static variables allow to share values between various formulas.

“The program” is the running AmiBroker process. Not the formula. Not the chart pane. Not the Analysis run. A static variable written by an exploration at nine in the morning is still there, unchanged, when a chart formula reads it at four in the afternoon, unless something explicitly removed it or AmiBroker was closed in between.

Who can see a static variable

  1. Chart pane AIndicator formulareads and writes
  2. Chart pane B, on another symbolDifferent formula entirelysame names
  3. Analysis window — ExplorationOne thread per symbol
  4. Analysis window — BacktestPhase I threads, then Phase II
  5. The static variable storeOne flat name → value map for the whole processno scoping of any kind

There is no scoping. Two unrelated formulas that both use the name "values" are using the same slot, and the second one to run wins. This is the single most common way static state goes wrong, and it is not a bug — it is what “share values between various formulas” means.

By default nothing survives a restart. StaticVarSet( "name", value, persistent = False, compressionMode = cmDefault ) takes persistence as its third positional argument; StaticVarSetText( "name", "value", persist = False ) takes it as its third argument too, and has no fourth. Setting it to True — available since 5.80 — means the variable is written to a file called PersistVars.bin when AmiBroker closes, and reloaded on the next startup.

Fragment — not a complete formula

// Survives a restart. Note the third argument.
StaticVarSet( "RS_universe_asof", DateNum(), True );
// StaticVarSetText has no compressionMode argument - do not copy the 4-argument form.
StaticVarSetText( "RS_universe_name", "ASX200", True );

SetOption( "StaticVarAutoSave", interval ) adds periodic saving on top of the save on exit, which the documentation says “is always done”. The interval is in seconds; zero disables it; and from version 6.90 an interval of -1 performs a single one-shot save.

The compressionMode argument (6.10+) has three documented values: cmDefault compresses persistent variables only, cmNever never compresses, cmAlways compresses persistent and non-persistent alike. Compression works by removing repeated values from the sequence and restoring them on read — which is why it is incompatible with align = False on StaticVarGet, where the repeated values are simply not retrieved.

Arrays became storable in 5.30 and are where the memory goes. The StaticVarSet page is blunt about the cost: a static array variable “will consume 8 * (number_of_bars) bytes of memory and it won’t be released until program is closed or variable is removed using StaticVarRemove()”. Five hundred symbols at twenty thousand bars is roughly eighty megabytes that nothing will ever reclaim on its own.

Three behaviours around arrays cause more confusion than the memory does.

Only the bars currently in use are stored. StaticVarSet stores “only as many bars as there are currently in use by given chart”. Under QuickAFL that is not the whole history, so a static array written from a zoomed chart is shorter than you expect. This is the one place where AddToComposite behaves differently — it forces all bars — and the difference is documented explicitly.

Reads are aligned by timestamp unless you say otherwise. StaticVarGet( "varname", align = True ) synchronises stored values against the current symbol’s date and time stamps. With align = False there are no checks at all: values are filled in consecutively regardless of their timestamps, and if the stored array is shorter than the current one, “the last value of static array will be propagated till BarCount - 1”. The documentation advises against align = False unless you know exactly what you are doing, and adds that speed is not a reason to reach for it, because alignment “has similar complexity as plain memory copy”.

Intervals must match. Read a static back in the same interval you wrote it in. Cross-interval reads do padding and compression automatically, but — quoting the page — “Foreign compresses data always from base-time interval, while static variables operate on previously stored interval, hence result may differ”. Reading a daily-written static on an intraday chart gives “essentially flat lines for each day”. Tick, n-volume and n-tick intervals do not work reliably at all, because timestamps can repeat.

AmiBroker’s threading model is stated as one rule: one operation on one symbol is one thread. Three chart panes on one symbol are three threads. A scan over four hundred symbols is up to four hundred pieces of work spread over the available threads, capped at 2 per Analysis window in the Standard edition and 32 in Professional, and in both cases capped by the number of logical processors Windows reports.

What the multi-threading chapter says about static variables specifically is narrow and worth quoting rather than paraphrasing: access is “fast, thread-safe, and atomic at the single StaticVarSet/StaticVarGet call level. This means that it reads/writes an entire array in an atomic way, so no other thread will read/write that array in the middle of another thread updating it.”

Read that as: one call is indivisible. Two calls are not. A read-modify-write done as StaticVarGet, then arithmetic, then StaticVarSet is three operations, and another thread can slip between them. That is the classic lost-update race, and it is the reason the following pattern is wrong in an Analysis run:

Fragment — not a complete formula

// WRONG in a multi-symbol Analysis run: three operations, not one.
Total = Nz( StaticVarGet( "TotalVolume" ) );
Total = Total + Volume;
StaticVarSet( "TotalVolume", Total );

There are three documented ways out, in increasing order of cost.

The documented rule of thumb: as long as only one thread writes and many threads only read, no synchronisation is needed. In practice that means doing all the writing in a block that AmiBroker runs on its own:

Fragment — not a complete formula

if( Status( "stocknum" ) == 0 )
{
StaticVarRemove( "score*" ); // note the asterisk
// ... build every score here, single threaded ...
}
// Every symbol, in parallel, reads only.
MyScore = StaticVarGet( "score" + Name() );

AmiBroker detects that Status("stocknum") == 0 statement, runs the very first symbol in one thread only, waits for it to complete, and only then launches the other threads.

StaticVarAdd( "name", value, keepAll = True, persistent = False ), added in 6.10, does the read-add-write as one interlocked operation. It is the documented multi-threading-safe replacement for AddToComposite. Two details catch people:

  • It converts all Nulls to zeros, as AddToComposite does.
  • AddToComposite skips group 253 by default so composites are not added to themselves. StaticVarAdd does not, so guard it yourself: if( GroupID() != 253 ) StaticVarAdd( ... );
  • It does not raise the “required bars” estimate the way AddToComposite does, so under QuickAFL you may be accumulating a truncated array. The 7.00 release notes recommend adding SetBarsRequired( sbrAll, sbrAll ) when you need every bar.

Its documented speed advantage over AddToComposite is roughly twofold single-threaded and around fourfold with eight threads — and the docs are candid that lock contention means “it does not scale as much as naive person may think”.

StaticVarCompareExchange( "varname", exchange, comperand ) is an atomic compare-and-swap on a scalar numeric static variable. It compares the variable with comperand, stores exchange if they are equal, and returns the value the variable had before the call — not a success flag. Zero comes back if the variable did not exist. The idiom is:

Fragment — not a complete formula

// Acquire: we own the lock only if the previous value was 0.
if( StaticVarCompareExchange( "MySemaphore", 1, 0 ) == 0 )
{
// ... critical section ...
StaticVarSet( "MySemaphore", 0 ); // release - you must do this yourself
}

One flat namespace means collisions are your problem. Two conventions are worth adopting.

For the function-based API there is no automatic namespacing at all, so use an explicit module prefix — "MOM_score" + Sym, "RS_rank" + Sym — and always remove with that same prefix. Choose prefixes that are long and distinctive, because AmiBroker’s matching is “starts with”: a prefix of "val" will also swallow every "values*" variable belonging to something else and treat the wrong remainder as a symbol name.

For declared statics there is a documented mechanism. #pragma enable_static_decl "prefix" enables the static keyword, and the prefix is prepended automatically to the real static variable name “to avoid name clashes/conflicts”:

Fragment — not a complete formula

#pragma enable_static_decl "MOMPANE{chartid}"
static _lastAlertBar; // real name is MOMPANE<chartid>_lastAlertBar
if( IsNull( _lastAlertBar ) ) _lastAlertBar = 0;

The tokens {chartid}, {interval} and {symbol} are substituted at run time, which gives you per-chart, per-interval or per-symbol namespaces for free. The documented convention is to prefix declared statics with _ or s_ so they stand out in the code.

Two properties of declared statics differ from the function calls, and both matter. A declared static is read once at the declaration and saved once when execution completes, so it costs no more than a normal variable — but it is therefore not a live channel between threads during a run. And the underlying variable is still an ordinary static reachable under its full prefixed name from anywhere, so the prefix is namespacing, not privacy.

Nothing is removed automatically. Not at the end of a formula, not at the end of an Analysis run. StaticVarRemove( "variablename" ) is the only documented way to free static array memory short of closing AmiBroker, and since 5.30 it accepts wildcards: * matches any number of characters including zero, ? matches exactly one.

AmiBroker’s own ranking examples carry this comment, shouting included:

Fragment — not a complete formula

// delete static variables - DO NOT forget the asterisk (wildcard) at the end
StaticVarRemove( "ValuesToSort*" );

Forget the asterisk and last run’s scores stay resident, and the ranking silently uses a mixture of old and new. Put the removal inside the Status("stocknum") == 0 block so it runs once, before the writes, rather than once per symbol.

When a formula “mysteriously” sees stale data, look at the store before you look at the formula.

A read-only pane that answers three questions: how many static variables exist, how much memory they occupy, and how that splits across the prefixes you care about.

Complete runnable AFL

static-var-inspector.afl
// static-var-inspector.afl
// Part 36 - Static Variables in Depth
//
// Reports what is currently resident in AmiBroker's application-wide static
// variable namespace: how many variables, how much memory, and how that
// breaks down across the name prefixes you care about.
//
// WHY THIS EXISTS
// Static variables outlive every formula run. Nothing clears them for you.
// When a formula "mysteriously" sees stale data, the first question is what
// is actually in the namespace - and this pane answers it.
//
// ASSUMPTIONS
// - AmiBroker 7.00.1. StaticVarInfo(..., "count") needs 6.90 or later;
// StaticVarInfo itself needs 5.60; StaticVarCount needs 5.30.
// - Read-only. This formula never writes and never removes a static
// variable, so it cannot damage state that another formula depends on.
// - Memory figures are bytes as reported by AmiBroker, not an estimate.
_SECTION_BEGIN("Static Variable Inspector");
// Prefixes you want broken out, separated by commas. Everything not matched
// by one of these still appears in the totals.
PrefixList = "rank,score,~,MOM_,RS_";
TotalCount = StaticVarCount();
TotalMemory = StaticVarInfo( "*", "totalmemory" );
Report = "Static variable namespace\n"
+ "Variables resident: " + NumToStr( TotalCount, 1.0 ) + "\n"
+ "Total memory: " + NumToStr( TotalMemory / 1024, 1.1 ) + " KB\n"
+ "-----------------------------\n";
for( i = 0; ( Prefix = StrExtract( PrefixList, i ) ) != ""; i++ )
{
// The wildcard matters. "*" matches any number of characters INCLUDING
// zero, so "score*" also matches a variable named exactly "score".
Pattern = Prefix + "*";
Count = StaticVarInfo( Pattern, "count" );
Memory = StaticVarInfo( Pattern, "totalmemory" );
Report = Report + Pattern + " "
+ NumToStr( Nz( Count ), 1.0 ) + " var(s), "
+ NumToStr( Nz( Memory ) / 1024, 1.1 ) + " KB\n";
}
// The full list goes to the Interpretation window rather than the chart title,
// because on a busy installation it is far too long for a title bar.
// Pass the list as an ARGUMENT, never as the format string itself: a stray
// "%" inside the data would be read as a formatting specifier and raise
// Error 61. The %s specifier needs AmiBroker 6.20 or later.
printf( "Static variables currently resident:\n%s\n",
StaticVarInfo( "*", "list" ) );
Plot( Close, "Close", colorDefault, styleCandle );
Title = Report;
_SECTION_END();

Download static-var-inspector.afl60 lines

StaticVarCount() gives the global total — it has no filtering argument, which is why the per-prefix breakdown uses StaticVarInfo instead. StaticVarInfo( pattern, field ) accepts the same wildcards as StaticVarRemove, and its four documented fields are "list", "memory", "totalmemory" and "count". The loop asks for a count and a memory figure per prefix; the full name list goes to the Interpretation window via printf, because on a busy installation it is far too long for a chart title.

StaticVarInfo( "*", "totalmemory" ) reports bytes including the memory used by the variable names themselves; "memory" excludes them. With thousands of long per-symbol names the difference is not negligible.

StaticVarInfo( pattern, "count" ) requires AmiBroker 6.90 or later. StaticVarInfo itself requires 5.60 and StaticVarCount requires 5.30.

A title reading something like Variables resident: 412 with a memory figure and one line per prefix. On a freshly started AmiBroker with no formulas run, expect zero.

Run a Part 13 ranking exploration, then apply this pane. The rank* and score* counts should be non-zero and roughly equal to the number of symbols in your watch list. Now close AmiBroker, reopen it, and apply the pane again before running anything: everything that was not written with persistent = True must be gone. If something survived, you have a persistent variable you did not intend.

Using StaticVarGet’s align argument as though it were StaticVarGetText’s — the text function has no align argument, and StaticVarSetText has no compressionMode. Copying a signature across the family is a reliable way to produce an argument-count error.

Add a fourth column: for each prefix, the count of variables whose names end in a ticker that is not in the current watch list. Those are the leftovers from a different universe, and they are exactly what corrupts a ranking that looks fine.

You now hold the right model. Static variables are a process-wide key/value store; they outlive every execution; persistence to disk is opt-in per variable and happens on exit or on a timer; a single Get or Set is atomic but a group of them is not; in multi-symbol Analysis you write once under Status("stocknum") == 0 and read freely afterwards; and nothing cleans up unless you do, with the asterisk in the right place and the pattern narrow enough not to take out someone else’s state.

Check your understanding

Question 1. An exploration writes StaticVarSet("score" + Name(), value) for 300 symbols and finishes. AmiBroker is left running. What is true an hour later?
Show the answer and why

Answer: All 300 are still resident, holding memory, readable by any formula

Static duration is the program run. Nothing frees them at the end of a formula or an Analysis run — only StaticVarRemove or closing AmiBroker does. Persistence to disk requires the third argument set to True.

Question 2. Which of these is a genuine race in a multi-symbol Analysis run?
Total = Nz( StaticVarGet( "T" ) );
Total = Total + Volume;
StaticVarSet( "T", Total );
Show the answer and why

Answer: Yes — atomicity covers each call individually, not the read-modify-write as a whole

The documentation states atomicity at the single call level. Another thread can run between the Get and the Set, and its update is then lost. StaticVarAdd exists precisely to make this one interlocked operation.

Question 3. Which statements about cleanup are correct? Select all that apply.
Show the answer and why

Answer: StaticVarRemove("Test*") also removes a variable named exactly "Test", The removal belongs inside the Status("stocknum")==0 block, before the writes, A short or over-broad pattern can delete other formulas’ static variables

The asterisk matches zero or more characters, so it also matches the bare name. StaticVarRemove returns nothing — verify with StaticVarCount or StaticVarInfo(pattern,"count") if you need proof.

Question 4. Why must the `if( Status("stocknum") == 0 )` initialisation block stay in the main formula rather than an include file?
Show the answer and why

Answer: AmiBroker detects that statement in the formula itself in order to run the first symbol single-threaded

The detection is textual and applies to the main formula. Inside an #include the code still runs, but the serialisation that makes it safe stops happening — and nothing reports that it stopped.

Sources for this lesson

10 verified · checked 2026-08-31

  1. 01AFL Function Reference — StaticVarSetamibroker.com/guide/afl/staticvarset.html2026-08-31
  2. 02AFL Function Reference — StaticVarGetamibroker.com/guide/afl/staticvarget.html2026-08-31
  3. 03AFL Function Reference — StaticVarGetTextamibroker.com/guide/afl/staticvargettext.html2026-08-31
  4. 04AFL Function Reference — StaticVarRemoveamibroker.com/guide/afl/staticvarremove.html2026-08-31
  5. 05AFL Function Reference — StaticVarInfoamibroker.com/guide/afl/staticvarinfo.html2026-08-31
  6. 06AFL Function Reference — StaticVarAddamibroker.com/guide/afl/staticvaradd.html2026-08-31
  7. 07AFL Function Reference — StaticVarCompareExchangeamibroker.com/guide/afl/staticvarcompareexchange.html2026-08-31
  8. 08AmiBroker User's Guide — Efficient use of multi-threadingamibroker.com/guide/h_multithreading.html2026-08-31
  9. 09AmiBroker User's Guide — static keywordamibroker.com/guide/keyword/static.html2026-08-31
  10. 10AFL Function Reference — SetOption§ StaticVarAutoSaveamibroker.com/guide/afl/setoption.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.