Skip to content
Level 3 · AFL DeveloperProjectPart 15 · page 5 of 550 min
50Minutes
11AFL functions
6Sources
StandardRequires
AFL functions taught here11

Project: Relative Strength Comparison Tool

Four lessons of machinery come together here. You are going to build the tool that a lot of analysts reach for daily — a stock against its sector against its market, on one pane — and you are going to build it so that it tells you when it cannot answer.

That second requirement is the project. Anyone can divide two arrays. The work is in the guard rails.

The tool answers one question with three lines: where did this stock’s performance come from? A stock can be ahead of the market because its sector was ahead of the market, because it was ahead of its sector, or both. Three ratios, plotted together and anchored at a common bar, separate those cases at a glance.

Line Ratio The question it answers
1 stock / market Did holding this beat the obvious alternative?
2 stock / sector Did this company do better than its competitors?
3 sector / market Was the sector where the movement came from?

All three are rebased to 100 at a common anchor, so they share one axis and the pane needs no second scale.

The tool must not draw a ratio against a price that was never printed. Concretely, these three states have to be handled rather than averaged over:

  • a proxy symbol that does not exist in the database at all;
  • bars before a proxy’s first real quote and after its last;
  • bars inside a proxy’s life on which it did not trade.

The first two are fatal to the affected bars and must produce Null. The third is survivable — you usually do want the line to continue over a holiday — but it must be counted and reported, because a comparison that is ten per cent manufactured is a different object from one that is aligned.

The four design decisions

  1. Ratios, not levels, by defaultLevels answer "what happened"; ratios answer "compared with what"
  2. One anchor for all three linesDifferent anchors per line would make the pane unreadable
  3. Read each proxy twicefixup 0 to find the holes, fixup 1 to draw the line
  4. Every limitation printed in the titleA tool that hides its assumptions gets trusted more than it should

Ratios by default, levels on a toggle. A rebased-levels view is genuinely useful when you want to see the paths rather than the differences, so it is a display option rather than a second formula.

One anchor. Both anchor styles from the benchmarks lesson are offered: a fixed date for anything you intend to write down, and the left edge of the visible chart for interactive exploration. The fallback matters, because Status("firstvisiblebar") is documented as available in indicator mode only.

Two reads per proxy. Four Foreign() calls in total. This is a deliberate, stated cost: each is a cross-symbol access, and the User’s Guide notes that any access to a symbol other than the current one takes a global lock. On a chart pane that is irrelevant. The comment block in the file says so, so that nobody copies the pattern into a 3,000-symbol scan without thinking.

A market proxy that can come from the database. GetBaseIndex() returns the relative-strength base index configured for this symbol’s market in Symbol → Categories. Offering it as an option means the tool can follow a database’s own convention rather than a hard-coded ticker — and it gives you a reason to find out what that setting actually says.

Complete runnable AFL

