Ehlers and cycle indicators

Ehlers and cycle indicators

All functions · nseries package

Sections: Ehlers Indicators · Endpoint FFT

Ehlers Indicators

Departures from Ehlers’ published code

The Ehlers indicators follow the TradeStation code John Ehlers published, unless a defect justifies a departure. Each departure is stated in the method’s own doc comment, with what changed and what releases before the change did. This section discusses two that change a published recurrence for valid parameters: DSMA’s alpha (GLE-336) and DSSS on a zero scaled filter (GLE-335), both from 5 October 2026. The others are listed at the end, among them VIDYA’s constant, capped at 1 as DSMA’s alpha is (GLE-519, 16 October 2026).

DSMA: alpha is capped at 1 (GLE-336)

Ehlers’ deviation-scaled moving average (TASC, July 2018) ends with

ScaledFilt = Filt / RMS          (RMS over the last n values of Filt, the current one included)
alpha1     = |ScaledFilt| * 5 / n
DSMA       = alpha1 * Close + (1 - alpha1) * DSMA[1]

The last line is an exponential moving average. Its behaviour depends on alpha1:

  • 0 <= alpha1 <= 1: a true average. Each output lies between the previous output and the price.
  • 1 < alpha1 < 2: stable, but it overshoots: the “average” lands beyond the price.
  • alpha1 >= 2: unstable. Errors grow on every bar.

Because the RMS window holds the current filter value, |ScaledFilt| is at most sqrt(n), so alpha1 is at most 5/sqrt(n):

  • n of 26 or more: alpha1 stays below 1, so the published recurrence is always an average. At Ehlers’ default of 40 the bound is 0.79, which is why the problem does not show in his examples.
  • n from 7 to 24: alpha1 can lie between 1 and 2, so the output can overshoot. After a step from 100 to 110, the published DSMA reads 111.18 at n = 20 and 115.81 at n = 10.
  • n of 6 or less: alpha1 can reach 2 or more, so the recursion can diverge. A repeating pattern of eight prices takes DSMA(4) to -5.8e14 within 800 bars, and at n = 3 a step from 100 to 110 sends the output to 128.87 on the next bar, then as high as 153.4 and as low as 76.5.

nseries caps alpha1 at 1, so the output always lies between its previous value and the price. At the first valid bar, the previous value is the price seed. Ehlers himself describes alpha as “a number that can vary between zero and 1”, and his MAMA bounds its alpha the same way (FastLimit). Three of nine published ports read for this change clamp at 1 or 0.99; the other six, and Ehlers’ own code, do not.

What stays the same:

  • n of 26 or more: every output is unchanged, unless the squares of the filter values underflow (magnitudes below about 1e-154).
  • n of 25 or less: every bar before the first bar whose alpha1 would exceed 1 is unchanged. After it, the difference decays with the average.

Three alternatives were considered and rejected:

  • A cap just below 2 stops the divergence but keeps the overshoot, and has no precedent.
  • Making n below 7 invalid also stops the divergence but keeps the overshoot.
  • Documenting the behaviour would leave an accepted n diverging.

The cap does not depend on which DSMA edition is followed, a question still open in RECTIFICATION-PLAN C02. Both editions share the alpha formula and its bound.

Comparison with TradeStation. TradeStation runs Ehlers’ uncapped code. On the 2023 AAPL daily closes, DSMA(10)’s alpha1 is 1.17, 1.14 and 1.05 on bars 103 to 105. There TradeStation’s average lands beyond the price, while nseries reads the price itself:

BarPriceTradeStationnseriesDifference
102177.79177.86177.860.00
103179.46179.73 (above the price)179.460.27
104177.45177.12 (below the price)177.450.33
105176.57176.54 (below the price)176.570.03
106174.00174.18174.180.00

A capped bar discards the previous value, so the two agree again, to two decimals, from bar 106. The test that compares DSMA(10) with the TradeStation export therefore checks two things:

  • Agreement elsewhere: nseries matches the export to two decimals on every bar of bars 100 to 199 except 103 to 105.
  • The capped bars: on bars 103 to 105, the export lies outside the range from its previous value to the price, and nseries equals the price. If you compare nseries with TradeStation for n below 25, expect such bars wherever alpha1 would exceed 1.

DSSS: the nominal period on a zero scaled filter (GLE-335)

DSSS, Ehlers’ deviation-scaled super smoother, is a SuperSmoother whose critical period is in effect n / |ScaledFilt|:

a1   = exp(-sqrt(2)*pi*|ScaledFilt| / n)
b1   = 2*a1*cos(sqrt(2)*pi*|ScaledFilt| / n)
c2 = b1;  c3 = -a1*a1;  c1 = 1 - c2 - c3
DSSS = c1*(Close + Close[1])/2 + c2*DSSS[1] + c3*DSSS[2]

When ScaledFilt is exactly 0, the period is infinite and the coefficients become a1 = 1, c1 = 0, c2 = 2 and c3 = -1. The recursion is then DSSS = 2*DSSS[1] - DSSS[2]: it ignores the price and carries its last slope for ever in a straight line. ScaledFilt is exactly 0 in three situations:

  • Flat stretches: on an exactly flat stretch of at least n + floor(n/1.4) bars. A ramp from 100 to 119.75 followed by 200 flat bars at 120 ended at 140.11 for n = 2, still rising. A step to 120 drifted up by 10 a bar, reaching 2,500 within 240 bars.
  • Zero momentum: wherever the n-bar momentum is 0 across the whole Hann window. On prices alternating 101 and 99, DSSS(2) ran 103, 105, … 203.
  • n of 50 or more: on the first computed bar, whose Hann window holds only the warm-up’s zero momenta.

On a series where 40% of closes repeat the previous one, as illiquid or intraday data can, ScaledFilt was exactly 0 on 16% of DSSS(2)’s bars.

nseries gives such a bar the nominal period n, the period DSSS uses when the deviation is one standard deviation, so no deviation information means the neutral period. The step uses SuperSmoother(n)’s coefficients in the exact form

DSSS = DSSS[1] + c1*(m - DSSS[1]) - c3*(DSSS[1] - DSSS[2])

with m the mean of the two prices. That is algebraically the same recursion, but a constant series gives its level exactly. Ehlers’ adaptive SuperSmoother (TASC, September 2026) maps a zero deviation to the nominal period in the same way. The one other DSSS port found does too, and its remarks describe this defect.

The two-price mean is the published (Close + Close[1])/2, unless that sum overflows. Then each price is halved first, so a constant at the largest float64 gives its level rather than NaN.

Two alternatives were considered and rejected:

  • Holding the last value is bounded but freezes the output away from the price: it holds 101 for ever on the 101/99 input, whose mean is 100.
  • A floor on |ScaledFilt| (a cap on the period) either changes about 8% of ordinary bars or does almost nothing.

The branch is taken when ScaledFilt is exactly 0, not when the computed c1 rounds to 0. Whether c1 rounds to 0 depends on whether the platform fuses multiply-adds: on one test series, arm64 and amd64 would have taken different branches and ended 0.27 apart.

What stays the same:

  • Non-zero ScaledFilt: every bar on which ScaledFilt is not exactly 0 is computed as before, bit for bit.
  • n of 50 or more: the first computed bar moves (by up to 0.42 on the 2023 AAPL closes), and the difference fades over about n bars.
  • Tiny but non-zero ScaledFilt: such a bar still takes the published path, and can carry the slope for a few bars. On alternating input at n = 5, ScaledFilt is about 1e-15 on 7 bars. This is bounded and rare, and a period floor to remove it would change ordinary bars.

TradeStation’s DSSS(10) export on the AAPL closes is unaffected: ScaledFilt is never exactly 0 there.

Other departures

MethodDepartureReason
DSMAn below 3 is invalid and gives zeros (GLE-310)The band-pass is unstable for n of 1 or 2
DSMAThe average starts at bar 2n-1 from the price (GLE-308)The published start averaged warm-up zeros
DSSSn below 2 is invalid and gives zeros (GLE-331)With no Hann taps the filter is 0 on every bar
DSSSThe recursion starts from the price (GLE-296)The published start ramped up from 0
SuperSmoother, UltimateSmoother, AdaptiveSuperSmootherThe recursion is written in exact form (GLE-340)The published form drifts by rounding on a constant
Laguerre, LaguerreRSIThe stage update is written in exact form (GLE-307)The published form read rounding noise on flat stretches
MAMA, FAMAThe recursion starts at bar 12 and the output at bar 32, not at bar 5 (Ehlers’s CurrentBar > 5) (GLE-257 WP1.4)They follow TA-Lib’s MAMA, whose values nseries reproduces; pandas-ta also calls TA-Lib’s MAMA when TA-Lib is installed
MAMA, FAMAThe phase is 0 on a bar where I1 is 0, not held from the previous bar (GLE-257 WP1.4)TA-Lib’s MAMA does the same
SyntheticOscillator, SyntheticOscillatorPeriodThe dominant cycle uses 2*math.Pi in place of the printed 6.28, which makes the unclamped DC larger by the factor 2*pi/6.28 = 1.000507 (GLE-257 WP3.6)The package already writes math.Sqrt(2) and math.Pi for Ehlers’s 1.414 and 3.14159
VIDYAThe constant 0.2sd_short/sd_long is capped at 1, so the average stays between its previous value and the price; the cap can act only when long >= 25short (GLE-519)Chande requires the constant to be “less than 1 for the term (1-t) to be positive”; Ehlers’s listing has no cap, and after 118 bars at 100, then 95 and 105, the uncapped VIDYA(2, 100) read 94.97 and 109.15

AdaptiveSuperSmoother

func (s Series) AdaptiveSuperSmoother(n int, rmsLength ...int) Series

AdaptiveSuperSmoother implements the adaptive SuperSmoother according to the translation of the TradeStation algorithm provided by John Ehlers (in TASC September 2026). Rather than modulating the alpha of an EMA, as KAMA, VIDYA and their relatives do, the critical period of a second-order SuperSmoother is tuned on every bar: the one-bar rate of change of the smoothed output is divided by its root mean square over the trailing “rmsLength” bars (scaling it in standard deviations), clamped at two, and the period is then set to n*(1 - roc/2)*(1 - roc/2) with a floor of two bars. The period computed at the end of one bar is the one used to smooth the next, so the tuned period is never longer than n and the filter speeds up whenever the smoothed output is moving quickly.

Optionally, the length of the RMS window can be defined. If not defined, or not positive, it will default to 81, the value used by Ehlers in the article, who remarks that the length over which the RMS is taken is not particularly critical. The RMS is zero until its window is full, so the period is held at n and the first max(3, rmsLength) bars are identical to SuperSmoother(n). A window longer than the series simply never adapts.

Two deliberate deviations from the published code: the filter constants are math.Sqrt(2)*math.Pi rather than Ehlers’ 1.414 and 3.14159, so that this filter agrees bit for bit with the package’s SuperSmoother; and the rate of change on the first bar is zero rather than the whole first price (an artefact of the EasyLanguage variable initialiser), matching the package’s Delta convention.

