Skip to content
Level 1 · Chart ReaderLessonPart 04 · page 3 of 524 min
24Minutes
11AFL functions
10Sources
StandardRequires
AFL functions taught here11

Swings, Highs and Lows

Point at a chart and say “that is a swing high” and everyone in the room nods. Ask the room to write down the rule they just applied and the nodding stops. This lesson is about closing that gap: taking a judgement that feels like perception, turning it into an arithmetic test, and being honest about what the conversion costs.

By the end you will have one working swing definition, a clear account of what changing its single parameter does to your chart, and — the part that matters most for everything after Part 8 — the habit of asking of any visual claim: at what moment could this have been known?

Informally, a swing high is a bar that stands above its neighbours: price rose into it and fell away from it. A swing low is the mirror image. Almost every structural idea in technical analysis is built on these two objects. Higher highs and higher lows, support and resistance levels, trendline anchors, measured moves, the necklines of head-and-shoulders patterns — all of them are statements about swing points, so they inherit whatever vagueness the swing definition contains.

The word doing all the hidden work is neighbours. Above which neighbours? How many? The bar next to it, or the twenty bars on either side? The moment you have to answer, you discover there is nothing in the data that answers for you.

Here is the difficulty stated plainly. On any chart, the highest bar of the last three is one thing; the highest bar of the last thirty is usually a different bar; the highest bar of the whole history is a third. All three descriptions are true simultaneously. “Swing high” is not a property a bar has. It is a property of a bar and a window, and the window is supplied by you.

This has an unpleasant corollary. If you look at a chart and mark the swing highs by eye, you are applying a window — but not a constant one. Your eye adapts to the zoom level, to how much of the pane a move occupies, and to what you are hoping to find. Zoom out and the small swings you marked stop being swings. Zoom in and new ones appear. The set of swing points you produce is a function of your zoom setting, and you almost certainly did not intend that.

The standard objective definition is an n-bar pivot, also called a fractal:

A bar is a swing high of strength n if its high is the highest high in the window that extends n bars either side of it.

Nothing is left to interpretation. n is a number you choose, the window is 2n + 1 bars wide, and the test is a comparison. In AFL, a rolling maximum over that window is HHV( High, 2*n + 1 ), and the middle bar of the window — n bars back from wherever we are standing — is Ref( High, -n ). The candidate is a swing high exactly when those two are equal.

That is the whole rule:

Fragment — not a complete formula

Strength = 2;
Window = 2 * Strength + 1;
CandidateHigh = Ref( High, -Strength ); // the bar in the middle of the window
SwingHigh = CandidateHigh == HHV( High, Window );

Two details make it work. HHV() is documented as including the current bar in its window, so a window of 2n + 1 bars covers the candidate plus exactly n bars on each side. And both sides of the comparison are the same stored number pulled from the same array — Ref selects an element of High, and HHV returns an element of High — so the equality test is exact rather than an approximate float comparison.

Here is that definition with Strength = 2, run across ten bars. Read it as one column per bar: each row is an array, and the last row is the answer the rule gives at that bar.

Ref(High, -2) == HHV(High, 5), evaluated on every bar

Illustrative highs, not market data. The rule fires on bar 5 and on bar 10, and it is describing bars 3 and 8. The first four bars have no complete five-bar window, so the rule declines to answer.
Bar12345678910
High10.010.611.411.010.210.511.111.911.511.2
Ref(High, -2)10.010.611.411.010.210.511.111.9
HHV(High, 5)11.411.411.411.911.911.9
SwingHigh100001
Marker belongs on bar38
Illustrative highs, not market data. The rule fires on bar 5 and on bar 10, and it is describing bars 3 and 8. The first four bars have no complete five-bar window, so the rule declines to answer. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Three things are visible in that table that are hard to see on a chart.

The rule produces one value per bar, like every AFL expression. It is not a search that returns a list of swing points; it is a Boolean array with a 1 on the bars where the test passed. That is the array way of thinking, and Part 8 is devoted to it.

The 1 does not appear on the swing bar. It appears two bars later, because that is when the window is complete. Bar 3 is the swing; bar 5 is when you could first have known.

And the first four bars return nothing at all. Their window is not full yet. The official pages for HHV and LLV do not state what those functions return before their window fills, so the formula in this lesson refuses to answer there rather than trusting an undocumented value — a small, unglamorous habit that prevents a whole class of silent wrong answers.

