Swings and pivots

All functions · nseries package

Sections: Swings · Swing Points · Swing State · Pivots · Floor Trader Pivots · Darvas Box

Swings

Swings

func (s Series) Swings(opn Series, high Series, low Series, cls Series, hint ...int) []int

Swings returns a slice of integers that represents swing highs and lows in a series, where zero indicates no swing, a positive value indicates a swing high, and a negative value indicates a swing low. The magnitude of the value indicates how many values back in the series the high/low occurred. Unlike SwingsHL, outside bars are treated as directional bars based on whether the open is greater than the close (down bar) or less than the close (up bar). Inside bars are handled the same way as in SwingsHL. An optional hint parameter can be provided to specify the first known swing. A nonzero hint outside the high/low input’s index range returns len(high) zeros. Zero means no hint.

When one bar is both a swing high and a swing low (an adjacent pair in the raw list), the encoded result can hold only one value for that bar and keeps the later of the two.

From this release, a high followed by a reversal on the same bar is now reported as a high and a low on that bar rather than as two consecutive highs.

Revision: the result describes the whole input, and a swing is reported only once a later bar confirms it. When bars are appended, a final snap pass can move the most recent swing to a later bar while the following bars extend it, and new swings are added after it. Earlier swings have not been seen to change: re-running on every prefix of 500 random walks (149,000 prefix steps, 3 October 2026) revised the latest swing 1,712 times and an earlier one never. That is an observation, not a guarantee. For a swing list that is append-only by construction, see SwingPointIndices with SwingAppendOnly.

SwingsHL

func (s Series) SwingsHL(high Series, low Series, hint ...int) []int

SwingsHL returns a slice of integers that represents swing highs and lows in a series, where zero indicates no swing, a positive value indicates a swing high, and a negative value indicates a swing low. The magnitude of the value indicates how many values back in the series the high/low occurred. For example, for a series of 10 bars with a swing high on the 3rd bar, a low on the 5th bar, and a high on the 9th bar, the returned series would be: [0, 0, 7, 0, 0, -4, 0, 0, 1, 0]. The swings are calculated according to Brent Penfold’s algorithm. An optional hint parameter can be provided to specify the first known swing. If positive, it indicates a swing high at that bar index; if negative, it indicates a swing low at the absolute value of that index. No swing is reported before the hint bar, and none is reported on the hint bar with the opposite sign; earlier bars are still read to classify inside, outside and directional bars. The hinted swing itself can be replaced by a later, more extreme swing of the same kind, as the snap pass may replace any latest swing. A nonzero hint outside the high/low input’s index range returns len(high) zeros. Zero means no hint.

When one bar is both a swing high and a swing low (an adjacent pair in the raw list), the encoded result can hold only one value for that bar and keeps the later of the two.

From this release, a stacked outside bar no longer re-reports a swing it has already reported (which could put entries out of bar order), and a hinted call no longer panics on such windows. Duplicate outside bars no longer break the alternation of highs and lows, and hinted calls no longer report swings before the hint bar or a zero entry.

Revision: the result describes the whole input, and a swing is reported only once a later bar confirms it (stacked outside bars can confirm swings several bars back). When bars are appended, a final snap pass can move the most recent swing to a later bar while the following bars extend it, and new swings are added after it. Earlier swings have not been seen to change: re-running on every prefix of 500 random walks (149,000 prefix steps, 3 October 2026) revised the latest swing 32 times and an earlier one never. That is an observation, not a guarantee. For a swing list that is append-only by construction, see SwingPointIndices with SwingAppendOnly.

SwingsPath

func (s Series) SwingsPath(highs Series, lows Series, paths []float64) []int

SwingsPath is an unimplemented placeholder that always returns an empty slice. It ignores all inputs. Use Swings or SwingsHL for the existing swing constructions, or the SwingHighEvent and SwingLowEvent family for causal confirmation series.

Swing Points

SwingPointIndices

func (s Series) SwingPointIndices(opn, high, low, cls Series, opts ...any) []int

SwingPointIndices returns swing point positions as sequential signed indices. Positive values are swing high bar indices, negative values are negated swing low bar indices.

A swing high and a swing low can be reported on the same bar. A bar that reverses the trend (typically an outside bar, or a bar whose high or low equals the trend’s extreme) can itself be the trend’s extreme, reported as the swing that ends the trend; it is also where the new trend starts, so if the new trend reverses before making a new extreme, the same bar is reported again as the opposite swing. Such a pair appears as adjacent entries b, -b (or -b, b), the trend’s extreme first (at level 1; at levels 2 and above a promoted pair is listed high first).

At level 1, the default output has strictly alternating signs. Absolute bar indices never decrease and are equal only within an adjacent opposite-sign pair; no signed index repeats.

Optional arguments control behaviour (see SwingPoints for details): - int: hierarchical level (default 1) - bool: classifyOB mode (default false) - SwingOption: SwingAppendOnly (default off) skips the snap pass and makes level 1 append-only (see below); any other SwingOption value is ignored

Revision: by default a snap pass moves a confirmed swing forward to the bar that truly holds the extreme, stopping before the next swing’s bar (or at the end of the data for the latest swing). The main loop only appends, and the snap of an earlier swing reads only bars before the next swing, all present when that swing was confirmed. So when bars are appended, only the most recent swing can change at level 1. It can move forward as later bars extend it, and back again when the next swing is confirmed; a backtest that reads historical swing positions should allow for that one moving entry.

With SwingAppendOnly the snap pass is skipped and level 1 is append-only: for any inputs of length n and any m <= n, the result for the first m bars (opn[:m], high[:m], low[:m], cls[:m]) is a prefix of the result for all n bars, for either classifyOB value. Level-1 bar indices are then non-decreasing (equal only within a same-bar pair) and highs and lows strictly alternate. At levels 2 and above promotion needs the next same-type swing and flattening may replace the latest entry, so the result can still change when bars are appended. WilliamsSwingDirection, WilliamsSwingHighValue and WilliamsSwingLowValue render the append-only level-1 swings bar by bar, as the leg and as the latest confirmed levels.

Level construction stops once a level produces no swings, since no higher level can be derived from an empty set; there is no other cap on level.

SwingPoints

func (s Series) SwingPoints(opn, high, low, cls Series, opts ...any) []int

SwingPoints returns a slice of integers representing swing highs and lows in OHLC data, where zero indicates no swing, a positive value indicates a swing high, and a negative value indicates a swing low. The magnitude of the value indicates how many bars back from the end of the series the swing occurred.

Caveat: a swing confirmed at the very last bar has a lookback of zero, which this encoding cannot distinguish from “no swing”; such a swing is silently dropped. Use SwingPointIndices if swings at the final bar matter to the caller.

The encoding also holds only one value per bar, while a swing high and a swing low can be reported on the same bar (see SwingPointIndices). Where two entries of the SwingPointIndices result share a bar, the later entry in that list wins.

Inside bars (bars whose range falls within the prior non-inside bar) are always skipped. Outside bar direction is inferred from the open/close relationship: close > open is bullish, close < open is bearish, and close == open continues the prevailing trend.

Optional arguments control behaviour: - int: hierarchical level (default 1). Level 1 = short-term swings, Level 2 = medium-term (swings of swings), Level 3+ = progressively longer-term. - bool: classifyOB mode (default false). When true, uses outside bar direction (from open vs close) to determine which swing extreme the bar can update. A bearish outside bar in an uptrend will NOT capture a new swing high. When false, the swing point is always the bar with the most extreme price regardless of outside bar direction. - SwingOption: SwingAppendOnly (default off) skips the snap pass described below. Level-1 swings are then append-only: the SwingPointIndices result for the first m bars is a prefix of the result for all the bars, for any m, so in that list a swing once reported keeps its bar. This encoding holds one value per bar, so when a later bar completes a same-bar pair (see SwingPointIndices) the slot changes sign and the earlier swing drops out of this slice. Any other SwingOption value is ignored.

Revision: by default, after the single pass that confirms swings, a snap pass moves a confirmed swing forward to each following bar that extends it (a higher high for a swing high, a lower low for a swing low), stopping at the first bar that does not, inside bars aside, and before the next swing’s bar, so that the swing sits on the bar that truly holds the extreme. The main loop only appends, and the snap of an earlier swing reads only bars before the next swing, all present when that swing was confirmed. So when bars are appended, only the most recent swing can change at level 1. It can move forward as later bars extend it, and back again when the next swing is confirmed; a backtest that reads historical swing positions should allow for that one moving entry. SwingAppendOnly removes the snap and guarantees append-only level 1 in SwingPointIndices.

In the default level-1 SwingPointIndices list, signs strictly alternate, absolute bar indices never decrease and are equal only within an adjacent opposite-sign pair, and no signed index repeats.

Levels 2 and above can still change when bars are appended, with or without SwingAppendOnly: promotion needs the next swing of the same type, and flattening may replace the latest entry.

Level construction stops once a level produces no swings, since no higher level can be derived from an empty set; there is no other cap on level.

Swing State

Causal swing state

These methods report swing structure bar by bar, from data up to each bar: a swing appears on the bar that confirms it and is not moved afterwards, so they can drive a backtest or a live strategy without repainting, given a fixed history start (the Clear, Gann and Williams methods are path-dependent: dropping old bars can change their later output). They differ from Pivots, Swings, SwingsHL and SwingPoints (under “Pivots”, “Swings” and “Swing Points”), which return index lists over a whole history and may revise a recent swing as more bars arrive.

Four constructions are offered:

  • Bar-count pivots (SwingHighEvent, SwingLowEvent and their Value and Age views, with MarketStructure on top): a bar n bars back that is strictly beyond every other value of the window [t-2n, t], confirmed on bar t. They take one series, so they work on highs, lows, closes or an oscillator.
  • Ron Black’s Clear method (ClearMethodDirection, ClearMethodLine, ClearMethodHighValue, ClearMethodLowValue): a swing turns when a bar’s whole range clears the highest low of an up swing or the lowest high of a down swing. It has no parameter.
  • Gann swings (GannSwingDirection, GannSwingHighValue, GannSwingLowValue): a swing turns up after n consecutive higher highs and down after n consecutive lower lows.
  • Williams swings (WilliamsSwingDirection, WilliamsSwingHighValue, WilliamsSwingLowValue): Larry Williams’s one-bar swings, which skip inside bars and settle outside bars by their open and close; a swing is confirmed when the leg reverses, as in SwingPointIndices with SwingAppendOnly.

Values and ages are NaN until the first swing; events are 0 or 1, and directions and structure are 0 until their first decision. The k-th previous swing of a bar-count view is its back argument; for the Clear, Gann and Williams views it is value.ValueWhen(event, k) over the turn events of the direction. For the Clear method and the Williams swings these are dir.Delta(1).LT(-1) (a turn down, which confirms a high) and dir.Delta(1).GT(1): the start, 0 to -1 or +1, confirms nothing. For Gann swings they are dir.Delta(1).LT(0) and dir.Delta(1).GT(0), because the first turn out of the undetermined 0 already confirms a swing.

Trend from swing levels

LevelBreakDirection is the Dow trend of a swing chart as a stop-and-reverse latch over any two level series: up after the highs break strictly above the upper level of the bar before, down after the lows break strictly below the lower level, with one flip per bar and 0 before the first break of one level alone. LevelBreakStop is the level that would reverse it, in force during the next bar. Away from bars that break both levels it equals up.GT(hi.Shift(1)).SetN(1, 0).Sub(dn.LT(lo.Shift(1)).SetN(1, 0)).Hysteresis(0, 0). SwingTrend(l, n) is the latch over the bar-count swings. Other levels give the other trends:

  • the Williams, Clear and Gann trend layers, for example h.LevelBreakDirection(l, h.WilliamsSwingHighValue(l, o, c), h.WilliamsSwingLowValue(l, o, c));
  • the Donchian breakout, h.LevelBreakDirection(l, h.Highest(n), l.Lowest(n));
  • the close-break form, c.LevelBreakDirection(c, h.SwingHighValue(n), l.SwingLowValue(n));
  • Penfold’s stop line, h.LevelBreakStop(l, h.SwingHighValue(n), l.SwingLowValue(n));
  • Livermore Reaction, under Compositions below.

Williams also projects a measured move, a swing high plus the height of the advance to it (his text uses the intermediate-term high, a level not offered here), and his three-bar system buys at l.Average(3) and sells at h.Average(3) in the direction of the trend.

Magee’s basing points (BasingPointDirection, BasingPointStop and their event and value views, under “Stops and Reversals”) are a related stop: a reaction low becomes a basing point once a number of bars trade wholly above its bar’s high, and the stop trails a fraction below it. That is a separation rule, not a window test or a level break, so there is no identity with the bar-count swings, MarketStructure or this latch.

Ties follow one rule, an equal price is not more extreme and does not trigger, with two exceptions: Gann and Williams swings extend to the latest of equal extremes.

ConstructionTwo equal extremesA price equal to the trigger
Bar-count pivotsneither is a swing (the window is strict)does not apply
Clear methodkeeps the firstdoes not turn (a touch does not clear)
Gann swingskeeps the latestis not a higher high or lower low
Williams swingskeeps the latest, as SwingPointIndicesis not a move; an equal-range bar is inside
Level-break latchdoes not applydoes not break
Basing pointskeeps the firstis not a bar away or a new wave extreme

Views over the swing state

Two families read the swing state rather than build it.

  • Widner’s levels and oscillators (WidnerSupportLevel, WidnerResistanceLevel, WidnerSupportOscillator, WidnerResistanceOscillator): his confirmed swing lows (highs) as horizontal levels at any depth, from his own non-strict window rule with a level pushed only when it differs from the last, and the share of the six latest below the close. His N = 9 is n = 4. The levels give a level-break trend too: c.LevelBreakDirection(c, h.WidnerResistanceLevel(n), l.WidnerSupportLevel(n)).
  • Swing divergence (SwingHighDivergence, SwingLowDivergence, SwingHighDivergenceIndex, SwingLowDivergenceIndex): two consecutive bar-count swing highs (lows) of price compared with an oscillator at the same two bars, as regular-divergence flags and as Storz’s divergence index. Pivots on the oscillator instead are the same methods with the receiver and the argument exchanged.

Threshold swings

The directional-change state (DirectionalChange, under “Regime Detection”) confirms a swing when the price moves from its running extreme by more than a threshold, and its age counts the bars since the extreme that the latest confirmation confirmed. ShiftVariable reads a series a per-bar number of bars back, so over the age it turns the state into swing values, and ValueWhen and BarsSince over the confirmations give the swing levels, the earlier swings and their ages, as they do for the Clear, Gann and Williams views. Each threshold mode is a recipe over the same lines, not a method family: a fraction of the price (Merrill, Vakkur), a move in the price’s own units (Poster, Raftopoulos), or a threshold series such as a multiple of the average true range (Raschke).

// Fraction mode: closes c, theta a fraction (0.05 is 5%).
age := c.DirectionalChangeAge(theta)
ev := c.DirectionalChangeEvent(theta) // -1 confirms a swing high, +1 a swing low
ref := c.ShiftVariable(age)           // the last confirmed extreme's value, either kind, from the first
                                      // confirmation on; before it the age is 0 and ref is the bar itself
hi := ref.ValueWhen(ev.LT(0))         // the latest confirmed swing high; NaN before the first
lo := ref.ValueWhen(ev.GT(0))         // the latest confirmed swing low
hi1 := ref.ValueWhen(ev.LT(0), 1)     // the previous swing high (back = 1)
lo1 := ref.ValueWhen(ev.GT(0), 1)     // the previous swing low
hiAge := age.ValueWhen(ev.LT(0)).Add(ev.LT(0).BarsSince()) // bars since the latest high's extreme bar
dir := c.DirectionalChange(theta)     // the direction view is the state itself

