Moving averages and filters

Moving averages and filters

All functions · nseries package

Sections: Moving Averages · KAMA · Cong Adaptive MA · Kalman Filter · One Euro Filter

Moving Averages

ALMA

func (s Series) ALMA(n int, offset, sigma float64) Series

ALMA returns the Arnaud Legoux moving average of s over a window of n bars, as TA-Lib’s TA_ALMA computes it (bit for bit given the same exp, except that at n = 1 TA-Lib copies the input, so a -0 stays -0 there and reads +0 here): each bar from n-1 on is the sum of the n values ending at that bar, each multiplied by its Gaussian weight from ALMAWeights(n, offset, sigma). The weights sum to 1 to rounding and peak at bar m = floor(offset times (n-1)) of the window, counting the oldest as 0, about a fraction offset of the way from the oldest bar to the newest. The result is s.FIRSum(ALMAWeights(n, offset, sigma)) bit for bit.

n is the window length, at least 1. offset, in [0, 1], places the peak of the weights (0 on the oldest bar, 1 on the newest): a larger offset follows the price more closely, and the smoothing is greatest at offset 0.5 and falls towards either end. sigma, finite and positive, sets their width as n/sigma bars: a larger sigma concentrates them around the peak, and a smaller one flattens them towards equal weights. The defaults, as in TA-Lib and pandas-ta (and the example on TradingView’s ta.alma page), are n = 9, offset = 0.85 and sigma = 6.

The peak is at m = floor(offset times (n-1)), floored as TA-Lib, Tulip and pandas-ta floor it. TradingView’s ta.alma does not floor m by default (its floor argument is false), so its values differ from TA-Lib’s, Tulip’s, pandas-ta’s and this method’s whenever the binary64 product offset times (n-1) is not an integer.

The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1. With n = 1 each bar is its value plus 0. Parameters for which ALMAWeights returns nil (invalid ones, or a sigma so large that the weights degenerate) and n > len(s) give len(s) zeros; n > len(s) is checked before any weights are built, so a huge n builds no weights.

The average is window-local, so a NaN or an infinity affects only the windows that hold it: a NaN gives NaN, and an infinity gives an infinity of the same sign, or NaN when the window holds infinities of both signs or the infinity meets a weight that underflowed to 0.

Sources: Legoux and Kouzis-Loukas (2009); TA-Lib’s TA_ALMA.

ALMAWeights

func ALMAWeights(n int, offset, sigma float64) []float64

ALMAWeights returns the n normalised weights of the Arnaud Legoux moving average (ALMA), oldest first: index 0 weights the oldest bar of a window and index n-1 the newest. They follow a Gaussian curve, built as TA-Lib’s TA_ALMA builds it. The peak is at m = floor(offset times (n-1)) and the width is sd = n/sigma. The raw weight at index j is exp(-(j-m)^2/(2 sd^2)), which is 1 at the peak and falls with the distance from it. Each raw weight is then divided by the sum of them all, so the weights sum to 1 to rounding.

n is the window length, at least 1. offset, in [0, 1], places the peak (0 on the oldest bar, 1 on the newest). m is floored as TA-Lib floors it, so an offset times (n-1) that rounds to just below an integer gives the integer below it (TradingView’s ta.alma does not floor m by default; see ALMA). sigma, finite and positive, sets the width: a larger sigma concentrates the weights around the peak, and a smaller one flattens them towards equal weights. The defaults, as in TA-Lib and pandas-ta (and the example on TradingView’s ta.alma page), are n = 9, offset = 0.85 and sigma = 6.

Invalid parameters give nil: n below 1, an offset outside [0, 1] or NaN, or a sigma that is not finite and positive. The result is also nil when the sum of the raw weights is 0 or not finite, as it is when sigma is so large that the doubled square of sd underflows to 0. Otherwise the slice is newly allocated and holds n values, so n must be allocatable.

Sources: Legoux and Kouzis-Loukas (2009); TA-Lib’s TA_ALMA.

Average

func (s Series) Average(n int) Series

Average returns the moving average of n observations. Before a full window is available, it returns the corresponding input value. Values of n below one, or above the input length, return a copy of the input.

EMA

func (s Series) EMA(n int) Series

EMA is an alias for XAverage.

FadingMemoryForecast

func (s Series) FadingMemoryForecast(n, degree int) Series

FadingMemoryForecast returns the one-bar-ahead forecast of the fading-memory polynomial of the given degree, its value at the next bar, with the smoothing constant set by a period of n bars. It is FadingMemoryForecastAlpha(2/(float64(n)+1), degree), bit for bit, with Meyers’s conversion of his period N to the smoothing constant, alpha = 2/(n+1): n = 1 gives alpha = 1, and n < 1 gives len(s) zeros. The degree runs from 0 to 4, and any other degree gives len(s) zeros.

There is no warm-up: the first valid index is 0. The filter is recursive, so it is not start-invariant: the method on a slice that starts later gives different values. From the first bar at which the value is NaN or infinite, or an updated state is not finite, every bar is NaN, and the recursion does not recover.

FadingMemoryForecastAlpha describes the forecast, its identities and Meyers’s trading rules for it, and FadingMemoryLevelAlpha gives the definition, the gains, the seed and the sources.

FadingMemoryForecastAlpha

func (s Series) FadingMemoryForecastAlpha(alpha float64, degree int) Series

FadingMemoryForecastAlpha returns the one-bar-ahead forecast of the fading-memory polynomial of the given degree, the value at the next bar of the polynomial fitted to the whole past by least squares with weights that decay by the factor 1 - alpha a bar. At bar t it is F[t] = p(t+1) = c[0] + c[1] + … + c[m], the forecast for bar t+1 stamped at bar t, the bar on which it is known; this is Meyers’s traded curve, his X1est(t+1) in 1998 and p_est(t+1) in 2002. FadingMemoryLevelAlpha sets out the filter, its gains, its seed and its sources in full.

alpha must be in (0, 1] and degree from 0 to 4; any other value gives len(s) zeros. There is no warm-up: the first valid index is 0. Bar 0 is s[0] (+0 when s[0] is -0), and the filter is recursive, so it is not start-invariant: the method on a slice that starts later gives different values. From the first bar at which the value is NaN or infinite, or an updated state is not finite, every bar is NaN, the same bar as in the level and the velocity, and the recursion does not recover.

Degree 0 equals the level, FadingMemoryLevelAlpha(alpha, 0), bit for bit. With E1 = XAverageAlpha(alpha) and E2 the same average of E1, the degree-1 forecast is 2E1[t] - E2[t-1] to rounding for t >= 1, and it is HoltForecast(1-theta^2, (1-theta)/(1+theta), 0) to rounding, Holt’s linear method at Brown’s constants. Kalman(k1) (Banes) is a one-step predictor whose second gain is sqrt(2 k1); with k1 = (1-theta)^2 its gains are the degree-1 gains only at theta = sqrt(2) - 1, that is alpha = 2 - sqrt(2) (an equivalent period of about 2.41), and only there does it equal the degree-1 forecast, to rounding.

Meyers’s 1998 trading oscillator TOSC is the degree-1 forecast less the exponential moving average, his X1est(t+1) less his X0est. On closing prices c:

f := c.FadingMemoryForecastAlpha(alpha, 1)
tosc := f.Sub(c.XAverageAlpha(alpha))

TOSC equals (E1 - E2)/(1-alpha) to rounding for alpha < 1 and is the degree-1 velocity divided by alpha. Meyers trades long above a threshold dup and short below -ddn, thresholds in price units (his dollar up and dollar down), whose optimised values ran from 0.1 to 0.525 on yen futures with N = 20 or 25 and alpha = 2/(N+1).

Meyers’s 2002 IBM system on one-minute bars is FadingMemoryForecastAlpha(0.055, 4): his beta is 0.945, so pass alpha = 0.055 itself, a period of about 35.4 that no integer n gives (1 - 0.945 in float64 is not 0.055). He trades the forecast by percent reversal, a strict move of more than a fraction from the forecast’s running extreme, which is DirectionalChange applied to the forecast with his up and down fractions as theta and thetaDown; the rule itself is not built here.

Sources: Dennis Meyers, “Trading IBM Intraday”, Stocks & Commodities V20:6 (June 2002), pp. 68-75; Dennis Meyers, “The Yen, Recursed”, Stocks & Commodities V16:12 (December 1998), pp. 547-558, with an Excel sidebar by Thom Hartle.

FadingMemoryLevel

func (s Series) FadingMemoryLevel(n, degree int) Series

FadingMemoryLevel returns the value at the current bar of the fading-memory polynomial of the given degree, the polynomial fitted to the whole past by least squares with weights that decay geometrically, with the smoothing constant set by a period of n bars. It is FadingMemoryLevelAlpha(2/(float64(n)+1), degree), bit for bit, with Meyers’s conversion of his period N to the smoothing constant, alpha = 2/(n+1): n = 1 gives alpha = 1, and n < 1 gives len(s) zeros. The degree runs from 0 to 4, and any other degree gives len(s) zeros.

There is no warm-up: the first valid index is 0. The filter is recursive, so it is not start-invariant: the method on a slice that starts later gives different values. From the first bar at which the value is NaN or infinite, or an updated state is not finite, every bar is NaN, and the recursion does not recover.

Degree 0 is XAverageAlpha(2/(float64(n)+1)) to rounding, not XAverage(n), which seeds with the simple average of the first n values; likewise TEMA(n), the triple exponential moving average built from XAverage(n), equals the degree-2 level only after its seeds have decayed. Degree 1 is, to rounding, Mulloy’s DEMA1 with his simple seed, both averages seeded with the first value (Stocks & Commodities V12:1, January 1994), the third form of the DEMA composition under “Moving Averages” in the manual. FadingMemoryLevelAlpha gives the definition, the gains, the seed, the identities and the sources.

FadingMemoryLevelAlpha

func (s Series) FadingMemoryLevelAlpha(alpha float64, degree int) Series

FadingMemoryLevelAlpha returns the value at the current bar of the fading-memory polynomial of the given degree, the polynomial fitted to the whole past by least squares with weights that decay by the factor 1 - alpha a bar. At each bar t it fits the polynomial p of degree m = degree that minimises the sum over tau >= 0 of theta^tau (x[t-tau] - p(t-tau))^2, where x is the series and theta = 1 - alpha, and returns p(t), the fitted level. FadingMemoryForecastAlpha and FadingMemoryVelocityAlpha return the value p(t+1) and the slope p’(t+1) of the same polynomial at the next bar, from the same filter. The degree runs from 0 to 4 (Meyers calls it the order, or the degree): degree 0 is the exponential moving average, degree 1 is Brown’s double exponential smoothing (level and trend), degree 2 is Brown’s triple smoothing, and degrees 3 and 4 are Meyers’s higher orders.

The kernel is Morrison’s fading-memory polynomial filter in scaled-derivative form. Write the fitted polynomial about bar t as p(t+h) = c[0] + c[1] h + … + c[m] h^m, so that c[0] is the level and c[1] the slope per bar at t (c[k] is the k-th derivative divided by k!, Morrison’s scaled derivatives), and cp for the prediction of these coefficients for the next bar. Each bar takes the one-step error e of the previous prediction, updates the state with Morrison’s gains g[i] and predicts the next bar:

e     = x[t] - cp[0]
c[i]  = cp[i] + g[i] e                     for i = 0 to m
cp[i] = sum over j >= i of C(j, i) c[j]    for i = 0 to m

The level is c[0], the forecast cp[0] and the velocity cp[1]. The gains are polynomials in theta, with g[0] = 1 - theta^(m+1); the degree-1 gains are 1 - theta^2 and (1-theta)^2. Each gain is computed as an exact rational from the binary64 alpha with math/big and rounded once to the nearest float64 with big.Rat’s Rat.Float64, so every platform has the same gains, and every product in the recursion is rounded on its own rather than fused into a multiply-add, so the outputs are the same bits on every platform.

In exact arithmetic the filter equals Meyers’s Meixner recursion and the binomial cascade of exponential moving averages: with E1 = XAverageAlpha(alpha), E2 the same average of E1 and so on, the level of degree m is the sum over k = 1 to m+1 of (-1)^(k+1) C(m+1, k) E^k, which is E1, 2E1 - E2, 3E1 - 3E2 + E3 and so on, and the three forms agree exactly from bar 0, sharing the seed below.

The past before bar 0 is taken to be flat at the first value: the prediction for bar 0 is s[0] with a slope and higher derivatives of 0, which is Brown’s natural start, and the one the cascade’s averages share, each being seeded with its first value. So bar 0 is s[0] in the level and the forecast (+0 when s[0] is -0) and +0 in the velocity, and a constant series is returned exactly. There is no warm-up: the first valid index is 0. Nothing is masked: the early bars carry the seed’s influence, which decays like theta^t times a polynomial of degree m in t, and which the caller trims if they wish. On the input 1, 0, 0, …, where each output is the weight that bar 0 and the flat past before it still carry, the degree-1 level at N = 25 (alpha = 2/26) is -0.0845 at bar 40 and -0.0520 at bar 50, so bar 0 still carries 8.5% of its weight at bar 40 and 5.2% at bar 50, and the level stays within 0.01 of 0 only from bar 78 on. The Excel sidebar of Meyers’s 1998 article, by Thom Hartle, seeds the discounted sum b_0 with the first value rather than the first value divided by alpha, which leaves a large transient, so Hartle says to let about 50 days pass; Meyers’s own system needs 40 bars of data before its first signal; both are at N = 25, and even with this seed both are on the short side at degree 1. The seed decays faster for a smaller period.

The filter is recursive, so each value depends on every earlier value and on the seed: it is not start-invariant, since the method on a slice that starts later gives different values, but it is causal and does not repaint, the outputs on s[:j] being the first j outputs on s.

alpha is the smoothing constant, in (0, 1], and theta = 1 - alpha is Meyers’s beta and Morrison’s theta. alpha = 1 is the limit theta = 0, at which the fit interpolates the last degree+1 values (the flat past standing in for values before bar 0), and every alpha down to 5e-324 is valid. The degree runs from 0 to 4. Any other alpha (0 or less, above 1, NaN or infinite) or degree gives len(s) zeros, and an empty series gives an empty result.

From the first bar at which the value is NaN or infinite, or an updated state is not finite (an overflow, as on Series{1e308, -1e308} at bar 1), every bar of every output is NaN, and earlier bars are unchanged. FadingMemoryLevelAlpha, FadingMemoryForecastAlpha and FadingMemoryVelocityAlpha check the same states, so they go NaN on the same bar. The recursion does not recover from a lost state and does not restart, not even at alpha = 1, unlike XAverageAlpha(1), which copies its input.

