Skip to content
Level 1 · Chart ReaderLabPart 05 · page 5 of 645 min
45Minutes
11AFL functions
7Sources
StandardRequires
AFL functions taught here11

Lab: Map the Structure of a Real Chart

Reading a chart feels objective while you are doing it. This lab is designed to show you, with your own numbers, how much of it is not — and, more usefully, exactly which parts.

Three completed structure maps, on three different instruments, each built by the same eight-step procedure. Then a disagreement log: the same three charts read a second time, independently, either by a partner or by you after a gap, with the two readings compared element by element.

The deliverable that matters is the log, not the maps. By the end you should be able to say something like “our prior-month levels agreed exactly, our swing zones agreed on four out of six, and our trendlines shared no anchor points at all” — which is a far more useful thing to know about chart reading than any opinion about whether it works.

Budget about 45 minutes: 25 for your own three maps, 10 for the second reading, 10 for the comparison and the write-up.

You need:

  • AmiBroker with a daily database containing at least three years of history for three liquid instruments. Free end-of-day data is entirely sufficient; nothing here needs a subscription, a live feed or the Professional edition.
  • Three instruments chosen for variety, not for interest. Pick one that trended persistently over the period, one that spent most of the period range-bound, and one that changed character somewhere in the middle. Choose them by glancing at three-year charts and do not look more closely than that yet.
  • A partner, ideally. Failing that, the same person 24 hours later, working from the procedure and not from the earlier map. Self-comparison is weaker evidence — you will remember some of your marks — but it still works, and the memory effect biases the result towards agreement, which means any disagreement you do find is real.
  • The helper formula below, saved in the Formula Editor.

Follow these in order. The order is deliberate: the steps that need no judgement come first, so that they cannot be influenced by the ones that do.

The eight steps, in order

  1. 1. Fix the frameSymbol, interval, date range, scale, bars visible — written down
  2. 2. Mark swing points by ruleCentred n-bar pivots, n stated, not by eye
  3. 3. Convert points into zonesHalf-width = a stated multiple of ATR
  4. 4. Keep only zones with two or more touchesThe anti-cherry-picking step
  5. 5. Add the zero-discretion levelsPrevious month high and low, round numbers
  6. 6. Draw exactly one trendlineAnd write its full justification
  7. 7. Mark consolidations by the meterPercentile threshold, not impression
  8. 8. Write the structure briefIncluding what you do not know

Set the interval to daily, choose a fixed date range and stick to it, choose linear or logarithmic scaling and stick to that, and note roughly how many bars are on screen.

This step exists because the structure you find depends on all four. A level that is obvious across 250 bars can be invisible across 1,000. A trendline that is intact on a log scale can be broken on a linear one. Two people who have not agreed the frame are not reading the same chart, and any disagreement between them is uninformative.

Record it like this:

Symbol: ABC Interval: Daily Range: 2022-01-01 to 2024-12-31
Scale: semi-log Bars visible: about 750

Apply the helper formula. It marks a bar as a swing high when that bar is the highest of the PivotN bars either side of it, and a swing low by the mirror rule. Set PivotN to 5 and leave it there for all three instruments.

You are not choosing these points. That is the entire idea. If a peak you consider important is not marked, it is not a five-bar pivot, and your disagreement is with the rule rather than with the chart — which you may then argue about explicitly.

The formula draws a band of half-width ZoneMult × ATR(20) around the most recent two swing highs and the most recent two swing lows. Use 0.3 for all three instruments.

Read the zone half-width off the title bar and write it down in price units for each symbol. You will need it when you compare readings: “we both marked a zone at about 47” means nothing until you know whether “about” is 0.10 or 1.50.

Step 4: keep only zones with two or more touches

Section titled “Step 4: keep only zones with two or more touches”

Now do something the formula cannot do for you. For each of the four zones on the chart, count how many separate occasions price entered the zone — separate meaning that price left the zone for at least ten bars in between. Discard every zone with fewer than two.

This is the step that hurts, and it is the most valuable one in the lab. The zones you are about to discard are often the ones you find most convincing, because you remember the single dramatic reaction that formed them. A single reaction at a price is exactly what a swing high is; it is not additional evidence that the price matters.

Two families, both drawn by the formula or trivially added:

  • the previous calendar month’s high and low, plotted as staircases;
  • the round numbers nearest current price, at whatever grid step you fixed in the earlier lesson.

