Skip to content
Level 3 · AFL DeveloperLessonPart 09 · page 2 of 726 min
26Minutes
10AFL functions
8Sources
StandardRequires
AFL functions taught here10

Running Extremes: HHV, LLV and Friends

“The highest high of the last twenty bars” sounds like a single sentence with a single meaning. It is not. It hides three separate decisions — how long the window is, whether the current bar counts, and whether you want the level or the bar it happened on — and getting any of the three wrong produces a formula that either fires constantly, fires never, or fires on the wrong bars.

This lesson covers the functions that answer questions about extremes over a window, the two functions that answer the different question of extremes over all of history, and the specific trap that stops the most commonly written breakout formula in AFL from ever being true.

The two workhorses have matching signatures:

Fragment — not a complete formula

hhv( ARRAY, periods ) // highest value over the trailing window
llv( ARRAY, periods ) // lowest value over the trailing window

Both return arrays. For every bar, they look back over periods bars and return the largest or smallest value found. The official pages describe the window as “the preceding periods”, and both add a clarification in brackets that decides almost everything else in this lesson: periods includes the current day.

Both accept a periods argument that is either a constant or a time-variant array, so the window length can itself change bar by bar.

A three-bar rolling high, and the same window shifted back one bar

At bar 3 the un-shifted window covers bars 1-3, including bar 3 itself. The shifted version covers bars 0-2, all of which had already printed.
Bar012345
High101211131214
HHV( High, 3 )warm-up cells are not documented??12131314
Ref( HHV( High, 3 ), -1 )the window that closed before this barNull??121313
At bar 3 the un-shifted window covers bars 1-3, including bar 3 itself. The shifted version covers bars 0-2, all of which had already printed. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Look at bar 3. HHV( High, 3 ) reports 13, which is bar 3’s own high. The shifted version reports 12, which is the highest high of the three bars that had finished before bar 3 opened. Those are different questions with different answers, and most of the mistakes in this area come from asking the first while believing you asked the second.

Here is the formula almost everyone writes first:

Fragment — not a complete formula

// This is never true. Not rarely - never.
Buy = Close > HHV( Close, 20 );

The window includes the current bar, so HHV( Close, 20 ) on any bar is the largest of twenty values, one of which is that bar’s own close. A number cannot be strictly greater than the largest of a set that contains it. The formula compiles, runs, produces an array of zeros, and returns an empty scan with no explanation.

The same reasoning kills Low < LLV( Low, 50 ) and every variation on the pattern. There is one case that looks like an exception and is not:

Fragment — not a complete formula

// Also never true on well-formed data - and if it is ever true,
// you have found a data defect rather than a breakout.
Buy = Close > HHV( High, 20 );

For this to be true, the close would have to exceed the highest high of a window that includes the current bar — that is, exceed the current bar’s own high. In a correctly built bar, the close lies between the low and the high. If this condition ever fires, treat it as a data-quality alarm rather than a signal; Part 2 describes how such bars get into a database.

There are two forms, and they produce the same array:

Fragment — not a complete formula

PriorHigh = Ref( HHV( High, 20 ), -1 ); // compute, then shift the result
PriorHigh = HHV( Ref( High, -1 ), 20 ); // shift the input, then compute

Both give, on every bar, the highest high of the twenty bars that ended on the previous bar. Use whichever reads more clearly to you, but use one of them consistently — mixing the two styles inside one project makes code review harder than it needs to be.

With a well-defined level in hand, the breakout itself is a crossing:

Fragment — not a complete formula

Breakout = Cross( Close, PriorHigh );

Even with the window fixed, “a twenty-bar breakout” is still ambiguous, and the ambiguity matters more than the window length does.

A closing breakout asks whether the bar closed beyond the prior extreme: Cross( Close, PriorHigh ). It is conservative, it can only be known at the end of the bar, and it ignores intrabar spikes that were rejected.

An intrabar breakout asks whether price traded beyond the prior extreme: High > PriorHigh. It fires earlier and more often, it includes moves that were immediately reversed, and it raises a question the closing version does not: at what price would you have been filled? Assuming a fill exactly at the level, on the bar where it was first touched, is one of the classic ways to build a backtest that cannot be reproduced in practice. Part 30 examines that assumption in detail.