The SuperSmoother step is written in the exact form SuperSmoother uses, with its overflow-safe mean and its fallback to the published form, so a constant series gives its level exactly; releases before this change used the published form. Because the tuned period feeds the smoothed output back into the filter, a rounding difference can grow over a long series: in the published form, changing one price of a 2,000-bar random walk by one part in 10^12 moved later outputs of AdaptiveSuperSmoother(40, 81) by about 10%. So the change of form moves outputs by rounding at first, and on long series by more, as a change of platform (arm64 against amd64) can.

A value of n less than one returns an aligned zero-filled series rather than panicking. A value of n of one is floored to a period of two bars on every bar, so its output equals SuperSmoother(2). The first three bars are copied from the input, as in SuperSmoother.

Unlike the older indicators in this package, this one is not verified against a column exported from TradeStation. Its reference values come from an independent translation of the published EasyLanguage into Python, reproduced by a second independent translation, and are anchored to the existing SuperSmoother: until the RMS window fills, the adaptive filter is arithmetically the fixed-period one, and the tests assert that the two agree bit for bit across that region. That identity is what ties these values to the TradeStation-verified ones already in the package, without requiring a further export.

AdaptiveSuperSmootherOsc

func (s Series) AdaptiveSuperSmootherOsc(n int, rmsLength ...int) Series

AdaptiveSuperSmootherOsc implements the oscillator presentation of the adaptive SuperSmoother according to the translation of the TradeStation algorithm provided by John Ehlers (in TASC September 2026). It is the adaptive SuperSmoother minus the fixed-period SuperSmoother(n), the ordinary fast-minus-slow convention, so that a value above the zero reference implies that a long position is preferred and a value below it implies that a short position is preferred. Ehlers notes that the cyclic turning points of the oscillator almost perfectly align with the cyclic price turning points.

Optionally, the length of the RMS window can be defined, as for AdaptiveSuperSmoother. If not defined, or not positive, it will default to 81. The oscillator is exactly zero until the RMS window is full, and on every bar it is exactly AdaptiveSuperSmoother(n, rmsLength) minus SuperSmoother(n).

A value of n less than one returns an aligned zero-filled series rather than panicking.

AdaptiveSuperSmootherPeriod

func (s Series) AdaptiveSuperSmootherPeriod(n int, rmsLength ...int) Series

AdaptiveSuperSmootherPeriod returns the tuned critical period of the adaptive SuperSmoother according to the translation of the TradeStation algorithm provided by John Ehlers (in TASC September 2026). The value at bar i is the period computed at the end of bar i, i.e. the one used to smooth bar i+1. It is always finite and lies within [2, max(2, n)], even when the input contains NaN or infinities: it is exactly n while the RMS window is still filling, and it falls to as little as two bars when the rate of change of the smoothed output reaches two standard deviations.

Note that quiet data does not reliably restore the period to n. The scaled rate of change is only updated on bars where the RMS is positive, so once the RMS window holds nothing but zero changes it keeps its last value, as the published code does; and while the smoothed output is still settling on a flat stretch, its tiny changes are measured against an equally tiny RMS, so the period can still take any value in its range. On a series that is constant from the start the smoothed output is exactly the level, every change is 0, and the period is exactly max(n, 2) on every bar; releases before this change smoothed in the published form, whose rounding residue was amplified into a period anywhere within its range.

Optionally, the length of the RMS window can be defined, as for AdaptiveSuperSmoother. If not defined, or not positive, it will default to 81.

A value of n less than one returns an aligned zero-filled series, not a series of n, rather than panicking.

Angle

func (s Series) Angle() Series

Angle implements the angle function according to the translation of the TradeStation algorithm provided by John Ehlers.

The warm-up reads 0, not NaN: bars 0 and 1 are 0, as the peak trackers start at 0.

AutoTune

func (s Series) AutoTune(window int, bandwidth ...float64) Series

AutoTune implements the AutoTune filter according to the translation of the TradeStation algorithm provided by John Ehlers (in TASC May 2026). The data is detrended with a two-pole highpass filter whose critical period is “window” bars, and a rolling autocorrelation of the filtered data (over “window” bars, for lags of 1 to “window” bars) measures the dominant cycle, which is twice the lag of the most negative autocorrelation. The dominant cycle then tunes a bandpass filter on a bar-by-bar basis. The result is a smooth, nominally zero-mean waveform whose peaks and valleys identify the peaks and valleys in the price data for mean-reversion strategies.

Optionally, the bandwidth of the bandpass filter can be defined (as a fraction of its centre frequency). If not defined, it will default to 0.25, the value used by Ehlers in the article.

A window for which the required history is unavailable, including an extreme window value that would otherwise overflow internal bounds checks, returns an aligned zero-filled series rather than panicking.

bandwidth is a fraction of the centre frequency, defaulting to 0.25 when it is omitted. A given bandwidth must be finite and strictly between 0 and 0.5, below a quarter of the shortest dominant cycle the measurement reports (2 bars): from 0.5 up the bandpass is unstable on any bar whose dominant cycle is below four times the bandwidth. 0, a negative value, NaN, an infinity or 0.5 and above (including a percentage such as 25) is invalid and gives all zeros. The warm-up reads 0, not NaN: bars 0 to max(2, 2*window-1)-1 are 0.

AutoTuneDC

func (s Series) AutoTuneDC(window int) Series

AutoTuneDC returns the dominant cycle period measured by the rolling autocorrelation used in the AutoTune filter (see AutoTune). The dominant cycle is twice the lag at which the autocorrelation of the highpass-filtered data is most negative, and is limited to change by no more than 2 bars from one bar to the next. HilbertPeriod measures the dominant cycle with Ehlers’s Hilbert transform and homodyne discriminator instead, as TA-Lib’s HT_DCPERIOD does.

The warm-up reads 0, not NaN: bars 0 to 2*window-2 are 0, and the dominant cycle then ramps up from 0 by at most 2 bars per bar, so the first values after the warm-up are not a measured cycle.

The ramp is the source’s own (Ehlers seeds the cycle at 0 and limits each step to 2 bars): it lasts until the cycle reaches the measured value, at most window bars after bar 2*window-1. AutoTune’s band-pass carries its own memory for longer (about 120 to 160 bars to within 1% at window 20).

AutoTuneMinCorr

func (s Series) AutoTuneMinCorr(window int) Series

AutoTuneMinCorr returns the minimum value (across lags of 1 to window bars) of the rolling autocorrelation used in the AutoTune filter (see AutoTune). Values near -1 indicate a strong dominant cycle; Ehlers’ pro forma strategy only takes trades when this value is below a threshold (e.g. -0.22).

The warm-up reads 0, not NaN: bars 0 to 2*window-2 are 0, which is above the -0.22 gate, so no trade fires on them.

BandPass

func (s Series) BandPass(n int, bandwidth float64) Series

BandPass implements the band-pass filter according to the translation of the TradeStation algorithm provided by John Ehlers.

bandwidth is a fraction of the centre frequency 1/n, so 0.3 is 30%, not a percentage. The warm-up reads 0, not NaN: bars 0 and 1 are 0 and the recursion starts from zero history; the output is zero-mean.

ContinuationIndex

func (s Series) ContinuationIndex(n int, gamma float64, order int) Series

ContinuationIndex implements Ehlers’ continuation index from TASC Sep 2025. A good starting point for the length (n) is to use the desired number of bars to be in a trade.

gamma is the Laguerre damping factor between 0 and 1 and is not validated. The warm-up reads 0, not NaN: bars 0 to 2n-3 are 0 and the first valid index is 2n-2, where the Laguerre filter (which starts at bar n-1) has run for a full n-bar averaging window. On a constant series the index is exactly 0 from there. Releases before this change gave about plus or minus 0.964 (tanh(2), with the sign of the price) before bar n-1 and then stayed near plus or minus 0.95 while zero-seeded Laguerre stages caught up (about 300 bars on a flat series at n = 40), which reads as strong continuation.

DSMA

func (s Series) DSMA(n int) Series

DSMA calculates the deviation-scaled moving average according to the translation of the TradeStation algorithm provided by John Ehlers. TODO – Revisit this to deal with de-gapping (which we have left out).

The warm-up reads 0, not NaN: bars 0 to 2n-2 are 0 and the first valid index is 2n-1, the first bar whose n-bar RMS window holds only computed filter values; when 2n-1 is at or past the end, every bar is 0. The average starts there from the price of bar 2n-2, so a constant series gives its level exactly from bar 2n-1. Releases before this change started the average from 0 at bar n, while the RMS window still held n-1 zeros, so the first value was about 5/sqrt(n) times the price (2.24 times at n = 5) and settled over several multiples of n bars, and a constant input gave 0 on every bar. The choice of DSMA edition is still open (RECTIFICATION-PLAN C02).

n must be at least 3. For n of 1 or 2 the band-pass’s g1 = cos(1.4*pi/n) is negative, so its recursion is unstable and the output grows without bound (to about 6e62 on the 2023 AAPL daily bars at n = 2); a smaller n is invalid and gives all zeros.

alpha1 is capped at 1. The RMS window holds the current filter value, so |scaledFilt| is at most sqrt(n) and alpha1 at most 5/sqrt(n): above 1 for n below 25, where the published recurrence places the average beyond the price (a step after a flat stretch overshoots by 12% of the step at n = 20 and by 58% at n = 10), and above 2 for n below 7, where the recursion can diverge (a repeating pattern of eight prices took DSMA(4) to -5.8e14 in 800 bars). With the cap the average always lies between its previous value (at the first valid bar, the price seed) and the price, as an EMA does and as Ehlers describes alpha (“a number that can vary between zero and 1”); MAMA bounds its alpha the same way. For n of 26 or more no bar can reach the cap and the output is unchanged, unless the squares of the filter values underflow (filter values below about 1e-154 in magnitude), which can make the RMS too small; at n = 25 the cap is reached only by an exact impulse and changes nothing beyond rounding. Releases before this change did not cap alpha1.

DSSS

func (s Series) DSSS(n int) Series

DSSS calculates the deviation-scaled super smoother according to the translation of the TradeStation algorithm provided by John Ehlers. TODO – Revisit this to deal with de-gapping (which we have left out).

The warm-up reads 0, not NaN: bars 0 to max(n-1, 49)-1 are 0, the RMS length being fixed at 50. The two-pole recursion starts from the prices of the two bars before its first output, the filter’s steady state (as SuperSmoother is seeded), so a constant series gives its level exactly from bar max(n-1, 49). Releases before this change started it from 0, so a ramp that could overshoot followed, and a constant input gave 0 on every bar.

n must be at least 2. For n = 1 the Hann window has floor(1/1.4) = 0 taps, so the filter is 0 on every bar and the smoother would never adapt; a smaller n is invalid and gives all zeros.

