Skip to content
Level 5 · Real-Time AmiBroker UserProjectPart Capstone · page 2 of 960 min
60Minutes
23AFL functions
7Sources
StandardRequires
AFL functions taught here23

Component 1: Market Dashboard

One pane that answers four questions about the market as a whole, before you look at any individual instrument:

  1. Is the benchmark above or below its own long-term average, and by how much in units of its own volatility?
  2. How widely is the move shared? — breadth, from the Part 16 composites.
  3. Is the range wide or narrow by the benchmark’s own recent standards?
  4. Which of your sector proxies have been strongest over a fixed window?

None of the four predicts anything. Every one describes a state that already exists, and the dashboard exists so that the state is written down before you start looking for reasons to trade.

Four, and each one changes what the dashboard means. Write your answers in the research log before you run it.

Which benchmark? The dashboard is plotted on it, and Foreign() aligns everything else to the current symbol’s bars. Choose one whose trading calendar matches your universe. A broad index is usual; if you trade a sector, a sector index is more informative and a broad one is more comparable.

Which universe were the breadth composites built from? This is the question people skip, and it matters more than the benchmark. Breadth measured over a different universe than the one you trade is a statement about somebody else’s market. If your composites cover 500 large caps and you trade 30 mid caps, the participation figure is not about your candidates.

Which sector proxies? They must exist in your database and share the benchmark’s calendar. Index symbols, sector ETFs or your own composites are all valid; a handful of representative shares is not, because one company’s news becomes “the sector”.

What lookback for “strength”? 63 bars is about a quarter. Longer measures a slower thing. There is no right answer, and the point is that the number is a choice you made rather than one you inherited.

Complete runnable AFL

