Skip to content
Level 3 · AFL DeveloperLessonPart 15 · page 2 of 528 min
28Minutes
9AFL functions
7Sources
StandardRequires
AFL functions taught here9

Benchmarks and Relative Performance

“Did this stock outperform?” sounds like a question with an answer. It is not. It is a question with three hidden parameters — outperform what, measured from when, and measured how — and changing any one of them changes the answer. This lesson builds the tool that answers it, and then spends as much effort demonstrating how much of the answer you chose as it spends on the arithmetic.

A benchmark is the alternative you are being compared with. Its job is to absorb everything you were not trying to measure, so that what remains is attributable to the thing you were.

That makes the choice a research decision, not a formatting decision. Four questions decide it:

Could you actually have held it? A broad index is not investable; a fund tracking it is. If the comparison is meant to answer “was owning this share better than the obvious alternative”, the obvious alternative has to be something a person could have bought, with its costs included. If the comparison is meant to answer “did this share move differently from its market”, an index is the cleaner instrument because it carries no fund-level tracking error.

Is it in the same currency? A share priced in one currency compared with an index priced in another produces a ratio that is partly a currency chart. Nothing in AmiBroker will tell you this has happened.

Does it trade on the same calendar? This is the subject of Lesson 4, and it is not a minor caveat. A domestic share against a domestic index is usually fine. A share against an index from another country, or against a futures contract that trades on days the equity market is closed, is a comparison across two different sets of dates that Foreign() has already quietly reconciled for you.

Does it include income? Most equity indices quoted in retail data are price indices: they exclude dividends. Individual share prices in most databases are also price series, though some vendors supply back-adjusted totals. Comparing a dividend-adjusted share against a price-only index tilts the whole comparison, and the tilt grows with the length of the window. State which you have; if you do not know, that is itself worth writing down.

The mechanics are trivial. Divide one series by the other:

Fragment — not a complete formula

BenchmarkClose = Foreign( "^GSPC", "C" );
Ratio = IIf( BenchmarkClose > 0, Close / BenchmarkClose, Null );

The IIf() guard is not decoration. A benchmark value of zero — which is what you can get from a badly imported bar — turns the whole array into infinities, and the chart auto-scales to accommodate them, leaving you with a flat line at the bottom of an empty pane.

A ratio line rises whenever the numerator gained relative to the denominator. That is a smaller claim than it looks, and it is the claim people routinely overstate:

Both series fall. The ratio rises.

A rising relative strength line through a falling market. The holder of the stock has lost ten per cent.
BarDay 1Day 2Day 3Day 4
Stock Close100.096.092.090.0
Benchmark Close100.094.088.084.0
Ratio1.0001.0211.0451.071
A rising relative strength line through a falling market. The holder of the stock has lost ten per cent.

A raw ratio is hard to read because its level is arbitrary — it depends on the two symbols’ price scales, so 0.043 and 17.2 are equally meaningless. Rebasing fixes that. Pick an anchor bar, divide the whole series by its value at that bar, and multiply by 100:

Fragment — not a complete formula

AnchorValue = ValueWhen( AnchorBar, Series );
Rebased = IIf( AnchorValue > 0, 100 * Series / AnchorValue, Null );

Now every series starts at 100 on the anchor bar and its value reads directly as a percentage of where it began. Two rebased price series plotted together compare cleanly regardless of whether one trades at 4 and the other at 40,000.

The interesting part is AnchorBar, which has to be an array that is true on exactly one bar. There are two useful ways to build it.

Anchor at a fixed date. ParamDate() returns a DateNum by default, and this expression is true only on the first bar dated on or after it:

Fragment — not a complete formula

AnchorDateNum = ParamDate( "Anchor date", "2020-01-02", 0 );
AnchorBar = Cum( DateNum() >= AnchorDateNum ) == 1;

Cum() counts qualifying bars from the beginning of the array, so the running count equals 1 on the first one and never again. If no bar qualifies — the date is beyond your data — the condition is never true, ValueWhen() returns Null throughout, and nothing is drawn. That is the right behaviour: an empty pane is a better answer than a confident wrong one.