Identities with other methods, with E1, E2 and so on as above: degree 0 is XAverageAlpha(alpha) to rounding. Degree 1 is 2E1 - E2 to rounding, and it is Holt’s linear method at Brown’s constants to rounding: the level is HoltLevel(1-theta^2, (1-theta)/(1+theta), 0), and the velocity and the forecast are HoltTrend and HoltForecast at the same constants, which start, like this filter, at the level s[0] with velocity 0. The degree-2 level is the cascade 3E1 - 3E2 + E3 to rounding, and every degree is its binomial cascade to rounding. TEMA(n) is the same cascade built from XAverage(n), which passes the input through for n-1 bars and seeds each of its three averages with an n-bar simple average, so it equals FadingMemoryLevel(n, 2) only after its seeds have decayed.

Bit for bit, a constant series gives that constant in the level and the forecast and +0 in the velocity on every bar; scaling the series by a power of two scales every output exactly when no operation overflows, underflows or meets a subnormal value; and negating the series negates every non-zero output, every zero output being +0 both times.

A polynomial of degree k <= m is tracked once the seed has decayed: the level tends to its value at t, the forecast to its value at t+1 and the velocity to its slope at t+1. At alpha = 2/11 (a period of n = 10), on a line, a quadratic, a cubic and a quartic, every output of every degree m >= k is within 1e-9 of its target, relative to the larger of 1 and the size of the polynomial at t+1, from bar 150 on. A polynomial of higher degree than m is not tracked: a quadratic through degree 1 lags by a constant amount. The equal-weight least-squares polynomial over a window of n bars is PolyFit.

The cost is O(degree^2) per bar in the recursion’s terms, though the kernel runs all five states whatever the degree, so every degree takes the same time, and O(len(s)) per call, with the gains computed once per call. A caller who wants the level, the forecast and the velocity calls three methods, which run the filter three times, as HoltLevel, HoltTrend and HoltForecast do.

Sources: Norman Morrison, Introduction to Sequential Smoothing and Prediction (McGraw-Hill, 1969), the fading-memory polynomial filter in scaled-derivative form and its gains, also in Eli Brookner, Tracking and Kalman Filtering Made Easy (Wiley, 1998), section 7.2; Robert G. Brown, Smoothing, Forecasting and Prediction of Discrete Time Series (Prentice-Hall, 1963), double and triple exponential smoothing; Dennis Meyers, “The Yen, Recursed”, Stocks & Commodities V16:12 (December 1998), pp. 547-558, degrees 0 and 1 by the Meixner recursion, with an Excel sidebar by Thom Hartle; Dennis Meyers, “Trading IBM Intraday”, Stocks & Commodities V20:6 (June 2002), pp. 68-75, the degree-4 forecast; and Dennis Meyers, “Trading Eurodollar One-Minute Bar Charts”, Parts 1 and 2, Stocks & Commodities V32:7 (June 2014), pp. 32-35, and V32:8 (July 2014), the general Meixner recursion and the velocity.

FadingMemoryVelocity

func (s Series) FadingMemoryVelocity(n, degree int) Series

FadingMemoryVelocity returns the velocity of the fading-memory polynomial of the given degree, its slope per bar at the next bar, with the smoothing constant set by a period of n bars. It is FadingMemoryVelocityAlpha(2/(float64(n)+1), degree), bit for bit, with Meyers’s conversion of his period N to the smoothing constant, alpha = 2/(n+1): n = 1 gives alpha = 1, and n < 1 gives len(s) zeros. The degree runs from 0 to 4, and any other degree gives len(s) zeros; degree 0 gives +0 on every bar before the first non-finite one.

There is no warm-up: the first valid index is 0. The filter is recursive, so it is not start-invariant: the method on a slice that starts later gives different values. From the first bar at which the value is NaN or infinite, or an updated state is not finite, every bar is NaN, and the recursion does not recover.

FadingMemoryVelocityAlpha describes the velocity, its identities and Meyers’s trading rule for it, and FadingMemoryLevelAlpha gives the definition, the gains, the seed and the sources.

FadingMemoryVelocityAlpha

func (s Series) FadingMemoryVelocityAlpha(alpha float64, degree int) Series

FadingMemoryVelocityAlpha returns the velocity of the fading-memory polynomial of the given degree, the slope per bar at the next bar of the polynomial fitted to the whole past by least squares with weights that decay by the factor 1 - alpha a bar. At bar t it is V[t] = p’(t+1) = c[1] + 2 c[2] + 3 c[3] + 4 c[4], the slope at t+1, the point at which Meyers evaluates it in 2014, forward in time and so positive when the fit is rising, in units of the series per bar. For degrees 0 and 1 it is also the slope at t, +0 and c[1]; from degree 2 on it differs from the slope at t, which is c[1]. Degree 0 gives +0 on every bar before the first non-finite one. FadingMemoryLevelAlpha sets out the filter, its gains, its seed and its sources in full.

alpha must be in (0, 1] and degree from 0 to 4; any other value gives len(s) zeros. There is no warm-up: the first valid index is 0. Bar 0 is +0, and the filter is recursive, so it is not start-invariant: the method on a slice that starts later gives different values. From the first bar at which the value is NaN or infinite, or an updated state is not finite, every bar is NaN, the same bar as in the level and the forecast, and the recursion does not recover.

With E1 = XAverageAlpha(alpha) and E2 the same average of E1, the degree-1 velocity is (alpha/(1-alpha))*(E1 - E2) to rounding for alpha < 1, and it is HoltTrend(1-theta^2, (1-theta)/(1+theta), 0) to rounding, Holt’s linear method at Brown’s constants. Meyers’s 1998 trading oscillator TOSC, the degree-1 forecast less XAverageAlpha(alpha), is V/alpha.

Meyers (2014) buys when the velocity rises above a threshold vup and sells when it falls below -vdn, thresholds whose units he does not state; with this method that is a threshold rule on its output. His article prints the velocity as a derivative in the lag argument, which taken literally has the opposite sign to the forward velocity returned here.

Source: Dennis Meyers, “Trading Eurodollar One-Minute Bar Charts”, Stocks & Commodities V32:7 (June 2014), pp. 32-35, with Part 2 in Stocks & Commodities V32:8 (July 2014).

FibonacciWeights

func FibonacciWeights(n int) []float64

FibonacciWeights returns the first n Fibonacci numbers, 1, 1, 2, 3, 5, 8 and so on, oldest first, rising to the current bar: the weights of pandas-ta’s fwma, which computes them inexactly by Binet’s formula. FibonacciWeights(1) is [1]. The weights are exact through F_78 (n <= 78) and rounded beyond. It returns nil when n < 1 or when the newest weight overflows to +Inf, which happens from n = 1477.

FIR

func (s Series) FIR(weights []float64) Series

FIR returns the weighted moving average of s with the caller’s weights, a finite impulse response filter. With n = len(weights), each bar t >= n-1 is

out[t] = (weights[0]*s[t-n+1] + ... + weights[n-1]*s[t]) / sw

where sw is the sum of the weights, accumulated from weights[0]. weights[0] multiplies the oldest bar of each window and weights[n-1] the current bar, the convention of pandas-ta, TA-Lib and TradingView, and of Hann. The weights are not normalised, validated, copied or modified. Each product is rounded on its own and the products are summed oldest first, so every platform gives the same bits; a zero result is +0.

The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1. Trim it with Window(-(n-1)) for n >= 2 (Window(0) is empty) or mask it with SetN(n-1, math.NaN()).

Empty weights, n > len(s) or a weight sum of exactly 0 give len(s) zeros, aligned with s; weights that sum to zero cannot be normalised, so use FIRSum for them. A NaN in a window gives NaN at that bar, and a NaN weight gives NaN at every valid bar. Infinities follow IEEE arithmetic: 0*Inf is NaN, so a zero weight does not mask an infinite value, and infinite weights or an overflowing weight sum are used as they are. There is no flat-window rule: a flat window of x gives x to rounding.

Each valid bar costs O(n) and depends only on its own window. With finite non-negative weights whose sum does not overflow, the output stays within the window’s range, to rounding; negative weights (detrenders, Savitzky-Golay) can overshoot it.

s.FIR(HannWeights(n)) equals s.Hann(n) over the whole series: bit for bit where multiply-adds are not fused (amd64 at GOAMD64 v1 or v2), apart from the sign of a zero (a quotient that underflows to -0 is -0 in Hann and +0 here); where Hann’s own sum is fused (arm64), to within a few ulps of the weighted absolute mean sum|w[j]*x[j]|/sw, which is a few ulps of the result on price levels and far more, relative to the result, where it cancels to near 0. s.FIR(LinearWeights(n)) equals s.WAverage(n) at every t >= n-1: bit for bit on integer-valued data whose products and partial sums, taken in either order, stay below 2^53, and otherwise to within rounding of the weighted terms in the same sense, as WAverage sums newest first (and is fused on arm64). Their warm-ups differ: WAverage passes the input through.

FIRSum

func (s Series) FIRSum(weights []float64) Series

FIRSum returns the unnormalised dot product of the weights with each window of s. With n = len(weights), each bar t >= n-1 is

out[t] = weights[0]*s[t-n+1] + ... + weights[n-1]*s[t]

with weights[0] on the oldest bar and weights[n-1] on the current bar, each product rounded on its own and summed oldest first, as in FIR. It is for weights that sum to zero (detrenders), which FIR rejects, and for weights that are already normalised, such as those of ALMAWeights, which ALMA passes to it (TA-Lib, Tulip and pandas-ta normalise ALMA’s weights first; the Pine version on TradingView’s ta.alma page divides the weighted sum by the sum of the weights). For a finite non-zero weight sum sw, s.FIR(weights)[t] is out[t]/sw + 0, bit for bit. FracDiff and PolyFit are this sum over FracDiffWeights and PolyFitWeights, bit for bit.

The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1; trim or mask it as for FIR. Empty weights or n > len(s) give len(s) zeros, aligned with s. The weights are not validated, copied or modified, and non-finite values and weights propagate as in FIR. Each valid bar costs O(n) and depends only on its own window.

HannWeights

func HannWeights(n int) []float64

HannWeights returns the n Hann window weights 1 - cos(2pi(j+1)/(n+1)) for j = 0 to n-1, oldest first, symmetric in exact arithmetic and heaviest in the middle: exactly the weights of Hann, so s.FIR(HannWeights(n)) reproduces s.Hann(n) (see FIR for the platform caveat). It returns nil when n < 1; a larger n must be allocatable.

HMA

func HMA(s Series, n int) Series

HMA is an alias for HullAverage.

HoltForecast

func (s Series) HoltForecast(alpha, beta, gamma float64) Series

HoltForecast returns the one-bar-ahead forecast of Holt’s exponential smoothing with an acceleration term, the three-state “Holt-Winter formula” of Ichu Cheng’s “Holt-Winter Channel”: at bar t it is F + V + 0.5 A after the update at bar t, the level plus the velocity plus half the acceleration, which is the forecast for bar t+1. HoltLevel sets out the level F, the velocity V and the acceleration A in full.

alpha in (0, 1], beta in [0, 1] and gamma in [0, 1], all finite, weight the newest information in the level, the velocity and the acceleration. There are no defaults; Cheng’s worksheet uses 0.5 for each. Invalid parameters give len(s) zeros. With gamma = 0 this is the forecast of Holt’s linear method, level plus velocity, and with beta = 0 it is the exponential moving average.

The first valid index is 0. There is no warm-up: bar 0 is s[0]. The smoother is recursive, so each value depends on every earlier price, and the method on a slice that starts later gives different values. The effect of the seed decays only where the recursion is stable; HoltLevel says when it is not.

From the first bar at which the price is NaN or infinite, or the level, velocity, acceleration or forecast after the update is not finite, every bar is NaN; earlier bars are unchanged. HoltLevel, HoltTrend and HoltForecast check the same values, so they go NaN on the same bar. There is no seasonal term: Winters’s (1960) method is seasonal, but Cheng, who names the formula after Holt and Winters, finds that seasonal models “may not be applicable to stock market analysis”.

Sources: Ichu Cheng, “Holt-Winter Channel: taking the formula one step beyond”, Technical Analysis of Stocks & Commodities, August 1988, which cites Holt (1957) and Winters (1960).

HoltLevel

func (s Series) HoltLevel(alpha, beta, gamma float64) Series

HoltLevel returns the level of Holt’s exponential smoothing with an acceleration term, the three-state “Holt-Winter formula” of Ichu Cheng’s “Holt-Winter Channel”. The smoother tracks a level F, a velocity V and an acceleration A, which start at s[0], 0 and 0. At each later bar t, the forecast made at bar t-1, F + V + 0.5 A, is blended with the price to give the new level; the velocity is smoothed towards the change in level, and the acceleration towards the change in velocity:

F[t] = (1-alpha) (F[t-1] + V[t-1] + 0.5 A[t-1]) + alpha s[t]
V[t] = (1-beta) (V[t-1] + A[t-1]) + beta (F[t] - F[t-1])
A[t] = (1-gamma) A[t-1] + gamma (V[t] - V[t-1])

HoltTrend returns V, HoltForecast returns F + V + 0.5 A (the forecast for the next bar) and HoltWintersBand returns that forecast plus a multiple of the standard deviation of the one-step forecast errors.

alpha in (0, 1], beta in [0, 1] and gamma in [0, 1], all finite, weight the newest information in the level, the velocity and the acceleration. There are no defaults; Cheng’s worksheet uses 0.5 for each. Invalid parameters give len(s) zeros.

With gamma = 0 the acceleration stays 0 and this is Holt’s linear (double exponential) method; with beta = 0 the velocity and the acceleration stay 0, whatever gamma is, and this is the exponential moving average alpha s[t] + (1-alpha) F[t-1], seeded with s[0]. Mulloy’s DEMA2 for a period w, with his alpha = 2/(w+1) and the beta = 2/(w/2+1) of his example (he notes that beta need not be tied to w), is HoltLevel(2/(w+1), 2/(w/2+1), 0) with w a float64 (an int period needs float64(w), since 2/(w+1) is 0 in integer arithmetic), with the start he gives, the level at s[0] and the velocity at 0. Brown’s double exponential smoothing, FadingMemoryLevelAlpha(a, 1) with theta = 1 - a, is HoltLevel(1 - theta^2, (1-theta)/(1+theta), 0) to rounding, with HoltForecast and HoltTrend at those constants its forecast and velocity. There is no seasonal term: Winters’s (1960) method is seasonal, but Cheng, who names the formula after Holt and Winters, finds that seasonal models “may not be applicable to stock market analysis”.

