Skip to content
Level 1 · Chart ReaderReality checkPart 05 · page 6 of 635 min
35Minutes
10AFL functions
8Sources
StandardRequires
AFL functions taught here10

Reality Check: Do Support Levels Actually Hold?

This lesson does two jobs. It takes the most-repeated sentence in technical analysis and converts it into something a computer could measure. And it establishes the template that every later reality check in this course follows — for RSI in Part 6, for candlestick patterns in Part 7, for volume breakouts in Part 12.

The template matters more than the particular claim. Learn its shape here and you will be able to interrogate any market claim you meet for the rest of your life, including the ones nobody has thought to question yet.

Six steps from a slogan to a measurement

  1. 1. State the claimIn the form its believers would accept, then in a form that could fail
  2. 2. Define the terms objectivelyEvery noun becomes a formula; every adjective becomes a number
  3. 3. Choose the universe and the periodBefore looking at any result
  4. 4. Define the measurementWhat is counted, over what window, from which bar
  5. 5. Define the comparisonA number without a benchmark means nothing
  6. 6. Say what would change your mindWrite the threshold down before you run anything
Steps 5 and 6 are where most market research falls over, and they cost nothing but discipline.

Here is the version you will find in books, courses and forum posts:

Support tends to hold. When price falls back to a level where buyers previously stepped in, it often stops there and turns.

As stated, this cannot be wrong. “Tends” and “often” have no values attached, “support” has no definition, “hold” has no criterion, and no population of cases is specified. Every possible dataset is consistent with it.

Rewriting it so that it could fail requires five additions — a level definition, a touch definition, a hold definition, a population, and a comparison:

H1. For a stated universe of liquid instruments over a stated period, when the daily low first enters a zone around the lowest low of the previous 60 trading days, the proportion of such episodes in which price does not fall materially below that level over the following 10 trading days is higher than the proportion measured on ordinary bars with no level involved.

That sentence is longer, uglier and infinitely more useful. It can be false.

The level has to be computable from data available before the event, with no judgement. Several candidates satisfy that. We must pick one, and pick it now, before seeing anything.

Candidate level Free parameters Notes
Lowest low of the previous N bars 1 (the lookback) Simple, always defined, includes levels nobody would draw
Previous calendar month’s low 0 The cleanest, but only twelve per year per symbol
Confirmed n-bar swing low 1 (the pivot width) Closest to what a chartist draws; needs a confirmation lag
Round number nearest price 1 (the grid step) Zero-discretion, tests a different mechanism

We choose the first: the lowest low of the previous 60 trading days, measured as of the bar before the touch. The reasons are worth stating, because in your own work you will have to give reasons of the same kind.

It is always defined, so the sample is not concentrated in symbols that happen to produce many pivots. It has one parameter, which we can vary later to see how much the answer depends on it. It is close enough to what a person would call “the recent low” that a positive result would be interesting, and far enough from a hand-drawn line that it is reproducible.

It also has a known weakness: it will nominate levels that no chartist would ever draw, for instance in a symbol that has fallen steadily and whose 60-day low is simply last week. If the claim only holds for “proper” support, this test will understate it. That is a real limitation, and the honest response is to say so now rather than to discover it after seeing an unwelcome result.

In AFL, with the current bar excluded from its own level:

Fragment — not a complete formula

// Ref( ..., -1 ) keeps the touch bar out of the window that defines its level.
Level = Ref( LLV( Low, 60 ), -1 );

Two more definitions, and both contain traps.

A touch is a bar whose low enters a zone around the level. The zone half-width is 0.25 × ATR(20), measured — like the level — as of the previous bar.

Fragment — not a complete formula

ZoneHalf = Ref( 0.25 * ATR( 20 ), -1 );
InZone = Low <= Level + ZoneHalf AND Low >= Level - ZoneHalf;

