Skip to content
Level 1 · Chart ReaderLessonPart 05 · page 4 of 622 min
22Minutes
10AFL functions
8Sources
StandardRequires
AFL functions taught here10

Consolidation and Volatility Contraction

“The market is consolidating” is a description dressed as an observation. By the end of this lesson you should be able to replace it with two numbers that another person could compute from the same data and get the same answer — and to say precisely what those numbers licence you to conclude, which is less than most people assume.

Four different things travel under the same word, and they do not always occur together.

Sideways drift. Price is going neither up nor down in any sustained way: the trend component is small relative to the noise.

A narrowing range. The distance between recent highs and recent lows is shrinking, so successive bars occupy less vertical space.

Falling volatility. The size of a typical bar is decreasing, whether or not the overall range has narrowed.

Falling participation. Volume is declining, which is often bundled into the idea but is a separate measurement about a separate quantity.

You can have a narrowing range in a market that is still moving decisively — a wedge, in the usual vocabulary. You can have falling bar-by-bar volatility while price grinds steadily higher. Since these come apart, a lesson that treats them as one thing will produce confusion later; so pick the ones you care about and measure them separately.

The two most useful, because they are the easiest to make objective, are range width and volatility contraction.

The raw measurement is simple: over the last N bars, how far apart are the highest high and the lowest low?

Fragment — not a complete formula

RangeHigh = HHV( High, 20 );
RangeLow = LLV( Low, 20 );
Width = RangeHigh - RangeLow;

That number is in price units, which makes it useless for comparison. A 4-unit range means one thing on a 20-unit stock and another on a 400-unit one. Dividing by price fixes the scale problem:

Fragment — not a complete formula

// SafeDivide returns the third argument instead of Null when the divisor is zero,
// so one bad bar with a zero close does not empty the whole array.
WidthPct = 100 * SafeDivide( RangeHigh - RangeLow, Close, 0 );

Now WidthPct is a percentage, comparable across instruments. It still is not comparable across regimes: 6% is a tight twenty-day range for a small-cap technology share and a wide one for a large utility. The fix for that is to compare each instrument with itself.

PercentRank(array, range) reports, on a 0 to 100 scale, where the current value of an array sits within its own last range values. A reading of 0 means this is the lowest value in the lookback; 100 means the highest.

Fragment — not a complete formula

// "Tighter than 90% of the last year of readings" is a statement that means the
// same thing on any instrument.
WidthRank = PercentRank( WidthPct, 250 );
IsTight = WidthRank < 10;

From a price range to a comparable reading

Only the last row is a statement about this instrument's own history rather than about its price level.
Bart-3t-2t-1t
HHV(High,20) - LLV(Low,20)8.407.906.205.10
Close104.0103.1102.6102.9
WidthPct8.087.666.044.96
PercentRank(WidthPct, 250)4638198
IsTight (rank below 10)0001
Only the last row is a statement about this instrument's own history rather than about its price level. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Range width asks how much ground price has covered. Contraction asks whether the typical bar is getting smaller, which is a slightly different question and often moves first.

The standard measurement is a ratio of two volatility estimates over different windows:

Fragment — not a complete formula

// Below 1 means recent bars are smaller than the longer-run norm.
Compression = SafeDivide( ATR( 10 ), ATR( 50 ), 1 );

ATR — average true range — is covered properly in Part 6, including why true range rather than high-minus-low is the right base and why AmiBroker uses Wilder’s smoothing. For now, read ATR(10) as “the size of a typical bar over the last ten bars”.

Two alternatives you will meet, both legitimate:

  • Standard deviation of returns over the window, using StDev. Closer to the statistical definition of volatility, blind to gaps and intraday range.
  • Bollinger band width: the distance between the upper and lower bands as a fraction of the middle band. Since the bands are built from StDev, this is a repackaging of the previous option rather than an independent measurement, which is worth knowing before you present them as two pieces of agreeing evidence.

Expansion is the mirror image: Compression rising above 1, or WidthRank climbing back towards 100. Nothing new is needed to measure it.

Two built-in single-bar markers are conventionally associated with the two states, and both have exact documented definitions:

  • Inside() is true when today’s high is below yesterday’s high and today’s low is above yesterday’s low. A run of inside bars is the smallest visible form of contraction.
  • Outside() is true on an outside bar, whose range engulfs the previous bar’s.

They are cheap, precise and available with no parameters at all, which makes them useful as a cross-check on the parametrised measurements above. If your contraction meter says the market is tight and there has not been an inside bar in six weeks, one of the two is describing something other than what you think.

The formula below computes both measurements, converts both to percentiles of their own history, and requires the two to agree before a bar is called tight.

Complete runnable AFL

