Darvas Box

All functions · nseries package

The classic box and its four views

Nicolas Darvas bought stocks breaking out of a “box”, a range that forms after a new high. He did not set down mechanical rules; the four methods of this category follow Daryl Guppy’s statement of the classic box in “Darvas-Style Trading” (Technical Analysis of Stocks & Commodities, May 2005), cross-read with the MetaStock formulas by Matthew Ford printed with his “Something Darvas, Something New” (June 2005) where the text is silent. They are views of one state machine over the highs (the receiver) and the lows l, with the same arguments (l, lookback):

MethodOn each barBefore the first box
DarvasBoxTopthe top of the active box, the high of its top barNaN
DarvasBoxBottomthe bottom of the active box, the low confirmed after the topNaN
DarvasBoxAgethe number of bars since the top bar of the active boxNaN
DarvasBoxBreakout+1 on a high above the active box, -1 on a low below it, 0 otherwise0

DarvasBoxTop’s doc carries the whole machine; the others summarise it. In short:

  • The top. Bar k is a top when k >= lookback, its high is at least each of the lookback highs before it (a new high for the period; an equal high counts) and the highs of the three bars after it are strictly lower. The top is confirmed on bar k+3, and it is sought only while no box is active. While a confirmed top waits for its bottom, a strictly higher high abandons it, a newly confirmed top with a different high replaces it, and one with an equal high is ignored. Darvas started his boxes from a new 12-month high, about lookback 252 on daily bars; the MetaStock formulas use 100 bars.
  • The bottom. The bottom candidate is the latest bar holding the lowest low from the top bar on, so an equal or lower low moves it to the later bar; a NaN low is not a candidate. The candidate is confirmed on the third bar after it when the three lows after it are all strictly higher, and that bar completes the box. When the top bar’s own low is the lowest, the top and the bottom are confirmed on the same bar, k+3: Guppy’s special case, a box of four bars, the earliest a box can complete.
  • The break. A high strictly above the top or a low strictly below the bottom ends the box; highs equal to the top and lows equal to the bottom touch it. An outside bar, which breaks both sides, reads 0 and still ends the box. The three level views read NaN from the break bar until the next box completes, and the search for a new top resumes on the break bar. There is no expiry: a box stays active until a bar breaks it.
  • Missing and extreme values act only through the comparisons, which are false with NaN: a NaN high in the window, on the top bar or among the three highs after it stops that top, and a NaN high or low on a bar of an active box breaks nothing on that side while a valid value beside it still counts. +Inf and -Inf are ordinary values, +0 and -0 compare equal, the levels keep the bits of the bar they come from, and lows above highs are not checked.

The machine decides everything on the bar that confirms it, so nothing is redrawn and the output on bar t uses bars 0 to t only. The first box completes on bar lookback + 3 at the earliest and the first break comes on bar lookback + 4 at the earliest. The machine is latched: a run on a slice of the history agrees with the full run from the bar after the first bar, at least lookback + 2 bars into the slice, on which neither run holds a box or a waiting top.

The MetaStock formulas differ from Guppy’s text in ways that are not followed, among them: they require the three highs after the bottom bar to be below the top as well, so a high equal to the top delays the bottom; they fire on the first bar of each pattern only; they hold the box levels after a break until the next box completes (the held levels below reproduce this); they can move the top of an active box once the old top has left the look-back window; and they can complete a box whose top a higher high exceeded before its bottom formed, a top the machine abandons.

Not built. A view of the waiting top (the confirmed top before its bottom is found), Guppy’s ghost boxes, the breakout variant’s second valid box after a downtrend breakout, Darvas’s volume condition, a machine that breaks on closes, an expiry at a maximum length, and one run that returns all four views. Each view runs the machine again.