Neither definition is more correct. They are different rules, and comparing a result obtained under one with a result obtained under the other is meaningless. Write down which one you used.

Draw the levels a breakout rule would actually use — the highest high and lowest low of the window that closed before the current bar — and mark the bars on which the close crossed them. Seeing the levels on a chart is the fastest way to notice that a rule is measuring something other than what you intended.

Complete runnable AFL

prior-window-channel.afl
// prior-window-channel.afl
// Part 9 - Running Extremes: HHV, LLV and Friends
//
// Plots the highest high and the lowest low of the PREVIOUS N bars, and marks
// the bars on which the close crossed beyond them.
//
// The one-bar shift is the whole point. HHV( High, N ) includes the current bar,
// which is documented behaviour, so "Close > HHV( High, N )" compares the close
// with a window that already contains it. Shifting the channel back by one bar
// gives the level that was already fixed before the current bar opened.
//
// Assumptions:
// - A breakout is defined here as a CLOSE beyond the prior-window extreme.
// An intrabar definition (High > level) is a different rule and gives
// different results; neither is more correct than the other, but mixing
// the two makes results incomparable.
// - The first ChannelBars bars have an incomplete window. The title reports
// how many leading bars are unusable rather than hiding them.
// - Marking a breakout describes what happened. It is not a forecast, and
// this formula deliberately sets no Buy or Sell variable.
_SECTION_BEGIN( "Prior-window channel" );
ChannelBars = Param( "Channel length (bars)", 20, 2, 250, 1 );
// ---------------------------------------------------------------------------
// Levels
// ---------------------------------------------------------------------------
// The extremes of the window that ENDED ON THE PREVIOUS BAR. Ref( ..., -1 ) is
// what makes these levels knowable before the current bar prints.
UpperLevel = Ref( HHV( High, ChannelBars ), -1 );
LowerLevel = Ref( LLV( Low, ChannelBars ), -1 );
// ---------------------------------------------------------------------------
// Events
// ---------------------------------------------------------------------------
// Cross() fires on one bar only. Cross( a, b ) is "a crossing above b", so the
// downside break needs the arguments the other way round.
UpBreak = Cross( Close, UpperLevel );
DownBreak = Cross( LowerLevel, Close );
// How stale is the extreme the channel is built on? HHVBars and LLVBars return
// BAR COUNTS, not prices, so they belong in the title and never on the price
// scale - plotting a count of 37 next to a price of 4.15 destroys the chart.
HighAgeBars = HHVBars( High, ChannelBars );
LowAgeBars = LLVBars( Low, ChannelBars );
// ---------------------------------------------------------------------------
// Drawing
// ---------------------------------------------------------------------------
Plot( Close, "Close", colorDefault, styleCandle );
Plot( UpperLevel, "Prior high", colorGreen, styleLine | styleThick );
Plot( LowerLevel, "Prior low", colorRed, styleLine | styleThick );
PlotShapes( UpBreak * shapeUpArrow, colorGreen, 0, Low, -20 );
PlotShapes( DownBreak * shapeDownArrow, colorRed, 0, High, 20 );
LeadingNulls = NullCount( UpperLevel, 1 );
_N( Title =
Name() + " - " + Interval( 2 ) +
StrFormat( " - %g-bar prior-window channel\n", ChannelBars ) +
StrFormat( "Prior-window high %g, prior-window low %g\n",
UpperLevel, LowerLevel ) +
StrFormat( "Window high is %g bar(s) old, window low is %g bar(s) old\n",
HighAgeBars, LowAgeBars ) +
StrFormat( "The first %g bar(s) of this chart have no complete window",
LeadingNulls ) );
_SECTION_END();

Download prior-window-channel.afl73 lines

The formula has four sections. It reads one parameter, the channel length. It builds the two levels with the shift discussed above, so both are knowable before the current bar prints. It converts the levels into events with Cross(), taking care to swap the arguments for the downside case, because Cross( a, b ) means “a crossing above b” and there is no direction argument. Finally it draws everything and writes the numbers into the title.