The points and scaled modes replace the first two lines. Their families have no event method: the events are the state’s changes, and the state is the direction view.

// Points mode: pts a move in the closes' units.
state := c.DirectionalChangePoints(pts)
age := c.DirectionalChangePointsAge(pts)
ev := state.Delta().Clamp(-1, 1)
// ATR mode: highs h, lows l and closes c, with k from 2 to 3.
dist := c.ATR(h, l, c, 10).Mul(k).Shift(1).SetN(11, math.NaN())
state := c.DirectionalChangePointsScaled(dist)
age := c.DirectionalChangePointsAgeScaled(dist)
ev := state.Delta().Clamp(-1, 1)

At a confirmation bar t the age is the distance to the confirmed extreme, so ref[t] is the highest value from the previous confirmation bar to t for a -1 event and the lowest for a +1 event (from the first valid bar for the first event), the latest of equal extremes. hi, lo, hi1 and lo1 hold from that bar to the next confirmation of their kind, hiAge counts on from the high’s extreme bar, and each is NaN until its swing exists. The thresholds are strict: a source that counts a move equal to its threshold as a crossing passes math.Nextafter(theta, 0) or math.Nextafter(pts, math.Inf(-1)), as the methods’ docs say.

The ATR mode is Raschke’s volatility filter after Wilder (in Bensignor, ed., 2000): a new wave when the price moves k times the 10-bar simple average of the true range from the swing extreme. Shift(1) commits each threshold a bar before the bar it tests. ATR passes TrueRange through on bars 0 to 8, and its first average, at bar 9, includes bar 0’s range h[0] - l[0], which has no previous close; after the shift these values sit on bars 1 to 10, and SetN(11, math.NaN()) masks them with the shift’s 0 at bar 0, so the machine starts at bar 11, the first bar whose threshold averages ten true ranges that each have a previous close (DirectionalChangePointsScaled’s doc gives the same reason for any n). Raschke tests the up wave on the highs (“rallies above”); the scaled machine reads one series, so here both tests run on the closes, a departure until a high-low form of the scaled machine exists.

The two-wave trend on threshold swings is MarketStructure’s Hysteresis form over the four levels:

x := hi.GT(hi1).Mul(lo.GT(lo1)).Sub(lo.LT(lo1).Mul(hi.LT(hi1)))
trend := x.Hysteresis(0.5, -0.5) // 1 after a higher high and a higher low, -1 after a lower high and a lower low

The zigzag-anchored VWAP reads the age as a window length. VWMAVariable averages the last n[t] bars, so over the age its window starts on the bar after the last confirmed extreme, leaving the extreme bar out as Levine, Reyna and Coles leave the launch bar out of a MIDAS curve; the inclusive form puts the extreme bar in, and its mask keeps the bars before the first confirmation, where the age is 0, NaN:

mp := h.Add(l).Div(2)                                 // the mid price
zz := mp.VWMAVariable(v, age)                         // MIDAS form, the extreme bar left out
zzIn := mp.VWMAVariable(v, age.Add(1).Mul(age.GT(0))) // inclusive form, the extreme bar in

Any age series serves: the three modes’ ages above, or SwingHighAge(n) and SwingLowAge(n) of the bar-count pivots. The curve is NaN before the first confirmation and jumps at each confirmation bar to the window that starts after the newly confirmed extreme; it is causal, and nothing is redrawn. Roberts (Technical Analysis of Stocks & Commodities, November 2023) zigzags closes at a 10% reversal, inclusive, which is c.DirectionalChange(math.Nextafter(0.10, 0)) up to the rounding of his product tests, with Reyna’s volume weighting and the mid price; his published curve is redrawn from each new extreme, which the causal form replaces by the jump at the confirmation bar. Whether his sums hold the extreme bar was not checked against his article, so neither form is named as his. The package examples ExampleSeries_DirectionalChange_swingHighValue, ExampleSeries_DirectionalChangePoints_swingHighValue, ExampleSeries_DirectionalChangePointsScaled_swingHighValue and ExampleSeries_VWMAVariable_zigzag run the recipes on short vectors.

ClearMethodDirection

func (s Series) ClearMethodDirection(low Series) Series

ClearMethodDirection implements Black’s Clear method, returning 1 for an up swing and -1 for a down swing. It starts down on the first valid bar and is path-dependent: the first swings depend on the history’s start. A bar is valid when neither value is NaN and low <= high; invalid bars hold the previous state. The result is 0 before the first valid bar and contains no NaN. Unequal lengths, checked first, or empty inputs return a non-nil empty Series.

The source is Ron Black, “Getting Clear With Short-Term Swings”, Technical Analysis of Stocks & Commodities, V28:10 (September 2010), sidebar listing, with Richard Denning’s TradersStudio port in the same issue’s Traders’ Tips as a second published translation. The up block precedes the down block, including on turns down. All updates and clearing comparisons are strict; a touch does not clear.

For dir := s.ClearMethodDirection(low), the high and low turn events are dir.Delta(1).LT(-1) and dir.Delta(1).GT(1); the initial 0 to -1 transition is not a turn. The bars since the confirmation of the k-th previous swing high are dir.Delta(1).LT(-1).BarsSince(k). With line := s.ClearMethodLine(low), Black’s noise flag is dir.Delta(1).EQ(0).Mul(line.Delta(1).EQ(0)).SetN(1, 0). It marks a bar on which neither direction nor line changes: the range straddles or touches the level. The noise recipe reads a held invalid bar as noise. Its approximate annual percentage is noise.Average(252).Mul(100) from bar 251 on; earlier bars, and every bar of a history shorter than 252 bars, read the flag times 100, because Average passes its input through before a full window.

For swings confirmed by a move of more than a threshold from the running extreme, rather than by a range that clears a level, see the threshold-swing recipes over DirectionalChange in the manual’s “Swing State” section.

ClearMethodHighValue

func (s Series) ClearMethodHighValue(low Series) Series

ClearMethodHighValue holds the highest high of the last completed up swing, confirmed on its turn down. The value is posted on the confirmation bar and copied verbatim, including signed zeros. With dir := s.ClearMethodDirection(low) and value := s.ClearMethodHighValue(low), earlier swings are value.ValueWhen(dir.Delta(1).LT(-1), k), where k = 0 selects the most recent swing. The method starts down on the first valid bar and is path-dependent. The result is NaN before the first turn down. A bar with NaN in either column or low > high holds the previous output. Unequal lengths, checked first, or empty inputs return a non-nil empty Series.

ClearMethodLine

func (s Series) ClearMethodLine(low Series) Series

ClearMethodLine follows the highest low in an up swing and the lowest high in a down swing. A high below the highest low turns down; a low above the lowest high turns up. Updates and turns are strict. The method starts down on the first valid bar and is path-dependent. Level values retain their input bits, including signed zeros. A bar with NaN in either column or low > high holds the previous output. The result is NaN before the first valid bar. Unequal lengths, checked first, or empty inputs return a non-nil empty Series.

ClearMethodLowValue

func (s Series) ClearMethodLowValue(low Series) Series

ClearMethodLowValue holds the lowest low of the last completed down swing, confirmed on its turn up. The value is posted on the confirmation bar and copied verbatim, including signed zeros. With dir := s.ClearMethodDirection(low) and value := s.ClearMethodLowValue(low), earlier swings are value.ValueWhen(dir.Delta(1).GT(1), k), where k = 0 selects the most recent swing. The method starts down on the first valid bar and is path-dependent. The result is NaN before the first turn up. A bar with NaN in either column or low > high holds the previous output. Unequal lengths, checked first, or empty inputs return a non-nil empty Series.

GannSwingDirection

func (s Series) GannSwingDirection(low Series, n int) Series

GannSwingDirection reports Gann’s n-bar swing direction as -1, 0 or 1. An up trigger needs n consecutive strictly higher highs; a down trigger needs n consecutive strictly lower lows. Triggers require consecutive valid bars: NaN in either column or low > high holds the state and resets the counts. The initial direction is 0; in that state simultaneous triggers turn down. Thereafter only the trigger opposite to the current direction can turn it. Trigger tests precede extensions, and the new leg starts on the confirmation bar. So an extreme made on a confirming bar itself can belong to no confirmed swing: with highs {5, 6, 9, 1}, lows {4, 3, 2, 0} and n = 2, bar 2 turns down and confirms the high 6, and the 9 of bar 2 is in neither leg. Equal extremes extend to the latest bar, preserving the input’s signed zero. The running extremes start at the first valid bar, so this method is path-dependent. Unequal lengths, checked first, or empty inputs return a non-nil empty Series. n < 1 returns all +0. The result is 0 before the first valid bar and until the first trigger; it contains no NaN.

Sources: Robert Krausz, “The New Gann Swing Chartist”, Technical Analysis of Stocks & Commodities, February 1998; Thom Hartle, “Gann Swings And Intraday Trading”, Technical Analysis of Stocks & Commodities, October 1999; and Curtis Arnold, “Gann”, Technical Analysis of Stocks & Commodities, March 1983. Hartle’s n = 2 counts two strict high-to-high rises; Arnold uses n = 3. Gann’s higher-lows qualifier and Arnold’s reversal-day wait are omitted. With valid bars, a turn up satisfies s.ConsecUp()[t] >= n; a turn down satisfies low.ConsecDown()[t] >= n.

For dir := s.GannSwingDirection(low, n), the turn events are dir.Delta(1).LT(0) (a turn down, which confirms a swing high) and dir.Delta(1).GT(0); the first turn out of the undetermined 0 already confirms a swing. The k-th previous swing high is s.GannSwingHighValue(low, n).ValueWhen(dir.Delta(1).LT(0), k), and the bars since its confirmation are dir.Delta(1).LT(0).BarsSince(k).

For swings confirmed by a move of more than a threshold from the running extreme, rather than by a count of higher highs or lower lows, see the threshold-swing recipes over DirectionalChange in the manual’s “Swing State” section.

GannSwingHighValue

func (s Series) GannSwingHighValue(low Series, n int) Series

GannSwingHighValue uses a down trigger to confirm a swing high and holds its value from that bar onwards. The confirmed extreme comes from before the confirmation bar; the new down leg starts on that bar. Extensions use >=, so equal highs retain the latest input value verbatim, including its signed zero. Initial running extremes start at the first valid bar, making the method path-dependent. A bar with NaN in either column or low > high holds the output and resets the consecutive trigger counts. The result is NaN before the first confirmed high. Unequal lengths, checked first, or empty inputs return a non-nil empty Series. n < 1 returns all +0.

GannSwingLowValue

func (s Series) GannSwingLowValue(low Series, n int) Series

GannSwingLowValue uses an up trigger to confirm a swing low and holds its value from that bar onwards. The confirmed extreme comes from before the confirmation bar; the new up leg starts on that bar. Extensions use <=, so equal lows retain the latest input value verbatim, including its signed zero. Initial running extremes start at the first valid bar, making the method path-dependent. A bar with NaN in either column or low > high holds the output and resets the consecutive trigger counts. The result is NaN before the first confirmed low. Unequal lengths, checked first, or empty inputs return a non-nil empty Series. n < 1 returns all +0.

LevelBreakDirection

func (up Series) LevelBreakDirection(dn, hi, lo Series) Series

LevelBreakDirection is the stop-and-reverse latch of a pair of level series: +1 after up breaks above the upper level, -1 after dn breaks below the lower level, and 0 until the first break of one level alone. It is Dow’s trend reduced to its last two levels: the Donchian breakout latch when the levels are the channel bounds,

h.LevelBreakDirection(l, h.Highest(n), l.Lowest(n))

and the swing trend of SwingTrend when they are the last confirmed swing high and swing low, and Aan’s weekly high/low moving-average stops and Krausz’s hi-lo activator when they are moving averages of the highs and lows (compositions under “Stops and Reversals”). Over the levels of the classic Darvas box, DarvasBoxTop and DarvasBoxBottom, it holds the direction of the last DarvasBoxBreakout wherever no bar breaks both levels (the manual’s “Darvas Box” section).

up is the series that breaks upwards and dn the one that breaks downwards; they may be the same series, as in a close-break recipe. hi is the upper level and lo the lower level, each read one bar late: on bar t the breaks are U := up[t] > hi[t-1] and D := dn[t] < lo[t-1], both strict, and both false when either side is NaN, so a NaN level cannot be broken. Bar 0 is not tested. From the 0 state U alone seeds +1 and D alone seeds -1, while a double break leaves 0. From +1 only D is tested, and flips the state to -1; from -1 only U is tested, and flips it to +1. So the latch allows one flip per bar, seeds on the level it breaks alone, and keeps a stale level in force until it is broken: there is no ratchet and no invalidation. A stale level can whipsaw: when the level on the new side has already been passed, the next bar beyond it flips the state straight back. Breaks are strict: unlike FlipStop, which flips a long on a trigger equal to its line, an equal price does not break here. Where no bar breaks both levels at once the result equals

up.GT(hi.Shift(1)).SetN(1, 0).Sub(dn.LT(lo.Shift(1)).SetN(1, 0)).Hysteresis(0, 0)

where the SetN(1, 0) masks bar 0, which Shift pads with 0. LevelBreakStop is the level in force on the next bar, as FlipStop’s line is for its flip. A slice of the history that starts later agrees with the full run from the first single-sided break after both levels have a value set inside the slice; until then a level the slice has not yet seen can flip the full run alone. That holds for levels computed over the slice from window-local rules (Highest and Lowest once their windows are full, the bar-count swings once confirmed inside the slice); over path-dependent levels (the Williams, Clear or Gann swings) the latch is path-dependent too.

Validation: dn, hi and lo must have the receiver’s length, otherwise the result is Series{}, as it is for an empty receiver. Warm-up: 0 until the first single-sided break. Timing: the output on bar t uses bars 0 to t only; a level set on bar t is in force from bar t+1. The output holds only -1, 0 and +1. Pure: a new Series is returned and the inputs are not modified.

LevelBreakStop

func (up Series) LevelBreakStop(dn, hi, lo Series) Series

LevelBreakStop is the level a LevelBreakDirection state rests on: lo[t] where the direction is +1, hi[t] where it is -1, and NaN where it is 0, copied verbatim (a NaN or infinite level included). It is the level in force during bar t+1, the one that bar must break to flip the state, as FlipStop’s line is the level the next flip is tested against. The arguments are those of LevelBreakDirection: up and dn the breaking series, hi and lo the levels. Unlike FlipStop, which flips a long on a trigger equal to its line, a price equal to this level does not break it.

Validation: dn, hi and lo must have the receiver’s length, otherwise the result is Series{}, as it is for an empty receiver. Warm-up: NaN until the first single-sided break. Timing: the output on bar t uses bars 0 to t only and is the level in force on bar t+1. Pure: a new Series is returned and the inputs are not modified.

MarketStructure

func (s Series) MarketStructure(low Series, n int) Series

MarketStructure holds the two-wave trend: 1 after a higher high and a higher low, -1 after a lower high and a lower low, and the previous state for mixed or equal comparisons. The state starts at 0. The swing-chart rule is Linda Bradford Raschke’s, in New Thinking in Technical Analysis (Bensignor, ed., 2000), ch. 1: “An uptrend is established when two up waves make higher highs and higher lows”. The chapter signals the downtrend by two down waves with lower lows; this method also requires a lower high, the mirror of the uptrend rule.