That alone is not enough, because of the re-arm problem. When price drifts sideways along a level for three weeks, InZone is true on fifteen consecutive bars. Counting those as fifteen touches would let a handful of episodes dominate the entire sample, and would tell you about persistence rather than about support.

So a touch is the first in-zone bar in a 20-bar window:

Fragment — not a complete formula

// True only when this is the only in-zone bar in the trailing 20 bars.
Touch = InZone AND Sum( InZone, 20 ) == 1;

“Held” needs a distance and a horizon. Ours: over the next 10 bars, the lowest low never falls more than 0.5 × ATR below the level.

Fragment — not a complete formula

// A forward reference. Measuring an outcome IS a forward reference; it is only
// illegitimate when the result feeds a trading decision.
FutureLow = Ref( LLV( Low, 10 ), 10 );
Held = FutureLow >= Level - Ref( 0.5 * ATR( 20 ), -1 );

One episode, evaluated

One row of output per touch. Bar t+1 is also inside the zone but is not a second touch, because the re-arm rule has not reset. The verdict is attached to bar t and computed from bars t+1 to t+10.
Bart-1tt+1t+2t+3t+10
Level (from t-1)48.0048.0048.0048.0048.0048.0048.00
Zone half-width0.300.300.300.300.300.300.30
Low49.1048.2047.9048.6049.4050.20
InZone011000
Touch (first in 20 bars)010000
Lowest low over t+1..t+1047.90
Held (47.90 >= 48.00 - 0.60)1
One row of output per touch. Bar t+1 is also inside the zone but is not a second touch, because the re-arm rule has not reset. The verdict is attached to bar t and computed from bars t+1 to t+10. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Six numbers have now been fixed: a 60-bar lookback, a 20-bar ATR, a zone of 0.25 ATR, a 20-bar re-arm window, a 10-bar horizon and a 0.5 ATR break distance. Every one of them is a choice, so there are a great many possible versions of “the test”, and picking the version that gives the nicest answer is exactly the error Part 30 is named after. We fix them now and vary them afterwards as a sensitivity check, reporting the whole range rather than the best point in it.

Step 4: choose the universe and the period

Section titled “Step 4: choose the universe and the period”

Decide this before running anything, and write it down where you cannot quietly edit it.

Universe. Something broad enough to have statistical content and liquid enough that the prices are real. A reasonable Level A choice: 100 to 300 of the largest listed equities in a market whose end-of-day history you can obtain free, imported as described in Part 3. Not the symbols you follow. Not the symbols that have done well.

Period. Long enough to contain several different market environments. Something like 2005 to 2024 covers at least one severe bear market, one long expansion, one volatility shock, and several regimes in between. A test run only over a rising market answers a question about rising markets.

Why one regime is one observation

Whole period
2005-2024: several regimes
Sub-periods
CrisisRecoveryExpansionShockRecovery
Report the result for each sub-period as well as the whole. A single number over twenty years can hide two opposite answers.

This is the step that separates research from decoration.

Suppose the test comes back: 63% of touches held. Is that impressive? There is no way to know, because “held” as we defined it — price not dropping half an ATR below a price it just visited, within ten days — happens fairly often to any bar in a market that spends most of its history drifting upward. Without a benchmark, 63% is not a result. It is a number.

Two benchmarks, and the formula can produce both.

Take ordinary bars — every twentieth bar, so that the forward windows do not overlap — and apply the identical “held” test using that bar’s own low as the reference price. No level, no touch, no story. This measures how often price simply does not fall much further over ten days, in this universe over this period.

If the level result and the base rate are the same, the level contributed nothing.

Run everything again with the level displaced by an arbitrary 7%: a price that is not a 60-day low, is not a swing point, is not round, and that no one would call support. The touch rule, the zone, the re-arm window and the hold test are all identical.

This is the stronger of the two controls, because it holds the entire measurement apparatus constant and changes only the one thing under test. If the placebo scores the same, then whatever your machinery is measuring, it is not the level.

Write this down, in advance, in both directions. An honest form:

If the touch-hold rate exceeds both the base rate and the placebo rate by less than 4 percentage points, in the full period and in a majority of the sub-periods, I will conclude that the 60-day-low level added nothing detectable in this universe.

If it exceeds both by more than 4 percentage points consistently across sub-periods and across a range of zone widths from 0.1 to 0.5 ATR, I will conclude that something is there and is worth a properly costed trading test.

If the result is between those, or is inconsistent across sub-periods, I will conclude that the evidence is inconclusive and say so.

The third branch is the one people leave out, and it is the outcome you should expect most often. A conclusion of “I do not know” is a real conclusion, and it is a great deal more useful than a confident answer produced by squinting.

Pick your own threshold and defend it — but pick it now. A threshold chosen after seeing the result is not a threshold, it is a rationalisation with a number in it.

The formula is an Exploration: it emits one row per touch, with the level, the zone width, whether it held, and the forward move. A ParamList switches between the claim and its two controls, so the same code produces all three numbers.

Complete runnable AFL

level-touch-test.afl
// level-touch-test.afl
// Part 5 - Reality Check: Do Support Levels Actually Hold?
//
// An Exploration that emits ONE ROW PER TOUCH of an objectively defined level,
// together with what happened over the following HoldBars bars.
//
// It deliberately does not report a conclusion. It produces the rows; you
// aggregate them and compare the result with the two controls this same formula
// can produce, using the threshold you wrote down BEFORE running it.
//
// Run it three times, changing only "What is being measured":
// 1. Prior-low level - the claim under test.
// 2. Placebo level - the identical machinery on a price displaced by
// PlaceboPct%, which nobody would call support.
// 3. Unconditional base rate- every SampleEvery-th bar, using that bar's own
// low as the reference. This is what "held" scores
// when no level is involved at all.
// If (1) does not beat (2) and (3) by more than the margin you pre-registered,
// the level did no work in this universe over this period.
//
// Definitions, all fixed before looking at any result, all visible in Parameters:
// Level = lowest Low of the previous LevelLen bars, as known one bar before
// the touch. Ref( ..., -1 ) keeps the touch bar out of its own level.
// Zone = Level +/- ZoneMult x ATR( AtrLen ), also measured one bar before.
// Touch = a bar whose Low enters the zone and which is the only such bar in
// the trailing ReArmBars window.
// Held = over the next HoldBars bars the lowest Low never fell more than
// BreakMult x ATR below the level.
//
// Assumptions and limits:
// - daily bars, split- and dividend-adjusted (unadjusted history puts old
// levels at prices the instrument never traded at);
// - the universe was chosen before any result was seen, and it includes
// delisted symbols if you can get them - if it does not, say so;
// - the forward measurement reads future bars. That is what an outcome IS,
// and it is legitimate here only because nothing feeds a trading rule;
// - end the Analysis date range at least HoldBars bars before the last bar in
// the database, or the final touches are scored on incomplete windows;
// - costs, slippage and tradability are NOT modelled. This measures whether
// price behaved differently near the level, not whether you could profit.
_SECTION_BEGIN("Level touch test");
TestMode = ParamList( "What is being measured",
"Prior-low level|Placebo level|Unconditional base rate", 0 );
LevelLen = Param( "Level lookback (bars)", 60, 10, 250, 5 );
AtrLen = Param( "ATR period", 20, 5, 100, 1 );
ZoneMult = Param( "Zone half-width (x ATR)", 0.25, 0.05, 2, 0.05 );
BreakMult = Param( "Break distance (x ATR)", 0.5, 0.1, 3, 0.1 );
HoldBars = Param( "Forward window (bars)", 10, 1, 60, 1 );
ReArmBars = Param( "Re-arm window (bars)", 20, 1, 120, 1 );
PlaceboPct = Param( "Placebo displacement (%)", 7, 1, 30, 0.5 );
SampleEvery = Param( "Base rate: sample every n bars", 20, 1, 200, 1 );
TrueRange = ATR( AtrLen );
// The reference price under test.
PriorLow = Ref( LLV( Low, LevelLen ), -1 );
if ( TestMode == "Placebo level" )
{
Level = PriorLow * ( 1 - PlaceboPct / 100 );
}
else
{
if ( TestMode == "Unconditional base rate" )
{
// Every sampled bar is its own reference point, so "held" is measured
// from an ordinary price with no story attached to it.
Level = Low;
}
else
{
Level = PriorLow;
}
}
ZoneHalf = Ref( ZoneMult * TrueRange, -1 );
BreakDist = Ref( BreakMult * TrueRange, -1 );
InZone = Low <= Level + ZoneHalf AND Low >= Level - ZoneHalf;
if ( TestMode == "Unconditional base rate" )
{
// Sampling every n bars keeps the output manageable and, when n is at least
// HoldBars, stops consecutive observations from sharing forward windows.
Event = ( BarIndex() % SampleEvery ) == 0;
}
else
{
// Re-arming matters more than it looks. Without it a single slow approach
// to a level counts as twenty touches, and a handful of episodes dominates
// the whole sample.
Event = InZone AND Sum( InZone, ReArmBars ) == 1;
}
// The outcome. Ref( ..., +HoldBars ) reads forward, which is what measuring an
// outcome means; the window excludes the event bar itself.
FutureLow = Ref( LLV( Low, HoldBars ), HoldBars );
FutureClose = Ref( Close, HoldBars );
Held = FutureLow >= Level - BreakDist;
ForwardPct = 100 * SafeDivide( FutureClose - Close, Close, 0 );
Filter = Event AND NOT IsNull( Level ) AND NOT IsNull( FutureLow )
AND NOT IsNull( ZoneHalf );
// The 1/0 in the "Held" column is the value the summary row averages. Colour is
// decoration only - the number is always readable as text.
AddColumn( Level, "Level", 1.3 );
AddColumn( Low, "Bar low", 1.3 );
AddColumn( ZoneHalf, "Zone +/-", 1.3 );
AddColumn( BreakDist, "Break distance", 1.3 );
AddColumn( Held, "Held 1/0", 1.0, colorDefault,
IIf( Held, ColorBlend( colorGreen, colorWhite, 0.8 ),
ColorBlend( colorRed, colorWhite, 0.8 ) ) );
AddColumn( FutureLow, "Lowest low next " + HoldBars, 1.3 );
AddColumn( ForwardPct, "Forward move %", 1.2 );
// Flag 2 adds an AVERAGE row, flag 16 a COUNT row. The average of the Held
// column is the hold rate - the single number this whole exercise exists for.
AddSummaryRows( 2 | 16, 1.3 );
_SECTION_END();