Anchor at the left edge of the visible chart. This is what the built-in relative performance example in AmiBroker’s own documentation does, and it makes the chart interactive: pan or zoom and every line re-anchors to whatever is now on the left.

Fragment — not a complete formula

AnchorBar = BarIndex() == Status( "firstvisiblebar" );

Formula: rebased comparison with a sensitivity read-out

Section titled “Formula: rebased comparison with a sensitivity read-out”

Draw the current symbol, a benchmark, and the ratio between them, all set to 100 at a chosen anchor — and, in the same pane, report what the same comparison would have concluded from two other anchors. The second half is the part that changes how people use the first half.

Complete runnable AFL

rebased-comparison.afl
// ===========================================================================
// Rebased comparison and relative strength line
//
// Puts the chart's symbol and a benchmark on the same scale by setting both
// to 100 at a chosen anchor bar, and draws the ratio between them - also
// rebased to 100 - as a third line.
//
// Reading it: the ratio line rises on bars where the symbol gained more (or
// lost less) than the benchmark SINCE THE ANCHOR. It is a description of two
// price series. It carries no claim about what either series does next.
//
// HOW TO RUN
// Apply Indicator to a new pane. Ctrl+R sets the benchmark, the anchor mode
// and the two alternative anchor dates used by the sensitivity read-out.
//
// WHY THE ANCHOR IS A PARAMETER AND NOT A CONSTANT
// Every rebased comparison is relative to one bar. Move that bar and the
// whole picture moves with it. The title therefore prints the same
// comparison measured from three different anchors, so the reader can see
// the size of that effect instead of being asked to take it on trust.
//
// ASSUMPTIONS
// - Foreign() synchronises the benchmark to THIS symbol's bars. Bars the
// benchmark did not trade are filled from its previous Close (fixup 1) and
// are counted and reported below rather than hidden.
// - Rebasing needs a strictly positive value at the anchor bar. A zero or
// Null anchor produces an empty line, which is the honest outcome.
// ===========================================================================
_SECTION_BEGIN( "Rebased comparison" );
BenchmarkSymbol = ParamStr( "Benchmark symbol", "^GSPC" );
UseVisibleAnchor = ParamToggle( "Anchor at", "Fixed date|First visible bar", 1 );
PrimaryAnchor = ParamDate( "Anchor date", "2018-01-02", 0 );
AlternateAnchorA = ParamDate( "Sensitivity anchor A", "2020-03-23", 0 );
AlternateAnchorB = ParamDate( "Sensitivity anchor B", "2022-01-03", 0 );
// -- helpers ---------------------------------------------------------------
// True on exactly one bar: the first bar whose date is on or after the target.
// Cum() counts qualifying bars, so the count equals 1 only on the first of them.
function FirstBarAtOrAfter( TargetDateNum )
{
return Cum( DateNum() >= TargetDateNum ) == 1;
}
// ValueWhen() holds the value of the most recent bar where the condition was
// true, and Null before it has ever been true - which is exactly the behaviour
// a rebased series needs on the left of its anchor.
function RebaseTo100( Series, AnchorBar )
{
AnchorValue = ValueWhen( AnchorBar, Series );
return IIf( AnchorValue > 0, 100 * Series / AnchorValue, Null );
}
// -- data ------------------------------------------------------------------
BenchmarkClose = Foreign( BenchmarkSymbol, "C" ); // fixup defaults to 1
BenchmarkRaw = Foreign( BenchmarkSymbol, "C", 0 ); // holes left as Null
BenchmarkBars = LastValue( Cum( NOT IsNull( BenchmarkRaw ) ) );
PaddedBars = LastValue( Cum( IsNull( BenchmarkRaw ) AND NOT IsNull( BenchmarkClose ) ) );
HomeBars = LastValue( Cum( 1 ) );
// -- anchor ----------------------------------------------------------------
// Status("firstvisiblebar") is documented as available in indicator mode only,
// so the fixed-date anchor is used everywhere else. BarIndex() is documented to
// count from zero even when QuickAFL trims the array, which makes this
// comparison safer than subscripting the array directly.
UseVisible = UseVisibleAnchor AND Status( "action" ) == actionIndicator;
if ( UseVisible )
AnchorBar = BarIndex() == Status( "firstvisiblebar" );
else
AnchorBar = FirstBarAtOrAfter( PrimaryAnchor );
AnchorFound = LastValue( Cum( AnchorBar ) ) > 0;
HomeIndexed = RebaseTo100( Close, AnchorBar );
BenchIndexed = RebaseTo100( BenchmarkClose, AnchorBar );
// The relative strength line. Rebasing the ratio as well puts all three lines
// on one scale, so the pane needs no second axis to be read.
Ratio = IIf( BenchmarkClose > 0, Close / BenchmarkClose, Null );
RatioIndexed = RebaseTo100( Ratio, AnchorBar );
// -- start-date sensitivity ------------------------------------------------
// The same question, asked from three different anchors.
RatioFromPrimary = LastValue( RatioIndexed );
RatioFromA = LastValue( RebaseTo100( Ratio, FirstBarAtOrAfter( AlternateAnchorA ) ) );
RatioFromB = LastValue( RebaseTo100( Ratio, FirstBarAtOrAfter( AlternateAnchorB ) ) );
// -- plot ------------------------------------------------------------------
Plot( HomeIndexed, Name() + " (=100)", colorBlue, styleLine | styleThick );
Plot( BenchIndexed, BenchmarkSymbol + " (=100)", colorOrange, styleLine );
Plot( RatioIndexed, "Ratio " + Name() + "/" + BenchmarkSymbol, colorSeaGreen, styleLine | styleThick );
PlotGrid( 100, colorLightGrey );
_N( Title =
StrFormat( "%s against %s, both set to 100 at the anchor bar\n", Name(), BenchmarkSymbol )
+ WriteIf( UseVisible, "Anchor: first visible bar (pan or zoom the chart and every line moves).\n",
"Anchor: the fixed date set in Parameters.\n" )
+ WriteIf( AnchorFound, "",
"NO ANCHOR BAR FOUND - the anchor date is outside this symbol's history, so nothing is drawn.\n" )
+ StrFormat( "Symbol bars: %g benchmark bars with real quotes: %g padded bars: %g\n",
HomeBars, BenchmarkBars, PaddedBars )
+ StrFormat( "Ratio today, anchored at the chosen anchor: %.1f\n", RatioFromPrimary )
+ StrFormat( "Same ratio, anchored at sensitivity date A: %.1f\n", RatioFromA )
+ StrFormat( "Same ratio, anchored at sensitivity date B: %.1f\n", RatioFromB )
+ "Three numbers, one pair of symbols. The anchor is a choice, not a fact." );
_SECTION_END();