Let H1 = s.SwingHighValue(n), H0 = s.SwingHighValue(n, 1), L1 = low.SwingLowValue(n), and L0 = low.SwingLowValue(n, 1). It equals x.Hysteresis(0.5, -0.5), where x := H1.GT(H0).Mul(L1.GT(L0)).Sub(L1.LT(L0).Mul(H1.LT(H0))). The same form over the swing highs and lows of the threshold-swing recipes in the manual’s “Swing State” section gives the two-wave trend on DirectionalChange’s swings, confirmed by a move of more than a threshold.

NaN suppresses a pivot in its window, and comparisons with unavailable swings are false. The result is 0 until two highs and two lows exist and agree on a trend; it contains no NaN. Unequal lengths return a non-nil empty Series before validation of n. n < 1 returns all +0. Empty inputs return a non-nil empty Series.

SwingHighAge

func (s Series) SwingHighAge(n int, back ...int) Series

SwingHighAge counts bars since the extreme bar of the requested confirmed swing high; its age is at least n. With k = back[0], or 0 when omitted, it equals s.SwingHighEvent(n).BarsSince(k).Add(float64(n)). The age since confirmation is s.SwingHighEvent(n).BarsSince(k). Only the first back argument is used: 0 selects the latest swing, 1 the previous swing, and so on. NaN in a pivot window suppresses that swing. The result is NaN until the requested swing exists, including when no complete window fits. n < 1 or a negative back returns all +0. Empty input returns a non-nil empty Series.

SwingHighDivergence

func (s Series) SwingHighDivergence(osc Series, n int) Series

SwingHighDivergence flags regular bearish divergence between the receiver, the highs, and the oscillator osc: on the confirmation bar of each strict swing high of strength n that has a previous swing high, the output is 1 when the price makes a higher high while the oscillator, sampled on the two pivot bars, makes a lower value, and 0 on every other bar.

The pivots are the strict bar-count swing highs of the receiver, as SwingHighEvent(n) reports them: bar c is a pivot when the window from c-n to c+n has no NaN and the high of c is strictly above every other high in it (any tie, +0 against -0 included, gives no pivot), and it is confirmed on bar c+n. For each pivot c with a previous pivot p, the flag on bar c+n is 1 when h[c] > h[p] and osc[c] < osc[p]. The comparisons are strict and a comparison with NaN is false, so equal pivot highs, equal oscillator values or a NaN oscillator at p or c give 0. With ev := h.SwingHighEvent(n), hc := h.SwingHighValue(n), hp := h.SwingHighValue(n, 1), ic := osc.Shift(n).ValueWhen(ev) and ip := osc.Shift(n).ValueWhen(ev, 1), the flag is ev.Mul(hc.GT(hp)).Mul(ic.LT(ip)) (the n zeros that Shift pads are harmless: no event exists before bar 2n); hidden bearish divergence, not a method of its own, is the recipe ev.Mul(hc.LT(hp)).Mul(ic.GT(ip)). A NaN strictly inside the span between the pivots does not affect the flag, which reads the two pivot bars only, so the flag can be 1 while the index is NaN (SwingHighDivergenceIndex reads the whole span). Pivots on the oscillator are the same methods with receiver and argument exchanged: TradingView’s hidden bearish divergence, before its range gate and with strict pivots in place of Pine’s, is osc.SwingHighDivergence(h, n) (the gate below then reads osc.SwingHighAge), its regular bearish divergence is the hidden recipe with receiver osc and sampled h, and Storz’s index on its pivots is osc.SwingHighDivergenceIndex(h, n).Mul(-1).

Gates are recipes rather than parameters. Le Beau and Lucas’s level gate (Day Trading Systems & Methods) is prose only (its momentum form is quoted in x100 price units, +/-150 for 1.50 index points; its RSI and stochastic forms use 80/20): it keeps the signal only where the oscillator at the earlier peak is above a level U, which is h.SwingHighDivergence(osc, n).Mul(ip.GT(U)). The distance between the pivots is gap := h.SwingHighAge(n, 1).Sub(h.SwingHighAge(n)), which is c - p on each event bar with a previous pivot; TradingView’s _inRange with its defaults (5 and 60 bars since the bar after the previous pivot) is gap.GTE(6).Mul(gap.LTE(61)).

Validation: an osc of a different length (a nil osc has length 0) returns an empty Series, checked first; an empty receiver returns an empty Series; n < 1 returns all +0. Any n >= 1 is valid; when n > (len(s)-1)/2 no window fits, so there are no pivots and every bar is 0. Warm-up: 0 until the second pivot is confirmed, on bar 3n+1 at the earliest. Timing: bar t uses bars 0 to t only; a pair is evaluated on the confirmation bar of its later pivot, and pivots are not moved afterwards. Flags are the literals 0 (+0) and 1 and are not NaN. The method is pure, runs in linear time for any n and returns fresh storage. On a slice of history the flags agree with the full run bit for bit from the confirmation bar of the slice’s second pivot.

SwingHighDivergenceIndex

func (s Series) SwingHighDivergenceIndex(osc Series, n int) Series

SwingHighDivergenceIndex returns Matt Storz’s divergence index Dp between the receiver, the highs, and the oscillator osc: on the confirmation bar of each strict swing high of strength n that has a previous swing high, the price move and the oscillator move between the two pivots, each normalised by its own range over the span between them, are differenced and scaled to the range -100 to 100; the value is held until the next pair.

The pivots are the strict bar-count swing highs of the receiver, as SwingHighEvent(n) reports them, confirmed on bar c+n. For each pivot c with a previous pivot p, with the span [p, c] inclusive and Hh, Hl the maximum and minimum of h over the span and Ih, Il those of osc (Go’s max and min, NaN if any value in the span is NaN), the posting on bar c+n is, each operation rounded once:

q1 := (h[c] - h[p]) / (Hh - Hl)
q2 := (osc[c] - osc[p]) / (Ih - Il)
d := float64(50 * (q1 - q2))

Each term lies in [-1, 1], so |d| <= 100; a higher high with a lower oscillator gives d >= 0 where d is not NaN. The division is plain IEEE arithmetic: a NaN in the span gives NaN, a flat oscillator over the span gives 0/0 = NaN, an infinite range with a finite difference gives a zero term, and Inf - Inf and Inf/Inf give NaN. A NaN posting is held like any other value. The span ends at the pivot bar c, not at the confirmation bar; the Omega listing of the Traders’ Tips (Dunbar) spans [p, c+n-1] and holds the previous value on a zero range: its span agrees with this method’s only for n = 1 and its hold does not agree at all; neither is the definition. The index reads the whole span while the flag SwingHighDivergence reads the two pivot bars only, so the flag can be 1 while the index is NaN. With ev := h.SwingHighEvent(n) and D this index, D.ValueWhen(ev, 1) is the previous pair’s posting, read on the current bar, and samples a NaN posting like any other (D.ValueWhen(ev) is D itself). Equal pivot values make the price term 0. Pivots on the oscillator with Storz’s orientation (the price term first) give osc.SwingHighDivergenceIndex(h, n).Mul(-1) by value, and h.SwingHighDivergenceIndex(h, n) is +0 wherever it is not NaN.

Storz (“Quantifying Divergence With The Divergence Index”, Technical Analysis of Stocks & Commodities V14:1, January 1996) prints 39.64 for the gold peak; with highs 389.5, 390.0, 389.6, 390.6, 390.2, 389.0 and oscillator 60, 69.25, 63.13, 68.07, 66, 64 at n = 1 (the outer bars chosen below his pivots): the pivots are bars 1 and 3, so bar 4 posts 39.64052287581819, within 1e-11 of the exact 6065/153, held on bar 5 and NaN on bars 0 to 3.

Validation: an osc of a different length (a nil osc has length 0) returns an empty Series, checked first; an empty receiver returns an empty Series; n < 1 returns all +0. Any n >= 1 is valid; when n > (len(s)-1)/2 no window fits, so there are no pivots and every bar is NaN. Warm-up: NaN until the second pivot is confirmed, on bar 3n+1 at the earliest, the first pivot’s confirmation bar included. Timing: bar t uses bars 0 to t only; a pair is posted on the confirmation bar of its later pivot and held until the next posting, and pivots are not moved afterwards. The method is pure, runs in linear time for any n and returns fresh storage. On a slice of history the index agrees with the full run bit for bit from the confirmation bar of the slice’s second pivot.

SwingHighEvent

func (s Series) SwingHighEvent(n int) Series

SwingHighEvent reports 1 at the confirmation bar, n bars after a strict swing-high pivot, and 0 otherwise. A pivot at t-n must exceed every other value in [t-2*n, t]; a flat top or any other equal maximum is a tie. Equal +0 and -0 values tie. NaN anywhere in the window suppresses its event; infinities compare normally. n < 1 returns all +0. Empty input returns a non-nil empty Series. Events are +0 before bar 2*n, and throughout when n > (len(s)-1)/2.

TA-Lib’s FRACTAL function (added in 2026; strict on both sides) flags the same pivots with optInLeftBars = optInRightBars = n, on the same confirmation bar.

The strict recipe is s.Shift(n).GT(s.Shift(n+1).Highest(n)).Mul(s.Shift(n).GT(s.Highest(n))).SetN(2n, 0). Pine’s rule is non-strict on the left and strict on the right: s.Shift(n).GTE(s.Shift(n+1).Highest(n)).Mul(s.Shift(n).GT(s.Highest(n))).SetN(2n, 0). The non-strict rule on both sides is s.Shift(n).GTE(s.Highest(2n+1)).SetN(2n, 0). On NaN-free data the strict event set is contained in Pine’s, which is contained in the non-strict set. DoubleTop pairs consecutive confirmed swing highs into double tops (the manual’s “Chart Patterns” section). TripleTop reads three consecutive confirmed swing highs as a triple top (the manual’s “Chart Patterns” section). HeadShouldersTop and HeadShouldersBottom merge these swings with the swing lows into head-and-shoulders patterns (the manual’s “Chart Patterns” section). RoundingBottom fits a semicircle band to the closes from a confirmed swing high to the first close that regains it (the manual’s “Chart Patterns” section).

SwingHighValue

func (s Series) SwingHighValue(n int, back ...int) Series

SwingHighValue holds the input value of the requested confirmed swing high. With k = back[0], or 0 when omitted, it equals s.Shift(n).ValueWhen(s.SwingHighEvent(n), k). Only the first back argument is used: 0 selects the latest swing, 1 the previous swing, and so on. Values are copied verbatim, including signed zeros. NaN in a pivot window suppresses that swing. The result is NaN until the requested swing exists, including when no complete window fits. n < 1 or a negative back returns all +0. Empty input returns a non-nil empty Series.

SwingLowAge

func (s Series) SwingLowAge(n int, back ...int) Series

SwingLowAge counts bars since the extreme bar of the requested confirmed swing low; its age is at least n. With k = back[0], or 0 when omitted, it equals s.SwingLowEvent(n).BarsSince(k).Add(float64(n)). The age since confirmation is s.SwingLowEvent(n).BarsSince(k). Only the first back argument is used: 0 selects the latest swing, 1 the previous swing, and so on. NaN in a pivot window suppresses that swing. The result is NaN until the requested swing exists, including when no complete window fits. n < 1 or a negative back returns all +0. Empty input returns a non-nil empty Series.

SwingLowDivergence

func (s Series) SwingLowDivergence(osc Series, n int) Series

SwingLowDivergence flags regular bullish divergence between the receiver, the lows, and the oscillator osc: on the confirmation bar of each strict swing low of strength n that has a previous swing low, the output is 1 when the price makes a lower low while the oscillator, sampled on the two pivot bars, makes a higher value, and 0 on every other bar.

It is the mirror of SwingHighDivergence: the pivots are the strict bar-count swing lows of the receiver, as SwingLowEvent(n) reports them (a tie, +0 against -0 included, gives no pivot), and for each pivot c with a previous pivot p the flag on bar c+n is 1 when l[c] < l[p] and osc[c] > osc[p]; the comparisons are strict and a comparison with NaN is false. With ev := l.SwingLowEvent(n), lc := l.SwingLowValue(n), lp := l.SwingLowValue(n, 1), ic := osc.Shift(n).ValueWhen(ev) and ip := osc.Shift(n).ValueWhen(ev, 1), the flag is ev.Mul(lc.LT(lp)).Mul(ic.GT(ip)); hidden bullish divergence, not a method of its own, is the recipe ev.Mul(lc.GT(lp)).Mul(ic.LT(ip)), and h.SwingHighDivergence(osc, n) equals h.Mul(-1).SwingLowDivergence(osc.Mul(-1), n) bit for bit. The flag reads the two pivot bars only, so it can be 1 while the index is NaN (SwingLowDivergenceIndex reads the whole span). Le Beau and Lucas’s level gate (Day Trading Systems & Methods), prose only (x100 price units for its momentum form, 80/20 for its RSI and stochastic forms), keeps the signal only where the oscillator at the earlier trough is below a level L: l.SwingLowDivergence(osc, n).Mul(ip.LT(L)). The distance gate of SwingHighDivergence applies with SwingLowAge in place of SwingHighAge, and l.SwingLowDivergence(osc, n).Sub(h.SwingHighDivergence(osc, n)) is a signed flag, +1 bullish, -1 bearish and 0 where an outside bar fires both. Pivots on the oscillator are the same methods with receiver and argument exchanged: TradingView’s hidden bullish divergence, before its range gate and with strict pivots in place of Pine’s, is osc.SwingLowDivergence(l, n), and its regular bullish divergence is the hidden bullish recipe with receiver osc and sampled l.

Validation: an osc of a different length (a nil osc has length 0) returns an empty Series, checked first; an empty receiver returns an empty Series; n < 1 returns all +0. Any n >= 1 is valid; when n > (len(s)-1)/2 no window fits, so there are no pivots and every bar is 0. Warm-up: 0 until the second pivot is confirmed, on bar 3n+1 at the earliest. Timing: bar t uses bars 0 to t only; a pair is evaluated on the confirmation bar of its later pivot, and pivots are not moved afterwards. Flags are the literals 0 (+0) and 1 and are not NaN. The method is pure, runs in linear time for any n and returns fresh storage. On a slice of history the flags agree with the full run bit for bit from the confirmation bar of the slice’s second pivot.

SwingLowDivergenceIndex

func (s Series) SwingLowDivergenceIndex(osc Series, n int) Series

SwingLowDivergenceIndex returns Matt Storz’s divergence index Dt between the receiver, the lows, and the oscillator osc: on the confirmation bar of each strict swing low of strength n that has a previous swing low, the oscillator move and the price move between the two pivots, each normalised by its own range over the span between them, are differenced and scaled to the range -100 to 100; the value is held until the next pair.

It is the trough form of SwingHighDivergenceIndex: the pivots are the strict bar-count swing lows of the receiver, as SwingLowEvent(n) reports them, confirmed on bar c+n, and for each pivot c with a previous pivot p, with the span [p, c] inclusive, Hh, Hl the maximum and minimum of l over the span and Ih, Il those of osc (Go’s max and min, NaN if any value in the span is NaN), the posting on bar c+n is, each operation rounded once:

q1 := (osc[c] - osc[p]) / (Ih - Il)
q2 := (l[c] - l[p]) / (Hh - Hl)
d := float64(50 * (q1 - q2))