Download level-touch-test.afl125 lines

To produce evidence rather than an impression: a table of episodes you can count, sort, export and check, from a definition that another person could re-implement and get the same rows.

Mode selection comes first. TestMode chooses the prior-low level, the displaced placebo level or the unconditional base rate, and everything downstream is written once and applied to whichever was chosen.

Level and zone are both computed with Ref(..., -1) so that the touch bar cannot influence its own reference price. Event detection applies the re-arm rule, except in base rate mode where (BarIndex() % SampleEvery) == 0 samples every n-th bar instead.

Outcome uses Ref(LLV(Low, HoldBars), HoldBars), which at bar t is the lowest low over bars t+1 to t+HoldBars, excluding the touch bar itself. Held compares that with the level minus the break distance.

Output is Filter plus a set of AddColumn calls, and AddSummaryRows(2 | 16, 1.3) appends an average row and a count row. The average of a column of ones and zeroes is the proportion — which is the single number the whole exercise exists to produce.

  • ParamList(name, values, default) — returns the chosen item as a string, so the mode can be compared with == in an if.
  • LLV(array, periods) with Ref(..., -1) — the prior-window low, excluding today.
  • Sum(array, periods) — the rolling count used to implement re-arming.
  • BarIndex() — the zero-based bar number, used with the % modulus operator to sample.
  • IsNull(x) — a synonym of IsEmpty, used to drop warm-up and end-of-range rows.
  • AddColumn(array, name, format, textColour, backgroundColour) — one exploration column.
  • AddSummaryRows(flags, format) — flag 2 adds an average row, flag 16 a count row.