consolidation-meter.afl
// consolidation-meter.afl
// Part 5 - Consolidation and Volatility Contraction
//
// Turns "the market is consolidating" into two numbers you could sort a whole
// watchlist on:
// Width - the N-bar high-to-low range as a percentage of price;
// Compression - a short-window ATR divided by a long-window ATR.
// Both are then reported as a PERCENTILE of their own recent history, because a
// 4% range is tight for one instrument and wide for another, and neither number
// means anything until it is compared with that instrument's own past.
//
// Apply this in its OWN pane. It is a ratio, not a price.
//
// Assumptions:
// - daily bars;
// - at least SlowAtr + RankLen bars of history, otherwise the early bars are
// warm-up and the percentiles are computed from too little data;
// - a tight reading says the recent range is small relative to this
// instrument's own history. It says nothing at all about direction.
_SECTION_BEGIN("Consolidation meter");
RangeLen = Param( "Range window (bars)", 20, 5, 120, 1 );
FastAtr = Param( "Fast ATR period", 10, 2, 50, 1 );
SlowAtr = Param( "Slow ATR period", 50, 10, 250, 5 );
RankLen = Param( "Percentile lookback (bars)", 250, 50, 1000, 10 );
TightPct = Param( "Tight threshold (percentile)", 20, 1, 50, 1 );
RangeHigh = HHV( High, RangeLen );
RangeLow = LLV( Low, RangeLen );
// SafeDivide stops a zero or missing close from turning the whole array Null
// and silently emptying the pane.
Width = 100 * SafeDivide( RangeHigh - RangeLow, Close, 0 );
Compression = SafeDivide( ATR( FastAtr ), ATR( SlowAtr ), 1 );
// PercentRank reports where the current value sits within its own last RankLen
// values, on a 0-100 scale. 0 means the lowest in that window, 100 the highest.
WidthRank = PercentRank( Width, RankLen );
CompressionRank = PercentRank( Compression, RankLen );
// Both measures have to agree before a bar counts as tight. Requiring agreement
// between two different measurements of the same idea is cheap insurance
// against one of them being an artefact of its own window length.
IsTight = WidthRank < TightPct AND CompressionRank < TightPct;
// Colour carries no information that the histogram height does not already
// carry, so a reader who cannot distinguish the two colours loses nothing.
MeterColor = IIf( IsTight, colorOrange, colorBlueGrey );
Plot( WidthRank, "Range width percentile", MeterColor,
styleHistogram | styleThick );
Plot( CompressionRank, "ATR compression percentile", colorDarkBlue,
styleLine | styleThick );
PlotGrid( TightPct, colorRed, 9, 1, True );
TightRun = BarsSince( NOT IsTight );
Title = Name() + " " + RangeLen + "-bar range " + WriteVal( Width, 1.2 ) +
"% of price (percentile " + WriteVal( WidthRank, 1.0 ) + ")" +
" | ATR" + FastAtr + " / ATR" + SlowAtr + " = " +
WriteVal( Compression, 1.2 ) +
" (percentile " + WriteVal( CompressionRank, 1.0 ) + ")" +
" | bars tight in a row: " + WriteVal( TightRun, 1.0 );
_SECTION_END();

Download consolidation-meter.afl67 lines

Three stages. Measure: WidthPct from the N-bar high-low range divided by price, and Compression from the fast ATR over the slow ATR. Normalise: PercentRank converts both into a 0 to 100 position within their own recent history, so a reading means the same thing on any instrument. Classify: a bar is tight only when both percentiles are below the threshold, and BarsSince(NOT IsTight) counts how long that has been true.

The plot puts the width percentile as a histogram and the compression percentile as a line in the same pane, with a grid line at the threshold. The histogram changes colour when the bar qualifies as tight — but the colour is redundant, since the bar height already crosses the grid line, so nothing is lost by a reader who cannot distinguish the two colours.

  • PercentRank(array, range) — the current value’s position within its own last range values, on a 0 to 100 scale.
  • SafeDivide(x, y, valueifzerodiv) — division that returns a stated value instead of Null when the divisor is zero.
  • ATR(period) — average true range.
  • BarsSince(array) — how many bars have passed since the condition was last true.
  • PlotGrid(level, colour, pattern, width, label) — a constant horizontal line, cheaper than plotting a flat array.

Find a chart period you would describe by eye as “a base” and check that the meter agrees. It usually will. Then do the harder test: scroll to a period where the meter says tight and you would not have said so by eye. Work out which of the two measurements drove it. Most of the time it is the compression ratio picking up a sequence of small bars inside a range that is still wide because of one old spike — which tells you something true about the market and something true about the limitations of a twenty-bar high-minus-low.