Nobody can disagree about where these are. They are the control group: when you compare readings later, any disagreement here is a data or procedure error, not a difference of interpretation.

One. Not the three or four you can see. Use AmiBroker’s trend line tool from the Draw toolbar or the Insert menu, enable right extension in its properties, and give it a Study ID of "SU" or "RE" so you can reference it later.

Then write the justification in full. It must contain all six of the following, because these are the six free choices identified in the trendlines lesson:

Anchors: 2022-06-17 low and 2022-10-13 low
Anchored to: wicks (bar lows)
Scale: semi-log
Direction: rising support
Touches: 3 (the two anchors plus 2023-03-10)
Redraw rule: none - if this line breaks I will record the break, not redraw

The redraw line is not optional. Committing in advance to not redrawing is what turns the line from a description into a claim.

Add the consolidation meter from the previous lesson in a second pane, or use the tightness ribbon that the helper formula already draws along the bottom of the price pane. A consolidation, for the purposes of this lab, is five or more consecutive bars whose range percentile is below 20.

Note the start and end dates of each such episode. Do not add episodes that look like consolidations but do not meet the rule, and do not remove ones that meet it but do not look right. Both temptations will arise; resisting them is the exercise.

Roughly 120 words per instrument, covering: the current structural state in the vocabulary from Part 4, the surviving zones with their widths, the one trendline and its status, the consolidations found, and — this is the part people skip — what you do not know.

A worked example of the last sentence, so that the standard is clear:

I do not know whether the 47.0 zone matters, because both of its touches occurred during the same six-week period and may be one episode rather than two. I do not know whether the trendline would survive on a linear scale; I did not check.

Complete runnable AFL