Each term lies in [-1, 1], so |d| <= 100; a lower low with a higher oscillator gives d >= 0 where d is not NaN. The division is plain IEEE arithmetic, as for the peak form: a NaN in the span or a flat oscillator over the span gives NaN, and a NaN posting is held like any other value. The method is computed from its own formula; it equals l.Mul(-1).SwingHighDivergenceIndex(osc.Mul(-1), n) by value only: a zero posting can carry the other sign in the negated form (a finite difference over an infinite range gives a signed zero). Pivots on the oscillator with Storz’s orientation give osc.SwingLowDivergenceIndex(l, n).Mul(-1) by value. The span ends at the pivot bar c, not at the confirmation bar, as for the peak form (the Omega listing spans [p, c+n-1]). The index reads the whole span while the flag SwingLowDivergence reads the two pivot bars only, so the flag can be 1 while the index is NaN. With ev := l.SwingLowEvent(n) and D this index, D.ValueWhen(ev, 1) is the previous pair’s posting, read on the current bar, and samples a NaN posting like any other (D.ValueWhen(ev) is D itself). Equal pivot values make the price term 0.

Storz prints 56.63 for the gold trough with lows 386.0, 385.3, 385.4, 384.0, 383.1, 384.5 and oscillator 30, 16.43, 39.76, 25, 20.54, 22 at n = 1 (the middle bars chosen inside his printed ranges and not troughs): the pivots are bars 1 and 4, so bar 5 posts 56.6344881566939, within 1e-11 of the exact 3038950/53659, and bars 0 to 4 are NaN.

Validation: an osc of a different length (a nil osc has length 0) returns an empty Series, checked first; an empty receiver returns an empty Series; n < 1 returns all +0. Any n >= 1 is valid; when n > (len(s)-1)/2 no window fits, so there are no pivots and every bar is NaN. Warm-up: NaN until the second pivot is confirmed, on bar 3n+1 at the earliest, the first pivot’s confirmation bar included. Timing: bar t uses bars 0 to t only; a pair is posted on the confirmation bar of its later pivot and held until the next posting, and pivots are not moved afterwards. The method is pure, runs in linear time for any n and returns fresh storage. On a slice of history the index agrees with the full run bit for bit from the confirmation bar of the slice’s second pivot.

SwingLowEvent

func (s Series) SwingLowEvent(n int) Series

SwingLowEvent reports 1 at the confirmation bar, n bars after a strict swing-low pivot, and 0 otherwise. A pivot at t-n must be below every other value in [t-2*n, t]. Equal values, including signed zeros, tie. NaN anywhere in the window suppresses its event; infinities participate in the strict comparisons. n < 1 returns all +0. Empty input returns a non-nil empty Series. Events are +0 before bar 2*n, and throughout when n > (len(s)-1)/2. DoubleBottom pairs consecutive confirmed swing lows into double bottoms (the manual’s “Chart Patterns” section). TripleBottom reads three consecutive confirmed swing lows as a triple bottom (the manual’s “Chart Patterns” section). HeadShouldersTop and HeadShouldersBottom merge these swings with the swing highs into head-and-shoulders patterns (the manual’s “Chart Patterns” section). RoundingTop fits the mirror band to the closes from a confirmed swing low to the first close that falls back to it (the manual’s “Chart Patterns” section).

SwingLowValue

func (s Series) SwingLowValue(n int, back ...int) Series

SwingLowValue holds the input value of the requested confirmed swing low. With k = back[0], or 0 when omitted, it equals s.Shift(n).ValueWhen(s.SwingLowEvent(n), k). Only the first back argument is used: 0 selects the latest swing, 1 the previous swing, and so on. Values are copied verbatim, including signed zeros. NaN in a pivot window suppresses that swing. The result is NaN until the requested swing exists, including when no complete window fits. n < 1 or a negative back returns all +0. Empty input returns a non-nil empty Series.

SwingTrend

func (s Series) SwingTrend(low Series, n int) Series

SwingTrend is the Dow trend of the bar-count swing chart: +1 after the highs break above the last confirmed swing high, -1 after the lows break below the last confirmed swing low, and 0 until the first break of one level alone. It is Penfold’s trend rule for swing charts, which follows Dow: an up trend holds until a confirmed swing low is broken and a down trend until a confirmed swing high is. The receiver is the highs and low the lows; n is the bar count of the swings, as in SwingHighValue and SwingLowValue. The result is

s.LevelBreakDirection(low, s.SwingHighValue(n), low.SwingLowValue(n))

bit for bit, computed from the same kernels rather than through those methods. A swing confirmed on bar t is a level from bar t+1, and only the break opposite to the state is tested, so the trend flips at most once per bar and a stale level stays in force until it is broken; a stale level can whipsaw, as LevelBreakDirection says. A slice of the history that starts later agrees with the full run from the first single-sided break after a swing of each kind has been confirmed inside the slice; until then a level the slice has not yet seen can flip the full run alone.

Recipes. The Dow and Hamilton closing-price penetration rule tests the close against both levels,

c.LevelBreakDirection(c, h.SwingHighValue(n), l.SwingLowValue(n))

MarketStructure reads the same swings differently, as higher or lower highs and lows compared pairwise, with no break of price against a level; the two share no identity. The Livermore Reaction entry, in the direction of this trend after two reactions against it, is a recipe under “Compositions” in the manual’s “Swing State” section and in the package examples (ExampleSeries_SwingTrend).

Validation: low must have the receiver’s length, otherwise the result is Series{}, as it is for an empty receiver; n < 1 gives a zero series of the receiver’s length. Warm-up: 0 until the first single-sided break, which needs a confirmed swing of the kind broken. Timing: the output on bar t uses bars 0 to t only. Pure: a new Series is returned and the inputs are not modified.

WidnerResistanceLevel

func (s Series) WidnerResistanceLevel(n int, back ...int) Series

WidnerResistanceLevel returns Mel Widner’s automated resistance levels of the receiver, the highs: on each bar, the value of the (k+1)-th most recent resistance level confirmed at or before the bar, with k the optional back (0 when omitted): R(k+1) is back k, so back 0 is R1, the latest level, and back 5 is R6. The strength n is the number of bars on each side of the extreme, so Widner’s lookback N is the window 2n+1 and Widner’s N = 9 is n = 4.

It is the mirror of WidnerSupportLevel: a resistance event occurs on bar t when t >= 2n, no value of bars t-2n to t is NaN, and the high of bar t-n is greater than or equal to every high of that window (ties included, +0 and -0 tying); its value is the high of bar t-n, the extreme bar, and it is confirmed on bar t, the event series being h.Shift(n).EQ(h.Highest(2n+1)).SetN(2n, 0). Events are pushed as levels only when their value differs from the latest level, so an equal double top or a flat top adds a single level, and the latest level always equals the latest event by value (the bits differ only where an event repeats the latest level with the other sign of zero). The strict bar-count view SwingHighValue agrees with this one on data without ties inside any window and without two consecutive equal event values; the view equals h.Mul(-1).WidnerSupportLevel(n, k).Mul(-1) bit for bit. Consecutive levels are confirmed at least n+1 bars apart, the first on bar 2n or later and the sixth on bar 7n+5 or later.

Validation: an empty receiver returns an empty Series; n < 1, or a negative back, returns all +0; only back[0] is read. Any n >= 1 is valid; when n > (len(s)-1)/2 no window fits, so every bar is NaN. Warm-up: NaN until the (k+1)-th level is confirmed, a data-dependent start; zeros are not seeded. Timing: bar t uses bars 0 to t only, and a level is posted on its confirmation bar. Infinities are ordered values; a NaN inside a window suppresses its event. Levels are copied verbatim. The method is pure, runs in linear time for any n and returns fresh storage. On a slice of history the view agrees with the full run by value from the bar where the slice has confirmed k+1 levels. On Widner’s sheet (n = 4), the only resistance event on the 17 printed bars is the high 434.36 of row 11, confirmed on row 15 (bar 13), so R1 is NaN on bars 0 to 12 and 434.36 from bar 13; back 1 is NaN throughout.

WidnerResistanceOscillator

func (s Series) WidnerResistanceOscillator(high Series, n int) Series

WidnerResistanceOscillator returns Mel Widner’s resistance oscillator (WRO) of the receiver, the closes, against the six latest resistance levels of the argument high: on each bar, the count of the levels R1 to R6 of high.WidnerResistanceLevel(n) that lie strictly below the close, scaled to a percentage of six. The strength n is the number of bars on each side of a level’s extreme, so Widner’s lookback N is the window 2n+1 and Widner’s N = 9 is n = 4.

It is the mirror of WidnerSupportOscillator with the resistance levels in place of the support levels and the same rule: NaN when the close is NaN or fewer than six resistance levels are confirmed at or before the bar, otherwise float64(100*k) / 6 with k the number of the six latest levels strictly below the close (a level equal to the close is not below), one of the seven values 0 (+0), 16.666666666666668, 33.333333333333336, 50, 66.66666666666667, 83.33333333333333 and 100 (Widner prints 17, 33, 67 and 83, display rounding). It is computed from its own levels: c.Mul(-1).WidnerSupportOscillator(h.Mul(-1), n).Mul(-1).Add(100) is not this method: it can differ by an ulp, and differs by 100/6 for each resistance level equal to the close. Widner’s MetaStock entry rule is the OR of the WSO leg given under WidnerSupportOscillator and the WRO leg wro.Average(30).GT(95), with wro this oscillator, quoted for reference only and not tested here.

Validation: a high of a different length (a nil high has length 0) returns an empty Series, checked first; an empty receiver returns an empty Series; n < 1 returns all +0. Any n >= 1 is valid; when n > (len(s)-1)/2 no window fits, so every bar is NaN. Warm-up: NaN until the sixth level, which is confirmed on bar 7n+5 at the earliest; the two oscillators warm up independently. Timing: bar t uses bars 0 to t only, and a level counts from its confirmation bar. The method is pure, runs in linear time for any n and returns fresh storage. On a slice of history the output agrees with the full run bit for bit from the bar where the slice has confirmed six levels.

WidnerSupportLevel

func (s Series) WidnerSupportLevel(n int, back ...int) Series

WidnerSupportLevel returns Mel Widner’s automated support levels of the receiver, the lows: on each bar, the value of the (k+1)-th most recent support level confirmed at or before the bar, with k the optional back (0 when omitted): S(k+1) is back k, so back 0 is S1, the latest level, and back 5 is S6. The strength n is the number of bars on each side of the extreme, so Widner’s lookback N is the window 2n+1 and Widner’s N = 9 is n = 4.

A support event occurs on bar t when t >= 2n, no value of bars t-2n to t is NaN, and the low of bar t-n is less than or equal to every low of that window (ties included, compared with Go’s ==, so +0 and -0 tie and a flat bottom of equal lows gives an event at each centre). Its value is the low of bar t-n, the extreme bar; it is confirmed, and posted, on bar t and is not moved afterwards. The event series is l.Shift(n).EQ(l.Lowest(2n+1)).SetN(2n, 0), where l is the receiver (Lowest uses partial windows, Shift pads zeros and a NaN window gives NaN; the SetN mask covers both). An event is pushed as a new level only when no level exists yet or when its value differs from the latest level, so an equal double bottom or a flat bottom adds a single level, an event equal to an older level but not to the latest is pushed again, and the latest level always equals the latest event by value. The latest level keeps the bits of the event that pushed it, so a -0 event after a +0 level adds nothing and leaves the +0. With distinct the event series restricted to the events that push, the view at back k is l.Shift(n).ValueWhen(distinct, k) bit for bit, and the latest level is l.Shift(n).ValueWhen(newS) by value, with newS the full event series. The strict bar-count view SwingLowValue agrees with this one on data without ties inside any window and without two consecutive equal event values; flat and double bottoms part them. The levels give a level-break trend: c.LevelBreakDirection(c, h.WidnerResistanceLevel(n), l.WidnerSupportLevel(n)) turns up on a close above the latest resistance level and down on a close below the latest support level.

Two events within n bars of each other have equal values, since each centre lies inside the other’s window, so consecutive levels are confirmed at least n+1 bars apart (at least five bars for n = 4), the first on bar 2n or later and the sixth on bar 7n+5 or later.

Validation: an empty receiver returns an empty Series; n < 1, or a negative back, returns all +0; only back[0] is read. Any n >= 1 is valid; when n > (len(s)-1)/2 no window fits, so every bar is NaN. Warm-up: NaN until the (k+1)-th level is confirmed, a data-dependent start; zeros are not seeded. Timing: bar t uses bars 0 to t only, and a level is posted on its confirmation bar. Infinities are ordered values; a NaN inside a window suppresses its event. Levels are copied verbatim. The method is pure, runs in linear time for any n and returns fresh storage. On a slice of history the view agrees with the full run by value from the bar where the slice has confirmed k+1 levels; the bits can differ only in the sign of a zero.

On Widner’s sheet in “Automated Support And Resistance” (Technical Analysis of Stocks & Commodities V16:5, May 1998; OEX daily, 22 February to 16 March 1994, n = 4), the only support event on the 17 printed bars is the low 425.27 of row 8, confirmed on row 12 (bar 10), so S1 is NaN on bars 0 to 9 and 425.27 from bar 10; back 1 is NaN throughout.

WidnerSupportOscillator

func (s Series) WidnerSupportOscillator(low Series, n int) Series

WidnerSupportOscillator returns Mel Widner’s support oscillator (WSO, Technical Analysis of Stocks & Commodities V16:5, May 1998) of the receiver, the closes, against the six latest support levels of the argument low: on each bar, the count of the levels S1 to S6 of low.WidnerSupportLevel(n) that lie strictly below the close, scaled to a percentage of six. The strength n is the number of bars on each side of a level’s extreme, so Widner’s lookback N is the window 2n+1 and Widner’s N = 9 is n = 4.

On bar t the output is NaN when the close is NaN or fewer than six support levels are confirmed at or before t; otherwise it is float64(100k) / 6 with k the number of the six latest levels strictly below the close (a level equal to the close is not below). The output is one of seven values, each the correctly rounded 100k/6: 0 (+0), 16.666666666666668, 33.333333333333336, 50, 66.66666666666667, 83.33333333333333 and 100. Widner prints 17, 33, 67 and 83: display rounding of these values. With c the close and Lk the level at back k, the oscillator is the sum of c.GT(Lk) for k from 0 to 5, followed by Mul(100).Div(6), on the bars where c and L5 are not NaN. Note that k(100.0/6) gives 83.33333333333334 at k = 5 and Widner’s literal 100*(1 - j/6) differs at k = 1 and 5; the method divides the exact integer 100k by 6 once.

Widner writes WSO = 100*(1 - (INT(S1/C) + … + INT(S6/C))/6). The count of levels below the close equals his INT form whenever 0 < S < 2C and C > 0, and his suggested correction Min(1, INT(S/C)) for all positive prices; the method counts the levels below the close and does not evaluate any INT term, so zero and negative prices are ordered like any other value. Infinities: a +Inf close counts every level except a +Inf level, a -Inf close counts none, and a -Inf level is below every close except -Inf. Compute the resistance mirror with WidnerResistanceOscillator rather than by negation: c.Mul(-1).WidnerSupportOscillator(h.Mul(-1), n).Mul(-1).Add(100) can differ from it by an ulp (it does at three of the seven values) and differs by 100/6 for each level equal to the close.

Widner’s published entry rules, with wso this oscillator and quoted for reference only: the MetaStock rule’s WSO leg is wso.GT(wso.Average(4)) (its other leg, on the WRO, is in WidnerResistanceOscillator), the article’s text reads the rule against the previous four bars, which is wso.GT(wso.Average(4).Shift(1)), and the Omega listing uses a 30-bar average of the oscillator. None of them is tested here.