Add a third condition: volume percentile also below the threshold. Then compare how often all three agree with how often two of them do. The count of three-way agreements will be much smaller, and you will have to decide whether you have built a better filter or simply a rarer one. That decision — precision against sample size — recurs throughout the course.

Now the part that matters.

What the evidence supports. Volatility clustering is among the better-documented statistical regularities in financial series, and Part 1 introduced it: large moves tend to be followed by large moves, small by small, more than would be expected if bar sizes were independent. So a reading of “volatility is low now” carries genuine information about volatility over the next few bars. That is a real, useful and modest conclusion — and it is about volatility, not about price.

What follows almost tautologically. Contraction is bounded below by zero and mean-reverts in the sense that a period of unusually low readings is, by construction of the percentile, unusual and therefore temporary. “Contraction is eventually followed by expansion” is close to a definition. It is not a discovery, and it certainly is not a timing tool: “eventually” carries all the weight and has no length attached to it.

What does not follow at all. Direction. This is where the popular version overreaches:

The market is coiling like a spring. When it breaks out, the move will be explosive — and upward.

Nothing in a range-width or ATR-ratio calculation contains directional information. Both are computed from absolute distances. A tight range that resolves downward produces exactly the same contraction reading beforehand as one that resolves upward. Any directional expectation has to come from somewhere else, and be tested separately.

And the selection problem again. Every dramatic breakout in market history was preceded by a quieter period, because every period is preceded by a quieter or a noisier one and history remembers the dramatic ones. Scrolling back to find contractions that preceded big moves will find them, in the same way that drawing support lines under old lows finds bounces. The count that matters is the other one: of all contractions meeting your definition, what fraction were followed by a move of the size you care about, and how does that compare with the same fraction measured on ordinary bars?

That question has the shape of a reality check, and the last lesson of this part builds the machinery for exactly that shape of question.

Consolidation is four ideas sharing a word. Two of them — range width and volatility contraction — become numbers easily: measure the range as a percentage of price, measure the short-window ATR against the long-window one, and express both as percentiles of each instrument’s own history so that the readings are comparable.

Requiring two genuinely different measurements to agree is worth more than stacking two measurements built from the same quantity. Inside and outside bars provide a parameter-free cross-check.

Low volatility tending to persist is a defensible claim with evidence behind it. Contraction being followed by expansion is close to a tautology once you have defined contraction as an extreme. Contraction implying a direction is not supported by anything in the calculation, and the vivid coiled-spring language quietly attaches the third claim to the first two.

Check your understanding

Question 1. Why is a raw twenty-bar high-minus-low range a poor way to compare consolidation across instruments?
Show the answer and why

Answer: It is expressed in price units, so it scales with the instrument’s price level

A 4-unit range is tight on a 400-unit index and enormous on a 5-unit stock. Dividing by price makes it a percentage; ranking it against the instrument’s own history makes it comparable across regimes as well.

Question 2. What does PercentRank( WidthPct, 250 ) returning 8 tell you?
WidthRank = PercentRank( WidthPct, 250 );
Show the answer and why

Answer: The current range width is near the bottom of its last 250 readings

PercentRank reports position on a 0 to 100 scale within the lookback window: 0 is the lowest value seen, 100 the highest. A reading of 8 means only a small fraction of the last 250 readings were tighter.

Question 3. A contraction reading reaches its lowest level in two years. Which conclusion is supported?
Show the answer and why

Answer: Volatility over the next few bars is more likely to be low than a random bar would suggest

Volatility clustering supports the persistence claim and nothing more. Direction is absent from the calculation entirely, and no timing follows from an extreme reading — "eventually" is not a number of bars.

Question 4. Which pairs of measurements are different enough that requiring both to agree adds real information? Select all that apply.
Show the answer and why

Answer: Range width from highs and lows, and an ATR ratio from true ranges, Inside-bar count and range-width percentile

Bollinger band width is built from standard deviation, so the first pair is one measurement twice; the fourth pair is the same ratio with cosmetically different windows. Highs-and-lows versus true ranges, and a parameter-free bar-shape count versus a windowed range, are genuinely distinct.

Sources for this lesson

8 verified · checked 2026-08-31

  1. 01AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
  2. 02AFL Function Reference — StDevamibroker.com/guide/afl/stdev.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 — Insideamibroker.com/guide/afl/inside.html2026-08-31
  6. 06AFL Function Reference — Outsideamibroker.com/guide/afl/outside.html2026-08-31
  7. 07AFL Function Reference — BarsSinceamibroker.com/guide/afl/barssince.html2026-08-31
  8. 08AFL Function Reference — HHVamibroker.com/guide/afl/hhv.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.