The life of one swing high

  1. Bar printsNothing is known yet; it is just a bar
  2. n bars passEach one either exceeds it or does not
  3. Test appliedIs the candidate still the window maximum?
  4. ConfirmedEarliest moment the answer exists
  5. Marker drawn backOnto the bar it describes, n bars ago
The chart shows a marker on the swing bar. The knowledge arrived n bars later. Both facts are true and they are constantly confused.

A swing high of strength n cannot be identified before n bars have printed after it. This is not a limitation of AmiBroker, of AFL, or of the definition. It is logical: the claim “nothing higher came in the next n bars” is a claim about the next n bars, and until they exist there is nothing to check.

Every downstream idea inherits this. “Price made a higher high” cannot be asserted until the higher high has been confirmed. A structure classification built on two confirmed swings is late by at least the strength of the swing definition. A support level anchored on a swing low did not exist as a level on the day the low was made.

The lag is also the reason larger is not better. A strength of 20 produces few, clean, important-looking swing points, and each of them is confirmed 20 bars late. A strength of 2 produces a cluttered chart confirmed 2 bars late. You are trading noise against timeliness, and there is no setting that wins both.

Three edge cases will eventually bite you, so decide about them deliberately.

Ties. If two bars in the window share the highest high, both satisfy == and both are marked. On a liquid instrument at four decimal places this is rare; on a low-priced instrument with a coarse tick size, or in a quiet range, it is not. If you need exactly one swing per window, you must add a tie-break — for instance requiring the candidate to be strictly greater than the bars before it and greater than or equal to the bars after.

Using high and low, or using close. The definition above uses High for swing highs and Low for swing lows, which is conventional and matches how people read a chart. Defining swings on closing prices instead is entirely legitimate, produces a different set of points, and is more robust on instruments whose intraday extremes are unreliable. Neither is correct in the abstract. State which you used.

Not enough history. With fewer than 2n + 1 bars loaded, the rule can never fire. On a chart zoomed to sixty bars with a strength of 40, you will see nothing at all and conclude the formula is broken.

The n-bar pivot is not the only option, and knowing the alternatives is what stops you treating your first choice as the natural one.

A percentage-change definition ignores bar counts entirely and marks a new swing whenever price reverses by more than some percentage from the last extreme. This is what a zig-zag indicator does. It adapts to volatility in a way that a fixed bar count does not, and it has its own hidden parameter — the percentage — plus a much nastier property.

They remain useful for what the documentation recommends them for: pattern and trend recognition, and drawing. So this compiles, draws a beautiful chart, and must never feed a trading decision:

Fragment — not a complete formula

// Illustration of what NOT to build a signal on.
// The official Peak() page states this function may look into the future.
LastPeak = Peak( High, 5, 1 );

An ATR-scaled definition sits between the two: require the candidate to exceed its neighbours by some multiple of average true range, so the threshold breathes with volatility. It has two parameters instead of one, which is a cost.

None of these is the true definition. There is no true definition. There are only definitions you have stated, whose parameters you have tested, and which you apply the same way every time.

Put the definition on a chart, so that you can see which bars it selects, change its one parameter and watch the selection change, and read off how late each confirmation was. The formula deliberately does two things at once: it draws the marker on the bar the swing actually happened, because that is the useful picture, and it reports in the title how many bars after the fact the confirmation arrived, because that is the fact the picture hides.

Complete runnable AFL