Validation: a low of a different length (a nil low has length 0) returns an empty Series, checked first; an empty receiver returns an empty Series; n < 1 returns all +0. Any n >= 1 is valid; when n > (len(s)-1)/2 no window fits, so every bar is NaN. Warm-up: NaN until the sixth level, which is confirmed on bar 7n+5 at the earliest (bar 33 for n = 4) and usually much later; the two oscillators warm up independently. Timing: bar t uses bars 0 to t only, and a level counts from its confirmation bar. The close and the levels are not compared for validity (a close below a low is fine). The method is pure, runs in linear time for any n and returns fresh storage. On a slice of history the output agrees with the full run bit for bit from the bar where the slice has confirmed six levels, since the count compares values only.

WilliamsSwingDirection

func (s Series) WilliamsSwingDirection(low, open, cls Series) Series

WilliamsSwingDirection is the leg of Larry Williams’s one-bar swing chart: +1 while the leg is up, -1 while it is down, and 0 until the first leg is established. It is the swing construction of his Long-Term Secrets to Short-Term Trading (2nd edition, chapters 1 and 9). The receiver is the highs; low, open and cls are the lows, the opens and the closes.

Only valid bars count, those with low <= high; an invalid bar is neither inside nor a reference bar and changes nothing. An inside bar, one whose range lies within the previous valid bar’s or within that of the last valid bar that is not inside (equal bounds count as within), is skipped. Any other bar is compared with the last valid bar that is not inside, its reference: a higher high alone is an up bar, a lower low alone a down bar, and an outside bar, with both, takes the side of its close against its open (a doji outside bar continues the leg, and is skipped while there is no leg, though it still becomes the reference). The first directional bar establishes the leg, with that bar and its reference as the running extremes, and confirms nothing. In an up leg the running swing high moves to any stepped bar (one that is not inside) whose high is at least the running high (the latest of equal highs), and a down bar then confirms that swing high and starts a down leg from its own low. A down leg is the mirror: the running swing low moves to any low at least as low, and an up bar confirms it and starts an up leg from its own high. At most one swing is confirmed per bar.

On data whose every bar is valid the swings, in order, are those of

s.SwingPointIndices(open, s, low, cls, SwingAppendOnly)

and each is confirmed on the bar whose step appends it. Delta(1).LT(-1) marks the bars that confirm a swing high (a turn from +1 to -1) and Delta(1).GT(1) those that confirm a swing low; the establishment, 0 to +1 or -1, confirms nothing. This is not the bar-count pivot with n = 1, which an inside bar after the extreme confirms; here that bar is skipped and the swing waits for a bar beyond the extreme bar’s range. The run is path-dependent: the first leg and the reference bar start from the history’s first valid bar, so a slice that starts later can differ from the full run. The Williams short-term trend is the latch of the confirmed swing levels,

h.LevelBreakDirection(l, h.WilliamsSwingHighValue(l, o, c), h.WilliamsSwingLowValue(l, o, c))

Validation: low, open and cls must have the receiver’s length, otherwise the result is Series{}, as it is for an empty receiver. Warm-up: 0 until the first leg is established. Timing: the output on bar t is the leg after bar t is stepped, from bars 0 to t only, and is held on invalid and skipped bars. The output holds only -1, 0 and +1. Pure: a new Series is returned and the inputs are not modified.

WilliamsSwingHighValue

func (s Series) WilliamsSwingHighValue(low, open, cls Series) Series

WilliamsSwingHighValue is the high of the last confirmed swing high of Larry Williams’s one-bar swing chart, posted on its confirmation bar and held until the next, NaN before the first. The receiver is the highs; low, open and cls are the lows, the opens and the closes, and the run is that of WilliamsSwingDirection: a swing high is confirmed on the down bar that ends an up leg, at the highest high of the leg (the latest of equal highs). It is a level for LevelBreakDirection, which reads it from the bar after it is posted. With dir := s.WilliamsSwingDirection(low, open, cls), the k-th previous swing high is value.ValueWhen(dir.Delta(1).LT(-1), k).

Validation: low, open and cls must have the receiver’s length, otherwise the result is Series{}, as it is for an empty receiver. Warm-up: NaN until the first swing high is confirmed. Timing: the output on bar t uses bars 0 to t only, and a swing confirmed on bar t is posted on bar t. Pure: a new Series is returned and the inputs are not modified.

WilliamsSwingLowValue

func (s Series) WilliamsSwingLowValue(low, open, cls Series) Series

WilliamsSwingLowValue is the low of the last confirmed swing low of Larry Williams’s one-bar swing chart, posted on its confirmation bar and held until the next, NaN before the first. The receiver is the highs; low, open and cls are the lows, the opens and the closes, and the run is that of WilliamsSwingDirection: a swing low is confirmed on the up bar that ends a down leg, at the lowest low of the leg (the latest of equal lows). It is a level for LevelBreakDirection, which reads it from the bar after it is posted. With dir := s.WilliamsSwingDirection(low, open, cls), the k-th previous swing low is value.ValueWhen(dir.Delta(1).GT(1), k).

Validation: low, open and cls must have the receiver’s length, otherwise the result is Series{}, as it is for an empty receiver. Warm-up: NaN until the first swing low is confirmed. Timing: the output on bar t uses bars 0 to t only, and a swing confirmed on bar t is posted on bar t. Pure: a new Series is returned and the inputs are not modified.

Compositions

Livermore Reaction

Penfold’s reading of Livermore’s reaction rule (The Universal Tactics of Successful Trend Trading, 2021): after the Dow trend of the swing chart turns up, wait for two reactions, that is two swing lows whose troughs come after the turn, then buy the next break of the latest swing high; exit when the trend turns down. Shorts are the mirror.

T := h.SwingTrend(l, n)
upF := T.Delta(1).EQ(2) // a turn from down to up; the seed, 0 to +1, is not a turn
dnF := T.Delta(1).EQ(-2)
rL := l.SwingLowEvent(n).Mul(l.SwingLowAge(n).LT(upF.BarsSince())) // a trough after the turn bar
rS := h.SwingHighEvent(n).Mul(h.SwingHighAge(n).LT(dnF.BarsSince()))
eL := rL.SumSince(upF).GTE(2).Mul(h.GT(h.SwingHighValue(n).Shift(1)).SetN(1, 0)).Mul(T.EQ(1))
eS := rS.SumSince(dnF).GTE(2).Mul(l.LT(l.SwingLowValue(n).Shift(1)).SetN(1, 0)).Mul(T.EQ(-1))
pos := c.PositionSigned(eL, dnF, eS, upF)
  • First valid index: 0; the book is flat until the first entry, which needs a turn of the trend (not its seed) and two swing lows after it. Two bar-count lows are more than n bars apart, so an entry comes no earlier than 2n + 2 bars after the turn bar.
  • Parameters: n >= 1, as for SwingTrend; n < 1 gives a flat book.
  • Timing: each flag acts on its own bar (close fills); pass the flags with Shift(1) for next-bar fills.
  • The trend gate: Mul(T.EQ(1)) keeps an outside bar that breaks both levels in an up trend (the latch turns down on it) from entering long on the same bar.
  • Anchors: a long position is held only while T is +1 and a short one only while it is -1; each trend gives at most one entry, since a long is held until the trend turns down; and a new trend inherits no reactions, because a break of the swing high while down is itself the turn, on which the count restarts.
  • Pinned by TestRecipeT2LivermoreReaction; runnable example ExampleSeries_SwingTrend.

Pivots

PivotIndices

func (s Series) PivotIndices(level int, thresholdPercent float64, filter ...[]bool) []int

PivotIndices returns a slice of integers that represents pivot highs and lows in a series, where a high is a value with a lower value either side, and a low is a value with a higher value either side. A specified threshold represents a percentage price movement to be observed to qualify as a pivot. An optional extra slice of booleans can be provided, indicating whether a bar should be considered (which is useful if, for example, inside bars were to be ignored in the determination of a pivot point).

The specified level determines the number of times the price series is processed. When the level is 1, the pivots of the raw prices (which could be thought of as the short-term pivots) will be returned. When the level is 2, the pivots of the short-term pivots (which could be thought of as the medium-term pivots) will be returned. When the level is 3, the pivots of the medium-term pivots (which could be thought of as the long-term pivots) will be returned.

The slice of indices returned will contain positive values for pivot highs and negative values for pivot lows. Non-pivots are excluded from the results.

The results alternate strictly between highs and lows. Consecutive level-1 pivots of the same type (highs with no low between them, or lows with no high between them) collapse to the most extreme of them, the highest high or the lowest low, the later bar winning an exact tie. For example, Series{1, 5, 2, 2, 3, 1}.PivotIndices(1, 0) keeps only bar 1: the pivot highs at bars 1 (5) and 4 (3) collapse to the higher. At levels 2 and above consecutive same-type pivots also collapse to the most extreme of them, but the earlier bar wins an exact tie.

At level 1, when bars are appended, only the most recent entry can change (a later same-type pivot that is at least as extreme replaces it), so earlier entries are final.

Level construction stops once a level produces no pivots, since no higher level can be derived from an empty set; there is no other cap on level.

thresholdPercent is a percentage, so 5 means 5% and 0.05 means 0.05%, not 5%, and at level 1 it is compared strictly in the product form on the candidate pivot’s own price p, as in Pivots: a neighbour must lie strictly below (1 - t/100)*p for a high or strictly above (1 + t/100)*p for a low. A negative, NaN or infinite threshold is invalid and gives an empty, non-nil slice, whatever the level and filter. A threshold of 100 or more is accepted, but on positive prices it allows no level-1 pivot highs, because (1 - t/100)*p is then at most 0.

Pivots

func (s Series) Pivots(level int, thresholdPercent float64, filter ...[]bool) []int

Pivots returns a slice of integers that represents pivot highs and lows in a series, where a high is a value with a lower value either side, and a low is a value with a higher value either side. A specified threshold represents a percentage price movement to be observed to qualify as a pivot. An optional extra slice of booleans can be provided, indicating whether a bar should be considered (which is useful if, for example, inside bars were to be ignored in the determination of a pivot point).

The specified level determines the number of times the price series is processed. When the level is 1, the pivots of the raw prices (which could be thought of as the short-term pivots) will be returned. When the level is 2, the pivots of the short-term pivots (which could be thought of as the medium-term pivots) will be returned. When the level is 3, the pivots of the medium-term pivots (which could be thought of as the long-term pivots) will be returned.

Pivot highs and lows alternate, as in PivotIndices. Consecutive level-1 pivots of the same type (highs with no low between them, or lows with no high between them) collapse to the most extreme of them, the highest high or the lowest low, the later bar winning an exact tie. At levels 2 and above consecutive same-type pivots also collapse to the most extreme of them, but the earlier bar wins an exact tie.

The slice of indices returned will contain lookback offsets from the end of the slice (positive for pivot highs, negative for pivot lows, zero for non-pivots). For example, if the latest pivot high occurred four bars from the end of the input series, the returned slice would contain a value of 4 for that pivot high in the position four back from the end of the returned slice.

At level 1, when bars are appended, only the most recent pivot can change (a later same-type pivot that is at least as extreme replaces it), so earlier pivots are final: their bars do not change, although their lookback offsets grow by one with each appended bar.

Level construction stops once a level produces no pivots, since no higher level can be derived from an empty set; there is no other cap on level.

thresholdPercent is a percentage, so 5 means 5% and 0.05 means 0.05%, not 5%. At level 1 a neighbour must lie strictly below (1 - t/100)*p for a high or strictly above (1 + t/100)*p for a low, the product form on the candidate pivot’s own price p; because (1 + 10/100)*100 is 110.00000000000001 in floating point, a move of exactly 10% does not qualify. A negative, NaN or infinite threshold is invalid and gives all zeros (no pivots). A threshold of 100 or more is accepted, but on positive prices it allows no level-1 pivot highs, because (1 - t/100)*p is then at most 0.

Floor Trader Pivots

FloorTraderPivot

func (s Series) FloorTraderPivot(h, l, c Series) Series

FloorTraderPivot computes the floor trader pivot point as (H+L+C)/3. The receiver is not used. The high, low and close series h, l and c are right-aligned at the most recent bar: with m the length of the shortest of them, the last m bars of each are combined, and the result has the length of h with zeros in its first len(h)-m positions, so an empty h, l or c gives a zero-filled series. Series are paired by position only; see “Time alignment” in the manual.

The seven FloorTrader methods are pointwise: each level on a bar is computed from the h, l and c values given for that bar, bar by bar, the pivot in the fixed order ((H + L) + C)/3. The sources compute today’s levels from the prior period’s high, low and close, so the period is chosen through the inputs. For today’s levels from yesterday’s bar on daily bars, pass h.Shift(1), l.Shift(1), c.Shift(1), or shift the result: the two are equal bit for bit on every bar, and both read 0 on bar 0, so mask that bar with SetN(1, math.NaN()). For weekly or monthly levels on daily bars (or daily levels on intraday bars), pass the prior completed period’s high, low and close, latched from a flag that is 1 on each period’s first bar: with newM := tdm.Equal(1) for months, h.HighestSince(newM).Shift(1).SetN(1, math.NaN()).ValueWhen(newM) is the prior month’s high, LowestSince gives its low in the same way and c.Shift(1).SetN(1, math.NaN()).ValueWhen(newM) its last close, each NaN until the first completed period. A last-bar-of-period flag would read the next bar’s date, which is look-ahead; that is why the period is the caller’s input and not a parameter of these methods. h.SAverage(l, c) equals the pivot bit for bit; c.SAverage(h, l) adds in another order and differs from it in the last bit on some bars. Published examples print rounded or truncated levels (Le Beau and Lucas truncate the pivot to the cent before computing the others), so compare with them under the source’s rounding rule. The compositions (the prior-bar and prior-period levels, the Camarilla, Fibonacci and DeMark levels and the central pivot range) are listed under “Compositions” in “Floor Trader Pivots”.

FloorTraderR1

func (s Series) FloorTraderR1(h, l, c Series) Series

FloorTraderR1 computes the first floor trader resistance level, R1 = 2*P - L. The receiver is not used; h, l and c are right-aligned at the most recent bar exactly as for FloorTraderPivot.

FloorTraderR2

func (s Series) FloorTraderR2(h, l, c Series) Series

FloorTraderR2 computes the second floor trader resistance level, R2 = P - S1 + R1, which is P + (H - L) in exact arithmetic. The receiver is not used; h, l and c are right-aligned at the most recent bar exactly as for FloorTraderPivot.

FloorTraderR3

func (s Series) FloorTraderR3(h, l, c Series) Series

FloorTraderR3 computes the third floor trader resistance level, R3 = H + 2*(P - L), mirroring FloorTraderS3 (S3 = L - 2*(H - P)). The receiver is not used; h, l and c are right-aligned at the most recent bar exactly as for FloorTraderPivot.

FloorTraderS1

func (s Series) FloorTraderS1(h, l, c Series) Series

FloorTraderS1 computes the first floor trader support level, S1 = 2*P - H. The receiver is not used; h, l and c are right-aligned at the most recent bar exactly as for FloorTraderPivot.

FloorTraderS2

func (s Series) FloorTraderS2(h, l, c Series) Series

FloorTraderS2 computes the second floor trader support level, S2 = P - R1 + S1, which is P - (H - L) in exact arithmetic. The receiver is not used; h, l and c are right-aligned at the most recent bar exactly as for FloorTraderPivot.

FloorTraderS3

func (s Series) FloorTraderS3(h, l, c Series) Series