When the deviation-scaled filter is exactly 0 the smoother takes its nominal period n, the period it uses at a deviation of one standard deviation. The published coefficients have a double pole at 1 there (a1 = 1, so c1 = 0, c2 = 2 and c3 = -1) and the recursion r[i] = 2r[i-1] - r[i-2] carries its last slope for ever and ignores the input. The filter is exactly 0 on a stretch that has been flat for n + floor(n/1.4) bars, wherever the n-bar momentum is 0 across the whole Hann window (prices alternating 101 and 99 with n = 2) and, for n of 50 or more, on the first computed bar, whose Hann window holds only the warm-up’s zero momenta. On such a bar the output is r[i-1] + c1*(m - r[i-1]) - c3*(r[i-1] - r[i-2]) with SuperSmoother(n)’s coefficients and m the mean of the current and previous prices, a form that is exact when the three values are equal, so a constant series still gives its level exactly; every other bar is computed as before. Releases before this change kept the last slope on those bars: a ramp from 100 to 119.75 over 80 bars followed by 200 bars at 120 ended at 140.11 for n = 2 and 119.32 for n = 20, still moving, and 100 prices alternating 101 and 99 took DSSS(2) from 103 at bar 49 to 203 at bar 99.

The mean of the two prices is (s[i]+s[i-1])/2, as published, unless that sum overflows, when it is formed as s[i]/2 + s[i-1]/2; so a constant series at the largest finite float64 gives that value rather than NaN. A series that is not constant and lies within a factor of about 2 of the largest float64 still overflows the recursion’s history terms, as SuperSmoother does.

EhlersLoop

func (s Series) EhlersLoop(hpLen, ssLen int) Series

EhlersLoop returns one leg of an Ehlers Loop, the roofing-filtered series divided by its exponentially averaged root mean square, in standard deviations.

The filter stage is Ehlers’s roofing filter, f = s.HighPass(hpLen).SuperSmoother(ssLen): a two-pole high-pass filter that attenuates cycles longer than hpLen bars, then a super smoother that attenuates cycles shorter than ssLen bars. The mean square MS is an exponential moving average of ff with coefficient 0.0242, which Ehlers calls “a critical period of one year” on daily bars: by his one-pole formula, alpha = (cos w + sin w - 1)/cos w, it has a critical period of about 256 bars, and as alpha = 2/(N+1) it is that of an N of about 82 bars. MS starts at MS[0] = f[0]f[0] and follows MS[t] = 0.0242f[t]f[t] + 0.9758MS[t-1], with each product rounded on its own. The output is f[t] divided by the square root of MS[t], and 0 where the mean square is 0, as it is before the first non-zero f. At extreme price scales the output degenerates to 0: near 1e-170, ff underflows and MS stays 0; near 1e157, MS overflows to +Inf and every later output is 0. Rescale such inputs first (scaling by a power of two leaves the output unchanged). The result has len(s).

John Ehlers published the loop in “Ehlers Loops, Part 1”, Stocks & Commodities 40:7, June 2022, and its use for pairs in “Pairs Rotation With Ehlers Loops, Part 2”, Stocks & Commodities 40:8, July 2022.

Departures from the published code: Ehlers’s EasyLanguage zeroes both filters while CurrentBar < 3 and computes their coefficients with 1.414, pi as 3.14159 and degrees, whereas the library’s HighPass is zero through index 2 and never reads s[0], its SuperSmoother passes those zeros through, and both use math.Sqrt(2), math.Pi and radians. The two differ by a transient that decays as the filters settle. His code also holds the previous value where the mean square is 0, which before the first non-zero f is 0 as well.

The output is in standard deviations centred on 0, and according to Ehlers a move beyond one or two standard deviations carries a high probability of a reversal. The first non-zero output, at the first non-zero f (index 3 on generic data), is about ±6.43, the reciprocal of the square root of 0.0242. The first valid index is 0 and the minimum length is 1, but the output is meaningful only after a burn-in of about 450 bars at the defaults. That is about 250 bars of high-pass transient and about 200 bars of mean-square burn-in. The method is recursive and causal (the output on s[:k] is the first k outputs on s): a NaN or an infinity in s at index p > 0 makes every output from index max(p, 3) on NaN, permanently, and s[0] affects no output. It is not start-invariant.

Ehlers’s defaults are HPPeriod = 125 and LPPeriod = 20, that is, hpLen = 125 and ssLen = 20, and his guidance is that the difference of the two periods should be at least 20% of their average. Any hpLen and ssLen of at least 1 is valid, however large, though for a very large ssLen (from about 1e9 on some platforms) the super smoother’s coefficient cancels to 0 and the output is all zeros; if either is below 1, the result is all zeros.

For a pair, plot s2.EhlersLoop on the horizontal axis and s.EhlersLoop on the vertical, with s2 the reference (for example SPY) and s the stock. Ehlers is long the stock when the move up is the strongest condition and long the reference when the move to the right is. LoopRotation reduces the path of the loop to one signed series.

Verification: no exact identity with an existing method covers the mean square and the normalisation, so the alternative oracle stands in: a Python reference and a Julia reference, each translated independently from this definition, agree with this method on reference data. The filter stage is pinned by an identity with HighPass and SuperSmoother: normalising s.HighPass(hpLen).SuperSmoother(ssLen) by the recursion above reproduces this method bit for bit.

EMAT

func (s Series) EMAT(n int) Series

EMAT calculates the truncated EMA according to the translation of the TradeStation algorithm provided by John Ehlers.

The warm-up reads 0, not NaN: bars 0 to n-2 are 0, a price level at 0.

FAMA

func (s Series) FAMA(fastLimit, slowLimit float64) Series

FAMA returns Ehlers’s following adaptive moving average of the receiver, in price units: the second output of TA-Lib’s MAMA (TA_MAMA). It is an exponential average of MAMA that uses half the smoothing constant of MAMA. The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price, h.Add(l).Div(2).

The smoothing constant alpha of MAMA is fastLimit divided by the phase advance per bar in degrees, the previous bar’s atan(Q1/I1) minus this bar’s, held between slowLimit and fastLimit; an advance of one degree or less, which includes the snapback of the arctangent every half cycle, gives fastLimit. Ehlers suggests 0.5 for fastLimit and 0.05 for slowLimit, the defaults of TA-Lib. The parameters are valid if both are finite and 0 < slowLimit <= fastLimit <= 1; otherwise FAMA returns aligned zeros (one +0 per bar), whatever the input. Both averages start from a zero seed, so the first values are pulled towards 0, as in TA-Lib. Ehlers’s crossover signal is MAMA crossing FAMA.

The recursion starts at bar 12 as in TA-Lib, rather than at Ehlers’s bar 5, and the phase is 0 rather than held when I1 is 0; both are departures from Ehlers’s printed code.

The warm-up reads 0, not NaN: bars 0 to 31 are 0 and the first valid index is 32. The early values carry a start-up transient from the zero state. The method is recursive, so a slice of the history that starts later gives different values until the start-up transient has decayed.

Non-finite values do not recover. If a price is NaN or infinite, or a value of the recursion or either average becomes NaN or infinite, every bar from that bar on, but not before the first valid index, is NaN; earlier bars are unchanged. Inputs large enough to overflow the products in the discriminator therefore give NaN.

In practice the values agree with the current TA-Lib C (release 0.8.1) to about 1e-15 times the price level (about 1e-13 at prices near 100), and bit for bit given the same atan, sin and cos; TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “MESA Adaptive Moving Averages”, Technical Analysis of Stocks & Commodities, September 2001; TA-Lib TA_MAMA.

FRAMA

func (s Series) FRAMA(h, l, c Series, n int) Series

FRAMA calculates the fractal EMA according to the translation of the TradeStation algorithm provided by John Ehlers. The October 2005 TradeStation dimension persists when a range term is not positive. This library uses close prices and seeds pre-window values from c. Source: https://traders.com/documentation/feedbk_docs/2005/10/TradersTips/TradersTips.html

n must be even, as Ehlers’s source requires; an odd n returns a zero-filled series.

Hann

func (s Series) Hann(n int) Series

Hann calculates the Hann windowed low-pass FIR filter according to the translation of the TradeStation algorithm provided by John Ehlers. A value of n less than one returns a zero-filled series.

The warm-up reads 0, not NaN: bars 0 to n-2 are 0, a price level at 0.

HighPass

func (s Series) HighPass(n int) Series

HighPass implements the two-pole high-pass filter according to the translation of the TradeStation (EasyLanguage) function provided by John Ehlers in “The Ultimate Smoother”, TASC April 2024.

The first three outputs (indices 0 to 2) are zero, and the recursion begins at index 3 with zero history, as in the printed function (CurrentBar < 4). This corrects the earlier start at index 2, which carried a decaying start-up artefact into later values.

The coefficients use math.Sqrt(2), math.Pi and radians in place of the printed 1.414, 3.14159 and degrees.

n is the critical period in bars; a value of n less than one returns a zero-filled series.

HilbertInPhase

func (s Series) HilbertInPhase() Series

HilbertInPhase returns the in-phase component I1 of John Ehlers’s Hilbert transform of the receiver, in price units: the detrended price three bars back. It is the first output of TA-Lib’s HT_PHASOR (TA_HT_PHASOR); HilbertQuadrature returns the second. The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price, h.Add(l).Div(2).

The warm-up reads 0, not NaN: bars 0 to 31 are 0 and the first valid index is 32. The recursion starts at bar 12 as in TA-Lib, so the early values carry a start-up transient from the zero state. The method is recursive, so a slice of the history that starts later gives different values until the start-up transient has decayed.

Non-finite values do not recover. If a price is NaN or infinite, or a value of the recursion becomes NaN or infinite, every bar from that bar on, but not before the first valid index, is NaN; earlier bars are unchanged. Inputs large enough to overflow the products in the discriminator therefore give NaN.

In practice the values agree with the current TA-Lib C (release 0.8.1) to about 1e-15 times the price level (about 1e-13 at prices near 100), and bit for bit given the same atan, sin and cos; TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “MESA Adaptive Moving Averages”, Technical Analysis of Stocks & Commodities, September 2001; TA-Lib TA_HT_PHASOR.

HilbertLeadSine

func (s Series) HilbertLeadSine() Series

HilbertLeadSine returns the lead sine of John Ehlers’s sine wave indicator, the sine of the dominant-cycle phase advanced by 45 degrees: the second output of TA-Lib’s HT_SINE. At every bar t from 63 where it is not NaN it equals, bit for bit, math.Sin of HilbertPhase()[t] plus 45 degrees in radians: the product of that sum and TA-Lib’s deg2Rad (1 divided by its degrees per radian), rounded before the sine. It therefore lies in [-1, 1]. HilbertSine is the sine of the phase itself. Ehlers reads the crossings of the two lines as the turning signals of cycle mode: the lead sine crossing above the sine comes ahead of a cycle low, and the lead sine crossing below it ahead of a cycle high.

The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price h.Add(l).Div(2). The result has the receiver’s length, and the recursion starts at bar 37, as in TA-Lib. The warm-up reads 0, not NaN: bars 0 to 62 are 0 and the first valid index is 63. If a price or a value of the Hilbert recursion is not finite at bar p, every bar from p, or from 63 if p is earlier, to the end is NaN; earlier bars keep their values. The sine, the lead sine and the trendline are checked too but are finite whenever the recursion is, so the four Hilbert sine-wave outputs end at the same bar.