market-dashboard.afl
// market-dashboard.afl
// Capstone Component 1 - Market Dashboard
//
// GOAL
// One pane that answers four questions about the market as a whole, before
// you look at any individual instrument:
// 1. Is the benchmark above or below its own long-term average, and by how
// much in units of its own volatility?
// 2. How widely is the move shared? (breadth, from the Part 16 composites)
// 3. Is the range wide or narrow by the benchmark's own recent standards?
// 4. Which of your sector proxies have been strongest over a fixed window?
//
// None of the four predicts anything. Every one of them describes a state
// that already exists, and the dashboard exists so that the state is written
// down before you start looking for reasons to trade.
//
// WHERE TO PLOT IT
// On the benchmark symbol itself, in its own pane. Foreign() aligns foreign
// data to the CURRENT symbol, so plotting this on an instrument that does not
// trade every session silently deletes bars from the picture.
//
// PREREQUISITE
// The breadth composites from Part 16. Run breadth-composite-builder.afl as
// a Scan over your universe first. If they are missing, the breadth band is
// drawn empty and the title says so rather than showing a plausible zero.
//
// ASSUMPTIONS - write these into your capstone report
// Interval daily end-of-day bars.
// Benchmark the symbol this pane is plotted on.
// Universe whatever universe the breadth composites were built from.
// Breadth measured over a different universe than the one you
// trade is a statement about somebody else's market.
// Sectors the proxy symbols named in the parameter below. They must
// exist in the database and share the benchmark's calendar.
// Warm-up nothing on this pane means anything until the longest
// look-back has filled. The title reports how many bars that is.
// Not modelled dividends, currency, and anything at all about the future.
_SECTION_BEGIN( "Capstone market dashboard" );
// The advance/decline line is a running total, so the pane must see every bar
// rather than only the ones currently on screen.
SetBarsRequired( sbrAll, sbrAll );
SetChartOptions( 2, chartWrapTitle );
// ---------------------------------------------------------------- settings
BreadthPrefix = ParamStr( "Breadth composite prefix", "~BR_" );
SectorList = ParamStr( "Sector proxy symbols (comma separated)", "" );
TrendPeriod = Param( "Benchmark trend average", 200, 20, 400, 10 );
AtrPeriod = Param( "ATR period", 20, 2, 100, 1 );
RankPeriod = Param( "Volatility percentile look-back", 252, 20, 1000, 1 );
SectorPeriod = Param( "Sector strength look-back (bars)", 63, 5, 500, 1 );
StretchCap = Param( "Trend band limit (ATR units)", 4, 1, 12, 0.5 );
UpTint = ParamColor( "Positive colour", colorBrightGreen );
DownTint = ParamColor( "Negative colour", colorRed );
QuietTint = ParamColor( "Neutral colour", colorBlueGrey );
GuideTint = ParamColor( "Guide colour", colorLightGrey );
RequiredBars = Max( Max( TrendPeriod, RankPeriod ), SectorPeriod ) + 1;
Ready = BarIndex() >= RequiredBars;
// ------------------------------------------------------------- band helper
// Identical to the Part 10 analysis panel: minvalue/maxvalue are documented as
// applying to styleOwnScale plots only, so a band is produced by choosing
// scale bounds that place DataMin at BandBottom and DataMax at BandTop, where
// both band figures are pane fractions running 0 at the foot to 1 at the top.
function PlotInBand( DataSeries, PlotName, PlotTint, StyleBits,
DataMin, DataMax, BandBottom, BandTop )
{
local Span, ScaleMin;
Span = ( DataMax - DataMin ) / ( BandTop - BandBottom );
ScaleMin = DataMin - BandBottom * Span;
Plot( DataSeries, PlotName, PlotTint,
StyleBits | styleOwnScale | styleNoLabel,
ScaleMin, ScaleMin + Span );
return Span;
}
// ----------------------------------------------------- 1. benchmark trend
TrendLine = MA( Close, TrendPeriod );
Volatility = ATR( AtrPeriod );
// Distance from the average in ATR units, so the number means the same thing
// on any benchmark, then clipped so one extreme bar cannot flatten the band.
Stretch = SafeDivide( Close - TrendLine, Volatility, Null );
Stretch = Max( Min( Stretch, StretchCap ), -StretchCap );
Stretch = IIf( Ready, Stretch, Null );
StretchTint = IIf( Stretch > 0, UpTint, DownTint );
PlotInBand( 0, "", GuideTint, styleLine | styleNoTitle,
-StretchCap, StretchCap, 0.72, 1.00 );
PlotInBand( Stretch, "Benchmark distance from trend (ATR units)", StretchTint,
styleHistogram | styleThick,
-StretchCap, StretchCap, 0.72, 1.00 );
// ------------------------------------------------------------- 2. breadth
Members = Nz( Foreign( BreadthPrefix + "MEMBERS", "V" ) );
AboveSlow = Nz( Foreign( BreadthPrefix + "ABOVE200", "V" ) );
NewHighs = Nz( Foreign( BreadthPrefix + "NEWHIGH", "V" ) );
NewLows = Nz( Foreign( BreadthPrefix + "NEWLOW", "V" ) );
HaveBreadth = LastValue( Members ) > 0;
PctAboveSlow = 100 * SafeDivide( AboveSlow, Members, Null );
NetNewHighs = 100 * SafeDivide( NewHighs - NewLows, Members, Null );
PlotInBand( 50, "", GuideTint, styleLine | styleDashed | styleNoTitle,
0, 100, 0.40, 0.68 );
PlotInBand( PctAboveSlow, "Percent of members above their 200-bar average",
QuietTint, styleLine | styleThick, 0, 100, 0.40, 0.68 );
// -------------------------------------------------------- 3. volatility
AtrPercent = 100 * SafeDivide( Volatility, Close, Null );
VolRank = IIf( Ready, PercentRank( AtrPercent, RankPeriod ), Null );
VolTint = IIf( VolRank >= 80, DownTint, IIf( VolRank <= 20, UpTint, QuietTint ) );
PlotInBand( 20, "", GuideTint, styleLine | styleDashed | styleNoTitle,
0, 100, 0.00, 0.32 );
PlotInBand( 80, "", GuideTint, styleLine | styleDashed | styleNoTitle,
0, 100, 0.00, 0.32 );
PlotInBand( VolRank, "Benchmark volatility percentile", VolTint,
styleHistogram, 0, 100, 0.00, 0.32 );
// --------------------------------------------------- 4. sector strength
// Each proxy's plain percentage change over SectorPeriod bars, read on the
// SELECTED bar. This is display only: it reads one bar and must never be used
// to build a signal.
SectorCount = StrCount( SectorList, "," );
if( StrLen( SectorList ) > 0 )
SectorCount = SectorCount + 1;
else
SectorCount = 0;
SectorText = "";
SectorsRead = 0;
for( i = 1; i <= SectorCount; i++ )
{
Ticker = StrExtract( SectorList, i - 1 );
if( StrLen( Ticker ) > 0 )
{
SectorClose = Foreign( Ticker, "C" );
// A missing symbol yields an empty array. Say so instead of printing a
// plausible zero next to the ones that worked.
if( SelectedValue( IsNull( SectorClose ) ) )
{
SectorText = SectorText + " " + Ticker + " n/a";
}
else
{
SectorMove = SelectedValue( ROC( SectorClose, SectorPeriod ) );
SectorText = SectorText + " " + Ticker + " "
+ NumToStr( SectorMove, 1.1 ) + "%";
SectorsRead = SectorsRead + 1;
}
}
}
if( SectorCount == 0 )
SectorText = " (no sector proxies configured - set them in the "
+ "Parameters dialog)";
// ------------------------------------------------------------------ title
TrendWord = WriteIf( Stretch > 0.5, "above trend",
WriteIf( Stretch < -0.5, "below trend", "at trend" ) );
BreadthWord = WriteIf( NOT HaveBreadth, "NO COMPOSITE DATA",
WriteIf( PctAboveSlow > 60, "broad participation",
WriteIf( PctAboveSlow < 40, "narrow participation",
"mixed participation" ) ) );
VolWord = WriteIf( VolRank >= 80, "wide range",
WriteIf( VolRank <= 20, "narrow range", "ordinary range" ) );
_N( Title = StrFormat(
"%s %s bars loaded %g warm-up needed %g\n"
+ "Trend: %s (%g ATR from the %g-bar average) Volatility: %s (percentile %g)\n"
+ "Breadth: %s %g%% of %g members above their 200-bar average net new highs %g%%\n"
+ "Sector %g-bar change:%s\n"
+ "Every line above describes what has already happened. None of it is a forecast.",
Name(), Interval( 2 ), BarCount, RequiredBars,
TrendWord, SelectedValue( Stretch ), TrendPeriod,
VolWord, SelectedValue( VolRank ),
BreadthWord, SelectedValue( PctAboveSlow ), SelectedValue( Members ),
SelectedValue( NetNewHighs ),
SectorPeriod, SectorText ) );
_SECTION_END();