structure-map.afl
// structure-map.afl
// Part 5 - Lab: Map the Structure of a Real Chart
//
// The lab's marking tool. It applies ONE stated rule to each element of the
// map, so that two people running it on the same symbol over the same range see
// the same picture. Whatever you then add by hand is, by definition, the
// discretionary part of your reading - which is exactly what the lab measures.
//
// What it draws:
// * centred n-bar swing highs and lows (a bar that is the highest, or lowest,
// of the PivotN bars on either side of it);
// * a zone of stated half-width around the last two swing highs and the last
// two swing lows;
// * the previous calendar month's high and low, which involve no judgement
// at all;
// * a ribbon along the bottom marking bars whose range sits in the tightest
// part of its own recent history.
//
// DELIBERATE LOOK-AHEAD - read this before reusing any line below.
// A centred pivot cannot be known until PivotN bars after it happened. The
// CONDITION here is computed without look-ahead: at each bar it asks whether the
// bar PivotN bars ago was the extreme of the surrounding window. To draw the
// marker AT the pivot rather than PivotN bars to its right, the formula then
// shifts that flag with Ref( flag, +PivotN ), which reads a future bar. That is
// acceptable because the result is only ever drawn. Never copy that line into a
// Buy or Sell rule: it would use data that did not exist at the time. The last
// PivotN bars of the chart therefore cannot show a confirmed pivot yet.
//
// Assumptions: daily bars; at least RankLen + RangeLen bars of history before the
// percentile readings mean anything; a split-adjusted series.
_SECTION_BEGIN("Structure map");
PivotN = Param( "Pivot half-width (bars each side)", 5, 2, 30, 1 );
ZoneMult = Param( "Zone half-width (x ATR)", 0.3, 0.05, 2, 0.05 );
AtrLen = Param( "ATR period", 20, 2, 100, 1 );
RangeLen = Param( "Range window (bars)", 20, 5, 120, 1 );
RankLen = Param( "Tightness lookback (bars)", 250, 50, 1000, 10 );
TightPct = Param( "Tight threshold (percentile)", 20, 1, 50, 1 );
PivotWindow = 2 * PivotN + 1;
// Ref( High, -PivotN ) is the centre bar of the window that ends today, so this
// comparison is made PivotN bars AFTER the pivot. No look-ahead in the test.
IsPivotHigh = Ref( High, -PivotN ) == HHV( High, PivotWindow );
IsPivotLow = Ref( Low, -PivotN ) == LLV( Low, PivotWindow );
// Drawing only: move the confirmed flag back onto the pivot bar itself.
PivotHighHere = Ref( IsPivotHigh, PivotN );
PivotLowHere = Ref( IsPivotLow, PivotN );
HalfWidth = ZoneMult * ATR( AtrLen );
// The price of the most recent pivot of each kind, and the one before it.
LastHigh = ValueWhen( PivotHighHere, High, 1 );
PrevHigh = ValueWhen( PivotHighHere, High, 2 );
LastLow = ValueWhen( PivotLowHere, Low, 1 );
PrevLow = ValueWhen( PivotLowHere, Low, 2 );
// Previous calendar month's extremes. Nobody has to agree with anybody about
// where these are, which makes them the control group for the lab.
NewMonth = Month() != Ref( Month(), -1 );
MonthHigh = HighestSince( NewMonth, High );
MonthLow = LowestSince( NewMonth, Low );
PrevMonthHigh = ValueWhen( NewMonth, Ref( MonthHigh, -1 ) );
PrevMonthLow = ValueWhen( NewMonth, Ref( MonthLow, -1 ) );
RangeWidth = 100 * SafeDivide( HHV( High, RangeLen ) - LLV( Low, RangeLen ),
Close, 0 );
IsTight = PercentRank( RangeWidth, RankLen ) < TightPct;
Plot( Close, "Close", colorDefault, styleCandle );
PlotOHLC( LastHigh + HalfWidth, LastHigh + HalfWidth,
LastHigh - HalfWidth, LastHigh - HalfWidth,
"Swing-high zone (latest)", ColorBlend( colorRed, colorWhite, 0.72 ),
styleCloud | styleNoLabel );
PlotOHLC( PrevHigh + HalfWidth, PrevHigh + HalfWidth,
PrevHigh - HalfWidth, PrevHigh - HalfWidth,
"Swing-high zone (previous)", ColorBlend( colorRed, colorWhite, 0.86 ),
styleCloud | styleNoLabel );
PlotOHLC( LastLow + HalfWidth, LastLow + HalfWidth,
LastLow - HalfWidth, LastLow - HalfWidth,
"Swing-low zone (latest)", ColorBlend( colorBlue, colorWhite, 0.72 ),
styleCloud | styleNoLabel );
PlotOHLC( PrevLow + HalfWidth, PrevLow + HalfWidth,
PrevLow - HalfWidth, PrevLow - HalfWidth,
"Swing-low zone (previous)", ColorBlend( colorBlue, colorWhite, 0.86 ),
styleCloud | styleNoLabel );
Plot( PrevMonthHigh, "Previous month high", colorDarkGreen, styleStaircase );
Plot( PrevMonthLow, "Previous month low", colorDarkGreen, styleStaircase );
// The tightness ribbon uses its own scale so it sits along the bottom of the
// pane instead of squashing the price axis.
Plot( IsTight, "Tight range", ColorBlend( colorOrange, colorWhite, 0.5 ),
styleArea | styleOwnScale | styleNoLabel, 0, 20 );
PlotShapes( IIf( PivotHighHere, shapeSmallCircle, shapeNone ), colorRed, 0, High, 14 );
PlotShapes( IIf( PivotLowHere, shapeSmallCircle, shapeNone ), colorBlue, 0, Low, -14 );
PivotHighCount = LastValue( Cum( PivotHighHere ) );
PivotLowCount = LastValue( Cum( PivotLowHere ) );
Title = Name() + " " + Interval( 2 ) +
" | pivots: " + PivotWindow + "-bar centred (" +
NumToStr( PivotHighCount, 1.0 ) + " highs, " +
NumToStr( PivotLowCount, 1.0 ) + " lows on loaded bars)" +
" | zone half-width " + WriteVal( HalfWidth, 1.2 ) +
" | last swing high " + WriteVal( LastHigh, 1.2 ) +
", last swing low " + WriteVal( LastLow, 1.2 );
_SECTION_END();

Download structure-map.afl116 lines

To do the mechanical part of the map — pivots, zones, prior-month levels, tightness — by a stated rule, so that everything left over is visibly your judgement rather than invisibly mixed in with the arithmetic.

Pivots. A bar is a swing high when Ref(High, -PivotN) equals HHV(High, 2*PivotN+1): in words, the bar in the middle of the window is the highest of it. Because this is evaluated PivotN bars after the pivot, the condition contains no look-ahead.