FloorTraderS3 computes the third floor trader support level, S3 = P - R2 + S1, which is L - 2*(H - P) in exact arithmetic. The receiver is not used; h, l and c are right-aligned at the most recent bar exactly as for FloorTraderPivot.

Compositions

The central pivot range is Mark Fisher’s daily pivot range and Franklin Ochoa’s CPR: the pivot of the prior bar, the prior bar’s midpoint (Ochoa’s bottom central, BC, and Fisher’s “second number”) and TC = 2P - BC, the midpoint’s mirror about the pivot, with the range running from the smaller to the larger of the two, since “the highest level of the two is automatically designated TC” in Ochoa’s software. As on the pivot-level page, the recipes pass the prior bar’s values, ph := h.Shift(1).SetN(1, math.NaN()) and likewise pl and pc (the doc note on FloorTraderPivot), so every level is NaN on bar 0 and every flag on the levels reads 0 there; a level is known at its bar’s open and a flag at its bar’s close. Inputs are daily bars of one length, paired by position, positive and finite with h >= l on every bar; the range’s width and histogram divide by the pivot and need positive prices.

Central pivot range, width and histogram (Fisher; Ochoa)

Fisher (The Logical Trader, 2002, ch. 2, pp. 36-39) adds “the high, low, and close of the previous trading day and divide[s] by three”, takes the high plus the low over 2 as the second number, and their difference as the “daily pivot differential”: “The daily pivot range is equal to the pivot number plus or minus the pivot differential”. Ochoa (Secrets of a Pivot Boss, 2010, ch. 6, pp. 135-179, Table 6.1) writes TC = (Pivot - BC) + Pivot and BC = (High + Low)/2, and his Pivot Range Histogram (Appendix B, iPivotRangeHIST) plots the width as a percentage of the pivot, reading “above 0.75” as sideways and “below 0.25” as trending. t6CentralPivotRange returns all seven series.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
p := c.FloorTraderPivot(ph, pl, pc)
bc := ph.Add(pl).Div(2)
tc := p.Mul(2).Sub(bc)
top := tc.Maximum(bc)
bot := tc.Minimum(bc)
width := top.Sub(bot)
hist := width.Div(p).Mul(100)
  • Inputs: daily highs, lows and closes of one sign (positive prices; the histogram divides by the pivot and reads 0 where it is 0).
  • First valid index: 1 for all seven outputs, NaN on bar 0.
  • Anchors: Fisher’s form, pv.Add(pv.Sub(m).Abs()) and pv.Sub(pv.Sub(m).Abs()) with pv := ph.SAverage(pl, pc) and m := ph.Add(pl).Div(2), equals top and bot bit for bit; width equals tc.Sub(bc).Abs() bit for bit; top is bc and bot is tc exactly where tc <= bc. Ochoa’s listing order p.Sub(bc).Add(p) equals tc bit for bit on prices of one sign and differs on a bar that crosses zero (high 2.38, low -4.65, close 2.18: 1.075 against 1.0750000000000002). Fisher’s four examples hold, and pin the helper through the exact p, top and bot: p. 37 (21.00, 20.00, 20.75) gives 20.58 and 20.50 to 20.66; p. 38 (24.50, 22.50, 23.25) 23.42, a differential of 0.08 and 23.34 to 23.50, where the exact range is 23.3333 to 23.50; the midterm question of p. 110 (29.00, 28.15, 28.65), answered on p. 112, 28.60 and 28.57 to 28.63; all three under his rule, the pivot rounded to the tick and the differential from it rounded half up, computed in whole ticks. His p. 52 range, 9.005 to 8.650 from 9.870, 8.140 and 8.472, is the exact range rounded to the tick; his own rule would give 8.649.
  • Traps: the width is |2C - H - L|/3 only to rounding (449, 577 and 629 of 719 bars of the three test tapes differ in the last bits); at an exact-midpoint prior close the width is 0 on the tick tapes but on only 22 of 47 such bars of the unrounded tape. TC falls below BC whenever the prior close is below its midpoint (305, 323 and 352 of 719 bars of the three test tapes), so the labels TC and BC are not the top and bottom; the histogram’s operand is top minus bottom, which is not negative, rather than TC - BC. Fisher’s printed 23.34 is his rounding, not the exact bound.
  • Variant: Ochoa’s histogram markers as flags, hist.LTE(0.25) (“at or below the 0.25 level”) and hist.GT(0.75); ties at the markers are his reading of the text, not stated.
  • Evidence: Ochoa reads the histogram qualitatively, with no statistics, and Fisher’s ACD examples are charts. Evidence, not a reference.
  • Pinned by TestRecipeT6CentralPivotRange; runnable example ExampleSeries_Maximum_centralPivotRange.

Two-day pivot range relationships (Ochoa)

Ochoa’s Table 6.2 classes today’s range against yesterday’s: Higher Value, when “today’s bottom central pivot point is higher than the top of the prior day’s pivot range”, Lower Value, Overlapping Higher Value (“the top of the range is higher than the top of yesterday’s range, but the bottom of the range is lower than the top of yesterday’s range”), Overlapping Lower Value, Outside Value (“completely engulfs”), Inside Value and Unchanged Value, each with a bias that the open then confirms or rejects. t6PivotRangeRelations returns the six with exact definitions, made exclusive.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
p := c.FloorTraderPivot(ph, pl, pc)
bc := ph.Add(pl).Div(2)
tc := p.Mul(2).Sub(bc)
top := tc.Maximum(bc)
bot := tc.Minimum(bc)
top1 := top.Shift(1)
bot1 := bot.Shift(1)
higher := bot.GT(top1)
lower := top.LT(bot1)
outside := top.GT(top1).Mul(bot.LT(bot1))
inside := top.LT(top1).Mul(bot.GT(bot1))
overHigher := top.GT(top1).Mul(bot.LTE(top1)).Mul(bot.GTE(bot1))
overLower := bot.LT(bot1).Mul(top.GTE(bot1)).Mul(top.LTE(top1))
  • Inputs: daily highs, lows and closes, as for the central pivot range.
  • First valid index: 2 (0 before): on bar 1 yesterday’s range is bar 0’s NaN.
  • Anchors: each class equals a loop of its definition on the test tapes and witnesses, and at most one is 1 on a bar.
  • Traps: Ochoa’s literal Overlapping Higher Value, without the test of today’s bottom against yesterday’s bottom, also matches Outside Value on some bars; the extra bot.GTE(bot1) (and top.LTE(top1) for the lower class) makes the classes exclusive. “Unchanged Value” (“virtually unchanged”) has no tolerance, so a range with equal top and bottom falls in no class; a caller’s tolerance decides it.
  • Variant: the same classes on the Camarilla band (Ochoa, ch. 8), with the masked h3 and l3 in place of top and bot.
  • Evidence: Ochoa’s biases are illustrated with charts and come with no statistics. Evidence, not a reference.
  • Pinned by TestRecipeT6PivotRangeRelations.

Trailing-centrals stop and the Golden Pivot Zone (Ochoa)

Ochoa’s stop listing (Appendix B, stoTrailingCentralsStop) exits a long “if L <= fTCS” with the bottom central and a short “if H >= fTCS” with the top, where his ch. 6 text says the trade will “exit once price closes beyond the pivot range”. His Golden Pivot Zone (ch. 10, pp. 266-273; the double pivot hot zones from p. 261) “occurs when one of the pivot levels from either the Camarilla Equation or the Money Zone lies within the central pivot range”: the Camarilla third resistance for a bearish zone, the third support for a bullish one. t6PivotRangeSignals pins the listing’s touches and the Camarilla zones, both ends inclusive.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
p := c.FloorTraderPivot(ph, pl, pc)
bc := ph.Add(pl).Div(2)
tc := p.Mul(2).Sub(bc)
top := tc.Maximum(bc)
bot := tc.Minimum(bc)
rg := ph.Sub(pl)
h3 := pc.Add(rg.Mul(1.1).Div(4))
l3 := pc.Sub(rg.Mul(1.1).Div(4))
exitLong := l.LTE(bot)
exitShort := h.GTE(top)
gpzBear := h3.GTE(bot).Mul(h3.LTE(top))
gpzBull := l3.GTE(bot).Mul(l3.LTE(top))
  • Inputs: daily highs, lows and closes, as for the central pivot range.
  • First valid index: 1 (0 before).
  • Anchors: the exits equal loops of the listing’s touches. The bearish zone equals the close-location test pc.Sub(pl).LTE(ph.Sub(pl).Mul(0.225)) and the bullish zone pc.Sub(pl).GTE(ph.Sub(pl).Mul(0.775)) on every bar of the test tapes: the Camarilla zone says nothing beyond where the prior close sat in its range. The dossier’s literal h3.GTE(bc).Mul(h3.LTE(tc)) fires on 33, 28 and 24 bars of the three tapes against the zone’s 170, 167 and 191.
  • Traps: the “trail” is each session’s own bottom central, which moves against the position too; it is not a ratchet. Ochoa prints the zone’s inequality as “(TC >= MZ/CE <= BC)”, a misprint his scan section corrects to “TC >= POC >= BC”, and the bearish zone needs TC below BC, so the literal [bc, tc] form cannot fire there. Ochoa’s Money Zone levels are the prior session’s value area and point of control, built from its trading at each price; VolumeProfilePOC, VolumeProfileVAH and VolumeProfileVAL build a rolling profile from bars’ highs, lows and volumes instead, a stand-in this page does not pin. Double pivot hot zones need a caller’s tolerance, a.Sub(b).Abs().LTE(tol).
  • Variant: the text’s close form of the stop, c.LT(bot) and c.GT(top).
  • Evidence: Ochoa’s examples of both are charts with no statistics. Evidence, not a reference.
  • Pinned by TestRecipeT6PivotRangeSignals.

Fisher’s tone and plus and minus days (Fisher)

Fisher (pp. 38-39) reads the “underlying tone or sentiment of the market … when you look at the close of the previous trading day compared with today’s pivot range”: above it is bullish, below it bearish, within it neutral. A plus day (pp. 49-51) is “Opening Range < Pivot Range < Close”, a minus day the mirror, and “Everything else is a zero day”. t6FisherTone counts the prior bar’s 2C - H - L in whole ticks for the tone, and tests the day’s open and close against today’s range for plus and minus days.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
pv := ph.SAverage(pl, pc)
m := ph.Add(pl).Div(2)
d := pv.Sub(m).Abs()
top := pv.Add(d)
bot := pv.Sub(d)
x := c.Mul(2).Sub(h).Sub(l).Mul(ticks).Round()
tone := x.GT(0).Sub(x.LT(0)).Shift(1).SetN(1, math.NaN())
plus := o.LT(bot).Mul(c.GT(top))
minus := o.GT(top).Mul(c.LT(bot))
  • Inputs: daily opens, highs, lows and closes; ticks, the instrument’s ticks per price unit (100 for cents).
  • First valid index: 1: tone is NaN on bar 0, where a 0 would read as a real tone, the neutral one (unlike a latch’s undetermined 0); plus and minus read 0 there.
  • Anchors: Fisher’s four examples give the tones +1, -1, +1 and -1. On the cent tape tone equals the sign of 2C - H - L counted in whole cents on every bar, its 54 decimal ties included, where the float tests (the prior close against its own range, or against its midpoint) misread 6 bars; with the data’s own tick the tick form is right on every tie. A half-tick value of 2C - H - L (ticks 2 on quarter prices) pins Round’s tie: half away from zero gives +1 and -1 where half to even would give 0.
  • Traps: on the daily range the tone is degenerate: the prior close lies above its own range exactly when it is above its own midpoint, and inside only at an exact midpoint, so the tone carries information only against a range not built from the same bar. The opening range is approximated by the open, a departure: the opening range needs intraday bars. Off the data’s tick (an unrounded tape) the tick form reads 0 within half a tick of the midpoint.
  • Variant: the text’s point form of plus days, o.LT(pv).Mul(c.GT(pv)) with pv := ph.SAverage(pl, pc), which differs from the range form when the open lies between the bottom and the pivot; the 30-day recurrence, plus.Shift(30).Mul(o.LT(bot)).
  • Evidence: Fisher reports “a statistically significant chance” that a plus day 30 days ago repeats, with no statistics. Evidence, not a reference.
  • Pinned by TestRecipeT6FisherTone.

Fisher’s rolling pivot range (Fisher)

Fisher’s three-day rolling pivot (pp. 47-48) uses “the highest high and the lowest low of the three-day trading period, and the settlement of the third trading day”, plugged into the pivot range formula, and “can be used to determine a trailing stop”: “With a long position, a stop is placed just below the bottom of the pivot range.” t6RollingPivotRange gives tomorrow’s range from the n bars to yesterday.

hn := h.Highest(n).Shift(1).SetN(n, math.NaN())
ln := l.Lowest(n).Shift(1).SetN(n, math.NaN())
cn := c.Shift(1).SetN(n, math.NaN())
pv := hn.SAverage(ln, cn)
m := hn.Add(ln).Div(2)
d := pv.Sub(m).Abs()
top := pv.Add(d)
bot := pv.Sub(d)
  • Inputs: daily highs, lows and closes; n (Fisher’s 3).
  • First valid index: n, NaN before: the shifted windows are full from bar n.
  • Anchors: the three outputs equal a loop of Fisher’s rule bit for bit (n = 1, 3 and 5, a NaN high included); with n = 1 they equal the central pivot range’s p, top and bot bit for bit. His midterm bar (29.00, 28.15, 28.65) gives 28.599999999999998, 28.624999999999996 and 28.575.
  • Traps: the stop’s offset (“just below”) is the caller’s. Fisher calls it a stop “moved in the direction of the trade” with profits “locked in” (pp. 47-48), a ratchet in words, but the rolling range he computes also moves against the position (the ratchet variant below is that reading). The dossier’s c.SAverage(h.Highest(3), l.Lowest(3)) adds in another order and differs in the last bit on some bars.
  • Variant: his pivot moving averages, h.SAverage(l, c).Average(14).SetN(13, math.NaN()) with 30 and 50, whose slopes agree for a consensus (ch. 5, pp. 113-118).
  • Variant: a ratchet, bot.HighestSinceEntry(pos) over the caller’s long position.
  • Evidence: Fisher’s three-day pivot moving average example (pp. 115-116: 34.20, 34.25, 34.30 and so on) reproduces with Average(3), a check of Average rather than of the range; his Figure 2.9 is a chart. Evidence, not a reference.
  • Pinned by TestRecipeT6RollingPivotRange.

The shipped FloorTraderPivot, FloorTraderR1 to FloorTraderR3 and FloorTraderS1 to FloorTraderS3 are pointwise: they compute each level from the high, low and close they are given, bar by bar, while every source below computes today’s levels from the prior bar or period. The recipes therefore pass the prior bar’s values, ph := h.Shift(1).SetN(1, math.NaN()) and likewise pl and pc, or the prior period’s latched values, and change no method: the doc note on FloorTraderPivot states the rule (shifting the inputs or the output gives the same bits, and both read 0 on bar 0, hence the mask). Inputs are daily bars o, h, l and c of one length, paired by position, positive and finite with h >= l on every bar; calendar information comes from the caller’s keys only (tdm and weekday, “Calendar keys” under Comparison) and the week-start flag nw of the weekly channel (“Weekly channel stop and reverse” in donchian-channels/t6-breakout-systems.md). Every level is NaN on bar 0 and every flag built on the levels reads 0 there; a level is known at its bar’s open, a flag at its bar’s close. TradingView’s ta.pivot_point_levels(type, anchor, developing) takes the completed higher-timeframe bar at each anchor, the first bar of the new period, and holds its levels until the next one: with a daily anchor its types “Traditional”, “Fibonacci”, “DM” and “Camarilla” are the first, third, fourth and fifth sections below, and “Classic” and “Woodie” are variants. “TD Range Projection”, “TD Tolerance Levels” and the other TD names are trademarks of DeMark Analytics LLC (formerly Market Studies LLC), used here only to name the indicators.