The recursion makes the result path dependent: a slice of the history that starts later gives different values until the start-up transient has decayed. Given the same atan, sin and cos the arithmetic is TA-Lib 0.8.1’s bit for bit; with Go’s functions the values agree with TA-Lib’s to rounding (about 1e-13) where the price moves, but on a flat stretch, where the phase is set by rounding as HilbertPhase describes, the two can differ by any amount within [-1, 1]. TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “Stay In Phase”, Technical Analysis of Stocks & Commodities, November 1996 (the sine wave indicator); TA-Lib TA_HT_SINE.

HilbertPeriod

func (s Series) HilbertPeriod() Series

HilbertPeriod returns the dominant cycle period of the receiver, in bars, from John Ehlers’s Hilbert transform with the homodyne discriminator: the smoothed period that TA-Lib calls HT_DCPERIOD (TA_HT_DCPERIOD). The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price, h.Add(l).Div(2).

The warm-up reads 0, not NaN: bars 0 to 31 are 0 and the first valid index is 32. The recursion starts at bar 12 as in TA-Lib, so the early values carry a start-up transient from the zero state. The method is recursive, so a slice of the history that starts later gives different values until the start-up transient has decayed.

Non-finite values do not recover. If a price is NaN or infinite, or a value of the recursion becomes NaN or infinite, every bar from that bar on, but not before the first valid index, is NaN; earlier bars are unchanged. Inputs large enough to overflow the products in the discriminator therefore give NaN.

In practice the values agree with the current TA-Lib C (release 0.8.1) to about 1e-13 bars, and bit for bit given the same atan, sin and cos; TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “MESA Adaptive Moving Averages”, Technical Analysis of Stocks & Commodities, September 2001; TA-Lib TA_HT_DCPERIOD.

HilbertPhase

func (s Series) HilbertPhase() Series

HilbertPhase returns the dominant cycle phase of the receiver, in degrees: the phase of a one-cycle discrete Fourier transform of the smoothed price over the dominant cycle period, with TA-Lib’s compensation for the one-bar lag of the smoother. It reproduces TA-Lib’s HT_DCPHASE (TA_HT_DCPHASE). The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price, h.Add(l).Div(2).

The warm-up reads 0, not NaN: bars 0 to 62 are 0 and the first valid index is 63. The recursion starts at bar 37 as in TA-Lib, so the early values carry a start-up transient from the zero state. The method is recursive, so a slice of the history that starts later gives different values until the start-up transient has decayed. From bar 63 the phase lies in (-45, 315] degrees. Where the price has been flat for a whole cycle, as on a constant input, the Fourier sums are rounding residues and the phase is set by rounding, not by the data.

Non-finite values do not recover. If a price is NaN or infinite, or a value of the recursion or of the Fourier sums becomes NaN or infinite, every bar from that bar on, but not before the first valid index, is NaN; earlier bars are unchanged. Inputs large enough to overflow the products in the discriminator therefore give NaN.

In practice the values agree with the current TA-Lib C (release 0.8.1) to about 1e-10 degrees, and bit for bit given the same atan, sin and cos; the difference grows as the cycle becomes small relative to the price, and on flat stretches the two can differ by any angle. TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “MESA Adaptive Moving Averages”, Technical Analysis of Stocks & Commodities, September 2001; TA-Lib TA_HT_DCPHASE.

HilbertQuadrature

func (s Series) HilbertQuadrature() Series

HilbertQuadrature returns the quadrature component Q1 of John Ehlers’s Hilbert transform of the receiver, in price units: the Hilbert transform of the detrended price, in quadrature with I1. It is the second output of TA-Lib’s HT_PHASOR (TA_HT_PHASOR); HilbertInPhase returns the first. The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price, h.Add(l).Div(2).

The warm-up reads 0, not NaN: bars 0 to 31 are 0 and the first valid index is 32. The recursion starts at bar 12 as in TA-Lib, so the early values carry a start-up transient from the zero state. The method is recursive, so a slice of the history that starts later gives different values until the start-up transient has decayed.

Non-finite values do not recover. If a price is NaN or infinite, or a value of the recursion becomes NaN or infinite, every bar from that bar on, but not before the first valid index, is NaN; earlier bars are unchanged. Inputs large enough to overflow the products in the discriminator therefore give NaN.

In practice the values agree with the current TA-Lib C (release 0.8.1) to about 1e-15 times the price level (about 1e-13 at prices near 100), and bit for bit given the same atan, sin and cos; TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “MESA Adaptive Moving Averages”, Technical Analysis of Stocks & Commodities, September 2001; TA-Lib TA_HT_PHASOR.

HilbertSine

func (s Series) HilbertSine() Series

HilbertSine returns John Ehlers’s sine wave indicator, the sine of the dominant-cycle phase: the first output of TA-Lib’s HT_SINE. At every bar t from 63 where it is not NaN it equals, bit for bit, math.Sin of HilbertPhase()[t] in radians: the product of the phase and TA-Lib’s deg2Rad (1 divided by its degrees per radian), rounded before the sine. It therefore lies in [-1, 1]. HilbertLeadSine is the sine of the phase advanced by 45 degrees. Ehlers reads the crossings of the two lines as the turning signals of cycle mode: the lead sine crossing above the sine comes ahead of a cycle low, and the lead sine crossing below it ahead of a cycle high.

The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price h.Add(l).Div(2). The result has the receiver’s length, and the recursion starts at bar 37, as in TA-Lib. The warm-up reads 0, not NaN: bars 0 to 62 are 0 and the first valid index is 63. If a price or a value of the Hilbert recursion is not finite at bar p, every bar from p, or from 63 if p is earlier, to the end is NaN; earlier bars keep their values. The sine, the lead sine and the trendline are checked too but are finite whenever the recursion is, so the four Hilbert sine-wave outputs end at the same bar.

The recursion makes the result path dependent: a slice of the history that starts later gives different values until the start-up transient has decayed. Given the same atan, sin and cos the arithmetic is TA-Lib 0.8.1’s bit for bit; with Go’s functions the values agree with TA-Lib’s to rounding (about 1e-13) where the price moves, but on a flat stretch, where the phase is set by rounding as HilbertPhase describes, the two can differ by any amount within [-1, 1]. TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “Stay In Phase”, Technical Analysis of Stocks & Commodities, November 1996 (the sine wave indicator); TA-Lib TA_HT_SINE.

HilbertTrendline

func (s Series) HilbertTrendline() Series

HilbertTrendline returns John Ehlers’s instantaneous trendline, as TA-Lib’s HT_TRENDLINE computes it. At each bar it takes the mean of the raw prices over the smoothed dominant cycle period rounded to the nearest whole number of bars (the bar’s price and those before it, at most 50; a period that rounds to 0 gives a mean of 0), and then smooths the last four means with a 4-3-2-1 weighted average, the newest mean weighing 4 and the oldest 1.

The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price h.Add(l).Div(2). The result has the receiver’s length, and the recursion starts at bar 37, as in TA-Lib. The warm-up reads 0, not NaN: bars 0 to 62 are 0 and the first valid index is 63. If a price or a value of the Hilbert recursion is not finite at bar p, every bar from p, or from 63 if p is earlier, to the end is NaN; earlier bars keep their values. The sine, the lead sine and the trendline are checked too but are finite whenever the recursion is, so the four Hilbert sine-wave outputs end at the same bar.

The recursion makes the result path dependent: a slice of the history that starts later gives different values until the start-up transient has decayed. Given the same atan, sin and cos the arithmetic is TA-Lib 0.8.1’s bit for bit; with Go’s functions the trendline agreed with TA-Lib’s bit for bit on every test case, and can differ only at a bar where the smoothed period lies within rounding of a half-bar boundary, where the rounded period, and so the mean, changes outright. TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “Adaptive Trends And Oscillators”, Technical Analysis of Stocks & Commodities, May 2000 (the instantaneous trendline); TA-Lib TA_HT_TRENDLINE.

HilbertTrendMode

func (s Series) HilbertTrendMode() Series

HilbertTrendMode returns John Ehlers’s trend mode, as TA-Lib’s HT_TRENDMODE computes it: 1 at a bar in trend mode and 0 at a bar in cycle mode, as a float64. Each bar starts as a trend and four rules then apply in turn: a crossing of the sine and the lead sine (HilbertSine, HilbertLeadSine) restarts the day count and marks a cycle; fewer days since the last crossing, counting the crossing bar as the first, than half the smoothed dominant cycle period mark a cycle; a phase advance since the previous bar (the phase HilbertPhase returns, read from the recursion, so bar 63 uses bar 62’s phase; with no correction for its wrap) strictly between 0.67 and 1.5 times 360 degrees divided by the smoothed period marks a cycle; and a smoothed price at least 1.5% from the trendline (HilbertTrendline), relative to the trendline, marks a trend whatever the other rules say.

The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price h.Add(l).Div(2). The result has the receiver’s length, and the recursion starts at bar 37, as in TA-Lib. The warm-up reads 0, not NaN: bars 0 to 62 are 0 and the first valid index is 63. If a price or a value of the Hilbert recursion is not finite at bar p, every bar from p, or from 63 if p is earlier, to the end is NaN; earlier bars keep their values. The sine, the lead sine and the trendline are checked too but are finite whenever the recursion is, so the four Hilbert sine-wave outputs end at the same bar. Every other bar from 63 is 0 or 1.

The recursion, and the day count that latches between sine crossings, make the result path dependent: a slice of the history that starts later gives different values until the start-up transient has decayed. Given the same atan, sin and cos the arithmetic is TA-Lib 0.8.1’s bit for bit. With Go’s functions the result agreed with TA-Lib’s at every bar of every test case; it can differ where a value lies within rounding of a threshold and, because a differing sine crossing restarts the day count, on the bars that follow until the next crossing; on a flat stretch, where the phase is set by rounding as HilbertPhase describes, the crossings themselves can fall on different bars. TA-Lib releases before 0.8.1, whose source does not call fma, can differ in the same way.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001) (the sine wave indicator and the trend-mode rules); Ehlers, “Stay In Phase”, Technical Analysis of Stocks & Commodities, November 1996 (the sine wave indicator and the 67% phase-rate rule); TA-Lib TA_HT_TRENDMODE.

Laguerre

func (s Series) Laguerre(n int, gamma float64, order int) Series

Laguerre implements Ehlers’ Laguerre filter code from TASC Sep 2025. Note that the first stage’s recursion intentionally uses the previous bar’s values (unlike the classic 2004 Laguerre filter used by LaguerreRSI); this matches Ehlers’ published Sep 2025 code. Values of n or order less than one return a zero-filled series.

gamma is a damping factor between 0 and 1. The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1, where every stage starts at the UltimateSmoother’s value, the filter’s steady state, so a constant series gives its level from bar n-1 (exactly at bar n-1, and to within rounding after it); n longer than the series gives zeros. Releases before this change started the stages at 0, so the output ramped up to the price level over roughly 20 bars or more for order > 1.

Each stage update is written as p + gamma*(q - p), equal to Ehlers’s -gamma*p + p + gamma*q but exact when its inputs are equal.

LaguerreRSI

func (s Series) LaguerreRSI(gamma float64) Series

LaguerreRSI implements John Ehlers’ Laguerre Relative Strength Index.