Evidence. Guppy reports reliabilities of “around 80%” for 12-month highs and “around 70%” for six-month highs without saying what he measured. Brent Penfold’s results for his Darvas rules come from his own unprinted code, and he writes of his rules that “Given their fragile nature I’d vote to remove Darvas’s Box for not following good design principles”, though he keeps the strategy in his top ten. No source gives a test of the box as built here.

Recipes

The blocks share their names: h, l and c are the highs, lows and closes, n is the look-back, tdy is the trading-day-of-the-year key (see “Calendar keys” under “Comparison”), and each block may use the names of the blocks before it.

Penfold’s setup (Brent Penfold, The Universal Tactics of Successful Trend Trading, Wiley, 2021, pp. 204-207). Penfold buys a break of the high of a box 20 to 100 daily bars long and no taller than 5 times the 20-bar average true range. He calls the rules his own interpretation of Darvas and does not say how a box forms, how its length is counted, or which average true range he reads at which bar. The recipe takes n as a parameter, counts a box’s length from its top bar to the bar before its break, and reads ATR, the simple average, on the bar before the break; ATRWilder is Wilder’s smoothing.

top, bot := h.DarvasBoxTop(l, n), h.DarvasBoxBottom(l, n)
age, brk := h.DarvasBoxAge(l, n), h.DarvasBoxBreakout(l, n)
size := age.Shift(1).Add(1) // on a break bar, the box's length from its top bar to the bar before the break
atr := c.ATR(h, l, c, 20)   // or c.ATRWilder(h, l, c, 20)
height := top.Shift(1).Sub(bot.Shift(1))
ok := size.GTE(20).Mul(size.LTE(100)).Mul(height.LTE(atr.Shift(1).Mul(5)))
buy := brk.GT(0).Mul(ok) // 1 on the break above a box that meets both limits

The limits filter the breakouts and do not change the machine, so a box that fails them still blocks new boxes until a bar breaks it.

Trend filter. Penfold buys only when the “previous close must be above the previous year’s high” and sells only below the previous year’s low. His Figure 6.19 draws the previous year’s low as a flat line that steps once, a calendar year’s level, which needs the tdy key:

ny := tdy.EQ(1)                                                      // the first session of each calendar year
pyh := h.HighestSince(ny).Shift(1).SetN(1, math.NaN()).ValueWhen(ny) // the high of the previous calendar year
pyl := l.LowestSince(ny).Shift(1).SetN(1, math.NaN()).ValueWhen(ny)  // the low of the previous calendar year
upTrend := c.GT(pyh).Shift(1)                                        // the previous close above that high
dnTrend := c.LT(pyl).Shift(1)
entry := buy.Mul(upTrend)

On the first session of each year pyh takes the highest high of the year that has just ended and holds it for the year. It is NaN, so the filter reads 0, until the history holds a whole calendar year: a first year that starts after its first session is not used, and SetN(1, math.NaN()) replaces the 0 that Shift puts on bar 0, which would otherwise read as the previous year’s high when the history starts on a year’s first session. A NaN high makes its year’s high NaN, and the filter reads 0 through the next year. The rolling 52-week form, c.GT(h.Highest(252).Shift(1)).Shift(1), passes no upward breakout of a box whose top bar lies in its window, for closes at or below their highs: the previous close is at most the previous high, which is at most the box’s top, the high of a bar in the window.

The short side. Penfold’s rules are the same for buying and selling, and his Figure 6.19 shows boxes in a falling market, where no new high of a long look-back starts a classic box. No source prints a mirror box. The mirror of this one, started by a new low with three strictly higher lows, then the latest highest high followed by three strictly lower highs, and broken by a low below its bottom or a high above its top, is this machine on the negated, swapped series, exactly:

nl, nh := l.Mul(-1), h.Mul(-1)
mirBot := nl.DarvasBoxTop(nh, n).Mul(-1)             // the mirror box's bottom, the low that starts it
mirTop := nl.DarvasBoxBottom(nh, n).Mul(-1)          // its top
mirAge := nl.DarvasBoxAge(nh, n)                     // the number of bars since its starting low
mirBrk := nl.DarvasBoxBreakout(nh, n).Mul(-1).Add(0) // -1 on a low below the mirror box, +1 on a high above it
mirSize := mirAge.Shift(1).Add(1)
mirOK := mirSize.GTE(20).Mul(mirSize.LTE(100)).Mul(mirTop.Shift(1).Sub(mirBot.Shift(1)).LTE(atr.Shift(1).Mul(5)))
sell := mirBrk.LT(0).Mul(mirOK).Mul(dnTrend) // Penfold's short entry

Mul(-1) flips the sign of every zero, so the levels take a second Mul(-1) to get back the bits of the bars they come from (a -0 low is a -0 mirror bottom), and the breakout takes .Add(0) to turn the -0 of a bar without a break into +0.

Held levels and Guppy’s close rules. Guppy finds the entry more reliable “if you wait for a close above the top” of the box, and writes that in modern markets “the preference for an exit signal is for the close to be below” its bottom. The views end a box at the first high or low beyond it, so the close rules read the levels of the last box, held after its break until the next box completes, as the MetaStock formulas hold them:

act := top.EQ(top)                             // 1 while a box is active
heldTop := top.ValueWhen(act)                  // the last box's top, held after its break
heldBot := bot.ValueWhen(act)                  // the last box's bottom
closeBuy := c.GT(heldTop.Shift(1)).SetN(1, 0)  // a close above the top held on the bar before
closeExit := c.LT(heldBot.Shift(1)).SetN(1, 0) // a close below the bottom held on the bar before
completed := act.Sub(act.Shift(1)).GT(0)       // 1 on the bar that completes a box

act is 0 where the top is NaN. A box does not complete on the bar that breaks the one before it, so every completion follows a bar without an active box and completed marks each completion exactly. The held levels are NaN until the first box completes, and SetN(1, 0) clears bar 0, where Shift puts 0. closeBuy is 1 on every bar whose close is above the held top; the entry is its first such bar. Guppy’s aggressive trader buys “once the bottom of the box was confirmed”, in anticipation of a breakout, with the bottom as the stop: that entry is completed.

The stop. Penfold’s “Sell break of the 2-week low” (and “Buy break of the 2-week high” for a short) is a trade rule. On daily bars the ten-session reading is:

longStop := l.Lowest(10).Shift(1).SetN(1, math.NaN())   // the lowest low of the ten bars before this one
shortStop := h.Highest(10).Shift(1).SetN(1, math.NaN()) // the highest high of the ten bars before this one

Before bar 10 the window holds the bars there are, and a NaN low makes the stop NaN while it is in the window. Penfold’s other strategies count weeks as calendar weeks, which weekly bars give: l.Lowest(2).Shift(1) over weekly lows.

The direction of the last break. The package’s level-break latch over the box levels holds the direction of the last breakout:

dir := h.LevelBreakDirection(l, top, bot) // +1 after a break above a box, -1 after a break below one

Where no bar breaks both levels, dir equals brk.Hysteresis(0, 0). On an outside bar the breakout reads 0, while the latch flips from +1 to -1 or from -1 to +1, and stays 0 from 0.

DarvasBoxAge

func (s Series) DarvasBoxAge(l Series, lookback int) Series

DarvasBoxAge returns the number of bars since the top bar of the active Darvas box, and NaN on every bar without an active box.

The receiver holds the highs and l the lows of the same bars, and lookback is the look-back of the new high that starts the box; DarvasBoxTop gives the rules of the machine. On a bar t of an active box whose top bar is k the age is t - k, a whole number: at least 3 on the bar that completes the box (3 when the top and the bottom are confirmed on the same bar), rising by 1 a bar while the box is active, and NaN from the break bar on. A box broken on bar t is age[t-1] + 1 bars long, the length that the 20-to-100-bar limit of Brent Penfold reads as age.Shift(1).Add(1) on the break bar; a box older than 100 bars stays active until it breaks. The recipe is in the manual’s “Darvas Box” section.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is Series{}; when lookback < 1 the result is all +0.