Before believing any aggregate, verify three individual rows by hand. Pick a row, open that symbol’s chart at that date, and check three things: that the level equals the lowest low of the 60 bars before the touch bar; that the bar’s low really is inside the zone; and that the “Held” verdict matches what the next ten bars did.

Then run a deliberate sanity check: set the break distance to 20 ATR. Every episode should now be recorded as held, and the average should be exactly 1.0. If it is not, the measurement is broken and no amount of interpretation will fix it.

Add a second outcome column: the forward move measured from the touch bar’s close over 20 bars as well as 10. Then compare the hold rate with the average forward move. It is entirely possible for a level to “hold” frequently while the average forward move is negative, because the definition of holding tolerates a drift downward that is smaller than half an ATR. Two measurements of the same episode disagreeing is not a contradiction; it is a sign that “support held” was a vaguer idea than it looked.

Everything above is the easy part. Here is the honest list of what still stands between that table and a trustworthy conclusion.

Overlapping windows break independence. Two touches 5 bars apart share 5 of their 10 forward bars. Nearby symbols in the same sector share market-wide moves on the same dates. The effective number of independent observations is far smaller than the row count, so any significance calculation based on the row count overstates confidence, sometimes by a large factor. The re-arm rule and the base-rate sampling help; they do not fix it.

Trend contamination. In a universe and period that drifted upward, price frequently does not fall much further from wherever it is. A level test conducted only in such a period will show a high hold rate that has nothing to do with levels. This is precisely why the base rate is not optional.

The zone width moves the answer. Widen the zone and touches become more common and more casual; narrow it and only violent approaches qualify. The relationship is not monotonic in any obvious way, and there is no correct width. The only defensible presentation is a curve of hold rate against zone width, with the base rate plotted alongside it.

The garden of forking paths. Level definition, lookback, zone width, re-arm window, horizon, break distance, universe, period, and the definition of “held” itself — nine decisions, most with several defensible values. Trying them all and reporting the best will produce something impressive and meaningless every time. Fixing them in advance is the only real defence, and reporting the sensitivity across all of them is the second best.

Adjusted versus unadjusted history. A split-adjusted series moves every historical price, so levels computed on adjusted data sit where the split-adjusted low was, not where anybody watched price stop. Dividend adjustment does the same thing more gently and more often. Part 2 covers the choice; here it is enough to know that the two versions produce different levels, different touches and different answers.

Data errors manufacture touches. One erroneous low, 15% below the true one, creates a 60-day level that no participant ever saw, and then a touch of it. On free end-of-day data this happens often enough to matter across a few thousand rows.

Survivorship. Restated because it is the largest single bias here: the instruments whose levels most spectacularly failed are the ones most likely to be missing from your database.

Tradability is a separate question. Even a clear positive result would not be a strategy. Trading it requires an entry, an exit, a stop and a size, each of which adds assumptions, and the spread and commission have to come out of whatever the edge turns out to be. Parts 27 to 34 exist because that second journey is longer than the first.

This lesson does not tell you the answer, and that is a decision rather than an omission.

Any number quoted here would come with a universe, a period, a vendor, an adjustment convention and nine parameter choices attached to it, and stripped of those it would be repeated as “the course found that support holds 63% of the time” — which would be exactly the kind of unmoored statistic this part exists to inoculate you against. Worse, it would let you skip the work, and the work is the lesson.

So the deliverable is yours. Run the three modes, record the three numbers with your universe and period beside them, compare them with the threshold you wrote down in step 6, and write two sentences of conclusion — including, if it applies, “the evidence here is inconclusive”.

