Skip to content
Level 1 · Chart ReaderLessonPart 07 · page 2 of 522 min
22Minutes
9AFL functions
8Sources
StandardRequires
AFL functions taught here9

Multi-Bar Candlestick Patterns

One bar gives you a handful of named shapes. Two bars give you hundreds. Three give you thousands. This lesson builds the two- and three-bar vocabulary properly — you will need it — and then does the arithmetic that most treatments of it leave out: how many patterns you could invent, and what that number does to the credibility of whichever one you eventually like best.

Everything multi-bar starts with the ability to reach backwards. In AFL that is Ref(), which shifts an array in time. A negative period looks back; a positive one looks forward, into bars that had not printed, which is the mechanism behind most look-ahead bugs. Part 9 treats it properly. For now you need one form:

Fragment — not a complete formula

PrevOpen = Ref( Open, -1 ); // yesterday's open, aligned onto today's bar
PrevHigh = Ref( High, -1 );
PrevLow = Ref( Low, -1 );
PrevClose = Ref( Close, -1 );

After those four lines, every bar carries eight prices instead of four, and a two-bar pattern is just a condition on the eight.

Engulfing: one name, two different patterns

Section titled “Engulfing: one name, two different patterns”

The bullish engulfing pattern is described as a down bar followed by an up bar that “engulfs” it. The ambiguity is in the word engulfs, and it is not a small one.

The body definition asks whether today’s body covers yesterday’s body:

Fragment — not a complete formula

BodyEngulfUp = Close > Open // today closed up
AND PrevClose < PrevOpen // yesterday closed down
AND BodyBottom <= PrevBodyBottom // covers the bottom of it
AND BodyTop >= PrevBodyTop; // and the top of it

The range definition asks whether today’s whole bar covers yesterday’s whole bar, wicks included:

Fragment — not a complete formula

RangeEngulfUp = Close > Open
AND PrevClose < PrevOpen
AND High >= PrevHigh
AND Low <= PrevLow;

These are not two ways of writing the same test. The range version is much stricter, because a bar must exceed both extremes rather than both body edges, so it fires far less often. On ten years of daily data on a liquid instrument you can easily find several times as many body-engulfing bars as range-engulfing ones.

Both definitions are in common use, both are called “bullish engulfing”, and almost nobody says which one they mean. If you ever cannot reproduce someone’s pattern count, this is the first thing to check.

There is a third decision buried in both versions: what happens when today’s body edge exactly equals yesterday’s. Written with <= and >=, as above, ties count. Written with < and >, they do not. On instruments with coarse tick sizes, or on low-priced instruments, ties are common enough to move the count noticeably.

Inside and outside bars, and why AmiBroker’s answer may differ from yours

Section titled “Inside and outside bars, and why AmiBroker’s answer may differ from yours”

AmiBroker ships two of these relationships as built-in functions, and their documented definitions are worth reading precisely.

Inside() returns true when today’s high is lower than yesterday’s high and today’s low is higher than yesterday’s low. Both comparisons are strict. A bar whose high exactly equals yesterday’s high is therefore not an inside bar as far as Inside() is concerned, even though most people drawing it on paper would call it one.

Outside() returns true on an outside bar. GapUp() returns true when yesterday’s high is lower than today’s low, and GapDown() when yesterday’s low is higher than today’s high — note that these are full gaps between the bar extremes, not merely an open away from the previous close, which is what many traders mean by the word.

This is a small example of a large habit. When a function exists, read its definition before assuming it matches the one in your head, and when your own version disagrees with it, find out which bars differ before deciding who is right.

Three bars, and the growth of unstated parameters

Section titled “Three bars, and the growth of unstated parameters”

Three-bar patterns are built by composing the vocabulary you already have. A morning star, for example, is usually described as: a long down bar, then a small-bodied bar that gaps below it, then a long up bar that closes well into the body of the first.

Count the decisions in that one sentence:

  • how long is “long”, as a fraction of range or of ATR
  • how small is “small”
  • must the gap be a full gap, as GapDown() defines it, or just a lower open
  • how far into the first body is “well into” — a third, a half, more
  • does the middle bar’s colour matter
  • does the third bar have to close above the first bar’s open, or merely inside it
  • and, as ever, what prior trend is required, measured how

Seven decisions, most of them unstated in the description, each with several plausible settings. The pattern in the book is one point in a space of thousands of patterns, and the point that made it into the book was chosen by somebody looking at charts.

Do the arithmetic honestly and it becomes uncomfortable quickly.