Download rebased-comparison.afl115 lines

Two small user-defined functions carry the logic. FirstBarAtOrAfter() turns a date into a one-bar-true array. RebaseTo100() turns any series plus such an array into a rebased series, relying on ValueWhen()’s documented behaviour of returning Null before its condition has ever been true — which is exactly what a rebased series should show to the left of its anchor.

The benchmark is read twice, with fixup = 1 for the series that gets plotted and fixup = 0 for a diagnostic copy in which holes remain Null. The difference between the two counts gives the number of padded bars, printed in the title. It costs one extra Foreign() call and it is the difference between a chart and a chart you can defend.

The ratio line is rebased as well as the two price lines. That is what lets all three share one axis: each starts at 100, so the pane needs no second scale and no explanation of which line belongs to which side.

The last section computes the final ratio three times, from three different anchors, and prints all three.

  • ParamDate( "name", "default", format ) — a date in the Parameters dialog. format = 0 returns a DateNum, which is what DateNum() produces, so the two compare directly. The reference warns that the default must be a constant, because parameter defaults are cached and not re-read on later evaluations.
  • ValueWhen( condition, array, n = 1 ) — the value of array at the most recent bar where condition was true. With a condition true on exactly one bar it becomes “hold this one value forward”.
  • Status( "firstvisiblebar" ) — the bar number at the left edge of the chart, in indicator mode only.