Keep that record. Part 35 builds a research log properly, and this will be its first entry.

The reality-check template is six steps: state the claim in a form that could fail, define every term objectively, choose the universe and period before looking, define the measurement, define the comparison, and commit in advance to what would change your mind.

Applied to support levels, that meant a 60-day-low level measured one bar early, an ATR-scaled zone, a re-armed touch rule, a ten-bar forward window with an ATR-scaled break distance, and two controls: an unconditional base rate on ordinary bars and a placebo level displaced by an arbitrary percentage.

The obstacles are real and worth respecting: overlapping windows, trend contamination, a zone width that moves the answer, nine forking decisions, adjustment conventions, data errors, survivorship, and the gap between a statistical tendency and a tradable one.

You now have a template and a working measurement. What you do not have is a conclusion, because that one is yours to produce — and the next time somebody tells you that support tends to hold, you know precisely which six questions to ask them.

Check your understanding

Question 1. Why does the formula compute the level as Ref( LLV( Low, 60 ), -1 ) rather than LLV( Low, 60 )?
Level = Ref( LLV( Low, 60 ), -1 );
Show the answer and why

Answer: So the touch bar’s own low cannot be part of the level it is being tested against

LLV includes the current bar. Without the shift, a bar that sets a new 60-day low would define the level it is then measured against, and the "touch" would be an artefact of the arithmetic rather than an event.

Question 2. The test returns a hold rate of 63%. What must you know before that number means anything?
Show the answer and why

Answer: The unconditional base rate for the same universe, period and measurement

A proportion needs a benchmark. If ordinary bars with no level involved also score around 63% under the identical "held" test, the level added nothing. Sector and interval matter, but neither turns a bare proportion into evidence.

Question 3. What problem does the re-arm rule Sum( InZone, 20 ) == 1 solve?
Touch = InZone AND Sum( InZone, 20 ) == 1;
Show the answer and why

Answer: A single slow approach to a level being counted as many separate touches

Without it, fifteen consecutive bars drifting along the level produce fifteen rows, and a few long sideways episodes dominate the sample. The rule keeps only the first in-zone bar in each twenty-bar window.

Question 4. Which of these are legitimate reasons that the measured hold rate could be too high, independently of whether levels do anything? Select all that apply.
Show the answer and why

Answer: The database contains only instruments that still exist today, The period tested was predominantly a rising market, The break distance is generous relative to typical daily movement

Survivorship removes the worst failures; a rising market makes "did not fall much further" common for any bar; and a generous break distance tolerates declines that a stricter definition would count as breaks. The length of the forward window changes the answer but not in a knowable direction — a longer window gives price more time to break as well as more time to recover.

Question 5. What is the purpose of running the identical test on a level displaced by an arbitrary 7%?
Show the answer and why

Answer: To hold the entire measurement apparatus constant while removing the only thing under test

A placebo control keeps the touch rule, the zone, the re-arm window and the hold test exactly as they were, and changes only whether the reference price has any claim to being support. If the placebo scores the same, the machinery is measuring something other than the level.

Sources for this lesson

8 verified · checked 2026-08-31

  1. 01AFL Function Reference — LLVamibroker.com/guide/afl/llv.html2026-08-31
  2. 02AFL Function Reference — Refamibroker.com/guide/afl/ref.html2026-08-31
  3. 03AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
  4. 04AFL Function Reference — AddColumnamibroker.com/guide/afl/addcolumn.html2026-08-31
  5. 05AFL Function Reference — AddSummaryRowsamibroker.com/guide/afl/addsummaryrows.html2026-08-31
  6. 06AFL Function Reference — ParamListamibroker.com/guide/afl/paramlist.html2026-08-31
  7. 07AFL Function Reference — BarIndexamibroker.com/guide/afl/barindex.html2026-08-31
  8. 08AmiBroker User's Guide — Explorationamibroker.com/guide/h_exploration.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.