Classify each bar coarsely — three body sizes (small, medium, large), three close positions (lower third, middle, upper third), and two directions. That is 18 states per bar. It is a deliberately crude classification; nobody would call it detailed.

The pattern space, one step at a time

  1. 18 single-bar states3 body sizes × 3 close positions × 2 directions
  2. 324 two-bar sequences18 × 18
  3. 5,832 three-bar sequences18 × 18 × 18
  4. 52,488 with gapsadd gap up / gap down / no gap at each of the two transitions
  5. 209,952 with a context filterfour plausible definitions of "after a decline"
None of these steps is unreasonable on its own. The product is the problem.

You will not test two hundred thousand patterns. But you do not need to. The number that matters is how many you tried before you settled on the one you are now excited about — and that number is usually much larger than people remember, because it includes every threshold you nudged, every context filter you added, and every variant you abandoned after a quick look.

Suppose none of the patterns in your search has any real association with what follows. Test each one and accept it as “significant” whenever it clears a 5% threshold. The probability that at least one of them clears it by chance alone is 1 - 0.95^k, where k is the number of patterns tested:

Patterns tested Chance at least one looks significant
1 5.0%
5 22.6%
10 40.1%
20 64.2%
50 92.3%
100 99.4%
324 above 99.9%

At 324 two-bar patterns — the crude classification above, tested exhaustively — you would expect roughly 16 of them to clear a 5% threshold even if the market were pure noise. Sixteen patterns, each with a plausible-sounding story available after the fact, each with a chart you could put in a presentation.

There are three practical responses, in increasing order of usefulness:

  1. Adjust the threshold. The crude Bonferroni correction divides your significance level by the number of tests: 20 tests at an overall 5% level means demanding 0.25% from each. It is conservative and easy to apply, and it makes most pattern results disappear, which is informative in itself.
  2. Hold data back. Define the pattern on one period, then measure it on a period you have never looked at. Parts 30 and 32 build this into a discipline.
  3. Have a reason first. A pattern proposed because you can say why it might work, before you measure it, is a single test. A pattern found by ranking hundreds of candidates is not, and no amount of subsequent storytelling converts it into one.

Keeping a written research log — every pattern tried, every threshold moved, dated — is what makes the third response possible. It is unexciting and it is the difference between research and rummaging.

Put both engulfing definitions, both inside-bar definitions, Outside() and the two gap functions on one chart, and report all the counts at once. The purpose is not to find signals. It is to make definitional disagreements visible as numbers, so that a conversation about what “engulfing” means can be settled in seconds.

Complete runnable AFL