relative-strength-comparison.afl
// ===========================================================================
// Relative strength comparison tool
// Part 15 project - stock against its sector proxy against the broad market,
// in one pane, with the alignment evidence printed rather than assumed.
//
// HOW TO RUN
// Apply Indicator to a new pane. Ctrl+R sets the two proxy symbols, the
// anchor and the display mode.
//
// WHAT IT DRAWS
// Relative strength mode (default) - three ratio lines, each set to 100 at
// the anchor bar:
// stock / market how the stock did against the whole market
// stock / sector how the stock did against its own peer group
// sector / market how the peer group did against the whole market
// Reading the three together separates "this stock led" from "this stock was
// carried by its sector".
//
// Rebased level mode - the same three price series, each set to 100 at the
// anchor, for when you want to see the paths rather than the ratios.
//
// THE ALIGNMENT GUARD
// Both proxies are read twice: once with fixup = 0, so that holes stay Null
// and can be counted, and once with fixup = 1, which is the series actually
// plotted. Bars outside a proxy's real quoting history are then masked back
// to Null, so the tool never draws a ratio against a number that was
// manufactured before the proxy existed or after it stopped. The remaining
// padded bars - the ones inside the proxy's life where it simply did not
// trade - are counted, marked with the ribbon along the bottom and stated in
// words in the title.
//
// COST
// Four Foreign() calls. Each one is a cross-symbol access, which the User's
// Guide notes takes a global lock, so this formula is noticeably heavier
// than a single-symbol indicator. That is affordable on a chart and worth
// thinking about before putting the same code in a scan over 3,000 symbols.
//
// ASSUMPTIONS, STATED SO THEY CAN BE CHECKED
// - A sector ETF or index is a PROXY for a sector, not the sector. Its own
// weighting, constituents and rebalancing rules sit between you and the
// thing you meant to measure.
// - Every number here is relative to the anchor bar. Change the anchor and
// every line changes with it. The anchor is a choice you are making.
// - Nothing in this formula is a signal, and none of it says what any of the
// three series does next.
// ===========================================================================
_SECTION_BEGIN( "Relative strength comparison" );
MarketSymbol = ParamStr( "Market proxy", "^GSPC" );
SectorSymbol = ParamStr( "Sector proxy", "XLK" );
UseBaseIndex = ParamToggle( "Market proxy source", "Parameter above|Database base index", 0 );
DisplayMode = ParamToggle( "Show", "Relative strength lines|Rebased price levels", 0 );
AnchorMode = ParamToggle( "Anchor at", "Fixed date|First visible bar", 1 );
AnchorDateNum = ParamDate( "Anchor date", "2020-01-02", 0 );
// GetBaseIndex() returns the relative-strength base index configured for this
// symbol's market in Symbol -> Categories. It is empty when nothing has been
// configured there, so fall back to the typed parameter.
BaseIndex = GetBaseIndex();
if ( UseBaseIndex AND StrLen( BaseIndex ) > 0 )
MarketSymbol = BaseIndex;
// -- helpers ---------------------------------------------------------------
// True on exactly one bar: the first bar dated on or after the target.
function FirstBarAtOrAfter( TargetDateNum )
{
return Cum( DateNum() >= TargetDateNum ) == 1;
}
// Blanks out every bar outside the proxy's real quoting history, so that no
// ratio is ever computed against a value the proxy never printed.
function MaskToRealRange( PaddedSeries, RawSeries )
{
HasReal = NOT IsNull( RawSeries );
Started = Cum( HasReal ) > 0;
LastReal = LastValue( ValueWhen( HasReal, BarIndex() ) );
StillLive = BarIndex() <= LastReal;
return IIf( Started AND StillLive, PaddedSeries, Null );
}
// Sets a series to 100 at the single bar where AnchorBar is true.
function RebaseTo100( Series, AnchorBar )
{
AnchorValue = ValueWhen( AnchorBar, Series );
return IIf( AnchorValue > 0, 100 * Series / AnchorValue, Null );
}
// -- read the two proxies --------------------------------------------------
MarketRaw = Foreign( MarketSymbol, "C", 0 );
MarketPadded = Foreign( MarketSymbol, "C", 1 );
SectorRaw = Foreign( SectorSymbol, "C", 0 );
SectorPadded = Foreign( SectorSymbol, "C", 1 );
MarketClose = MaskToRealRange( MarketPadded, MarketRaw );
SectorClose = MaskToRealRange( SectorPadded, SectorRaw );
MarketRealBars = LastValue( Cum( NOT IsNull( MarketRaw ) ) );
SectorRealBars = LastValue( Cum( NOT IsNull( SectorRaw ) ) );
HomeBars = LastValue( Cum( 1 ) );
MarketPaddedBar = IsNull( MarketRaw ) AND NOT IsNull( MarketClose );
SectorPaddedBar = IsNull( SectorRaw ) AND NOT IsNull( SectorClose );
PaddedBarCount = LastValue( Cum( MarketPaddedBar OR SectorPaddedBar ) );
HaveMarket = MarketRealBars > 0;
HaveSector = SectorRealBars > 0;
// -- anchor ----------------------------------------------------------------
// Status("firstvisiblebar") is documented as indicator-mode only, so anything
// else falls back to the date anchor. Comparing against BarIndex(), which is
// documented to start at zero even under QuickAFL, avoids the array-subscript
// mismatch that direct indexing can produce.
UseVisible = AnchorMode AND Status( "action" ) == actionIndicator;
if ( UseVisible )
AnchorBar = BarIndex() == Status( "firstvisiblebar" );
else
AnchorBar = FirstBarAtOrAfter( AnchorDateNum );
AnchorFound = LastValue( Cum( AnchorBar ) ) > 0;
// -- the three comparisons -------------------------------------------------
StockVsMarket = IIf( MarketClose > 0, Close / MarketClose, Null );
StockVsSector = IIf( SectorClose > 0, Close / SectorClose, Null );
SectorVsMarket = IIf( MarketClose > 0, SectorClose / MarketClose, Null );
if ( DisplayMode == 0 )
{
LineOne = RebaseTo100( StockVsMarket, AnchorBar );
LineTwo = RebaseTo100( StockVsSector, AnchorBar );
LineThree = RebaseTo100( SectorVsMarket, AnchorBar );
NameOne = Name() + " / " + MarketSymbol;
NameTwo = Name() + " / " + SectorSymbol;
NameThree = SectorSymbol + " / " + MarketSymbol;
}
else
{
LineOne = RebaseTo100( Close, AnchorBar );
LineTwo = RebaseTo100( SectorClose, AnchorBar );
LineThree = RebaseTo100( MarketClose, AnchorBar );
NameOne = Name() + " rebased";
NameTwo = SectorSymbol + " rebased";
NameThree = MarketSymbol + " rebased";
}
Plot( LineOne, NameOne, colorBlue, styleLine | styleThick );
Plot( LineTwo, NameTwo, colorSeaGreen, styleLine );
Plot( LineThree, NameThree, colorOrange, styleLine );
PlotGrid( 100, colorLightGrey );
// The ribbon flags bars where at least one proxy price was manufactured. It is
// a convenience: the same information is counted in the title, so the pane does
// not depend on the reader being able to distinguish the two shades.
RibbonColour = IIf( MarketPaddedBar OR SectorPaddedBar, colorRose, colorPaleGreen );
Plot( 2, "Alignment", RibbonColour,
styleOwnScale | styleArea | styleNoLabel | styleNoTitle, -0.5, 100 );
// -- what the reader needs in order to judge the picture -------------------
LastStockVsMarket = LastValue( RebaseTo100( StockVsMarket, AnchorBar ) );
LastStockVsSector = LastValue( RebaseTo100( StockVsSector, AnchorBar ) );
_N( Title =
StrFormat( "%s vs sector proxy %s vs market proxy %s\n",
Name(), SectorSymbol, MarketSymbol )
+ "AmiBroker files this symbol under sector: " + SectorID( 1 )
+ ", industry: " + IndustryID( 1 ) + "\n"
+ WriteIf( HaveMarket, "", "MARKET PROXY HAS NO DATA - check the ticker spelling.\n" )
+ WriteIf( HaveSector, "", "SECTOR PROXY HAS NO DATA - check the ticker spelling.\n" )
+ WriteIf( AnchorFound, "", "ANCHOR DATE IS OUTSIDE THIS SYMBOL'S HISTORY - nothing is drawn.\n" )
+ StrFormat( "Bars: this symbol %g, market proxy %g real, sector proxy %g real\n",
HomeBars, MarketRealBars, SectorRealBars )
+ StrFormat( "Bars where a proxy price was padded rather than traded: %g\n", PaddedBarCount )
+ StrFormat( "Since the anchor: %s is at %.1f against the market and %.1f against the sector (100 = level with it)\n",
Name(), LastStockVsMarket, LastStockVsSector )
+ "These are descriptions of what has already happened, measured from one "
+ "chosen bar. They are not forecasts." );
_SECTION_END();