Three lines meeting at 100 on the anchor bar. The blue line is the symbol, orange is the benchmark, and the thick green line — the ratio — sits above 100 wherever the symbol has gained more than the benchmark since the anchor, and below it wherever it has not. The title reports the bar counts, the padded-bar count, and three versions of today’s ratio.

  1. Set the benchmark to the symbol you are already charting. The ratio line should sit flat at exactly 100 across the whole history. Any deviation means the two series are not what you think they are.
  2. Set the anchor date to something before your symbol’s first bar. Every line should start at 100 on the first available bar, because that is the first bar at or after the date.
  3. Set the anchor date beyond the last bar. Nothing should be drawn, and the title should say the anchor was not found.
  4. Read the three sensitivity numbers. Then change the two sensitivity dates to anything else and read them again.
Symptom Cause
Pane is empty, title says no anchor bar found Anchor date is outside this symbol’s history
Ratio line is drawn but the price lines are not The benchmark exists but the symbol’s own data starts later than the anchor
One flat line at 100 and one wild line Benchmark ticker is wrong; Foreign() returned nothing usable
Ratio jumps vertically on one bar A zero or corrupt benchmark bar. The IIf() guard blanks division by zero but not division by 0.0001
Numbers disagree with a website’s “year-to-date” figure Different anchor, different dividend treatment, or both

Add a fourth line: the ratio’s own moving average, so that “is the outperformance still going” becomes a question about a line rather than about a slope you eyeballed. Then ask yourself what the moving-average length is doing to the answer, and whether you would have chosen the same length if you had not already seen the chart.

Run the formula on any share against any broad index and read the three numbers in the title. On most instruments over most periods they will not merely differ in magnitude; they will differ in sign. The same pair of symbols, the same data, the same arithmetic, and the sentence “this stock has outperformed” is true from one anchor and false from another.

This is not a defect in the tool. It is a property of the question. A ratio between two series has no absolute level, so every statement about it is a statement about a change between two points, and both points are choices.

Two habits follow.

Choose the anchor for a reason you can state. “The start of the current market regime”, “the date the company changed its business”, “three years back, because that is my holding period” are all defensible. “Wherever the chart happened to be scrolled to” is not a reason, even though it is what an interactive anchor gives you by default — which is why the interactive mode is a convenience for looking, and the fixed date is the one you use when you are writing something down.

Report the sensitivity, not just the number. If a conclusion survives being measured from three anchors, say so; that is genuine evidence about its robustness. If it does not survive, the honest report is that the answer depends on the start date, which is a real finding about the pair and often more useful than the number you were hoping for.

AmiBroker ships a function for this: RelStrength( "tickername", fixup = 1 ). It returns the comparative relative strength of the current symbol against another. It is genuinely convenient, and there are two reasons this course computes ratios by hand instead.

The first is definitional. The function reference describes what RelStrength() is for, but the arithmetic it performs is not published on that page; the paragraph that explains how to interpret it is a user comment rather than vendor documentation. A number whose formula you cannot state is a number you cannot defend in a write-up, and it is not one you can reproduce in a spreadsheet when someone asks you to.

The second is the empty-string form. RelStrength("") uses the standard base security taken from Symbol → Categories. The Categories window documents Base indexes as a per-market setting that controls the index used for the Relative Strength indicator, for composites built through the Composite calculation option, and for Beta. That is a useful feature and a trap in equal measure: the same formula compares against different things for symbols filed under different markets, and moving a symbol between markets changes its meaning without touching a line of code.

If you want to know what that setting currently is for a symbol, GetBaseIndex() returns it as a string — a good column to add to any exploration that audits a database you inherited.

Use RelStrength() when you want a quick look. Use an explicit ratio when the number is going into a decision or a document.