engulfing-and-bars.afl
// engulfing-and-bars.afl
// Part 7 - Multi-Bar Candlestick Patterns
//
// Counts four two-bar relationships on the chart in front of you, so that a
// disagreement about a definition can be settled with numbers instead of
// adjectives:
//
// - bullish and bearish engulfing, under BOTH definitions in common use
// - inside bars, comparing AmiBroker's built-in Inside() with a hand-written
// version that allows ties
// - outside bars, via Outside()
// - gaps, via GapUp() and GapDown()
//
// Assumptions:
// - "Engulfing" is ambiguous in the literature. The body definition asks
// whether today's body covers yesterday's body; the range definition asks
// whether today's high-low covers yesterday's high-low. They are different
// patterns wearing the same name, so the formula reports both counts.
// - Nothing here is a trading signal. It is a labelling exercise whose whole
// purpose is to show how sensitive a count is to a definition.
_SECTION_BEGIN("Two-bar relationships");
// Cum() no longer forces AmiBroker to process every bar (that changed in
// version 5.30), so a running count would otherwise depend on how much history
// the chart happened to compute. -2 means "all bars", which makes the counts
// below reproducible.
SetBarsRequired( -2, -2 );
UseRangeDefinition = ParamToggle("Engulfing definition", "Body|Range", 0 );
MinBodyPct = Param("Minimum body as % of range", 20, 0, 60, 5 );
// -------------------------------------------------- yesterday's four prices
PrevOpen = Ref( Open, -1 );
PrevHigh = Ref( High, -1 );
PrevLow = Ref( Low, -1 );
PrevClose = Ref( Close, -1 );
PrevBodyTop = Max( PrevOpen, PrevClose );
PrevBodyBottom = Min( PrevOpen, PrevClose );
BodyTop = Max( Open, Close );
BodyBottom = Min( Open, Close );
BodyPct = 100 * SafeDivide( BodyTop - BodyBottom, High - Low, 0 );
BigEnough = BodyPct >= MinBodyPct;
// ------------------------------------------------------- engulfing, twice
// Body definition: today's body covers yesterday's body.
BodyEngulfUp = Close > Open AND PrevClose < PrevOpen
AND BodyBottom <= PrevBodyBottom AND BodyTop >= PrevBodyTop;
BodyEngulfDown = Close < Open AND PrevClose > PrevOpen
AND BodyBottom <= PrevBodyBottom AND BodyTop >= PrevBodyTop;
// Range definition: today's whole bar covers yesterday's whole bar. This is
// Outside() plus a direction requirement, and it is a much rarer event.
RangeEngulfUp = Close > Open AND PrevClose < PrevOpen
AND High >= PrevHigh AND Low <= PrevLow;
RangeEngulfDown = Close < Open AND PrevClose > PrevOpen
AND High >= PrevHigh AND Low <= PrevLow;
BullEngulf = IIf( UseRangeDefinition, RangeEngulfUp, BodyEngulfUp ) AND BigEnough;
BearEngulf = IIf( UseRangeDefinition, RangeEngulfDown, BodyEngulfDown ) AND BigEnough;
// ------------------------------------------------- inside, outside and gaps
// Inside() is documented as: today's high LOWER than yesterday's high AND
// today's low HIGHER than yesterday's low. Both inequalities are strict, so a
// bar that exactly equals yesterday's high is not an inside bar. The
// hand-written version below allows those ties, and the two counts differ.
BuiltInInside = Inside();
TiesAllowed = High <= PrevHigh AND Low >= PrevLow;
Disagreement = BuiltInInside != TiesAllowed;
BuiltInOutside = Outside();
Gapped = GapUp() OR GapDown();
// ------------------------------------------------------------------ display
// SetBarFillColor must be called BEFORE the Plot() it applies to.
SetBarFillColor( IIf( Close > Open, colorPaleGreen, colorRose ) );
Plot( Close, "Price", colorDefault, styleCandle );
PlotShapes( IIf( BullEngulf, shapeUpArrow, shapeNone ), colorGreen, 0, Low, -16 );
PlotShapes( IIf( BearEngulf, shapeDownArrow, shapeNone ), colorRed, 0, High, 16 );
PlotShapes( IIf( BuiltInInside, shapeSmallCircle, shapeNone ), colorBlue, 0, High, 30 );
PlotShapes( IIf( BuiltInOutside, shapeHollowCircle, shapeNone ), colorOrange, 0, High, 30 );
PlotShapes( IIf( Disagreement, shapeStar, shapeNone ), colorViolet, 0, Low, -30 );
Title = Name() + " " + Date()
+ "\nEngulfing definition in use: "
+ WriteIf( UseRangeDefinition, "range (high-low covers high-low)",
"body (body covers body)" )
+ "\nBullish engulfing: " + NumToStr( Cum( BullEngulf ), 1.0 )
+ " Bearish engulfing: " + NumToStr( Cum( BearEngulf ), 1.0 )
+ "\nBody definition would give: " + NumToStr( Cum( BodyEngulfUp AND BigEnough ), 1.0 )
+ " up / " + NumToStr( Cum( BodyEngulfDown AND BigEnough ), 1.0 ) + " down"
+ "\nRange definition would give: " + NumToStr( Cum( RangeEngulfUp AND BigEnough ), 1.0 )
+ " up / " + NumToStr( Cum( RangeEngulfDown AND BigEnough ), 1.0 ) + " down"
+ "\nInside() " + NumToStr( Cum( BuiltInInside ), 1.0 )
+ " ties-allowed version " + NumToStr( Cum( TiesAllowed ), 1.0 )
+ " they disagree on " + NumToStr( Cum( Disagreement ), 1.0 ) + " bars"
+ "\nOutside() " + NumToStr( Cum( BuiltInOutside ), 1.0 )
+ " gaps " + NumToStr( Cum( Gapped ), 1.0 )
+ " total bars " + NumToStr( Cum( 1 ), 1.0 );
_SECTION_END();

Download engulfing-and-bars.afl104 lines

The formula lifts yesterday’s four prices onto today’s bar with Ref(), then states each relationship as a separate named condition. Both engulfing definitions are computed every time; the ParamToggle() only decides which one drives the arrows, while the title reports both counts regardless. The inside-bar section computes AmiBroker’s Inside() and a hand-written version that allows ties, and marks the bars where they disagree with a star. The title carries eight counts, which together make up a small, reproducible fact sheet about the chart in front of you.

  • Ref( array, -1 ) shifts an array one bar forward in position, so that each bar can see the previous bar’s value.
  • Inside() and Outside() are AmiBroker’s built-in inside-day and outside-day tests, both documented under “Basic price pattern detection”.
  • GapUp() and GapDown() test for full gaps between bar extremes.
  • ParamToggle( name, "A|B", default ) produces a two-state parameter; the first string is the false value and the second is the true value.
  • WriteIf( condition, "true text", "false text" ) picks between two strings, used here so the title states which definition is currently driving the arrows.

