Project: ATR Volatility Indicator
Volatility is the measurement most often used and least often defined. “It has been volatile lately” means nothing until you say volatile compared with what, in what units, over what window. This project builds an indicator that answers all three, and that carries its own proof of correctness on the chart.
Objective
Section titled “Objective”A single pane that shows:
- average true range in the instrument’s own price units, because that is the number that goes into a stop distance or a position size;
- average true range as a percentage of price, because that is the number you can compare across instruments;
- where today’s reading sits inside its own recent history, because “wide” and “narrow” are only meaningful relative to a distribution;
- a switchable validation trace that must be a flat line at zero if the arithmetic is right.
Prerequisites
Section titled “Prerequisites”The four lessons of this part. From earlier parts: Ref(), IIf(), and the idea that an
indicator has a warm-up period. Any daily data will do, but the percentile section wants at
least two years so the lookback window is genuinely populated.
Two units, two purposes
Section titled “Two units, two purposes”Average true range comes out of AmiBroker in price units. On a share trading at £42.10, an
ATR( 14 ) of 0.84 means a typical recent bar spanned 84 pence including gaps.
That number is exactly what you want for arithmetic on the instrument itself: a stop two ATR away is £1.68 away; a position sized to risk £500 at that stop is about 297 shares. Nothing about that calculation is improved by normalising.
It is exactly what you do not want for comparison. Is an ATR of 0.84 high? Compared to a £42 share, that is 2.0% of price. Compared to an index at 7,600 with an ATR of 61, which is 0.8% of price, the share is more than twice as volatile in the only sense that transfers.
So the indicator computes both:
Fragment — not a complete formula
AtrPoints = ATR( AtrPeriod );AtrPercent = 100 * AtrPoints / Close;Why percent of price is not the whole answer either
Section titled “Why percent of price is not the whole answer either”Dividing by Close makes two instruments comparable at a moment in time. It does not make one
instrument comparable with itself across years, because the same 2.0% reading can be
historically calm for one market and historically extreme for another — and can be calm in one
decade and extreme in the next for the same market.
That is what the third measurement is for.
Ranking a reading against its own history
Section titled “Ranking a reading against its own history”Fragment — not a complete formula
Rank = PercentRank( AtrPercent, RankPeriod );PercentRank( array, range ) returns a number from 0 to 100: the percentage of the lookback
window that the current value exceeds. 100 means today is the widest reading in the window; 0
means it is the narrowest. With RankPeriod set to 252 — roughly a year of trading days — a
rank of 15 says “narrower than about 85% of the last year”.
The same ATR reading, ranked differently
| Bar | calm year | wild year |
|---|---|---|
ATR as % of price today | 2.0 | 2.0 |
Median of the last 252 bars | 1.2 | 3.4 |
PercentRank | ~90 | ~20 |
Reads as | unusually wide | unusually narrow |
That is the whole argument for the percentile: it converts an absolute measurement into a statement about this instrument’s own recent distribution, which is the comparison a reader is actually making in their head when they say “quiet”.
Contraction and expansion
Section titled “Contraction and expansion”With the rank in hand, “contraction” stops being an impression:
Fragment — not a complete formula
Quiet = Rank <= QuietLevel; // default 20Loud = Rank >= LoudLevel; // default 80Those thresholds are choices, and the tool makes them adjustable so you can see how much the
picture depends on them. The two dashed reference lines on the chart are not the thresholds
themselves but the ATR-percentage values at those ranks, produced by Percentile(), so the
lines move as the instrument’s own history moves. A reader can therefore see both the reading
and the distribution it is being judged against.
The complete formula
Section titled “The complete formula”Complete runnable AFL
/* * ATR volatility indicator - Part 10 project. * * What it does * Shows recent volatility three ways in one pane: * - ATR in the instrument's own price units, for stop and size arithmetic; * - ATR as a percentage of price, so two instruments can be compared; * - where today's ATR percentage sits inside its own recent history, * which is what makes "quiet" and "loud" mean something specific. * * Assumptions and limits * - Insert as its OWN pane, not as a price overlay. * - ATR uses Wilder's smoothing, not a simple average, so MA( ATR(1), n ) * will not reproduce ATR( n ). * - ATR has no documented default period. It is always passed explicitly. * - A contraction reading says only that the recent range has been narrow * relative to this instrument's own history. It is a description of past * bars, not a signal, and it carries no claim about what follows. * - Percentile and PercentRank both re-sort their window on every bar. On a * long look-back across a large universe they are noticeably slow. */
_SECTION_BEGIN( "ATR volatility" );
AtrPeriod = Param( "ATR periods", 14, 1, 200, 1 );RankPeriod = Param( "History look-back (bars)", 252, 20, 1000, 1 );QuietLevel = Param( "Contraction at or below percentile", 20, 1, 50, 1 );LoudLevel = Param( "Expansion at or above percentile", 80, 50, 99, 1 );ShowPoints = ParamToggle( "Overlay ATR in price units", "No|Yes", 1 );ShowCheck = ParamToggle( "Validation trace", "Off|On", 0 );
QuietTint = ParamColor( "Contraction colour", colorTeal );LoudTint = ParamColor( "Expansion colour", colorOrange );NormalTint = ParamColor( "Normal colour", colorBlueGrey );
// ATR(1) is the documented way to obtain the true range of a single bar.BarTrueRange = ATR( 1 );
AtrPoints = ATR( AtrPeriod );AtrPercent = 100 * AtrPoints / Close;
// PercentRank returns 0..100: the percentage of the look-back window that the// current value exceeds. 100 means the widest reading in the window.Rank = PercentRank( AtrPercent, RankPeriod );
Quiet = Rank <= QuietLevel;Loud = Rank >= LoudLevel;
BarTint = IIf( Quiet, QuietTint, IIf( Loud, LoudTint, NormalTint ) );
Plot( AtrPercent, "ATR " + NumToStr( AtrPeriod, 1.0 ) + " as % of price", BarTint, styleHistogram | styleThick );
// The two threshold lines are the actual ATR-percentage values at those// percentile ranks, so they move as the instrument's own history moves.// styleNoRescale keeps them from stretching the pane when they run away.Plot( Percentile( AtrPercent, RankPeriod, QuietLevel ), "Contraction level", QuietTint, styleLine | styleDashed | styleNoRescale );
Plot( Percentile( AtrPercent, RankPeriod, LoudLevel ), "Expansion level", LoudTint, styleLine | styleDashed | styleNoRescale );
// The same volatility in price units, on its own scale, for the arithmetic you// actually do with it: stop distances and position sizes.if( ShowPoints ) Plot( AtrPoints, "ATR " + NumToStr( AtrPeriod, 1.0 ) + " in points", colorGrey40, styleLine | styleOwnScale | styleNoLabel );
/* * Validation trace. True range is defined as the largest of * high - low, |high - previous close| and |low - previous close|. * If ATR(1) really is that quantity, the difference below is zero on every bar * except the first, where there is no previous close. Anything else means the * assumption behind this whole indicator is wrong, and the flat line at zero * is what tells you it is not. */ManualTrueRange = Max( High - Low, Max( abs( High - Ref( Close, -1 ) ), abs( Low - Ref( Close, -1 ) ) ) );
if( ShowCheck ) Plot( ManualTrueRange - BarTrueRange, "Check (should be flat zero)", colorRed, styleLine | styleOwnScale | styleNoLabel );
_N( Title = StrFormat( "%s ATR(%g) = %g points = %g%% of price percentile rank %g of the last %g bars", Name(), AtrPeriod, SelectedValue( AtrPoints ), SelectedValue( AtrPercent ), SelectedValue( Rank ), RankPeriod ) );
_SECTION_END();How it works
Section titled “How it works”The three series
Section titled “The three series”BarTrueRange is ATR( 1 ) — the true range of a single bar. This is the documented way to
obtain true range in AFL; there is no TrueRange() function in the official reference, and
the ATR page itself gives ATR(1) as the recipe for building your own non-Wilder averages such
as MA( ATR(1), period ).
AtrPoints is the smoothed version at the user’s period, and AtrPercent divides it by
Close.
Rank is the percentile rank of AtrPercent inside its own lookback window.
The colouring and the levels
Section titled “The colouring and the levels”BarTint is a nested IIf() producing one colour per bar: the quiet colour when the rank is at
or below the low threshold, the loud colour at or above the high one, neutral in between. The
histogram is drawn with that array, so the classification is visible at a glance while the
underlying value is still readable on the axis.
The two Percentile() plots carry styleNoRescale, which keeps them out of the pane’s
autoscaling. Without it, a single extreme value in the expansion line could compress the
histogram into an unreadable strip along the bottom.
The optional points overlay uses styleOwnScale, because ATR in price units and ATR in percent
share a pane but not a range. It carries styleNoLabel too, so the axis is not competing with
itself.
The validation trace
Section titled “The validation trace”Fragment — not a complete formula
ManualTrueRange = Max( High - Low, Max( abs( High - Ref( Close, -1 ) ), abs( Low - Ref( Close, -1 ) ) ) );True range is defined as the largest of three quantities: the bar’s own high-to-low range, the
distance from the previous close up to this high, and the distance from the previous close down
to this low. Max() in AFL takes two arguments, so three quantities need two nested calls.
Plotting ManualTrueRange - BarTrueRange gives a line that is zero on every bar where the two
agree. That is the test, and it is built into the tool rather than described in a paragraph
someone will not read.
What you should see
Section titled “What you should see”Insert the formula as its own pane, below the price chart.
A histogram of ATR as a percentage of price, coloured teal where the rank is at or below 20,
orange where it is at or above 80, and grey-blue in between. Two dashed lines threading through
it at the contraction and expansion levels. A faint grey line on its own scale tracing ATR in
points, which will look similar in shape but not identical — the difference between them is
entirely the movement of Close.
The title reports all three numbers at the selected bar.
Turning Validation trace on adds a red line. It should be indistinguishable from flat, at zero, across the whole chart except possibly at the very first bar.
Test it
Section titled “Test it”Validate the arithmetic by hand
Section titled “Validate the arithmetic by hand”This is the test that matters, and it takes two minutes.
- Turn Validation trace on. The red line must be flat at zero. If it is not, either your understanding of true range or AmiBroker’s implementation differs from the definition above, and you need to know which before you use the number for anything.
- Pick a single bar with a visible gap. Open the Data window and read its high, its low and
the previous bar’s close. Compute the three candidate ranges on paper and take the
largest. Compare with
ATR( 1 )for that bar. They should match. - Set ATR periods to 1 and confirm the main histogram becomes that single bar’s true range as a percentage of close.
Confirm ATR is not a simple average
Section titled “Confirm ATR is not a simple average”- Add a temporary plot of
MA( ATR( 1 ), 14 )alongsideATR( 14 ). They will not coincide. This is expected and documented: AmiBroker’s ATR uses Wilder’s smoothing, not a simple moving average, as stated by AmiBroker’s author on the ATR page. Anyone reconciling AFL against a spreadsheet that averages true ranges arithmetically will find exactly this discrepancy and conclude, wrongly, that one of them is broken.
Confirm the normalisation does its job
Section titled “Confirm the normalisation does its job”- Apply the unchanged formula to three instruments with very different price levels. The ATR percentage should be in a comparable range on all three — typically a fraction of one per cent to a few per cent on daily bars — while the points figure differs by orders of magnitude.
- Apply it to an instrument that had a stock split in the loaded history, if your data is not split-adjusted. The points series will show a step; the percentage series should not. That is a useful data-quality check in its own right.
Probe the percentile
Section titled “Probe the percentile”- Set the lookback to 20. The rank becomes twitchy and reaches 0 and 100 constantly, because twenty observations do not describe a distribution. Set it to 500 and the rank becomes slow to acknowledge a genuine regime shift in volatility. Somewhere in between is a judgement, and the tool should make you feel the trade-off rather than hide it.
- Check the left edge. For the first
RankPeriodbars the window is not full. Treat those ranks with suspicion; the official pages do not document the warm-up behaviour of either ranking function.
Common errors
Section titled “Common errors”ATR()with no argument. Not documented. Always give the period.- Expecting
TrueRange()to exist. It does not — the page returns 404 and it is absent from both official indexes. UseATR( 1 ). Percentilegiven a rank of 0.2 instead of 20. The rank argument runs 0 to 100. The symptom is a reference line pinned to the very bottom of the pane.- Confusing
PercentilewithPercentRank. One returns a value, the other a rank. If your “percentile line” is on a 0–100 scale while the histogram is on a 0–5 scale, you have them the wrong way round. - The overlay squashes the histogram. The points series needs
styleOwnScale; without it, a series in tens of points shares a scale with one in single-digit percentages. - The percentage series spikes absurdly.
Closeapproached zero, or the data has a bad bar. Check the underlying data before believing the indicator. - The validation line is not flat at the first bar. Expected: there is no previous close for bar 0, so the manual calculation is undefined there.
Extensions
Section titled “Extensions”A second volatility estimator. Add StDev( ROC( Close, 1 ), 20 ) — the standard deviation
of one-bar percentage returns — on its own scale, and compare its shape with the ATR percentage.
They measure related but different things: ATR includes gaps and uses the full bar range, while
a close-to-close standard deviation ignores everything that happened inside the bar. Where they
disagree is where the intraday range and the closing move told different stories. Remember that
StDev’s third argument defaults to True, the population form.
Annualise it. Multiply a daily percentage volatility by the square root of the number of trading periods in a year to get a figure comparable with quoted volatilities. Then be careful: that scaling assumes independent, identically distributed returns, and the whole reason volatility ranking is interesting is that returns are not independent in that way. Doing the calculation and then stating its assumption is the exercise.
A contraction counter. Report in the title how many consecutive bars the rank has been below
the quiet threshold, using BarsSince(). Then compare that count with its own history: is a
six-bar contraction ordinary or unusual for this instrument? A count without a base rate is
just a number.
Export the readings. Part 12’s Exploration lets you produce a table of every symbol’s
current volatility rank, which turns this pane into a screening tool. That is the natural next
step, and it is also the point at which you should start caring about how slow Percentile is.
What changed
Section titled “What changed”You can now say precisely what you mean by volatile: which measurement, in which units, over which window, relative to which distribution. You also have a habit worth carrying into every indicator you write from here on — building the validation into the tool, as a line that must be flat, rather than trusting that the arithmetic was right when you typed it.
Check your understanding
Sources for this lesson
6 verified · checked 2026-08-31
- 01AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
- 02AFL Function Reference — PercentRankamibroker.com/guide/afl/percentrank.html2026-08-31
- 03AFL Function Reference — Percentileamibroker.com/guide/afl/percentile.html2026-08-31
- 04AFL Function Reference — StDevamibroker.com/guide/afl/stdev.html2026-08-31
- 05AFL Function Reference — Maxamibroker.com/guide/afl/max.html2026-08-31
- 06AFL Function Reference — Plotamibroker.com/guide/afl/plot.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.