Drawing the pivots in the right place. To put the marker on the pivot bar rather than PivotN bars to its right, the formula shifts the confirmed flag forward with Ref(flag, +PivotN). That is a deliberate forward reference, and the header comment says so in capitals. It is legitimate here for one reason only: the array is drawn and never feeds a signal. A consequence you will see on screen is that the last PivotN bars never carry a marker, because their pivot status is not yet decided.

Zones. ValueWhen(condition, array, n) returns the value of an array at the n-th most recent occurrence of a condition, so ValueWhen(PivotHighHere, High, 2) is the price of the swing high before last. Four such values, each turned into a band by adding and subtracting ZoneMult * ATR(AtrLen) and drawn with PlotOHLC in styleCloud.

Prior-month levels. A new month is detected by Month() != Ref(Month(), -1). HighestSince(NewMonth, High) runs a high-water mark from the start of the current month — note the argument order, condition first and array second, which is the reverse of how the name reads. Taking its value one bar before each month change and holding it with ValueWhen gives the completed previous month’s extreme.

Tightness ribbon. The same range-width percentile as the consolidation meter, plotted with styleOwnScale and an explicit maximum so that it occupies a strip at the bottom of the pane instead of flattening the price axis.

  • Ref(array, period) — negative shifts back, positive shifts forward into data that did not exist at the time.
  • ValueWhen(expression, array, n) — the array’s value at the n-th most recent occurrence.
  • HighestSince(expression, array) / LowestSince(expression, array) — running extreme since a condition was last true. Condition first.
  • PercentRank(array, range) — position within the array’s own recent history, 0 to 100.
  • PlotOHLC(...) with styleCloud — fills a band.
  • PlotShapes(shape, colour, layer, yposition, offset) — places a marker; a positive offset raises it above the bar, a negative one drops it below.

Set PivotN to 2 and count the markers. Set it to 15 and count again. The counts should differ by a large multiple, and — this is the point — the set of marked points at 15 is not simply a subset of the set at 2 in the way you might expect, because a wider window can promote a different bar within a cluster. Two people using different pivot widths are not disagreeing about the market; they are running different rules.

Then check the look-ahead claim directly. Note the date of the last marked swing high. Now shorten the chart’s date range so it ends ten bars earlier, and look again. Markers near the new right-hand edge will have disappeared, because their confirming bars are no longer loaded. That is the confirmation lag made visible.

Add a second zone pair for the third most recent swing high and low, using ValueWhen(..., 3), and observe how quickly the chart becomes unreadable. The clutter is the argument for step 4: a map that marks everything communicates nothing.

Now the measurement. Put the two maps of the same instrument side by side and fill in this table. Do it for each of the three instruments.

Element Reading A Reading B Agreed? Notes
Frame (range, scale) Should always agree
Previous month high Should always agree
Previous month low Should always agree
Surviving zones (count)
Zones agreeing within one half-width The key number
Trendline anchor 1 (date)
Trendline anchor 2 (date)
Trendline still intact?
Consolidation episodes (count)
Episodes with overlapping dates
Structural classification Trend / range / transition

Two zones “agree” when the centre of one falls inside the other. Two consolidation episodes “agree” when their date ranges overlap by at least half the shorter one. Fix those two conventions before you start scoring, not after you see the results.

Then compute three simple fractions per instrument: the proportion of zones that agreed, the proportion of consolidation episodes that agreed, and whether the two structural classifications matched.

Almost everyone finds the same shape of result, and it is worth predicting it before you look so that you can be surprised if you are wrong.

Element Typical agreement Why
Previous month high and low Total No parameters, no judgement
Round numbers Total, once the grid step is agreed The only choice is the step
Swing points at a fixed PivotN Total A rule computed it, not a person
Surviving zones after step 4 Partial The touch count involves judgement about what counts as leaving the zone
Consolidation episodes Partial to good The rule is objective; the boundaries are fuzzy
Trendline anchors Poor Several free choices, made independently
Structural classification Mixed Depends on which zones each reader kept

The lesson is not “chart reading is worthless”. It is more precise and more useful than that: discretion enters at identifiable points, and those points are where disagreement concentrates. The parts of a chart reading that a rule produced are reproducible. The parts that a person produced are not, and the gap between two readings is a direct measurement of how much of your analysis was you.