gamma is a damping factor between 0 and 1 and is not validated. The output is on a 0 to 100 scale (a reading can exceed 100 by one ulp from rounding), where Ehlers publishes it on 0 to 1, so his 0.2 and 0.8 levels are 20 and 80 here. The four stages start at the first price, the filter’s steady state, and stay there exactly while the price stays at its opening value, so those bars read 0 and the first bar that moves reads as bar 1 would: at gamma 0.5, 71.43 after a rise and 28.57 after a fall, whatever their size. Releases before this change started the stages at 0, so the first readings climbed to 70 to 100 for tens of bars whatever the data, which read as overbought. A NaN or infinite first price now makes every bar NaN, as a bad price at any later bar already did from that bar on.

Each stage update is written as v + gamma*(p - v), equal to Ehlers’s (1-gamma)*v + gamma*p but exact when its inputs are equal. So on a flat stretch after the price has moved the stages settle on the level, exactly for gamma below 0.5 (reading 0 within about 30 bars at 0.2 after a 1% move), to within an ulp at 0.5 (reading 0), and a few ulps short for larger gamma, where the reading freezes, usually at 0 or 100 in the direction from which the stages approached, as the exact filter would read. The time this takes grows with the move against the spacing of floating-point values at the level: at a level of exactly 0, where that spacing shrinks with the stages, the reading after a rise stays near 100 until the stages underflow, about 480 bars at 0.2 and 1,100 at 0.5, and for larger gamma it stays there. A NaN or infinite gamma makes every bar NaN.

LoopRotation

func (s Series) LoopRotation(s2 Series, hpLen, ssLen int) Series

LoopRotation returns the signed area swept between consecutive bars by the Ehlers Loop of s plotted against that of s2, positive for counter-clockwise rotation.

s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero. Each leg’s loop is computed on its own full input before the alignment, so the longer input keeps all of its filter history.

With x = s2.EhlersLoop(hpLen, ssLen) on the horizontal axis and y = s.EhlersLoop(hpLen, ssLen) on the vertical, both taken on the overlap, the output at overlap index j is the cross product of the loop points at j-1 and j, the signed area of the parallelogram they span (twice that of the triangle they form with the origin): x[j-1]*y[j] - y[j-1]*x[j], with each product rounded on its own. Its units are squared standard deviations. The sign measures rotation about the origin: positive when the loop point moves counter-clockwise about (0, 0). That is not the turning direction of the path that Ehlers reads, which can differ for a loop away from the origin; summed over a closed loop, the outputs equal twice the loop’s signed area. The first valid index is len(s) - min(len(s), len(s2)) + 1 and the minimum overlap is 2. If hpLen or ssLen is below 1, the result is all zeros.

On finite inputs, s2 == s gives zero at every index, bit for bit; a swap of the legs negates every non-zero output on the overlap exactly; and a constant s2, whose loop is exactly 0, gives zero at every index.

The method works per leg, so on a common bar interval the current value is correct even when the two calendars differ, but earlier outputs pair bars by position: every session one input has and the other lacks shifts the earlier pairings by one bar, so align by date before a backtest. Mixed bar intervals break it: the rotation pairs x[j-1] with y[j] at a common index, and a weekly price forward-filled onto daily bars is a staircase that daily-tuned filters smear. Both legs need a common clock, and each needs the burn-in of EhlersLoop, which is about 450 bars at the defaults. NaN is permanent per leg: a NaN or an infinity at index p > 0 of either input makes that leg’s loop NaN from index max(p, 3) of that input on, and every output that reads it is NaN. It is not start-invariant.

One reading comes from a review of Ehlers’s figures, which condenses his two eight-condition charts into one rule on the direction of travel atan2(dy, dx) between successive loop points: moving east, south-east or south, hold the reference; south-west, stay out; west, north-west or north, hold the stock; north-east, either. That rule is a consumer reading, not a method. The curvature of the path, the same cross product taken of successive displacements rather than of successive points, is the alternative.

Verification: the rotation is original to this library, so the alternative oracle stands in: a Python reference and a Julia reference, each translated independently from this definition, agree with this method on reference data, and the per-leg identity holds bit for bit: the output equals the formula above applied to two separate EhlersLoop calls.

MAMA

func (s Series) MAMA(fastLimit, slowLimit float64) Series

MAMA returns Ehlers’s MESA adaptive moving average of the receiver, in price units, as computed by TA-Lib’s MAMA (TA_MAMA). The receiver is the price series and may be any series; Ehlers and TA-Lib’s tests use the median price, h.Add(l).Div(2).

The smoothing constant alpha is fastLimit divided by the phase advance per bar in degrees, the previous bar’s atan(Q1/I1) minus this bar’s, held between slowLimit and fastLimit; an advance of one degree or less, which includes the snapback of the arctangent every half cycle, gives fastLimit. Ehlers suggests 0.5 for fastLimit and 0.05 for slowLimit, the defaults of TA-Lib. The parameters are valid if both are finite and 0 < slowLimit <= fastLimit <= 1; otherwise MAMA returns aligned zeros (one +0 per bar), whatever the input. The average starts from a zero seed, so the first values are pulled towards 0, as in TA-Lib. Ehlers’s crossover signal is MAMA crossing FAMA, the following average returned by FAMA.

The recursion starts at bar 12 as in TA-Lib, rather than at Ehlers’s bar 5, and the phase is 0 rather than held when I1 is 0; both are departures from Ehlers’s printed code.

The warm-up reads 0, not NaN: bars 0 to 31 are 0 and the first valid index is 32. The early values carry a start-up transient from the zero state. The method is recursive, so a slice of the history that starts later gives different values until the start-up transient has decayed.

Non-finite values do not recover. If a price is NaN or infinite, or a value of the recursion or either average becomes NaN or infinite, every bar from that bar on, but not before the first valid index, is NaN; earlier bars are unchanged. Inputs large enough to overflow the products in the discriminator therefore give NaN.

In practice the values agree with the current TA-Lib C (release 0.8.1) to about 1e-15 times the price level (about 1e-13 at prices near 100), and bit for bit given the same atan, sin and cos; TA-Lib releases before 0.8.1, whose source does not call fma, differ in the last bits.

Sources: Ehlers, Rocket Science for Traders (Wiley, 2001); Ehlers, “MESA Adaptive Moving Averages”, Technical Analysis of Stocks & Commodities, September 2001; TA-Lib TA_MAMA.

Phase

func (s Series) Phase(n int) Series

Phase implements the phase function according to the translation of the TradeStation algorithm provided by John Ehlers.

Bars 0 and 1 pass the input through, seeding the recursion as SuperSmoother is seeded, so the output starts at the price level; on a constant series it stays within a few ulps of the level, as the all-pass coefficients are not exact in binary64. An n of 0 or less returns zeros. Releases before this change started from zero history, so bars 0 and 1 were 0 and the output ramped from 0 to the price level over the next few bars.

PMA

func (s Series) PMA(n int) Series

PMA implements John Ehlers’ Projected Moving Average (as described in TASC Mar 2025).

Bars 0 to n-2 pass the input through; the first averaged bar is n-1.

PolyFit(n, 1, 0.5) is this projection to rounding from bar n-1, the regression line half a bar ahead.

PMAPredict

func (s Series) PMAPredict(n int) Series

PMAPredict implements John Ehlers’ prediction based on the predicted moving average (see TASC Mar 2025, p. 11)

Bars 0 to n pass PMA through (so bars 0 to n-2 are the input itself): the prediction term reads Slope two bars back, which is valid only from bar n+1, the first predicted value. Releases before this change added a prediction term built on Slope’s warm-up 0 at bars n-1 and n.

RocketRSI

func (s Series) RocketRSI(nSmooth, n int) Series

RocketRSI applies a Fisher transform to the normalised rolling up/down sums of smoothed momentum. RocketRSI(1, 1) uses zero-lag momentum and returns zeros. The shared Momentum(0) convention remains a one-bar difference.

The warm-up reads 0, not NaN: bars 0 to 2n-2 are 0 and the first valid index is 2n-1. The momentum s[i] - s[i-n+1] exists from bar n-1, so the SuperSmoother and the n-bar sums of its changes run on it from there. Releases before this change smoothed Momentum’s warm-up zeros as well, so bars n and n+1 read about plus or minus 3.8 (the clamp) for almost any data.

Slope

func (s Series) Slope(n int) Series

Slope calculates the slope over “n” points at each point in the series (see TASC Mar 2025, p. 11). The underlying calculation uses numerically stable, centred coordinates so Slope remains accurate for windows with a large common offset. A flat window of two or more points yields a slope of exactly zero. Slope(1) is the existing singleton case and returns NaN.

RepeatedMedianSlope is Siegel’s robust substitute for this slope, which it equals at n = 3 to rounding, and the line PolyFit fits at degree 1 has this slope: PolyFit(n, 1, 1) less PolyFit(n, 1, 0) is Slope(n) to rounding.

A window whose exact slope is 0 but which is not flat can give a tiny value of either sign rather than 0: Slope(7) of 41.5, 41.5, 41.5, 41.5, 40.5, 40.5 and 42.5 is about -1.2e-17. BullFlag and BearFlag test the sign of this slope over the closes of their flags, bit for bit, so they inherit it (the manual’s “Chart Patterns” section).

The warm-up reads 0, not NaN: bars 0 to n-2 are 0, which reads as flat.

SuperSmoother

func (s Series) SuperSmoother(n int) Series

SuperSmoother implements the super-smoother function according to the translation of the TradeStation algorithm provided by John Ehlers.

Bars 0 to 2 pass the input through, seeding the recursion.

The recursion is written as r[i-1] + c1*(m - r[i-1]) - c3*(r[i-1] - r[i-2]), with m the mean of the current and previous prices. That is Ehlers’ c1m + c2r[i-1] + c3*r[i-2] rearranged with c2 = 1 - c1 - c3, and it gives a constant series its level exactly, which the published form does not: the three coefficients sum to 1 only up to rounding. Releases before this change used the published form, which was off by rounding on most bars of some constants (297 of 300 bars of a constant 120 at n = 10 on arm64); every output moves by rounding only. The mean is formed by halving each price first when their sum would overflow, and a bar whose exact form is not finite (its differences can overflow for prices within a factor of about 2 of the largest float64) takes the published form instead, so no finite published value becomes infinite. SyntheticOscillator’s front end is s.Hann(12) passed through HighPass(ub) and then this filter at the period lb.

SyntheticOscillator

func (s Series) SyntheticOscillator(lb, ub int) Series

SyntheticOscillator returns the sine of a phase that advances at the measured dominant cycle and is re-anchored each time a band-pass filter crosses zero. It is John Ehlers’s oscillator from “A Synthetic Oscillator”, Stocks & Commodities V44:4 (April 2026), pp. 9-15, in which the phase advances by 360/DC degrees a bar, DC being the measured dominant cycle clamped to [lb, ub], and is re-anchored when a band-pass at the float period sqrt(lb*ub) crosses zero. The EasyLanguage listing (p. 10) is the definition, and where the prose differs (it calls the phase an arctangent and says the phase is forced to zero at an up-crossing) the listing is followed. Ehlers’s defaults are lb = 15 and ub = 25, plus or minus 25% about a 20-bar month; the Hann length of 12 and the RMS length of 100 are fixed as published.