Download market-dashboard.afl195 lines

PlotInBand() is the same helper as Part 10’s analysis panel, and for the same reason: minvalue and maxvalue are documented as applying to styleOwnScale plots only, so three series with different natural ranges can share a pane only if each is given scale bounds that place its data inside its own horizontal slice.

Three bands, with deliberate gaps between them so a spike in one is never mistaken for part of the next.

Distance from the long average, divided by ATR, then clipped:

Fragment — not a complete formula

Stretch = SafeDivide( Close - TrendLine, Volatility, Null );
Stretch = Max( Min( Stretch, StretchCap ), -StretchCap );

Dividing by ATR is what makes the number portable across benchmarks. The clip is a display decision — it keeps one extreme bar from flattening the band — and the parameter is named “Trend band limit” rather than something implying the data was changed for analytical reasons.

Fragment — not a complete formula

Members = Nz( Foreign( BreadthPrefix + "MEMBERS", "V" ) );
AboveSlow = Nz( Foreign( BreadthPrefix + "ABOVE200", "V" ) );
PctAboveSlow = 100 * SafeDivide( AboveSlow, Members, Null );

The composites store counts in the Volume field, which is why every read is Foreign( ticker, "V" ). SafeDivide returns Null on bars where no member was yet eligible, so the line is empty there rather than showing a plausible zero.

Fragment — not a complete formula

Ticker = StrExtract( SectorList, i - 1 );
SectorClose = Foreign( Ticker, "C" );
if( SelectedValue( IsNull( SectorClose ) ) )
SectorText = SectorText + " " + Ticker + " n/a";

StrExtract pulls the i-th comma-separated item, zero-based. A missing symbol yields an empty array, and the loop says n/a rather than printing a plausible zero next to the ones that worked — which is the same fail-loud principle as the breadth health check.

Note that this reads the selected bar only. It is display, and it must never be used to build a signal.

Check the band mapping. Temporarily plot the constant 100 into the breadth band with the same bounds. It must land exactly on that band’s top edge. Do the same with 0 and the bottom edge. Remove both when satisfied.

Check the breadth arithmetic against a source you trust. Pick a date. Count by hand — from the Part 2 exploration or a spreadsheet — how many of your universe members closed above their 200-bar average on that date. It must match the composite. If it does not, the composite was built over a different universe or a different date range than you think.

Check the ATR normalisation. Read the stretch figure from the title on a chosen bar, then verify it by hand: close, minus the moving-average value, divided by an ATR reading from a separate pane. Within rounding they must agree.

Check the warm-up. Scroll to the extreme left edge. The volatility band must be empty for the first RankPeriod bars and the trend band for the first TrendPeriod. Anything drawn there is a seeded value, not data.

Check a sector figure by hand. Take one proxy, read its close today and its close 63 bars ago from its own chart, compute the percentage change, and compare with the title.

Everything collapses into one flat line. A plot is missing styleOwnScale, so its scale bounds are ignored and it rejoins the pane’s shared axis.

Breadth is flat at zero and the title does not say NO COMPOSITE DATA. LastValue( Members ) is above zero but the individual counts are not — usually a prefix mismatch between the builder and the dashboard. Check BreadthPrefix.