Two supporting measures go into the title rather than onto the chart. HHVBars() and LLVBars() return bar counts, and a count of 37 plotted on a price scale that runs from 4.00 to 5.00 flattens the entire price series into a line at the bottom of the pane. NullCount() reports how many bars at the left edge have no complete window, which is information the chart itself hides.

  • HHV( ARRAY, periods ) / LLV( ARRAY, periods ) — rolling extremes, with the current bar inside the window.
  • HHVBars( ARRAY, periods ) / LLVBars( ARRAY, periods ) — how many bars have passed since the array reached its window peak or trough. Documented to return a count of periods, not a price.
  • Cross( ARRAY1, ARRAY2 ) — true on the bar ARRAY1 crosses above ARRAY2, and only on that bar. The next lesson but one is devoted to it.
  • PlotShapes( shape, color, layer, yposition, offset ) — draws arrows. The idiom condition * shapeUpArrow works because the shape constants are numbers and shapeNone is zero, so a false condition plots nothing.
  • Param( name, default, min, max, step ) — exposes the channel length in the chart’s Parameters dialog (right-click the pane, or Ctrl+R).

The check that matters is that no arrow can be explained by the current bar. Pick any up-arrow, read the upper level printed at that bar, then look at the twenty bars before it. The level should equal the highest high among those bars, and it should not be affected by the arrow bar’s own high — even when the arrow bar has the highest high on the screen.

A second check: temporarily remove the Ref( ..., -1 ) from UpperLevel and re-apply. Every up-arrow should vanish, for the reason set out earlier in this lesson. Restore the shift afterwards.

  • Forgetting the shift on one side only. An upper level shifted and a lower level unshifted gives a formula whose long and short rules are not comparable.
  • Plotting HHVBars() on the price pane. The price series collapses to a flat line. Put counts in the title, or in their own pane.
  • Assuming the channel implies a trade. The formula deliberately sets no Buy or Sell. A level being crossed is an observation about price, not a reason to act.

Add a filter that ignores breakouts of a stale extreme: require HHVBars( High, ChannelBars ) > 5, so that the level being broken was set at least five bars ago rather than on the bar before last. Whether that improves anything is an empirical question — the tools to answer it arrive in Part 28.

HHV() tells you what the highest value was. HHVBars() tells you how long ago it happened, within the same window. LLVBars() does the same for the low.

The pair answers questions that a level alone cannot:

Fragment — not a complete formula

// Which came first inside this window, the high or the low?
HighIsOlder = HHVBars( High, 50 ) > LLVBars( Low, 50 );
// A fresh extreme rather than one set two months ago.
FreshHigh = HHVBars( High, 100 ) < 10;

That first line is a swing-structure building block: within a fifty-bar window, if the high is older than the low, price made its high and then its low, which is one of the definitions Part 4 used when classifying structure.

These two look like relatives of HHV and LLV and are not:

Fragment — not a complete formula

Highest( ARRAY ) // no periods argument
Lowest( ARRAY ) // no periods argument

There is no second argument. The documentation defines them as the highest and lowest value since the first day or bar present in the database. The result is a running staircase: Highest( Close ) never falls, and Lowest( Close ) never rises.

Two consequences follow, and both catch people out.

First, Highest is not HHV with a very large number. HHV( Close, 100000 ) asks for a window; Highest( Close ) asks for everything so far. On a symbol with fewer bars than the window they may agree, which makes the confusion survive longer than it should.

Second — and this is the one worth remembering — their answer depends on how much history was delivered to the run. Change the Analysis range, zoom the chart, or move the same formula to a database with a longer history, and the numbers change. That is documented behaviour, not a fault. But it means a Highest() value is never a fact about the instrument; it is a fact about this particular execution.

Put the two families side by side on the same symbol so the difference is visible as numbers rather than as a description.

Complete runnable AFL