The front end composes the package’s filters exactly: LP is s.Hann(12) passed through HighPass(ub) and then SuperSmoother(lb), the package’s forms of Ehlers’s 2025 EasyLanguage functions (math.Sqrt(2), math.Pi and radians for his 1.414, 3.14159 and degrees, and the exact-form recursion), which seed as his listings do and, given the same constants, agree with his to rounding; his printed constants move LP by up to about 4e-4 of its range. Real is LP divided by its RMS(100), ROC is the one-bar change of Real, and Imag is ROC divided by its RMS(100). As in EasyLanguage, every variable starts at 0 and keeps its previous value when it is not assigned, so Real holds when its RMS is 0 and Imag holds when its RMS is 0. Denom is ROCImag - (Imag - Imag[1])Real, and DC is 2math.Pi(Real^2 + Imag^2)/Denom when Denom is not zero; otherwise DC holds its previous clamped value, from an initial 0 that the clamp turns into lb. The clamp raises DC to lb first and then lowers it to ub. The band-pass is HighPass followed by UltimateSmoother, both at the period Mid = sqrt(lb*ub) as a float, not rounded to an integer (EasyLanguage’s SquareRoot is a float), and it filters the input itself, not the Hann path.

The phase is in degrees and is not wrapped. Each bar adds 360/DC to it; then, from bar 1 on, a band-pass crossing over 0 (the previous bar at or below 0 and this bar above it, the tie rule of CrossesAboveS) sets the phase to 180/DC, and a crossing under 0 (the previous bar at or above 0 and this bar below it, as in CrossesBelowS) sets it to 180 + 180/DC. Synth is the sine of the phase. While the phase lies in (0, 90) and the sine is below the previous bar’s final (held) Synth, or in (180, 270) and the sine is above it, Synth keeps that previous value; the hold alters Synth only, not the phase.

Within the listing’s own lines the departure is the constant 2math.Pi in place of the printed 6.28, which makes the unclamped DC larger by the factor 2pi/6.28 = 1.000507, as the package already writes math.Sqrt(2) and math.Pi for Ehlers’s 1.414 and 3.14159; the filters it calls carry the package’s documented departures, and the warm-up below masks what the listing plots.

Valid bounds are 1 <= lb <= ub, lb = ub included; any other lb or ub, lb > ub among them, gives len(s) zeros. The warm-up reads 0, not NaN: bars 0 to 98 are 0 and the first valid index is 99. Series shorter than 100 bars give zeros, and from bar 99 on the output is the literal translation of the listing, including a hold at bar 99 that compares with bar 98’s unmasked Synth.

Hann(12) is 0 on bars 0 to 10, so HighPass(ub) sees a step from 0 to the price level at bar 11, a transient that dominates the first RMS windows. TradeStation hides this region behind MaxBarsBack, which nseries does not model, so the first values after bar 99 are start-up values. The band-pass does not use the Hann path, so its zero crossings are valid from bar 3.

From the first bar p whose input is NaN or an infinity, DC, the phase and Synth are NaN to the end: the output is NaN from bar max(p, 99), bars below 99 stay 0, and bars before p are unchanged. Finite inputs that overflow inside the filters propagate by IEEE arithmetic. The filters, the holds and the phase carry state from bar 0, so the oscillator is recursive and not start-invariant: its values depend on where the history starts. It is causal, though: a shorter history gives the first values of a longer one, bit for bit, so values do not change when later bars arrive.

Every finite output lies in [-1, 1]. A constant series has a band-pass of exactly 0, so the phase is not reset and grows without bound; the outputs stay finite and in range.

On 1% log random walks with (15, 25), DC sits at a clamp on about 60% of bars from bar 200, Synth correlates about 0.97 with the same construction at a fixed DC equal to Mid, and the Length 4 rule below holds the same position as the same rule applied to the band-pass on about four bars in five: the band-pass resets, not the adaptivity, drive the oscillator.

Ehlers’s demonstration strategy smooths and differences the oscillator, with synth the result of this method,

roc2 := synth.Hann(length).Delta(1)

and buys at the next open on roc2.CrossesAboveS(0) and sells, or reverses to short, at the next open on roc2.CrossesBelowS(0); the published Length is 4. His results are on daily bars from 31 December 2008 to 18 July 2025, each market optimised in sample, with no costs mentioned: ES long only, with (17, 23) and Length 8, made about 260 trades with a profit factor of 2.06 and 66.9% winners; gold long and short, with (24, 30) and Length 12, a profit factor of 1.43; and Treasury bonds, with (8, 10) and Length 6, a profit factor of 1.34.

The cost is O(len(s)) per call: the front-end filters and the band-pass, then one pass over the bars, with a fixed number of allocations (the front-end series, Real, ROC and the two float filters), the same for every length above 100. RMS(100)’s rolling window grows its storage when it first drops a value, at bar 100, so a series of 100 bars or fewer allocates less.

Within a platform the output is fixed bit for bit. Across platforms (arm64 against amd64) the sine, cosine and exponential of the math package can differ in the last bit, and arm64 fuses the multiply-adds that the package’s filters leave unwrapped, so results agree only to rounding for ub of about 8 or more, and a crossing or a hold decided within rounding can go either way. For ub below about 8 the band lies in Hann(12)’s stopband, LP is a small residue, and the agreement degrades. SyntheticOscillatorPeriod with the same bounds returns the clamped dominant cycle that sets the phase advance.

SyntheticOscillatorPeriod

func (s Series) SyntheticOscillatorPeriod(lb, ub int) Series

SyntheticOscillatorPeriod returns the dominant cycle in bars, clamped to the range from lb to ub, that sets the phase advance of the synthetic oscillator with the same bounds. It is the DC of Ehlers’s “A Synthetic Oscillator”, Stocks & Commodities V44:4 (April 2026), pp. 9-15, the clamped period that drives SyntheticOscillator(lb, ub), and it is reported on every bar, as AdaptiveSuperSmootherPeriod reports its period, rather than masked by a warm-up. It lies in the range [lb, ub] and is lb while DC is unmeasured, through bar 99: the RMS windows are 0 before bar 99, and at bar 99 Real[98] and Imag[98] are 0, so ROC equals Real, Denom is exactly 0 and DC is first measured at bar 100. Finite series shorter than 100 bars give lb on every bar.

LP is s.Hann(12) passed through HighPass(ub) and then SuperSmoother(lb). Real is LP divided by its RMS(100) and holds its previous value when that RMS is 0; Imag is ROC, the one-bar change of Real, divided by its own RMS(100), and holds when that RMS is 0; and DC is 2math.Pi(Real^2 + Imag^2) divided by Denom = ROC*Imag - (Imag - Imag[1])*Real, holding its previous clamped value when Denom is 0, from an initial 0 that the clamp, to lb first and then to ub, turns into lb. The listing’s constant is the printed 6.28, and this method uses 2*math.Pi in its place, which makes the unclamped DC larger by the factor 1.000507.

In the package’s methods, with c the price series, DC is

price := c.Hann(12)
lp := price.HighPass(ub).SuperSmoother(lb)
re := lp.Div(lp.RMS(100))
d := re.Delta(1)
im := d.Div(d.RMS(100))
den := d.Mul(im).Sub(im.Delta(1).Mul(re))
dc := re.Mul(re).Add(im.Mul(im)).Mul(2*math.Pi).Div(den).Clamp(float64(lb), float64(ub))

and SyntheticOscillatorPeriod(lb, ub) equals dc bit for bit on every bar (lb before bar 100 on both sides, where bar 99’s Denom is exactly 0 on both) until the first bar from 100 on where an RMS or Denom is exactly 0. There this method holds the previous Real, Imag or DC where Div gives 0: at a zero Denom the method keeps its previous DC while Clamp gives the recipe lb, and where the RMS of LP is 0 the method holds Real, not its period, so the period can move, while the recipe measures the jump of Real to 0 as a cycle of its own and gives ub (on a 1% walk gone flat at bar 250, with (2, 5), the method’s period falls from 5 to 2 and the recipe’s stays at 5). On random walks no such bar occurs.

Valid bounds are 1 <= lb <= ub; any other lb or ub gives len(s) zeros. From the first bar whose input is NaN or an infinity the result is NaN to the end, and bars before it are unchanged. With lb = ub the result is lb on every bar before that bar. Within a platform the result is fixed bit for bit; across platforms it agrees only to rounding for ub of about 8 or more, and less closely below that, as for SyntheticOscillator.

UltimateOscillator

func (s Series) UltimateOscillator(bandEdge int, bandWidth int, rmsLength ...int) Series

UltimateOscillator implements the ultimate-oscillator function according to the translation of the TradeStation algorithm provided by John Ehlers (in TASC April 2025). Despite the shared name, this is Ehlers’s 2025 band-pass filter, not Larry Williams’s Ultimate Oscillator (1985), which averages buying pressure over 7, 14 and 28 bars: that one (TA-Lib’s ULTOSC, Tulip’s ultosc, pandas-ta’s uo, TradingView’s Ultimate Oscillator) is WilliamsUltimateOscillator. The optional RMS length defaults to 100, matching the published version. With RMS length 1, the signal is divided by its absolute value; zero stays zero. Standalone RMS retains its zero output for lengths <= 1.

bandWidth is an integer multiple of bandEdge, the slower high-pass using bandWidth*bandEdge bars, not a fraction. The warm-up reads 0, not NaN: bars 0 to rmsLength-2 are 0 (99 bars at the default 100), from RMS’s zero warm-up.

UltimateSmoother

func (s Series) UltimateSmoother(n int) Series

UltimateSmoother implements the ultimate-smoother function according to the translation of the TradeStation algorithm provided by John Ehlers (in TASC April 2024). Note that the Sep 2025 TASC has different code for the UltimateSmoother, but we have stuck with the previous version here (for the moment, at least).

Bars 0 to 2 pass the input through, seeding the recursion.

The recursion is written as r[i-1] + (1-c1)(s[i] - r[i-1]) + (2c1-c2)(s[i-1] - r[i-1]) - (c1+c3)(s[i-2] - r[i-1]) - c3*(r[i-1] - r[i-2]). That is Ehlers’ form rearranged, using the fact that its five weights sum to 1, and it gives a constant series its level exactly, which the published form does not: the weights sum to 1 only up to rounding. Releases before this change used the published form, which was off by rounding on most bars of some constants (297 of 300 bars of a constant 120 at n = 2 on arm64); every output moves by rounding only. A bar whose exact form is not finite (its differences can overflow for prices within a factor of about 2 of the largest float64) takes the published form instead, so no finite published value becomes infinite.

USI

func (s Series) USI(n int) Series

USI implements John Ehlers’ Ultimate Strength Index (as described in TASC Nov 2024).

The warm-up reads 0, not NaN: bar 0 is 0, and a bar whose smoothed up or down sum is not above 0.01, in price units, holds the previous value, which starts at 0, so on flat data it stays 0.

VIDYA

func (s Series) VIDYA(short int, long int) Series

VIDYA implements Chande/Kroll’s variable dynamic average according to the translation of the TradeStation algorithm provided by John Ehlers. Requires 1 <= short <= long; invalid windows return a zero-filled series.