Breadth data disappears on some bars. You plotted the dashboard on an instrument that does not trade every session. Foreign() aligns to the current symbol, so bars the current symbol lacks are silently dropped. Plot it on the benchmark or on the ~BR_MEMBERS composite itself.

A sector shows n/a that you expected to work. The symbol is missing, misspelled, or has no data in the loaded range. Check it in the symbol tree before assuming the formula is wrong.

The volatility band never leaves the middle. RankPeriod is longer than the loaded history, so PercentRank never has a full window. Load more bars or shorten the lookback.

The advance/decline figures look wrong after a data update. Composites are stored data, not live calculations. Re-run the builder scan after every data update — or chain it with #pragma sequence as Part 12’s workflow lesson describes.

  1. Add the net-new-highs line as a fourth band. The composite is already read; only the plotting is missing. Decide where it goes and what it does to the readability of the other three.

  2. Add a divergence note to the title that fires when the benchmark is above its trend average while breadth is below 50 — a state that is interesting precisely because the two disagree. Resist making it a signal; the value is that it names a state you would otherwise scroll past.

  3. Build your own sector composites with AddToComposite() rather than using proxy symbols, so the sector strength is measured over exactly the names you trade. This removes the “different universe” objection entirely and is the more honest version.

  4. Record the dashboard state daily, appending one line per day to a CSV with the journal technique from Part 12. After six months you have a dataset of your own market states — which is the raw material for asking whether any of them mattered.

One paragraph in the research log:

  • Which benchmark, and why.
  • Which universe the composites were built from, and whether it is the universe you trade.
  • Which sector proxies, and what they actually represent.
  • The lookback periods you chose and why.
  • The date you last rebuilt the composites.

Component 9 asks about the first two directly.

Check your understanding

Question 1. Why must the breadth composites be built from the same universe you trade?
Show the answer and why

Answer: Because breadth measured over a different universe is a statement about a different market — a participation figure from 500 large caps says nothing about your 30 mid caps

The composites will read fine either way, which is exactly the danger. The number is arithmetically correct and answers a question about a population you are not trading.

Question 2. The breadth composites are read with Foreign( ticker, "V" ). Why the Volume field?
Show the answer and why

Answer: Because AddToComposite stores the counts in the field named when the composite was built, and the Part 16 builder writes counts into "V"

AddToComposite takes a field code — "C", "O", "H", "L", "V", "I", "1", "2" or "X" — and the builder chose "V" for counts. The dashboard must read the same field the builder wrote.

Question 3. Why does the sector loop print "n/a" instead of a zero for a missing symbol?
if( SelectedValue( IsNull( SectorClose ) ) )
    SectorText = SectorText + "   " + Ticker + " n/a";
Show the answer and why

Answer: Because zero looks like data — a missing symbol would appear as the weakest sector rather than as an absent one

This is the same fail-loud principle as the NO COMPOSITE DATA check. A dashboard that cannot distinguish "measured and it was zero" from "not measured" will eventually be read as though it could.

Question 4. You plot the dashboard on a thinly traded share and some breadth bars are missing. Why?
Show the answer and why

Answer: Foreign() aligns foreign data to the CURRENT symbol's bars, so composite bars for sessions the current symbol did not trade are silently dropped

Alignment is to the current symbol, which is why the file says to plot this on the benchmark or on the composite itself. The data is not missing — it is being discarded by the alignment.

Question 5. Which statements about this dashboard are accurate? Select all that apply.
Show the answer and why

Answer: Every band describes a state that already exists rather than forecasting one, The sector strength figures read the selected bar only and must not be used to build a signal, The clipping of the trend band is a display decision, not an analytical one

Broad participation is a description of what has happened, not a prediction of what follows — establishing whether it has any predictive content would require the kind of study Part 12 and Part 30 build, not a dashboard.

Sources for this lesson

7 verified · checked 2026-08-31

  1. 01AFL Function Reference — Foreignamibroker.com/guide/afl/foreign.html2026-08-31
  2. 02AFL Function Reference — Plot§ minvalue and maxvalue (used by styleOwnScale plots ONLY)amibroker.com/guide/afl/plot.html2026-08-31
  3. 03AFL Function Reference — PercentRankamibroker.com/guide/afl/percentrank.html2026-08-31
  4. 04AFL Function Reference — SafeDivideamibroker.com/guide/afl/safedivide.html2026-08-31
  5. 05AFL Function Reference — StrExtractamibroker.com/guide/afl/strextract.html2026-08-31
  6. 06AFL Function Reference — SetBarsRequiredamibroker.com/guide/afl/setbarsrequired.html2026-08-31
  7. 07AFL Function Reference — AddToCompositeamibroker.com/guide/afl/addtocomposite.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.