Download relative-strength-comparison.afl187 lines

GetBaseIndex() is read unconditionally, and used only when the toggle asks for it and it returns a non-empty string. A database with no base index configured returns nothing, and the typed parameter is what remains. That ordering — read, test, then use — is the same discipline as testing SetForeign()’s return value.

FirstBarAtOrAfter() converts a date into an array true on exactly one bar, using the Cum( condition ) == 1 idiom from the benchmarks lesson.

RebaseTo100() divides a series by its value at the anchor bar and multiplies by 100, relying on ValueWhen() returning Null before its condition has ever been true.

MaskToRealRange() is the one doing the safety work. It takes a padded series and an unpadded copy of the same series and blanks every bar outside the proxy’s real quoting history:

  • Started is true from the proxy’s first real bar onwards, using Cum() on the real-bar flag.
  • LastReal is the bar number of the proxy’s most recent real bar, found by holding BarIndex() forward with ValueWhen() and taking LastValue().
  • Everything outside that window becomes Null, so no ratio is ever computed against a manufactured pre-listing or post-delisting price.

Inside the window, the padded series is used as-is — which is what you want, because a line that vanished on every holiday would be unreadable — and the padded bars are counted separately.

The three ratios are computed with an IIf() guard against a non-positive denominator, then rebased. The display toggle chooses whether the three plotted lines are the ratios or the three rebased price series; the plotting code below is identical either way, which is why the names are assigned into variables rather than written into the Plot() calls.

The ribbon along the bottom marks bars where at least one proxy price was padded. It is drawn with styleOwnScale | styleArea | styleNoLabel | styleNoTitle so it does not disturb the main scale, in the same way as the market-structure ribbon from Part 4.