The first valid index is 0. There is no warm-up: bar 0 is s[0]. The smoother is recursive, so each value depends on every earlier price, and the method on a slice that starts later gives different values. The effect of the seed decays only where the recursion is stable, as it is whenever gamma = 0 and at Cheng’s constants. A gamma well above a small alpha makes it unstable: at alpha = 0.07, beta = 0.14 and gamma = 0.3 any difference grows by about 3% a bar, so the level swings ever further from the price.

From the first bar at which the price is NaN or infinite, or the level, velocity, acceleration or forecast after the update is not finite, every bar is NaN; earlier bars are unchanged. HoltLevel, HoltTrend and HoltForecast check the same values, so they go NaN on the same bar.

Sources: Ichu Cheng, “Holt-Winter Channel: taking the formula one step beyond”, Technical Analysis of Stocks & Commodities, August 1988, which cites Holt (1957) and Winters (1960); Patrick G. Mulloy, “Smoothing Data With Less Lag”, Technical Analysis of Stocks & Commodities, February 1994 (DEMA2).

HoltTrend

func (s Series) HoltTrend(alpha, beta, gamma float64) Series

HoltTrend returns the velocity of Holt’s exponential smoothing with an acceleration term, the three-state “Holt-Winter formula” of Ichu Cheng’s “Holt-Winter Channel”: the smoothed change in level per bar. The velocity V starts at 0, and at each later bar t it is (1-beta) times the previous velocity plus acceleration, V[t-1] + A[t-1], plus beta times the change in level, F[t] - F[t-1]. HoltLevel sets out the level F and the acceleration A in full.

alpha in (0, 1], beta in [0, 1] and gamma in [0, 1], all finite, weight the newest information in the level, the velocity and the acceleration. There are no defaults; Cheng’s worksheet uses 0.5 for each. Invalid parameters give len(s) zeros. With gamma = 0 there is no acceleration (Holt’s linear method), and with beta = 0 the velocity stays 0.

The first valid index is 0. There is no warm-up: bar 0 is 0, the starting velocity. The smoother is recursive, so each value depends on every earlier price, and the method on a slice that starts later gives different values. The effect of the seed decays only where the recursion is stable; HoltLevel says when it is not.

From the first bar at which the price is NaN or infinite, or the level, velocity, acceleration or forecast after the update is not finite, every bar is NaN; earlier bars are unchanged. HoltLevel, HoltTrend and HoltForecast check the same values, so they go NaN on the same bar. There is no seasonal term: Winters’s (1960) method is seasonal, but Cheng, who names the formula after Holt and Winters, finds that seasonal models “may not be applicable to stock market analysis”.

Sources: Ichu Cheng, “Holt-Winter Channel: taking the formula one step beyond”, Technical Analysis of Stocks & Commodities, August 1988, which cites Holt (1957) and Winters (1960).

HoltWintersBand

func (s Series) HoltWintersBand(alpha, beta, gamma, delta, k float64) Series

HoltWintersBand returns the one-bar-ahead forecast of Holt’s exponential smoothing with an acceleration term (HoltForecast) plus k times the exponentially weighted standard deviation of the past one-step forecast errors: a causal form of the bands of Ichu Cheng’s “Holt-Winter Channel”. The error at bar t is s[t] minus the forecast for bar t made at bar t-1. Their variance starts at bar 1 as the square of the first error, and at each later bar it is the previous variance times (1-delta) plus delta times the square of the new error. The band at bar t is the forecast for bar t+1 plus k times the square root of the variance at bar t.

alpha in (0, 1], beta in [0, 1] and gamma in [0, 1] weight the newest information in the level, the velocity and the acceleration, as in HoltLevel; delta in (0, 1] weights the newest squared error in the variance; and k, of either sign, sets the width and the side (a positive k gives an upper band, a negative k a lower one, and k = 0 the forecast itself from bar 1 on). All must be finite. There are no defaults; Cheng’s worksheet uses 0.5 for alpha, beta, gamma and delta (his a, b, c and d) and k = 1 and -1 (his comfort factor T, added for the upper band and subtracted for the lower). Invalid parameters give len(s) zeros.

The band at bar t is for the close of bar t+1 and uses every error up to bar t, so it is causal: test s[t+1] against band[t], that is, s against band.Shift(1). Cheng’s printed worksheet judges each bar against a band built with that bar’s own error, and so is not causal. There is no seasonal term: Winters’s (1960) method is seasonal, but Cheng, who names the formula after Holt and Winters, finds that seasonal models “may not be applicable to stock market analysis”.

The warm-up reads 0, not NaN: bar 0 is 0 and the first valid index is 1.

The smoother is recursive, so each value depends on every earlier price, and the method on a slice that starts later gives different values. The effect of the seed decays only where the recursion is stable; HoltLevel says when it is not.

From the first bar at which the price is NaN or infinite, or the level, velocity, acceleration, forecast, variance or band after the update is not finite, every bar is NaN, from bar 1 on (bar 0 stays 0); earlier bars are unchanged. The band checks the variance and itself as well as the values that HoltLevel, HoltTrend and HoltForecast check, so it can go NaN where they stay finite, for example when the square of an error overflows.

Sources: Ichu Cheng, “Holt-Winter Channel: taking the formula one step beyond”, Technical Analysis of Stocks & Commodities, August 1988, which cites Holt (1957) and Winters (1960).

HullAverage

func (s Series) HullAverage(n int) Series

HullAverage returns the Hull moving average over the last <n>{=html} values in the provided Series. If there is insufficient input to provide an average over <n>{=html} values, the original Series is returned.

The first bars pass the input through, as its inner weighted averages do, then mix pass-through and averaged values; the result is a clean Hull average from bar n + floor(sqrt(n)) - 2.

LinearWeights

func LinearWeights(n int) []float64

LinearWeights returns the n linear weights 1, 2, …, n, oldest first, so the current bar weighs most: the weights of WAverage, unnormalised, and exact. s.FIR(LinearWeights(n)) matches s.WAverage(n) from index n-1; see FIR. It returns nil when n < 1; a larger n must be allocatable.

McGinley

func (s Series) McGinley(n int, k ...float64) Series

McGinley returns the McGinley Dynamic of s, John McGinley’s adaptive moving average, as TA-Lib’s TA_MCGD computes it (bit for bit on the bars TA-Lib returns, from n-1 on, wherever this result is finite). The line speeds up when the price falls below it and slows down when the price rises above it. It starts at the first price, and at each later bar it moves towards the price by the gap between them divided by k times n times the fourth power of the ratio of the price to the previous line:

MD[0] = s[0]
MD[t] = MD[t-1] + (s[t] - MD[t-1]) / (k n (s[t]/MD[t-1])^4)

The fourth power is taken as the square of the square of the ratio, as TA-Lib takes it.

n is the period, at least 1; the default, as in TradingView and TA-Lib, is n = 14. k, optional, multiplies n (pandas-ta’s denominator multiplier c, which it notes is sometimes set to 0.6; Twomey’s formula has none), so the line moves as it would with a period of k times n. While k times n is at least 1, a smaller k follows the price more quickly; below 1 each step overshoots the price. No k means k = 1, which gives exactly the period n; one value is k; more than one value is invalid. Invalid parameters (n below 1, a k that is not finite and positive, or more than one k) give len(s) zeros, and an empty s gives an empty result.

The first valid index is 0. There is no warm-up: bar 0 is s[0]. The Dynamic is recursive, so each value depends on every earlier price, and the method on a slice that starts later gives different values while the effect of the seed decays.

From the first bar at which the price is NaN or infinite, the ratio of the price to the previous line is not finite or not positive (a zero price, a price and a line of opposite signs, or a zero line), or the new line is not finite, every bar is NaN; earlier bars are unchanged. A consistently negative series is fine, as the ratio stays positive: apart from overflow and underflow, only a sign change (a zero price included), a zero line or a step that carries the line to 0 or past it gives NaN. A price below (k n)^(-1/4) times the line makes the step overshoot the price, and a price below r times the line, where r is the root of k n r^4 + r = 1, carries the line to 0 or past it (for n = 14 and k = 1, below about 0.517 and 0.446 times the line); every later bar is then NaN. TA-Lib holds the previous value only where the new value would not be finite; where the ratio is negative or the line is 0 it carries on with the finite value it computes.

Sources: John McGinley (1990s); Brian Twomey, “The McGinley Dynamic”, Technical Analysis of Stocks & Commodities, March 2010; TA-Lib’s TA_MCGD.

Mean

func (s Series) Mean(n int) Series

Mean is an alias for Average.

PascalWeights

func PascalWeights(n int) []float64

PascalWeights returns row n-1 of Pascal’s triangle, the binomial coefficients C(n-1, j) for j = 0 to n-1, oldest first, symmetric and heaviest in the middle: the weights of pandas-ta’s pwma. It builds the row by the additive recurrence in O(n^2) time; the weights are exact for n <= 57 and rounded beyond. It returns nil when n < 1 or when a weight overflows to +Inf, which happens from n = 1031.

PowerWeights

func PowerWeights(n int, a float64) []float64

PowerWeights returns the n weights (j+1)^a for j = 0 to n-1, oldest first: the general weighted moving average of Hutchinson and Zhang (“Weighted Moving Averages”, Stocks & Commodities V11:12, 1993). a = 0 gives equal weights (the SMA) and a = 1 exactly LinearWeights(n); as a grows, the weight moves to the current bar and FIR tends to the current value. It returns nil when n < 1, when a is NaN, infinite or negative, or when the newest and largest weight overflows to +Inf; a larger n must be allocatable.

RMA

func (s Series) RMA(n int) Series

RMA returns Wilder’s smoothing of the series over n bars, also called the smoothed moving average (SMMA): the exponential moving average with alpha = 1/n, seeded at bar n-1 with the simple average of the first n values (summed in order from 0, then divided by n):

y[n-1] = (s[0] + s[1] + ... + s[n-1]) / n
y[t]   = (1/n)*s[t] + (1 - 1/n)*y[t-1]   for t >= n

The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1. Trim or mask the warm-up before use, since 0 is also a legitimate reading. n is in bars; n < 1 is invalid and gives aligned zeros (len(s) zeros), and n greater than len(s) gives len(s) zeros, as there is no valid index. RMA(1) is the input with each non-finite value replaced by NaN.

This is Wilder’s smoothing (New Concepts in Technical Trading Systems, 1978) as Achelis gives it (Technical Analysis from A to Z), TA-Lib’s TA_RMA, TradingView’s ta.rma and “SMMA”, Tulip’s wilders and pandas-ta’s smma. pandas-ta’s rma is the first-value-seeded form, which is XAverageAlpha(1/float64(n)), not RMA. XAverage(2n-1) has the same constant, 1/n, but seeds over 2n-1 bars, so it converges to RMA(n) only as the seed decays. Applied to the true range it gives the average true range: c.TrueRange(h, l, c).RMA(n) is TradingView’s ta.atr, and ATRWilder is the TA-Lib convention, which leaves out bar 0’s true range.

A NaN or infinite value, or an overflow, makes the output NaN from that bar on (from bar n-1 if it is in the seed), and the recursion does not recover; with n = 1 only that bar is NaN. The method is recursive, so under a sliding slice of history its value depends on where the slice starts; the seed’s weight decays by the factor 1 - 1/n per bar.

SinePedestalWeights

func SinePedestalWeights(n int, pedestalDeg float64) []float64

SinePedestalWeights returns Ehlers’s sine window on a pedestal of P = pedestalDeg degrees (Ehlers, “Windowing”, Stocks & Commodities V39:10, September 2021), oldest first: w[j] = sin(deg*pi/180), where deg runs linearly from P at j = 0 to 180-P at j = n-1; n = 1 gives [1]. The window is symmetric in exact arithmetic.

Ehlers calls this window “Hamming”, but it is not the textbook Hamming window 0.54 - 0.46*cos(…). With P = 180/(n+1) it equals SineWeights(n) to rounding, and with P = 90 every weight is 1. With P = 0 the first weight is 0 and the last is the sine of the float64 nearest pi, a tiny positive number, not 0.

It returns nil when n < 1 or when P is NaN, infinite, below 0 or above 90; a larger n must be allocatable.

SineWAverage

func (s Series) SineWAverage(n int) Series

SineWAverage returns the sine-weighted moving average of s over n bars, s.FIR(SineWeights(n)), whose weights sin(pi*(j+1)/(n+1)) run from the oldest bar (j = 0) to the current one (j = n-1), symmetric in exact arithmetic and heaviest in the middle. It is the n-point form of Lafferty’s SWMA (Lafferty, “How Smooth Is Your Data Smoother?”, Stocks & Commodities V17:6, 1999, and its sidebar “The sine-weighted moving average”) and pandas-ta’s sinwma, to rounding (pandas-ta normalises its weights first).

The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1; trim or mask it as for FIR. n < 1 or n > len(s) gives len(s) zeros without building weights.

The name SWMA means different things: Lafferty’s SWMA is this average, pandas-ta’s swma uses the triangular weights of TriangularAverage, and TradingView’s ta.swma is s.FIR(TriangularWeights(4)) to rounding.

SineWeights

func SineWeights(n int) []float64

SineWeights returns the n weights sin(pi*(j+1)/(n+1)) for j = 0 to n-1, oldest first: half a sine period, symmetric in exact arithmetic and heaviest in the middle. They are the weights of Lafferty’s sine-weighted moving average, SWMA (Lafferty, “How Smooth Is Your Data Smoother?”, Stocks & Commodities V17:6, 1999, sidebar “The sine-weighted moving average”), and of pandas-ta’s sinwma; see SineWAverage. It returns nil when n < 1; a larger n must be allocatable.

SMA

func (s Series) SMA(n int) Series

SMA is an alias for Average.

SMMA

func (s Series) SMMA(n int) Series

SMMA is an alias for RMA, Wilder’s smoothing over n bars (the smoothed moving average); see RMA for the seed, the warm-up and invalid values.

TEMA

func (s Series) TEMA(n int) Series

TEMA returns the triple-exponential moving average.

Bars 0 to n-2 pass the input through, being three nested XAverage passes, and the result is free of seed effects only from bar 3n-3. FadingMemoryLevel(n, 2) is the same cascade, 3E1 - 3E2 + E3, from averages seeded with the first value, which TEMA(n) matches to rounding once its seeds have decayed, within 1e-12 from about bar 14n on a 1% random walk, and not bit for bit.