That has three practical consequences for everything that follows in this course.

First, when you eventually test an idea, test the reproducible version of it. A test of “hand-drawn support” is untestable in principle, because no two runs are the same study. A test of “the lowest low of the previous 60 bars” is a real experiment.

Second, when a rule-based version of an idea performs worse than your discretionary version seemed to, consider both explanations before choosing the flattering one. Perhaps your judgement adds something the rule misses. Perhaps your judgement had the benefit of already knowing what happened next. This lab does not settle which, but it should have made the second explanation feel much more live than it did an hour ago.

Third, the discretion ladder from the table above is a design tool. When you build a screening rule in Part 12 or a system in Part 27, prefer the elements near the top. Not because the ones lower down are wrong, but because you can find out whether the ones at the top are.

  • Three frame records, written before marking.
  • Three structure maps produced by the eight-step procedure.
  • Zone survival counts for each instrument, with the discard ratio.
  • One trendline per instrument with its six-line justification.
  • Consolidation episodes listed with start and end dates.
  • Three 120-word briefs, each containing an explicit “what I do not know”.
  • A comparison table per instrument, with the two agreement fractions computed.
  • Two or three sentences on which elements disagreed most, and why you think so.

You have applied a fixed procedure rather than an impression, and you have measured how much two applications of it by two people have in common. The elements produced by rules agreed; the elements produced by judgement did not, and the trendline anchors probably agreed least of all.

You have also met, on your own data, the two habits this part exists to install: discarding levels that rest on a single event, and writing down the choices — frame, pivot width, zone width, anchors, scale — that were invisible until you were asked to record them.

The next lesson takes one of the reproducible elements and asks the question this whole part has been circling: does it actually do anything?

Check your understanding

Question 1. Why does the helper formula compute the pivot condition with Ref( High, -PivotN ) and only then shift the result forward?
IsPivotHigh   = Ref( High, -PivotN ) == HHV( High, 2 * PivotN + 1 );
PivotHighHere = Ref( IsPivotHigh, PivotN );
Show the answer and why

Answer: So the condition itself uses no future data, and only the drawing position is shifted

The first line asks whether the bar in the middle of a completed window was its extreme, which is knowable PivotN bars later. The second line moves the marker back onto the pivot for drawing, and that line alone reads a future bar — which is why it must never be reused in a signal.

Question 2. In step 4 you discard swing zones with fewer than two separate touches. What problem is that step aimed at?
Show the answer and why

Answer: Treating a single vivid reaction at a price as if it were repeated evidence

Every swing high is by construction one reaction at one price. Counting that single reaction as evidence that the price matters is circular; requiring a second, separate visit is the cheapest available guard against it.

Question 3. Two readers disagree about whether an uptrend line is still intact. Which explanation should be checked first?
Show the answer and why

Answer: They anchored the line to different pivots, or used different axis scaling

Anchor choice and linear-versus-log scaling are the two largest sources of trendline disagreement, and both are free choices the lab asks you to record. Vendor differences and gaps matter, but they rarely flip a trendline verdict on liquid daily data.

Question 4. Which elements of the map should agree completely between two readers who followed the procedure? Select all that apply.
Show the answer and why

Answer: Previous calendar month high and low, Swing points at an agreed PivotN

The monthly extremes and the rule-computed pivots involve no judgement, so a difference there indicates a procedural or data error. Zone survival requires deciding what counts as leaving a zone, and anchor choice is pure discretion — both are expected to differ.

Sources for this lesson

7 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — Charting guide§ Using drawing toolsamibroker.com/guide/h_charting.html2026-08-31
  2. 02AFL Function Reference — Refamibroker.com/guide/afl/ref.html2026-08-31
  3. 03AFL Function Reference — ValueWhenamibroker.com/guide/afl/valuewhen.html2026-08-31
  4. 04AFL Function Reference — HighestSinceamibroker.com/guide/afl/highestsince.html2026-08-31
  5. 05AFL Function Reference — LowestSinceamibroker.com/guide/afl/lowestsince.html2026-08-31
  6. 06AFL Function Reference — PercentRankamibroker.com/guide/afl/percentrank.html2026-08-31
  7. 07AFL Function Reference — PlotShapesamibroker.com/guide/afl/plotshapes.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.