swing-points.afl
// swing-points.afl
// Part 4 - Swings, Highs and Lows
//
// Marks swing highs and swing lows using one explicit, stated definition:
//
// a bar is a swing high if its High is the highest High of the window that
// extends Strength bars either side of it.
//
// The test is applied Strength bars AFTER the candidate bar, which is the
// earliest moment at which the answer is knowable. No future bar is read. The
// arrow is then drawn back onto the candidate bar so the chart shows where the
// swing was, while the title reports when it could first have been confirmed.
//
// Assumptions:
// - Any instrument, any interval.
// - A swing point is a definition, not a market fact. Change Strength and the
// set of swing points changes with it. That is the lesson, not a defect.
// - Ties are possible: if two bars in the window share the highest High, both
// satisfy the definition. Decide deliberately whether you mind.
_SECTION_BEGIN("Swing points");
Strength = Param( "Swing strength (bars either side)", 3, 1, 25, 1 );
Window = 2 * Strength + 1;
// Cum(1) counts 1, 2, 3, ... across the bars the formula was given, so it is a
// simple way to discard the warm-up bars where the rolling window is not full.
// The User's Guide does not document what HHV returns before its window fills,
// so the formula refuses to answer there rather than trusting an undefined value.
BarNumber = Cum( 1 );
HasWindow = BarNumber >= Window;
// The candidate sits in the middle of the window: Strength bars back from the
// bar we are standing on.
CandidateHigh = Ref( High, -Strength );
CandidateLow = Ref( Low, -Strength );
// HHV and LLV include the current bar, so a window of 2*Strength+1 bars covers
// the candidate plus exactly Strength bars either side of it.
WindowHigh = HHV( High, Window );
WindowLow = LLV( Low, Window );
// The comparison is exact rather than approximate because both sides are the
// same stored value pulled from the same array, not two separate calculations.
SwingHighConfirmed = HasWindow AND CandidateHigh == WindowHigh;
SwingLowConfirmed = HasWindow AND CandidateLow == WindowLow;
GraphXSpace = 6;
Plot( Close, "Price", colorDefault, styleCandle );
// The last argument shifts the marker back onto the candidate bar. The anchor
// price is the candidate's own High or Low, so the arrow lands at the right
// height. The offset before it is in screen pixels: positive is up.
PlotShapes( IIf( SwingHighConfirmed, shapeDownArrow, shapeNone ),
colorDarkRed, 0, CandidateHigh, 14, -Strength );
PlotShapes( IIf( SwingLowConfirmed, shapeUpArrow, shapeNone ),
colorDarkGreen, 0, CandidateLow, -14, -Strength );
// The most recent confirmed swing points, and how long ago the confirmation
// arrived. What BarsSince returns BEFORE the condition has ever been true is
// not documented, so treat the two "bars ago" figures as meaningless until at
// least one arrow of that kind is actually on the chart.
LastSwingHigh = ValueWhen( SwingHighConfirmed, CandidateHigh, 1 );
LastSwingLow = ValueWhen( SwingLowConfirmed, CandidateLow, 1 );
BarsSinceHigh = BarsSince( SwingHighConfirmed );
BarsSinceLow = BarsSince( SwingLowConfirmed );
_N( Title =
Name() + " - " + Interval( 2 ) +
StrFormat( " - swing strength %g, window %g bars\n", Strength, Window ) +
StrFormat( "Last confirmed swing high %g, confirmed %g bars ago\n",
LastSwingHigh, BarsSinceHigh ) +
StrFormat( "Last confirmed swing low %g, confirmed %g bars ago\n",
LastSwingLow, BarsSinceLow ) +
StrFormat( "Every arrow appeared %g bars after the bar it points at.", Strength ) );
_SECTION_END();

Download swing-points.afl79 lines

The first block reads the strength and computes the window width. The second block builds the warm-up guard: Cum( 1 ) counts 1, 2, 3, … across the bars the formula was given, so comparing it with the window width identifies the bars where the rolling window is not yet full.

The third block is the definition itself, applied to highs and to lows. The fourth draws it. The last argument of each PlotShapes() call is -Strength, which shifts the marker back onto the bar it describes; the anchor price is the candidate bar’s own high or low, so the arrow lands at the right height rather than at the height of whatever bar happens to be current.

The title then reports the two most recent confirmed swings, how long ago they were confirmed, and a standing reminder of the lag.

  • Ref( array, period ) shifts an array in time. A negative period looks back; a positive period looks forward, and the official page says so explicitly. Every Ref in this formula is negative, which is what makes it safe.
  • HHV( array, periods ) and LLV( array, periods ) are rolling extremes over a trailing window that includes the current bar.
  • Cum( array ) is a running total from the first bar of the range. Cum( 1 ) is the documented idiom for a bar counter.
  • ValueWhen( expression, array, n ) returns the value the array had on the n-th most recent bar where the expression was true. Note the argument order — the condition comes first, and swapping the first two arguments compiles without complaint.
  • BarsSince( expression ) counts bars since the expression was last true.
  • PlotShapes( shape, colour, layer, yposition, offset, XShift ) draws glyphs. The offset is in screen pixels, not price units, and its sign is inverted from intuition: negative shifts down. XShift shifts the glyphs by a number of bars.

Set the strength to 3 and pick any marked swing high. Count the bars either side and confirm none of them has a higher high — that is the definition, checked by hand.