TA-Lib’s TEMA, which pandas-ta’s tema calls when TA-Lib is installed, and Tulip’s tema stagger their seeds: each of the three averages starts on its input’s first valid bar, n-1 bars after the one before it, seeded with a simple average in TA-Lib and with a first value in Tulip. TEMA(n) seeds its second and third averages from the pass-through bars instead, so it differs from both in the warm-up; the staggered forms are among the compositions under “Moving Averages” in the manual.

HACO and HACOLT use Vervoort’s zero-lag TEMA of it, t.Add(t.Sub(t.TEMA(n))) with t = x.TEMA(n), in his printed order TMA1 + (TMA1 - TMA2); the recipes in the “Heikin-Ashi” category build his crossover from it.

TriangularAverage

func (s Series) TriangularAverage(n int) Series

TriangularAverage returns the triangular moving average of s over n bars, s.FIR(TriangularWeights(n)), whose weights 1, 2, …, 2, 1 run from the oldest bar to the current one: the kernel of TA-Lib’s TRIMA. At t >= n-1 it equals s.Average((n+1)/2).Average(n/2+1), with integer division, to rounding of the window’s terms (close in relative terms on price levels, not where the result cancels to near 0).

The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1; trim or mask it as for FIR. n < 1 or n > len(s) gives len(s) zeros without building weights.

For even n, pandas-ta’s native trima and TradeStation’s TRIMA are different kernels. The name SWMA means different things: TradingView’s ta.swma is s.FIR(TriangularWeights(4)) to rounding, pandas-ta’s swma uses these triangular weights and Lafferty’s SWMA is SineWAverage.

TriangularWeights

func TriangularWeights(n int) []float64

TriangularWeights returns the n triangular weights min(j+1, n-j) for j = 0 to n-1, oldest first: 1, 2, …, 2, 1, with two equal middle weights when n is even. The weights are exact integers and form the kernel of TA-Lib’s TRIMA and of Ehlers’s triangle window (Ehlers, “Windowing”, Stocks & Commodities V39:10, September 2021); s.FIR(TriangularWeights(4)) is TradingView’s ta.swma, to rounding. See TriangularAverage. It returns nil when n < 1; a larger n must be allocatable.

TRIMA

func (s Series) TRIMA(n int) Series

TRIMA is an alias of TriangularAverage, under TA-Lib’s name.

VIDYACMO

func (s Series) VIDYACMO(m, n int) Series

VIDYACMO returns Chande’s 1995 CMO-driven variable index dynamic average: an exponential average whose constant at each bar is 2/(n+1) times the absolute value of CMO(m) over 100, seeded with s[m-1].

The seed y[m-1] is s[m-1], the close before the first constant, and for t >= m the average is y[t] = a[t]*s[t] + (1-a[t])*y[t-1] with a[t] = EfficiencyRatio(m)[t] times 2/(n+1), each product rounded on its own; the efficiency ratio is the absolute value of CMO(m) over 100. For valid parameters the result is s.XAverageAlphaS(s.EfficiencyRatio(m).Mul(2 / (float64(n) + 1)).Right(len(s) - (m - 1))), bit for bit on every bar: Right keeps the constants from bar m-1 on, so XAverageAlphaS seeds at bar m-1 and reads its first constant at bar m. The constant is written 2 / (float64(n) + 1) so that a huge n cannot overflow.

The constant needs no clamp: EfficiencyRatio is at most 1 (it clamps its own rounding) and 2/(n+1) is at most 1 for n >= 1, so a[t] lies between 0 and 1 or is NaN, and each value that is not NaN lies in the hull of the previous value and the close, to rounding (fl(1-a[t]) + a[t] can exceed 1 by an ulp). On a rising series, where the efficiency ratio is exactly 1, the result is XAverageAlpha(2/(n+1)) of the closes from bar m-1 on, bit for bit from bar m-1. A flat window gives a constant of 0, so the average holds, where Marisch’s TradeStation listing keeps the last ratio.

Chande published it in “Identifying Powerful Breakouts Early” (Technical Analysis of Stocks & Commodities, October 1995) with the sidebar “Variable-length moving average” and the defaults m = 9 and n = 12. With the first of the sidebar’s 23 yen closes duplicated in front for its spreadsheet’s placeholder row, VIDYACMO(9, 12) gives its average at two decimals from the seed 103.2. His 1% bands are y.Mul(1.01) and y.Mul(0.99), with y the average; they and his breakout rules are compositions under “Moving Averages” in the manual. VIDYA is his 1992 standard-deviation-ratio form; pandas-ta and TradingView scripts call this CMO-driven form VIDYA.

The warm-up reads 0, not NaN: bars 0 to m-2 are 0 and the first valid index is m-1, the seed. Validation: m < 1 or n < 1 gives len(s) zeros, as does len(s) < m; len(s) == m gives the seed alone at bar m-1, and an empty series gives an empty result.

With n >= 2, a NaN close at bar p makes every bar from max(p, m) on NaN, and the seed bar m-1 too when p is m-1; an infinite close does the same from p when p >= m-1, and from p+m when p < m-1 (between, the efficiency ratio is 0, so the average holds the seed); earlier bars are unchanged. Finite closes whose difference overflows count as an infinite change: on -math.MaxFloat64 and math.MaxFloat64 the ratio is +Inf over +Inf, so VIDYACMO(1, 1) is NaN at bar 1, not the close. With n = 1 the constant is exactly 1 where the efficiency ratio is 1, and there the recursion restarts at the close: on the closes 1, 2, 3, NaN, 5, 6 and 7, VIDYACMO(2, 1) is NaN on bars 3 to 5 and restarts at bar 6.

VIDYACMO is recursive and so start-dependent: each value depends on every close from the seed on, and a later start gives other values. It is causal, costs O(1) a bar and returns a new Series without modifying the receiver.

VWAPSince

func (s Series) VWAPSince(v, anchor Series) Series

VWAPSince returns the anchored volume-weighted average price of the receiver, which holds the price, with the volume v: at each bar, the sum of price times volume divided by the sum of volume, both summed from the most recent anchor bar up to that bar, the anchor bar included. The anchor bar is inclusive, as in TradingView’s ta.vwap(source, anchor) and in the vwap of pandas-ta.

A bar is an anchor bar when its anchor value is an event: non-zero and not NaN, so +Inf and -Inf are events while 0, -0 and NaN are not. The anchor is typically 1 on the anchor bars and 0 elsewhere. A session VWAP takes the caller’s session-open events as the anchor. Several curves need one call per anchor series.

The MIDAS curves of Levine, Reyna (Technical Analysis of Stocks & Commodities, May 2001) and Coles (Technical Analysis of Stocks & Commodities, September 2008) leave the launch bar out. That is the exclusive convention of Levine, Reyna and Coles, and it is the same method with the anchor moved one bar later, which Shift(1) does:

mp := h.Add(l).Div(2)
curve := mp.VWAPSince(v, launch.Shift(1))

The price is whatever the receiver holds. TradingView’s ta.vwap variable (anchored to the session) and the vwap of pandas-ta use the typical price h.Add(l).Add(c).Div(3); ta.vwap(source, anchor) uses the source it is given; the MIDAS curves use the mid price mp above.

Alignment: v and anchor are each right-aligned with the receiver at the most recent bar, and a longer v or anchor contributes only its last len(s) values. Receiver bars before the start of v have no volume and contribute nothing to the sums (their prices are not read, so a NaN there does not poison the segment); bars before the start of anchor see no event. The result is a non-nil Series of length len(s).

NaN rules: the result is NaN before the first anchor bar. It is NaN while the volume since the anchor sums to exactly 0, so a zero-volume anchor bar reads NaN until a bar with volume, and a later zero-volume bar leaves the value unchanged. A NaN or infinite price or volume, or a product or sum that overflows, gives NaN until the next anchor bar (NaN times 0 is NaN, so a NaN price poisons the segment even where the volume is 0). A zero quotient is written as +0. Volume is not validated: negative volumes are summed as given, and a quotient that overflows (a volume sum below 1 under a price sum near the largest float) is returned as an infinity.

Where v and anchor have the receiver’s length, the volume sum is finite and not 0 and the sum of price times volume is finite, the result equals s.Mul(v).SumSince(anchor) divided by v.SumSince(anchor), bar by bar, apart from writing a zero quotient as +0. A single anchor at bar 0 gives the cumulative VWAP. The rolling n-bar form, whose sums cover the last n bars rather than the bars since an anchor, is VWMA.

The method is latched: the bars before the first anchor read NaN, so inside a slice of the inputs the bars before the slice’s first anchor read NaN even where the full series has a value from an earlier anchor.

Sources: TradingView, ta.vwap; pandas-ta, vwap; Levine (1995), as quoted by Reyna and Coles; Reyna, “Volume-Weighted Average Price”, Technical Analysis of Stocks & Commodities, May 2001; Coles, “The MIDAS Touch, Part 1”, Technical Analysis of Stocks & Commodities, September 2008.

VWEMA

func (s Series) VWEMA(v Series, n int) Series

VWEMA returns Hawkins’s volume-weighted exponential moving average of the price s with the volume v: the ratio of the EMA of price times volume to the EMA of volume.

VWEMA[t] = EMA(s*v)[t] / EMA(v)[t]

Each EMA is seeded and smoothed as in XAverage: at bar n-1 of the overlap its state y is the plain mean of its first n inputs, and each later input x updates it as

y = alpha*x + (1-alpha)*y, with alpha = 2/(n+1)

v is right-aligned with s as in VWMA, and the result, of length len(s), is 0 before the overlap. Series are paired by position only; see “Time alignment” in the manual.

The warm-up reads 0, not NaN: the first n-1 bars of the overlap are 0 whatever the data, and the first valid index is off+n-1, with off = len(s) - min(len(s), len(v)); trim or mask the warm-up before use. With n = 1 the result is the price on the overlap, as in VWMA, whatever the volume; an n below 1 or above the overlap’s length gives all zeros. For n >= 2, where the EMA of volume is 0 the result is NaN, as in VWMA.

For n >= 2 the method is recursive. A NaN or infinite price or volume, or a product that overflows, makes the result NaN from its bar on, or from bar n-1 of the overlap if it falls in the seed window, with no recovery. Its value also depends on where the history starts; the seed’s weight decays by the factor 1-alpha per bar. The seed costs O(n) once, then each bar O(1).

For n >= 2 on amd64 at GOAMD64 v1 or v2, when v has the length of s, from bar n-1 s.VWEMA(v, n) equals s.Mul(v).XAverage(n).Div(v.XAverage(n)) bit for bit where the products and both EMAs are finite and the EMA of volume is not 0, except that VWEMA writes a zero quotient as +0. Elsewhere the two agree to rounding, as XAverage’s recursion may be compiled with fused multiply-adds. At bar n-1 VWEMA equals VWMA to rounding.

VWMA

func (s Series) VWMA(v Series, n int) Series

VWMA returns the n-bar volume-weighted moving average of the price s with the volume v.

VWMA[t] = (s[t-n+1]*v[t-n+1] + ... + s[t]*v[t]) / (v[t-n+1] + ... + v[t])

pandas-ta calls it vwma, TradingView ta.vwma and TA-Lib VWMA (TA_VWMA, which agrees to rounding). With tp the typical price, tp.VWMA(v, n) is the rolling n-bar VWAP. Anchored VWAP is not this: its sums restart at an anchor such as a session open; VWAPSince computes it. For fixed weights of the caller’s choosing in place of the volumes, use FIR.

v is right-aligned with s: the two are paired at the most recent bar over their last min(len(s), len(v)) bars, and the result, of length len(s), is 0 before that overlap. Series are paired by position only; see “Time alignment” in the manual.

The warm-up reads 0, not NaN: the first n-1 bars of the overlap are 0 whatever the data, and the first valid index is off+n-1, with off = len(s) - min(len(s), len(v)); trim or mask the warm-up before use. With n = 1 the result is the price on the overlap, with no division, whatever the volume (0 included), or NaN where the price or the volume is NaN or infinite. An n below 1 or above the overlap’s length gives all zeros.

For n >= 2, a window whose volumes sum to exactly 0 gives NaN, since the 0 that Div gives would read as a price of 0. A NaN or infinite price or volume, or a product or window sum that overflows, gives NaN at each bar whose window holds it; later bars recover. Volume is not validated: negative volumes are summed as given, so the result can then fall outside the window’s price range, and a quotient that overflows is returned as an infinity.

Both window sums are compensated, as in Sum, and each bar costs O(1) amortised time. For n >= 2, when v has the length of s, from bar n-1 s.VWMA(v, n) equals s.Mul(v).Sum(n).Div(v.Sum(n)) bit for bit where both sums are finite and the volume sum is not 0, except that VWMA writes a zero quotient as +0; and with ones a series of 1s of that length, s.VWMA(ones, n) equals s.Average(n) bit for bit on finite windows.

WAverage

func (s Series) WAverage(n int) Series

WAverage returns a weighted moving average over the last <n>{=html} values in the provided Series. Bars 0 to n-2 pass the input through (result[i] = s[i]) and the first weighted average is at bar n-1; where n exceeds the length of the series the original Series is returned.

WilderAverage

func (s Series) WilderAverage(n int) Series

WilderAverage is an alias for RMA, Wilder’s smoothing over n bars; see RMA for the seed, the warm-up and invalid values.

WMA

func (s Series) WMA(n int) Series

WMA is an alias for WAverage.

XAverage

func (s Series) XAverage(n int) Series

XAverage returns an exponential moving average over the last <n>{=html} values in the provided Series. Bars 0 to n-2 pass the input through (result[i] = s[i]); bar n-1 is the simple average of the first n values, which is the seed; later bars apply the exponential recursion with alpha = 2/(n+1). Where n exceeds the length of the series there is no seed, so the original Series is returned. A value of n less than one returns a copy of the original Series.

Zeros or other warm-up values of an upstream method enter the seed and decay only gradually, so trim an upstream warm-up before smoothing, as ZLEMA, AvgVelocity and the CaseyPercentC variants do.

XAverageAlpha

func (s Series) XAverageAlpha(alpha float64) Series

XAverageAlpha returns the exponential moving average of the series with the smoothing constant alpha given directly, seeded with the first value:

y[0] = s[0]
y[t] = alpha*s[t] + (1-alpha)*y[t-1]   for t >= 1

This is the first-value seed and recursion of pandas ewm(alpha=alpha, adjust=False).mean(), Tulip’s ema (which takes a period, alpha = 2/(period+1)) and Achelis’s exponential moving average (Technical Analysis from A to Z); they agree to rounding, not bit for bit. There is no warm-up: the first valid index is 0. alpha is a fraction in (0, 1]; an alpha that is 0 or less, greater than 1, NaN or infinite is invalid and gives aligned zeros (len(s) zeros).