The ribbon is a convenience, not the message. The same information is counted and printed in the title, so the pane remains usable by a reader who cannot distinguish the two shades — a requirement this course applies to every chart it ships.

The title carries eight pieces of information: the three symbols; the sector and industry AmiBroker has this symbol filed under; a warning line for each proxy with no data; a warning if the anchor is outside the history; the bar counts; the padded-bar count; the two current ratio readings; and a closing sentence stating what the numbers are and are not.

This is the part worth reading twice, because it is where the tool differs from the version most people write.

A proxy that does not exist. MarketRealBars or SectorRealBars is zero, the title says so in capitals, and the affected lines are Null throughout because the masked series is Null throughout. Nothing is drawn and nothing is implied. Compare that with the naive version, where a missing proxy produces either a blank pane with no explanation or, worse, a line derived from whatever Foreign() returned.

A proxy listed after the stock. The masked series is Null for every bar before the proxy’s first quote, so the ratio lines simply begin later than the price data. The bar counts in the title make the shortfall explicit: a stock with 6,000 bars compared against a sector fund with 1,400 real quotes is a study over 1,400 bars, whatever the horizontal extent of the chart suggests.

A proxy that stopped quoting. The mask ends at the last real bar, so the ratio lines stop there too, instead of continuing as a copy of the stock. This is the single most valuable behaviour in the file, because the failure it prevents looks like success.

A proxy that did not trade on some days. The line continues, the bar is counted, the ribbon marks it and the title reports the total. You are told, and then you decide.

Apply the formula to a share in a sector you have a proxy for. You should see:

  • three lines meeting at 100 on the anchor bar, one thick blue, one green, one orange;
  • a thin ribbon along the bottom of the pane, mostly one colour, with occasional marks;
  • a title block of six or seven lines, ending with the sentence about descriptions rather than forecasts.

Read it as follows. If the blue line (stock / market) and the orange line (sector / market) have risen together while the green line (stock / sector) is flat, the stock’s outperformance came from its sector. If blue and green have both risen while orange is flat, it came from the company. If blue is flat while green rises and orange falls, the company beat its peers in a sector that lagged, and the two effects cancelled.

Do all six. The first three prove the arithmetic; the last three prove the guards.

  1. Identity test. Set both proxies to the chart’s own symbol. All three ratio lines must sit at exactly 100 across the entire history. A deviation of any size means the rebasing or the masking is wrong.
  2. Consistency test. Switch to Rebased price levels mode and check that the stock’s line ends where the stock / market ratio and the market’s line imply it should. If the stock is at 150 and the market at 120, the ratio should read 125 — that is the arithmetic, and it either holds or something is off by a bar.
  3. Anchor test. Move the anchor date and confirm all three lines re-anchor together and still meet at 100. Then set the anchor beyond the last bar and confirm the title reports it and nothing is drawn.
  4. Missing proxy test. Misspell the sector proxy. The title must say the sector proxy has no data, the two lines involving it must disappear, and the stock / market line must be unaffected.
  5. Delisting test. Set the sector proxy to any symbol in your database whose last quote is well in the past — a delisted share is ideal. The lines involving it must stop at that date rather than running to the right edge. This is the guard that matters most; test it on real data, not a thought experiment.
  6. Cross-check against the audit. Run the previous lesson’s alignment audit on the same three symbols, in both directions. The padded-bar count in the audit should be consistent with the count in this tool’s title, and the audit will tell you about the direction this tool cannot see.
Symptom Likely cause What to do
All three lines flat at 100 Both proxies set to the chart symbol, or the anchor is the last bar Check the parameters; this is also test 1 passing
Only one line drawn One proxy has no data; the title says which Fix the ticker spelling
Ratio lines stop part-way across the pane The proxy’s last real quote is there. This is correct behaviour Update the proxy’s data, or choose a different one
Ratio lines start part-way across the pane The proxy was listed later than the stock Note the real length of the study; do not quote the chart’s full span
Ribbon is almost entirely marked The proxy trades on a different calendar Reconsider the proxy, or restrict the range
Lines meet at 100 in the middle of the pane, not the left Fixed-date anchor mode with a date inside the history Working as intended; switch to visible-bar mode to anchor at the left edge
A vertical spike on one bar A corrupt proxy bar close to zero. The guard blocks division by zero, not by 0.001 Inspect that bar in the Quote Editor
Title says Name() is in a sector you did not expect AmiBroker’s classification, not your data source’s Fix it in Symbol → Categories, or use a different classification