The driver is the standard-deviation ratio: y[t] = k*s[t] + (1-k)*y[t-1] with k = 0.2*sd_short/sd_long, the population standard deviations of the last short and last long values (Chande, “Adapting Moving Averages To Market Volatility”, Stocks & Commodities, March 1992; the constant 0.2 and the suggestion short 9, long 30 from Ehlers, “Nonlinear Ehlers Filters”, Stocks & Commodities, April 2001). The CMO form of Chande’s 1995 article, which pandas-ta and many TradingView scripts call VIDYA, has a different driver, |CMO|/100 (EfficiencyRatio), and is the method VIDYACMO; its 1% bands are a composition under “Moving Averages” in the manual.

k is capped at 1, so the average stays between its previous value and the price, as an exponential average does. Chande writes that “t must be less than 1 for the term (1-t) to be positive”; Ehlers’s listing has no cap. The short window is the end of the long one, so k is at most 0.2sqrt(long/short), and the cap can act only when long >= 25short; a bar where it acts takes the price. A ratio that overflows to +Inf, which needs prices of about 1e154, is capped too; a NaN ratio stays NaN, and so does every bar after a NaN value. Releases before this change let the constant exceed 1 when long >= 25*short, so the average could pass the price: after 118 bars at 100, then 95 and 105, VIDYA(2, 100) read 94.97 and 109.15.

Bars 0 to long-1 pass the input through, and the recursion is seeded with s[long-1].

Voss

func (s Series) Voss(lead int) Series

Voss implements the Voss predictor function according to the translation of the TradeStation algorithm provided by John Ehlers. A lead of less than one returns a zero-filled series.

The recursion runs from bar 0 with zero history, as Ehlers’s source does (feedback terms before bar 0 read 0), so there are no warm-up zeros; it feeds back on its own early output, so apply it to a band-passed, zero-mean input, whose first values are small. Releases before this change held bars 0 to 3lead-1 at 0 and started the recursion at bar 3lead, where it met a full-size input with no history and gave a spike of about (3+3lead)/2 times the input; the output then equals this one on the input with its first 3lead values set to 0.

ZeroLag

func (s Series) ZeroLag(n int) Series

ZeroLag implements John Ehlers’ zero-lag indicator. A value of n less than one returns a zero-filled series.

The warm-up reads 0, not NaN: bars 0 to F-1 are 0, where F = 2n-1 (F = 2 when n = 1) is the first valid index, the first bar whose Hann window holds only bars where the required price is defined (from bar n, or bar 2 when n = 1). Releases before this change gave values near minus the price on bars n-1 to F-1, because the window averaged the simple average’s pass-through against a required price of 0; every later bar is unchanged.

Compositions

Ehlers’s centre of gravity oscillator is a ratio of two window sums of the price. Its value is NaN before its first valid index, where the window is not yet full; its turns read 0 there and are decided at bar t’s close. No average is seeded. The Relative Vigor Index on the gap-and-vigor page under “QStick” has the same author and year, the same hold of the last value on a zero denominator and the same one-bar-lag trigger; LinearWeights and FIRSum are under “Moving Averages”, and Slope, which the oscillator’s second identity uses, is in this category. Prices are positive and finite and lengths are in bars.

Centre of gravity oscillator (Ehlers)

John Ehlers (“The Center Of Gravity Oscillator”, Stocks & Commodities V20:5, May 2002, pp. 20-24) takes “the summation of the product of position within the observation window multiplied by the price at that position divided by the summation of prices across the window”, the current bar at position 1, and negates it (“All I had to do was invert the sign of the CG”); “by delaying the CG by one bar you can get an effective crossover signal”. His listing holds the value on a zero sum, If Denom <> 0 then CG = -Num/Denom, on the median price over 10 bars. t6CentreOfGravity writes his ratio with LinearWeights, which weigh the current bar n: the weighted sum over the plain sum, less n + 1.

ratio := p.FIRSum(LinearWeights(n)).Div(p.Sum(n))
cg := ratio.Sub(float64(n+1)).SetN(n-1, math.NaN()).ValueWhen(p.Sum(n).EQ(0).LT(1))
centred := cg.Add(float64(n+1) / 2)
up := cg.CrossesAboveS(cg.Shift(1))
down := cg.CrossesBelowS(cg.Shift(1))
  • Inputs: the price p (Ehlers’s (h + l)/2) and n (10; “Ideally, the selected length should be half the dominant cycle”). cg is on Ehlers’s scale, near -(n + 1)/2, which is -5.5 at n = 10; centred adds (n + 1)/2 for a zero line, nseries’s presentation, not his; up and down are 0 or 1. Arch-identical (FIRSum rounds each product on its own).
  • First valid index: n - 1 (9) for cg and centred; the turns can first fire at n + 1, since bar n - 1’s previous value is NaN. Unmasked, bars 0 to n - 2 read -(n + 1). A missing price on bar j makes cg NaN on bars j to j + n - 1.
  • Anchors: p.WAverage(n).Div(p.Average(n)).Mul(float64(n+1) / 2).Sub(float64(n+1)) gives cg to rounding, and so does the Slope form, the centred value being (n^2 - 1)/12 times p.Slope(n) over p.Average(n), and a loop of Ehlers’s listing; the turns are the zero crosses of cg.Delta(1) bit for bit; a constant price reads -5.5, centred 0. The hold binds only on an exactly zero window sum, which positive prices do not give: on a toy input with zero-sum windows it holds 3, 3, -4 and -3.2, and the first windows, which all sum to 0, read NaN where the listing reads its initial 0.
  • Traps: pandas-ta’s cg is the mirror image, -(n + 1) less Ehlers’s value, because it weighs the oldest bar 1; it reads the close and has no hold: a conformance trap, not a reference. The WAverage form fuses multiply-adds on arm64, the FIRSum form does not. Ehlers’s adaptive and other CG forms in Cybernetic Analysis for Stocks and Futures (Wiley, 2004) were not available, so none is given.
  • Variant: Mike Slattery’s passband (“An Early Warning System (Part 2)”, Stocks & Commodities V35:10, September 2017), the centred CG of 10 bars of (o + c)/2 less that of 6 bars of the close, through ALMA(4, 0.67, 1.1), is pb.ALMA(4, 0.67, 1.1).SetN(12, math.NaN()) with mid := o.Add(c).Div(2) and pb := mid.FIRSum(LinearWeights(10)).Div(mid.Sum(10)).Sub(5.5).Sub(c.FIRSum(LinearWeights(6)).Div(c.Sum(6)).Sub(3.5)); his COG is drawn on the price axis, so its scale is not shown, and (o + c)/2 is the reading of his “open + close / 2”.
  • Evidence: Ehlers’s figure (March 1996 T-bond futures, -5.52468 on its last bar) and Slattery’s AXP charts; no trades. On a sine of period 24 with n = 12 the CG peaks one bar before the price. Evidence, not a reference.
  • Pinned by TestRecipeT6CentreOfGravity; runnable example ExampleSeries_FIRSum_centreOfGravity.

The Ehlers compositions below mask their values with NaN before their first valid index, where the root mean square reads +0 in its warm-up or the three-bar window is not yet full; the flags and the position read 0 there. Prices are positive and finite and lengths are in bars, and the filters use math.Sqrt(2), math.Pi and radians where Ehlers’s listings print 1.414, 3.14159 and degrees.

Cybernetic oscillator (Ehlers 2025)

John Ehlers (“Making A Better Oscillator”, Stocks & Commodities V43:7, June 2025, pp. 9-14 and 19) calls the cybernetic oscillator “just a SuperSmoother lowpass filter for smoothing, followed by a second-order highpass filter”, whose response “is scaled to its RMS (root mean squared) value”. His indicator listing applies $HighPass first and $SuperSmoother second, and t6CyberneticOscillator follows the code, dividing by the root mean square of the last 100 filtered values as UltimateOscillator does; the text’s “the order of filter application is irrelevant” holds in the steady state only, and the two orders’ start-ups differ for well over a hundred bars at a high-pass period of 250 (past bar 170, at 1% of the filtered scale, on a random walk). The defaults are 30 and 20 for swing trading and 250 and 20 for the trend.

f := c.HighPass(hpLen).SuperSmoother(lpLen)
osc := f.Div(f.RMS(100)).SetN(99, math.NaN())
  • Inputs: closes; hpLen, the high-pass period (the “lower edge”), and lpLen, the smoothing period (the “upper edge”), which “usually should be greater than 8”, with the lower edge “at least a half-octave (1.4 times) larger than the upper edge”.
  • First valid index: 99, where RMS(100)’s +0 warm-up ends; the mask turns it into NaN, since 0 would pass a LT(1) test. Ehlers’s listing over zero history is not 0 there: at bar 3 it reads exactly +-10.
  • Anchors: from bar 99 the helper equals a direct loop of the printed listing (exact constants, zero history, the naive RMS loop and its hold) within 1e-12. c.UltimateOscillator(e, w) is s.Div(s.RMS(100)) for its band-pass s, bit for bit, the same scaling; the sign is EhlersLoop’s from bar 99 (the same filtered series over positive normalisers). A constant or an integer ramp gives 0, scaling the closes by a power of two leaves it unchanged and negating them negates it, bit for bit, and |osc| is at most 10, the square root of the window length.
  • Traps: the listing’s printed constants move osc by up to about 1.3e-3, and the text’s order, c.SuperSmoother(lpLen).HighPass(hpLen), by up to 0.32 from bar 99 on AAPL’s 2023 closes at (30, 20).
  • Variant: Ehlers’s zero-history start-up, f.Div(f.Squared().Sum(100).Div(100).Sqrt()) in place of the second line, reproduces his listing from bar 0 (+-10 at bar 3), a start-up TradeStation does not plot.
  • Variant: his swing reading, “swing extremes larger than one standard deviation can be used for swing trade entries and exits”, is osc.GT(1) and osc.LT(-1) at (30, 20); he does not test it.
  • Evidence: the article prints charts only (Figure 4’s last readings, 1.20 at (30, 20) and -1.53 at (250, 20)) and no data. Evidence, not a reference.
  • Pinned by TestRecipeT6CyberneticOscillator; runnable example ExampleSeries_HighPass_cyberneticOscillator.

Ehlers’s dual-ROC strategy

The same article’s “super-simple strategy” takes “a swing position only in the direction of a trend” by “correlating the directions of rate-of-change of the two indicators”: its listing smooths the close first, $SuperSmoother(Close, LPLength), high-passes the result at 55 and 156 bars, takes the two-bar changes BP1 - BP1[2] and BP2 - BP2[2], buys “Next Bar on Open” when both are positive and sells when either is negative. t6CyberneticStrategy returns the two flags and Position’s long-only latch, decided on bar t’s close; a rate of change of exactly 0 neither enters nor exits, as the listing’s strict tests.