A NaN or infinite value, or an overflow, makes the output NaN from that bar on, and the recursion does not recover. With alpha = 1 the output is the input with each non-finite value replaced by NaN, so a bad value is NaN on its own bar only.

The method is recursive, so under a sliding slice of history its value depends on where the slice starts; the seed’s weight decays by the factor 1 - alpha per bar. XAverage(n) has alpha = 2/(n+1) but seeds with the simple average of the first n values. FadingMemoryLevelAlpha(alpha, 0) is this average to rounding, and its higher degrees are the binomial cascades of it, 2E1 - E2 and 3E1 - 3E2 + E3, also to rounding. Faith’s Turtle N (Way of the Turtle, 2007) smooths the true range with alpha = 1/20 but seeds with a 20-day simple average, so from scratch it is c.ATRWilder(h, l, c, 20) (or c.TrueRange(h, l, c).RMA(20)); XAverageAlpha(1.0/20) over a published N followed by the later true ranges continues it.

XAverageAlphaS

func (s Series) XAverageAlphaS(alpha Series) Series

XAverageAlphaS returns the exponential moving average of the series with a smoothing constant for each bar, read from alpha:

y[t] = a[t]*s[t] + (1-a[t])*y[t-1]

where a[t] is the constant paired with bar t, clamped to [0, 1]: +Inf gives 1, -Inf gives 0 and NaN stays NaN. A constant of 0 keeps the previous value for a finite input (a -0 state becomes +0), and a constant of 1 copies the input and forgets the past. With every constant equal to a valid alpha the result is XAverageAlpha(alpha) bit for bit. The constants are fractions in [0, 1]. With the constant 2/(q+1) for a period q at each bar it is the exponential analogue of AverageVariable.

The seed is the first value: the receiver’s value at the first paired bar, whose constant is not read. The warm-up reads 0, not NaN: the bars before the first paired bar are 0 and the first valid index is that bar, len(s) - min(len(s), len(alpha)). Trim or mask the warm-up before use, since 0 is also a legitimate reading. An empty or nil alpha gives aligned zeros (len(s) zeros).

A NaN or infinite value, a NaN constant or an overflow makes the output NaN from that bar on, and the recursion does not recover until a bar whose constant is 1, which restarts it at that bar’s value (NaN again if that value is not finite).

The method is recursive, so under a sliding slice of history its value depends on where the slice starts; the seed’s weight decays by the factor 1 - a[t] at each bar.

alpha is right-aligned with the receiver at the most recent bar: the last value of alpha pairs with the last bar of s. If alpha is shorter, the bars before the overlap read 0; if it is longer, its oldest values are not used. Series are paired by position only; see “Time alignment” in the manual.

XAverageHalfLife

func (s Series) XAverageHalfLife(hl float64) Series

XAverageHalfLife returns the exponential moving average of the series with the smoothing constant set by a half-life of hl bars:

alpha = 1 - exp(-ln(2)/hl)

so that (1-alpha)^hl = 1/2: after hl bars the weight of a past value has halved, and hl = 1 gives alpha = 0.5. The result is XAverageAlpha(alpha), seeded with the first value, with no warm-up: the first valid index is 0. hl is in bars and may be fractional; for daily bars and a half-life in years, multiply the years by the number of trading days per year. An hl that is 0 or less, NaN or infinite is invalid and gives aligned zeros (len(s) zeros).

A NaN or infinite value, or an overflow, makes the output NaN from that bar on, and the recursion does not recover. The method is recursive, so under a sliding slice of history its value depends on where the slice starts; the seed’s weight decays by the factor 1 - alpha per bar, halving every hl bars. For the half-life form see Grinold and Kahn, Advances in Active Portfolio Management (2019).

XMA

func (s Series) XMA(n int) Series

XMA is an alias for XAverage.

ZLEMA

func (s Series) ZLEMA(n int) Series

ZLEMA returns Ehlers’s zero-lag EMA: the EMA of the compensated price 2*s[i] - s[i-lag], lag = floor((n-1)/2), from “Zero-Lag Data Smoothers” (TASC, July 2002). ZLEMAGain is the error-correcting form of Ehlers and Way (TASC, November 2010). At period 1 the mathematical lag is zero, and ZLEMA(1) returns an independent copy of the input. At period 2 the lag is also mathematically zero; ZLEMA(2) uses its normal recurrence with that zero lag and XAverage(2)’s existing initialisation.

For n >= 3 the warm-up reads 0, not NaN: bars 0 to lag-1 are 0 and the first valid index is lag, the first bar where the compensated price exists. From bar lag the EMA runs as if the series started there: it passes the compensated price through on bars lag to lag+n-2 and is seeded at bar lag+n-1 with the simple average of its first n values. Releases before this change fed 2*s[i] into the EMA’s seed on bars 0 to lag-1 (the missing s[i-lag] read as 0), so the first values were about twice the price and decayed slowly.

ZLEMAGain

func (s Series) ZLEMAGain(n int, gainMax int) Series

ZLEMAGain implements a gain-adjusted moving average. Based on code from Robert Pardo.

gainMax is in tenths: the gains searched are j/10 for j = -gainMax..gainMax, so gainMax 50 searches -5.0 to 5.0. Bars 0 to n-1 copy XAverage(n), which passes the input through on bars 0 to n-2.

Compositions

The average compositions below mask their values with NaN before their first valid index, where the methods read +0 or pass the input through; a +0 average would put any band at 0 and a comparison would fire. Flags and positions built on masked values read 0 there. Prices are positive and finite and lengths are in bars.

CMO-driven VIDYA with 1% bands (Chande)

Tushar Chande (“Identifying Powerful Breakouts Early”, Stocks & Commodities V13:10, October 1995, pp. 418-424) sets up VIDYA “to be equivalent to a 12-day exponential moving average (Ema) driven by a nine-day CMO”, with “1% bands around VIDYA”: “Whenever the market closes above the upper band or below the lower band, we can assume that prices have broken out in that direction”, and “a reversal will be defined when prices close beyond the 1% band in the opposite direction”. The magazine’s sidebar “Variable-length moving average” (p. 424) computes it with the smoothing constant 2/(n + 1) times |CMO|/100, seeded with the close of the row before the first |CMO|. |CMO(m)|/100 is EfficiencyRatio(m), so t6VIDYACMO is XAverageAlphaS with that constant, seeded at bar m - 1.

er := c.EfficiencyRatio(m)
alpha := er.Mul(2 / float64(n+1))
vidya := c.XAverageAlphaS(alpha.Right(len(c)-(m-1))).SetN(m-1, math.NaN())
upper, lower := vidya.Mul(1.01), vidya.Mul(0.99)
book := c.PositionSigned(c.GT(upper), nil, c.LT(lower), nil)
  • Inputs: positive closes with len(c) >= m; m (9) for the CMO and n (12) for the average’s length.
  • First valid index: m - 1 for vidya, upper and lower (the seed, the close; NaN before); the first update is at bar m with the first full |CMO|, and book reads 0 until the first close outside the bands. Unmasked, XAverageAlphaS reads +0 before the seed, which would put both bands at 0 and the book long from bar 0.
  • Anchors: on the sidebar’s yen closes (8 August to 8 September 1994) with the first repeated in front, the spreadsheet’s zero placeholder, the helper gives its VIDYA and band columns on rows 9 to 24 at two decimals, EfficiencyRatio(9) its |CMO| column on rows 10 to 24, and the up and down sums its two sum columns. On Chande and Kroll’s five S&P 500 closes (“Stochastic RSI And Dynamic Momentum Index”, Stocks & Commodities V11:5, May 1993) ER(4) is 2.17/10.19 and 100 Su/(Su + Sd) the printed 60.64769382. EfficiencyRatio(m) equals the |CMO|/100 composed from the up and down sums bit for bit on the test’s walk, and to rounding in general. book equals c.GT(upper).Sub(c.LT(lower)).Hysteresis(0, 0). A flat window gives ER 0, so VIDYA holds there.
  • Traps: the seed decides two-decimal values for many bars after a spike: seeding at bar m, as the dossier did, throws away the first |CMO| and reads 104.78 at the sidebar’s row 24 against the printed 103.98. A NaN close makes VIDYA NaN for good. The method VIDYA is Chande’s 1992 standard-deviation form, a different driver; pandas-ta’s vidya takes one length for both, seeds with 0 and, without TA-Lib, stays NaN after a flat window.
  • Variant: TradeStation’s listing (Gerald Marisch, “Breaking Out Of Price Channels”, Stocks & Commodities V16:1, January 1998) keeps the last |CMO| on a flat window: er.ValueWhen(c.Delta(1).Abs().Sum(m).GT(0).Add(c.Mul(0).SetN(1, 1))) in place of er, whose bar-0 event keeps a series flat from bar 0 at 0 rather than NaN for good (the held ratio reads 0 on bars 0 to m - 1, so the same Right and mask apply).
  • Variant: the signed CMO is c.Delta(m).Div(c.Delta(1).Abs().Sum(m)).Mul(100).
  • Missing piece: Chande’s Dynamic Momentum Index, his confirmation filter (“averring that the DMI rise above 70 or fall below 30”), is left out: it is a concept of its own, not Wilder’s DMI.
  • Evidence: Chande’s charts (the yen “would have netted you a full 15 points” before commissions) and Jack Weinberg’s walk-forward test of a band system that counts up and down days instead of summing them (“Walk-Forward Testing”, Stocks & Commodities V27:1, January 2009: 225 trades, 127 winners) are on unprinted data. Evidence, not a reference.
  • Method: vidya is c.VIDYACMO(m, n) from bar m - 1, bit for bit for n below 2^53, and the method reads 0, not NaN, before; the signed CMO of the variant above is c.CMO(m) from bar m. At n = math.MaxInt the block’s 2 / float64(n+1) wraps to a negative constant, which XAverageAlphaS clamps to 0, so vidya holds its seed, where the method’s 2 / (float64(n) + 1) is 2^-62.
  • Pinned by TestRecipeT6VIDYACMO; runnable example ExampleSeries_XAverageAlphaS_vidyaCMO.

Triangular moving average (Lafferty, Johnson, Payne)

Patrick Lafferty (“How Smooth Is Your Data Smoother?”, Stocks & Commodities V17:6, June 1999, pp. 251-256) writes: “For a three-point TMA, the weights are 1,2,1. For a five-point TMA, the weights are 1,2,3,2,1”, the sum divided by the sum of the weights. TA-Lib’s source comment adds that “the TRIMA is equivalent to doing a SMA of a SMA”, the inner average over (n + 1)/2 bars and the outer over n/2 + 1, in integer division. TriangularAverage(n) computes the weights 1, 2, …, 2, 1 over n bars (a double peak for even n), and t6Triangular gives it beside that cascade.

tma := s.TriangularAverage(n).SetN(n-1, math.NaN())
casc := s.Average((n+1)/2).Average(n/2+1).SetN(n-1, math.NaN())
  • Inputs: a series and n in bars.
  • First valid index: n - 1 for both (NaN before). Unmasked, TriangularAverage reads +0 on bars 0 to n - 2, while the cascade, in either order, mixes raw values and partial averages there, since each Average passes its input through.
  • Anchors: the cascade equals the method from bar n - 1 to a few ulp; it is bit for bit for n <= 2, and on prices on a binary grid (integers or multiples of 1/32) when (n + 1)/2 is a power of two, which makes the inner division exact. The other order, s.Average(n/2+1).Average((n+1)/2), agrees there too.
  • Witnesses: A. Bruce Johnson (“Finding Cycles In Time Series Data”, Stocks & Commodities V8:8, August 1990, pp. 287-293) gives his “10-day” and “20-day” averages weights that end in a zero, so they are TriangularAverage(9) and TriangularAverage(19) (his program divides by the sum of his fractional weights, so their scale drops out): on his printed sine they give all 28 MA10 and 18 MA20 values at two decimals, and his MACD rows 20 to 27 come out causally at bar row + 8 as s.TriangularAverage(9).Shift(5).Sub(s.TriangularAverage(19)). On Lafferty’s tent (1 to 100 and back to 1) the smallest period of each lag 1 to 7 is 2, 4, …, 14, his TMA column (and his SMA column). J.S. Payne’s single spike of 100 (“A better way to smooth data”, Stocks & Commodities V7:10, October 1989) moves TriangularAverage(11) in steps of 100/36, his 2.778%; Tulip’s trima 5 test vectors reproduce; and John Ehlers’s eight-element kernel [1 2 3 4 4 3 2 1]/20 (“Windowing”, Stocks & Commodities V39:10, September 2021) is TriangularWeights(8).
  • Traps: Johnson’s own MACD reads MA10 at t + 4 and MA20 at t + 9, a centred oscillator that repaints the last 9 bars when plotted at t. Payne’s formula, W_n = N - n, gives an N-wide window N - 1 weights, while his table’s period 11 has 11. Perry Kaufman’s 2026 “triangular” listing (“Smoothing The Data”, Stocks & Commodities V44:1, January 2026) skips its second loop, whose bounds run backwards, and weights the oldest bar most: it is not a TMA.
  • Variant: other conventions for even n map to another length: TradeStation (per TA-Lib’s comment) TriangularAverage(2*(n/2) + 1), Payne and Johnson TriangularAverage(n - 1), and pandas-ta’s native path TriangularAverage(2*h - 1) with h the half-even rounding of (n + 1)/2.
  • Variant: Kaufman’s direction rule, long while the average rises and short while it falls, ties holding: c.PositionSigned(tma.GT(tma.Shift(1)), nil, tma.LT(tma.Shift(1)), nil).
  • Evidence: Kaufman’s 2018 tests (“In Search Of The Best Trend”, Stocks & Commodities V36:8, July 2018; four futures markets, US bonds, euro, S&P and crude oil, 1991 to March 2018, 8 dollars a contract per side) rank the triangular average third of five trend rules on the average across the four markets, and his 2026 “Triangular” rows come from the mis-coded loop. Evidence, not a reference.
  • Pinned by TestRecipeT6Triangular; runnable example ExampleSeries_TriangularAverage_johnson.

The cascades, the volume clock and the Bezier smoother below mask their values with NaN before their first valid index, where the methods read +0 or pass the input through (a +0 average would fire a close-above rule on every warm-up bar); the latches built on them read 0 there. Prices are positive and finite, volumes non-negative and finite, and lengths are in bars; the volume clock’s share count is in shares.

DEMA in three seed conventions (Mulloy; TA-Lib; Tulip)