Add a fourth comparison. An equal-weighted proxy for the same sector alongside the capitalisation-weighted one. Where the two disagree, the difference is telling you how much of the sector’s move came from its largest members.

Compute the sector yourself. Instead of a fund, loop over CategoryGetSymbols( categorySector, SectorID() ) with SetForeign() and RestorePriceArrays() inside the loop, and build an equal-weighted average of the members’ rebased prices. This removes the fund from the comparison entirely and introduces its own survivorship question — which members are in the list today rather than at the time. Both problems are real; making the swap knowingly is the point.

Add a rolling excess return. Rather than one ratio anchored at one date, plot the difference between the stock’s and the benchmark’s trailing 63-bar returns. That removes the anchor sensitivity entirely, at the cost of introducing a window length you now have to justify. Compare it with the anchored version and decide which question you were actually asking.

Turn it into an exploration. Replace the plotting with AddColumn() calls and you have a table of every symbol in a watch list, with its ratio against sector and market, and its padded-bar count as a data-quality column beside the result. The Status("firstvisiblebar") anchor must be replaced by the date anchor for this, which the formula already handles.

Add the alignment audit’s verdict column. The two files share most of their diagnostic logic. Merging them gives you one tool that draws the comparison and grades its own inputs — a good exercise in factoring shared code into the personal library from Part 11.

You have a chart tool that separates a company’s contribution from its sector’s, anchors all of it at a bar you chose deliberately, refuses to compute against data that was never printed, and states in words how much of what remains was manufactured rather than traded.

What it is not is a signal generator. Nothing in the file produces a Buy array, and that is not an omission. Every line on the pane is a description of bars that have already printed, measured from an anchor you selected; none of it constrains the next bar. Relative strength is widely used as an entry filter, and Part 13 showed the cross-sectional machinery for doing that at scale. Whether it earns its place in a system is a question for a backtest with costs, not for a chart — and the honest version of that test is several parts away.

Check your understanding

Question 1. The stock/market line and the sector/market line have both risen strongly since the anchor, while the stock/sector line is flat. What does that indicate?
Show the answer and why

Answer: The stock moved with its sector, and the sector is where the outperformance came from

Flat against the sector means the company did what its peers did. The gain relative to the market is attributable to the sector, which redirects the next question away from the company entirely.

Question 2. Why does MaskToRealRange() blank bars after the proxy's last real quote, rather than letting the padded series continue?
Show the answer and why

Answer: Because a ratio against a flat denominator becomes a copy of the numerator, so relative strength rises whenever the stock rises

A stale proxy padded forward at its last close makes any ratio against it a rescaled copy of the stock. It looks like sustained outperformance and it is an artefact of missing data.

Question 3. The tool reports zero padded bars. Which of these has it established? Select all that apply.
Show the answer and why

Answer: Both proxies had a real quote on every bar the stock had a bar, Neither proxy is stale at the right-hand edge

The tool sees holes in the proxies only. Bars that exist in a proxy but not in the stock were removed by Foreign() before the formula ran, so the second direction needs the audit run with the symbols swapped.

Question 4. What is the purpose of reading each proxy with both fixup = 0 and fixup = 1?
Show the answer and why

Answer: The unpadded copy shows where real quotes exist; the padded copy is what gets drawn

It is the whole diagnostic technique of this part: the difference between the two arrays is exactly the set of manufactured bars, and there is no other way to see them from inside AFL.

Question 5. You want to run this comparison across a 2,000-symbol watch list as an exploration. What should you reconsider?
Show the answer and why

Answer: The four Foreign() calls per symbol, because each cross-symbol access takes a global lock

The multithreading page recommends reducing Foreign() use and preferring static variables. Where the same foreign series is needed for every symbol, computing it once into a static variable turns thousands of locked reads into one.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AFL Function Reference — Foreignamibroker.com/guide/afl/foreign.html2026-08-31
  2. 02AFL Function Reference — GetBaseIndexamibroker.com/guide/afl/getbaseindex.html2026-08-31
  3. 03AFL Function Reference — ValueWhenamibroker.com/guide/afl/valuewhen.html2026-08-31
  4. 04AFL Function Reference — Status§ firstvisiblebaramibroker.com/guide/afl/status.html2026-08-31
  5. 05AFL Function Reference — Plotamibroker.com/guide/afl/plot.html2026-08-31
  6. 06AmiBroker User's Guide — Multithreading§ Reducing the use of AddToComposite / Foreign to a minimumamibroker.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.