Swing State
All functions · nseries package
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,SwingLowEventand theirValueandAgeviews, withMarketStructureon 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 inSwingPointIndiceswithSwingAppendOnly.
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.
| Construction | Two equal extremes | A price equal to the trigger |
|---|---|---|
| Bar-count pivots | neither is a swing (the window is strict) | does not apply |
| Clear method | keeps the first | does not turn (a touch does not clear) |
| Gann swings | keeps the latest | is not a higher high or lower low |
| Williams swings | keeps the latest, as SwingPointIndices | is not a move; an equal-range bar is inside |
| Level-break latch | does not apply | does not break |
| Basing points | keeps the first | is 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). 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). 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
nbars apart, so an entry comes no earlier than2n + 2bars after the turn bar. - Parameters:
n >= 1, as forSwingTrend;n < 1gives 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
Tis +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 exampleExampleSeries_SwingTrend.