window-versus-history.afl
// window-versus-history.afl
// Part 9 - Running Extremes: HHV, LLV and Friends
//
// Side-by-side evidence that HHV() and Highest() are different functions rather
// than the same function with a different period:
//
// HHV( High, N ) - the highest high of a rolling N-bar window
// Highest( High ) - the highest high since the first bar DELIVERED to this run
//
// Run it on one symbol over "All quotations" and read down the columns. The HHV
// column rises and falls. The Highest column is a staircase that never falls.
//
// Assumptions:
// - Highest() and Lowest() measure from the first bar present in the delivered
// range, so the same formula prints different numbers after a range change,
// a zoom, or on a database with more history. That is documented behaviour,
// not a defect - but it means a Highest() value is never a fact about the
// instrument, only a fact about this run.
// - One symbol at a time: the output is one row per bar.
// - No trading rule is expressed here; no Buy or Sell variable is set.
SetBarsRequired( sbrAll ); // ask for the whole history, not the QuickAFL slice
WindowBars = 50;
RollingHigh = HHV( High, WindowBars );
RollingLow = LLV( Low, WindowBars );
RangeHigh = Highest( High );
RangeLow = Lowest( Low );
// Bar counts, not prices: how old is the extreme the window is currently using?
HighAgeBars = HHVBars( High, WindowBars );
LowAgeBars = LLVBars( Low, WindowBars );
Filter = 1;
AddColumn( BarIndex(), "Bar index", 1.0 );
AddColumn( High, "High", 1.4 );
AddColumn( RollingHigh, StrFormat( "HHV(High,%g)", WindowBars ), 1.4 );
AddColumn( RangeHigh, "Highest(High): no window", 1.4 );
AddColumn( Low, "Low", 1.4 );
AddColumn( RollingLow, StrFormat( "LLV(Low,%g)", WindowBars ), 1.4 );
AddColumn( RangeLow, "Lowest(Low): no window", 1.4 );
AddColumn( HighAgeBars, "Bars since the window high", 1.0 );
AddColumn( LowAgeBars, "Bars since the window low", 1.0 );

Download window-versus-history.afl45 lines

It is an Exploration with Filter = 1, so every bar is reported. Each row shows the raw high and low, the rolling window extremes, the range-wide extremes, and the two bar-count measures. SetBarsRequired( sbrAll ) at the top asks for the full delivered history so that the Highest/Lowest columns mean what they appear to mean.

  • Highest( ARRAY ) / Lowest( ARRAY ) — running extremes with no window, measured from the first bar present in the delivered range.
  • SetBarsRequired( backwardref, forwardref ) — controls how many bars AmiBroker must supply. sbrAll (available since version 5.20) asks for all of them. The documentation notes that AmiBroker normally supplies at least thirty past bars beyond what it calculates a formula needs.

Run it once with the Analysis range set to all quotations and note the final Highest(High) value. Then set the range to the last 500 bars and run again. The HHV column should be unchanged on the bars that appear in both runs; the Highest column will usually differ, because the “first bar present” has moved. If that surprises you, re-read this section — it is the single most common misunderstanding about these two functions.

  • Writing Highest( Close, 20 ). There is no second argument. This is a syntax error, which is the kindest possible outcome.
  • Treating Highest( Close ) as a number. It is an array. The all-time high as a single number is LastValue( Highest( Close ) ) — with the caveat, from the previous lesson, that LastValue() reaches the end of the data and so must be kept out of trading rules.
  • Quoting a Highest() figure without saying what range produced it.

Add a column for 100 * ( Close - Lowest( Low ) ) / ( Highest( High ) - Lowest( Low ) ), which places the current close as a percentage of the whole delivered range. Then change the Analysis range and watch the same formula report a different number for the same bar. That is the lesson, expressed as a number that moves.

There is no correct window length. There is only a length that matches the question, and the discipline of writing down which question you asked.

Three practical notes:

  • The window is a definition, not a discovery. A twenty-bar high on daily data is a month; on five-minute data it is under two hours. The same number means different things on different intervals, which is why hard-coded periods travel badly between timeframes.
  • The current bar counts. A “twenty-bar breakout” written as Ref( HHV( High, 20 ), -1 ) compares against twenty bars ending on the previous bar — twenty-one bars of data in total are involved. Say which you mean when you document a rule.
  • Do not choose the period by trying every value and keeping the best. That is optimisation, it needs out-of-sample evidence to mean anything, and Parts 31 and 32 are about doing it responsibly. Choosing a round number for a defensible reason and leaving it alone is a perfectly respectable alternative.

A window of twenty bars cannot be complete until twenty bars exist. What the first nineteen bars contain is a question the User’s Guide answers for moving averages — the IsEmpty page explains that AFL marks unavailable values as empty, using the first twenty bars of a twenty-day average as its example — and does not answer for HHV, LLV, Sum or BarsSince.