What a rising ratio does and does not imply

Section titled “What a rising ratio does and does not imply”

It does establish that, over the window between the anchor and now, the numerator gained more or lost less than the denominator. That is arithmetic and it is not in dispute.

It does not establish that the stock went up. It does not establish that the relationship will continue — the ratio is a description of bars that have already printed, and nothing in it constrains the next one. It does not establish that the stock is a better holding, because that comparison needs costs, position size, and what else you might have done with the money. And it does not establish that whatever caused the outperformance is still operating, which is the assumption almost every use of a relative strength line quietly makes.

Part 13 showed the cross-sectional version of the same caution: “strongest” is period-dependent, and a ranking is not a strategy. The pairwise version is the same sentence with fewer symbols in it.

A benchmark is a choice about what counts as ordinary, and it needs to match your question in investability, currency, calendar and income treatment. A ratio chart is one division and one IIf() guard. Rebasing makes ratios readable by fixing them at 100 on an anchor bar, which you build either from a date with Cum( DateNum() >= d ) == 1 or from the left edge of the chart with BarIndex() == Status("firstvisiblebar").

The measurement is easy. The anchor is the research. Any relative-performance claim that does not come with its start date attached is incomplete, and one that changes sign when the start date moves is a finding in its own right.

Check your understanding

Question 1. Over four bars a stock falls from 100 to 90 and its benchmark falls from 100 to 84. What does the rebased ratio line do?
Show the answer and why

Answer: Rises to about 107, because the stock fell less

The ratio measures one series against the other, not against zero. It rises through a falling market whenever the numerator falls more slowly — which is why "relative strength" and "went up" are different statements.

Question 2. What does this expression produce?
AnchorBar = Cum( DateNum() >= AnchorDateNum ) == 1;
Show the answer and why

Answer: True on exactly one bar: the first bar dated on or after the anchor date

Cum() counts qualifying bars from the start of the array, so the running count equals 1 only on the first of them. That single-true-bar array is what ValueWhen() needs to hold one anchor value forward.

Question 3. Why does the lesson prefer an explicit Foreign() ratio to RelStrength() for published work?
Show the answer and why

Answer: Its arithmetic is not published on the function reference page, and its empty-string form depends on a per-market database setting

RelStrength() is a supported function and fine for a quick look. The objection is that a quantity you cannot define, and whose meaning changes with a Categories setting, is hard to defend or reproduce.

Question 4. A colleague reports that a share "outperformed its index by 14 per cent". What is the minimum you need before that sentence means anything? Select all that apply.
Show the answer and why

Answer: The start date of the measurement, Which index, and whether it includes dividends, Whether the share price series is dividend-adjusted

Anchor, benchmark identity and income treatment change the number directly. Beta is a different question — useful, but not needed to make the quoted figure well-defined.

Question 5. Status("firstvisiblebar") is used as an anchor. In which context will that formula fail to anchor as intended?
Show the answer and why

Answer: An exploration in the Analysis window

The Status page documents the visible-bar codes as available in indicator mode only. A cross-symbol formula that might be run as an exploration needs a fallback anchor, which is why the example formula tests Status("action") first.

Sources for this lesson

7 verified · checked 2026-09-01

  1. 01AFL Function Reference — Foreignamibroker.com/guide/afl/foreign.html2026-08-31
  2. 02AFL Function Reference — ValueWhenamibroker.com/guide/afl/valuewhen.html2026-08-31
  3. 03AFL Function Reference — ParamDateamibroker.com/guide/afl/paramdate.html2026-08-31
  4. 04AFL Function Reference — Status§ firstvisiblebaramibroker.com/guide/afl/status.html2026-08-31
  5. 05AFL Function Reference — BarIndexamibroker.com/guide/afl/barindex.html2026-08-31
  6. 06AFL Function Reference — RelStrengthamibroker.com/guide/afl/relstrength.html2026-08-31
  7. 07AmiBroker User's Guide — Categories window§ Base indexesamibroker.com/guide/w_categories.html2026-09-01

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.