Patrick Mulloy (“Smoothing Data With Faster Moving Averages”, Stocks & Commodities V12:1, January 1994, pp. 11-19) defines “DEMA1 = 2EMA1 - EMA2”, EMA2 being the EMA of EMA1, and for general use recommends seeding both averages with “the simple use of the initial database value at time 0”. The published forms differ in the seeds: TA-Lib’s DEMA, which pandas-ta calls when TA-Lib is installed, seeds each average with the simple average of its own input’s first n values, and Tulip Indicators’ dema and TA-Lib’s former MetaStock compatibility mode (a no-op since TA-Lib 0.8.1, September 2026) seed each with its input’s first value, n - 1 bars apart (how MetaStock seeds its own nested MOV is not documented); t6DEMA returns the three, dema, demaMS and Mulloy’s demaC0, the last in one shipped call. The nested form e.Mul(2).Sub(e.XAverage(n)), which is how TEMA(n) nests its averages, matches no published DEMA, and Mulloy’s own regression seed reads the first w bars at bar 0, a look-ahead in the warm-up.

e1 := c.XAverage(n)
e2 := e1.Window(-(n - 1)).XAverage(n)
dema := e1.Mul(2).Sub(e2).SetN(len(c)-len(e2)+n-1, math.NaN())
a := 2 / (1 + float64(n))
m1 := c.XAverageAlpha(a)
m2 := m1.Window(-(n - 1)).XAverageAlpha(a)
demaMS := m1.Mul(2).Sub(m2).SetN(len(c)-len(m2)+n-1, math.NaN())
demaC0 := c.FadingMemoryLevel(n, 1)
  • Inputs: closes; n >= 2 for dema and demaMS, which are all NaN when len(c) < 2n - 1 and at n = 1 (where Window(0) is empty); demaC0 accepts n = 1, where it returns the closes.
  • First valid index: 2(n - 1) for dema and demaMS, the value of the masks len(c)-len(e2)+n-1 and len(c)-len(m2)+n-1; 0 for demaC0. Unmasked, dema reads +0 on bars 0 to n - 2 and is e1 itself on bars n - 1 to 2n - 3 (twice e1 less e2’s pass-through of e1), which is why TA-Lib emits nothing before 2(n - 1).
  • Anchors: demaMS gives Tulip’s dema 5 test vector (tests/untest.txt, seven values, 84.159 to 87.646) and Achelis’s (Technical Analysis from A to Z, p. 123, as Tulip’s tests/atoz.txt prints it, six values, 172.0780 to 194.1428) to every printed digit, where dema, demaC0 and the nested form each miss both. demaC0 gives the 133.34 and 229.903 of Mulloy’s sidebar “Dema1 Derivation” on his noise-free ramp 25 + 5t at w = 11 (bars 22 and 41). On the sheet of “Smoothing Data With Less Lag” (Stocks & Commodities V12:2, February 1994, sidebar Figures 1 and 2), with his regression seeds substituted at bar 0 of XAverageAlpha(2.0 / 27) chains (slope -6.16 and intercept 448.35 over the first 26 closes, x = 0 to 25), the EMA1, EMA2 and DEMA1 columns reproduce at two decimals on all 56 rows. With m1 := c.XAverageAlpha(a), demaC0 equals m1.Mul(2).Sub(m1.XAverageAlpha(a)) and Tillson’s twicing m1.Add(c.Sub(m1).XAverageAlpha(a)) to rounding from bar 0. demaMS and demaC0 are the same bits on every platform and return a constant exactly; dema rests on XAverage, which fuses on arm64, and on a constant 0.1 at n = 3 reads 0.09999999999999999.
  • Traps: the conventions agree to 1e-12 relative only after their seeds decay, from about 12n to 14n bars; at 4n they are still 1e-5 to 1e-4 apart. Mulloy’s V12:1 worked example mixes precisions (its 449.33 and 608.72 are each within 0.01 of a computation, not of one arithmetic) and its intercept 454.513 is the line one bar before bar 0 (Excel’s x = 1 to 26), and his sidebar’s “N = 41, i = 0 .. N - 1” stops one bar short of the printed y = 230: the V12:2 sheet is the reproducible witness.
  • Variant: TA-Lib’s TEMA staging: e3 := e2.Window(-(n - 1)).XAverage(n) and e1.Mul(3).Sub(e2.Mul(3)).Add(e3).SetN(len(c)-len(e3)+n-1, math.NaN()), valid from 3(n - 1); over XAverageAlpha the same lines give Tulip’s tema. TEMA(n) nests instead, so it differs in the warm-up, as its doc says.
  • Variant: Mulloy’s MACD-DEMA1, macd := c.FadingMemoryLevel(12, 1).Sub(c.FadingMemoryLevel(26, 1)), with signal := macd.XAverageAlpha(0.2) as the nine-period signal seeded with the first MACD value, as his DEMA2 sheet seeds it (V12:2, sidebar “Calculating DEMA2 and the MACD”), trading macd.CrossesAboveS(signal) and macd.CrossesBelowS(signal).
  • Evidence: V12:2’s Figure 7 (weekly NASDAQ Composite, 30 September 1988 to 27 August 1993, 1% a transaction): MACD-DEMA1 made nine round trips with three losses and 838 per 1,000 dollars invested, the standard MACD 12 with six losses and 441; one market and one period, and the standard MACD’s round-trip count is 11 in both articles’ text but 12 in this figure. Evidence, not a reference.
  • Pinned by TestRecipeT6DEMA; runnable example ExampleSeries_FadingMemoryLevel_dema.

Tillson’s T3 and Burns’s adaptive T3

Tim Tillson (“Smoothing Techniques For More Accurate Signals”, Stocks & Commodities V16:1, January 1998, pp. 33-37) defines the generalised DEMA “GD(n,v) = EMA(n) * (1+v)-EMA(EMA(n)) * v” and “T3(n) = GD(GD(GD(n)))”, which his MetaStock code expands to “c1e6+c2e5+c3e4+c4e3” over six nested EMAs with c1 = -a^3, c2 = 3a^2 + 3a^3, c3 = -6a^2 - 3a - 3a^3 and c4 = 1 + 3a + a^3 + 3a^2. t6T3 stages the six averages as TA-Lib does (each seeded with the simple average of its own input’s first n values), which the sidebar sheet of Steve Burns’s “Tweaking The T3 Trading System” (Stocks & Commodities V19:6, June 2001, pp. 30-37) also does, and forms the coefficients as Series from the volume factor a: c.SetN(len(c), 0.7) gives Tillson’s T3, and c.R2(m).SetN(m-1, math.NaN()) Burns’s AT3, in which “r-squared is substituted in the original calculation, replacing the"hot” input entry".

e1 := c.XAverage(n)
e2 := e1.Window(-(n - 1)).XAverage(n)
e3 := e2.Window(-(n - 1)).XAverage(n)
e4 := e3.Window(-(n - 1)).XAverage(n)
e5 := e4.Window(-(n - 1)).XAverage(n)
e6 := e5.Window(-(n - 1)).XAverage(n)
a2 := a.Mul(a)
a3 := a2.Mul(a)
c1 := a3.Mul(-1)
c2 := a2.Add(a3).Mul(3)
c3 := a2.Mul(-6).Sub(a.Mul(3)).Sub(a3.Mul(3))
c4 := a.Mul(3).Add(a3).Add(a2.Mul(3)).Add(1)
t3 := c.SetN(len(c), 0).Add(e6.Mul(c1)).Add(e5.Mul(c2)).Add(e4.Mul(c3)).Add(e3.Mul(c4)).
    SetN(len(c)-len(e6)+n-1, math.NaN())
  • Inputs: closes; the volume factor a per bar, right-aligned with the closes, in Tillson’s design range 0 to 1 (“where v ranges between zero and 1”; at 1 the cascade is DEMA run three times, and above it overshoots, though his MetaStock input allows 0 to 2); n >= 2 (default 5, which Burns’s AT3 code fixes). For AT3 the caller masks the r-squared’s +0 warm-up, which the helper cannot know.
  • First valid index: 6(n - 1), 24 at n = 5, the value of the mask len(c)-len(e6)+n-1, or later where a is NaN (bar 39 for AT3 with m = 40); closes shorter than 6n - 5 bars give all NaN. A +0 T3 on the warm-up bars would fire Burns’s c.GT(at3) on every one of them.
  • Anchors: on Burns’s sheet (26 closes from 2 January 1992) the six stages match the 72 printed cells at two decimals and T3 gives the printed 411.04 and 411.54 at bars 24 and 25. Tillson’s coefficients at 0.7 are -0.34299999999999992, 2.4989999999999997, -6.0689999999999991 and 4.9129999999999994 (the sheet’s -0.343, 2.499, -6.069 and 4.913) and sum to exactly 1. With a constant factor the helper equals the four stage products with scalar coefficients summed from +0 in the order e6, e5, e4, e3, bit for bit, and a factor of 0 gives e3 bit for bit, a triple-smoothed EMA, not TEMA(n). AT3 equals a loop of TA-Lib’s stages, a direct r-squared and Tillson’s scalar coefficients to rounding.
  • Traps: Burns’s sidebar prints the general formula without the first term’s minus sign (“T3 = (a^3) * e6 + …”; the sheet’s first coefficient, -0.343, and the code’s c1:=-a*a*a; have it). TA-Lib’s comment says “T3 = GD (GD ( GD(t, Period, vFactor), Period, vFactor), Period, vFactor)”, but its code computes the coefficient form; nesting GD over averages staged that way is also valid from bar 24 and differs from it there by an amount that depends on the data (0.04 to 0.12 on 1% random walks). pandas-ta’s default length is 10, not 5.
  • Variant: Tillson’s GD nesting with first-value seeds and no warm-up: with al := 2 / (1 + float64(n)), g1e := c.XAverageAlpha(al) and g1 := g1e.Mul(1 + v).Sub(g1e.XAverageAlpha(al).Mul(v)), the same two lines on g1 and on their result give T3 from bar 0, equal to the coefficient form over XAverageAlpha to rounding.
  • Variant: the rules: Tillson’s long-only test enters at a trough of the one-bar change, d1 := t3.Delta(1) and d1.GT(d1.Shift(1)).Mul(d1.Shift(1).LT(d1.Shift(2))) (“d1 > Ref(d1,-1) AND Ref(d1,-1) < Ref(d1,-2)”), and exits on the mirror, which “closed at tops” implies but he does not print; Burns’s contrarian rule, long “when the closing price fell below the T3 level”, is c.PositionSigned(c.LT(at3), c.GTE(at3), c.GT(at3), c.LT(at3)).
  • Evidence: Tillson’s Figure 5 (the NASDAQ index, NDX, from 19 July 1993 to 30 June 1997, long only, each average’s period optimised over 2 to 10, 0.1% for entry and exit) gives T3 at 5 and 0.7 the best APR, 33.59, against 28.81 for DEMA and 28.17 for the EMA; Burns’s “Comparing T3s” (weekly bars, no costs) calls AT3’s results “astronomical (and unrealistic)”. Both are optimised in sample on one market. Evidence, not a reference.
  • Pinned by TestRecipeT6T3; runnable example ExampleSeries_XAverage_tillsonT3.

Elastic volume-weighted average and the reference price (Fries; Grinblatt and Han)

Christian Fries (“Elastic Moving Averages”, Stocks & Commodities V19:6, June 2001, pp. 52-56) lets the period of an average fluctuate with volume: with N “the number of shares floating”, “eVWMA_t = [(N - v) eVWMA_{t-1} + vp] / N”, seeded with the first price, the approximate average price paid per share. That is XAverageAlphaS with the constant v/N, which the method clamps to [0, 1]: in t6EVWMA, ev is the eVWMA on a share count, and evK the float-free form with N the last k bars’ volume, TTR’s EVWMA(c, v, k). Mark Grinblatt and Bing Han’s reference price (“Prospect Theory, Mental Accounting, and Momentum”, Yale ICF Working Paper 00-71, 2004, eq. (1)), “R_{t+1} = V_t P_t + (1 - V_t) R_t” with V the turnover, is the same recursion one bar late, ref, and cgo their capital-gains overhang (P_t - R_t)/P_t.

ev := c.XAverageAlphaS(v.Div(shares))
evK := c.XAverageAlphaS(v.Div(v.Sum(k)).SetN(k, 1)).SetN(k-1, math.NaN())
ref := ev.Shift(1).SetN(1, math.NaN())
cgo := c.Sub(ref).Div(c)
  • Inputs: prices; volumes; shares a scalar share count (Fries’s sheet uses 1% of the shares, 565,000; Grinblatt and Han use the shares outstanding on weekly bars), and a time-varying or split-adjusted float goes through v.Div(floatSeries) in place of v.Div(shares); k >= 1 bars.
  • First valid index: 0 for ev, whose first bar is the price (bar 0’s volume is not read, as on Fries’s sheet); k - 1 for evK, whose seed is the close of bar k - 1, as TTR’s, with NaN before; 1 for ref and cgo, which would read 0 and 1 at bar 0 from the Shift pad.
  • Anchors: Fries’s IPO example (1,000 shares issued at 10, then trades of 100 at 15, 200 at 20 and 100 at 25) gives his 10, 10.50, 12.40 and 13.66, each the float64 of the decimal, and his sidebar’s eight rows (565,000 shares) give his 23.10 to 22.83 at two decimals; on those rows his formula and the kernel agree to rounding. A constant v/N is XAverageAlpha(v/N) bit for bit; a volume at or above the share count gives the price and a zero volume holds the value, bit for bit. A window of zero volumes holds evK, where TTR’s EVWMA, whose window sum is 0, goes NaN for good.
  • Traps: Fries’s formula is not clamped: a volume above N makes N - v negative, and the average overshoots and oscillates (prices 10, 10, 12, 12, 12 on volumes of 0, 1,500, 2,500 and 2,500 from bar 1 with N = 1,000 give 10, 10, 13, 10.5, 14.25 where the helper gives 10, 10, 12, 12, 12); the June 2001 Traders’ Tips’ TradeStation code freezes the average instead for the rest of the chart. A NaN volume makes ev NaN for good; v.Div(v.EQ(v)) turns it into a held bar first. VWEMA, the EMA of p v over the EMA of v, is another average: its memory fades per bar, the eVWMA’s per share traded.
  • Variant: Grinblatt and Han’s empirical proxy c.Sub(ev).Div(c).Shift(1), (P_{t-1} - R_t)/P_{t-1}, lags the price a week “to avoid confounding market microstructure effects, such as bid-ask bounce” (mask bars 0 and 1); they also truncate the sum behind R at five years “and rescale the weights to sum to one”, which the recursion from the first bar does not do.
  • Missing piece: Fries’s N-volume VWMA, a volume-weighted average over the last N shares traded rather than over a number of bars (the oldest bar entering in part), is not built: no shipped method gives the bars back to an accumulated volume of N, and Fries offers the eVWMA as “an improvement” on it.
  • Evidence: Fries’s charts of an online broker’s shares (99 and 138 samples) show the average, not a rule, and Grinblatt and Han’s regressions on weekly CRSP data from July 1962 to December 1996 support the overhang as a cross-sectional quantity. Evidence, not a reference.
  • Pinned by TestRecipeT6EVWMA; runnable example ExampleSeries_XAverageAlphaS_eVWMA.