Warm-up: the result is NaN rather than 0 until the first box completes, on bar lookback + 3 at the earliest, since the start depends on the data. Timing: the output on bar t uses bars 0 to t only. The machine is latched and so path-dependent: a run on a slice agrees with the full run from the bar after the first bar, at least lookback + 2 bars into the slice, on which both runs are idle. The inputs are not modified, the result shares no storage with them, and each call costs O(len(s)) time.

DarvasBoxBottom

func (s Series) DarvasBoxBottom(l Series, lookback int) Series

DarvasBoxBottom returns the bottom of the active Darvas box, the low confirmed after its top, from the bar that completes the box to the bar before it is broken, and NaN on every other bar.

The receiver holds the highs and l the lows of the same bars, and lookback is the look-back of the new high that starts the box; DarvasBoxTop gives the rules of the machine. On the bar u that completes a box the bottom is l[u-3], the latest lowest low from the top bar on, confirmed by the three strictly higher lows after it, and it is copied verbatim with its bits (a -0 low gives a -0 bottom). It is constant while the box is active and NaN from the break bar on, until the next box completes. The mirror bottom of the short side and the held bottom of the close-confirmed exits are recipes in the manual’s “Darvas Box” section.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is Series{}; when lookback < 1 the result is all +0.

Warm-up: the result is NaN rather than 0 until the first box completes, on bar lookback + 3 at the earliest, since the start depends on the data. Timing: the output on bar t uses bars 0 to t only. The machine is latched and so path-dependent: a run on a slice agrees with the full run from the bar after the first bar, at least lookback + 2 bars into the slice, on which both runs are idle. The inputs are not modified, the result shares no storage with them, and each call costs O(len(s)) time.

DarvasBoxBreakout

func (s Series) DarvasBoxBreakout(l Series, lookback int) Series

DarvasBoxBreakout returns +1 on a bar whose high breaks above the active Darvas box, -1 on a bar whose low breaks below it, and 0 on every other bar.

The receiver holds the highs and l the lows of the same bars, and lookback is the look-back of the new high that starts the box; DarvasBoxTop gives the rules of the machine. The breaks are strict and test the box active after the previous bar: the result is +1 when the high is strictly above its top and the low is not below its bottom, and -1 when the low is strictly below its bottom and the high is not above its top. An outside bar, breaking both sides, reads 0 and still ends the box, as every break does, so the level views read NaN on the break bar. The values are -1, +0 and +1 only, with no -0 and no NaN.

With h the receiver, top := h.DarvasBoxTop(l, n) and bot := h.DarvasBoxBottom(l, n), the result is h.GT(top.Shift(1)).Sub(l.LT(bot.Shift(1))).SetN(1, 0) on every bar, and where no bar breaks both levels, h.LevelBreakDirection(l, top, bot) equals h.DarvasBoxBreakout(l, n).Hysteresis(0, 0), the direction of the last breakout. The filters of Brent Penfold, the short side by negation and the close-confirmed rules are recipes in the manual’s “Darvas Box” section.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is Series{}; when lookback < 1 the result is all +0.

Warm-up: the result is 0 until the first break, on bar lookback + 4 at the earliest. Timing: the output on bar t uses bars 0 to t only. The machine is latched and so path-dependent: a run on a slice agrees with the full run from the bar after the first bar, at least lookback + 2 bars into the slice, on which both runs are idle. The inputs are not modified, the result shares no storage with them, and each call costs O(len(s)) time.

DarvasBoxTop

func (s Series) DarvasBoxTop(l Series, lookback int) Series

DarvasBoxTop returns the top of the active Darvas box, the high that starts the box, from the bar that completes the box to the bar before it is broken, and NaN on every other bar.

