Trend and regime
All functions · nseries package
Sections: Directional Movement · Average Directional Index · Aroon · Vortex Indicator · Random Walk Index · Choppiness Index · Efficiency Ratio · Congestion Index · Follow-Through Index · Regime Detection · Hurst Exponent · Structural Breaks
Directional Movement
DMIMinus
func (s Series) DMIMinus(h, l, c Series, n int) Series
DMIMinus computes the negative Directional Movement Indicator over n bars. It measures the strength of downward price movement, consistent with the TradeStation DirMovement implementation.
If a high, low or close is NaN or infinite, the result is NaN from that bar onwards, except on the first bar, whose result is always 0; earlier bars are unaffected. A close counts on its own bar, although the calculation first reads it on the next one, and any later bad bars change nothing. This is the library’s policy for recursive methods: a non-finite value poisons the running averages permanently, so the result does not recover. It is NaN rather than 0 because 0 would read as “no downward movement” and pass for a real reading instead of showing up as missing data. Ordered comparisons such as > and < are false for NaN, and != is true, so test the result with math.IsNaN before comparing it with a threshold. The rule is keyed on the input values, not on the arithmetic: finite inputs whose arithmetic overflows, such as magnitudes near 1e308, are outside it and keep whatever result that arithmetic gives, which can include infinities or NaN.
The result is a new series of the same length as the inputs. If n is less than 1, or the series are empty or differ in length, it is an empty, non-nil series and the values are not inspected. Any n of 1 or more is accepted, even one longer than the series. The receiver is not used and may be nil.
The Wilder averages start from zero, so the first values after bar 0 are partial; the numerator and the denominator share the same weights, so the ratio is largely unaffected.
DMIPlus
func (s Series) DMIPlus(h, l, c Series, n int) Series
DMIPlus computes the positive Directional Movement Indicator over n bars. It measures the strength of upward price movement, consistent with the TradeStation DirMovement implementation.
If a high, low or close is NaN or infinite, the result is NaN from that bar onwards, except on the first bar, whose result is always 0; earlier bars are unaffected. A close counts on its own bar, although the calculation first reads it on the next one, and any later bad bars change nothing. This is the library’s policy for recursive methods: a non-finite value poisons the running averages permanently, so the result does not recover. It is NaN rather than 0 because 0 would read as “no upward movement” and pass for a real reading instead of showing up as missing data. Ordered comparisons such as > and < are false for NaN, and != is true, so test the result with math.IsNaN before comparing it with a threshold. The rule is keyed on the input values, not on the arithmetic: finite inputs whose arithmetic overflows, such as magnitudes near 1e308, are outside it and keep whatever result that arithmetic gives, which can include infinities or NaN.
The result is a new series of the same length as the inputs. If n is less than 1, or the series are empty or differ in length, it is an empty, non-nil series and the values are not inspected. Any n of 1 or more is accepted, even one longer than the series. The receiver is not used and may be nil.
The Wilder averages start from zero, so the first values after bar 0 are partial; the numerator and the denominator share the same weights, so the ratio is largely unaffected.
Average Directional Index
ADX
func (s Series) ADX(h, l, c Series, n int) Series
ADX calculates the Average Directional Index (ADX) based on the given high, low, and close price series and a period n. Returns a series representing the ADX values for the input data.
If a high, low or close is NaN or infinite, the result is NaN from that bar onwards, except on the first bar, whose result is always 0; earlier bars are unaffected. A close counts on its own bar, although the calculation first reads it on the next one, and any later bad bars change nothing. This is the library’s policy for recursive methods: ADX is built on DMIPlus and DMIMinus, and both its initial running sum over the first n bars and its later smoothing depend on every earlier bar, so the result does not recover. It is NaN rather than 0 because a near-zero ADX reads as “no trend” and, in a regime filter for example, would pass for a real reading instead of showing up as missing data. Ordered comparisons such as > and < are false for NaN, and != is true, so test the result with math.IsNaN before comparing it with a threshold. The rule is keyed on the input values, not on the arithmetic: finite inputs whose arithmetic overflows, such as magnitudes near 1e308, are outside it and keep whatever result that arithmetic gives, which can include infinities or NaN.
The result is a new series of the same length as the inputs. If n is less than 1, or the series are empty or differ in length, it is an empty, non-nil series and the values are not inspected. Any n of 1 or more is accepted, even one longer than the series. The receiver is not used and may be nil.
The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n, the mean of the directional index over bars 1 to n; Wilder’s smoothing follows. Releases before this change gave the running sum divided by n on bars 1 to n-1, so a directional index of 100 read 20, 40, 60 and 80 at n = 5, a rising ramp that an ADX-rising rule reads as a strengthening trend. A NaN or infinite input inside the warm-up still gives NaN from its own bar.
The directional index that ADX averages is pdi.Sub(mdi).Abs().Mul(100).Div(pdi.Add(mdi)) with pdi = c.DMIPlus(h, l, c, n) and mdi = c.DMIMinus(h, l, c, n), bit for bit, 0 on bar 0 and where the sum is 0. Wilder’s ADXR on this ADX is a.Add(a.Shift(n-1)).Div(2) with a = c.ADX(h, l, c, n), valid from bar 2n-1. Both, and Wilder’s own sum-seeded DX, ADX and ADXR, are among the compositions under “Average Directional Index” in the manual.
ADXTrendMode
func (s Series) ADXTrendMode(h, l, c Series, n int, on, off, exhaust, early float64) Series
ADXTrendMode is Ruggiero’s ADX trend mode: for each bar it returns +1 while the market is trending, -1 while it is consolidating and 0 while the mode is undetermined. The rules are those of Table 4.9, “Rules for trend mode”, in the “Market Modes” section of chapter 4, “Trading Using Technical Analysis”, of Murray A. Ruggiero Jr., Cybernetic Trading Strategies (Wiley, 1997). n is the ADX length (Ruggiero uses 14). The receiver is not used (as in ADX).
With a the ADX series of the package (a[t] is its value on bar t), the five rules are:
- If ADX crosses above on, the market is trending: a[t-1] <= on and a[t] > on.
- If ADX crosses below off, it is consolidating: a[t-1] >= off and a[t] < off.
- If ADX crosses below exhaust from above, it is consolidating: a[t-1] >= exhaust and a[t] < exhaust.
- If ADX rises from below early on 3 out of 4 days, the market will start to trend (an early trend): a[t-4] < early and at least 3 of the 4 steps a[t-k] > a[t-k-1], k = 0 to 3, hold (4 of 4 qualifies). Rises are strict: an equal step is not a rise, and a[t-4] equal to early is not below.
- If a trend is based on rule 4, it remains in effect until the 5-day difference in ADX is less than 0: a[t] < a[t-5], a comparison and not a subtraction. A trend confirmed by a rule-1 cross is read here as no longer based on rule 4, so only rules 2 and 3 end it; Table 4.9 does not settle this.
Ruggiero prints no code for rules 4 and 5, so the forms above, and the crosses’ ties (a prior bar equal to a level counts, where EasyLanguage’s “cross over” wording is strict), are this package’s reading.
Precedence: an exit wins on the same bar in every state (rules 2 and 3 beat rule 4, and rule 5 beats rule 4 on the entry bar), so an early trend starts only on a bar with a[t] >= a[t-5], or before bar n+5. Rules 2 and 3 end an early trend too. A rule-1 cross clears the early flag, so rule 5 cannot end that trend on a later bar; on the cross’s own bar an exit still wins, rule 5 included. A bar with both a rule-1 cross and rule 4 starts a trend that is not early. Rules 1 and 2 cannot fire on one bar when off <= on, and rules 1 and 3 cannot when on <= exhaust; rules 2 and 3 can, and both mean consolidation.
The levels are parameters: Table 4.9 gives Ruggiero’s four (on 25, off 20, exhaust 45, early 10) and its text his ADX length, for a run of his rules as printed (his platform’s ADX is another matter, see below). The levels on and off must be finite with off <= on, exhaust must be finite or +Inf with exhaust >= on, and early must be finite or -Inf with early <= on. Anything else (NaN included) returns len(h) zeros without inspecting the values. exhaust = +Inf disables rule 3 and early = -Inf disables rule 4. If n < 1, h is empty or h, l and c differ in length, the result is the empty Series{}.
ADX here is the package’s ADX: zero-seeded Wilder averages, the first ADX on bar n, no rounding. Wilder’s and TA-Lib’s sum seeds give their first ADX on bar 2n-1 instead, so over a short history a crossing can move by several bars or appear in one series and not the other; the two agree once the seeds have decayed. Wilder’s original rounds ADX to whole numbers, which moves crossings of the integer levels; what TradeStation 4.0, Ruggiero’s platform, did is not known.
The warm-up reads 0, not NaN: bars 0 to n are 0 and the first valid index is n+1. ADX’s own warm-up (bars 0 to n-1 are 0) would otherwise fake a rule-1 cross on bar n and a rule-4 rise. From bar n+1 the result stays 0, the undetermined state, until the first event. A bad input inside the warm-up still gives NaN from max(p, 1): NaN wins over the warm-up zeros.
With early = -Inf, before the first NaN, rules 1 to 3 alone give the same series as
up := a.CrossesAboveS(on).SetN(n+1, 0)
dn := a.CrossesBelowS(off).Add(a.CrossesBelowS(exhaust)).GT(0).SetN(n+1, 0)
up.Sub(dn).Hysteresis(0, 0)
(a.CrossesBelowS(+Inf) is all zeros for a finite a). Rule 4 then adds trend bars and, through rule 5, consolidation bars where rules 1 to 3 are undetermined, and keeps every trend bar of rules 1 to 3 up to the first bar where a rule-1 cross meets the rule-5 exit of an early trend: the exit wins there. That needs ADX to rise from below early to above on within three bars and then fall back to on or below before the cross, so only a small n reaches it.
The trade gate is the caller’s GT(0), which keeps the trending bars; LT(0) gives the consolidating bars, and 0 is neither. The companion filter is BreakoutModeIndex, whose gate is GTE(2).
NaN: if a has a NaN, the result is NaN from its first NaN bar to the end, and the bars before it are those of the finite prefix. ADX is NaN from max(p, 1) to the end, where p is the first bar with a NaN or infinite high, low or close, so the result is NaN exactly where ADX is, and bar 0 is 0 even when its input is bad (unlike BreakoutModeIndex, which is NaN from p).
The result is path-dependent and not start-invariant: ADX is recursive from bar 0 and the latch carries its state from bar n+1, so a different start can change the mode on a bar. It is prefix-invariant: the result on a prefix of the inputs is the prefix of the result. It takes O(len(h)) time for any n, math.MaxInt included.
Ruggiero’s levels were fitted on Treasury bond futures, in-sample, so they are a starting point and not evidence that they hold out of sample or on another market.
Compositions
The recipes below read the package’s DMIPlus, DMIMinus and ADX, which start Wilder’s averages from zero (TradeStation’s DirMovement) and give the first ADX on bar n, the mean of the directional index over bars 1 to n; Wilder’s worksheet and TA-Lib sum the first 13 bars and give theirs on bar 2n - 1. Over about the first 5n bars the package’s directional index differs from theirs by more than a point, and its ADX for about 6n (within 0.01 only after about 9n bars for the index and 10n to 11n for the ADX), so a level read in that burn-in compares seed transients; the last section gives Wilder’s own seed. Values are NaN before their first valid index and flags read 0 there; prices are positive and finite, lengths are in bars, and series are paired by position, so the caller aligns highs, lows and closes by timestamp first.
DX filter for breakouts (White)
Adam White (“Filtering Breakouts”, Stocks & Commodities V11:4, April 1993, pp. 149-155) enters long “when the market made an x-bar high and a y-bar ADX made a z-bar high”, repeats the test “using Dx in place of ADx”, exits when the trend “makes a seven-bar low” and settles on the universal DX setting “3,17,7”. The directional index is the one ADX averages, 100 times the absolute difference of the two directional indicators over their sum; t6DXFilter composes it from DMIPlus and DMIMinus in ADX’s order, reads each “x-bar high” and “low” as today included, and books White’s long-only trades with Position.
pdi := c.DMIPlus(h, l, c, n)
mdi := c.DMIMinus(h, l, c, n)
dx := pdi.Sub(mdi).Abs().Mul(100).Div(pdi.Add(mdi))
up := h.GTE(h.Highest(x)).SetN(x-1, 0)
dxUp := dx.GTE(dx.Highest(z)).SetN(z, 0)
entry := up.Mul(dxUp)
exit := l.LTE(l.Lowest(e)).SetN(e-1, 0)
pos := c.Position(entry, exit)
dxV := dx.SetN(1, math.NaN())- Inputs: highs, lows and closes; x, n, z and e (3, 17, 7 and 7; his ADX universal is 5, 17 and 6).
- First valid index: 1 for
dxV(bar 0 is NaN; 0 where the two indicators sum to 0, NaN from a missing bar on);entryreads 0 before max(x - 1, z) andexitbefore e - 1. The masks matter: on the first barsh.GTE(h.Highest(x))reads the expanding high, and the first directional movement makes a z-bar high of the index. - Anchors:
dxVequals a loop of ADX’s own index expression bit for bit, and a loop of ADX’s expressions over it rebuildsADXbit for bit (dxV.Window(-1).RMA(n)to about 1e-13);h.GTE(h.Highest(x))equalsh.GTE(h.Highest(x-1).Shift(1))from bar x - 1; on a bar where entry and exit both fire, an outside bar,Positionenters a flat book and exits a long one. - Traps: on a bar without directional movement (an inside bar, or equal up and down moves) the three averages decay alike, so the index is unchanged in exact arithmetic and its new value differs from the last by a few units of rounding; when the previous bar was the z-bar high the entry then rests on that rounding, which differs between arm64, where Go fuses the recursions of the DMIs, and amd64 (the index’s bits differ on most bars; whether an entry moves depends on the path). The same holds inside the burn-in while one indicator is exactly 0: the index sits at 100 to within a unit of rounding, so a z-bar high there is decided by rounding too. TA-Lib’s order,
Div(...).Mul(100), differs in the last bits. Over the burn-in the package’s index is not Wilder’s: on his own wheat it is 100 on the first eight bars, where only -DM had occurred. After a missing high, low or close the index is NaN for good, soentryreads 0 from that bar on and the book does not re-enter, and a missing low also silencesexitfor e bars. - Variant: the strict reading,
h.Breakout(x, 1).GT(0)andl.Breakout(e, 1).LT(0), a window one bar longer (different on 6% to 14% of random-walk bars), and his ADX form,a.GTE(a.Highest(z)).SetN(n+z-1, 0)witha := c.ADX(h, l, c, n). - Missing piece: a named
DX(TA-Lib’s TA_DX, Tulip’s dx); the four-call composition is bit for bit ADX’s internal index. - Evidence: his Figures 4 and 11 (six futures markets, 1,000 bars each from late 1987 to 1991, long only, no costs): the universal DX filter raised the share of dollars made by winning trades from 51.8% for the 3-bar breakout alone to 59.9%, with no trade counts printed; on a random walk the filter removes about two-thirds of the entries with little selectivity. Evidence, not a reference.
- Pinned by
TestRecipeT6DXFilter.
ADXR and Wilder’s 25/20 gate (Wilder; Drinka and Kille)
Thomas Drinka and Steven Kille (“A modification of Wilder’s directional movement system”, Stocks & Commodities V3:6, 1985, pp. 218-221) restate Wilder’s “ADXR = (ADXt +ADXt-13)/2” and his rule that the system “will be profitable on commodities that have an ADXR value above 25” and is not to be used “when ADXR drops below 20”. t6ADXR lags n - 1, as they, TA-Lib, Tulip and TradeStation’s DirMovement do, and reads the two levels as a latch on the package’s ADX. The signals such a gate admits, the DI crosses that Summers gates with the ADX itself at 20 rather than the ADXR, Wilder’s Extreme Point stop and reverse and Pruitt’s ADX-above-35 reading of the system, are on t6-dmi-systems.md in this category.
a := c.ADX(h, l, c, n)
adxr := a.Add(a.Shift(n-1)).Div(2).SetN(n+n-1, math.NaN())
state := adxr.Hysteresis(on, off)- Inputs: highs, lows and closes, n (14),
on>=off(25 and 20). - First valid index: 2n - 1 for
adxr(27 at n = 14), NaN before: unmasked, bars n to 2n - 2 read half the ADX (theShiftpad) and bars 0 to n - 1 read 0, which latches -1 from bar 0.stateis 1 (trade) or -1 (stand aside) from the first ADXR outside [off, on], and 0 before; the gatestate.GT(0)treats 0 and -1 alike. Wilder’s own seed gives his day 41, bar 3n - 2 (the last section). - Anchors:
Div(2)equalsMul(0.5)bit for bit;stateequals a loop of the rule, and an ADXR equal toonoroffholds the state;c.ADXTrendMode(h, l, c, n, 25, 20, math.Inf(1), math.Inf(-1))equalsa.SetN(n, math.NaN()).Hysteresis(25, 20)from the bar both have decided on (Ruggiero’s latch waits for a cross, this one decides on a level). - Traps: on Wilder’s own 42 bars the package’s ADXR is 51.16 on bar 27 and 33.89 and 34.26 on bars 40 and 41, and opens the gate from bar 27, where Wilder’s is 22 and 23, undetermined; Drinka and Kille’s printed formulas omit the absolute value, the factor 100 and Wilder’s smoothing; Balsara’s book lags n (“the ADX value 14 days ago”); Wilder’s two quoted sentences say nothing about the band between the levels, so the latch is a reading.
- Variant: the level alone,
adxr.GT(25.0), and Balsara’s lag,a.Add(a.Shift(n)).Div(2).SetN(n+n, math.NaN()). - Missing piece: a named
ADXR(TA-Lib’s TA_ADXR, Tulip’s adxr); the composition is two calls and a mask. - Evidence: Drinka and Kille’s Tables 1 to 3 (six S&P 500 contracts of 1983 and 1984, 65 dollars a trade) test an ADX level, not the ADXR: at n = 14 the average net result per contract rose from a loss of 5,709 dollars with a gate at 5 to a gain of 3,602 at 30. Evidence, not a reference.
- Pinned by
TestRecipeT6ADXR; runnable exampleExampleSeries_ADX_adxr.
ADX environments and the rising-ADX gate (Kramer; Le Beau and Lucas)
Bruce Kramer (“Using Indicators In Trading Ranges And Trends”, Stocks & Commodities V12:4, April 1994, pp. 153-157) finds that “an increasing ADX signals a trending market” and names four environments: “A flat market is characterized by ADX values declining to less than 10 to 15, while a trading range market has ADX values of 16 to 25, a trend will have ADX values of 26 to 40, and a hypertrend will be characterized by sharply increasing ADX values to greater than 41.” Charles Le Beau and David Lucas (Day Trading Systems & Methods) trade only “in the direction of the trend” when “the daily ADX is rising” and “in either direction” when it is declining. t6ADXEnvironments reads Kramer’s bands as integer ranges (15, 25 and 40 in the lower band, as his flat market of Figure 7 has an ADX “less than or equal to 15”), a hypertrend as above 40 and rising on the bar, and the trend’s direction as the larger directional indicator.
a := c.ADX(h, l, c, n)
d := a.Delta()
rise := d.GT(0.0)
env := a.GT(15.0).Add(a.GT(25.0)).Add(a.GT(40.0).Mul(rise)).MaskNonFinite(a).SetN(n+1, math.NaN())
trending := rise.MaskNonFinite(a).SetN(n+1, math.NaN())
pdi := c.DMIPlus(h, l, c, n)
mdi := c.DMIMinus(h, l, c, n)
longOK := d.LTE(0.0).Add(d.GT(0.0).Mul(pdi.GT(mdi))).SetN(n+1, 0)
shortOK := d.LTE(0.0).Add(d.GT(0.0).Mul(mdi.GT(pdi))).SetN(n+1, 0)- Inputs: highs, lows and closes, n (Kramer gives none, 14 assumed; Le Beau and Lucas 18 on daily bars).
- First valid index: n + 1 for
envandtrending(the one-bar change reads ADX’s warm-up 0 at bar n), NaN before and wherever the ADX is NaN;longOKandshortOKread 0 on bars 0 to n and wherever the ADX is missing. - Anchors:
envequals a loop of the bands;env.EQ(0)isa.LTE(15)on valid bars;MaskNonFinite(a)equals the dossier’sAdd(a.Sub(a))bit for bit; exact ties fall where they are written (an ADX of exactly 15 is flat, 25 a range and 40, rising, a trend), and an ADX unchanged on a bar is not rising, so both sides are allowed. - Traps: the dossier’s Le Beau form,
rising.Mul(pdi.GT(mdi)).Add(rising.Mul(-1).Add(1)).GT(0), reads 1, allowed, on the bars after a missing bar, wherelongOKreads 0; withoutMaskNonFinitea missing ADX reads 0, flat. Kramer’s bands leave gaps (9% of the bars of an ADX(14) random walk fall in them), and his two paragraphs disagree when the ADX is above 25 and falling, soenvandtrendingare given apart. The levels depend on n and on the bar size: ADX(4) below 30 on weekly bars, ADX(6) below 40 daily, ADX(10) below 25 on 30-minute bars. - Variant: single levels, each masked with
SetN(n, 0): Pezzutti’sc.ADX(h, l, c, 14).GT(18.0); D’Errico and Zaffino’s codea.LT(30.0)on a weekly ADX(4), and their prose’s “below 30 for the last four weeks”,a.SetN(4, math.NaN()).LT(30.0).Sum(4).EQ(4.0).SetN(7, 0); Unger’sa.LT(25.0)on a 30-minute ADX(10) anda.LT(40.0)on a daily ADX(6). - Variant: Kramer’s literal hypertrend,
a.GT(41.0), and the levels alone,a.GT(15.0).Add(a.GT(25.0)).Add(a.GT(40.0)).MaskNonFinite(a).SetN(n, math.NaN()). - Missing piece: holding a daily value on intraday bars (Unger’s daily gate, Le Beau and Lucas’s daily ADX): with no timestamps, the caller computes the flag on daily bars and applies the last completed session’s value to the next session’s bars.
- Evidence: Pezzutti’s ADX(14) above 18 cut 250 S&P 500 trades to 185 and raised the profit factor from 3.53 to 4.14; Unger’s ADX(10) below 25 raised gold’s net from 201,100 to 255,020 dollars over 2010 to 2023 and lowered it from 177,800 to 139,250 over 2024 to 2026; Summers’s Lotus labels (“Completing the DMI rules”, Stocks & Commodities V7:1, January 1989, pp. 30-34) use inclusive bounds at 20, 25 and 30 and reproduce all 99 printed rows of his Figure 1, four ties included. All in sample. Evidence, not a reference.
- Pinned by
TestRecipeT6ADXEnvironments; runnable exampleExampleSeries_ADX_kramerEnvironments.
Wilder’s own seed: DX, ADX and ADXR as in his worksheet (Wilder; Summers)
Jim Summers (“Finishing DMI calculations, Part 9”, Stocks & Commodities V6:9, September 1988, pp. 350-353) prints Wilder’s March 1978 wheat worksheet “with Wilder’s answers as my program computed them”, noting that Wilder “starts counting at the first row of price data, not at the first row of TR1 data. Thus, the first calculation of TR14 contains only 13 day’s worth of data”, then “Today’s TR14 = Previous TR14 - (Previous TR14/14) + TR1”, DX as “(DI DIFF/DI SUM)” times 100, and “The first ADX = 14-day sum of DX/14”. TA-Lib seeds the same way. t6WilderDMI writes the seed with RMA on a true range whose bar 0 is set to 0, so every smoothed sum is Wilder’s divided by n.
up := h.Delta()
dn := l.Delta().Mul(-1)
pdm := up.Max(0).Mul(up.GT(dn))
mdm := dn.Max(0).Mul(dn.GT(up))
atr := c.TrueRange(h, l, c).SetN(1, 0).RMA(n)
pdiW := pdm.RMA(n).Mul(100).Div(atr).SetN(n, math.NaN())
mdiW := mdm.RMA(n).Mul(100).Div(atr).SetN(n, math.NaN())
dxW := pdiW.Sub(mdiW).Abs().Mul(100).Div(pdiW.Add(mdiW))
adxW := h.Mul(0).Add(dxW.Window(-n).RMA(n)).SetN(n+n-1, math.NaN())
adxrW := adxW.Add(adxW.Shift(n - 1)).Div(2)- Inputs: highs, lows and closes, n >= 2 (14).
- First valid index: n for
pdiW,mdiWanddxW(TA-Lib’s first DX), 2n - 1 foradxWand 3n - 2 foradxrW(his day 41 at n = 14), NaN before; a bar with a missing high, low and close makes every output NaN from that bar on. - Anchors: on Summers’s Figure 2 (1 June to 29 July 1977, 42 bars) the smoothed sums times n match all 84 printed TR14, +DM14 and -DM14 values at 2 decimals, and the outputs all 99 printed +DI, -DI, DX and ADX integers after rounding; the first ADX is 15.514692926698602 and the last 28.942424641228108, bit for bit on every architecture (
RMAis fenced); the ADXR of days 41 and 42 is 21.52 and 22.86 (derived, since Summers prints no ADXR), which Wilder’s 25/20 gate leaves undetermined. - Traps: the figure’s slashed zero prints the TR14 of 11 July as “57.85”: the value is 57.05 (53.63 - 53.63/14 + 7.25), from which the next row’s 57.48 follows; a sum of the two indicators of exactly 0 gives a DX of 0 here (
Div’s zero rule), where TA-Lib treats a sum within 1e-14 of 0 as zero and repeats the previous bar’s DX; the package’s zero-seeded ADX reads 68.39, 33.94 and 34.79 on bars 14, 27 and 41 of the same data, so on a short history the two seeds give opposite gates. - Variant: Hartle’s sidebar sheet (“The ADX”, Stocks & Commodities V11:4, pp. 154-155) truncates every sum to 3 decimals and the indicators and DX to integers, seeds with 14 values and averages 13 DX values for the first ADX: a departure that reproduces only with his own formulas.
- Evidence: the worksheet illustrates the arithmetic on one market and tests nothing, and Wilder’s 1978 book was not read; on random walks the composition agrees with ports of TA-Lib’s DX, ADX and ADXR to about 7e-14 (measured when the recipe was extracted, not pinned by a test). Evidence, not a reference.
- Pinned by
TestRecipeT6WilderDMI; runnable exampleExampleSeries_RMA_wilderADX.
The helpers below take the two directional indicators and the ADX as series, so one block serves the package’s DMIPlus, DMIMinus and ADX (zero-seeded, first ADX on bar n), Wilder’s own seed (t6WilderDMI, the last section of the directional index page beside this one, which Summers’s program computes) and a printed table. A gate read in the first 6n to 8n bars compares seed transients: the share of bars on which the two seeds put the ADX on different sides of 20, 25 or 35 falls from about a third on bar 2n - 1 to a few per cent from bar 4n and under 1% from bar 6n (500 random walks of 560 bars, n = 14; the shares depend on the walk), the burn-in of that page’s opening paragraph. Signals act at the next open through Shift(1) unless a source fixes the close (Pruitt’s “this bar on close”); Summers’s (H+L+C)/3 fills are a convention of his spreadsheet, not an executable price. His Figure 1 is the December 1986 S&P 500 contract (his title says September, but the rows run to 8 December 1986). Values are NaN before their first valid index and flags read 0 there; prices are positive and finite, lengths are in bars and ADX levels in index points.
DMI signals with the ADX gate (Wilder; Summers)
Jim Summers (“Completing the DMI rules”, Stocks & Commodities V7:1, January 1989, pp. 30-34) programs Wilder’s rules in Lotus: when the DI lines cross, “Is ADX greater than 20? If not, no trend exists”, “Is ADX below both DI lines? If so, ignore the signal”, “Is ADX above both DI lines? If so; ignore the signal”, and “If the ADX goes above both DI lines and then turns down, exit”. His code tests >=20 and passes an ADX equal to the larger DI. t6DMISignals gives the gated crosses up and dn and the downturn turn. Wilder’s ADXR levels, 25 to trade and 20 to stand aside, read as a latch, are on t6-directional-index.md (“ADXR and Wilder’s 25/20 gate (Wilder; Drinka and Kille)”): a gate on the ADXR where this one reads the ADX.
above := a.GT(pdi).Add(a.GT(mdi))
gate := a.GTE(adxMin).Mul(above.EQ(1))
up := pdi.CrossesAboveS(mdi).Mul(gate)
dn := pdi.CrossesBelowS(mdi).Mul(gate)
turn := a.LT(a.Shift(1)).Mul(a.Shift(1).GT(a.Shift(2))).Mul(above.EQ(2)).SetN(f+2, 0)- Inputs:
pdi,mdiandaof one length: the package’s DMIs, those oft6WilderDMIor printed columns;adxMin(20, or 25);f, the first valid index of the ADX: n for the package’s, n + n - 1 for Wilder’s seed, 0 for a printed table. - First valid index: f + 2 for
turn, 0 before: the mask keeps the first rise from reading the ADX’s warm-up (with the package’s ADX and f = 0, bar n + 1 reads the warm-up 0 as a rise, and fires on one of the test’s walks);upanddnread 0 until the gate first opens (bar n at the earliest with the package’s ADX, 2n - 1 with Wilder’s seed). - Anchors: on Summers’s Figure 1 (99 printed rows in eight blocks of consecutive days, f = 0) every cross of the printed DIs carries its printed label: three valid signals (04/30, 08/12 and 10/17), 14 “No trend”, three “ADX<+/-DI”, three “ADX>+/-DI” and three adverse crosses of an open trade; the only turn is 05/20’s.
above.EQ(1)is his Entry3 without Entry4, min(DI) < ADX <= max(DI), and the gate equals a loop of Entry2 to Entry4 bit for bit;a.GTE(adxMin)passes an ADX of exactly 20.0. - Traps:
fis the ADX’s first valid index, not the DMI length:SetN(n+2, 0)with n = 14 masks the 05/20 turn of the printed table, and Summers’s first trade does not close.CrossesAboveSandCrossesBelowSread the prior bar inclusively where his code is strict on both bars: Figure 1 prints 22.3 and 22.3 on 09/10, which the recipe reads as the prior bar of a cross on 09/11 and his code as no cross. His prose says “greater than 20” where the code tests>=20, and the dossier’sSetN(28, 0)is neither n nor 2n - 1. A NaN in the inputs gives a closed gate and 0 flags, not NaN, since every comparison with NaN reads 0. - Variant: the dossier’s strict interval,
a.Sub(pdi).Mul(a.Sub(mdi)).LT(0)in place ofabove.EQ(1), which differs only where the ADX equals the larger DI, a tie of printed one-decimal tables. - Variant: Rule 4 off,
gate := a.GTE(adxMin).Mul(above.GT(0)), which takes the three “ADX>+/-DI” crosses of Figure 1. - Evidence: “In 245 days of trading, the DI lines crossed 32 times. Of those 32 times, only three constituted a valid trade using these rules!” (rows deleted from the print, so the count is not checkable). Evidence, not a reference.
- Pinned by
TestRecipeT6DMISignals.
Extreme Point stop and reverse (Wilder; Summers)
Summers’s rules continue: “If the DI lines cross against you, evaluate an exit in terms of the Extreme Point Rule”, the low of the day of a long’s entry signal and the high of a short’s; his code tests it as a standing stop on every bar of a trade, strictly, and reverses (“Wilder calls this process a stop-and-reverse or SAR”) when a valid opposite signal comes on a bar that breaks it. t6DMIExtremePoint books the signals at the next open on PositionSignedRules with OppositeOnStopOnly: true: each trade’s stop is the signal bar’s low (a long) or high (a short), sampled on the entry bar and frozen for the trade, and touched by the decision bar’s low or high, l.Shift(1) and h.Shift(1); an opposite signal reverses the trade only on a bar whose stop fires, a stop without one goes flat, a turn closes the trade while its stop holds, and a stopped trade takes the next valid signal. Summers’s test is strict (“is the low lower than the day of the entry signal? If so, exit”) and the touch is inclusive, so the levels move one ulp outwards, lower and raise, which is exact for every finite level: a low x < ep is x <= math.Nextafter(ep, math.Inf(-1)).
lower := func(x float64) float64 { return math.Nextafter(x, math.Inf(-1)) }
raise := func(x float64) float64 { return math.Nextafter(x, math.Inf(1)) }
pos := h.PositionSignedRules(up.Shift(1), turn.Shift(1), dn.Shift(1), turn.Shift(1), PositionRules{
OppositeOnStopOnly: true,
LongStop: PositionLevel{Level: l.Shift(1).Map(lower), Touch: l.Shift(1)},
ShortStop: PositionLevel{Level: h.Shift(1).Map(raise), Touch: h.Shift(1)},
})
epL := l.Shift(1).EntryValue(pos)
epH := h.Shift(1).EntryValue(pos)- Inputs: highs and lows, and
up,dnandturnof the previous section, of one length. The receiverhgives the length only, since both stops have aTouch. - First valid index:
posreads 0 until the bar after the first valid signal;epLandepH, the Extreme Points read back frompos, are NaN while it is flat. - Anchors: on Figure 1 the three trades reproduce: the short of 05/01 (Extreme Point 245.40) leaves on 05/21 after the 05/20 turn; the long of 08/13 (242.10) is stopped on 09/12 after the low of 09/11, 234.10 (“STOP HIT!!!”, “Extreme Pt Rule”, on a bar without a cross); the short of 10/20 (239.35) is stopped on 10/24 after the high of 10/23, 240.60. The six fills
h.Add(l).Add(c).Div(3)are the printed four-decimal values truncated (the exit of 10/24 is 238.45000000000002, printed 238.4500). A loop of Wilder’s rules equalsposon every bar of Summers’s eight blocks, of the constructed tables9and of 84 runs on random walks (706 trades). - Traps: the dossier’s
l.ValueWhen(setup)latches at the latest setup, not at the trade’s entry, and moves on an ignored same-side signal; itsc.LT(ep)is a close test where Summers tests the low on every bar. Without the one-ulp move a low equal to the Extreme Point stops, which is the inclusive touch, not Summers’s strict test. The touch is the decision bar’s low,l.Shift(1), not the fill bar’s: the signals and the stop act at the next open, and the entry bar itself is not tested. Summers’s code departures not modelled: the stale “SAR?” flag (set by a valid signal of either side while a trade is open and cleared only by an exit, so that a later adverse cross failing the gate can still reverse), the adverse-cross tie (a low equal to the Extreme Point on an adverse-cross bar stops), and the downturn exit on signal and SAR bars with Rule 4 off. - Variant: the stop pattern of the trade-state pages on the stopless book
base := h.PositionSigned(up.Shift(1), turn.Shift(1), dn.Shift(1), turn.Shift(1)), the form this page printed beforePositionSignedRules(t6DMISystem, which the clean-room references pin): it cuts a base trade on the bar after its Extreme Point breaks and waits for the base’s next entry, so it equalsposon every bar before the first bar of a same-side re-entry after a stop or of a base reversal while the Extreme Point held, and can part from there (38 such events in the 706 trades of the 84 runs). - Evidence: Summers reports that “none of the trades made money” on the contract; at his fills the three trades lose 1.62, 16.52 and 3.28 points (derived). Evidence, not a reference.
- Pinned by
TestRecipeT6DMISystem; runnable exampleExampleSeries_PositionSignedRules_wilderDMI.
The Extreme Point as an entry trigger (Agostino and Dolan)
Kenneth Agostino and Brian Dolan (“Make The Trend Your Friend In Forex”, Stocks & Commodities V22:9, September 2004, pp. 14-20) use the Extreme Point as an entry trigger: “when the DI+/- lines cross, traders should note the extreme point for that period in the direction of the crossover (the high if DI+ crosses up over DI-; the low if DI- crosses up over DI+). Only if that extreme point is breached in the subsequent period is a trade signal confirmed”, and they exit “When the ADX begins to move lower from its highest level”. t6EPTrigger reads “in the subsequent period” as the next bar.
up0 := pdi.CrossesAboveS(mdi).SetN(2, 0)
dn0 := pdi.CrossesBelowS(mdi).SetN(2, 0)
le := up0.Shift(1).Mul(h.GT(h.Shift(1)))
se := dn0.Shift(1).Mul(l.LT(l.Shift(1)))
turnA := a.LT(a.Shift(1)).Mul(a.Shift(1).GT(a.Shift(2))).SetN(f+2, 0)
pos := h.PositionSigned(le, turnA, se, turnA)- Inputs: highs and lows,
pdi,mdianda, andfas in the signals section (they give no DMI length). - First valid index: 3 for
leandse(the crosses are masked withSetN(2, 0): on bar 1 the package’s DIs read their bar-0 zeros as a cross); f + 2 for the downturn;posreads 0 before the first entry. - Anchors:
le,seandposequal a loop of the one-bar rule bit for bit (the test’s walks, with Wilder’s seed and with the package’s DMIs), and no bar has both entries. The armed reading, waiting until the breach or until the DIs cross back, composes: witharm := up0.Sub(dn0).Hysteresis(0, 0)andtrigL := h.GT(h.ValueWhen(up0)).Mul(arm.GT(0)),firstL := trigL.Mul(trigL.SumSince(up0).EQ(1))equals its loop bit for bit; it differs fromlewhere the breach comes later than the next bar or the DIs cross back on it. - Traps: the breach is strict: a high equal to the cross bar’s high does not confirm. Their exit is the bare downturn, without Summers’s condition that the ADX lies above both DIs. The dossier’s “a multi-bar wait needs state” is the armed form above.
- Evidence: their two charts (daily EUR/USD, an up cross on 13 November 2003 breached “the following day (November 14)”; hourly Australian dollar), with no DMI length, fills or statistics. Evidence, not a reference.
- Pinned by
TestRecipeT6EPTrigger.
ADX above 35 with a three-close stop (Pruitt)
George Pruitt (The Ultimate Algorithmic Trading System Toolbox, Wiley, 2016, ch. 2, Box 2.1, pp. 30-33) states Wilder’s system as “If 14-day ADX is greater than 35 then If +DI > -DI, then Buy market on close”, with the mirror “SellShort market on close”, and, below 35, “If position is long and close is less than lowest close of prior three days, then Sell market on close”, the short the mirror; his p-code writes close < Lowest(close[1],3). t6DMIPruitt uses the package’s DMIs and books his rules at the close.
a := c.ADX(h, l, c, n)
pdi := c.DMIPlus(h, l, c, n)
mdi := c.DMIMinus(h, l, c, n)
on := a.GT(lvl)
off := a.LT(lvl)
le := on.Mul(pdi.GT(mdi))
se := on.Mul(mdi.GT(pdi))
lx := off.Mul(c.LT(c.Lowest(3).Shift(1)))
sx := off.Mul(c.GT(c.Highest(3).Shift(1)))
pos := c.PositionSigned(le, lx, se, sx)- Inputs: highs, lows and closes; n (14) and
lvlin index points (35; the test also runs 25). - First valid index: n for
leandse(the package’s ADX is 0 before, sooffis 1 there, but no position exists);posreads 0 until the first entry. - Anchors: the book equals a loop of the p-code with TradeStation’s order semantics (a Buy while long ignored, a SellShort while long reversing, exits only from a position, all at the close) bit for bit at 35 and 25; an ADX of exactly 35 neither enters nor exits, and a close equal to the lowest of the three closes before does not exit (strict, as the p-code).
- Traps: the burn-in: on Wilder’s own wheat (Summers, Stocks & Commodities V6:9, September 1988) the package’s zero-seeded ADX is above 35 on bars 14 to 25 and Wilder’s seed on none, so the gate opens on a seed transient; the mask
a.SetN(n+n-1, math.NaN())before the flags keeps the entries to bar 2n - 1 and later. His Figure 2.3 flowchart departs from the p-code (“Lowest Close 4 Days” for three, and an ADX of 35 on the exit branch), and his ADXR formula(ADX[1] + ADX[14]) / 14is a misprint that plays no part here. On the test’s walks no ADX lies within 0.02 of a level and no pair of DIs within 0.2 where the ADX is above it, so the rounding of the package’s recursions, which differs between arm64 and amd64, decides no flag there; near a level it can. - Variant: the flowchart’s ties,
off := a.LTE(lvl)withlx := off.Mul(c.LTE(c.Lowest(3).Shift(1)))and the mirror forsx. - Evidence: his Table 2.1 (35 futures markets; no platform, period or costs printed, most likely AmiBroker runs, as he “used it for most of the testing in Chapters 2 and 3”, p. 141): 68,261.80 dollars summed over 1,445 trades, 21 markets positive; the table adds up (each average P/L is the net over the trades to the cent). Evidence, not a reference.
- Pinned by
TestRecipeT6DMIPruitt; runnable exampleExampleSeries_ADX_pruitt35.
The recipe below reads daily opens, highs, lows and closes, the open o only for the fill price. Prices are positive and finite, lengths are in bars and series are paired by position, so the caller aligns them by timestamp first. Recipe values and levels are NaN before their first valid index (SetN(F, math.NaN())), and flags, states and latches read 0 there: the package’s ADX(n) reads 0 on bars 0 to n - 1 and XAverage(n) passes the close through before bar n - 1, so both are masked before they are compared, and every shifted series a pad can satisfy is masked, or the flag is (the prior high’s +0 pad on bar 0 meets a shifted touch of 0). A resting stop is touched when the high reaches it (GTE) unless the source says “above”, which is strict (on a tick grid the caller adds the tick), a protective stop below is touched when the low reaches it (LTE), and a stop fill price is max(o, level), written o.Maximum(level). The package’s ADX, DMIPlus and DMIMinus are zero-seeded, so for about the first 6n bars a level compares seed transients (the directional index page t6-directional-index.md), and Go fuses their recursions and the exponential average on arm64, which moves them by rounding only. The book’s exhibits are charts over data we do not have: evidence, not a reference. Counts on probe paths that the tests do not reproduce are illustrations, not pins.
Holy Grail (Raschke and Connors)
Linda Bradford Raschke and Laurence A. Connors (Street Smarts, M. Gordon Publishing, 1995, ch. 10, pp. 80-86) require that “A 14-period ADX must initially be greater than 30 and rising”, “Look for a retracement in price to the 20-period exponential moving average”, and “When the price touches the 20-period exponential moving average, put a buy stop above the high of the previous bar”; once filled, “enter a protective sell stop at the newly formed swing low” and “look to exit at the most recent swing high”. t6HolyGrailFrozen arms on the first bar of each spell of an ADX above 30 and rising, takes the long side where +DI is above -DI (implied by the book: its next chapter says “+DI and -DI indicate the direction of the trend”), and renews a one-bar buy stop above each touch bar’s high until one fills. It returns the fills, their price px, the target tgt (the highest high since the arm, to the bar before the fill), the stop stp (the lowest low since that high), both frozen at the fill that opens a trade and read back while it is open, and the trade, which PositionSignedRules closes on a low at or below the stop or a high at or above the target.
a := c.ADX(h, l, c, n).SetN(n, math.NaN())
e := c.XAverage(ne).SetN(ne-1, math.NaN())
up := c.DMIPlus(h, l, c, n).GT(c.DMIMinus(h, l, c, n))
strong := a.GT(30)
sr := strong.Mul(a.GT(a.Shift(1)))
arm := sr.Mul(sr.Shift(1).EQ(0))
touch := l.LTE(e).Mul(strong).Mul(up)
lvl := h.Shift(1)
brk := touch.Shift(1).Mul(h.GT(lvl))
gate := c.Position(arm, brk)
fill := gate.Exits()
px := o.Maximum(lvl)
peak := h.HighestSince(gate.Entries())
swLow := l.LowestSince(h.EQ(peak))
trade := c.PositionSignedRules(fill, nil, nil, nil, PositionRules{
LongTarget: PositionLevel{Level: peak.Shift(1), Touch: h},
LongStop: PositionLevel{Level: swLow.Shift(1), Touch: l},
})
tgt := peak.Shift(1).EntryValue(trade)
stp := swLow.Shift(1).EntryValue(trade)- Inputs: daily opens, highs, lows and closes; n 14 (the ADX and the DMIs) and ne 20 (the exponential average of the closes). The open enters only
px; the tick above the high is the caller’s. Shorts are the mirror (“SELLS ARE REVERSED”). - First valid index: n (14) for the ADX
a, NaN before;srreads 0 on bar n as well, since it compares with the masked bar n - 1, soarmcan fire from n + 1; ne - 1 (19) for the average and the touch; 20 forfillandtrade, 0 before;tgtandstpare NaN on every bar without an open trade. TheDMIPlusandDMIMinusof the direction are live from bar 1 (0 on bar 0 only).lvlis unmasked: its +0 pad on bar 0 reaches no fill, since the shifted touch is 0 there, andpxis meaningful on fill bars only. - Anchors: the gate
c.Position(arm, brk)equals bit for bit a state-machine loop (an arm while armed is ignored, and the first stop fill disarms),pxequals max(o[t], h[t-1]), andtgtandstpequal loops of “the highest high from the opening arm to the bar before the fill” and “the lowest low from the last bar at that high to the bar before the fill”, sampled at the fill that opens the trade, andtradea loop of the gate with the levels frozen there (bit for bit on the 320-bar group and on random paths). On the test’s 320-bar path the stop fills on bars 29, 81, 270 and 280, with (px,tgt,stp) = (101.40625, 102.3125, 100.875), (112.859375, 116.203125, 111.671875) and (111.984375, 128.3125, 106.59375) at the first three; the fill on bar 280, at 114.3125, comes inside the trade from bar 270, which ignores it and keeps its target and stop to the end of the path, where neither is touched.tradeis 1 on bars 29, 81 to 83 and 270 to 319, because bar 30’s low equals the stop (LTE) and bar 84’s high the target (GTE), while bar 80’s high equals the prior bar’s high and does not fill (the stop is strictly above). The five outputs agree bit for bit across architectures although the fused recursions do not: on the test dataais at least 0.357543 from 30, each change ofaat least 0.00444484 from 0, +DI at least 0.00435232 from -DI and the low at least 0.0111456 from the average. - Traps: the dossier’s composition leaves
e := c.XAverage(20)unmasked, sol.LTE(e)compares the low with the close on bars 0 to 18 and holds on all of them; it has no “rising” and no re-arm. “Initially greater than 30 and rising” is nearly vacuous for spells that start by crossing 30: an ADX that crosses 30 from at or below is rising on the crossing bar (by construction: the crossing bar is a rising bar), soarmmarks the first bar of each spell of “above 30 and rising”, and rule 6’s re-arm is the next such spell, an upturn while above 30 (the recrossa.CrossesAboveS(30)contradicts Exhibits 10.1 and 10.2, which take a second trade with the ADX above 30 throughout). Rule 2 expects “a turndown in the ADX” during the retracement, so the touch asks only that the ADX stay above 30. Anchoring the target on everyarmin place ofgate.Entries()restarts the high on an upturn during the pullback: on a probe path the target and the stop then matched their loops on only 11 and 12 of 15 fills (not pinned). The strictness ofa.GT(30)and of the touchl.LTE(e)cannot be pinned without bypassing the ADX and the average, whose values are not dyadic; the strict breakout, the target and the stop are pinned by the planted bars above, and the exit on a bar that reaches both stop and target by a constructed bar (bar 30’s high raised to the target), which the rule set closes, its stop winning over its target, at a price the book leaves to the caller. A NaN high or low inside a spell makes the running high and low, and so the target and the stop of later fills in that spell, NaN until the gate next opens, and a trade opened on a NaN level has no exit at that level, since the rule set reads a NaN level as no level. - Variant: the direction latched at the arm,
up.ValueWhen(arm)in place ofup(NaN before the first arm, which the latches read as no signal). - Variant: a touch that straddles the average,
l.LTE(e).Mul(h.GTE(e))in place ofl.LTE(e), so that a bar wholly below the average does not count. - Departures: the gate asks for an arm between consecutive fills, where rule 6, read for every trade, asks for one after the previous trade’s exit (the book says “After a successful trade”), so an arm inside an open trade opens the gate for the next fill. On the 320-bar path one fill is gated so, on bar 280, by the arm on bar 273 inside the trade from bar 270, and the trade ignores it, so the page’s trade and a loop of rule 6 agree on every bar there; on the test’s 300 random trending paths of 600 bars they part on 104 paths and 2,979 bars (pinned). Rules 5 and 6 as the book states them, “If stopped out, re-enter this trade by placing a new buy stop at the original entry price” and “After a successful trade, the ADX must once again turn up above 30 before another retracement to the moving average can be traded”, need a loop: rule 5 is a resting order across bars at the original price, and rule 6 conditions the re-arm on a trade whose exit reads levels frozen at fills the gate decides, so
PositionArmed(nil, exit, nil, arm, entry)would read the latch’s own output. The trailing stop and the partial exit of rule 4 are discretionary and are not written. - Evidence: the chapter’s Exhibits 10.1 to 10.4 (CBOT wheat and Citicorp in 1995, the Russell 2000 and a 60-minute orange juice contract) print price axes and an ADX pane with no bar values, no statistics and no costs; in Exhibits 10.1 and 10.4 the entry follows a second touch bar, as a one-bar order renewed on each touch bar takes it. Evidence, not a reference.
- Pinned by
TestRecipeT6HolyGrail; runnable exampleExampleSeries_ADX_holyGrail.
Aroon
AroonDown
func (s Series) AroonDown(n int) Series
AroonDown returns Chande’s Aroon-Down over n bars, which measures how recently the lowest low occurred. The window at bar i is the current bar plus the n prior bars, s[i-n] to s[i] (n+1 bars in all), and the value is (n - b) / n * 100, evaluated in that order, where b is how many bars back the lowest value in the window lies. The range is 0 to 100 inclusive: 100 when the current bar is the lowest and 0 when the lowest is exactly n bars back. On ties the most recent lowest bar counts. A NaN never displaces a number as the lowest, and a NaN current bar gives 100. Indices 0 to n-1 are 50 (warm-up), and every value is 50 when n >= len(s); every value is 0 when n < 1. This matches TA-Lib, TradingView and Chande’s stated definition; releases before this change, like Chande’s printed SuperCharts code and TradeStation, used an n-bar window whose minimum was 100/n. Note: The input series should always be the lows for this calculation.
The first valid index is n and the minimum length is n+1. A flat window gives 100, because on ties the most recent lowest bar counts (b = 0), so the warm-up (50) and the flat value differ. 50 reads as neither a strong down-trend (above 70) nor a weak one (below 30), and an AroonUp/AroonDown crossover rule compares 50 with 50 on warm-up bars, so trim (Window(-n)) or mask (SetN(n, math.NaN())) the first n bars. This warm-up was reviewed in GLE-254 (3 October 2026) and kept.
AroonUp
func (s Series) AroonUp(n int) Series
AroonUp returns Chande’s Aroon-Up over n bars, which measures how recently the highest high occurred. The window at bar i is the current bar plus the n prior bars, s[i-n] to s[i] (n+1 bars in all), and the value is (n - b) / n * 100, evaluated in that order, where b is how many bars back the highest value in the window lies. The range is 0 to 100 inclusive: 100 when the current bar is the highest and 0 when the highest is exactly n bars back. On ties the most recent highest bar counts. A NaN never displaces a number as the highest, and a NaN current bar gives 100. Indices 0 to n-1 are 50 (warm-up), and every value is 50 when n >= len(s); every value is 0 when n < 1. This matches TA-Lib, TradingView and Chande’s stated definition; releases before this change, like Chande’s printed SuperCharts code and TradeStation, used an n-bar window whose minimum was 100/n. Note: The input series should always be the highs for this calculation.
The first valid index is n and the minimum length is n+1. A flat window gives 100, because on ties the most recent highest bar counts (b = 0), so the warm-up (50) and the flat value differ. 50 reads as neither a strong up-trend (above 70) nor a weak one (below 30), and an AroonUp/AroonDown crossover rule compares 50 with 50 on warm-up bars, so trim (Window(-n)) or mask (SetN(n, math.NaN())) the first n bars. This warm-up was reviewed in GLE-254 (3 October 2026) and kept.
Vortex Indicator
VortexMinus
func (s Series) VortexMinus(h, l, c Series, n int) Series
VortexMinus implements the -VI component of the Vortex indicator, described by Etienne Botes and Douglas Siepman in TASC Jan 2010.
The warm-up reads 0, not NaN: bars 0 to n-1 are 0, below the indicator’s usual range of about 0.5 to 1.5.
VortexPlus
func (s Series) VortexPlus(h, l, c Series, n int) Series
VortexPlus implements the +VI component of the Vortex indicator, described by Etienne Botes and Douglas Siepman in TASC Jan 2010.
The warm-up reads 0, not NaN: bars 0 to n-1 are 0, below the indicator’s usual range of about 0.5 to 1.5.
Random Walk Index
RandomWalkIndexHigh
func (s Series) RandomWalkIndexHigh(h, l, c Series, minN, maxN int) Series
RandomWalkIndexHigh returns the Random Walk Index of the highs, how far each bar’s high stands above the low of a past bar in units of the move a random walk would make over that many bars, at its largest over the lags minN to maxN.
The receiver is not used. The bars are the highs h, lows l and closes c, one bundle of bars rather than a base and a comparison series, so they are not right-aligned: if the three lengths differ, the result is an empty Series. For lag k at bar t the numerator is h[t] - l[t-k] and the denominator is AR_k times sqrt(k), with AR_k the arithmetic mean of the true ranges of lags 1 to k, the symmetric true range of TrueRange, that is max(c[j-1], h[j]) - min(c[j-1], l[j]), over bars t-k to t-1. The sum S_k of those true ranges is formed in increasing lag order and incrementally across the lags, and the lag’s value is num / ((S_k / k) * sqrt(k)), with the product rounded on its own; the result is the largest of the values of lags minN to maxN. It keeps its sign, so a high below every one of the band’s lows gives a negative value rather than 0, and a zero result is +0. A lag whose computed average range S_k / k is 0 (flat bars with no gap, or a sum so small that the division underflows, so that a longer lag can be left out while a shorter one is kept) is left out, and a bar on which every lag is left out is 0.
The source is E. Michael Poulos, “Of Trends And Random Walks”, in Stocks & Commodities V9:2 (February 1991), with the symmetric true range of his September 1992 article (V10:9). That article is “Are There Persistent Cycles?”, in the same journal. He reads a value above 1 as a move beyond a random walk: on the long band (8, 64) as a trend, an uptrend from the highs and a downtrend from the lows, and on the short band (2, 7) as an overbought or oversold reading, a short-term high or low. The threshold is uncalibrated: on driftless random-walk bars a large share of the valid bars exceed 1 on either side, for both bands.
This is the lag pairing of Poulos’s formula text and of Investor/RT: lag k pairs the low k bars ago with sqrt(k) and the mean range of the k bars before today. Not provided are the MetaStock-style documentation and the TradeStation forum code, whose averages include today, Hartle’s spreadsheet, which pairs the extreme k-1 bars ago with sqrt(k) and uses a one-sided true range, Wilder smoothing of the range, and the anchored drunkard’s walk.
The warm-up reads 0, not NaN: bars 0 to maxN are 0 and the first valid index is maxN+1. The minimum length is maxN+2, and minN < 1, maxN < minN or maxN > len(h)-2 gives len(h) zeros, so maxN = len(h)-2 gives one valid bar, the last.
At a valid bar t the method reads h of bars t-maxN to t, l of bars t-maxN to t-1 and c of bars t-maxN-1 to t-2, whatever minN is, and the bar is NaN when any value read is NaN or an infinity, checked before the zero-range rule. On finite data whose sums overflow, a lag’s value can be 0 (an infinite average range) or NaN (an infinite numerator over one), so a bar can read 0 or NaN although no value it reads is bad. Today’s and yesterday’s closes are not read, so the value can be taken intrabar. Each value depends only on the bars it reads, so the method is window-local and start-invariant bit for bit: dropping bars from the front leaves every later valid bar unchanged. The cost is O(maxN) per valid bar.
The mirror RandomWalkIndexHigh(-l, -h, -c, minN, maxN), with -x the exact negation x.Mul(-1), equals RandomWalkIndexLow(h, l, c, minN, maxN) bit for bit on every input. Where no lag of the band is left out, the band’s value is the largest of the single-lag values of lags minN to maxN, and widening the band does not lower the value at a bar valid for both bands as long as the narrower band keeps a lag there. A single lag k equals h.Sub(l.Shift(k)).Div(c.ATR(h, l, c, k).Shift(1).Mul(math.Sqrt(float64(k)))) from bar k+1 wherever the lag’s average range is not 0, bit for bit on dyadic data, where every sum is exact, and to rounding otherwise. Scaling h, l and c by a power of two leaves the result unchanged bit for bit while nothing is subnormal or overflows. RandomWalkIndexLow is the mirror for the lows.
RandomWalkIndexLow
func (s Series) RandomWalkIndexLow(h, l, c Series, minN, maxN int) Series
RandomWalkIndexLow returns the Random Walk Index of the lows, how far each bar’s low stands below the high of a past bar in units of the move a random walk would make over that many bars, at its largest over the lags minN to maxN.
The receiver is not used. The bars are the highs h, lows l and closes c, one bundle of bars rather than a base and a comparison series, so they are not right-aligned: if the three lengths differ, the result is an empty Series. For lag k at bar t the numerator is h[t-k] - l[t] and the denominator is AR_k times sqrt(k), with AR_k the arithmetic mean of the true ranges of lags 1 to k, the symmetric true range of TrueRange, that is max(c[j-1], h[j]) - min(c[j-1], l[j]), over bars t-k to t-1. The sum S_k of those true ranges is formed in increasing lag order and incrementally across the lags, and the lag’s value is num / ((S_k / k) * sqrt(k)), with the product rounded on its own; the result is the largest of the values of lags minN to maxN. It keeps its sign, so a low above every one of the band’s highs gives a negative value rather than 0, and a zero result is +0. A lag whose computed average range S_k / k is 0 (flat bars with no gap, or a sum so small that the division underflows, so that a longer lag can be left out while a shorter one is kept) is left out, and a bar on which every lag is left out is 0.
The source is E. Michael Poulos, “Of Trends And Random Walks”, in Stocks & Commodities V9:2 (February 1991), with the symmetric true range of his September 1992 article (V10:9). That article is “Are There Persistent Cycles?”, in the same journal. He reads a value above 1 as a move beyond a random walk: on the long band (8, 64) as a trend, an uptrend from the highs and a downtrend from the lows, and on the short band (2, 7) as an overbought or oversold reading, a short-term high or low. The threshold is uncalibrated: on driftless random-walk bars a large share of the valid bars exceed 1 on either side, for both bands.
This is the lag pairing of Poulos’s formula text and of Investor/RT: lag k pairs the high k bars ago with sqrt(k) and the mean range of the k bars before today. Not provided are the MetaStock-style documentation and the TradeStation forum code, whose averages include today, Hartle’s spreadsheet, which pairs the extreme k-1 bars ago with sqrt(k) and uses a one-sided true range, Wilder smoothing of the range, and the anchored drunkard’s walk. On Hartle’s ten bars, with the blank close of the last bar set to NaN since it is not read, the band (2, 8) gives 1.4504754485877898 at bar 9, the value of lag 2, where his sheet prints 1.25, the largest of its lookbacks.
The warm-up reads 0, not NaN: bars 0 to maxN are 0 and the first valid index is maxN+1. The minimum length is maxN+2, and minN < 1, maxN < minN or maxN > len(h)-2 gives len(h) zeros, so maxN = len(h)-2 gives one valid bar, the last.
At a valid bar t the method reads h of bars t-maxN to t-1, l of bars t-maxN to t and c of bars t-maxN-1 to t-2, whatever minN is, and the bar is NaN when any value read is NaN or an infinity, checked before the zero-range rule. On finite data whose sums overflow, a lag’s value can be 0 (an infinite average range) or NaN (an infinite numerator over one), so a bar can read 0 or NaN although no value it reads is bad. Today’s and yesterday’s closes are not read, so the value can be taken intrabar. Each value depends only on the bars it reads, so the method is window-local and start-invariant bit for bit: dropping bars from the front leaves every later valid bar unchanged. The cost is O(maxN) per valid bar.
The mirror RandomWalkIndexLow(-l, -h, -c, minN, maxN), with -x the exact negation x.Mul(-1), equals RandomWalkIndexHigh(h, l, c, minN, maxN) bit for bit on every input, and RandomWalkIndexHigh(-l, -h, -c, minN, maxN) equals this result. Where no lag of the band is left out, the band’s value is the largest of the single-lag values of lags minN to maxN, and widening the band does not lower the value at a bar valid for both bands as long as the narrower band keeps a lag there. A single lag k equals h.Shift(k).Sub(l).Div(c.ATR(h, l, c, k).Shift(1).Mul(math.Sqrt(float64(k)))) from bar k+1 wherever the lag’s average range is not 0, bit for bit on dyadic data, where every sum is exact, and to rounding otherwise. Scaling h, l and c by a power of two leaves the result unchanged bit for bit while nothing is subnormal or overflows. RandomWalkIndexHigh is the mirror for the highs.
Choppiness Index
Chop
func (s Series) Chop(h, l, c Series, n int) Series
Chop computes the Choppiness Index from a single trailing sum of true ranges. At bar i it computes
100 * log10(sum(TR[j]) / (max(h[j]) - min(l[j]))) / log10(n)
over j = i-n+1 through i, where
TR[j] = max(h[j], c[j-1]) - min(l[j], c[j-1]), j >= 1.
The receiver is ignored. Invalid n, empty high, or mismatched OHLC lengths return an empty Series; otherwise the output has len(h) entries. For n >= 2, the first valid index is n, since index zero has no previous close. Earlier entries, insufficient history, and all entries for n == 1 are zero.
A zero range with otherwise finite active TR and extrema returns zero. Nonfinite values propagate naturally, with NaN taking precedence over that guard, and expire with their affected windows. Boundary gaps to the previous close can produce values above 100; results are not clipped. Formula reference: https://www.tradingview.com/support/solutions/43000501980-choppiness-index-chop/
The output is nominally on a 0 to 100 scale. The warm-up reads 0, not NaN: the first valid index is n and the minimum length is n + 1. 0 reads as a perfect trend, since a value below 38.2 is usually read as trending, so trim (Window(-n)) or mask (SetN(n, math.NaN())) the warm-up first.
Efficiency Ratio
EfficiencyRatio
func (s Series) EfficiencyRatio(n int) Series
EfficiencyRatio calculates the Kaufman Efficiency Ratio for a specified length n on a series: the absolute change over the last n bars divided by the sum of the absolute one-bar changes over those bars. Each computed ratio lies in [0, 1]; rounding can put the quotient fractionally above one, so it is clamped to exactly 1 (a NaN is left as NaN). A window with no movement at all (zero total absolute change) has an efficiency of zero. It returns a new Series of the same length without modifying s.
It is the absolute value of Chande’s momentum oscillator over 100: CMO(n) is the signed form times 100, s.Delta(n).Div(s.Delta(1).Abs().Sum(n)).Mul(100) from bar n. s.CMO(n).Abs() equals s.EfficiencyRatio(n).Mul(100) bit for bit on prices whose one-bar changes and window sums are exact, such as prices on a quarter grid, and within 2 ulps in general, and s.CMO(n).Abs().Div(100) is within 2 ulps of the ratio but not bit for bit. VIDYACMO is the exponential average whose constant this ratio drives.
The Vertical Horizontal Filter shares its denominator: it divides the range of the last n closes, c.Highest(n).Sub(c.Lowest(n)), by c.Delta(1).Abs().Sum(n), and the ratio equals c.Delta(n).Abs().Div(c.Delta(1).Abs().Sum(n)).Clamp(0, 1), bit for bit on ordinary prices and to rounding in general. VHF >= ER does not hold: the n-bar change reaches the close before the range’s window, so the closes 0, 10, 10, 10, 10, 10 give a VHF of 0 and an ER of 1 at n = 5, while the range of n+1 closes over the same sum is at least ER. The recipe is listed under “Compositions” in the “Efficiency Ratio” category.
For n >= 1, bars 0 to n-1 (the first min(n, len(s)) bars) are 0. The first valid index is n, and the minimum length for one valid bar is n + 1. n >= len(s) gives all zeros, as does n < 1.
The warm-up reads 0, not NaN. A warm-up 0 is indistinguishable from a computed “no net movement” (perfectly choppy) window, so an untrimmed rule such as ER < 0.3 (chop) fires on every warm-up bar, while a rule such as ER >= 0.3 (trend) does not. Callers should mask or trim the first n bars: er.Window(-n) drops them; er.SetN(n, math.NaN()) returns a copy with them marked NaN. Window(m) returns the last m values for positive m and all but the first -m values for negative m.
Releases before this change gave 0.5 on the warm-up bars. KAMA is unaffected because it reads the ratio only from bar n on. BreakoutModeIndex counts a ratio of at most 0.2 as one of its four conditions.
Compositions
The Vertical Horizontal Filter below divides the range of the last n closes by the sum of the last n absolute close changes: a trend-strength reading without a direction, (n - 1)/n on a straight line (the sum holds one change more than the range spans) and near 0 in congestion. It shares its denominator with EfficiencyRatio, which divides the n-bar change instead of the range. Values are NaN before their first valid index; prices are positive and finite and lengths are in bars. A flat window reads 0, as TA-Lib’s VHF (v0.8.1, September 2026) gives, where Tulip Indicators and pandas-ta give NaN.
Vertical Horizontal Filter (White; Chande; Gopalakrishnan)
Jayanthi Gopalakrishnan (“Vertical Horizontal Filter”, Stocks & Commodities V18:7, July 2000, pp. 42-48) reports Adam White’s 1991 indicator (Futures, August 1991) with Tushar Chande’s sidebar spreadsheet: “the numerator is the difference between the highest close and the lowest close for the last 28 days and the denominator is the 28-day sum of the absolute value of the one-day price change”. The sheet’s columns fix the windows, 28 closes for the range and 28 changes, reaching one close further back, for the sum. “A rising VHF line indicates the trend is strong”, while “it does not indicate trend direction”. t6VHF gives the close form and the high-low form over the same denominator.
den := c.Delta(1).Abs().Sum(n)
vhf := c.Highest(n).Sub(c.Lowest(n)).Div(den).SetN(n, math.NaN())
hl := h.Highest(n).Sub(l.Lowest(n)).Div(den).SetN(n, math.NaN())- Inputs: highs, lows and closes of one length;
n(28; Katsanos tests 36). - First valid index: n, NaN before. Bar n - 1 holds only n - 1 changes (
Delta’s bar-0 zero fills the n-th slot): on Chande’s sheet it would read 3.9375/14.5 = 0.2716, a value the sheet does not print. - Anchors: Chande’s sheet (33 rows in sixteenths, so every value is exact): its column F is the close changes, its column G the 28-day ranges (3.9375 and 3.875), and its five VHF cells in column H are 21/79, 63/212, 63/199, 62/199 and 62/201 bit for bit, printed 0.265823, 0.29717, 0.316583, 0.311558 and 0.308458. Achelis’s vector (Technical Analysis from A to Z, p. 354, through Tulip Indicators’ atoz.txt) gives 18/29, 21/40, 41/45 and 1 bit for bit, and Tulip’s untest vector agrees at its printed three decimals.
c.EfficiencyRatio(n)isc.Delta(n).Abs().Div(den).Clamp(0, 1)with the samedenbit for bit, and the range of n + 1 closes overden,c.Highest(n+1).Sub(c.Lowest(n+1)).Div(den), is at least ER on every bar. - Traps: VHF >= ER does not hold: ER’s change reaches the close before the range’s window, so closes 0, 10, 10, 10, 10, 10, 10.5 give VHF 0 against ER 1 at n = 5 (VHF < ER on 197 bars of the test path). The high-low form is at least the close form where l <= c <= h but is not bounded by 1: highs of 11, lows of 9 and closes of 10 rising to 10.1 on the last bar give 20.000000000000071 at n = 5, and the test path reads 19.75 on the bar after a flat stretch. The dossier masks with 0, which a congestion rule such as
vhf.LT(0.3)reads as congestion on every warm-up bar. The sidebar to Chande’s 1993 article (“Rating Trend Strength”, Stocks & Commodities V11:9, September 1993) prints price differences in its equation but fractional changes in its cell formula and prose, a range over a dimensionless sum; the 2000 sheet reproduces only with price changes, and its formula prints “+-” for the minus. pandas-ta 0.4.71b0 adds the float epsilon to every bar’s range when any window is flat. A NaN close gives NaN on the n + 1 bars whose windows hold it, and an infinite close NaN on n bars and 0 on the next (a finite range over an infinite sum). - Variant: White’s later 18-bar VHF smoothed by a 6-bar average (reported by IncredibleCharts and TA-Lib’s documentation),
vhf.Average(6), first valid at n + 5. - Variant: Markos Katsanos’s two entry conditions (“Which Trend Indicator Wins?”, Stocks & Commodities V34:11, October 2016) on the high-low form at n = 36,
v.CrossesAboveS(0.38).Mul(v.LT(0.45)).Mul(v.GT(v.Shift(10)))andv.GT(v.Lowest(10).Mul(1.5)).Mul(v.CrossesAboveS(0.24))withv := h.Highest(36).Sub(l.Lowest(36)).Div(c.Delta(1).Abs().Sum(36)).SetN(36, math.NaN()); his 50-day average filter, exits and next-open fills are the caller’s. - Evidence: Katsanos’s out-of-sample test (USO, April 2011 to March 2016, TradeStation’s high-low VHF, a cent a share): 21,257 dollars net on 18 trades against 44,592 for the moving average alone, “the worst performer”; on driftless random walks the 28-bar VHF is above 0.35 about a third of the time, so the published thresholds pass much noise. Evidence, not a reference.
- Pinned by
TestRecipeT6VHF; runnable exampleExampleSeries_Highest_verticalHorizontalFilter.
Congestion Index
CongestionIndex
func (s Series) CongestionIndex(h, l, c Series, n int) Series
CongestionIndex implements Markos Katsanos’s Congestion Index (as described in his book “Intermarket Trading Strategies”).
The output is roughly -100 to 100, a percentage rate of change over the range as a fraction of the low. The warm-up reads 0, not NaN: bars 0 to n-2 are 0, which reads as congestion.
If h, l and c differ in length the result is an empty Series. Otherwise it has their length, and every bar is 0 when n < 2 or n > len(h): a history shorter than the window gives zeros rather than an empty Series, so CongestionIndex over a prefix of the bars is that prefix of CongestionIndex over all of them. Releases before this change returned an empty Series when n < 2 or n > len(h). The receiver is not used; a nil receiver gives the same result.
Follow-Through Index
FTI
func (s Series) FTI(minPeriod, maxPeriod, halfLength, lookback int, beta, noiseCut float64) Series
FTI returns the Follow-Through Index at the optimal period for each bar. Khalsa’s FTI measures how strongly a market trends relative to its noise at each candidate cycle period. The optimal period is the local maximum (including endpoints) with the highest FTI value.
Defaults: minPeriod=5, maxPeriod=65, halfLength=40, lookback=100, beta=0.9, noiseCut=0.2.
minPeriod must be at least 3: the period-2 filter passes its input unchanged, so the channel width is 0 and the index would reflect only the floor. A smaller minPeriod is invalid and gives all zeros.
One bar costs about P*(lookback - halfLength)*(2*halfLength + 1) multiply-adds, where P = maxPeriod - minPeriod + 1 is the number of periods: about 300,000 at the defaults.
Memory is bounded by the input and a fixed budget whatever the period range: the periods’ filter coefficients are formed in batches of at most 2^20 values (one period at a time when its halfLength + 1 values are more), so a call allocates on the order of len(s) + lookback + halfLength + 2^20 floats. Releases before this change formed one table of P*(halfLength + 1) values, which for period ranges of the order of the input length could exceed memory (about 149 GiB for FTI(3, 199998, 99999, 100000, 0.9, 0.2) on 100,000 bars) or overflow an int and panic; every output is unchanged.
beta is the fractile, a fraction strictly between 0 and 1, and noiseCut is a fraction of the longest leg with 0 <= noiseCut < 1: a leg counts only if it is longer than noiseCut times the longest leg. The defaults are 0.9 and 0.2; beta 90, noiseCut 20 or a NaN for either gives all zeros (releases before this change accepted a NaN, and a NaN beta gave values that depended on the CPU). The warm-up reads 0, not NaN: bars 0 to lookback-2 are 0.
The channel width has a floor of 1e-5 times the absolute mean of the lookback window’s prices, so the index does not depend on the price unit: scaling the input by a power of 2 leaves every bar unchanged. Releases before this change added 1e-5 in price units, negligible at equity prices but material near 1 and below (8% low at a level of 0.01).
FTIAt
func (s Series) FTIAt(period, halfLength, lookback int, beta, noiseCut float64) Series
FTIAt returns the FTI at a specific fixed period.
period must be at least 3, as minPeriod in FTI; a smaller period is invalid and gives all zeros.
beta is a fraction strictly between 0 and 1 and noiseCut a fraction with 0 <= noiseCut < 1, as in FTI. The warm-up reads 0, not NaN: bars 0 to lookback-2 are 0.
The channel width’s floor is relative to the window’s mean price, as in FTI.
FTILog
func (s Series) FTILog(minPeriod, maxPeriod, halfLength, lookback int, beta, noiseCut float64) Series
FTILog is FTI on log prices, the form in which Aronson and Masters describe the source; its channel-width floor of 1e-5 log units is already scale-free. It is not FTI of the logged input, whose floor is relative to the window’s mean.
minPeriod must be at least 3, as in FTI; a smaller minPeriod is invalid and gives all zeros.
Cost and memory are as in FTI: memory is bounded by the input and a fixed budget whatever the period range.
beta is a fraction strictly between 0 and 1 and noiseCut a fraction with 0 <= noiseCut < 1, as in FTI. The warm-up reads 0, not NaN: bars 0 to lookback-2 are 0.
FTIPeriod
func (s Series) FTIPeriod(minPeriod, maxPeriod, halfLength, lookback int, beta, noiseCut float64) Series
FTIPeriod returns the optimal cycle period at each bar.
minPeriod must be at least 3, as in FTI; a smaller minPeriod is invalid and gives all zeros.
Cost and memory are as in FTI: memory is bounded by the input and a fixed budget whatever the period range.
beta is a fraction strictly between 0 and 1 and noiseCut a fraction with 0 <= noiseCut < 1, as in FTI. The warm-up reads 0, not NaN: bars 0 to lookback-2 are 0, which lies outside [minPeriod, maxPeriod], so it cannot be mistaken for a period.
The channel width’s floor is relative to the window’s mean price, as in FTI.
Regime Detection
BreakoutModeIndex
func (s Series) BreakoutModeIndex(h, l, c Series, n int) Series
BreakoutModeIndex is Ruggiero’s breakout-mode (“confusion”) index, after Linda Raschke’s “breakout mode”. It is described in the “Market Modes” section of chapter 4, “Trading Using Technical Analysis”, of Murray A. Ruggiero Jr., Cybernetic Trading Strategies (Wiley, 1997). On each bar from 29 on it scores four conditions, 0 or 1 each, and the index is the score averaged over n bars (n is Ruggiero’s averaging length N and has no default; he tested 4 to 30). A trade is taken only when the index is at least 2, so the gate is the caller’s idx.GTE(2). The receiver is not used.
The four conditions on a bar are:
- (a) momentum confusion: the 5-bar, 10-bar and 20-bar momentum of the close do not all have the same sign. A zero momentum has no sign, so it counts as confusion.
- (b) mid-range stochastics: the 3-bar averages of the 9-bar and the 14-bar Stochastic of the close (slow %K in its SMA form; TradeStation’s SlowK smooths the stochastic once, by a formula that Harris, TradeStation Made Easy! (Wiley, 2011), does not print, and the 3-bar length is this package’s choice) both lie in the inclusive band 40 to 60. A flat 14-bar window gives Stochastic’s 0, not 50, so it is not mid-range.
- (c) low-volatility latch: the volatility is ATRWilder(10). A latch reads low when the volatility equals its 20-bar lowest, otherwise high when it equals its 20-bar highest, otherwise it holds its last reading. The condition holds while the latch reads low. A flat volatility is both the highest and the lowest, and reads low. The latch starts with neither reading, so (c) is 0 until the first 20-bar low, even while the volatility sits at its high.
- (d) inefficiency: EfficiencyRatio(10) is at most 0.2, inclusive (this package’s reading of Ruggiero’s printed “+/-.20”). A window with no movement has ratio 0, so it counts.
The index on bar t is the integer sum of the counts on the n bars ending at t, converted to float64 and divided by float64(n) once, so index >= 2 holds exactly when the sum is at least 2n. The warm-up reads 0, not NaN: bars 0 to n+27 are 0 and the first valid index is n+28, so the minimum length is n+29. A computed 0 (no condition held on any bar of the window) reads the same as the warm-up. If h, l and c differ in length the result is the empty Series{}; if n < 1 or len(h) < n+29 it is len(h) zeros, even when an input is NaN or infinite. It takes O(len(h)) time for any n.
Neither Harris nor TradeStation’s help prints the formula of its Volatility function, so ATRWilder(10), Wilder’s form of the average true range, is this method’s choice for the volatility.
For finite inputs, from bar n+28 the index equals this composition of existing methods, the latch being Hysteresis’s event form:
m5, m10, m20 := c.Momentum(5), c.Momentum(10), c.Momentum(20)
same := m5.GT(0).Mul(m10.GT(0)).Mul(m20.GT(0)).Add(m5.LT(0).Mul(m10.LT(0)).Mul(m20.LT(0)))
a := same.Mul(-1).Add(1)
k9, k14 := c.Stochastic(h, l, 9).Average(3), c.Stochastic(h, l, 14).Average(3)
b := k9.GTE(40).Mul(k9.LTE(60)).Mul(k14.GTE(40)).Mul(k14.LTE(60))
v := c.ATRWilder(h, l, c, 10)
hi, lo := v.EQ(v.Highest(20)), v.EQ(v.Lowest(20))
cc := hi.Sub(hi.Mul(lo)).Sub(lo).SetN(29, 0).Hysteresis(0, 0).LT(0)
d := c.EfficiencyRatio(10).LTE(0.2)
idx := a.Add(b).Add(cc).Add(d).SetN(29, 0).Average(n)
Its bars before n+28 are not the warm-up zeros: mask them.
NaN: from the first bar p at which h, l or c is NaN or infinite, the result is NaN to the end, bar 0 included (unlike ADXTrendMode, which follows ADX from max(p, 1)). Bars before p are exactly as for finite inputs. No count is computed at or after p: ATRWilder does not recover where it goes NaN, a held latch would show a stale state, and a bad bar 0, which ATRWilder leaves out, is poisoned by this rule.
The index is prefix-invariant: the result on a prefix of the inputs is the prefix of the result. It is path-dependent and not start-invariant: ATRWilder is recursive and the latch holds its reading, so a different start can change a bar.
The evidence for the index is one market (Deutsche mark futures), in-sample, over an optimised range of N, with no random-subsampling control.
DFAAlpha
func (s Series) DFAAlpha(n int) Series
DFAAlpha computes the DFA (DFA1 / linear detrending) scaling exponent alpha over a rolling window of length n.
By default, this implementation applies DFA to first differences (returns) of the input series. This aligns the typical trading interpretation where alpha ≈ 0.5 corresponds to a random walk in returns, >0.5 trending persistence, and <0.5 mean reversion.
Implementation notes: - Scales considered are integers s in [4, n/4]. If fewer than two scales are available, the alpha for that bar is 0. - For each scale s, the profile is split into floor(n/s) non-overlapping segments from the start and again from the end (2*m segments total). Linear least squares detrending is applied within each segment and the total mean squared residual across all segments is used to compute F(s). - Alpha is estimated as the slope of the linear regression of log(F(s)) versus log(s) across the chosen scales.
Bars 0 to n-2 are exactly 0: the first valid index is n-1 and the minimum length is n. n must be at least 20, which gives the two scales (4 and 5) a fit needs; a smaller n is invalid and gives all zeros, as it did before. alpha = 0 reads as strongly mean-reverting, well below 0.5, so mask (SetN(n-1, math.NaN())) the bars before n-1.
DirectionalChange
func (s Series) DirectionalChange(theta float64, thetaDown ...float64) Series
DirectionalChange returns the confirmed directional-change state of a price series, +1 in an upward run, -1 in a downward run and 0 before the first confirmation, a run being confirmed when price moves by more than the fraction theta from its running extreme.
The machine is the directional change of Chen and Tsang (2021), Eq 2.1 and Appendix A, with this library’s strict tie rule. Here up is theta, and down is thetaDown[0] when thetaDown has at least one element, else theta: only thetaDown[0] is read, and further elements are ignored whatever their values. An upward run is confirmed when the price has risen from the running low LL by more than the fraction up, and a downward run when it has fallen from the running high LH by more than the fraction down. Before the first confirmation the machine tracks both extremes from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar with price p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p)/LH > down, and otherwise an upward run when (p - LL)/LL > up. The bar whose price completes the move is the confirmation bar; the extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. Each test is one subtraction and then one division, not a product limit such as LL*(1+up). DirectionalChangeExtension gives the extension (p - ref)/ref in the same shape, DirectionalChangeAge the age t - iRef and DirectionalChangeEvent the confirmations themselves.
The state is defined on every bar and is not NaN. It is +1 from a bar that confirms an upward run and -1 from a bar that confirms a downward run, held until the next confirmation, and 0 only before the first confirmation: once non-zero it is not 0 again. Before the first confirmation the down test comes first; with these ratio tests the two cannot both hold on one bar for valid thresholds, so the order is not observable and at most one confirmation fires on a bar.
The thresholds are valid if and only if 0 < up < +Inf and 0 < down < 1; NaN fails every comparison, so a symmetric theta must be below 1. An invalid thetaDown does not fall back to theta: DirectionalChange(0.1, 1) is invalid although DirectionalChange(0.1) is valid, and an empty thetaDown is the same as none. Every invalid pair gives len(s) zeros, and an empty or nil receiver gives an empty result.
Units. theta and thetaDown are fractions: 0.05 means a 5% move. This differs on purpose from the older price-move thresholds in nseries, which take percentages: UpDown, UpDownString and UpDownSum (tolerance), and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The DirectionalChange family follows the directional-change literature (Chen & Tsang, Eq 2.1), where the threshold and the extension are fractions. DirectionalChangeExtension is therefore a fraction; DeltaPercent gives the same move multiplied by 100. The two conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. To use a percentage p, pass p/100. Go folds constant expressions exactly, so the constants 0.7/100 and 350.01 equal 0.007 and 0.35. The caveat is for p a float64 variable, where p/100 is computed at run time: for a whole-number percentage it equals the decimal literal (p = 5 gives 0.05), but for a fractional one it can differ from the literal in the last bit (p = 0.7 gives p/100 == 0.006999999999999999, not 0.007), and p0.01 can differ even for whole numbers (p = 35 gives p0.01 == 0.35000000000000003). Write the fraction literal (0.007) when a rule is stated in decimals. Because the tests are strict and exact, a one-bit difference in theta can move a confirmation that lands exactly on the threshold. For the same reason, a percentage test such as (p-LL)/LL100 > 7 is not the same test as (p-LL)/LL > 0.07: on [13.00, 13.91] the percentage test fires and the fraction test does not. For thresholds in points, see DirectionalChangePoints.
A threshold is crossed only when it is exceeded: a move exactly equal to its threshold does not confirm, so on [100, 110] at 0.1 nothing is confirmed, because (110 - 100)/100 is exactly the float64 0.1. To reproduce a source that counts equality as a crossing, pass math.Nextafter(theta, 0) and, for an asymmetric pair, math.Nextafter(thetaDown[0], 0): both tests use >, so both thresholds move towards 0, and a ratio exceeds math.Nextafter(x, 0) exactly when it is at least x, for every float64 ratio, because no float64 lies between a value and its neighbour. The one edge is a threshold of 5e-324, where math.Nextafter gives 0, which is invalid: pass 5e-324 unchanged, since no relative move between two distinct positive float64 prices is at most 5e-324, so both rules agree there.
The first valid index is 0 and the minimum length is 1. There is no warm-up: bar 0 of a valid series gives state 0. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChange(theta, thetaDown…).EQ(0) detects exactly the undetermined bars, where the age and the event are 0. Callers that feed the state to a latch, a rank or a state builder mask the 0 first.
A bar is missing when its price is not finite or is at most 0: a missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. On a missing bar the state is the previous bar’s state, which is 0 at bar 0 and before the first confirmation. Missing bars are invisible except to the clock: deleting every missing bar from s gives the same state, event and extension on the remaining bars, and after a missing bar the machine carries on exactly as if the bar were absent. Non-positive finite prices are missing by design, because relative thresholds need positive prices, and so are +Inf and -Inf; the bar-count swing views treat infinities and non-positive values as ordered values instead.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars. On a 20,000-bar quarter-step random walk, runs started at 104 later bars agreed bit for bit with the full run, on the state, the extension and the age together, after a median of 14.5, 65.5 and 450.5 bars (maximum 58, 221 and 1062) at theta 0.01, 0.03 and 0.1, each within one confirmation of the full run.
The method is causal and deterministic: each output at bar t reads only s[0..t], every prefix of s gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose price completes the move, and the extreme it confirms lies in the past, so the machine lags each turn by at least a threshold’s move: a rise of more than up from the trough, or a fall of more than down from the peak. A zigzag, Pivots, PivotIndices, Swings, SwingsHL and SwingPoints place turning points with hindsight and are not causal. Brown (2004) shows what ignoring the delay costs, 718.5% against 36.4% a year.
The settings of the sources are documentation, not causal parameters. Chen and Tsang (2021) use thresholds from 0.001 to 0.01 on intraday FX and 0.003 (and 0.004) for daily index tracking, with the inclusive Eq 2.1, which is DirectionalChange(math.Nextafter(theta, 0)). Vakkur (1997) uses 0.10 on monthly S&P 500 closes, which the caller supplies; his spreadsheet is strict, close to DirectionalChange(0.10), from which it differs only by the rounding of its product tests and its assumed upward start, and his prose is inclusive (“100 to 110” fires), which is DirectionalChange(math.Nextafter(0.10, 0)). Merrill (1988) uses 0.05 for market averages and 0.10 for stocks, measured from the low point, “5% or more”: with f a float64 variable, so that f/(1+f) is computed at run time, this is DirectionalChange(math.Nextafter(f, 0), math.Nextafter(f/(1+f), 0)), and the plain DirectionalChange(f, f/(1+f)) is the strict rule “more than f”. The variable matters: Go folds the constant 1 - 1/1.05 exactly, to the constant 0.05/1.05, while at run time 1 - 1/1.05 is 0.04761904761904767 and, even after math.Nextafter, does not confirm the decline on [100, 105, 100]. The two calls equal Merrill’s literal rule, (H - p)/p against f, only away from rounding ties, because each rule rounds its own quotient: on [64.47, 61.4] at f = 0.05 his quotient rounds to 0.05, so his strict test misses a decline that DirectionalChange(f, f/(1+f)) confirms, and that the exact decline of those float64 prices, just above 5%, warrants; on [14.07, 13.4] the inclusive call confirms a decline just below 5%. They agree on every bar of the AAPL closes of 2023 and of a quarter-step and a cent random walk at f = 0.01, 0.05 and 0.10. Dai, Zhang and Zhu use a rally of “24% or more” and a decline of “more than 19%”, which is DirectionalChange(math.Nextafter(0.24, 0), 0.19), and their segment tables are full-sample. Raftopoulos (2003) uses 0.10 to 0.18, inclusive, which is DirectionalChange(math.Nextafter(f, 0)) with f the chosen setting.
DirectionalChange is not a Markov model but a state source for one, and no trading edge is demonstrated: Vakkur’s r^2 of 0.084 is in-sample, and Chen and Tsang score their tracking against labels drawn with hindsight.
AutoTuneDC is unrelated: DC there is the dominant cycle. Hysteresis is a latch on a value’s level against two fixed thresholds, not on a relative move from a running extreme. The swing views SwingHighValue, SwingLowValue, MarketStructure, ClearMethodDirection, GannSwingDirection, SwingTrend, WilliamsSwingDirection and BasingPointDirection build bar-count, Clear and basing-point swings; none of them confirms a swing by a threshold move. PointAndFigureDirection on log closes with a box of math.Log1p(p) is close to DirectionalChange((1+p)^reversal - 1) but not equal to it: it counts whole boxes from a grid level, not a relative move from the exact extreme.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the length and whether the thresholds are valid, and none for an empty receiver.
Recipes built on the DirectionalChange methods are listed under “Compositions” in the “Regime Detection” category. Meyers’s percentage reversal on a regression forecast curve is under “Linear Regression”.
Verification: all four outputs agree bit for bit with two independent references, one transcribed from the steps above and one from Chen and Tsang’s Eq 2.1 and Appendix A, and with the published examples of Chen and Tsang (their Fig 2.1 and Fig 5.2); on Vakkur’s Fig 1 the extension times 100 is within 0.1 of his printed figures on rows 11 to 13. On finite positive data, at a confirmation bar t with age k, and with p here the previous event bar (the first valid bar for the first event), the confirmed extreme s[t-k] is s.Highest(t-p+1)[t] for a -1 event and s.Lowest(t-p+1)[t] for a +1 event, and k is (t-p) - s.IndexOfHighestS(t-p+1)[t] for a -1 event and (t-p) - s.IndexOfLowestS(t-p+1)[t] for a +1 event. When up is below every one-bar rise (s[t] - s[t-1])/s[t-1] and down below every one-bar fall (s[t-1] - s[t])/s[t-1], the state is the forward-filled sign of s.Momentum(1), and 0 before the first non-zero move.
DirectionalChangeAge
func (s Series) DirectionalChangeAge(theta float64, thetaDown ...float64) Series
DirectionalChangeAge returns the number of bars from the last confirmed extreme of the directional-change state machine to the current bar.
The machine is that of DirectionalChange: the directional change of Chen and Tsang (2021), Eq 2.1 and Appendix A, with a strict tie rule. Here up is theta, and down is thetaDown[0] when thetaDown has at least one element, else theta: only thetaDown[0] is read, and further elements are ignored. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar with price p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p)/LH > down, and otherwise an upward run when (p - LL)/LL > up. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. DirectionalChange gives the state, DirectionalChangeExtension the extension (p - ref)/ref, DirectionalChangeAge the age t - iRef and DirectionalChangeEvent the confirmations.
The age at bar t is t - iRef: Chen and Tsang’s T(i), counted in bars rather than in physical time. It is a whole number, 0 before the first confirmation and at least 1 (and at most t) wherever the state is not 0. On a confirmation bar it is the distance back to the extreme just confirmed, which by the tie rule is the latest bar holding that extreme value since the previous confirmation. It is a bar-index difference, so it counts missing bars, and it is not NaN. StateAge of the state counts from the confirmation bar, not from the extreme.
The thresholds are valid if and only if 0 < up < +Inf and 0 < down < 1; NaN fails every comparison. An invalid thetaDown does not fall back to theta: DirectionalChangeAge(0.1, 1) is invalid although DirectionalChangeAge(0.1) is valid. Every invalid pair gives len(s) zeros, and an empty or nil receiver gives an empty result.
Units. theta and thetaDown are fractions: 0.05 means a 5% move. This differs on purpose from the older price-move thresholds in nseries, which take percentages: UpDown, UpDownString and UpDownSum (tolerance), and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The DirectionalChange family follows the directional-change literature (Chen & Tsang, Eq 2.1), where the threshold and the extension are fractions. DirectionalChangeExtension is therefore a fraction; DeltaPercent gives the same move multiplied by 100. The two conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. To use a percentage p, pass p/100. Go folds constant expressions exactly, so the constants 0.7/100 and 350.01 equal 0.007 and 0.35. The caveat is for p a float64 variable, where p/100 is computed at run time: for a whole-number percentage it equals the decimal literal (p = 5 gives 0.05), but for a fractional one it can differ from the literal in the last bit (p = 0.7 gives p/100 == 0.006999999999999999, not 0.007), and p0.01 can differ even for whole numbers (p = 35 gives p0.01 == 0.35000000000000003). Write the fraction literal (0.007) when a rule is stated in decimals. Because the tests are strict and exact, a one-bit difference in theta can move a confirmation that lands exactly on the threshold. For the same reason, a percentage test such as (p-LL)/LL100 > 7 is not the same test as (p-LL)/LL > 0.07: on [13.00, 13.91] the percentage test fires and the fraction test does not. For thresholds in points, see DirectionalChangePoints.
A threshold is crossed only when it is exceeded: a move exactly equal to its threshold does not confirm, so on [100, 110] at 0.1 nothing is confirmed, because (110 - 100)/100 is exactly the float64 0.1. To reproduce a source that counts equality as a crossing, pass math.Nextafter(theta, 0) and, for an asymmetric pair, math.Nextafter(thetaDown[0], 0): both tests use >, so both thresholds move towards 0, and a ratio exceeds math.Nextafter(x, 0) exactly when it is at least x, for every float64 ratio, because no float64 lies between a value and its neighbour. The one edge is a threshold of 5e-324, where math.Nextafter gives 0, which is invalid: pass 5e-324 unchanged, since no relative move between two distinct positive float64 prices is at most 5e-324, so both rules agree there.
The first valid index is 0 and the minimum length is 1. There is no warm-up: bar 0 of a valid series gives age 0. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChange(theta, thetaDown…).EQ(0) detects exactly the undetermined bars, which are the bars where the age is 0.
A bar is missing when its price is not finite or is at most 0: a missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. On a missing bar the age is the previous bar’s age plus 1 when the state is not 0, and 0 otherwise. Deleting every missing bar from s changes the age only by the missing bars it counts: with them present, the age is the bar-index distance to the same extreme bar.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars. On a 20,000-bar quarter-step random walk, runs started at 104 later bars agreed bit for bit with the full run, on the state, the extension and the age together, after a median of 14.5, 65.5 and 450.5 bars (maximum 58, 221 and 1062) at theta 0.01, 0.03 and 0.1, each within one confirmation of the full run.
The method is causal and deterministic: each output at bar t reads only s[0..t], every prefix of s gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose price completes the move, and its extreme lies in the past, so the output lags each turn by the confirmation delay.
The settings of the sources are documentation, not causal parameters, and DirectionalChange gives their calls: Chen and Tsang (2021) use thresholds from 0.001 to 0.01 on intraday FX and 0.003 (and 0.004) for daily index tracking, inclusive; Vakkur (1997) uses 0.10 on monthly S&P 500 closes, which the caller supplies, his spreadsheet strict and his prose inclusive; Merrill (1988) uses 0.05 for market averages and 0.10 for stocks, from the low point, “5% or more”; Dai, Zhang and Zhu use 0.24 up (inclusive) and 0.19 down (strict), and their segment tables are full-sample; Raftopoulos (2003) uses 0.10 to 0.18, inclusive. The machine is not a Markov model but a state source for one, and no trading edge is demonstrated: Vakkur’s r^2 of 0.084 is in-sample, and Chen and Tsang score their tracking against labels drawn with hindsight.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the length and whether the thresholds are valid, and none for an empty receiver.
Recipes built on the DirectionalChange methods are listed under “Compositions” in the “Regime Detection” category.
Verification: on finite positive data, at a confirmation bar t with age k, and with p here the previous event bar (the first valid bar for the first event), k equals (t-p) - s.IndexOfHighestS(t-p+1)[t] for a -1 event and (t-p) - s.IndexOfLowestS(t-p+1)[t] for a +1 event; this needs the rule that a tie moves the extreme to the latest bar, and a first-occurrence rule fails it. All four outputs also agree bit for bit with two independent references and with the published examples of Chen and Tsang (their Fig 2.1 and Fig 5.2).
DirectionalChangeEvent
func (s Series) DirectionalChangeEvent(theta float64, thetaDown ...float64) Series
DirectionalChangeEvent returns +1 on each bar that confirms an upward directional change, -1 on each bar that confirms a downward one, and 0 on every other bar.
The machine is that of DirectionalChange: the directional change of Chen and Tsang (2021), Eq 2.1 and Appendix A, with a strict tie rule. Here up is theta, and down is thetaDown[0] when thetaDown has at least one element, else theta: only thetaDown[0] is read, and further elements are ignored. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar with price p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p)/LH > down, and otherwise an upward run when (p - LL)/LL > up. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. DirectionalChange gives the state, DirectionalChangeExtension the extension (p - ref)/ref, DirectionalChangeAge the age t - iRef and DirectionalChangeEvent the confirmations.
The event is the confirmation itself: at most one fires on a bar, the event is non-zero exactly on confirmation bars and equals the new state there, and the first confirmation out of the undetermined state is an event. It is 0 on a missing bar and is not NaN. It equals DirectionalChange(theta, thetaDown…).Delta().Clamp(-1, 1) bit for bit on every bar, missing bars and invalid thresholds included, but is computed without forming the state. At an event bar t with age k, as DirectionalChangeAge gives it, the confirmed extreme is bar t-k: a swing high for a -1 event and a swing low for a +1 event.
The thresholds are valid if and only if 0 < up < +Inf and 0 < down < 1; NaN fails every comparison. An invalid thetaDown does not fall back to theta: DirectionalChangeEvent(0.1, 1) is invalid although DirectionalChangeEvent(0.1) is valid. Every invalid pair gives len(s) zeros, and an empty or nil receiver gives an empty result.
Units. theta and thetaDown are fractions: 0.05 means a 5% move. This differs on purpose from the older price-move thresholds in nseries, which take percentages: UpDown, UpDownString and UpDownSum (tolerance), and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The DirectionalChange family follows the directional-change literature (Chen & Tsang, Eq 2.1), where the threshold and the extension are fractions. DirectionalChangeExtension is therefore a fraction; DeltaPercent gives the same move multiplied by 100. The two conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. To use a percentage p, pass p/100. Go folds constant expressions exactly, so the constants 0.7/100 and 350.01 equal 0.007 and 0.35. The caveat is for p a float64 variable, where p/100 is computed at run time: for a whole-number percentage it equals the decimal literal (p = 5 gives 0.05), but for a fractional one it can differ from the literal in the last bit (p = 0.7 gives p/100 == 0.006999999999999999, not 0.007), and p0.01 can differ even for whole numbers (p = 35 gives p0.01 == 0.35000000000000003). Write the fraction literal (0.007) when a rule is stated in decimals. Because the tests are strict and exact, a one-bit difference in theta can move a confirmation that lands exactly on the threshold. For the same reason, a percentage test such as (p-LL)/LL100 > 7 is not the same test as (p-LL)/LL > 0.07: on [13.00, 13.91] the percentage test fires and the fraction test does not. For thresholds in points, see DirectionalChangePoints.
A threshold is crossed only when it is exceeded: a move exactly equal to its threshold does not confirm, so on [100, 110] at 0.1 nothing is confirmed, because (110 - 100)/100 is exactly the float64 0.1. To reproduce a source that counts equality as a crossing, pass math.Nextafter(theta, 0) and, for an asymmetric pair, math.Nextafter(thetaDown[0], 0): both tests use >, so both thresholds move towards 0, and a ratio exceeds math.Nextafter(x, 0) exactly when it is at least x, for every float64 ratio, because no float64 lies between a value and its neighbour. The one edge is a threshold of 5e-324, where math.Nextafter gives 0, which is invalid: pass 5e-324 unchanged, since no relative move between two distinct positive float64 prices is at most 5e-324, so both rules agree there.
The first valid index is 0 and the minimum length is 1. There is no warm-up: bar 0 of a valid series gives event 0. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChange(theta, thetaDown…).EQ(0) detects exactly the undetermined bars, where the event is 0.
A bar is missing when its price is not finite or is at most 0: a missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. The event is 0 on a missing bar. Deleting every missing bar from s gives the same events on the remaining bars, and after a missing bar the machine carries on exactly as if the bar were absent.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars. On a 20,000-bar quarter-step random walk, runs started at 104 later bars agreed bit for bit with the full run, on the state, the extension and the age together, after a median of 14.5, 65.5 and 450.5 bars (maximum 58, 221 and 1062) at theta 0.01, 0.03 and 0.1, each within one confirmation of the full run.
The method is causal and deterministic: each output at bar t reads only s[0..t], every prefix of s gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose price completes the move, and its extreme lies in the past, so the output lags each turn by the confirmation delay.
The settings of the sources are documentation, not causal parameters, and DirectionalChange gives their calls: Chen and Tsang (2021) use thresholds from 0.001 to 0.01 on intraday FX and 0.003 (and 0.004) for daily index tracking, inclusive; Vakkur (1997) uses 0.10 on monthly S&P 500 closes, which the caller supplies, his spreadsheet strict and his prose inclusive; Merrill (1988) uses 0.05 for market averages and 0.10 for stocks, from the low point, “5% or more”; Dai, Zhang and Zhu use 0.24 up (inclusive) and 0.19 down (strict), and their segment tables are full-sample; Raftopoulos (2003) uses 0.10 to 0.18, inclusive. The machine is not a Markov model but a state source for one, and no trading edge is demonstrated: Vakkur’s r^2 of 0.084 is in-sample, and Chen and Tsang score their tracking against labels drawn with hindsight.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the length and whether the thresholds are valid, and none for an empty receiver.
Recipes built on the DirectionalChange methods are listed under “Compositions” in the “Regime Detection” category.
Verification: DirectionalChangeEvent(theta, thetaDown…) equals DirectionalChange(theta, thetaDown…).Delta().Clamp(-1, 1) bit for bit on every bar, missing bars and invalid thresholds included, because the state changes only at confirmations, by 1 or by 2, and a held state gives Delta 0. All four outputs also agree bit for bit with two independent references and with the published examples of Chen and Tsang (their Fig 2.1 and Fig 5.2).
DirectionalChangeExtension
func (s Series) DirectionalChangeExtension(theta float64, thetaDown ...float64) Series
DirectionalChangeExtension returns the signed fractional move (p - ref)/ref of each price from the last confirmed extreme of the directional-change state machine.
The machine is that of DirectionalChange: the directional change of Chen and Tsang (2021), Eq 2.1 and Appendix A, with a strict tie rule. Here up is theta, and down is thetaDown[0] when thetaDown has at least one element, else theta: only thetaDown[0] is read, and further elements are ignored. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar with price p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p)/LH > down, and otherwise an upward run when (p - LL)/LL > up. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. DirectionalChange gives the state, DirectionalChangeExtension the extension (p - ref)/ref, DirectionalChangeAge the age t - iRef and DirectionalChangeEvent the confirmations.
The extension at a valid bar with price p is (p - ref)/ref, a signed fraction: positive above ref and negative below it. It is the test’s own shape, one subtraction and then one division, so on a confirmation bar it equals the test’s own value, above up on a +1 confirmation and below -down on a -1 confirmation, exactly. It is Vakkur’s extendedness as a fraction (he forms p/ref - 1, which can differ in the last bit), and |ext|/theta is Chen and Tsang’s TMV, Eq 4.1 written literally as (|Ps - Pe|/Ps)/theta with Ps the last confirmed extreme and Pe the current price, bit for bit. It is at least -1, it is +Inf when the ratio overflows (on [1e-300, 1e10] at 0.1 it is +Inf at bar 1), and it is not -Inf. It is 0 on valid bars before the first confirmation, and NaN on a missing bar and only there, so a non-positive price gives NaN by design.
Because the extension is a fraction, the exact bridge to percent outputs is DeltaPercent: on finite positive data, at every bar t where the state is not 0, the extension times 100 is s.DeltaPercent(k)[t] bit for bit, with k the age at t.
The thresholds are valid if and only if 0 < up < +Inf and 0 < down < 1; NaN fails every comparison. An invalid thetaDown does not fall back to theta: DirectionalChangeExtension(0.1, 1) is invalid although DirectionalChangeExtension(0.1) is valid. Every invalid pair gives len(s) zeros, and an empty or nil receiver gives an empty result.
Units. theta and thetaDown are fractions: 0.05 means a 5% move. This differs on purpose from the older price-move thresholds in nseries, which take percentages: UpDown, UpDownString and UpDownSum (tolerance), and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The DirectionalChange family follows the directional-change literature (Chen & Tsang, Eq 2.1), where the threshold and the extension are fractions. DirectionalChangeExtension is therefore a fraction; DeltaPercent gives the same move multiplied by 100. The two conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. To use a percentage p, pass p/100. Go folds constant expressions exactly, so the constants 0.7/100 and 350.01 equal 0.007 and 0.35. The caveat is for p a float64 variable, where p/100 is computed at run time: for a whole-number percentage it equals the decimal literal (p = 5 gives 0.05), but for a fractional one it can differ from the literal in the last bit (p = 0.7 gives p/100 == 0.006999999999999999, not 0.007), and p0.01 can differ even for whole numbers (p = 35 gives p0.01 == 0.35000000000000003). Write the fraction literal (0.007) when a rule is stated in decimals. Because the tests are strict and exact, a one-bit difference in theta can move a confirmation that lands exactly on the threshold. For the same reason, a percentage test such as (p-LL)/LL100 > 7 is not the same test as (p-LL)/LL > 0.07: on [13.00, 13.91] the percentage test fires and the fraction test does not. For thresholds in points, see DirectionalChangePoints.
A threshold is crossed only when it is exceeded: a move exactly equal to its threshold does not confirm, so on [100, 110] at 0.1 nothing is confirmed, because (110 - 100)/100 is exactly the float64 0.1. To reproduce a source that counts equality as a crossing, pass math.Nextafter(theta, 0) and, for an asymmetric pair, math.Nextafter(thetaDown[0], 0): both tests use >, so both thresholds move towards 0, and a ratio exceeds math.Nextafter(x, 0) exactly when it is at least x, for every float64 ratio, because no float64 lies between a value and its neighbour. The one edge is a threshold of 5e-324, where math.Nextafter gives 0, which is invalid: pass 5e-324 unchanged, since no relative move between two distinct positive float64 prices is at most 5e-324, so both rules agree there.
The first valid index is 0 and the minimum length is 1. There is no warm-up: bar 0 of a valid series gives extension 0. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChange(theta, thetaDown…).EQ(0) detects exactly the undetermined bars, where the extension is 0 or, on a missing bar, NaN.
A bar is missing when its price is not finite or is at most 0: a missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. The extension is NaN on a missing bar: a leading run of missing bars gives NaN until the first valid bar starts the machine, and an all-missing series gives NaN throughout. Deleting every missing bar from s gives the same extension bits on the remaining bars, and after a missing bar the machine carries on exactly as if the bar were absent.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars. On a 20,000-bar quarter-step random walk, runs started at 104 later bars agreed bit for bit with the full run, on the state, the extension and the age together, after a median of 14.5, 65.5 and 450.5 bars (maximum 58, 221 and 1062) at theta 0.01, 0.03 and 0.1, each within one confirmation of the full run.
The method is causal and deterministic: each output at bar t reads only s[0..t], every prefix of s gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose price completes the move, and its extreme lies in the past, so the output lags each turn by the confirmation delay.
The settings of the sources are documentation, not causal parameters, and DirectionalChange gives their calls: Chen and Tsang (2021) use thresholds from 0.001 to 0.01 on intraday FX and 0.003 (and 0.004) for daily index tracking, inclusive; Vakkur (1997) uses 0.10 on monthly S&P 500 closes, which the caller supplies, his spreadsheet strict and his prose inclusive; Merrill (1988) uses 0.05 for market averages and 0.10 for stocks, from the low point, “5% or more”; Dai, Zhang and Zhu use 0.24 up (inclusive) and 0.19 down (strict), and their segment tables are full-sample; Raftopoulos (2003) uses 0.10 to 0.18, inclusive. The machine is not a Markov model but a state source for one, and no trading edge is demonstrated: Vakkur’s r^2 of 0.084 is in-sample, and Chen and Tsang score their tracking against labels drawn with hindsight.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the length and whether the thresholds are valid, and none for an empty receiver.
Recipes built on the DirectionalChange methods are listed under “Compositions” in the “Regime Detection” category.
Verification: on finite positive data, at every bar t where the state is not 0, with k the age at t, the extension times 100 equals s.DeltaPercent(k)[t] bit for bit, the extension equals s.Momentum(k)[t] divided by s[t-k] bit for bit, and the extension times 100 agrees with s.RateOfChange(k)[t], which forms (p/ref - 1)100, to rounding, within 1002^-50*(1 + |ext|). All four outputs also agree bit for bit with two independent references and with the published examples of Chen and Tsang (their Fig 2.1 and Fig 5.2).
DirectionalChangePoints
func (s Series) DirectionalChangePoints(h float64, hDown ...float64) Series
DirectionalChangePoints returns the directional-change state with thresholds in the receiver’s own units: +1 in a confirmed upward run, -1 in a confirmed downward run and 0 before the first confirmation.
The machine is the directional change of DirectionalChange, after Chen and Tsang (2021), Eq 2.1 and Appendix A, with its ratio tests replaced by differences, so that the thresholds are absolute moves in the receiver’s units rather than fractions. Here up is h, and down is hDown[0] when hDown has at least one element, else h: only hDown[0] is read, and further elements are ignored whatever their values. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar with value p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p) > down, and otherwise an upward run when (p - LL) > up. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. Each test is one subtraction, not a level such as LL + up, which can round differently: on [0.1, 0.30000000000000004] at h = 0.2 the difference 0.20000000000000004 exceeds h and confirms, while the level 0.1 + 0.2 equals the second value and would not. DirectionalChangePoints gives the state, DirectionalChangePointsExtension the extension p - ref and DirectionalChangePointsAge the age t - iRef; the confirmations themselves are DirectionalChangePoints(h, hDown…).Delta().Clamp(-1, 1).
The state is defined on every bar and is not NaN. It is +1 from a bar that confirms an upward run and -1 from a bar that confirms a downward run, held until the next confirmation, and 0 only before the first confirmation: once non-zero it is not 0 again. Before the first confirmation the down test comes first; with these thresholds the two tests cannot both hold on one bar, so the order is not observable and at most one confirmation fires on a bar.
The thresholds are valid if and only if 0 <= h < +Inf and 0 <= hDown < +Inf; NaN fails every comparison. A threshold of 0 is valid: any non-zero move then confirms, so the state is the sign of the last non-zero change and is 0 only while every valid value equals the first. An invalid hDown does not fall back to h: DirectionalChangePoints(1, -1) is invalid although DirectionalChangePoints(1) is valid, and an empty hDown is the same as none. Every invalid pair gives len(s) zeros, and an empty or nil receiver gives an empty result.
Units. h and hDown are in receiver units, the units of the series itself: points or pips on prices, log units on a log series, and the indicator’s own units on an oscillator or a spread. This is a third convention beside the fractions of the DirectionalChange family (0.05 means a 5% move) and the percentages of UpDown, UpDownString and UpDownSum (tolerance) and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. On a log series, h = ln(1+f) approximates the relative rule of DirectionalChange(f) but is not the same float test. Because the tests are strict and exact, a one-bit difference in h can move a confirmation that lands exactly on the threshold. Go folds constant expressions exactly, so the constant 360.0001 equals 0.0036, but a threshold computed at run time, such as a pip count p times a pip size, can differ from the literal in the last bit (p = 36 gives p0.0001 == 0.0036000000000000003, not 0.0036): write the literal when a rule states one. For a threshold series, see DirectionalChangePointsScaled.
A threshold is crossed only when it is exceeded: a move exactly equal to its threshold does not confirm, so on [100, 101] at h = 1 nothing is confirmed. To reproduce a source that counts equality as a crossing, pass math.Nextafter(h, math.Inf(-1)) and, for an asymmetric pair, math.Nextafter(hDown[0], math.Inf(-1)): both tests use >, so both thresholds move down, and a move exceeds math.Nextafter(x, math.Inf(-1)) exactly when it is at least x, for every float64 move, because no float64 lies between a value and its neighbour. At a threshold of 5e-324 the nudge gives 0, which is valid, and the recipe stays exact. There is no recipe at 0, where the nudge gives -5e-324, which is invalid; an inclusive test at 0 would confirm on every unchanged bar.
Zero and negative values are valid, so the method serves oscillators, spreads and differences as well as prices; only a value that is not finite is missing. An upstream warm-up of zeros is therefore read as data: mask it with SetN(F, math.NaN()), with F the input’s first valid index, before passing the series.
The first valid index is 0 and the minimum length is 1. There is no warm-up: bar 0 of a valid series gives state 0. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChangePoints(h, hDown…).EQ(0) detects exactly the undetermined bars. Callers that feed the state to a latch, a rank or a state builder mask the 0 first.
A bar is missing when its value is not finite: a missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. On a missing bar the state is the previous bar’s state, which is 0 at bar 0 and before the first confirmation. Missing bars are invisible except to the clock: deleting every missing bar from s gives the same state on the remaining bars, and after a missing bar the machine carries on exactly as if the bar were absent.
A difference of two finite values of opposite signs near MaxFloat64 can overflow: (p - LL) or (LH - p) is then +Inf, which exceeds every valid threshold and confirms, and the extension is +Inf or -Inf.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars. On a 20,000-bar quarter-step random walk with values from 21.5 to 106.75, runs started at 104 later bars agreed bit for bit with the full run, on the state, the extension and the age together, after a median of 17, 75 and 568 bars (maximum 69, 251 and 1161) at h 1, 3 and 10.
The method is causal and deterministic: each output at bar t reads only s[0..t], every prefix of s gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose value completes the move, and the extreme it confirms lies in the past, so the machine lags each turn by more than a threshold’s move.
The settings of the sources are documentation, not causal parameters. Poster (2020) measures directional changes on FX in pips, from the highest or lowest price of the zigzag segment, and words the rule as a move of more than the threshold, which is this method’s strict test with h the pip count times the pip size. Poster (2024) overlays a zigzag of 15 pips on EURUSD and finds 10 pips the best threshold for his whipsaw filter there; his listing reverses on a move of at least the threshold (>= and <=), 15 pips by default, which is DirectionalChangePoints(math.Nextafter(h, math.Inf(-1))). Raftopoulos (2003) offers a points mode beside his percent mode, inclusive as well (>= and <=), which is the same recipe with h his amount in points. Chen and Tsang (2021) define the machine with relative thresholds; the absolute form follows Poster.
DirectionalChangePoints is not a Markov model but a state source for one, and no trading edge is demonstrated: Poster fits his scaling laws to the whole sample.
DirectionalChange is the fraction family: the same machine with the ratio tests (LH - p)/LH > down and (p - LL)/LL > up, which needs positive prices and treats zero and negative values as missing bars. Hysteresis is a latch on a value’s level against two fixed thresholds, also in receiver units, not on a move from a running extreme. The swing views SwingHighValue, SwingLowValue, MarketStructure, ClearMethodDirection, GannSwingDirection, SwingTrend, WilliamsSwingDirection and BasingPointDirection build bar-count, Clear and basing-point swings; none of them confirms a swing by a threshold move, and they read infinities as ordered values where this method holds its state. PointAndFigureDirection is the same kind of latch on a grid: it reads highs and lows at the levels of a fixed box in receiver units and reverses on a whole number of boxes counted from the column’s extreme level, so its moves are quantised to the grid where this method’s are exact differences. KagiYang runs this machine step for step with one reversal amount as both thresholds and adds the kagi chart’s thickness latch: +1 once a rising line passes the latest shoulder, -1 once a falling line passes the latest waist, held between such breaks, and the first line sets it to its own direction.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the length and whether the thresholds are valid, and none for an empty receiver.
Recipes built on the DirectionalChangePoints methods are listed under “Compositions” in the “Regime Detection” and “Kagi” categories.
Verification: all three outputs agree bit for bit with two independent references, one transcribed from the steps above and one written from the absolute-threshold formulation of the directional change. On finite data, with uo the ConsecUpOrEqual and do the ConsecDownOrEqual of s, DirectionalChangePoints(0) equals uo.GT(do).Sub(uo.LT(do)) bit for bit; when up is below every one-bar rise s[t] - s[t-1] and down below every one-bar fall s[t-1] - s[t], the state is the forward-filled sign of s.Momentum(1), and 0 before the first non-zero move; at a confirmation bar t with age k, and with p here the previous event bar (the first valid bar for the first event), the confirmed extreme s[t-k] is s.Highest(t-p+1)[t] for a -1 event and s.Lowest(t-p+1)[t] for a +1 event; negating s negates the state and swaps h and hDown; and on the base-2 exponents a of a series of powers of two 2^a the state at (h, hDown) = (u, d), whole numbers from 1 to 53, equals DirectionalChange(2^u - 1, 1 - 2^-d) on the series itself.
DirectionalChangePointsAge
func (s Series) DirectionalChangePointsAge(h float64, hDown ...float64) Series
DirectionalChangePointsAge returns the number of bars since the last confirmed extreme of DirectionalChangePoints.
The machine is the directional change of DirectionalChange, after Chen and Tsang (2021), Eq 2.1 and Appendix A, with its ratio tests replaced by differences, so that the thresholds are absolute moves in the receiver’s units rather than fractions. Here up is h, and down is hDown[0] when hDown has at least one element, else h: only hDown[0] is read, and further elements are ignored whatever their values. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar with value p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p) > down, and otherwise an upward run when (p - LL) > up. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. Each test is one subtraction, not a level such as LL + up, which can round differently: on [0.1, 0.30000000000000004] at h = 0.2 the difference 0.20000000000000004 exceeds h and confirms, while the level 0.1 + 0.2 equals the second value and would not. DirectionalChangePoints gives the state, DirectionalChangePointsExtension the extension p - ref and DirectionalChangePointsAge the age t - iRef; the confirmations themselves are DirectionalChangePoints(h, hDown…).Delta().Clamp(-1, 1).
The age at bar t is t - iRef, counted in bars. It is a whole number, 0 before the first confirmation and at least 1 (and at most t) wherever the state is not 0. On a confirmation bar it is the distance back to the extreme just confirmed, which by the tie rule is the latest bar holding that extreme value since the previous confirmation. It is a bar-index difference, so it counts missing bars, and it is not NaN. StateAge of the state counts from the confirmation bar, not from the extreme.
The thresholds are valid if and only if 0 <= h < +Inf and 0 <= hDown < +Inf; NaN fails every comparison. A threshold of 0 is valid: any non-zero move then confirms, so the state is the sign of the last non-zero change and is 0 only while every valid value equals the first. An invalid hDown does not fall back to h: DirectionalChangePointsAge(1, -1) is invalid although DirectionalChangePointsAge(1) is valid, and an empty hDown is the same as none. Every invalid pair gives len(s) zeros, and an empty or nil receiver gives an empty result.
Units. h and hDown are in receiver units, the units of the series itself: points or pips on prices, log units on a log series, and the indicator’s own units on an oscillator or a spread. This is a third convention beside the fractions of the DirectionalChange family (0.05 means a 5% move) and the percentages of UpDown, UpDownString and UpDownSum (tolerance) and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. On a log series, h = ln(1+f) approximates the relative rule of DirectionalChange(f) but is not the same float test. Because the tests are strict and exact, a one-bit difference in h can move a confirmation that lands exactly on the threshold. Go folds constant expressions exactly, so the constant 360.0001 equals 0.0036, but a threshold computed at run time, such as a pip count p times a pip size, can differ from the literal in the last bit (p = 36 gives p0.0001 == 0.0036000000000000003, not 0.0036): write the literal when a rule states one. For a threshold series, see DirectionalChangePointsScaled.
A threshold is crossed only when it is exceeded: a move exactly equal to its threshold does not confirm, so on [100, 101] at h = 1 nothing is confirmed. To reproduce a source that counts equality as a crossing, pass math.Nextafter(h, math.Inf(-1)) and, for an asymmetric pair, math.Nextafter(hDown[0], math.Inf(-1)): both tests use >, so both thresholds move down, and a move exceeds math.Nextafter(x, math.Inf(-1)) exactly when it is at least x, for every float64 move, because no float64 lies between a value and its neighbour. At a threshold of 5e-324 the nudge gives 0, which is valid, and the recipe stays exact. There is no recipe at 0, where the nudge gives -5e-324, which is invalid; an inclusive test at 0 would confirm on every unchanged bar.
Zero and negative values are valid, so the method serves oscillators, spreads and differences as well as prices; only a value that is not finite is missing. An upstream warm-up of zeros is therefore read as data: mask it with SetN(F, math.NaN()), with F the input’s first valid index, before passing the series.
The first valid index is 0 and the minimum length is 1. There is no warm-up: bar 0 of a valid series gives age 0. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChangePoints(h, hDown…).EQ(0) detects exactly the undetermined bars. Callers that feed the state to a latch, a rank or a state builder mask the 0 first.
A bar is missing when its value is not finite: a missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. On a missing bar the age is the previous bar’s age plus 1 when the state is not 0, and 0 otherwise. Deleting every missing bar from s changes the age only by the missing bars it counts: with them present, the age is the bar-index distance to the same extreme bar.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars. On a 20,000-bar quarter-step random walk with values from 21.5 to 106.75, runs started at 104 later bars agreed bit for bit with the full run, on the state, the extension and the age together, after a median of 17, 75 and 568 bars (maximum 69, 251 and 1161) at h 1, 3 and 10.
The method is causal and deterministic: each output at bar t reads only s[0..t], every prefix of s gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose value completes the move, and the extreme it confirms lies in the past, so the machine lags each turn by more than a threshold’s move.
The settings of the sources are documentation, not causal parameters, and DirectionalChangePoints gives their calls: Poster (2020) words his pip rule as strict, and the listing of Poster (2024) is inclusive (>= and <=), 15 pips by default on EURUSD; Raftopoulos (2003) offers an inclusive points mode. The machine is not a Markov model but a state source for one, and no trading edge is demonstrated: Poster fits his scaling laws to the whole sample.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the length and whether the thresholds are valid, and none for an empty receiver.
Recipes built on the DirectionalChangePoints methods are listed under “Compositions” in the “Regime Detection” and “Kagi” categories.
Verification: on finite data, at a confirmation bar t with age k, and with p here the previous event bar (the first valid bar for the first event), k equals (t-p) - s.IndexOfHighestS(t-p+1)[t] for a -1 event and (t-p) - s.IndexOfLowestS(t-p+1)[t] for a +1 event; this needs the rule that a tie moves the extreme to the latest bar, and a first-occurrence rule fails it. All three outputs also agree bit for bit with two independent references.
DirectionalChangePointsAgeScaled
func (s Series) DirectionalChangePointsAgeScaled(h Series, hDown ...Series) Series
DirectionalChangePointsAgeScaled returns the number of bars since the last confirmed extreme of DirectionalChangePointsScaled.
The machine is that of DirectionalChangePoints, the directional change of Chen and Tsang (2021), Eq 2.1 and Appendix A, with difference tests in the receiver’s units, and Scaled means that its thresholds are series read bar by bar: bar t tests its up move against its own up_t, the bar of h aligned with it, and its down move against its own down_t, the aligned bar of hDown[0] when hDown has at least one element, else of h. Only hDown[0] is read, and further elements are ignored whatever their values and lengths. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar t with value p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p) > down_t, and otherwise an upward run when (p - LL) > up_t. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. Each test is one subtraction compared with the bar’s own threshold, not a level such as LL + up_t, which can round differently. DirectionalChangePointsScaled gives the state, DirectionalChangePointsExtensionScaled the extension p - ref and DirectionalChangePointsAgeScaled the age t - iRef; the confirmations themselves are DirectionalChangePointsScaled(h, hDown…).Delta().Clamp(-1, 1).
The age at bar t is t - iRef, counted in bars of s. It is a whole number, 0 before the first confirmation and on the bars before the overlap, and at least 1 (and at most t - off) wherever the state is not 0. On a confirmation bar it is the distance back to the extreme just confirmed, which by the tie rule is the latest bar holding that extreme value since the previous confirmation. It is a bar-index difference, so it counts missing bars, those missing for their thresholds included, and it is not NaN. StateAge of the state counts from the confirmation bar, not from the extreme.
h and hDown[0] are right-aligned with s at their most recent bar. With m the minimum overlap, min(len(s), len(h)), or min(len(s), len(h), len(hDown[0])) when hDown is supplied, and off = len(s) - m, bar t of s from off on is paired with the bar of h and of hDown[0] at the same distance from their last bars, so a longer threshold series loses its oldest bars. The result has len(s), and the off bars before the overlap are 0 on every output, the extension included: they are not missing bars, and the machine has not started. The machine starts at the first valid bar at or after off. An empty h, or an empty hDown[0], leaves no overlap and gives len(s) zeros; an empty hDown, with no element, is the same as none; an empty or nil receiver gives an empty result. MarketStructure and the other swing views that take a second series return an empty Series when the lengths differ; this method right-aligns, as Add does.
A bar’s thresholds are valid if and only if 0 <= up_t < +Inf and 0 <= down_t < +Inf; NaN fails every comparison. A threshold of 0 is valid, and -0 behaves as 0: any non-zero move against the run, or in either direction before the first confirmation, then confirms on that bar. A bar is missing when its value s[t] is not finite or its thresholds are not valid, so a threshold that is NaN, infinite or negative on one bar makes that one bar missing and leaves the rest of the run alone, where an invalid constant threshold gives DirectionalChangePointsAge len(s) zeros. There is no fallback: when hDown is supplied, an invalid down_t makes bar t missing even where h is valid there.
Units. h and hDown are in receiver units, the units of the series itself: points or pips on prices, log units on a log series, and the indicator’s own units on an oscillator or a spread. This is a third convention beside the fractions of the DirectionalChange family (0.05 means a 5% move) and the percentages of UpDown, UpDownString and UpDownSum (tolerance) and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. A threshold series built from other series is in their units, so build it from the receiver’s own bars: an average true range of the same highs, lows and closes is in the closes’ units. Because the tests are strict and exact, a one-bit difference in a threshold can move a confirmation that lands exactly on it.
A threshold is crossed only when it is exceeded: a move exactly equal to its bar’s threshold does not confirm, so on [100, 101] with h = [1, 1] nothing is confirmed. To reproduce a source that counts equality as a crossing, nudge every positive finite threshold down by one float64 and leave the others alone, with h.Map(func(x float64) float64 { if x > 0 && x < math.Inf(1) { return math.Nextafter(x, math.Inf(-1)) }; return x }) and the same map on hDown[0]: both tests use >, and a move exceeds math.Nextafter(x, math.Inf(-1)) exactly when it is at least x, for every float64 move, because no float64 lies between a value and its neighbour. The guard keeps a bad bar bad, since math.Nextafter(+Inf, math.Inf(-1)) is MaxFloat64, a valid threshold, and it leaves a threshold of 0 strict: there is no recipe at 0, where the nudge gives -5e-324, which is invalid, and an inclusive test at 0 would confirm on every unchanged bar. At a threshold of 5e-324 the nudge gives 0, which is valid, and the recipe stays exact.
Bar t is compared with its own threshold as supplied; to commit each threshold before its bar, pass h.Shift(1).SetN(1, math.NaN()), as DirectionalChangePointsScaled describes, with its average-true-range form.
Zero and negative values of s are valid, so the method serves oscillators, spreads and differences as well as prices, and an upstream warm-up of zeros is read as data: mask it with SetN(F, math.NaN()), with F the input’s first valid index, before passing the series. A threshold warm-up of zeros is valid too, a 0 meaning that any move confirms, so mask a threshold series’ warm-up the same way: its bars are then missing, and the machine starts at the first bar with a valid threshold.
The first valid index is 0, counted from the start of the overlap, and the minimum overlap is 1. There is no warm-up: the first bar of the overlap gives age 0. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChangePointsScaled(h, hDown…).EQ(0) detects exactly the undetermined bars and the bars before the overlap, which are the bars where the age is 0. Callers that feed the state to a latch, a rank or a state builder mask these bars first.
A missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. On a missing bar the age is the previous bar’s age plus 1 when the state is not 0, and 0 otherwise. Deleting every missing bar from the aligned s, h and hDown[0] together changes the age only by the missing bars it counts: with them present, the age is the bar-index distance to the same extreme bar.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars; with a constant threshold series the settling lengths are those that DirectionalChangePoints gives.
The method is causal and deterministic: each output at bar t reads only the bars of s, h and hDown[0] up to bar t as aligned, cutting the same number of bars from the end of every input gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose value completes the move, and the extreme it confirms lies in the past, so the machine lags each turn by more than a threshold’s move.
The settings of the source are documentation, not causal parameters, and DirectionalChangePointsScaled gives Raschke’s average-true-range filter, illustrated on charts only. The machine is not a Markov model but a state source for one, and no trading edge is demonstrated.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the lengths and thresholds, and none for an empty receiver.
Recipes built on the DirectionalChangePointsScaled methods are listed under “Compositions” in the “Regime Detection” category.
Verification: on finite data with valid thresholds, at a confirmation bar t with age k, and with p here the previous event bar (the first bar of the overlap for the first event), k equals (t-p) - s.IndexOfHighestS(t-p+1)[t] for a -1 event and (t-p) - s.IndexOfLowestS(t-p+1)[t] for a +1 event, which needs the rule that a tie moves the extreme to the latest bar; when every bar of h is the valid value x, and every bar of hDown[0], if supplied, the valid value y, the age equals DirectionalChangePointsAge(x, y) bit for bit on the overlap. All three outputs also agree bit for bit with two independent references.
DirectionalChangePointsExtension
func (s Series) DirectionalChangePointsExtension(h float64, hDown ...float64) Series
DirectionalChangePointsExtension returns the signed move of each bar from the last confirmed extreme of DirectionalChangePoints, in the receiver’s units.
The machine is the directional change of DirectionalChange, after Chen and Tsang (2021), Eq 2.1 and Appendix A, with its ratio tests replaced by differences, so that the thresholds are absolute moves in the receiver’s units rather than fractions. Here up is h, and down is hDown[0] when hDown has at least one element, else h: only hDown[0] is read, and further elements are ignored whatever their values. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar with value p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p) > down, and otherwise an upward run when (p - LL) > up. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. Each test is one subtraction, not a level such as LL + up, which can round differently: on [0.1, 0.30000000000000004] at h = 0.2 the difference 0.20000000000000004 exceeds h and confirms, while the level 0.1 + 0.2 equals the second value and would not. DirectionalChangePoints gives the state, DirectionalChangePointsExtension the extension p - ref and DirectionalChangePointsAge the age t - iRef; the confirmations themselves are DirectionalChangePoints(h, hDown…).Delta().Clamp(-1, 1).
The extension at a valid bar with value p is p - ref, one subtraction, in the receiver’s units: positive above ref and negative below it. It is the test’s own shape, so on a confirmation bar it equals the test’s own value, above up on a +1 confirmation and below -down on a -1 confirmation, exactly, because a rounded difference changes sign with its operands, and it is -0 when p is -0 and ref is +0. It is +Inf or -Inf when the difference overflows. It is 0 on valid bars before the first confirmation, and NaN on a missing bar and only there.
The thresholds are valid if and only if 0 <= h < +Inf and 0 <= hDown < +Inf; NaN fails every comparison. A threshold of 0 is valid: any non-zero move then confirms, so the state is the sign of the last non-zero change and is 0 only while every valid value equals the first. An invalid hDown does not fall back to h: DirectionalChangePointsExtension(1, -1) is invalid although DirectionalChangePointsExtension(1) is valid, and an empty hDown is the same as none. Every invalid pair gives len(s) zeros, and an empty or nil receiver gives an empty result.
Units. h and hDown are in receiver units, the units of the series itself: points or pips on prices, log units on a log series, and the indicator’s own units on an oscillator or a spread. This is a third convention beside the fractions of the DirectionalChange family (0.05 means a 5% move) and the percentages of UpDown, UpDownString and UpDownSum (tolerance) and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. On a log series, h = ln(1+f) approximates the relative rule of DirectionalChange(f) but is not the same float test. Because the tests are strict and exact, a one-bit difference in h can move a confirmation that lands exactly on the threshold. Go folds constant expressions exactly, so the constant 360.0001 equals 0.0036, but a threshold computed at run time, such as a pip count p times a pip size, can differ from the literal in the last bit (p = 36 gives p0.0001 == 0.0036000000000000003, not 0.0036): write the literal when a rule states one. For a threshold series, see DirectionalChangePointsScaled.
A threshold is crossed only when it is exceeded: a move exactly equal to its threshold does not confirm, so on [100, 101] at h = 1 nothing is confirmed. To reproduce a source that counts equality as a crossing, pass math.Nextafter(h, math.Inf(-1)) and, for an asymmetric pair, math.Nextafter(hDown[0], math.Inf(-1)): both tests use >, so both thresholds move down, and a move exceeds math.Nextafter(x, math.Inf(-1)) exactly when it is at least x, for every float64 move, because no float64 lies between a value and its neighbour. At a threshold of 5e-324 the nudge gives 0, which is valid, and the recipe stays exact. There is no recipe at 0, where the nudge gives -5e-324, which is invalid; an inclusive test at 0 would confirm on every unchanged bar.
Zero and negative values are valid, so the method serves oscillators, spreads and differences as well as prices; only a value that is not finite is missing. An upstream warm-up of zeros is therefore read as data: mask it with SetN(F, math.NaN()), with F the input’s first valid index, before passing the series.
The first valid index is 0 and the minimum length is 1. There is no warm-up: bar 0 of a valid series gives extension 0. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChangePoints(h, hDown…).EQ(0) detects exactly the undetermined bars. Callers that feed the state to a latch, a rank or a state builder mask the 0 first.
A bar is missing when its value is not finite: a missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. The extension is NaN on a missing bar: a leading run of missing bars gives NaN until the first valid bar starts the machine, and an all-missing series gives NaN throughout. Deleting every missing bar from s gives the same extension bits on the remaining bars.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars. On a 20,000-bar quarter-step random walk with values from 21.5 to 106.75, runs started at 104 later bars agreed bit for bit with the full run, on the state, the extension and the age together, after a median of 17, 75 and 568 bars (maximum 69, 251 and 1161) at h 1, 3 and 10.
The method is causal and deterministic: each output at bar t reads only s[0..t], every prefix of s gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose value completes the move, and the extreme it confirms lies in the past, so the machine lags each turn by more than a threshold’s move.
The settings of the sources are documentation, not causal parameters, and DirectionalChangePoints gives their calls: Poster (2020) words his pip rule as strict, and the listing of Poster (2024) is inclusive (>= and <=), 15 pips by default on EURUSD; Raftopoulos (2003) offers an inclusive points mode. The machine is not a Markov model but a state source for one, and no trading edge is demonstrated: Poster fits his scaling laws to the whole sample.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the length and whether the thresholds are valid, and none for an empty receiver.
Recipes built on the DirectionalChangePoints methods are listed under “Compositions” in the “Regime Detection” and “Kagi” categories.
Verification: on finite data, at every bar t where the state is not 0, with k the age at t, the extension equals s.Momentum(k)[t] bit for bit, the same subtraction; on the base-2 exponents a of a series of powers of two 2^a, at (h, hDown) = (u, d), whole numbers from 1 to 53, 2 raised to the extension, less 1, equals DirectionalChangeExtension(2^u - 1, 1 - 2^-d) wherever the extension is at most 52 in size. All three outputs also agree bit for bit with two independent references.
DirectionalChangePointsExtensionScaled
func (s Series) DirectionalChangePointsExtensionScaled(h Series, hDown ...Series) Series
DirectionalChangePointsExtensionScaled returns the signed move of each bar from the last confirmed extreme of DirectionalChangePointsScaled, in the receiver’s units.
The machine is that of DirectionalChangePoints, the directional change of Chen and Tsang (2021), Eq 2.1 and Appendix A, with difference tests in the receiver’s units, and Scaled means that its thresholds are series read bar by bar: bar t tests its up move against its own up_t, the bar of h aligned with it, and its down move against its own down_t, the aligned bar of hDown[0] when hDown has at least one element, else of h. Only hDown[0] is read, and further elements are ignored whatever their values and lengths. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar t with value p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p) > down_t, and otherwise an upward run when (p - LL) > up_t. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. Each test is one subtraction compared with the bar’s own threshold, not a level such as LL + up_t, which can round differently. DirectionalChangePointsScaled gives the state, DirectionalChangePointsExtensionScaled the extension p - ref and DirectionalChangePointsAgeScaled the age t - iRef; the confirmations themselves are DirectionalChangePointsScaled(h, hDown…).Delta().Clamp(-1, 1).
The extension at a valid bar with value p is p - ref, one subtraction, in the receiver’s units: positive above ref and negative below it. It is the test’s own shape, so on a confirmation bar it equals the test’s own value, above up_t on a +1 confirmation and below -down_t on a -1 confirmation, exactly, because a rounded difference changes sign with its operands, and it is -0 when p is -0 and ref is +0. It is +Inf or -Inf when the difference overflows. It is 0 on valid bars before the first confirmation and on the bars before the overlap, and NaN on a missing bar of the overlap and only there, whether the bar is missing for its value or for its thresholds.
h and hDown[0] are right-aligned with s at their most recent bar. With m the minimum overlap, min(len(s), len(h)), or min(len(s), len(h), len(hDown[0])) when hDown is supplied, and off = len(s) - m, bar t of s from off on is paired with the bar of h and of hDown[0] at the same distance from their last bars, so a longer threshold series loses its oldest bars. The result has len(s), and the off bars before the overlap are 0 on every output, the extension included: they are not missing bars, and the machine has not started. The machine starts at the first valid bar at or after off. An empty h, or an empty hDown[0], leaves no overlap and gives len(s) zeros; an empty hDown, with no element, is the same as none; an empty or nil receiver gives an empty result. MarketStructure and the other swing views that take a second series return an empty Series when the lengths differ; this method right-aligns, as Add does.
A bar’s thresholds are valid if and only if 0 <= up_t < +Inf and 0 <= down_t < +Inf; NaN fails every comparison. A threshold of 0 is valid, and -0 behaves as 0: any non-zero move against the run, or in either direction before the first confirmation, then confirms on that bar. A bar is missing when its value s[t] is not finite or its thresholds are not valid, so a threshold that is NaN, infinite or negative on one bar makes that one bar missing and leaves the rest of the run alone, where an invalid constant threshold gives DirectionalChangePointsExtension len(s) zeros. There is no fallback: when hDown is supplied, an invalid down_t makes bar t missing even where h is valid there.
Units. h and hDown are in receiver units, the units of the series itself: points or pips on prices, log units on a log series, and the indicator’s own units on an oscillator or a spread. This is a third convention beside the fractions of the DirectionalChange family (0.05 means a 5% move) and the percentages of UpDown, UpDownString and UpDownSum (tolerance) and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. A threshold series built from other series is in their units, so build it from the receiver’s own bars: an average true range of the same highs, lows and closes is in the closes’ units. Because the tests are strict and exact, a one-bit difference in a threshold can move a confirmation that lands exactly on it.
A threshold is crossed only when it is exceeded: a move exactly equal to its bar’s threshold does not confirm, so on [100, 101] with h = [1, 1] nothing is confirmed. To reproduce a source that counts equality as a crossing, nudge every positive finite threshold down by one float64 and leave the others alone, with h.Map(func(x float64) float64 { if x > 0 && x < math.Inf(1) { return math.Nextafter(x, math.Inf(-1)) }; return x }) and the same map on hDown[0]: both tests use >, and a move exceeds math.Nextafter(x, math.Inf(-1)) exactly when it is at least x, for every float64 move, because no float64 lies between a value and its neighbour. The guard keeps a bad bar bad, since math.Nextafter(+Inf, math.Inf(-1)) is MaxFloat64, a valid threshold, and it leaves a threshold of 0 strict: there is no recipe at 0, where the nudge gives -5e-324, which is invalid, and an inclusive test at 0 would confirm on every unchanged bar. At a threshold of 5e-324 the nudge gives 0, which is valid, and the recipe stays exact.
Bar t is compared with its own threshold as supplied; to commit each threshold before its bar, pass h.Shift(1).SetN(1, math.NaN()), as DirectionalChangePointsScaled describes, with its average-true-range form.
Zero and negative values of s are valid, so the method serves oscillators, spreads and differences as well as prices, and an upstream warm-up of zeros is read as data: mask it with SetN(F, math.NaN()), with F the input’s first valid index, before passing the series. A threshold warm-up of zeros is valid too, a 0 meaning that any move confirms, so mask a threshold series’ warm-up the same way: its bars are then missing, and the extension is NaN there.
The first valid index is 0, counted from the start of the overlap, and the minimum overlap is 1. There is no warm-up: the first bar of the overlap gives extension 0 when its bar is valid. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChangePointsScaled(h, hDown…).EQ(0) detects exactly the undetermined bars and the bars before the overlap. Callers that feed the state to a latch, a rank or a state builder mask these bars first.
A missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. The extension is NaN on a missing bar: a leading run of missing bars gives NaN until the first valid bar starts the machine, and an overlap of missing bars gives NaN throughout it. Deleting every missing bar from the aligned s, h and hDown[0] together gives the same extension bits on the remaining bars.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars; with a constant threshold series the settling lengths are those that DirectionalChangePoints gives.
The method is causal and deterministic: each output at bar t reads only the bars of s, h and hDown[0] up to bar t as aligned, cutting the same number of bars from the end of every input gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose value completes the move, and the extreme it confirms lies in the past, so the machine lags each turn by more than a threshold’s move.
The settings of the source are documentation, not causal parameters, and DirectionalChangePointsScaled gives Raschke’s average-true-range filter, illustrated on charts only. The machine is not a Markov model but a state source for one, and no trading edge is demonstrated.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the lengths and thresholds, and none for an empty receiver.
Recipes built on the DirectionalChangePointsScaled methods are listed under “Compositions” in the “Regime Detection” category.
Verification: at every valid bar t of the overlap where the state is not 0, with k the age at t, the extension equals s.Momentum(k)[t] bit for bit, the same subtraction; when every bar of h is the valid value x, and every bar of hDown[0], if supplied, the valid value y, the extension equals DirectionalChangePointsExtension(x, y) bit for bit on the overlap. All three outputs also agree bit for bit with two independent references.
DirectionalChangePointsScaled
func (s Series) DirectionalChangePointsScaled(h Series, hDown ...Series) Series
DirectionalChangePointsScaled returns DirectionalChangePoints’s state with a threshold series h in the receiver’s units, compared bar by bar.
The state is +1 in a confirmed upward run, -1 in a confirmed downward run and 0 before the first confirmation. The machine is that of DirectionalChangePoints, the directional change of Chen and Tsang (2021), Eq 2.1 and Appendix A, with difference tests in the receiver’s units, and Scaled means that its thresholds are series read bar by bar: bar t tests its up move against its own up_t, the bar of h aligned with it, and its down move against its own down_t, the aligned bar of hDown[0] when hDown has at least one element, else of h. Only hDown[0] is read, and further elements are ignored whatever their values and lengths. Before the first confirmation the machine tracks both the running high LH and the running low LL from the first valid bar; in an upward run it tracks only LH, and in a downward run only LL. On each later valid bar t with value p the extremes extend first, a tie moving the extreme and its bar to the latest bar; then a downward run is confirmed when (LH - p) > down_t, and otherwise an upward run when (p - LL) > up_t. The extreme the move was measured from (the peak for a downward run, the trough for an upward run) becomes the last confirmed extreme ref, at bar iRef, and the other extreme restarts at the confirming bar. Each test is one subtraction compared with the bar’s own threshold, not a level such as LL + up_t, which can round differently. DirectionalChangePointsScaled gives the state, DirectionalChangePointsExtensionScaled the extension p - ref and DirectionalChangePointsAgeScaled the age t - iRef; the confirmations themselves are DirectionalChangePointsScaled(h, hDown…).Delta().Clamp(-1, 1).
The state is defined on every bar and is not NaN. It is +1 from a bar that confirms an upward run and -1 from a bar that confirms a downward run, held until the next confirmation, and 0 only before the first confirmation: once non-zero it is not 0 again. Before the first confirmation the down test comes first, and with thresholds that change from bar to bar both tests can hold on one bar: on [0, 10, 5] with h = [100, 100, 1] the fall of 5 and the rise of 5 both exceed 1 at bar 2, and the state is -1 there, measured from the high at bar 1. At most one confirmation fires on a bar.
h and hDown[0] are right-aligned with s at their most recent bar. With m the minimum overlap, min(len(s), len(h)), or min(len(s), len(h), len(hDown[0])) when hDown is supplied, and off = len(s) - m, bar t of s from off on is paired with the bar of h and of hDown[0] at the same distance from their last bars, so a longer threshold series loses its oldest bars. The result has len(s), and the off bars before the overlap are 0 on every output, the extension included: they are not missing bars, and the machine has not started. The machine starts at the first valid bar at or after off. An empty h, or an empty hDown[0], leaves no overlap and gives len(s) zeros; an empty hDown, with no element, is the same as none; an empty or nil receiver gives an empty result. MarketStructure and the other swing views that take a second series return an empty Series when the lengths differ; this method right-aligns, as Add does.
A bar’s thresholds are valid if and only if 0 <= up_t < +Inf and 0 <= down_t < +Inf; NaN fails every comparison. A threshold of 0 is valid, and -0 behaves as 0: any non-zero move against the run, or in either direction before the first confirmation, then confirms on that bar. A bar is missing when its value s[t] is not finite or its thresholds are not valid, so a threshold that is NaN, infinite or negative on one bar makes that one bar missing and leaves the rest of the run alone, where an invalid constant threshold gives DirectionalChangePoints len(s) zeros. There is no fallback: when hDown is supplied, an invalid down_t makes bar t missing even where h is valid there.
Units. h and hDown are in receiver units, the units of the series itself: points or pips on prices, log units on a log series, and the indicator’s own units on an oscillator or a spread. This is a third convention beside the fractions of the DirectionalChange family (0.05 means a 5% move) and the percentages of UpDown, UpDownString and UpDownSum (tolerance) and Pivots and PivotIndices (thresholdPercent), where 5 means 5%. The conventions were reviewed and kept in GLE-254 (3 October 2026): the unit follows the method family. A threshold series built from other series is in their units, so build it from the receiver’s own bars: an average true range of the same highs, lows and closes is in the closes’ units. Because the tests are strict and exact, a one-bit difference in a threshold can move a confirmation that lands exactly on it.
A threshold is crossed only when it is exceeded: a move exactly equal to its bar’s threshold does not confirm, so on [100, 101] with h = [1, 1] nothing is confirmed. To reproduce a source that counts equality as a crossing, nudge every positive finite threshold down by one float64 and leave the others alone, with h.Map(func(x float64) float64 { if x > 0 && x < math.Inf(1) { return math.Nextafter(x, math.Inf(-1)) }; return x }) and the same map on hDown[0]: both tests use >, and a move exceeds math.Nextafter(x, math.Inf(-1)) exactly when it is at least x, for every float64 move, because no float64 lies between a value and its neighbour. The guard keeps a bad bar bad, since math.Nextafter(+Inf, math.Inf(-1)) is MaxFloat64, a valid threshold, and it leaves a threshold of 0 strict: there is no recipe at 0, where the nudge gives -5e-324, which is invalid, and an inclusive test at 0 would confirm on every unchanged bar. At a threshold of 5e-324 the nudge gives 0, which is valid, and the recipe stays exact.
Bar t is compared with its own threshold as supplied, so a threshold may use bar t’s own data: an average true range that includes bar t is causal, but it grows with the bar it tests. To commit each threshold before its bar, pass h.Shift(1).SetN(1, math.NaN()): Shift(1) moves each threshold one bar later and pads bar 0 with 0, a valid threshold, which SetN masks. Raschke (in Bensignor, 2000) draws swing charts with a volatility filter after Wilder: a new up wave when price rallies k times the 10-bar simple average of TrueRange above the swing low, with k from 2 to 3, and a new down wave when the close falls below the swing high by as much. With closes c, highs hi and lows lo, the pre-committed filter on closes is c.DirectionalChangePointsScaled(dist) with dist = c.ATR(hi, lo, c, n).Mul(k).Shift(1).SetN(n+1, math.NaN()) and n = 10. ATR passes TrueRange through on bars 0 to n-2, and its first average, at bar n-1, includes bar 0’s range hi[0] - lo[0], which has no previous close; the shift moves these to bars 1 to n, and SetN masks them with the shift’s 0 at bar 0, so the machine starts at bar n+1, the first bar whose threshold averages n true ranges that each have a previous close. This method reads one series, so both of Raschke’s tests run on closes; her up test on highs is not built here.
Zero and negative values of s are valid, so the method serves oscillators, spreads and differences as well as prices, and an upstream warm-up of zeros is read as data: mask it with SetN(F, math.NaN()), with F the input’s first valid index, before passing the series. A threshold warm-up of zeros is valid too, a 0 meaning that any move confirms, so mask a threshold series’ warm-up the same way: its bars are then missing, and the machine starts at the first bar with a valid threshold.
The first valid index is 0, counted from the start of the overlap, and the minimum overlap is 1. There is no warm-up: the first bar of the overlap gives state 0 whatever its value and thresholds. The 0 before the first confirmation is the undetermined state, not a warm-up, and DirectionalChangePointsScaled(h, hDown…).EQ(0) detects exactly the undetermined bars and the bars before the overlap. Callers that feed the state to a latch, a rank or a state builder mask these bars first.
A missing bar updates nothing and fires nothing, the state is held and the age keeps counting. This is the hold class. On a missing bar the state is the previous bar’s state, which is 0 before the first confirmation. Missing bars are invisible except to the clock: deleting every missing bar from the aligned s, h and hDown[0] together gives the same state on the remaining bars, and after a missing bar the machine carries on exactly as if the bar were absent.
A difference of two finite values of opposite signs near MaxFloat64 can overflow: (p - LL) or (LH - p) is then +Inf, which exceeds every valid threshold and confirms, and the extension is +Inf or -Inf.
The machine is a latch: it remembers its last confirmation, so it is not start-invariant, and a run started later can differ from the full run until it re-synchronises, after a path-dependent number of bars; with a constant threshold series the settling lengths are those that DirectionalChangePoints gives.
The method is causal and deterministic: each output at bar t reads only the bars of s, h and hDown[0] up to bar t as aligned, cutting the same number of bars from the end of every input gives the same first values bit for bit, and nothing is revised. Each confirmation is stamped at the bar whose value completes the move, and the extreme it confirms lies in the past, so the machine lags each turn by more than a threshold’s move.
The settings of the source are documentation, not causal parameters: Raschke’s ten bars and k from 2 to 3 are illustrated on charts only. The machine is not a Markov model but a state source for one, and no trading edge is demonstrated.
DirectionalChangePoints is this machine with constant thresholds: when every bar of h is the valid value x, and every bar of hDown[0], if supplied, the valid value y, the three outputs equal those of DirectionalChangePoints(x, y) on the overlap bit for bit. An invalid constant differs: DirectionalChangePoints gives len(s) zeros, while here every bar is missing, so the extension is NaN on the overlap. DirectionalChange is the fraction family: its moves are fractions of the running extreme, which is not known when a threshold series is formed, so it is not this method with any threshold series. Hysteresis is a latch on a value’s level against two fixed thresholds, not on a move from a running extreme. The swing views SwingHighValue, SwingLowValue, MarketStructure, ClearMethodDirection, GannSwingDirection, SwingTrend, WilliamsSwingDirection and BasingPointDirection build bar-count, Clear and basing-point swings; none of them confirms a swing by a threshold move, and they read infinities as ordered values where this method holds its state.
The cost is one pass, O(1) per bar, and the method allocates only the result: one allocation per call whatever the lengths and thresholds, and none for an empty receiver.
Recipes built on the DirectionalChangePointsScaled methods are listed under “Compositions” in the “Regime Detection” category. The jump override of Meyers’s percentage reversal, run on the log of a regression forecast curve, is under “Linear Regression”.
Verification: when every bar of h is the valid value x, and every bar of hDown[0], if supplied, the valid value y, the state equals DirectionalChangePoints(x, y) bit for bit on the overlap; on finite data as long as h, with every threshold valid and below every non-zero one-bar move |s[t] - s[t-1]|, the state is the forward-filled sign of s.Momentum(1), and 0 before the first non-zero move; a threshold that is invalid on bar t gives the outputs of a valid threshold there with s[t] replaced by NaN. All three outputs also agree bit for bit with two independent references.
RegimeDFA
func (s Series) RegimeDFA(n int, args ...float64) Series
RegimeDFA maps DFA (Detrended Fluctuation Analysis) alpha into trading regimes using conventional thresholds. Good default thresholds for setting DFA-based regime classification in trading are: * Trending regime: alpha > 0.55 * Mean-reverting regime: alpha < 0.45 * Random walk/neutral: alpha ≈ 0.5
These thresholds create a buffer around the “random walk” mid-value of 0.5, reducing false positives from noise or estimation error. Using 0.55 and 0.45 as cutoffs is a widely-used, empirically validated starting point, but you may fine-tune these based on the specific instrument or the characteristics of your time series and trading strategy.
Returns a series with values: +1 for trending (alpha > trendThresh), 0 for random/neutral, -1 for mean-reverting (alpha < meanRevThresh).
trendThresh and meanRevThresh are values of the DFA scaling exponent alpha (dimensionless, where 0.5 is a random walk), not percentages, and default to 0.55 and 0.45. The comparisons are strict: alpha > trendThresh gives +1, alpha < meanRevThresh gives -1, and anything else gives 0. Only zero or two optional arguments are accepted. n < 20 (fewer than the two DFA scales a fit needs), one optional argument, a NaN threshold or meanRevThresh above trendThresh is invalid and gives all zeros; equal thresholds (no neutral band) and infinite thresholds are valid. The warm-up reads 0, not NaN: bars 0 to n-2 are 0, the neutral reading, and the first valid index is n-1. Releases before this change read -1 (mean-reverting) on those bars, and on every bar for n < 20, because DFAAlpha’s warm-up 0 is below the default 0.45. For n from 20 to 23 alpha is a two-point fit (scales 4 and 5).
Compositions
Directional change
DirectionalChange confirms an upward run when the price has risen from its running low by more than a fraction, and a downward run when it has fallen from its running high by more than a fraction; DirectionalChangeExtension, DirectionalChangeAge and DirectionalChangeEvent give the move from the last confirmed extreme, the bars since that extreme and the confirmations themselves. The recipes below take prices c and thresholds held in float64 variables.
Units (owner ruling 3). The thresholds theta and thetaDown are fractions: 0.05 means a 5% move. This differs on purpose from the percentage tolerance of UpDown, UpDownString and UpDownSum and the thresholdPercent of Pivots and PivotIndices, where 5 means 5%. The difference was flagged for review, and GLE-254 (3 October 2026) kept both conventions: the unit follows the method family. Recipe (d) converts a percentage.
The undetermined state. Before the first confirmation the state is 0, and the extension, the age and the event are 0 too; DirectionalChange(theta).EQ(0) detects those bars exactly. The 0 is not a warm-up and not a run: a recipe that feeds the state or the extension to a latch, a rank, an age or a state builder masks it first, as recipes (f) and (g) do. The state is not NaN on any bar.
Ties. A threshold is crossed only when it is exceeded. To reproduce a source that counts equality as a crossing, pass each threshold through math.Nextafter(x, 0); at the smallest threshold, 5e-324, pass it unchanged.
| Source | Rule | Call |
|---|---|---|
| Chen and Tsang (2021), Eq 2.1 | inclusive | c.DirectionalChange(math.Nextafter(theta, 0)) |
| Vakkur (1997), spreadsheet | strict | c.DirectionalChange(0.10) |
| Vakkur (1997), prose | inclusive | c.DirectionalChange(math.Nextafter(0.10, 0)) |
| Merrill (1988), “5% or more” from the low | inclusive | recipe (b) |
| Dai, Zhang and Zhu | rally “24% or more”, decline “more than 19%” | recipe (c) |
| Raftopoulos (2003) | inclusive, 0.10 to 0.18 | c.DirectionalChange(math.Nextafter(f, 0)) |
These settings are documentation, not causal parameters: each source chose its threshold on its own data, and no trading edge is demonstrated.
(a) Inclusive thresholds
st := c.DirectionalChange(math.Nextafter(theta, 0))- First valid index: 0; the minimum length is 1.
- Exact: a relative move exceeds
math.Nextafter(x, 0)exactly when it is at least x, because no float64 lies between a value and its neighbour. On [100, 110, 99] at 0.10 both moves are exactly 10% in float64: the strict method confirms neither, the inclusive call both. - Anchors: the four outputs equal an inclusive transcription of the machine (
>=in both tests) bit for bit on every bar (E). On the 20,000-bar quarter-step walk of the specification at 0.005, 0.01 and 0.03 they give the research reference’s D-28 digest 31d2229ded56e838. - Pinned by
TestRecipeDirectionalChangeViews; runnable exampleExampleSeries_DirectionalChange_inclusive.
(b) Merrill’s filter
Merrill measures a decline from the low point, (H - p)/p, so his down fraction is f/(1+f), computed at run time from the float64 variable f:
g := f / (1 + f)
st := c.DirectionalChange(math.Nextafter(f, 0), math.Nextafter(g, 0))
strict := c.DirectionalChange(f, g)- First valid index: 0; the minimum length is 1.
- The variable matters: Go folds the constant
1 - 1/1.05exactly, to0.05/1.05, but at run time1 - 1/(1+f)is 0.04761904761904767, not f/(1+f), and misses the decline on [100, 105, 100] even aftermath.Nextafter. - Anchors:
stequals Merrill’s literal rule with inclusive comparisons andstrictthe literal rule with strict ones, measured from the low, on every bar of the AAPL closes of 2023 and the specification’s quarter-step and cent walks at f = 0.01, 0.05 and 0.10 (E). - Rounding ties: each rule rounds its own quotient, so they can differ where one quotient rounds onto its threshold. On [64.47, 61.4] at f = 0.05, Merrill’s (H - p)/p rounds to 0.05 and his strict test misses the decline that
strictconfirms; on [14.07, 13.4],stconfirms a decline that is just below 5% for those float64 prices. - Pinned by
TestRecipeDirectionalChangeViews; runnable exampleExampleSeries_DirectionalChange_merrill.
(c) Dai, Zhang and Zhu’s mixed rule
st := c.DirectionalChange(math.Nextafter(0.24, 0), 0.19)- First valid index: 0; the minimum length is 1.
- Only the up threshold is nudged: a rally of exactly 24% ([100, 124]) confirms, a decline of exactly 19% ([200, 162]) does not, and [200, 161.99] does. Their segment tables are full-sample.
- Anchors: the four outputs equal a transcription with
>=in the up test and>in the down test bit for bit (E). - Pinned by
TestRecipeDirectionalChangeViews; runnable exampleExampleSeries_DirectionalChange_dai.
(d) A percentage threshold
theta := pct / 100- Whole numbers: for a whole-number
pct,pct / 100is the decimal literal (7 gives 0.07); for a fractional one it can differ in the last bit (0.7 gives 0.006999999999999999, not 0.007), andpct * 0.01can differ even for whole numbers (35 gives 0.35000000000000003). Go folds the constants0.7/100and35*0.01exactly. Write the fraction literal when a rule is stated in decimals. - Not a percentage test:
(p-LL)/LL*100 > 7is not(p-LL)/LL > 0.07; on [13.00, 13.91] the first fires andDirectionalChange(0.07)does not. - Anchors:
float64(k) / 100equals the parsed literalk/100for every whole number k from 1 to 10,000 (E). - Pinned by
TestRecipeDirectionalChangeViews; runnable exampleExampleSeries_DirectionalChange_percent.
(e) Gap mask
The method holds its state across a missing price (not finite, or at most 0), so a windowed chain fed the state cannot see the gap. Adding NaN on the missing bars makes it visible:
st := c.DirectionalChange(theta).Add(c.If(func(p float64) bool { return p > 0 && p < math.Inf(1) }, 0.0, math.NaN()))- First valid index: 0; the minimum length is 1.
- Footprint: NaN exactly on the missing bars, the state elsewhere; the age still counts the missing bars.
- Anchors: on tie walks with missing bars of every kind,
stis NaN exactly where the price is missing and equalsDirectionalChangebit for bit elsewhere (E). - Pinned by
TestRecipeDirectionalChangeViews; runnable exampleExampleSeries_DirectionalChange_gapMask.
(f) Run age
StateAge of the masked state counts each confirmed run from its confirmation bar, 1 on that bar; DirectionalChangeAge counts from the confirmed extreme, the delay earlier:
st := c.DirectionalChange(theta)
runAge := st.If(func(v float64) bool { return v == 0 }, math.NaN(), st).StateAge()- First valid index: the first confirmation bar;
runAgeis NaN before it. Unmasked,st.StateAge()ages the undetermined bars as a run. - Anchors: wherever the state is not 0,
runAge[t]is t minus the bar of the run’s confirmation plus 1, and on finite positive dataage[t] == age[t - runAge[t] + 1] + runAge[t] - 1withagetheDirectionalChangeAge(K; both sides are GLE-227 methods). - Pinned by
TestRecipeDirectionalChangeViews; runnable exampleExampleSeries_DirectionalChange_runAge.
(g) Extension with the undetermined bars as NaN
st := c.DirectionalChange(theta)
ext := c.DirectionalChangeExtension(theta)
det := st.If(func(v float64) bool { return v == 0 }, math.NaN(), ext)- First valid index: the first confirmation bar;
detis NaN before it. - Anchors:
detis NaN exactly where the state is 0 or the price is missing, and equals the extension bit for bit elsewhere (E). - Pinned by
TestRecipeDirectionalChangeViews; runnable exampleExampleSeries_DirectionalChangeExtension_detector.
(h) Total price movement
Chen and Tsang’s TMV (Eq 4.1) is the move from the last confirmed extreme in units of the threshold:
tmv := c.DirectionalChangeExtension(theta).Abs().Div(theta)- First valid index: 0; the minimum length is 1. It is 0 before the first confirmation and NaN on a missing price.
- Anchors: wherever the state is not 0,
tmvequals Eq 4.1 written literally,(|Ps - Pe|/Ps)/thetawith Ps the confirmed extreme, bit for bit (E). On their Fig 5.2 at book t = 10 it is 1.6842105263157894, with T 6. - Pinned by
TestRecipeDirectionalChangeViews; runnable exampleExampleSeries_DirectionalChangeExtension_tmv.
Directional change with a threshold series
DirectionalChangePointsScaled, DirectionalChangePointsExtensionScaled and DirectionalChangePointsAgeScaled run the directional change in the receiver’s units with a threshold that is itself a series: bar t tests its moves against its own threshold h[t] (and hDown[0][t] for the down test, when given). h is right-aligned with the closes c at the most recent bar, and the bars before the overlap read 0. A bar whose value is not finite, or whose threshold is NaN, infinite or negative, is missing: it holds the state for that bar only. A threshold of 0 is valid and lets any move confirm. With a constant series the methods are DirectionalChangePoints and its views. The recipes below take closes c, highs hi and lows lo; their outputs are codes, counts and differences of inputs, all exact.
(a) Swings on an average-true-range threshold
Raschke builds swing charts with a volatility filter after Wilder: a new wave when price moves k times the 10-bar simple average of TrueRange from the last swing extreme, with k from 2 to 3. On closes, with the threshold committed a bar early and its warm-up masked:
dist := c.ATR(hi, lo, c, n).Mul(k).Shift(1).SetN(n+1, math.NaN())
state := c.DirectionalChangePointsScaled(dist)
events := state.Delta().Clamp(-1, 1)- First valid index: 0 for all three; the minimum length is 1. Bars 0 to
nare missing, so the machine starts at barn+1with state 0.ATRpassesTrueRangethrough on bars 0 ton-2, and its first average, at barn-1, includes bar 0’s rangehi[0] - lo[0], which has no previous close; afterShift(1)these sit on bars 1 ton, and bar 0 holds the shift’s 0, a valid threshold. Without theSetNthe machine starts at bar 0 and reads those values as thresholds. - Units:
distis in the closes’ units, as the method requires. Raschke tests the rise on highs and the fall on closes; this recipe tests both on closes. - Anchors: at an event bar
twith agekfromc.DirectionalChangePointsAgeScaled(dist), the confirmed extreme is bart-kand the extension is beyonddist[t](E, throughMomentum). On the 200 AAPL bars of 2023 in the package’s test data, withn = 10andk = 2, the first confirmation is at bar 18, with 5 up and 6 down confirmations; without theSetNit is at bar 9, with 6 of each. Atk = 3there are 4 up and 3 down, the first at bar 29. - Pinned by
TestRecipeDirectionalChangePointsScaled; runnable exampleExampleSeries_DirectionalChangePointsScaled_atr.
(b) A threshold committed before its bar
Each bar is compared with its own threshold, so a threshold built from the bar itself, such as its range, grows with the move it tests. To commit each threshold a bar early:
pre := h.Shift(1).SetN(1, math.NaN())
state := c.DirectionalChangePointsScaled(pre)- First valid index: 0; the minimum length is 1. Bar 0 is masked, since
Shift(1)pads it with 0, a valid threshold, so the machine starts at bar 1. - Anchors:
preat bartish[t-1](E,Shift). On the 200 AAPL bars of 2023 withh := hi.Sub(lo), the bar’s own range gives 13 up and 13 down confirmations and the pre-committed range 16 of each, both first at bar 3; the two states differ on 28 bars. - Pinned by
TestRecipeDirectionalChangePointsScaled; runnable exampleExampleSeries_DirectionalChangePointsScaled_precommitted.
(c) Inclusive reversals
A source that reverses on a move of at least the threshold is the strict method with every positive finite threshold nudged down by one float64:
inclusive := h.Map(func(x float64) float64 {
if x > 0 && x < math.Inf(1) {
return math.Nextafter(x, math.Inf(-1))
}
return x
})
state := c.DirectionalChangePointsScaled(inclusive)- First valid index: 0; the minimum length is 1.
- Exactness: a move exceeds
math.Nextafter(x, math.Inf(-1))exactly when it is at leastx, for every float64 move. The guard keeps a bad bar bad (math.Nextafter(+Inf, math.Inf(-1))is MaxFloat64, a valid threshold) and leaves a threshold of 0 strict: unguarded, the nudge would turn 0 into -5e-324, an invalid threshold, and so make those bars missing. MaphDown[0]the same way. - Anchors: where no threshold is 0, the inclusive state equals the
>=reading of the step list on every bar (E). On the 20,000-bar quarter-step walk withh[t] = (t mod 5)/4and+Infwheret mod 50 = 49, there are 3,555 strict and 5,005 inclusive confirmations, and the states differ on 4,588 bars; the guarded map leaves 400 missing bars, where the unguarded nudge would leave 4,000. - Pinned by
TestRecipeDirectionalChangePointsScaled; runnable exampleExampleSeries_DirectionalChangePointsScaled_inclusive.
Directional change in points
DirectionalChangePoints, DirectionalChangePointsExtension and DirectionalChangePointsAge run the directional-change machine with thresholds in the receiver’s own units: points or pips on prices, and the indicator’s own units on an oscillator or a spread. Each move is one subtraction from the running extreme, and a threshold is crossed only when it is exceeded. Zero and negative values are valid; only a value that is not finite is missing, and a missing bar holds the state. The recipes below take closes c or an oscillator osc and a threshold h; their outputs are codes, counts and differences of inputs, all exact.
(a) Inclusive reversals
Poster’s 2024 listing reverses on a move of at least the threshold (>= and <=), and so does Raftopoulos’s points mode. Nudge the threshold down by one float64:
strict := c.DirectionalChangePoints(h)
inclusive := c.DirectionalChangePoints(math.Nextafter(h, math.Inf(-1)))- First valid index: 0 for both; the minimum length is 1. Both start in the undetermined state 0.
- Exactness: a move exceeds
math.Nextafter(h, math.Inf(-1))exactly when it is at leasth, for every float64 move. Nudge an asymmetrichDownthe same way. Ath = 5e-324the nudge gives 0, which is valid; there is no recipe ath = 0, where it gives -5e-324, which is invalid. - Anchors: the inclusive state equals the
>=reading of the method’s step list on every bar (E). On a 20,000-bar quarter-step random walk ath = 1the two states differ on 3,048 bars, with 1,086 strict and 1,524 inclusive confirmations. - Pinned by
TestRecipeDirectionalChangePoints; runnable exampleExampleSeries_DirectionalChangePoints_inclusive.
(b) Events and views on an oscillator
state := osc.DirectionalChangePoints(h)
events := state.Delta().Clamp(-1, 1)
ext := osc.DirectionalChangePointsExtension(h)
age := osc.DirectionalChangePointsAge(h)- First valid index: 0 for all four; the minimum length is 1.
eventsis +1 or -1 exactly on the confirmation bars and 0 on every other bar, missing bars included. - Views: at an event bar
twith agek, the confirmed extreme is bart-k, a swing high for -1 and a swing low for +1, andextis the test’s own move, abovehat +1 and below-hat -1. - Warm-up zeros: zero is a valid value here, unlike in
DirectionalChange, so mask an upstream warm-up withSetN(F, math.NaN())first. On the 200 AAPL closes of 2023 in the package’s test data,c.Sub(c.Average(10))is 0 on bars 0 to 8; ath = 2the unmasked oscillator confirms a rise from that 0 at bar 9, whilec.Sub(c.Average(10)).SetN(9, math.NaN())first confirms at bar 11. Both give 19 up and 19 down confirmations. - Anchors:
extequalsosc.Momentum(k)at every bar with a non-zero state and a finite value (E). - Pinned by
TestRecipeDirectionalChangePoints; runnable exampleExampleSeries_DirectionalChangePoints_events.
Three trend-quality gates from stock-selection articles, each with in-sample evidence on lists of index members, open to survivorship bias. Values are NaN before their first valid index, where a window would read an average’s pass-through or a warm-up; flags and scores read 0 there. Prices are positive and finite, volumes non-negative and finite, and lengths are in bars (weekly bars for the star rating); series are paired by position, so the caller puts the index closes on the stock’s calendar, aligned by timestamp, before they reach nseries.
Stiffness (Katsanos)
Markos Katsanos (“The Stiffness Indicator”, Stocks & Commodities V36:12, November 2018, pp. 9-13) counts “the number of times price closed above the MA during the indicator period”, with “a volatility threshold that excludes minimal penetrations of less than 0.2 standard deviations”: “MA2 = MA(C,100)-0.2StDev(C,100); P = Sum(C>MA2,60); Stiffness = P100/60”. His AmiBroker code smooths the share with EMA(STIF,3), which AmiBroker seeds with a simple average of the first valid values. t6Stiffness returns the printed share in his order and that smoothing, trimmed before XAverage so that its seed skips the warm-up.
raw := c.GT(c.Bollinger(a, -k)).Sum(p).Mul(100).Div(p)
stif := raw.SetN(a+p-2, math.NaN())
stiffness := raw.Mul(0.0).Add(raw.Window(-(a + p - 2)).XAverage(m)).SetN(a+p+m-3, math.NaN())- Inputs: closes, a, p and m (100, 60 and 3) and k (0.2);
Bollingeruses the population deviation, as AmiBroker’s StDev does. - First valid index: a + p - 2 for
stif(158) and a + p + m - 3 forstiffness(160), NaN before. On bars 0 to a - 2 the band is the close itself (Averagepasses it through and the deviation is 0), so the flag reads 0 there. - Anchors:
stifis the count of closes above the band times 100 over p, bit for bit;stiffnessequals AmiBroker’s EMA seeded on the first m shares, bit for bit at m = 3, whose constant 0.5 makes every product exact; a constant share of 90 smooths to 90 and does not cross over 90; and for his June 2018 coefficient,c.LT(ma).Sum(63).LTE(7)equalsc.GT(ma).Sum(63).GTE(56)when no close equals the average: 56 of 63 is 88.888…, so the dossier’s share level 88.9 misses it. - Traps: the dossier’s
Average(60).Mul(100)rounds twice and differs in the last bits; its full-seriesXAverage(3)seeds on the warm-up and reads 0.42 off at bar 160 and 4e-7 at bar 180 on the synthetic path; his printed index rule is strict where his code tests “>=”, and the platform ports depart (TradeStation’s scales by the average’s length, AIQ’s exits below 90 and uses 2 deviations, TradersStudio’s exits below 90, takes a sample deviation over the 60-bar period and prints both 2 and 0.2 as its deviation count, MetaStock’s exits after 21 bars). - Variant: his system,
buy := stiffness.CrossesAboveS(90.0).Mul(mkt)andsell := stiffness.CrossesBelowS(50.0), withe := spy.XAverage(100).SetN(99, math.NaN())andmkt := e.GTE(e.Shift(2)), andc.Position(buy.Shift(1), sell.Shift(1), 85): fills at the next open and his “Bars since entry >= 84”. - Variant: his June 2018 stiffness coefficient,
c.LT(c.Average(100)).Sum(63).LTE(7).SetN(161, math.NaN()), which counts the bars below the average, not the crossings. - Evidence: his Figure 4 (S&P 500 stocks over ten years to 6 July 2018): 1,030 trades, 63.4% profitable and a profit factor of 2.6, against buy and hold of SPY, with parameters he optimised. Evidence, not a reference.
- Pinned by
TestRecipeT6Stiffness; runnable exampleExampleSeries_Bollinger_stiffness.
Technical stock rating (Katsanos)
Markos Katsanos (“A Technical Method For Rating Stocks”, Stocks & Commodities V36:7, June 2018, pp. 9-14) scores five conditions: the volume flow indicator above zero, the close above its 100-day average, that average above its value four days earlier, his stiffness coefficient and the index’s 100-day EMA “either flat or pointing up”. He found the system “performed best when all conditions were assigned a weight of 1 except for the last condition, which earned a weight of 2”, and used “a rating threshold of greater or equal to 5”. t6StockRating follows the operators of his AmiBroker code (<=7 and >=), with the shipped VolumeFlowIndicator as his VFI.
vfi := c.VolumeFlowIndicator(h, l, c, v, 130, 0.2, 2.5)
ma := c.Average(100)
c1 := vfi.GT(0.0)
c2 := c.GT(ma)
c3 := ma.GT(ma.Shift(4))
c4 := c.LT(ma).Sum(63).LTE(7)
e := spy.XAverage(100).SetN(99, math.NaN())
c5 := e.GTE(e.Shift(2))
score := c1.Add(c2).Add(c3).Add(c4).Add(c5.Mul(2)).SetN(261, math.NaN())
buy := score.GTE(5)- Inputs: highs, lows, closes and volumes of the stock and the index closes
spyon the stock’s calendar; the lengths and levels are his. - First valid index: 261 for
score, the volume flow indicator’s 2n + 1 at n = 130, NaN before;buyreads 0 there. His code forces the indicator to 0 through bar 268 (BarIndex()>268), so the package’s indicator can read above 0 on bars 261 to 268 where his reads 0, and the two scores differ there. - Anchors: a score of 5 or more is the index gate and at least three of the four stock terms, on every bar (all 32 combinations); the index gate, masked to bar 99, reads 0 to bar 100 and equals his
EMA(SPY,100)>=Ref(EMA(SPY,100),-2)after, where an unmasked one fires in the warm-up. - Traps: his text says “less than seven times” and prints the index rule strict, where his code tests
<=7and>=, and it counts the bars below the average, not crossings; the ports depart (Wealth-Lab buys only on a score above 5, NeuroShell weights the stiffness term 2 and the index 1); a note in the Excel port gives a 30-day hold where his code holds 21 bars. - Variant: his code’s start,
vfi.SetN(269, 0)beforeGT(0.0). - Variant: his 21-bar hold with fills at the next open,
c.Position(buy.Shift(1), nil, 22)if AmiBroker’s N-bar stop exits at the open after the 21st bar,c.Position(buy.Shift(1), nil, 21)if on it; the article does not say which. - Evidence: his Figure 2 (S&P 500 stocks, 2 January 2008 to 2 January 2018, no costs): holding a month after a rating of 5 or more gave 29,093 trades, a profit factor of 1.64 and a maximum drawdown of 11.7%, against 1.35 and 53.4% without the rating. Evidence, not a reference.
- Pinned by
TestRecipeT6StockRating.
Technical star rating (D’Errico)
Domenico D’Errico (“Build Your Own Technical Rating”, Stocks & Commodities V35:8, July 2017, pp. 42-46) combines a “One-year linear regression slope”, “12-week higher highs” and “12-week higher lows”, “assigning two stars to slope, one star to highs, and two stars to lows, for a maximum of five stars”, and trades “Buy if the stock rating is above 2.5 stars, sell if it’s below 2.5” on weekly bars. t6StarRating reads the higher highs and lows quarter over quarter (his Figure 1’s “Previous Quarter” and “Current Quarter”) and the slope over 40 weeks, his tested setting.
hh := h.Highest(b)
ll := l.Lowest(b)
raw := c.Slope(sl).GT(0.0).Mul(2).Add(hh.GT(hh.Shift(b))).Add(ll.GT(ll.Shift(b)).Mul(2)).
SetN(max(sl-1, b+b-1), math.NaN())
long := raw.GT(2.5)- Inputs: weekly highs, lows and closes (the caller builds the weeks); sl and b (40 and 12).
- First valid index: max(sl - 1, 2b - 1) for
raw(39 at the defaults), NaN before;longreads 0 there. Unmasked,ll.Shift(b)pads 0, so the higher-lows flag is 1 on bars 0 to b - 1. - Anchors: with the weights a permutation of 1, 2 and 2 (his Figure 6’s 2-1-2, 1-2-2 and 2-2-1) a rating above 2.5 is at least two of the three flags, on every bar: one flag scores at most 2 and any two at least 3; the flags equal loops of their definitions.
- Traps: since the three weightings give one raw rule, his Figure 6’s three trade counts (707, 854 and 743) show that his published rating is smoothed, which he does not say; his Figure 7 (settings 1-2-2, Simulation 2’s weighting) shows 850 or so trades at a trigger of 5, which suggests an inclusive buy; Figure 6 prints a drawdown of -197291 where its own ratio needs about 19,729.
- Variant: a smoothed rating,
raw.Average(10).Hysteresis(2.5, 2.5), valid from bar max(sl - 1, 2b - 1) + 9: the 10-bar simple average is an assumption from a digitisation of his Figure 4 (about 11 weeks) and his Figure 9’s 4.9 for CAT, and the latch holds a tie at 2.5. - Variant: the current bar against the previous quarter,
h.GT(hh.Shift(b))andl.GT(ll.Shift(b)). - Evidence: his Figure 5 (S&P 100 stocks, January 2005 to December 2016, weekly): 707 trades and 87,553 dollars with a maximum drawdown of 19,897, against buy and hold’s 88,696 and 76,315. Evidence, not a reference.
- Pinned by
TestRecipeT6StarRating.
Hurst Exponent
Hurst
func (s Series) Hurst(n int) Series
Hurst estimates the Hurst exponent from the rescaled range (R/S) of each trailing window of n values.
For each window it takes the deviations of the n values from the window mean and forms their cumulative sums. R is the range of those cumulative sums (maximum minus minimum), S is the population standard deviation of the window (divisor n, not n−1), and the result is H = ln(R / (S·√(π/2))) / ln n. Indices 0 to n−2 are 0, as are windows where S is 0 or R/S is not a positive number (a flat window, for example, or one containing a NaN or an infinity), and every index is 0 when n <= 1, n > len(s) or len(s) <= 1.
A window whose n values are all equal and finite gives exactly 0, whatever the rounding in its mean: the test is exact equality (==) with the latest value, never a tolerance, so -0 and +0 count as equal, and a window holding a NaN or an infinity is never flat. A window whose values differ by one ulp, or a genuine walk at a tiny magnitude, is not flat and is computed as above. Releases before this change gave about 0.86 to 0.94 on a flat window at most price levels (n = 10 to 50): the computed mean of ten copies of 0.1 is just below 0.1, so every deviation was the same tiny number and R/S came out near n−1.
The factor √(π/2) is the large-sample (Feller/Hurst) constant: for independent data, E[R/S] ≈ √(nπ/2) as n grows. It is not an Anis-Lloyd correction; HurstCorrected uses Anis and Lloyd’s exact expected value instead.
This is a single-scale estimate at window n only. The classical method computes R/S at several window sizes and takes H as the slope of the regression of log(R/S) on log(n); that regression is not provided.
Because the large-sample constant overstates E[R/S] at small n, Hurst is biased low on independent data: in simulation, white noise gives a mean H of about 0.373 at n = 10, 0.427 at n = 20, 0.461 at n = 50, 0.475 at n = 100 and 0.482 at n = 200, where 0.5 is expected. Use HurstCorrected for an estimate centred near 0.5.
R/S analysis is normally applied to increments rather than to levels: for prices, apply Hurst to returns, not to the prices themselves.
HurstCorrected
func (s Series) HurstCorrected(n int) Series
HurstCorrected estimates the Hurst exponent like Hurst but compares R/S with its exact expected value under independence (Anis and Lloyd, 1976) rather than the large-sample constant, which removes most of Hurst’s downward bias on independent data.
For each trailing window of n values, R (the range of the cumulative deviations from the window mean) and S (the population standard deviation, divisor n, as Hurst uses) are computed exactly as in Hurst, and the result is H = 0.5 + ln((R/S) / E(n)) / ln n, where E(n) = Γ((n−1)/2) / (√π·Γ(n/2)) · Σ √((n−r)/r), summed over r = 1 to n−1, is Anis and Lloyd’s (1976) exact expected R/S for independent normal values, as given by Peters, Fractal Market Analysis (1994), eq. 5.4. The gamma ratio is computed by its recurrence in steps of two, not from the gamma function (which overflows near n = 343), so E(n) is finite for every n. E(n) is computed once per call.
Where both are non-zero, HurstCorrected(n) − Hurst(n) at the same index equals, to rounding, the constant 0.5 + (ln √(π/2) − ln E(n)) / ln n: about 0.117574 at n = 10, 0.065156 at n = 20, 0.032202 at n = 50 and 0.012129 at n = 200.
On white noise HurstCorrected averages about 0.491 at n = 10, rising to 0.495 at n = 200. The small negative bias that remains comes from centring R/S rather than ln(R/S): by Jensen’s inequality, the mean of ln(R/S) is below ln E(n).
Peters’s Table A2.1 prints E(n)·(n − 0.5)/n: the factor (n − 0.5)/n is his empirical correction for the sample standard deviation, so the table’s values are E(n) times that factor, not E(n) itself.
The flat-window rule is Hurst’s too: a window whose n values are all equal and finite gives exactly 0, whatever the rounding in its mean. Releases before this change gave about 0.97 on such a window at most price levels (n = 10 to 50), for example ten copies of 0.1.
Guards, warm-up and zero cases are those of Hurst: indices 0 to n−2 are 0, as are windows where S is 0 or R/S is not a positive number, and every index is 0 when n <= 1, n > len(s) or len(s) <= 1. As with Hurst, this is a single-scale estimate at window n only, and R/S analysis is normally applied to increments (returns, for prices) rather than to levels.
Structural Breaks
DickeyFuller
func (s Series) DickeyFuller(n int) Series
DickeyFuller returns the rolling Dickey-Fuller t-statistic of the lagged level in a regression of each change on a constant and the previous value over the last n lag pairs.
At bar t the n lag pairs are the regression rows i = t-n+1 .. t. Row i regresses the change s[i] - s[i-1] on a constant and the lagged level s[i-1], so a window of n pairs reads the n + 1 values s[t-n .. t]. With b the coefficient of the lagged level, Sxx the centred sum of squares of the lagged level and SSR the residual sum of squares, the statistic is t = b sqrt((n-2) Sxx/SSR), the multiplicative form of b/se with se = sqrt(SSR/(n-2)/Sxx). The statistic has n - 2 degrees of freedom: n rows less two coefficients. The coefficient b is OUTheta(n), bit for bit, and b + 1 is the slope of the level on its lag, so the statistic is also the t-statistic of that slope tested against 1. Its sign is the sign of OUTheta(n) wherever neither is 0.
DickeyFuller is built on the shared co-moment kernel behind OUTheta and SScore, with the residuals formed explicitly rather than by subtracting co-moments, which cancels near a perfect fit. The source is Dickey and Fuller (1979), Journal of the American Statistical Association 74, pages 427 to 431. The value at bar t is, in exact arithmetic, the test statistic of statsmodels adfuller(window, maxlag=0, regression=‘c’, autolag=None) on the n + 1 values s[t-n .. t]; the two agree to rounding on ordinary windows, while statsmodels, which fits the uncentred design, loses digits where the level is large against the moves (a level of 1e9 with unit moves) and this method does not. There are no lagged differences and no trend: the augmented and trend forms are not built. AFML picks the order d of FracDiff as the smallest d whose output passes its ADF test, which has one lagged difference, so this method does not reproduce that choice.
Pass log prices, c.Log().DickeyFuller(n). On raw prices a rejection of the unit root would imply stationary prices of finite variance, so return volatility would have to shrink as prices rise, while on log prices a rescaling of the price only shifts the series, which the constant absorbs (AFML, section 17.4.2.1, on raw and log prices). The method takes no logarithm and uses the receiver as given. Levels are legitimate input too (a spread, a yield): there is then no dependence on a logarithm and no NaN for non-positive values.
The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. Because 0 is also a legitimate value, mask the warm-up where it matters with DickeyFuller(n).SetN(n, math.NaN()). An n below 3 (three pairs leave one residual degree of freedom) or of at least len(s) gives len(s) zeros, and the minimum length of a series with a valid bar is n + 1.
A NaN or an infinity at bar p gives NaN from the later of p and n to the earlier of p+n and len(s)-1, exactly the windows s[t-n .. t] that hold it; later bars recover. The method forms no difference of input values, so a finite pair whose difference would overflow is not a NaN case. The kernel scales each window by a power of two first, so there is no limit on the magnitudes, upper or lower, but there is one on the dynamic range of a window s[t-n .. t], the ratio of the largest to the smallest nonzero difference between two of its values: finite inputs give finite, accurate outputs while that ratio stays below about 2^1000 (about 1e301). Beyond it the slope, rebuilt from the kernel’s scaled units, can overflow while the power of two that scales the root underflows, with no special rule, so the result can be NaN or an infinity: the series 0, 2^-700, 0, 2^400 gives NaN from DickeyFuller(3) at bar 3, where the statistic is about -0.577.
A window whose residual sum of squares is at most 1e-24 times the centred sum of squares of the levels it regresses (a perfect fit, or flat levels) gives 0, and so does a flat lagged level. Just outside that guard the value can be huge, far beyond any critical value, which marks a nearly deterministic window rather than a signal: a path converging geometrically to a level is a perfect fit up to the rounding of the stored values. While the largest move of a window exceeds about 1e6 times that rounding, the statistic here is very large, above 1e5, while SupADF gives 0; below about 1e5 times that rounding, SupADF gives nearly the same values as this method, and they shrink with the moves to ordinary sizes as the moves reach the rounding, where both methods read the noise.
The value at bar t reads only s[t-n .. t], so the method is causal, does not repaint and does not depend on where the history starts: a suffix s[k:] gives at bar t-k the full series’ value at bar t, for every t >= k + n, and a prefix gives the first values of the full series’ result, bit for bit. Scaling the series by a power of two, 2^k, for every k that keeps the values normal and finite, or negating it, leaves the result unchanged bit for bit (negation leaves every ADF-type statistic with a constant unchanged), and adding a constant changes it only by rounding. Inside SupADF’s narrower domain, SupADF(n, n) agrees with it to rounding, not bit for bit, except near a perfect fit, where the two guards differ. The bits are the same on every platform.
The period-4 pattern +1, +1, -1, -1 (repeating, in any phase, also scaled by a power of two) with n a multiple of 4 gives -sqrt(n-2) exactly at every valid bar, because the lagged level and the level both have mean 0 over whole periods, so their co-moment is 0 and b = -1. The powers of two s[i] = 2^-i are a perfect fit and give 0, as do a flat series and a strictly alternating two-price series.
In the bubble literature (see SupADF) the test is right-tailed: large positive values reject the unit root in favour of explosiveness. In the classical unit-root test the alternative is stationarity and the test is left-tailed: large negative values reject. Critical values depend on n, and none is quoted here.
The cost is O(n) per valid bar, one kernel fit and one residual pass. A call makes two allocations, the result and the kernel, for every length with a valid bar; an invalid n makes one, the result, and an empty series none. The result is a new series; the method reads s only, so concurrent calls on a shared input are safe.
Verification: two clean-room references implement the contract in testdata/gle257-wp32 on its 27 input series. A Python reference computes each window’s statistic exactly, in rational arithmetic with the square root to 50 digits (mpmath), and gives an error bound per bar; a Julia reference fits each window by direct least squares in binary64. The Julia fit and this method both lie within the Python bound at every compared bar (the knife edges of the guard and the bars beyond the domain are not compared), and match its zeros and NaN bars exactly. The identity with OUTheta, the period-4 anchor and the scaling and negation identities hold as stated, and the outputs are the same bytes natively (arm64) and as amd64 code. The adfuller of statsmodels is a cross-check only: it agrees to rounding on ordinary windows, but its uncentred design loses digits where the level is large against the moves.
SupADF
func (s Series) SupADF(minPairs, maxPairs int) Series
SupADF returns, at each bar, the largest Dickey-Fuller t-statistic over the windows of minPairs to maxPairs lag pairs that end at that bar, the bounded backward supremum ADF sequence.
A window of m lag pairs ending at bar t reads the m + 1 values s[t-m .. t] and has the statistic of DickeyFuller, t = b sqrt((m-2) Sxx/SSR), with b the coefficient of the lagged level in the regression of the change on a constant and the lagged level. Each window has m - 2 degrees of freedom: m rows less two coefficients. SupADF takes the largest of these statistics over m = minPairs .. maxPairs; the windows are visited from the shortest, the first defined value is kept on a tie, and the winning window is not returned (no argmax). A window with a flat lagged level or a perfect fit is undefined and is skipped, and the bar reads 0 when no window is defined. When minPairs and maxPairs both equal n there is one window, and inside the domain below SupADF agrees with DickeyFuller(n) to rounding, not bit for bit, except near a perfect fit, because the supremum accumulates each window in O(1) per row and its guard differs from that of DickeyFuller.
In the terms of Phillips, Shi and Yu (2015), PSY below, this is the backward SADF (BSADF) sequence, bounded to the last maxPairs pairs. It is AFML’s SADF_t (Advances in Financial Machine Learning, chapter 17, section 17.4.2), which follows Phillips, Wu and Yu (2011), PWY below, International Economic Review 52(1), with the start points t0 running from t - maxPairs to t - minPairs as price indices. AFML’s get_bsadf returns the key ‘gsadf’ for what PSY call the BSADF sequence. PWY’s SADF (a fixed start and an expanding end) and PSY’s GSADF (the supremum of BSADF over the end points) depend on where the history starts and are not methods. A rolling GSADF is the recipe SupADF(a, b).Highest(k), valid from bar b + k - 1; Highest carries a NaN for k bars, so a NaN of SupADF at bar j makes the recipe NaN from bar j to j+k-1.
minPairs and maxPairs count regression rows (lag pairs), so a window of m pairs uses m + 1 values. This is the unit of exuber’s minw and of AFML’s minSL at lag 0; psymonitor’s swindow0 counts prices, one more.
Pass log prices, c.Log().SupADF(minPairs, maxPairs). On raw prices a rejection of the unit root would imply stationary prices of finite variance, so return volatility would have to shrink as prices rise, while on log prices a rescaling of the price only shifts the series, which the constant absorbs (AFML, section 17.4.2.1, on raw and log prices). The method takes no logarithm and uses the receiver as given; levels (a spread, a yield) are legitimate input.
The statistic is right-tailed: large positive values mark explosive behaviour. It is unchanged by negation, so it responds to explosive falls as to rises.
A minPairs below 3, a maxPairs below minPairs, or a maxPairs of at least len(s) gives len(s) zeros; minPairs equal to maxPairs is valid and gives one window. The minimum length of a series with a valid bar is maxPairs + 1.
The warm-up reads 0, not NaN: bars 0 to maxPairs-1 are 0 and the first valid index is maxPairs. A partial supremum over the early bars, from minPairs, would depend on where the history starts and is not given. Because 0 is also a legitimate value, mask the warm-up where it matters with SupADF(minPairs, maxPairs).SetN(maxPairs, math.NaN()).
Row i of the regression is bad when s[i] - s[i-1] is not finite, which covers a non-finite s[i-1] or s[i] and a finite pair whose difference overflows. A bad row among the rows t-maxPairs+1 .. t gives NaN at bar t, so a shorter clean window does not win past a NaN. A non-finite s[p] makes rows p and p+1 bad and gives NaN from the later of p and maxPairs to the earlier of p+maxPairs and len(s)-1. A finite pair whose difference overflows makes row p bad only, with NaN from the later of p and maxPairs to the earlier of p+maxPairs-1 and len(s)-1. The bars p-1 and p+maxPairs, which read one huge value without a bad row, lie beyond the domain below and can be NaN or finite. Two adjacent non-finite values, at p and p+1, make three rows bad, with NaN up to the earlier of p+1+maxPairs and len(s)-1. Warm-up bars stay 0 even where the input is bad, and bars whose windows do not read the value are unchanged.
Finite inputs whose magnitudes (and so window ranges) stay below about 1e150 give finite outputs; beyond that the arithmetic decides, with no special rule. SupADF also has a lower end: it multiplies unscaled differences, so where some value of s[t-maxPairs+1 .. t] differs from s[t-maxPairs] by a nonzero amount below about 2^-500 its accuracy is not assured. DickeyFuller scales each window by a power of two first and does not need that limit. SupADF is also accurate only while the dynamic range of each window s[t-maxPairs .. t], the ratio of the largest to the smallest nonzero difference between two of its values, stays below about 2^500 (about 3e150), against about 2^1000 for DickeyFuller. Beyond it the ratio Sxx/SSR under the square root becomes subnormal and then 0, again with no special rule, so a bar can read 0 or a wrong value: the series 0, 2^-300, 0, 2^300 gives 0 from SupADF(3, 3) at bar 3, where the statistic is about -0.577.
A window is undefined when its lagged level is flat or when its residual sum of squares, formed as Sdd - b Sxd, is at most 1e-10 times Sdd, the centred sum of squares of the changes (a perfect fit, a flat change, or a negative value from rounding). This is far looser than the 1e-24 of DickeyFuller, whose guard is relative to the levels and uses explicit residuals. The subtraction form has a rounding floor that grows with the window length and lies far above that of explicit residuals, so a threshold as tight as 1e-24 would let a perfect fit through as an enormous statistic, and a strictly alternating two-price window (a bid-ask bounce on illiquid ticks) is exactly such a fit. The two guards differ, so a window can lie inside one and outside the other.
The windows at bar t share one anchor, the lagged level s[t-1] of the last row, and the rows are added from t backwards, one Welford step each, so after m rows the accumulator holds the window of the m pairs ending at t. The value of a window therefore does not depend on minPairs, maxPairs or the bars before s[t-maxPairs], and these identities hold bit for bit. For any j from minPairs to maxPairs-1, SupADF(minPairs, maxPairs) at a bar t >= maxPairs is the larger of SupADF(minPairs, j) and SupADF(j+1, maxPairs), and NaN where either is NaN, provided each part has at least one defined window. Where the narrower range has a defined window, the value does not fall as maxPairs grows and does not rise as minPairs grows. A part in which every window is undefined (a stretch of strictly alternating prices, say) reads 0 and contributes nothing to the whole, so a longer range that reaches back past the stretch can be negative where the shorter one reads 0.
The value at bar t reads only s[t-maxPairs .. t]. It is window-local and start-invariant: a suffix s[k:] gives at bar t-k the full series’ value at bar t, for every t >= k + maxPairs; a prefix gives the first values of the full series’ result; and the method is causal and does not repaint. Scaling the series by a power of two, 2^k, for every k that keeps the values and the products of their differences normal and finite (the domain above), or negating it, leaves the result unchanged bit for bit, and adding a constant changes it only by rounding. The period-4 pattern +1, +1, -1, -1, with minPairs and maxPairs both equal to a multiple n of 4, gives values within 1e-12 of -sqrt(n-2). The powers of two s[i] = 2^-i, a flat series and a strictly alternating two-price series have only undefined or perfect-fit windows, which SupADF skips, so they give 0.
No critical values are shipped: they depend on minPairs, maxPairs and the null. To simulate them, generate many series, each the cumulative sum of maxPairs + 1 independent N(0, 1) draws (the shortest series with a valid bar; the starting level does not matter, because the constant absorbs it), take the last value of SupADF(minPairs, maxPairs) for each series and read the percentile you need, for example the 95th. The Monte Carlo critical values of exuber and the bootstrap of psymonitor are the published tools. For minPairs, the minimum-window rule of Phillips, Shi and Yu, floor((0.01 + 1.8/sqrt(T))*T) with T the sample length, is guidance: T = 252 gives 31, so SupADF(31, 252) on daily log prices (exuber’s psy_minw computes the same rule).
The cost is O(maxPairs) per bar: maxPairs accumulator steps and up to maxPairs - minPairs + 1 square roots, which is 252 steps a bar at maxPairs = 252, instead of a fresh fit of every window. A call allocates only the result, a new series; the method reads s only, so concurrent calls on a shared input are safe. The bits are the same on every platform, because every product that meets an addition is rounded on its own. Not built: lagged differences (lag 0 only), trends, the QADF and CADF views, and PWY’s SADF and the GSADF as methods.
Verification: the two clean-room references of DickeyFuller (testdata/gle257-wp32) cover SupADF too. The Julia reference also transcribes line by line the binary64 program the contract pins for SupADF, and this method equals that transcription at every cell, bit for bit, NaN matching NaN. The transcription, the Julia direct least squares and this method all lie within the Python reference’s error bound at every compared bar (the knife edges of the guard and the bars beyond the domain at either end are not compared). The decomposition, the period-4 anchor and the scaling and negation identities hold as stated, and the outputs are the same bytes natively (arm64) and as amd64 code.