Classic floor-trader pivots from the prior bar (Weintraub via Le Beau and Lucas; Ochoa; TradingView Traditional)

Charles Le Beau and David W. Lucas (Day Trading Systems & Methods, 1992, ch. 7, pp. 40-44) credit the formula to Neal Weintraub, a Chicago floor trader: “We start the pivot point calculations by adding together the previous day’s high, low, and close. Then we divide by three”, and the pivot point high is that average times two less the previous day’s low. Franklin Ochoa (Secrets of a Pivot Boss, 2010, ch. 5, pp. 111-134, Table 5.1) gives the same three layers, “calculated using a prior period’s high, low, and close”, and TradingView’s help-centre article “Pivot Points Standard” (fetched 17 October 2026) prints them as its “Traditional” type, P = (prevHigh + prevLow + prevClose) / 3 and P * 2 - prevLow. t6FloorPivots feeds the shipped methods the prior bar’s masked values.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
p := c.FloorTraderPivot(ph, pl, pc)
r1 := c.FloorTraderR1(ph, pl, pc)
s1 := c.FloorTraderS1(ph, pl, pc)
r2 := c.FloorTraderR2(ph, pl, pc)
s2 := c.FloorTraderS2(ph, pl, pc)
r3 := c.FloorTraderR3(ph, pl, pc)
s3 := c.FloorTraderS3(ph, pl, pc)
  • Inputs: daily highs, lows and closes; the receiver c of the methods is not read. The levels are positive and ordered on ordinary bars; a NaN high, low or close makes the next bar’s seven levels NaN, and only that bar’s.
  • First valid index: 1 for all seven levels, NaN on bar 0. Unmasked, c.FloorTraderR1(h.Shift(1), l.Shift(1), c.Shift(1)) reads 0 on bar 0, so a touch flag such as h.GTE(r1) fires there.
  • Anchors: c.FloorTraderPivot(h, l, c).Shift(1) equals c.FloorTraderPivot(h.Shift(1), l.Shift(1), c.Shift(1)) bit for bit on every bar, bar 0 included (both +0), and so do the six other levels; each equals this helper’s output from bar 1. ph.SAverage(pl, pc) equals p bit for bit. In binary64 TradingView’s pivot, first resistance and support (P * 2 - prevLow, P * 2 - prevHigh) and third resistance (P * 2 + (prevHigh - 2 * prevLow)) equal p, r1, s1 and r3 bit for bit; its second levels, P + (prevHigh - prevLow) and P - (prevHigh - prevLow), and third support, P * 2 - (2 * prevHigh - prevLow), equal r2, s2 and s3 within 3 ulp (0 ulp on the test tapes; 1, 1 and 3 ulp on the floor-order witnesses). Le Beau and Lucas’s bar, high 365.30, low 361.30 and close 364.40, gives 363.6666666666667, 366.03333333333336, 362.03333333333336, 367.6666666666667 and 359.6666666666667 for p, r1, s1, r2 and s2; their printed 363.66, 366.02, 362.02, 367.66 and 359.66 come from the pivot truncated to the cent first and the levels computed from it (truncating each exact level gives 366.03 and 362.03). Ochoa’s YM bar of 24 March, 7,743, 7,587 and 7,618, gives 7649.333333333333, 7711.666666666666 and 7555.666666666666; his printed 7649, 7711 and 7555 come from the pivot cut to the YM’s one-point tick (the exact levels round to 7712 and 7556).
  • Traps: pc.SAverage(ph, pl) sums (C + H) + L and is not the pivot: it differs in the last bit on 133 and 127 of 719 bars of the cent and unrounded test tapes (none on the quarter-point tape), where ph.SAverage(pl, pc) is exact. The methods called on the same bar’s high, low and close, unshifted, give levels that read the bar they apply to (look-ahead). Ochoa’s tables print the second support as “Pivot - (High + Low)”, a typo for High - Low that gives a negative price. Published levels are rounded or truncated: compare with them under the source’s rule.
  • Variant: the shifted-output form, c.FloorTraderPivot(h, l, c).Shift(1).SetN(1, math.NaN()) and likewise for the other six, gives the same bits.
  • Evidence: Le Beau and Lucas checked the S&P “after the close”: “The low of the day was 362.10 versus our projected pivot point low of 362.02” (21 June 1990), and add that “these points only work because they are popular”. Ochoa’s Appendix A counts touches of the levels on 165 YM sessions, November 2008 to June 2009 (see the layer state below). One anecdote and one sample of eight months, without costs. Evidence, not a reference.
  • Pinned by TestRecipeT6FloorPivots; runnable example ExampleSeries_FloorTraderPivot_priorBar.

Fourth and fifth layers, named by source (Ochoa’s expanded pivots; TradingView Traditional)

Ochoa’s Table 5.2 (ch. 5) adds a fourth layer, the third resistance plus the distance from the first resistance to the second, and its mirror below (“I cannot take credit for discovering this formula, as it was already widely available”). TradingView’s “Traditional” type has different fourth and fifth layers, P * 3 + (prevHigh - 3 * prevLow) and P * 4 + (prevHigh - 4 * prevLow) with their mirrors, and the help-centre article notes that only the Traditional and Camarilla types draw a fifth layer. t6PivotsExpanded returns Ochoa’s pair (r4o, s4o) and TradingView’s four (r4t, s4t, r5t, s5t), each named by its source.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
p := c.FloorTraderPivot(ph, pl, pc)
r1 := c.FloorTraderR1(ph, pl, pc)
s1 := c.FloorTraderS1(ph, pl, pc)
r2 := c.FloorTraderR2(ph, pl, pc)
s2 := c.FloorTraderS2(ph, pl, pc)
r3 := c.FloorTraderR3(ph, pl, pc)
s3 := c.FloorTraderS3(ph, pl, pc)
r4o := r3.Add(r2.Sub(r1))
s4o := s3.Sub(s1.Sub(s2))
r4t := p.Mul(3).Add(ph.Sub(pl.Mul(3)))
s4t := p.Mul(3).Sub(ph.Mul(3).Sub(pl))
r5t := p.Mul(4).Add(ph.Sub(pl.Mul(4)))
s5t := p.Mul(4).Sub(ph.Mul(4).Sub(pl))
  • Inputs: daily highs, lows and closes, as for the classic levels.
  • First valid index: 1 for all six levels, NaN on bar 0.
  • Anchors: each TradingView composition equals a binary64 transcription of the help-centre table, with each product rounded, bit for bit on every bar. r4t - r4o and s4t - s4o equal TC - BC of the central pivot range (tc.Sub(bc), signed) to rounding only, within 1e-14 of the pivot: the two fourth layers are different levels, not rounding variants. Le Beau and Lucas’s bar gives r4o 371.6666666666667, s4o 355.6666666666667, r4t 372.39999999999986, s4t 356.39999999999986, r5t 374.76666666666665 and s5t 354.76666666666665.
  • Traps: Ochoa’s fourth layer is not TradingView’s Traditional fourth layer, though a page “matching TradingView” may label it so; it is TradingView’s Classic third layer, P + 2(H - L), to rounding only (two of the five floor-order witnesses differ in the last bit). Ochoa’s second-support typo (“Pivot - (High + Low)”) is printed in both of his tables and gives about -7,681 on his YM bar; FloorTraderS2 is P - (H - L) to rounding.
  • Variant: TradingView’s “Classic” third and fourth resistance, p.Add(rg.Mul(2)) and p.Add(rg.Mul(3)) with rg := ph.Sub(pl), equal to its table bit for bit; the supports subtract.
  • Evidence: Ochoa’s Appendix A finds the YM reached the fourth layer of support “5.5 percent of the time; just 9 times out of 165 days”; TradingView gives definitions only. Evidence, not a reference.
  • Pinned by TestRecipeT6PivotsExpanded.

Fibonacci pivots (TradingView Fibonacci; Woodie as a variant)

TradingView’s “Fibonacci” type (“Pivot Points Standard”, fetched 17 October 2026) keeps the classic pivot and spaces three levels either side of it at 0.382, 0.618 and 1 times the prior range, P + 0.382 * (prevHigh - prevLow) and so on. t6FibonacciPivots composes them on the masked prior bar.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
p := c.FloorTraderPivot(ph, pl, pc)
rg := ph.Sub(pl)
fr1 := p.Add(rg.Mul(0.382))
fs1 := p.Sub(rg.Mul(0.382))
fr2 := p.Add(rg.Mul(0.618))
fs2 := p.Sub(rg.Mul(0.618))
fr3 := p.Add(rg)
fs3 := p.Sub(rg)
  • Inputs: daily highs, lows and closes, as for the classic levels.
  • First valid index: 1 for all six levels, NaN on bar 0.
  • Anchors: every level equals a binary64 transcription of TradingView’s Fibonacci table, with each product rounded, bit for bit on every bar of the test tapes and witnesses. fr3 is TradingView’s Traditional second resistance, P + (prevHigh - prevLow), by construction, and FloorTraderR2 to rounding. Le Beau and Lucas’s bar gives fr1 365.1946666666667, fs1 362.13866666666667, fr2 366.13866666666667, fs2 361.1946666666667, fr3 367.6666666666667 and fs3 359.6666666666667.
  • Traps: Woodie’s pivot uses the current bar’s open, so it is known only at that bar’s open (TradingView’s release notes of August 2022 say developing “Cannot be true when type is set to ‘Woodie’”); on unmasked inputs it reads half of bar 0’s open there. The dossier’s Woodie form ph.Add(pl).Add(o.Mul(2)) lacks the division by 4.
  • Variant: Woodie’s levels, wp := ph.Add(pl).Add(o.Mul(2)).Div(4), wp.Mul(2).Sub(pl) and wp.Mul(2).Sub(ph): TradingView’s Woodie pivot, first resistance and first support, bit for bit.
  • Evidence: the help centre defines the levels and gives no test of them, and no source read argues the ratios from data. Evidence, not a reference.
  • Pinned by TestRecipeT6FibonacciPivots.

TD Range Projection and TD Tolerance Levels (DeMark, in Perl; TradingView DM)

Jason Perl (DeMark Indicators, 2008, ch. 9, pp. 153-157) describes TD Range Projection as the indicator “that compares the current close with the same price bar’s opening, high, and low, to forecast a high/low range for the following price bar”: X weights the high (an up bar), the low (a down bar) or the close (a close equal to the open) twice, and the projected high and low are X less the low and X less the high, his X already halved. TradingView’s “DM” type writes X without the halving, with P = X / 4, X / 2 - prevLow and X / 2 - prevHigh. The TD Tolerance Levels (p. 155) multiply “the previous price bar’s true range by 15 percent” and add it to, or subtract it from, “the current price bar’s open”. t6DeMarkPivots composes TradingView’s form, with a guard that makes X NaN for a NaN open.

x := h.Mul(c.GT(o).Add(1)).Add(l.Mul(c.LT(o).Add(1))).Add(c.Mul(c.EQ(o).Add(1))).Add(o.Mul(0))
px := x.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
ph := h.Shift(1).SetN(1, math.NaN())
dp := px.Div(4)
dr1 := px.Div(2).Sub(pl)
ds1 := px.Div(2).Sub(ph)
tr := c.TrueRange(h, l, c).Shift(1)
upTol := o.Add(tr.Mul(0.15)).SetN(1, math.NaN())
dnTol := o.Sub(tr.Mul(0.15)).SetN(1, math.NaN())
  • Inputs: daily opens, highs, lows and closes of one length (with unequal lengths TrueRange is empty and the tolerance levels read 0); the bar’s own open and close select the branch, and a close equal to the open, tested exactly, is the tie branch.
  • First valid index: 1 for all five outputs, NaN on bar 0. Bar 1’s tolerance uses TrueRange[0] = h[0] - l[0], the true-range page’s convention for the first bar.
  • Anchors: the weighted x equals TradingView’s three branch sums, 2 * prevHigh + prevLow + prevClose and the others in its order, bit for bit on every bar of the test tapes, ties included, and through them dp, dr1 and ds1; Perl’s X, the same sum halved, less the low and less the high gives dr1 and ds1 bit for bit. The guard .Add(o.Mul(0)) changes no bar with a finite open. dr1 - ds1 is the prior range to rounding only (one of the five floor-order witnesses differs in the last bit). On Le Beau and Lucas’s bar, with the open set to the close (X = H + L + 2C), dp is 363.85, dr1 366.40000000000003, ds1 362.40000000000003, upTol 365 and dnTol 363.79999999999995.
  • Traps: Perl’s X includes the division by 2 (his projected high is X - L), and his first case is misprinted “2 +” for “2 x”. The dossier’s If(c.GT(o), ...) passes a series as If’s condition, which takes a Go function, and returns zeros. Its order h.Add(l).Add(c).Add(h.Mul(c.GT(o)))... adds the doubled term last and differs in the last bit on 147 and 225 of 720 bars of the cent and unrounded tapes. Without the guard a NaN open makes all three branch flags 0 and x the finite, wrong H + L + C; with it x is NaN.
  • Variant: Perl’s trend-day reading of the open, o.GT(dr1) and o.LT(ds1): “When the market opens above the projected high or below the projected low, it is often a reflection of the near-term trend”.
  • Missing piece: the four tolerance scenarios (“dips below the downside TD Tolerance Level before the high exceeds the projected high”) need the order of the touches inside the bar, which daily bars do not hold: intraday data or algolang. The page ships the levels and the open-location flags only.
  • Evidence: Perl’s Figures 9.1 to 9.4 (the March 2008 S&P 500 futures and WTI crude oil, late 2007) are charts with no bar or level values, and he reports no statistics. Evidence, not a reference.
  • Pinned by TestRecipeT6DeMarkPivots.

Camarilla equation (Ochoa 2010 standard and expanded; TradingView; Bobrowski 2013)