The receiver holds the highs and l the lows of the same bars, as for BasingPointLowValue, and lookback is the number of bars before the top bar whose highs the top must match or exceed. The box is the classic box of Nicolas Darvas as Daryl Guppy states it, run as a state machine over the bars in order that decides everything on the bar that confirms it, so nothing is redrawn.

A bar k is a top when k >= lookback, its high is at least every high of the lookback bars before it (a new high for the period, where an equal high counts), and the highs of the three bars after it are all strictly lower; the top is then confirmed on bar k+3. A top is sought only while no box is active. The new-high test on bar k is h.GTE(h.Highest(lookback).Shift(1)) read at k, the inclusive look-back breakout (DonchianUpper(lookback) shifted one bar), with a NaN in the window failing it as it does here. While a confirmed top waits for its bottom, a strictly higher high abandons it, a newly confirmed top with a different high replaces it and restarts the bottom search from its own top bar, and a newly confirmed top with an equal high is ignored, the waiting top keeping its top bar and its candidate.

The bottom candidate is the latest bar holding the lowest low from the top bar on, so an equal or lower low moves it to the later bar, and a NaN low is not a candidate. A candidate has one chance to confirm, on the third bar after it: when the lows of the three bars after it are all strictly higher, that bar u completes the box, whose bottom is l[u-3]. When the low of the top bar is the lowest and the three lows after it are strictly higher, the top and the bottom are confirmed on the same bar, k+3, the earliest bar on which a box can complete.

The complete box is active until a bar whose high is strictly above its top or whose low is strictly below its bottom breaks it; highs equal to the top and lows equal to the bottom touch the box without breaking it. The break bar reads NaN and the search for a new top resumes on it. The top is the high of bar k, copied verbatim with its bits (a -0 high gives a -0 top), and is constant while the box is active. NaN enters only through comparisons, which are false with NaN: a NaN high in the window, on the top bar or among the three highs after it stops that top, a NaN low is not a candidate and stops a confirmation that needs it, and a NaN high or low on a bar of an active box breaks nothing on that side, while a valid value beside it still counts. The infinities are ordinary values (a +Inf top is not broken upwards), +0 and -0 compare equal, and lows above highs are not checked.

Nicolas Darvas started his boxes from a new 12-month high, about lookback 252 on daily bars; the MetaStock code that Daryl Guppy prints uses a look-back of 100 bars. Brent Penfold trades breakouts of boxes 20 to 100 bars long and no taller than 5 times the 20-bar average true range. These limits are recipes over the views, not parameters: the length of a box broken on bar t is age[t-1] + 1, so a box that fails them still blocks new boxes until it breaks. The trend filter of Penfold is the high of the previous calendar year, as with closes at or below their highs a rolling 252-bar high passes no upward breakout of a box whose top bar lies in its window. The short side is this box on the negated, swapped series: with h the receiver, l.Mul(-1).DarvasBoxTop(h.Mul(-1), n).Mul(-1) is the bottom of the mirror box. The levels held by top.ValueWhen(top.EQ(top)) serve the close-confirmed entries and exits of Daryl Guppy, and with act := top.EQ(top) the completion event is act.Sub(act.Shift(1)).GT(0). These recipes are in the manual’s “Darvas Box” section.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is Series{}; when lookback < 1 the result is all +0; when lookback > len(s) - 4 no top can be confirmed, so the result is all NaN.

Warm-up: the result is NaN rather than 0 until the first box completes, on bar lookback + 3 at the earliest, since the start depends on the data. Timing: the output on bar t uses bars 0 to t only, so every prefix of the inputs gives the first values of the full result bit for bit. The machine is latched and so path-dependent: a run on a slice agrees with the full run, bit for bit, from the bar after the first bar, at least lookback + 2 bars into the slice, on which both runs are idle, with no box and no waiting top; before it a box or a top that one run has not seen can make them differ. The inputs are not modified (l may be the receiver), the result shares no storage with them, concurrent calls are safe, and each call costs O(len(s)) time whatever lookback is.