Project: Multi-Indicator Analysis Panel
Objective
Section titled “Objective”Build a single chart pane that shows three different readings of the same instrument at once — where price sits relative to its trend, how momentum is behaving, and whether the current range is wide or narrow by that instrument’s own standards — with each reading in its own horizontal band, correctly scaled, readable on a phone, and clearly labelled.
By the end you will have solved a real AFL problem that has no built-in solution: putting several series with completely different natural ranges into one pane without any of them squashing the others flat.
You will also have built the thing this part has been leading to, and the thing the capstone’s market dashboard is a bigger version of.
Prerequisites
Section titled “Prerequisites”- Plotting with
Plot()— styles, panes and whatstyleOwnScaledoes - Parameters —
Param,ParamToggle,ParamColor - Chart titles and dynamic text
- User-defined functions is helpful but not required; the panel defines one function and the lesson explains it
The design problem, stated honestly
Section titled “The design problem, stated honestly”Put an RSI (0 to 100) and a distance-from-trend reading (roughly −5 to +5) in the same pane with normal scaling and you get a flat line at the bottom of the chart and nothing else. The axis has to cover both, so the smaller series occupies a pixel.
AFL gives you exactly one lever over an individual plot’s vertical scale, and the
documentation is precise about it: minvalue and maxvalue are the fifth and sixth
arguments of Plot(), and they are used by styleOwnScale plots only. That is the whole
toolkit. There is no “place this plot in the top third of the pane” option.
So we build one.
Turning a scale range into a band
Section titled “Turning a scale range into a band”Think of the pane as running from 0 at the foot to 1 at the top. You want a series whose data
runs from DataMin to DataMax to occupy the slice from BandBottom to BandTop.
With scale bounds of ScaleMin to ScaleMin + Span, a value v is drawn at the pane
fraction:
Pseudocode — not valid AFL
fraction( v ) = ( v - ScaleMin ) / SpanYou need two things to be true at once:
Pseudocode — not valid AFL
fraction( DataMin ) = BandBottomfraction( DataMax ) = BandTopSubtract the first from the second and ScaleMin cancels, leaving Span. Substitute back and
you get ScaleMin:
Pseudocode — not valid AFL
Span = ( DataMax - DataMin ) / ( BandTop - BandBottom )ScaleMin = DataMin - BandBottom * SpanThat is two lines of arithmetic, and it is the entire mechanism. Every band in the panel — the
data series, the reference lines, the zero line — is drawn by passing its own DataMin,
DataMax, BandBottom and BandTop through those two lines and into a styleOwnScale plot.
One pane, three bands
- Trend band — 0.70 to 1.00Distance from a long moving average, measured in ATR units and clipped to ±4. Histogram, coloured by sign, with a zero line.
- Momentum band — 0.36 to 0.64RSI on its native 0–100 scale, with dashed reference lines at 30 and 70 and a solid one at 50.
- Volatility band — 0.00 to 0.28PercentRank of ATR-as-a-percentage-of-price over its own recent history, 0–100, with 20 and 80 marked.
The gaps between the bands are deliberate. 0.64 to 0.70 and 0.28 to 0.36 are empty
space, and empty space is what stops a spike in one band from being mistaken for part of the
one below it.
Why the three readings, and why they are not added together
Section titled “Why the three readings, and why they are not added together”The panel shows trend, momentum and volatility because those are three genuinely different questions:
- Where is price relative to where it has been? — a trend question
- How has it been getting there? — a momentum question
- How large are the moves, by this instrument’s own standards? — a volatility question
What the panel deliberately does not do is combine them into a score.
That is why the non-compact title ends with a sentence stating it. A tool that could be misread should say what it is out loud, on screen, where the misreading would happen.
The complete formula
Section titled “The complete formula”Complete runnable AFL
/* * Multi-indicator analysis panel - Part 10 project. * * What it does * Puts three separate readings in one pane, each in its own horizontal band: * top - trend: how far price sits from a long average, in ATR units; * middle - momentum: RSI, with its 30/50/70 reference lines; * bottom - volatility: where ATR-as-a-percentage-of-price sits inside its * own recent history. * * What it deliberately does not do * It does not add the three readings together, score them, or call their * agreement "confirmation". All three are transformations of the same close * series, so they agree with each other for arithmetic reasons far more often * than three independent measurements would. The panel puts them side by side * so you can see the disagreement, which is the part that carries information. * * Assumptions and limits * - Insert as its OWN pane, below the price chart. * - Nothing here is a signal, a score or a forecast. * - RSI is Null for its first periods bars; ATR and the averages are * seed-contaminated for longer than that. Read the left edge of the chart * with suspicion. */
_SECTION_BEGIN( "Analysis panel" );
TrendPeriod = Param( "Trend average periods", 100, 20, 400, 5 );AtrPeriod = Param( "ATR periods", 20, 2, 100, 1 );RsiPeriod = Param( "RSI periods", 14, 2, 100, 1 );RankPeriod = Param( "Volatility look-back (bars)", 252, 20, 1000, 1 );StretchCap = Param( "Trend band limit (ATR units)", 4, 1, 12, 0.5 );Compact = ParamToggle( "Compact (small screens)", "No|Yes", 0 );
UpTint = ParamColor( "Positive colour", colorBrightGreen );DownTint = ParamColor( "Negative colour", colorRed );QuietTint = ParamColor( "Neutral colour", colorBlueGrey );GuideTint = ParamColor( "Guide line colour", colorLightGrey );
/* * Draws one series inside a horizontal slice of the pane. * * minvalue and maxvalue are documented as being used by styleOwnScale plots * only, and they are the sole control AFL gives over a plot's own Y range. So a * band is produced by choosing bounds that place DataMin at BandBottom and * DataMax at BandTop, where both band figures are fractions of the pane height * running 0 at the foot to 1 at the top. * * With scale bounds ScaleMin..ScaleMin+Span, a value v sits at the pane * fraction ( v - ScaleMin ) / Span. Substituting v = DataMin gives BandBottom * and v = DataMax gives BandTop, which is the whole trick. */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;}
// ---------------------------------------------------------------- trend band
TrendLine = MA( Close, TrendPeriod );Volatility = ATR( AtrPeriod );
// Distance from the average measured in ATR units, so the number means the same// thing on any instrument, then clipped so one extreme bar cannot squash the// rest of the band flat.Stretch = ( Close - TrendLine ) / Volatility;Stretch = Max( Min( Stretch, StretchCap ), -StretchCap );
StretchTint = IIf( Stretch > 0, UpTint, DownTint );
PlotInBand( 0, "", GuideTint, styleLine | styleNoTitle, -StretchCap, StretchCap, 0.70, 1.00 );
PlotInBand( Stretch, "Distance from trend (ATR units)", StretchTint, styleHistogram | styleThick, -StretchCap, StretchCap, 0.70, 1.00 );
// ------------------------------------------------------------- momentum band
Momentum = RSI( RsiPeriod );
PlotInBand( 30, "", GuideTint, styleLine | styleDashed | styleNoTitle, 0, 100, 0.36, 0.64 );PlotInBand( 50, "", GuideTint, styleLine | styleNoTitle, 0, 100, 0.36, 0.64 );PlotInBand( 70, "", GuideTint, styleLine | styleDashed | styleNoTitle, 0, 100, 0.36, 0.64 );
PlotInBand( Momentum, "RSI " + NumToStr( RsiPeriod, 1.0 ), QuietTint, styleLine | styleThick, 0, 100, 0.36, 0.64 );
// ----------------------------------------------------------- volatility band
AtrPercent = 100 * ATR( AtrPeriod ) / Close;Rank = PercentRank( AtrPercent, RankPeriod );
RankTint = IIf( Rank >= 80, DownTint, IIf( Rank <= 20, UpTint, QuietTint ) );
PlotInBand( 20, "", GuideTint, styleLine | styleDashed | styleNoTitle, 0, 100, 0.00, 0.28 );PlotInBand( 80, "", GuideTint, styleLine | styleDashed | styleNoTitle, 0, 100, 0.00, 0.28 );
PlotInBand( Rank, "Volatility percentile", RankTint, styleHistogram, 0, 100, 0.00, 0.28 );
// ------------------------------------------------------------------- reading
SetChartOptions( 2, chartWrapTitle );
TrendWord = WriteIf( Stretch > 0.5, "above trend", WriteIf( Stretch < -0.5, "below trend", "at trend" ) );
VolatilityWord = WriteIf( Rank >= 80, "wide range", WriteIf( Rank <= 20, "narrow range", "ordinary range" ) );
if( Compact ) _N( Title = StrFormat( "%s %s RSI %g vol pct %g", Name(), TrendWord, SelectedValue( Momentum ), SelectedValue( Rank ) ) );else _N( Title = StrFormat( "%s trend: %s (%g ATR) momentum: RSI(%g) = %g volatility: %s, percentile %g\n" + "Three views of one price series. Agreement between them is not independent evidence.", Name(), TrendWord, SelectedValue( Stretch ), RsiPeriod, SelectedValue( Momentum ), VolatilityWord, SelectedValue( Rank ) ) );
_SECTION_END();How it works
Section titled “How it works”PlotInBand()
Section titled “PlotInBand()”The only function in the file. It takes the series, its name, its colour, its style bits, the
data range you want mapped, and the band you want it mapped into. It computes Span and
ScaleMin from the four numbers, calls Plot() with styleOwnScale | styleNoLabel added to
whatever style you passed, and returns Span so a caller could reuse it.
local Span, ScaleMin; matters. Without it, a variable assigned inside a function is visible
outside it, and two calls would interfere. Part 11 takes scope apart properly; here, just note
that a reusable function that does not declare its locals is a bug waiting for its second
caller.
The trend band
Section titled “The trend band”Fragment — not a complete formula
Stretch = ( Close - TrendLine ) / Volatility;Stretch = Max( Min( Stretch, StretchCap ), -StretchCap );Dividing the distance from the average by ATR is what makes the number portable. “Two dollars above the average” means nothing without knowing the instrument; “two ATRs above the average” means the same thing on a $4 stock and a $400 one, which is what lets you compare two symbols by eye.
The clip is a display decision, not an analytical one. One extreme bar with a Stretch of 20
would force the band’s scale to cover ±20 and flatten every other bar to a stub. Clipping at
±4 keeps the band readable, and the cost — that you cannot tell 4 from 12 — is acceptable in a
panel whose job is orientation. Say so in the parameter name, which is why it is called
“Trend band limit” rather than something that implies the data was changed for analytical
reasons.
The zero line is plotted first, with the same bounds, so the histogram is drawn over it.
The momentum band
Section titled “The momentum band”RSI already lives on 0 to 100, so the band bounds are the natural ones. The three reference
lines are constants pushed through PlotInBand(): 30 and 70 dashed, 50 solid. The 50 line is
the one that actually matters — it is the midpoint of the oscillator — and it is drawn solid
for that reason.
The volatility band
Section titled “The volatility band”Fragment — not a complete formula
AtrPercent = 100 * ATR( AtrPeriod ) / Close;Rank = PercentRank( AtrPercent, RankPeriod );Two normalisations stacked. Dividing ATR by price makes it comparable across instruments;
PercentRank( array, range ) then reports where the current value sits within its own last
range values, on a 0–100 scale. So a reading of 90 means “wider range than 90% of the last
252 bars for this symbol” — a statement that needs no knowledge of the instrument to
interpret.
PercentRank needs its full lookback before it means anything. With the default of 252 bars,
the first year of any chart is telling you about a shorter and shorter window, and the very
left edge is meaningless. That is the same warm-up problem as every other lookback function,
and the header comment says so.
The title
Section titled “The title”SetChartOptions( 2, chartWrapTitle ) uses mode 2 — set flag — which ORs chartWrapTitle into
the pane’s existing options rather than overwriting them. That is the polite mode: it turns on
title wrapping without silently resetting anything the user configured in the Parameters
dialog.
The Compact toggle exists because the full title is two lines and will wrap to four or five
on a phone. _N() wraps the title assignment so AmiBroker does not try to interpret the
embedded formatting as something to evaluate.
What you should see
Section titled “What you should see”Test it
Section titled “Test it”Verify the band mapping is actually doing what you think
Section titled “Verify the band mapping is actually doing what you think”This is the part worth checking, because if the arithmetic is wrong the panel still looks plausible.
Temporarily add a plot of the constant 100 into the momentum band:
Fragment — not a complete formula
PlotInBand( 100, "top of momentum band", colorRed, styleLine, 0, 100, 0.36, 0.64 );It must land exactly on the top edge of the momentum band — at 64% of the pane height, just
below the gap. Do the same with 0; it must land on the bottom edge at 36%. If either misses,
Span or ScaleMin is wrong. Remove both when you are satisfied.
Verify the ATR normalisation
Section titled “Verify the ATR normalisation”Pick a bar and read the title’s ATR-unit figure. Then confirm it by hand: hover the price chart to get the close, subtract the moving-average value, divide by an ATR reading from a separate ATR pane. Within rounding, the numbers must agree. If your stretch figure is ten times too large, you divided by the wrong thing.
Verify the percentile means what the label says
Section titled “Verify the percentile means what the label says”Set RankPeriod to a small number — 20 — and watch the bottom band. It should now spend far
more time at the extremes, because a 20-bar window is easy to be the widest bar in. Set it to
1000 and it should become much steadier. If it does not respond to the parameter at all, the
parameter is not reaching PercentRank().
Confirm the warm-up
Section titled “Confirm the warm-up”Scroll to the extreme left edge of the chart. The volatility band should be empty or erratic
for the first RankPeriod bars, and the trend band empty for the first TrendPeriod. If
something is drawn there, you are looking at seeded values, not at data.
Common errors
Section titled “Common errors”Everything collapses into one flat line. styleOwnScale is missing from one of the plots.
Without it, minvalue/maxvalue are ignored — the documentation is explicit that they apply
to own-scale plots only — and that plot joins the pane’s shared axis, dragging the axis to
cover its full range.
The bands are in the wrong order vertically. BandBottom and BandTop are pane fractions
with 0 at the foot. Passing 0.70, 1.00 puts a band at the top. Swapping them gives a
negative Span and an upside-down plot.
Reference lines float away from their band. They were plotted with different DataMin/
DataMax values than the series they belong to. Every element of a band must use the same four
numbers.
The title shows values from the wrong bar. SelectedValue() reads the bar under the
crosshair or, with nothing selected, the last bar. If you expected a different bar, that is the
reason.
One band’s colour array does not change. IIf() builds the colour array element by
element; if you wrote a scalar comparison by accident — IIf( LastValue( Stretch ) > 0, … ) —
every bar gets the same colour.
The panel is unreadable on a phone. Turn on the Compact toggle. If it is still cramped, reduce to two bands rather than shrinking the type: three bands need roughly 200 pixels of pane height to be legible.
Extensions
Section titled “Extensions”Attempt these in order; each one is a genuine addition rather than a decoration.
-
Make the band layout a parameter. Add a
ParamListwith “Three bands”, “Trend and momentum” and “Momentum only”, and compute the band boundaries from the choice. This forces you to stop hard-coding0.70and0.36and is a real refactor. -
Add a fourth band showing relative strength against a benchmark, using
Foreign(). Part 13 builds the ratio properly. Note the new dependency: the panel now fails on a database without the benchmark symbol, so it needs a guard — which is exactly what the defensive AFL lesson is about. -
Show the disagreement explicitly. Add a title clause that fires only when the three readings diverge — for example price above trend while RSI is below 50. Resist the urge to turn it into a signal; the value is that it names a state you would otherwise scroll past.
-
Move
PlotInBand()into an include file so the next panel you build can use it. That is the personal library project, and this function is a good candidate for its first entry.
What changed
Section titled “What changed”You now know the one lever AFL gives you over an individual plot’s vertical scale, and the two-line arithmetic that turns it into arbitrary band placement. You have a reusable function rather than three hard-coded scale calculations. And you have built a tool that presents three views of one price series without pretending they are three independent witnesses — which is a design decision, visible in the code and stated in the title, not an accident.
Check your understanding
Sources for this lesson
6 verified · checked 2026-08-31
- 01AFL Function Reference — Plot§ minvalue and maxvalue (used by styleOwnScale plots ONLY)amibroker.com/guide/afl/plot.html2026-08-31
- 02AFL Function Reference — PercentRankamibroker.com/guide/afl/percentrank.html2026-08-31
- 03AFL Function Reference — SetChartOptionsamibroker.com/guide/afl/setchartoptions.html2026-08-31
- 04AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
- 05AFL Function Reference — RSIamibroker.com/guide/afl/rsi.html2026-08-31
- 06AmiBroker User's Guide — User-defined functionsamibroker.com/guide/a_userfunctions.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.