Bezier smoother (Angel)

Mark Angel (“Modeling the markets with Bezier curves”, Stocks & Commodities V8:2, February 1990, pp. 47-55) passes seven control prices, the most recent close and the closes 5, 8, 13, 18, 23 and 28 periods ago, to a Pascal routine that returns the points of a sixth-degree Bezier curve, and trades one “segment” of it. The routine reads only the prices (the time coordinates sit in a comment), so segment s is a fixed weighted sum of the lags 28, 23, 18, 13, 8, 5 and 0 with the Bernstein weights C(6,k) u^k (1-u)^(6-k), u = (s - 1)/30, which t6Bezier forms in the listing’s left-to-right product order. System 1 is always in the market: “If the market is now above the Bezier value, the system is long. If the close falls below the 13th point on the current Bezier curve, the system is short.”

u := float64(seg-1) / 30
v := 1 - u
bz := c.Shift(28).Mul(v*v*v*v*v*v).
    Add(c.Shift(23).Mul(6*u*v*v*v*v*v)).
    Add(c.Shift(18).Mul(15*u*u*v*v*v*v)).
    Add(c.Shift(13).Mul(20*u*u*u*v*v*v)).
    Add(c.Shift(8).Mul(15*u*u*u*u*v*v)).
    Add(c.Shift(5).Mul(6*u*u*u*u*u*v)).
    Add(c.Mul(u*u*u*u*u*u)).
    SetN(28, math.NaN())
pos := c.Sub(bz).Hysteresis(0, 0)
  • Inputs: closes; the segment seg from 2 to 31 (the listing overwrites segment 1 with segment 2, so the oldest control price itself is not returned, and segment 31 is the current price).
  • First valid index: 28 for bz, the first bar with the price 28 bars back (Shift pads with 0, so bars 0 to 27 would be partial sums biased towards 0, below the close on every bar); pos reads 0 there, decides on bar 28 and then holds on a tie and on NaN.
  • Anchors: the one-call FIRSum form below equals bz bit for bit from bar 28 on finite data. Segment 31 returns the closes bit for bit. On a ramp the curve is the line delayed by 29 - s + 2(6u^5(1-u) + u^6) bars, to rounding: 27.0000, 22.0032, 17.0552, 12.2877, 7.84035 and 3.6298 for Angel’s segments 2, 7, 12, 17, 22 and 27, whose weights equal the exact rationals to rounding. pos equals a loop of System 1, and at segment 31 every bar is a tie, so it stays 0.
  • Traps: weights from math.Pow differ in the last bits from the listing’s repeated products for 28 of the 31 segments (segments 1, 16 and 31, where u is 0, 1/2 and 1, agree). Angel’s Figure 4 text names segments 2 to 22 and the Swiss franc and soybeans among the daily markets, where its table runs to segment 27 and holds the D-mark and corn.
  • Variant: one call, c.FIRSum(w).SetN(28, math.NaN()), with the 29-tap vector w holding the weight of lag L at index 28 - L and zeros elsewhere: bit for bit on finite data, but NaN wherever a NaN lies anywhere in its 29-bar window, where the chain reads only the seven control bars. Not FIR, which divides by the weights’ float64 sum, 1 plus or minus one to three units for 13 of the 30 segments (3 to 12, 14, 15 and 25), and so equals the listing only to rounding there.
  • Variant: Systems 2 to 4 change the control series: the highs and the lows (buy when the high is above the curve of highs, sell when “the current price moves below the Bezier low curve”), 13-period moving averages of the highs and the lows (masked with NaN in their warm-up, first valid bar 40), or the RSI (13 periods in the text, “The RSI period = 10” in Figure 9’s note).
  • Evidence: Figure 4’s System 1 results (daily T-bonds, D-mark, yen, Value Line, gold, heating oil and corn, segments 2 to 27: 39 of the 42 profits per trade positive) are gross of costs and slippage on an unstated period before 1990, and Donald Lambert (“Bezier Curves: No Tool For Trading”, Stocks & Commodities V8:8, August 1990, pp. 313-315) answered that the systems “were in actual fact the equivalent of trading based on the prices that were used as the final point in the computations”. Evidence, not a reference.
  • Pinned by TestRecipeT6Bezier; runnable example ExampleSeries_FIRSum_bezier.

Star’s dual detrended price oscillator subtracts a displaced simple average from the close. Values are NaN before their first valid index, where an average passes the close through or the Shift pad reads +0; flags read 0 there and are decided at bar t’s close, and the zone reads 0 on its masked bars too. The averages are simple, so no seed is chosen. Her channels are Bollinger bands (the bands page under “Bollinger Bands”); North’s Insync index under “Comparison” holds Bernstein’s undisplaced detrended oscillator and a displaced variant; the trade-state pages hold the exits a close can decide. Prices are positive and finite and lengths are in bars.

Dual detrended price oscillator (Star)

Barbara Star (“Profit With Dual Oscillators & Bands”, Stocks & Commodities V32:8, July 2014, pp. 11-16) pairs a 9-period and a 20-period detrended price oscillator, “calculated by subtracting the simple moving average over an n-day period and shifted n/2+1 days back from the price”, with Bollinger bands one and three standard deviations from a 20-period average; her MetaStock formulas in the September 2014 letters, “C-Ref(Mov(C,9,S),-5)” and “C-Ref(Mov(C,20,S),-11)”, settle the form. Her continuation reads: “look for the DPO 20 to remain above its zero line while the DPO 9 dips below its zero line. Once the DPO 9 crosses back above zero, a price push back into the upper channel signals at least a short-term continuation of the trend.” t6DPO returns both oscillators, the lagged-price form of the fast one, her one-deviation zone and the continuation scan.

dpoF := c.Sub(c.Average(f).Shift(f/2+1)).SetN(f+f/2, math.NaN())
dpoS := c.Sub(c.Average(s).Shift(s/2+1)).SetN(s+s/2, math.NaN())
lagF := c.Shift(f/2+1).Sub(c.Average(f)).SetN(f-1, math.NaN())
z := c.Standardise(b).SetN(b-1, math.NaN())
zone := z.GT(1).Sub(z.LT(-1))
cont := dpoF.CrossesAboveS(0).Mul(dpoS.GT(0))
  • Inputs: the close c, the fast and slow lengths f and s and the band length b (9, 20 and 20); dpoF, dpoS and lagF are in price units, zone is +1 above the upper inner band, -1 below the lower and 0 between, and cont is 0 or 1. Arch-identical.
  • First valid index: f + f/2 (13) for dpoF, s + s/2 (30) for dpoS, f - 1 (8) for lagF (for f >= 3, Tulip’s start) and b - 1 (19) for the zone, which reads 0 on its masked bars, so 0 means “middle” only from b - 1. Unmasked, the pad bars read the close and the next ones c.Delta(d), with d = f/2 + 1.
  • Anchors: lagF is Tulip Indicators’ dpo (its test vector “dpo 5”, 11 values) and Achelis’s (Technical Analysis from A to Z, p. 119, Tulip’s vector “dpo 6”, 9 values at four decimals); on 15 bars dpoS and the zone have no valid bar and nothing panics. The exact relation dpoF = lagF + c.Delta(d) + c.Average(f).Delta(d) holds to rounding. On a ramp rising 0.25 a bar dpoF reads 2.25 and dpoS 5.125, and lagF -0.25 (-0.375 at 20): the lagged form leaves a residue of the trend. The single crosses are dpoF.CrossesAboveS(0), her aggressive entry, dpoS.CrossesAboveS(0), the conservative one, and their mirrors. The zone equals the band tests c.GT(c.Bollinger(b, 1)) and c.LT(c.Bollinger(b, -1)) to rounding, and on every bar of the test fixture.
  • Traps: the centred display, c.Sub(c.Average(f).Shift(-(f/2+1))) (pandas-ta’s default, StockCharts’ plot), reads the average d bars in the future, a look-ahead: moving the last close moves its value d bars back, where the causal forms move only on the last bar. The two causal forms are not one series displaced. TradingView’s default length is 21, and pandas-ta’s centered=False is the form of dpoF.
  • Variant: a book from the slow cross with the lower inner band as the exit, decided on closes, c.Position(dpoS.CrossesAboveS(0), c.LT(c.Bollinger(b, -1))).
  • Missing piece: Star’s resting stops below a band and her partial exits are orders and sizing, tier B (algolang).
  • Evidence: hand-picked 2013 charts (CVX, RGLD, JAZZ, TJX) and her own caution that “it is a strategy rather than a standalone trading system”. Evidence, not a reference.
  • Pinned by TestRecipeT6DPO; runnable example ExampleSeries_Average_dpo.

The moving-average stacks below compare closes with simple averages, which pass the close through before n - 1 bars, so every output is masked at the slowest length: codes are NaN before their first valid index, and the flags, books and the ladder read 0 there. A close equal to an average, or two equal averages, reads “not above”. A code or a book flips on a one-ulp difference of an average near a tie, so the cross-language goldens run on prices in sixty-fourths, whose window sums are exact. Prices are positive and finite and lengths are in bars; the weekly lengths of the sources are the caller’s bars or a daily emulation by length (relative-timeframe/t3-timeframes.md). Related: the side of a moving average with its age (consecutive/m1-stateage.md) and Pruitt’s entry-anchored bracket stop (trade-state/t2-entry-anchored-stops.md).

Dukas’s six trend phases

Chuck Dukas (“Defining The Bull And The Bear”, Stocks & Commodities V25:13, 2007 Bonus Issue, pp. 14-22) sorts “all price action” into six phases from the close and its 50- and 200-period simple averages, with strict tests: “Recovery phase: Close > 50 PMA; Close < 200 PMA; 50 PMA < 200 PMA”, Accumulation with the close above both and the 50 below the 200, Bullish with all three in order, and Warning, Distribution and Bearish their mirrors. A “60-period channel […] of highs and lows” shows new highs and lows, “accumulated only in that phase”, and his ladder would “expose a one-third position” in Recovery, add a third in Accumulation and reach “a full position” in Bullish. t6DukasPhases writes the three tests as the bits of a code.

af := c.Average(fast)
as := c.Average(slow)
code := c.GT(af).Mul(4).Add(c.GT(as).Mul(2)).Add(af.GT(as)).SetN(slow-1, math.NaN()).MaskNonFinite(as)
hhBull := h.Breakout(ch, 1).GT(0).Mul(code.EQ(7))
llBear := l.Breakout(ch, 1).LT(0).Mul(code.EQ(0))
ladder := code.EQ(4).Div(3).Add(code.EQ(6).Mul(2.0 / 3)).Add(code.EQ(7))
  • Inputs: highs, lows and closes; fast and slow (50, 200); ch (60). The codes are 7 Bullish, 6 Accumulation, 4 Recovery, 3 Warning, 1 Distribution and 0 Bearish.
  • First valid index: slow - 1 (199) for code, NaN before and wherever the slow average is not finite: a NaN close at bar p gives NaN on p to p + slow - 1, and the code recovers after. The flags and the ladder read 0 on those bars, the ladder’s sell-side value.
  • Anchors: codes 2 and 5 cannot occur (code 2 needs a200 < c <= a50 <= a200, code 5 a200 < a50 < c <= a200); on the test path the codes from bar 199 are 7 on 844 bars, 0 on 676, 3 on 487, 4 on 364, 1 on 249 and 6 on 181. code.GTE(4) is c.GT(af) (Dukas’s buy side is a close above the fast average) and the code’s parity is af.GT(as), bit for bit; the ladder takes 0, 1/3, 2/3 and 1 only.
  • Traps: unmasked, bars 0 to 48 read 0 and bars 49 to 198 read 1 or 4, because both averages pass the close through. Dukas’s strict boxes leave a tie in no phase and the code puts it in the phase of “below”: a constant 1/3 reads Accumulation, because both averages round below 1/3, while constants such as 0.1 or 7 read Bearish, so a flat history is not phase-neutral. The ladder is a function of the current phase: Dukas describes steps on the way up and gives no sizes on the sell side (“reduced”, “eliminated or further reduced”), and a path rule would need a latch of more than two states.
  • Variant: a cyclic ordinal in Dukas’s order (Bearish 0, Recovery 1, Accumulation 2, Bullish 3, Warning 4, Distribution 5), code.EQ(4).Add(code.EQ(6).Mul(2)).Add(code.EQ(7).Mul(3)).Add(code.EQ(3).Mul(4)) .Add(code.EQ(1).Mul(5)).Add(code.Mul(0)), whose last term carries the code’s NaN.
  • Variant: the two-close side of the 50-bar average in Dukas’s “Market Phase And Chart Formation” (Stocks & Commodities V18:2, February 2000), with a := c.Average(50).SetN(49, math.NaN()): c.GT(a).Sum(2).EQ(2).Sub(c.LT(a).Sum(2).EQ(2)).Hysteresis(0, 0).
  • Evidence: Dukas’s Figures 7 to 10 (data to August 2004) give each instrument’s share of time by phase, the S&P 500 Bullish 55% and Bearish 12% of 20 years; Norman Fosback’s weekly test (Stock Market Logic, 1976, Table 47; the stack state is code 7 on 10 and 30 weeks) found its bullish configurations “have not been large enough to justify the commission expenses”. Evidence, not a reference.
  • Pinned by TestRecipeT6DukasPhases; runnable example ExampleSeries_Average_dukasPhases.

Dual moving average, always in (Faith)

Curtis Faith (Way of the Turtle, McGraw-Hill, 2007, ch. 10, p. 141) describes “a very simple system that buys and sells when a 100-day moving average crosses over a slower 350-day moving average. Unlike the other systems, this system is always in the market, either long or short.” On an exact tie no cross has happened, so t6DualMA latches the difference with Hysteresis(0, 0), which holds it, as Pruitt’s AmiBroker state form Buy = MA(C,sl) > MA(C,il); Short = MA(C,sl) < MA(C,il) does (The Ultimate Algorithmic Trading System Toolbox, Wiley, 2016, ch. 4).