Switch the engulfing definition parameter from Body to Range and watch the arrows thin out. The title’s two count lines do not change when you switch, because both are always computed — so you can read the ratio directly. Write it down. That ratio is the size of the definitional disagreement, and you now have it as a number rather than an opinion.

Then find a star. Zoom in on it and check by eye that the bar’s high or low exactly equals the previous bar’s. If none of your instruments produces a star, try one with a coarser tick size or a lower price.

  • No arrows at all. MinBodyPct is too high, or the instrument gaps so much that bodies rarely overlap. Lower it to zero first to confirm the rest works.
  • Arrows on almost every bar. MinBodyPct is at zero and the body definition is catching every bar that merely closed the other way. This is a genuine property of the loose definition, not a bug.
  • Counts change when you scroll. Remove the SetBarsRequired( -2, -2 ) line and they will; leave it in and they will not.
  • The star markers never appear. On instruments where exact price ties are rare, they legitimately do not occur. Check a lower-priced instrument before assuming the test is wrong.

Add a third definition of engulfing — for instance, one that requires today’s close to exceed yesterday’s high rather than merely yesterday’s open — and add its count to the title. You now have three numbers for one pattern name. Decide which you would report, and write down why.

You can now express a two-bar relationship as arithmetic on eight prices, you know that two widely used definitions share the name “engulfing”, and you know that AmiBroker’s Inside() uses strict inequalities that your hand-written version probably does not.

More importantly, you have the count. The number of patterns available to be invented is large enough that a broad search will almost always throw up an impressive-looking one, whether or not the market had anything to do with it. Everything in the rest of this part follows from taking that seriously.

Check your understanding

Question 1. Yesterday: High 42.00, Low 41.00. Today: High 42.00, Low 41.50. Does AmiBroker Inside() report an inside bar?
Show the answer and why

Answer: No

Inside() is documented as today’s high LOWER than yesterday’s high and today’s low HIGHER than yesterday’s low. Both are strict. Today’s high equals yesterday’s, so the first condition fails. A hand-written version using <= and >= would report it as an inside bar, which is exactly why the two counts differ.

Question 2. You test 20 unrelated candlestick patterns, each at a 5% significance threshold, on data with no real structure. Roughly what is the chance that at least one clears the threshold?
Show the answer and why

Answer: About 64%

1 − 0.95 to the power 20 is about 0.64. This is why "I found one that works" is close to meaningless without the number of things tried alongside it.

Question 3. Which of these change the number of bars a bullish engulfing definition selects? Select all that apply.
Show the answer and why

Answer: Using the range definition instead of the body definition, Requiring a minimum body size as a fraction of range, Using >= instead of > when comparing today’s body top with yesterday’s

The first three are all definitional choices, and each of them moves the count. The last changes only the rendering. Every count you quote is a count under a specific set of the first three.

Question 4. Which response to the multiple-comparison problem is strongest?
Show the answer and why

Answer: Specifying the pattern and its thresholds before looking at the results, and measuring it once

A single pre-specified test carries the significance level it claims. Post-hoc justification of a search winner does not convert a search into a test, and reporting only the largest effect is the mechanism that creates the problem rather than a defence against it.

Sources for this lesson

8 verified · checked 2026-08-31

  1. 01AFL Function Reference — Insideamibroker.com/guide/afl/inside.html2026-08-31
  2. 02AFL Function Reference — Outsideamibroker.com/guide/afl/outside.html2026-08-31
  3. 03AFL Function Reference — GapUpamibroker.com/guide/afl/gapup.html2026-08-31
  4. 04AFL Function Reference — GapDownamibroker.com/guide/afl/gapdown.html2026-08-31
  5. 05AFL Function Reference — Refamibroker.com/guide/afl/ref.html2026-08-31
  6. 06AFL Function Reference — ParamToggleamibroker.com/guide/afl/paramtoggle.html2026-08-31
  7. 07AFL Function Reference — WriteIfamibroker.com/guide/afl/writeif.html2026-08-31
  8. 08AFL Function Reference — Cumamibroker.com/guide/afl/cum.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.