Ochoa’s Tables 7.1 and 7.2 (ch. 7, pp. 181-199) give “the most commonly used Camarilla Equation”, whose originator, a bond trader he names as Nick Stott, “has not disclosed the official equation”: four levels either side of the prior close at the prior range times 1.1/12, 1.1/6, 1.1/4 and 1.1/2, a fifth resistance of (High/Low) x Close and a fifth support as far below the close as that is above it. Slawomir Bobrowski (“Camarilla Points”, Stocks & Commodities V31:3, March 2013, pp. 10-15) and TradingView’s “Camarilla” type use the same set, written as the range times 1.1 divided by k. t6Camarilla pins that order and returns the ten levels.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
rg := ph.Sub(pl)
h1 := pc.Add(rg.Mul(1.1).Div(12))
h2 := pc.Add(rg.Mul(1.1).Div(6))
h3 := pc.Add(rg.Mul(1.1).Div(4))
h4 := pc.Add(rg.Mul(1.1).Div(2))
h5 := ph.Div(pl).Mul(pc)
l1 := pc.Sub(rg.Mul(1.1).Div(12))
l2 := pc.Sub(rg.Mul(1.1).Div(6))
l3 := pc.Sub(rg.Mul(1.1).Div(4))
l4 := pc.Sub(rg.Mul(1.1).Div(2))
l5 := pc.Sub(h5.Sub(pc))
  • Inputs: daily highs, lows and closes, positive: h5 divides by the prior low, and Div gives 0 for a zero divisor (on the zero-low witness h5 reads 0 and l5 twice the close).
  • First valid index: 1 for all ten levels, NaN on bar 0.
  • Anchors: every level equals a binary64 transcription of TradingView’s table, prevClose + 1.1 * (prevHigh - prevLow) / k and (prevHigh / prevLow) * prevClose, bit for bit. Ochoa’s constant form rg.Mul(1.1/k) is the same bits for k = 2 and 4 and within 1 ulp for 6 and 12 (for 6 on 5, 3 and 1 bars of the cent, unrounded and floor-order tapes, for 12 on 0, 2 and 1, and on none of the quarter-point tape). l5 equals pc.Mul(2).Sub(h5) where the prior high is at most twice the prior low. Bobrowski’s first example (13297.11, 12710.56, 12927.17) gives h5 13523.715829884759 and l5 12330.624170115241; 31 of his 36 printed levels hold at two decimals, example 2’s fifth support is within 0.01 (11876.45 against 11876.46), and the other four are misprints: example 1’s fourth resistance (13239.77 for 13249.77, which the issue’s Excel tip prints), example 4’s third resistance (12960.53 for 12960.33) and example 5’s third and fourth supports (13234.89 for 13234.39, and 13234.39 for 13197.13). The Excel tip’s h2, h1, l1 and l2 of example 1 hold: 13034.70, 12980.94, 12873.40 and 12819.64. Ochoa’s range of 194 points gives h3 - l3 = 106.7, his “107 point spread”.
  • Traps: pc.Mul(ph).Div(pl) reorders h5 and differs from it by up to 2 ulp (197, 250 and 335 bars of the three tapes). The dossier’s variant with 1.1/4 for the fourth resistance has no source: every source read uses 1.1/2 for h4 and 1.1/4 for h3, and the published variants differ above the fifth layer. The roles are prose, not flags: Ochoa fades at h3 and l3, takes breakouts beyond h4 and l4 and targets h5 and l5.
  • Variant: the sixth-layer forms (a marketcalls script of 2015, h5.Add(h5.Sub(h4).Mul(1.168)), and Barchart’s, unverified) are not built.
  • Evidence: Bobrowski’s six DJIA examples of 2012 use blocks of 3 to 27 sessions chosen with Wilder’s delta phenomenon and print levels only, no results; the issue’s Traders’ Tips results are platform output. Evidence, not a reference.
  • Pinned by TestRecipeT6Camarilla; runnable example ExampleSeries_Div_camarilla.

Prior completed period’s open, high, low and close (Ochoa; Fisher; TradingView’s anchor)

Ochoa computes weekly pivots “by using the prior week’s high, low, and close prices” (ch. 9, p. 241; his text later calls it Chapter 10) and Mark Fisher tells a longer-term player to “use the high, low, and close of a longer time period” (The Logical Trader, 2002, ch. 2, pp. 36-37). TradingView’s ta.pivot_point_levels with developing false returns “the values … calculated the last time the anchor condition was true”, which “remain constant until the anchor condition becomes true again” (Pine Script release notes, August 2022); its help centre’s Auto mode uses monthly levels on a daily chart. t6PriorPeriodLevels latches the prior period’s values from a flag that is 1 on each period’s first bar, the causal boundary rule of the timeframe page (“Mapping a higher timeframe onto base bars without look-ahead” in relative-timeframe/t3-timeframes.md). Its ph, pl and pc are now the methods PriorPeriodHighest, PriorPeriodLowest and PriorPeriodLast, bit for bit on every bar (pc when ev is at least as long as c; see relative-timeframe/t4b-prior-period-levels.md), and po stays o.ValueWhen(ev, 1).

po := o.ValueWhen(ev, 1)
ph := h.HighestSince(ev).Shift(1).SetN(1, math.NaN()).ValueWhen(ev)
pl := l.LowestSince(ev).Shift(1).SetN(1, math.NaN()).ValueWhen(ev)
pc := c.Shift(1).SetN(1, math.NaN()).ValueWhen(ev)
  • Inputs: daily opens, highs, lows and closes and the flag ev, non-zero and not NaN on each period’s first bar. Months: tdm.Equal(1). Weeks: the caller’s nw, 1 on each week’s first session from a change of ISO week and 0 on bar 0; on daily bars without it, weekday.LTE(weekday.Shift(1)), which equals nw on every session of the 2001 test calendar (Monday 17 September after Monday 10 September included) but misses a boundary when two sessions 8 or more calendar days apart have the later weekday greater, and fires on every intraday bar of a session after the first.
  • First valid index: data-dependent. po, ph and pl start on the second flagged bar, counting bar 0 when it is flagged (tdm[0] = 1); pc starts on the first flagged bar after bar 0; NaN before. On the 2001 calendar a history from 2 January gives bar 21 for all four (1 February); from 1 August, 23; from 15 August, 28 (pc 13); from 17 September, 33 (pc 10); weeks from 2 January, 9 (pc 4), since nw drops the first week.
  • Anchors: the four outputs equal, bit for bit, a loop over the calendar months and ISO weeks of the 2001 NYSE calendar (its holidays and the 11-14 September closure), built from dates, from four start dates. With every bar flagged ph, pl and pc are the classic levels’ masked prior-bar inputs, bit for bit.
  • Traps: without the inner SetN(1, math.NaN()) a history that starts on a month’s first session reads 0, the Shift’s zero latched, for its whole first month. pc is valid one period before ph when the history starts mid-period: that close is genuine, and every level built on ph is NaN there by itself. A NaN high in a period makes the next period’s ph NaN (13 March 2001 makes April’s). A last-bar-of-period flag would read the next bar’s date, which is look-ahead; daily levels on intraday bars need a caller’s session flag.
  • Variant: the monthly pivot, c.FloorTraderPivot(ph, pl, pc) on the month flag; Robert Krausz’s weekly balance point and bands (“Dynamic Multiple Time Frames”, Stocks & Commodities V14:11, November 1996), bp := c.FloorTraderPivot(ph, pl, pc), bp.Add(ph.Sub(pl).Mul(0.5)) and bp.Add(ph.Sub(pl).Mul(0.618)) on the week flag.
  • Missing piece: a week key in the calendar-key table, an ISO-week ordinal or a trading day of the week like tdm, would make the week flag exact across any closure and on intraday bars and keep a first week that starts on its first session; it is algolang’s key generation, not an nseries method.
  • Evidence: TradingView’s built-in uses this anchor convention; Krausz’s figures are charts with no numbers. Evidence, not a reference.
  • Pinned by TestRecipeT6PriorPeriodLevels; runnable example ExampleSeries_ValueWhen_priorMonth.

Layer state and the pivot trend (Ochoa; Person)

Ochoa’s Appendix A (pp. 289-296) notes “Anytime the YM merely touched a pivot”, counts the deepest layer reached and the layer of each close, and records the days the central pivot is not touched. His pivot trend (ch. 5), after John Person, turns bullish when the price “closed beyond” the first resistance and stays so until “price closed beneath” the first support, and the mirror turns it bearish. t6PivotLayers returns the touch layer, the signed close layer, the untouched-pivot flag and the trend latch, with Ochoa’s fourth layer.

ph := h.Shift(1).SetN(1, math.NaN())
pl := l.Shift(1).SetN(1, math.NaN())
pc := c.Shift(1).SetN(1, math.NaN())
p := c.FloorTraderPivot(ph, pl, pc)
r1 := c.FloorTraderR1(ph, pl, pc)
s1 := c.FloorTraderS1(ph, pl, pc)
r2 := c.FloorTraderR2(ph, pl, pc)
s2 := c.FloorTraderS2(ph, pl, pc)
r3 := c.FloorTraderR3(ph, pl, pc)
s3 := c.FloorTraderS3(ph, pl, pc)
r4 := r3.Add(r2.Sub(r1))
s4 := s3.Sub(s1.Sub(s2))
up := h.GTE(r1).Add(h.GTE(r2)).Add(h.GTE(r3)).Add(h.GTE(r4))
dn := l.LTE(s1).Add(l.LTE(s2)).Add(l.LTE(s3)).Add(l.LTE(s4))
touched := up.Maximum(dn)
closeLayer := c.GT(r1).Add(c.GT(r2)).Add(c.GT(r3)).Add(c.GT(r4)).Sub(c.LT(s1).Add(c.LT(s2)).Add(c.LT(s3)).Add(c.LT(s4)))
cpUntouched := h.LT(p).Add(l.GT(p))
trend := c.GT(r1).Sub(c.LT(s1)).Hysteresis(0, 0)
  • Inputs: daily highs, lows and closes, as for the classic levels.
  • First valid index: 1 (0 before); trend reads 0, undetermined, until the first close beyond r1 or s1.
  • Anchors: each output equals a loop of its rule on the three test tapes and a walk with a NaN high: the deepest layer the high or the low reached, touch inclusive; the number of layers the close is strictly beyond, signed; the pivot untouched; and the latch, which holds on a close equal to r1 or s1 (23, 19 and 89 such closes on the tapes). On the tapes closeLayer takes every value from -4 to 4, and on tick data the eight layers are in order on every bar.
  • Traps: trend on bar t includes bar t’s close; a rule that trades during bar t reads trend.Shift(1). A NaN close fires neither comparison, so the latch holds its state across it. The dossier’s “series thresholds or If-chaining” is not needed: the event latch on scalar thresholds reproduces the rule. Unmasked levels read 0 on bar 0, where every resistance touch fires and the trend latches +1. On a flat prior bar with unrounded prices the pivot can miss the low by an ulp and the layers cross by that much.
  • Variant: Person’s filter as gating flags, trend.Shift(1).GT(0).Mul(l.LTE(s1)) for an uptrend’s buy zone at the first support.
  • Evidence: Appendix A’s touch counts (63.0, 73.3, 38.2, 17.6 and 5.5 percent of 165 YM sessions for the central pivot and the first to fourth support layers) are nested, as touched is; intraday touches over eight months, data not available. Evidence, not a reference.
  • Pinned by TestRecipeT6PivotLayers; runnable example ExampleSeries_Hysteresis_pivotTrend.

A round-number level is a price on a fixed grid, and Ceil, Floor and Round (under Math) find the grid points around a price: c.Div(step).Ceil().Mul(step) is the grid level at or above the close and Floor the one at or below. That composition is exact only when the step and the prices are dyadic (quarter points, whole points); with a decimal step such as 0.0050 or 0.10 the quotient can land an ulp either side of an integer, so Ceil or Floor takes the wrong one, and the product can sit an ulp off the decimal level, so a high printed exactly at the level fails the touch test. This is the general lesson for any Div(step).Floor().Mul(step) composition: count in whole ticks and divide the level by the integer ticks per unit at the end. The levels below come from the prior session’s close and are known at the session’s open, masked with NaN on bar 0, where the touch flags read 0; a flag is known at its bar’s close, and a session that touches both levels does not say which came first. Inputs are session bars of one length, paired by position.

Round-number levels in integer ticks (Unger)

Andrea Unger (“Round Numbers And Price Behavior”, Stocks & Commodities V44:9, September 2026, pp. 32-35) calculates the levels “at the start of each trading session using the previous session’s close”: LevUp = step*ceiling(closes(1)/step) and LevDn = step*floor(closes(1)/step), with a long entry “when price breaks above the round-number level” and a short entry below, placed as stop orders in his PowerLanguage listing (p. 34); his example takes a close of 6,984.00 with a step of 10 to 6990 and 6980. t6RoundNumberLevels does the same in whole ticks, with osc the current close’s signed distance to its nearest level in steps.

ct := c.Mul(ticks).Round()
up := ct.Div(step).Ceil().Mul(step).Div(ticks).Shift(1).SetN(1, math.NaN())
dn := ct.Div(step).Floor().Mul(step).Div(ticks).Shift(1).SetN(1, math.NaN())
long := h.GTE(up)
short := l.LTE(dn)
osc := ct.Sub(ct.Div(step).Round().Mul(step)).Div(step)
  • Inputs: session highs, lows and closes of the contract actually traded (Unger’s custom non-back-adjusted ES series; the close is the session’s last trade, not a settlement and not a back-adjusted price); ticks > 0, the ticks per price unit (4 for ES, 10000 for four-decimal FX, 100 for cents), and step > 0, the grid spacing in ticks (40 for ten ES points, 50 for 50 pips). A zero step reads level 0 silently through Div’s zero rule.
  • First valid index: 1 for the levels and flags (NaN and 0 on bar 0), 0 for osc, which reads the current close.
  • Anchors: Unger’s example gives 6990 (bits 0x40bb4e0000000000) and 6980, touched by a high of 6990 and a low of 6980, and equals the price-unit form c.Div(10).Ceil().Mul(10) bit for bit, as on every dyadic grid. On decimal steps the levels are the decimal levels as parsed, bit for bit, on every bar of EUR/USD closes from 0.8000 to 1.5999 (50 and 100 pips) and cent stock prices at dimes, they bracket the prior close, dn <= c.Shift(1) <= up, and up - dn is 0 or one step in ticks; a prior close on the grid gives up == dn. The price-unit form errs there: at 50 pips it misses 102 up and 102 down levels and 75 touches of the up level in 600 bars, and at dimes on the cent tape 253 and 276 levels and 2 touches each way. A close of 1.1100 at 50 pips gives 1.11 for both levels, both touched, where the price-unit form gives 1.115 and misses the touch.
  • Traps: a ticks coarser than the data’s grid snaps each close to the nearest declared tick first, which moves the levels silently. osc uses Round, half away from zero, so an exact half step reads -0.5 for a positive close and +0.5 for a negative one: osc is in [-0.5, 0.5) for positive prices and (-0.5, 0.5] for negative ones. On negative prices Ceil of a quotient between -1 and 0 gives a level of -0, and Trunc in place of Floor gives the wrong down level. Unger’s “breaks above” is a stop order, a touch; his orders work only from 17:15 to 15:45, so a daily bar holds trades his system could not take, and on a session that touches both levels the order is unknown.
  • Variant: the price-unit form, c.Div(step).Ceil().Mul(step).Shift(1).SetN(1, math.NaN()) with step in price units, exact on dyadic grids only.
  • Variant: on 15-minute bars with a caller’s session-start flag first, the prior session’s close prev := c.Shift(1).SetN(1, math.NaN()).ValueWhen(first) gives his levels exactly, with his 17:15 to 15:45 order window and the first touch of each session as further flags.
  • Missing piece: his fixed stop, profit target and breakeven exits inside the session and the fill order of a bar that touches both levels are the strategy layer’s (algolang), not nseries methods.
  • Evidence: his Figure 2, the step optimisation on 15-minute ES bars from 2010 to 2025, prints no values; read from the chart, step 50 makes about 135,000 dollars where the text says “nearly” 150,000, and steps of 10, 20, 30, 60 and 90 lose. Figure 3 reports 200,762.50 dollars net with an average trade of 80.14 after optimising the stop and the target (the 2,505 trades are derived, not printed), in sample, with no costs stated. Neither of the next two bears on ES futures, the only market Unger tests: Carol Osler (Federal Reserve Bank of New York Staff Report 125, March 2001) finds stop-loss orders “clustered just above round numbers” for buys; Thomas Bulkowski (“Predicting Prices With Support And Resistance”, Stocks & Commodities V21:10, October 2003, pp. 34-40) finds round-number support “only appeared in HCRs 22% of the time in the 2,122 samples”. Evidence, not a reference.
  • Pinned by TestRecipeT6RoundNumberLevels; runnable example ExampleSeries_Ceil_roundNumbers.

Darvas Box

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.