Skip to content
Level 3 · AFL DeveloperProjectPart 10 · page 7 of 845 min
45Minutes
11AFL functions
6Sources
StandardRequires
AFL functions taught here11

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.

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.

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.

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.

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

Identical raw volatility, opposite descriptions. The rank is the part that carries the meaning.
Barcalm yearwild year
ATR as % of price today2.02.0
Median of the last 252 bars1.23.4
PercentRank~90~20
Reads asunusually wideunusually narrow
Identical raw volatility, opposite descriptions. The rank is the part that carries the meaning.

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”.

With the rank in hand, “contraction” stops being an impression:

Fragment — not a complete formula

Quiet = Rank <= QuietLevel; // default 20
Loud = Rank >= LoudLevel; // default 80

Those 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.

Complete runnable AFL

atr-volatility.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();

Download atr-volatility.afl91 lines

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.

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.

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.

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.

This is the test that matters, and it takes two minutes.

  1. 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.
  2. 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.
  3. Set ATR periods to 1 and confirm the main histogram becomes that single bar’s true range as a percentage of close.
  1. Add a temporary plot of MA( ATR( 1 ), 14 ) alongside ATR( 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.
  1. 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.
  2. 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.
  1. 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.
  2. Check the left edge. For the first RankPeriod bars the window is not full. Treat those ranks with suspicion; the official pages do not document the warm-up behaviour of either ranking function.
  • 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. Use ATR( 1 ).
  • Percentile given 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 Percentile with PercentRank. 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. Close approached 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.

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.

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

Question 1. Which is the documented way to obtain a single bar’s true range in AFL?
Show the answer and why

Answer: ATR( 1 )

There is no TrueRange() function in the official reference. The ATR page gives ATR(1) as the true range of the bar, and as the building block for non-Wilder averages such as MA( ATR(1), period ).

Question 2. What does PercentRank( AtrPercent, 252 ) return?
Show the answer and why

Answer: A number from 0 to 100 saying what proportion of the last 252 bars the current value exceeds

PercentRank returns 0..100: 100 means the current element is the highest in the lookback range, 0 the lowest. Percentile is the function that returns a value at a given rank.

Question 3. MA( ATR( 1 ), 14 ) and ATR( 14 ) produce different lines. Why?
Show the answer and why

Answer: ATR uses Wilder’s smoothing rather than a simple moving average

AmiBroker’s author states on the ATR page that the original formulation, which AmiBroker implements, uses Wilder’s smoothing and not a simple moving average. The two are genuinely different calculations.

Question 4. Which of these are good reasons to normalise ATR by price? Select all that apply.
Show the answer and why

Answer: To compare volatility between instruments with very different price levels, To make a series robust to an unadjusted split in the data

Normalising makes cross-instrument comparison meaningful and makes the series immune to a price-level step such as an unadjusted split. Stop distances need the raw points figure, and normalising has no effect on warm-up.

Question 5. The validation trace is a flat line at zero. What has that established?
Show the answer and why

Answer: That ATR( 1 ) matches the textbook definition of true range on this data

It checks one specific claim - that ATR(1) equals the largest of the three candidate ranges - and nothing else. A good test establishes something narrow and checkable.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
  2. 02AFL Function Reference — PercentRankamibroker.com/guide/afl/percentrank.html2026-08-31
  3. 03AFL Function Reference — Percentileamibroker.com/guide/afl/percentile.html2026-08-31
  4. 04AFL Function Reference — StDevamibroker.com/guide/afl/stdev.html2026-08-31
  5. 05AFL Function Reference — Maxamibroker.com/guide/afl/max.html2026-08-31
  6. 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.