ss := c.SuperSmoother(lpLen)
roc1 := ss.HighPass(fastLen).Delta(2)
roc2 := ss.HighPass(slowLen).Delta(2)
entry := roc1.GT(0).Mul(roc2.GT(0))
exit := roc1.LT(0).Add(roc2.LT(0))
pos := c.Position(entry, exit)
  • Inputs: closes; lpLen, fastLen and slowLen (20, 55 and 156, “after historical optimization” on the E-mini S&P 500 futures).
  • First valid index: 0 by construction, meaningful from about slowLen: HighPass(156) starts from zero history, so signals in about the first 156 bars are start-up artefacts, which Ehlers’s code shares and TradeStation’s MaxBarsBack hides. They do fire (dozens of entries before bar 156 on a random walk), so mask the flags there before trading them.
  • Anchors: pos equals a loop of the printed MarketPosition rule bit for bit; entry and exit are not both 1 on any bar, so Position and a Hysteresis latch over them agree.
  • Traps: the strategy reads the unscaled band-passes, not the oscillator, filtered in the other order; the June 2025 Traders’ Tips’ ports depart from the listing (the MetaStock sell needs both rates of change negative, the NeuroShell slow leg high-passes the raw close, and RealTest lags the smoother’s input a bar).
  • Variant: the trade over the next bar, “Next Bar on Open”, is pos.Shift(1), which equals c.Position(entry.Shift(1), exit.Shift(1)) (Shift pads with 0, which does not fire).
  • Evidence: on daily E-mini S&P 500 futures from 31 December 2008 to 18 November 2024 (Figure 5’s title) the strategy made 153 trades and 159,675 dollars before costs, with a profit factor of 2.39 and 64% winners, after optimisation, long only and on one rising market. Evidence, not a reference.
  • Pinned by TestRecipeT6CyberneticStrategy.

Even Better Sinewave (Ehlers 2013)

Ehlers’s Even Better Sinewave (Cycle Analytics for Traders, Wiley, 2013; reported as ch. 12) high-passes the closes with a one-pole filter, “HP = .5*(1 + alpha1)(Close - Close[1]) + alpha1HP[1]” with “alpha1 = (1 - Sine (360 / Duration)) / Cosine(360 / Duration)”, smooths it with a 10-bar SuperSmoother and divides the three-bar mean of the result by the square root of its three-bar mean power. The one-pole high-pass is XAverageAlpha(1 - a1) of the change times (1 + a1)/(2(1 - a1)) (HighPass is Ehlers’s two-pole filter), and in t6EvenBetterSinewave three zero pads in front make SuperSmoother’s pass-through bars zero history, as the listing starts. Source caveat: the book was not read; the listing is the one posted on the Sierra Chart support board (thread 59987, 21 January 2021), whose high-pass and SuperSmoother lines have the form of Ehlers’s own code in “Measuring Market Cycles” (*Stocks & Commodities* V34:10, September 2016, p. 15), there at 48 and 8 bars.

w := float64(2*math.Pi) / float64(duration)
a1 := (1 - math.Sin(w)) / math.Cos(w)
hp := c.Delta(1).Mul((1 + a1) / (1 - a1) / 2).XAverageAlpha(1 - a1)
f := Series{0, 0, 0}.Append(hp).SuperSmoother(10).Window(-3)
wave := f.Sum(3).Div(3)
pwr := f.Squared().Sum(3).Div(3)
ebsw := wave.Div(pwr.Sqrt()).SetN(1, math.NaN())
  • Inputs: closes; duration >= 5 bars (default 40). At 4 the cosine rounds to about 6e-17, a1 to 0 and the alpha to 1, a degenerate filter that passes half the change and is not zero, and below 4 a1 is negative and XAverageAlpha returns zeros. The SuperSmoother’s 10 bars are the listing’s constant.
  • First valid index: 1, where one filtered value fills the three-bar window, so bar 1 is +-1/sqrt(3); the high-pass needs some 130 to 175 bars to settle (to 1e-9 and 1e-12) at a duration of 40, which the listing shares. A zero power gives 0, where the listing divides by zero.
  • Anchors: from bar 1 the helper equals a direct loop of the listing (exact constants, zero history) within 1e-12 at durations 40, 10 and 5, and the one-pole high-pass equals its recursion to rounding. The sign is that of f.Sum(3) on every bar, |ebsw| is at most 1 + 2^-52 (the mean is at most the root mean square, and rounding can add one unit), scaling the closes by a power of two and negating them are exact, and a constant gives 0. Two pads would do here only because the high-pass is 0 at bar 0; on an input whose first value is not 0 only three give the zero history.
  • Traps: the two-pole c.HighPass(duration).SuperSmoother(10) with the same ratio is a different indicator, up to 2.0 away with the opposite sign; pandas-ta 0.3.14b0 takes the degrees as radians (0.4.71b0’s initial_version branch still does), and both seed the change with a zero close at bar length, which 0.4.71b0’s default form keeps, settling some 160 bars later.
  • Variant: the short form with the library’s start-ups, f := hp.SuperSmoother(10) and f.Average(3).Div(f.RMS(3)), equal to the listing from about bar 60 (bar 1 reads 0).
  • Variant: ebsw.Clamp(-1, 1) removes the one-unit excess.
  • Evidence: no source prints an Even Better Sinewave value, its data or a test; the book’s charts are the only evidence. Evidence, not a reference.
  • Pinned by TestRecipeT6EvenBetterSinewave; runnable example ExampleSeries_XAverageAlpha_evenBetterSinewave.

Endpoint FFT

EndpointFFTSlope

func (s Series) EndpointFFTSlope(n int, threshold float64) Series

EndpointFFTSlope returns the one-bar change at the newest point of a noise-filtered Fourier fit to the last n log prices. It is Dennis Meyers’s deltaEP from “The Endpoint Fast Fourier Transform System”, Stocks & Commodities V17:5 (May 1999), pp. 199-208, where EPFFT is his name for the method: the value of the fit at the newest bar of the window less its value at the bar before, in flattened log units, with the chord slope b not added back.

For the window ending at bar t, with x_j = ln s[t-n+1+j] for j = 0 to n-1, the method takes the log prices and end-flattens them as Meyers’s Equation 1 does: a = x_0, b = (x_{n-1} - x_0)/(n-1) and y_j = x_j - (a + bj), so that y_0 and y_{n-1} are 0 in exact arithmetic. It pads y with zeros to N = 2n points (Meyers pads 256 to 512) and takes the unscaled forward transform F_k, the sum over j of y_j e^{-2 pi i jk/N}, for k = 0 to n; the input is real, so F_{N-k} is the conjugate of F_k. Fmax is the largest magnitude of F_k over the harmonics k = 2 to n, the Nyquist harmonic included, as in his Equation 2. His noise filter (Equations 3A to 3C, after Donoho) keeps harmonics 0 and 1 and thresholds every other index on its own. This method decides on the half spectrum and mirrors each decision: it keeps the mirror N-1 of harmonic 1 with it, and for each k from 2 to n drops harmonic k and its mirror N-k together when the magnitude of F_k is at most thresholdFmax, and keeps both otherwise. So a tie drops (his Equation 3A keeps on a strict >, and the filter of his earlier article, Stocks & Commodities V17:4, zeroes on <=), and the masked spectrum stays conjugate-symmetric, so its inverse is real. The earlier article is “The Discrete Fourier Transform Illusion”, Stocks & Commodities V17:4 (April 1999), pp. 151-158. The masked inverse, with the factor 1/N, is taken at the two newest points only, yep at n-1 and yepm1 at n-2, and the result is yep - yepm1. The transform is a direct DFT over N = 2n points, so N need not be a power of two, and its result equals an FFT’s to rounding.

The published parameters are n = 256 and a threshold of 0.15. A threshold of 0 keeps every harmonic with a non-zero magnitude, so the inverse returns the flattened window and, from bar n-1 on, the result equals, to rounding, the one-bar log change less the chord slope, for a price series c:

c.Log().Delta(1).Sub(c.Log().Delta(n-1).Div(float64(n-1)))

A finite threshold of 1 or more keeps only harmonics 0 and 1 (with the mirror of 1), since no harmonic from 2 to n has a magnitude above Fmax, and every such threshold gives the same result as a threshold of 1. A threshold of -0 acts as 0.

The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1. Since 0 is also a value, trim the warm-up with Window(-(n-1)) or mask it with SetN(n-1, math.NaN()). The parameters n < 2 or n > len(s), or a threshold that is NaN, negative or infinite, give len(s) zeros. A price that is not finite and positive (NaN, an infinity, zero or a negative number) at bar p gives NaN at bars max(p, n-1) to min(p+n-1, len(s)-1), exactly the windows that hold it, while warm-up bars stay 0; later windows recover. Positive subnormal prices are valid: the logarithms come from the package’s subnormal-safe ln, as those of Log do.

Each valid bar depends only on its own window, so the slope is causal, does not repaint and is start-invariant: a series that starts later gives the same values, bit for bit, at every bar whose window it holds whole. A constant window flattens to exact zeros, so every F_k is 0 and the result is exactly 0; zero results are +0. For a fixed mask the result is a fixed linear weighting of the log prices in the window, a linear FIR filter; only the magnitude test makes it adaptive.

The cost is O(n^2) per bar: the forward transform takes 2n(n+1) real multiply-adds and the two endpoints at most 8n (fewer as harmonics drop), so at most 2n(n+1) + 8n in all, about 1.3e5 at n = 256 (131,584 and 2,048), with O(n) tables and scratch and a slice of the len(s) logarithms, all allocated once per call.

Every product is rounded on its own and every sum has a fixed order, so the result is fixed bit for bit on a given platform. Across platforms (arm64 against amd64) the cosine, sine and logarithm of the math package can differ in the last bit, so results agree to rounding rather than bit for bit, and a harmonic whose magnitude lies within rounding of threshold*Fmax can be kept on one platform and dropped on another.

Meyers’s EPFFT curve is the running sum dsumEP of these increments, a recipe rather than a method, with slope the result of this method:

dsum := slope.CumSum()

Its level depends on where the history starts; only its changes are start-invariant. Meyers trades a stop-and-reverse that reads only dsumEP: buy at the next open when dsum has risen more than ptup above its lowest value since the short entry, and sell, reversing to short, at the next open when it has fallen more than ptdn below its highest value since the long entry. His optimised ptup and ptdn are on an unstated scale (he prints the endpoint sum without the 1/N his narrative needs and cites MathCad 8, and the axis of his chart is unlabelled), so they do not transfer to the log units of this method. His evidence is the S&P 500 futures, walk-forward with three-year optimisation windows: out of sample from 1 January 1996 to 22 January 1999 the system made 15 trades with 8 winners (53%) and a net 105,900 dollars before costs, a profit factor of 3.74, compounding at 14.43% a year against 19.31% for buy-and-hold, so it lagged buy-and-hold. On log random walks with 1% a bar, n = 256 and a threshold of 0.15, the slope correlates about 0.73 to 0.76 with the one-bar change of c.Log().SuperSmoother(16) to c.Log().SuperSmoother(20), and about 0.79 to 0.80 with that change a bar later.

Meyers’s later “Trading The E-Mini” (Stocks & Commodities V21:1, January 2003) uses one-minute bars, n = 512 without padding and an optimised threshold of 0.13, and exponentiates the endpoints with the chord added back, so its increment is in price units and includes b. That form is not built; it would be a sibling method, EndpointFFTPriceSlope. Nor is there a level method: Meyers’s level yepab = exp(yep + a + b(n-1)) jitters as the window slides, and he discards it.