Then do the test that matters. Note the date of the most recent arrow. Count forward three bars from it and confirm that no arrow existed on the chart until that later bar had printed. The easiest way to see this is to zoom the chart so that the last visible bar is the swing bar itself: the arrow is not there.

  • No arrows anywhere. Fewer than 2 × Strength + 1 bars loaded, or the strength set higher than the visible history supports.
  • Arrows in unexpected places on a thin instrument. Ties. Two bars in the window share the same high, and both satisfy the rule.
  • Arrows overlapping the candles. The pixel offset is a matter of taste and chart height. Adjust the 14 and -14, and remember that GraphXSpace opens up head and foot room in the pane.
  • A swing that “should” be there is missing. Almost always correct behaviour: one bar within the window exceeded the candidate. Zoom in and check the highs rather than the shape.

Add a second strength — say 3 and 10 — and plot both sets of markers in different colours on one chart. The relationship between them is worth an hour of study on its own: the larger set is a subset of the smaller one, and the bars where the two agree are the turns that survive a change of scale.

You arrived with a phrase, “swing high”, that felt like an observation. You leave with a rule, a parameter, and a bill. The rule is Ref( High, -n ) == HHV( High, 2n+1 ). The parameter is n, and it has no correct value. The bill is n bars of confirmation lag, which cannot be paid off, negotiated down, or ignored without producing results that could not have been achieved in real time.

The next lesson stacks confirmed swings against each other to build the higher-high, higher-low vocabulary — and inherits every one of these caveats.

Check your understanding

Question 1. Using the definition in this lesson with Strength = 4, on which bar does the rule first report a swing high that occurred on bar 30?
Show the answer and why

Answer: Bar 34

The claim is that no higher high appeared in the four bars after bar 30. That claim cannot be evaluated until bar 34 has printed. The chart may draw the marker back on bar 30, but the knowledge is four bars younger than the bar it describes.

Question 2. Why does the formula use Ref(High, -Strength) rather than Ref(High, +Strength)?
CandidateHigh = Ref( High, -Strength );
SwingHigh = CandidateHigh == HHV( High, 2 * Strength + 1 );
Show the answer and why

Answer: A positive period reads bars that have not printed yet, which is look-ahead

The Ref reference page states that a positive period references periods in the future. Writing the test as "stand n bars later and look back" gives exactly the same set of swing points with no forward reference anywhere.

Question 3. Which statements about AmiBroker's Zig(), Peak() and Trough() functions are correct? Select all that apply.
Show the answer and why

Answer: Their official reference pages warn that they may look into the future, They are recommended for pattern and trend recognition, They are based on the zig-zag algorithm

All three are built on the zig-zag indicator and carry the same documented caveat. The Zig page says the function is provided for pattern and trend recognition formulas and warns of unrealistic results when backtesting a system that uses it.

Question 4. You increase the swing strength from 3 to 12. What should you expect?
Show the answer and why

Answer: Fewer swing points, confirmed later

A wider window is a harder test, so fewer bars pass it, and the window takes longer to complete, so each confirmation arrives later. Noise against timeliness is the trade the parameter controls, and there is no setting that improves both.

Question 5. True or false: with a fixed swing definition, marking swing points by eye at different zoom levels gives the same answer as the formula.
Show the answer and why

Answer: False

False, and the reason is the point of the lesson. Eyeballing applies a window that changes with zoom, screen size and expectation. The formula applies one stated window everywhere, which is what makes its output reproducible by somebody else.

Sources for this lesson

10 verified · checked 2026-08-31

  1. 01AFL Function Reference — Refamibroker.com/guide/afl/ref.html2026-08-31
  2. 02AFL Function Reference — HHVamibroker.com/guide/afl/hhv.html2026-08-31
  3. 03AFL Function Reference — LLVamibroker.com/guide/afl/llv.html2026-08-31
  4. 04AFL Function Reference — Cumamibroker.com/guide/afl/cum.html2026-08-31
  5. 05AFL Function Reference — ValueWhenamibroker.com/guide/afl/valuewhen.html2026-08-31
  6. 06AFL Function Reference — PlotShapesamibroker.com/guide/afl/plotshapes.html2026-08-31
  7. 07AFL Function Reference — Zigamibroker.com/guide/afl/zig.html2026-08-31
  8. 08AFL Function Reference — Peakamibroker.com/guide/afl/peak.html2026-08-31
  9. 09AFL Function Reference — Troughamibroker.com/guide/afl/trough.html2026-08-31
  10. 10AFL Function Reference — BarsSinceamibroker.com/guide/afl/barssince.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.