dual := c.Average(fast).Sub(c.Average(slow)).SetN(slow-1, math.NaN()).Hysteresis(0, 0)
  • Inputs: closes; fast and slow (100, 350), simple averages: Faith says “exponential” where he means it, as for the Donchian Trend’s filter.
  • First valid index: slow - 1 (349); 0 before. The value at bar t is the state after its close; dual.Shift(1) is the book for next-open fills.
  • Anchors: off ties the latch equals the masked sign form c.Average(100).GT(c.Average(350)).Mul(2).Sub(1).SetN(349, math.NaN()) (on every bar of the test path from 349: 1 on 1,426 bars and -1 on 1,225, 14 changes); on a constructed tie, closes 1, 2, 3, 4 and 3.5 with lengths 1 and 3, the latch holds 1 where the sign form reads -1.
  • Traps: the dossier’s unmasked c.Average(100).GT(c.Average(350)).Mul(2).Sub(1) is -1 from bar 0, where both averages are the close, and short on every tie. Faith tested the system with no stop: “the test with no stops is better for all the metrics”.
  • Variant: the exponential form, c.MACD(f, s).SetN(s-1, math.NaN()).Hysteresis(0, 0), the sign of XAverage(f) against XAverage(s) off ties.
  • Evidence: Faith’s Table 10-1 (27 futures, January 1996 to June 2006, Trading Blox runs): 57.8% a year, MAR 1.82, 210 trades; Table 10-3: extending the test from June to November 2006 cut the CAGR from 57.8% to 49.1%, a 15.1% fall. Evidence, not a reference.
  • Pinned by TestRecipeT6DualMA.

Triple moving average (Faith; Pruitt)

Faith (ch. 10, p. 142): “The buys and sells occur when the 150-day moving average crosses over a slower 250-day moving average. The system uses the longer 350-day average as a trend filter. Trades happen only when both moving averages are on the same side as the longer 350-day average”, and trades exit “when the 150-day average crosses the 250-day average”. Pruitt’s TMA (ch. 3, Box 3.3) is the same rule with the lengths 9/19/49, 19/49/199 and 49/199/399: “Buy when shortest moving average crosses above intermediate moving average”, both above the longest, and “Liquidate long when shortest moving average crosses below intermediate moving average”. t6TripleMA reads the cross as an event.

a := c.Average(sl).SetN(ll-1, math.NaN())
b := c.Average(il).SetN(ll-1, math.NaN())
z := c.Average(ll).SetN(ll-1, math.NaN())
le := a.GT(z).Mul(b.GT(z)).Mul(a.CrossesAboveS(b))
se := a.LT(z).Mul(b.LT(z)).Mul(a.CrossesBelowS(b))
book := c.PositionSigned(le, a.CrossesBelowS(b), se, a.CrossesAboveS(b))
  • Inputs: closes; sl < il < ll.
  • First valid index: ll - 1 for the masked averages; book first trades at bar ll or later (486 for 150/250/350 and 90 for 9/19/49 on the test path) and reads 0 before. The masks keep a cross from firing on pass-through values.
  • Anchors: book equals a direct loop of the rules bit for bit; on a cross bar the short average’s filter follows from the other two, so b.GT(z).Mul(a.CrossesAboveS(b)) is the same flag bit for bit (the factor is kept to match the wording).
  • Traps: the dossier’s state reading, which enters while the stack is aligned and has no exit, differs from the text’s event reading on about half the bars of a walk (1,175 bars of the test path at 150/250/350). Pruitt prints no TMA code: his Appendix A has none, and the AmiBroker crossover he prints is the dual average’s. His fills are at the signal bar’s close; Faith’s value is the state after the close, book.Shift(1) for next-open fills. Pruitt’s 3,000-dollar stop is the entry-anchored bracket on the close of the trade-state pages.
  • Variant: the state reading, c.PositionSigned(a.GT(b).Mul(b.GT(z)), a.LTE(b), a.LT(b).Mul(b.LT(z)), a.GTE(b)).
  • Variant: Pruitt’s DMA, c.PositionSigned(x.CrossesAboveS(y), nil, x.CrossesBelowS(y), nil) on the masked short and long averages x and y, which reverses on each cross after the first.
  • Evidence: Faith’s Table 10-1: 48.1% a year, MAR 1.53, 181 trades; Pruitt’s Table 3.7 (35 futures, one contract, the 3,000-dollar stop): net -71,434 dollars for 9/19/49 and 711,878 dollars for 49/199/399. Evidence, not a reference.
  • Pinned by TestRecipeT6TripleMA.

The recipe below reads daily highs, lows, closes and volumes (h, l, c and v) and fills at the signal bar’s close, as Beatson does; it does not read the open o, which only its next-open variant reads. Recipe values (the disparity of the anchors) are NaN before their first valid index (SetN(F, math.NaN())), and flags, states and latches read 0 there: every shifted series a pad can satisfy is masked, or the flag is, and here the entry flag is masked at the slowest length, because Average passes the close through before bar n - 1. Prices are positive and finite, volumes finite and non-negative, and series are paired by position (the caller aligns them by timestamp). An exact decimal tie at the 5% threshold is decided by binary rounding in every float form: the page pins the inclusive multiply form c.LTE(am.Mul(1 - pct)), the goldens run on prices in sixty-fourths, and a constructed case pins the tie. Backtests over data we do not have (Beatson’s S&P 500 tables) are evidence, not a reference. Counts on probe paths that the tests do not reproduce are illustrations, not pins.

Sharp drop below the 50-day average in a golden-cross regime (Beatson)

Stephen Beatson (“Identifying Short-Term Bottoms In Bull Markets”, Stocks & Commodities V34:5, April 2016, pp. 16-20) defines a sharp drop as “a 5% or greater fall below the 50-day moving average” and a bull market as “when the 50-day moving average is above the 200-day moving average”, and his rules box reads “Enter long after a 5% or greater drop below the 50-day moving average (and below the 10-day MA)”. His final system (Figure 7) times the entries “on either a contraction in volume or a contraction in daily range” and exits “on a close > the 10-day MA”, both at the close (“the entry would have occurred at the close of August 25, 2015”). t6SharpDrop returns the entry flag, the exit flag and the long-only book pos after each close.

af := c.Average(fast)
am := c.Average(mid)
bull := am.GT(c.Average(slow))
drop := c.LTE(am.Mul(1 - pct)).Mul(c.LT(af))
rng := h.Sub(l)
calm := rng.LT(rng.Shift(1)).Add(v.LT(v.Shift(1))).GT(0)
entry := bull.Mul(drop).Mul(calm).SetN(slow-1, 0)
exit := c.GT(af)
pos := c.Position(entry, exit)
  • Inputs: daily highs, lows, closes and volumes (the caller’s volume series; for an index, a composite volume); fast, mid and slow in bars (10, 50 and 200) and pct as a fraction (0.05). The averages are simple (his “moving average”, “MA” and “DMA”, as the sidebar chart shows); the regime is a level, read on the signal bar only, and calm is the “or” of the two contractions. One position at a time, re-entering on the next qualifying bar after an exit: the article is silent on re-entry, and its counts fit one position at a time (see the evidence).
  • First valid index: slow - 1 (199) for entry, which reads 0 on bars 0 to 198, so the earliest long bar of pos is 199. The mask is not a no-op: unmasked, bars 49 to 198 compare the 50-bar average with the close itself, which the 200-bar average passes through, and on the 700-bar fixture group the unmasked entry fires on bars 73, 74, 134 and 181 to 183. exit is unmasked: before bar fast - 1 it compares the close with itself and reads 0, and Position reads it only while long. The Shift pads of rng and v meet only masked bars.
  • Anchors: pos equals entry.Sub(exit).Hysteresis(0, 0).GT(0) bit for bit, Position’s identity for 0/1 flags that are not both 1 on a bar: entry and exit cannot fall on one bar, since drop needs c < af and exit needs c > af. calm equals rng.LT(rng.Shift(1)).Maximum(v.LT(v.Shift(1))) bit for bit, and v.LT(v.Shift(1)) equals v.ConsecDown().GTE(1) on non-negative data, the one-bar case of the run counts on the consecutive page t6-declining-volume-pullback.md; bull is the parity of Dukas’s phase code on the moving-averages page t6-ma-stack-systems.md. The sidebar’s “Close relative to 50-day MA” (-2.77 %, -5.74 %, -9.28 % and -10.32 %) is c.Div(am).Sub(1).Mul(100).SetN(mid-1, math.NaN()); no single-series disparity method exists. On the 700-bar fixture group the entry fires on bars 236-239, 283-285, 345, 411-414, 454, 455, 465, 467 and 468, in 6 trades whose entries include range-only, volume-only and two-way contractions.
  • Traps: the rules box says “5% or greater” and every table header “Close > 5% below”. The inclusive `c.LTE(am.Mul(1
    • pct))is pinned by a constructed close exactly 0.95 times a 50-bar average that is a multiple of 20/64 (1 - 0.05is exactly 0.95 in binary64), and a decimal tie is decided by binary rounding in every float form: at an average of 1.01 and a close of 0.9595 the multiply form misses, although 1.01 x 0.95 prints 0.9595; over 299,901 decimal ties (averages from 1.00 to 3,000.00 in cents with the close 0.95 times the average in decimal) the multiply form catches the most, 68%, the division and percent forms 58% and the relative formc.Sub(am).Div(am).LTE(-0.05)53%, and away from ties the four agree. A close enters its own average, so a tie must be built (49 closes of 64 and one of 60.8 average 63.93600000000001, not 64). The dossier's latchentry.Sub(exit).Hysteresis(1, -1).GT(0)is identically 0 (strict thresholds on a difference that does not exceed 1), and itspos.Shift(1)is the book lagged for close-to-close attribution, not next-open execution. The drop does not imply the 10-day condition in general (a slow slide can leave the close at or below 0.95 of the 50-day average yet above the 10-day one); none of the fixture's drop bars does, so the strictc.LT(af)is pinned by a constructed case.bull,c.LT(af),exit` and both contractions are strict, pinned by constructed cases: equal averages are in neither regime, and an equal range or volume is neither smaller nor greater (“Smaller than yesterday”). The day’s range and full volume are known only at the close at which the fill is taken, a market-on-close order on estimates.
  • Variant: fixed-day exits (Figures 1 and 4, “Exit after exactly 10 days”): c.Position(entry0, nil, k) with k 5, 10, 15 or 20 and entry0 := bull.Mul(drop).SetN(slow-1, 0), without calm; the trade exits at the close of bar e + k (his May 2015 article, Stocks & Commodities V33:6: “Sell on the close 10 days later”), and an entry flag on that bar is ignored.
  • Variant: next-open timing, o.Position(entry.Shift(1), exit.Shift(1)), Position’s documented next-open form with the open as the receiver of the trade views; it reads the open and removes the look-ahead of the close fill.
  • Members:
    • range only (Figure 5, “Smaller than yesterday”): calm := rng.LT(rng.Shift(1));
    • volume only (Figure 6): calm := v.LT(v.Shift(1));
    • both (“or ideally both”): calm := rng.LT(rng.Shift(1)).Mul(v.LT(v.Shift(1))).
  • Evidence: the S&P 500 index from 1990 to early 2016 (TradeStation, 100,000 dollars a position, no costs stated, one index, variants chosen in sample, table cells that overlap rather than nest). In the golden cross with the 10-day hold, 28 trades, 71.43% winners and a profit factor of 3.18 (Figure 1, where either regime gives 75 trades against 28 + 48, as one position at a time does); exiting above the 10-day average, 33 trades, 87.88% and 10.41 (Figure 4); a smaller range, 22 trades and 90.91%, and a smaller volume, 27 trades and 92.59% (Figures 5 and 6); Figure 7’s “or” system is an equity curve of 28 trades with no table. The sidebar enters at the close of 25 August 2015 at 1867.61 and exits on 3 September 2015 at 1,951.13, +4.47%, with no bar data printed, and the trade counts are not a regression check without his data. Evidence, not a reference.
  • Pinned by TestRecipeT6SharpDrop; runnable example ExampleSeries_Average_sharpDrop.

KAMA

KAMA

func (s Series) KAMA(n int, speeds ...int) Series

KAMA implements Kaufman’s Adaptive Moving Average across the provided series. Note that Kaufman recommends that the fast value be only 2 or 3, and the slow value be fixed at 30, which are the defaults. Either or both speeds may be overridden by passing one (fast) or two (fast, slow) values. Values of n less than one, or greater than the series length, return a copy of the original Series.

Bars 0 to n-1 pass the input through (result[i] = s[i]), the recursion starts at bar n from result[n-1] = s[n-1], and the first adapted bar is n. It reads EfficiencyRatio(n) only from bar n on, so the ratio’s warm-up never enters it: the ratio’s warm-up changed from 0.5 to 0 on 3 October 2026 with no effect on KAMA, bit for bit.

Cong Adaptive MA

CongAdaptiveMA

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

CongAdaptiveMA implements Scott Cong’s Adaptive Moving Average (as described in TASC March 2023). Mismatched input lengths (including the receiver, which provides the price series being smoothed) return a zero-filled series of the receiver’s length.

The warm-up reads 0, not NaN: bars 0 to n-2 are 0, a price level at 0, and the recursion is seeded with the input at bar n-1.

Kalman Filter

Kalman

func (s Series) Kalman(k1 float64) Series

Kalman implements the Kalman filter on a data series. Based on the article by Vince Banes, “Data Smoothing using a Kalman Filter”, Stocks & Commodities V3:5 (October 1985), pp. 162-165.

The gain parameter is the filter gain, and the second gain is the square root of twice it, so a negative gain gives NaN on every bar, bar 0 included. For a gain of 0 or more, bar 0 is s[0] and the state starts at the first input with zero velocity.

One Euro Filter

OneEuroFilter

func (s Series) OneEuroFilter(periodMin float64, beta float64) Series

OneEuroFilter applies an adaptive low-pass filter modelled on the 1-Euro filter, as translated from the TradeStation (EasyLanguage) implementation. The rate of change of the input is exponentially smoothed and used to adapt the cutoff period: cutoff = periodMin + beta*|smoothed dx|. The periodMin parameter sets the minimum cutoff period and beta controls the speed adaptation.

Note that this adaptation works in the period domain, so a larger beta increases smoothing during fast movements. This is the behaviour of the TradeStation original that this implementation is verified against, but it is the opposite of Casiez’s published 1-Euro filter, which raises the cutoff frequency (reducing smoothing and lag) as speed increases.

periodMin is in bars; beta multiplies the smoothed one-bar change in price units, so its effect depends on the price scale, and a beta tuned on a 10-dollar stock is 10 times too weak on a 100-dollar stock. Bar 0 is s[0], and the smoothed change starts at 0.