So this course will not tell you whether HHV( High, 20 ) returns Null or a partial-window maximum on bar 5. It will tell you how to find out in under a minute:

Fragment — not a complete formula

Rolling = HHV( High, 20 );
Filter = 1;
AddColumn( BarIndex(), "Bar", 1.0 );
AddColumn( High, "High", 1.4 );
AddColumn( Rolling, "HHV(High,20)", 1.4 );
AddColumn( IsNull( Rolling ), "Empty (1=yes)", 1.0 );
AddColumn( NullCount( Rolling, 1 ), "Leading empties", 1.0 );

Run that on one symbol over all quotations and read the first thirty rows. The answer is then a fact about your build of AmiBroker rather than a guess, and you will have written the diagnostic pattern you will use for the rest of the course.

Whatever the answer, the safe habit is the same: do not let the first bars of a chart influence a decision. Gating on BarIndex() >= WindowBars is explicit, costs nothing, and documents the assumption for the next person to read the code.

HHV and LLV are rolling-window extremes and the window includes the current bar — which is why the naive breakout comparison can never be true, and why the correct forms shift either the result or the input back by one bar. HHVBars and LLVBars answer the different question of when the extreme happened, and they return counts that must be kept off the price scale.

Highest and Lowest have no window at all. They measure from the first bar delivered to the run, which makes their output dependent on the range, the zoom and the database. That is documented, and it means those values describe an execution rather than an instrument.

The window length is a definition you choose and record, not a parameter to be searched. And the first bars of any windowed calculation are a place where the documentation is thinner than most people assume, so measure rather than assume.

Check your understanding

Question 1. How many bars are true in this array on a ten-year daily chart?
Signal = Close > HHV( Close, 20 );
Show the answer and why

Answer: None

The documented window includes the current bar, so the current close is one of the twenty candidates. A value cannot be strictly greater than the maximum of a set containing it, so the array is zero everywhere on every symbol.

Question 2. Which expressions give the highest high of the twenty bars that ended before the current bar? Select all that apply.
Show the answer and why

Answer: Ref( HHV( High, 20 ), -1 ), HHV( Ref( High, -1 ), 20 )

Shifting the result and shifting the input both move the window back by one bar and give identical arrays. The third subtracts one from a price. The fourth shifts forwards, which reads a window that includes bars after the current one.

Question 3. What does HHVBars( High, 50 ) return?
Show the answer and why

Answer: The number of bars since the 50-bar window high was set

It is an age, measured in bars, not a price and not an absolute bar number. Plotting it on a price pane is a common way to make a chart unreadable.

Question 4. True or false: Highest( Close ) always returns the same value for a given symbol, whatever the Analysis range.
Show the answer and why

Answer: False

False. It is documented as measuring since the first bar present, so a shorter delivered range means a different starting point and often a different answer. A Highest() figure describes one execution, not the instrument.

Question 5. A scan using an intrabar breakout produced twice as many trades as one using a closing breakout, on the same symbols and dates. What follows?
Show the answer and why

Answer: Nothing yet - they are different rules and the comparison needs equal fill assumptions and costs

More trades is not better or worse on its own. The intrabar version also carries a fill assumption the closing version does not need, and until costs and fills are stated identically the two results are not comparable.

Sources for this lesson

8 verified · checked 2026-08-31

  1. 01AFL Function Reference — HHVamibroker.com/guide/afl/hhv.html2026-08-31
  2. 02AFL Function Reference — LLVamibroker.com/guide/afl/llv.html2026-08-31
  3. 03AFL Function Reference — HHVBarsamibroker.com/guide/afl/hhvbars.html2026-08-31
  4. 04AFL Function Reference — LLVBarsamibroker.com/guide/afl/llvbars.html2026-08-31
  5. 05AFL Function Reference — Highestamibroker.com/guide/afl/highest.html2026-08-31
  6. 06AFL Function Reference — Lowestamibroker.com/guide/afl/lowest.html2026-08-31
  7. 07AFL Function Reference — SetBarsRequiredamibroker.com/guide/afl/setbarsrequired.html2026-08-31
  8. 08AmiBroker User's Guide — Functions accepting variable periodsamibroker.com/guide/a_varperiods.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.