Relative: Regression and Spread
All functions · nseries package
Beta
func (s Series) Beta(s2 Series, n int) Series
Beta returns the rolling least-squares slope of s regressed on s2 over the trailing n right-aligned bars, the hedge ratio of s against s2.
The response is s and the regressor s2. The regression is the window’s ordinary least squares with an intercept: each value is the slope cov(s, s2)/var(s2) of the line fitted to the n paired bars, and Intercept is the constant of the same line. The regression is not symmetric, so s.Beta(s2, n) is the slope of s on s2 and not the reciprocal of s2.Beta(s, n) unless the window’s points are collinear. William Brown’s “price regression line” (Stocks & Commodities, October 2002) is Beta and Intercept on levels, graded by Correlation(…).Squared(); his RYURX against the S&P 500 over twelve monthly closes gives a slope of -0.00912 and an intercept of 21.07013.
s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n bars. The warm-up reads 0, not NaN, so a threshold test (for example a beta filter b > 1) must be masked before the first valid index.
On levels Beta is in units of s per unit of s2 (a share hedge ratio); on Log() inputs it is an elasticity (a market-value hedge ratio); on returns it is the dimensionless market beta. Futures need their multipliers: on futures prices the ratio is in points of s per point of s2, so multiply it by the multiplier of s over the multiplier of s2 to get contracts of s2 per contract of s. Any finite data are valid: levels, log levels, returns, spreads and rates.
The kernel is window-local and power-of-two scaled, each variable anchored at its first value and scaled by a power of two, so levels near 1e9 are safe and the result is bitwise start-invariant. The result is NaN on exactly the windows containing a NaN or an infinity, and later windows recover. A flat s2 gives 0, and a flat s with a non-flat s2 gives exactly zero. Beta explodes when s2 is nearly flat, since the denominator is the variance of s2 over the window, and a slope beyond the float64 range is ±Inf. Invalid parameters (no overlap, n < 2, or n greater than the overlap) give len(s) zeros.
Uses: the hedge ratio of a pairs spread, s less Beta times s2 (with or without Intercept), whose deviations from their mean are traded; the market beta of a stock when both inputs are returns (ReturnBeta forms them); and Brown’s regression line, whose slope and intercept project s from s2.
Deviations from the published method: Brown fits one fixed sample, where Beta rolls the fit over every window of n bars, and the warm-up reads 0 rather than being absent.
Compositions that start from it are listed under “Compositions” in the “Relative: Regression and Spread” category.
Verification: a Python reference in exact rational arithmetic and a Julia reference, which also translates Markos Katsanos’s published MetaStock regression code for ReturnBeta, agree with Beta, and s.Beta(ramp, n) equals s.Slope(n) bit for bit for a unit-step ramp (for example 1000 + i) wherever the kernel’s products, sums and slope are normal or zero; s.Beta(s, n) is 1 on windows that are not flat.
BetaOrigin
func (s Series) BetaOrigin(s2 Series, n int) Series
BetaOrigin returns the rolling least-squares slope of s regressed on s2 through the origin over the trailing n right-aligned bars.
The response is s and the regressor s2. Each value is sum(ss2) divided by sum(s2s2) over the n paired bars, the least-squares slope of a line forced through the origin, for hedge ratios where a constant has no meaning. Ernest Chan’s hedge ratio of GLD on GDX in Quantitative Trading (2nd edition, 2021, Example 3.6) is such a fit on levels; the book prints 1.6368 from MATLAB and 1.631 from Python and R. It is not Markos Katsanos’s “no-intercept” form, which uses the with-intercept slope r * sd(y)/sd(x) and drops only the constant: use Beta for that.
s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n bars. The warm-up reads 0, not NaN, so a threshold test (for example a beta filter b > 1) must be masked before the first valid index.
BetaOrigin is in units of s per unit of s2, a share hedge ratio on levels. Any finite data are valid; it suits prices that move in proportion, whose ratio rather than whose difference from a constant is stable.
The kernel is window-local and power-of-two scaled: each variable is scaled by the power of two of its largest magnitude and nothing is subtracted, since a fit through the origin has no anchor and no centring, so levels near 1e9 are safe and the result is bitwise start-invariant. The result is NaN on exactly the windows containing a NaN or an infinity, and later windows recover. A flat non-zero s2 with the value c gives ybar/c, the window mean of s over c, not 0; an all-zero s2 gives 0, and a zero result is always +0. The scaling cannot keep a product of two values that are each far below their variable’s largest: one that falls below the smallest subnormal after scaling is lost, so a window whose values span more than about 1e300 can give a wrong result, 0 in the extreme case. Invalid parameters (no overlap, n < 2, or n greater than the overlap) give len(s) zeros.
Uses: the pairs spread of Chan, s less BetaOrigin times s2 (GLD less the hedge ratio times GDX), traded on its deviations from its mean, and hedge ratios between prices that move in proportion.
Deviations from the published code: Chan fits once over a fixed training set, where BetaOrigin rolls the fit, so his hedge ratio is its value at the last bar of that set with n equal to the set’s length; the warm-up reads zeros rather than being absent.
Verification: there is no identity with an existing method, so the alternative oracle is the pair of references: a Python reference in exact rational arithmetic and a Julia reference, which also translates Katsanos’s published MetaStock regression code for ReturnBeta, agree with each other bit for bit after rounding; on their grid of 13,714 rows BetaOrigin differs from them by at most 3e-16 of max(1, |value|). s.BetaOrigin(s.Mul(2^k), n) is exactly 2^-k on windows where s is not all zero.
DeltaBeta
func (s Series) DeltaBeta(s2 Series, n, k int) Series
DeltaBeta returns the rolling least-squares slope of the k-bar price changes of s regressed on those of s2 over the trailing n changes.
The response is s and the regressor s2: each value is Beta over the n most recent k-bar changes v[j] - v[j-k] of the two inputs, the difference form of ReturnBeta. It follows the regression code of Markos Katsanos in Intermarket Trading Strategies (2008), a correlation times a ratio of standard deviations, with price changes in place of percent returns, and it is the Momentum(k) composition after the warm-up.
s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n + k - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar: the first k paired bars have no k-bar change and the first window takes n changes, so the minimum overlap is n + k bars. The warm-up reads 0, not NaN, so a threshold test (for example a beta filter b > 1) must be masked before the first valid index.
DeltaBeta is in units of s per unit of s2, a hedge ratio of price moves. The difference form is valid on Panama back-adjusted futures, spreads, rates and zero-crossing series, where a percent return is meaningless or undefined: use it in place of ReturnBeta there. Any finite data are valid.
The kernel is window-local and power-of-two scaled, so levels near 1e9 are safe and the result is bitwise start-invariant. The result is NaN on exactly the windows containing a non-finite change: a NaN or an infinity at pair p makes changes p and p + k NaN, so the outputs at pairs p to p+n-1 and p+k to p+k+n-1 are NaN from the first valid index on, and a difference that overflows is infinite and gives NaN on the windows that contain it. A zero price is an ordinary value. Invalid parameters (no overlap, n < 2, k < 1, k not less than the overlap, or n greater than the overlap less k) give len(s) zeros, in time proportional to len(s) however large n or k.
Uses: the hedge ratio of one back-adjusted future on another, the sensitivity of one yield or spread to another over k-bar changes, and a beta filter on series that cross zero.
Deviations from the published code: Katsanos’s code regresses percent returns, where DeltaBeta regresses price changes, and the warm-up reads zeros before the first valid index.
Verification: a Python reference in exact rational arithmetic and a Julia reference, which also translates Katsanos’s published MetaStock regression code for ReturnBeta, agree with DeltaBeta, and s.DeltaBeta(s2, n, k) equals s.Momentum(k).Beta(s2.Momentum(k), n) bit for bit from the first valid index on equal-length finite inputs.
DistanceScore
func (s Series) DistanceScore(s2 Series, n int) Series
DistanceScore returns the current difference between s and s2 rebased to the start of the trailing n-bar window, divided by the standard deviation of that difference over the window.
At bar j the window is the n bars c = j - n + 1 to j, and each series is divided by its own value at bar c, so both start the window at 1, the normalised prices of Gatev, Goetzmann and Rouwenhorst (GGR). With q running over the window’s paired bars, the rebased difference is d[q] = s[q]/s[c] - s2[q]/s2[c], which is 0 at q = c, and the output is d[j]/sigma, where sigma is the population standard deviation of d over the window, the deviation about the window mean with the divisor n. It is computed in two passes, never from raw moments (the mean of the squares less the square of the mean), which cancel catastrophically. A compensated (Kahan-Neumaier) sum in increasing q order gives mean = sum(d[q])/n, and a second gives sigma = sqrt(sum(ee)/n) with e = d[q] - mean, each square being rounded by float64(ee) before it is added so that the compiler cannot fuse it with the sum. The q = c term, which is 0, is kept in every sum. When sigma is 0 the output is 0, because the score is then undefined and 0 is neutral.
The distance is that of Evan Gatev, William N. Goetzmann and K. Geert Rouwenhorst, “Pairs Trading: Performance of a Relative-Value Arbitrage Rule”, Review of Financial Studies 19(3):797-827 (2006), and NBER Working Paper 7032 (1999), sections III.1-III.2, here taken over a rolling window.
The inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n bars. Bars before the first valid index are 0 because no full window exists there. A call returns zeros for every bar, allocating only the result, when there is no overlap, when n < 2 or when n exceeds the overlap; a huge or negative n, such as math.MaxInt or math.MinInt, returns at once, in time proportional to len(s).
Each output depends only on its own window, so the method is window-local and start-invariant: dropping leading values leaves the remaining outputs unchanged bit for bit. The window is recomputed in O(n) per bar, because the base moves every bar, and a valid call makes two allocations, the result and one scratch buffer of capacity n, whatever the input length.
Both DistanceSSD and DistanceScore need positive levels: a window in which any value of either input is not finite and greater than zero (NaN, an infinity, zero or negative) gives NaN, and short of overflow (below) only such windows do. So a bad value at aligned bar p makes the outputs for bars p to p+n-1 (clipped to the valid bars) NaN, and later outputs recover. Use total-return, dividend-adjusted prices; for signed series (spreads, rates) use the regression entries, RegressionSpread and RegressionChannel.
The output is 0 when the two rebased series coincide: s2 == s, and s2 == 2^ks while it stays normal, give 0 at every valid bar, because the power of two cancels exactly in (2^k*s[q])/(2^ks[c]). The score is bounded by 2*sqrt(n - 1) in magnitude (the sharp bound is sqrt(2n)), up to rounding while the squared deviations stay normal, by Samuelson’s inequality |d[j] - mean| <= sqrt(n - 1)*sigma together with |mean - d[c]| <= sqrt(n - 1)*sigma and d[c] = 0. Extreme ratios, a price rising by a factor of about 1e155 or more within one window, overflow the squared deviations and give NaN.
These identities hold bit for bit: the score is antisymmetric, so s.DistanceScore(s2, n) equals -s2.DistanceScore(s, n) (a zero may keep its sign), because swapping the inputs changes only the sign of each difference; and scaling either input by a power of two leaves the score unchanged while the scaled values stay normal.
DistanceSpread, the rebased difference itself, is the composition a.Div(a.Shift(n-1)).Sub(b.Div(b.Shift(n-1))) over a, b := s.Overlap(s2), trimmed by n-1, which equals d[j] bit for bit; times 100 it approximates a.RateOfChange(n-1) minus b.RateOfChange(n-1). The distance band is plus or minus k*sigma_d, where sigma_d is the sigma above, so a score beyond plus or minus k lies outside it.
The standard deviation of Gatev, Goetzmann and Rouwenhorst is ambiguous. This method uses the deviation about the window mean; the alternative is the root-mean-square about zero, sqrt(SSD/n), available from DistanceSSD and called the RMS here. The squared RMS is sigma squared plus the squared window mean of d, so the two agree only when that mean is 0.
GGR open a position when the normalised prices diverge by more than two historical standard deviations (|DistanceScore| > 2 approximates this) and close it when the prices cross; in rolling form DistanceSSD monitors pair quality and the score triggers. The rolling version merges the disjoint formation and trading periods of GGR, so it is an adaptation.
Verification: DistanceScore is on the library’s alternative oracle list, because no existing method has an exact identity with it. Two independent references agree with it to a tolerance scaled by each window’s condition number, a Python translation of Gatev, Goetzmann and Rouwenhorst’s distance in exact rational arithmetic and a Julia translation in high precision, rounded once; s2 == s and s2 == 2^ks give 0; the antisymmetry holds bit for bit; and its magnitude stays within 2sqrt(n - 1), up to rounding while the squared deviations stay normal.
DistanceSSD
func (s Series) DistanceSSD(s2 Series, n int) Series
DistanceSSD returns the sum of squared differences between s and s2 over the trailing n bars after each is rebased to 1 at the start of the window.
At bar j the window is the n bars c = j - n + 1 to j, and each series is divided by its own value at bar c, so both start the window at 1, the normalised prices of Gatev, Goetzmann and Rouwenhorst (GGR). With q running over the window’s paired bars, the rebased difference is d[q] = s[q]/s[c] - s2[q]/s2[c], which is 0 at q = c, and the output is the sum of d[q]d[q] for q = c to j, the squared Euclidean distance between the two rebased series. The squares are added in increasing q order into a compensated (Kahan-Neumaier) sum, each square being rounded by float64(dd) before it is added so that the compiler cannot fuse it with the sum, and the q = c term is kept. The sum is never expanded into raw moments, the sums of squares and of cross products of the rebased series, which cancel catastrophically for a good pair because its rebased series nearly coincide.
The distance is that of Evan Gatev, William N. Goetzmann and K. Geert Rouwenhorst, “Pairs Trading: Performance of a Relative-Value Arbitrage Rule”, Review of Financial Studies 19(3):797-827 (2006), and NBER Working Paper 7032 (1999), sections III.1-III.2, here taken over a rolling window.
The inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n bars. Bars before the first valid index are 0 because no full window exists there. A call returns zeros for every bar, allocating only the result, when there is no overlap, when n < 2 or when n exceeds the overlap; a huge or negative n, such as math.MaxInt or math.MinInt, returns at once, in time proportional to len(s).
Each output depends only on its own window, so the method is window-local and start-invariant: dropping leading values leaves the remaining outputs unchanged bit for bit. The window is recomputed in O(n) per bar, because the base moves every bar, and a valid call allocates only the result.
Both DistanceSSD and DistanceScore need positive levels: a window in which any value of either input is not finite and greater than zero (NaN, an infinity, zero or negative) gives NaN, and short of overflow (below) only such windows do. So a bad value at aligned bar p makes the outputs for bars p to p+n-1 (clipped to the valid bars) NaN, and later outputs recover. Use total-return, dividend-adjusted prices; for signed series (spreads, rates) use the regression entries, RegressionSpread and RegressionChannel.
The output is 0 when the two rebased series coincide: s2 == s, and s2 == 2^ks while it stays normal, give 0 at every valid bar, because the power of two cancels exactly in (2^k*s[q])/(2^ks[c]). The output is never negative. Extreme ratios, a price rising by a factor above about 1e154 within one window (less for long windows, whose n squares are summed), overflow the squares, and the compensated sum then gives NaN; a fall cannot overflow.
These identities hold bit for bit: swapping the inputs leaves the output unchanged, because it changes only the sign of each difference and the square does not; scaling either input by a power of two leaves the output unchanged while the scaled values stay normal; and with s[q] = 2^q and s2 constant the output is (4^n - 1)/3 - 2*(2^n - 1) + n exactly for n up to 26, so n = 3 gives 10.
GGR rank candidate pairs by the minimum SSD over a formation period. In rolling form the SSD monitors pair quality, a pair whose SSD stays small keeping its normalised prices together, and DistanceScore supplies the trigger. The root-mean-square difference sqrt(SSD/n), called the RMS, is the same distance per bar. The rolling version merges the disjoint formation and trading periods of GGR, so it is an adaptation.
Verification: DistanceSSD is on the library’s alternative oracle list, because no existing method has an exact identity with it. Two independent references agree with it to a tolerance scaled by each window’s condition number, a Python translation of Gatev, Goetzmann and Rouwenhorst’s distance in exact rational arithmetic and a Julia translation in high precision, rounded once; s2 == s and s2 == 2^k*s give 0; the symmetry holds bit for bit; and it matches the closed form for powers of two exactly.
HalfLife
func (s Series) HalfLife(n int) Series
HalfLife returns the half-life in bars of mean reversion of the series, estimated by regressing each value on the previous one over the trailing n pairs.
At each bar t the n lag pairs (s[i-1], s[i]) for i = t-n+1 .. t are fitted by least squares, s[i] = a + b*s[i-1], with the shared co-moment kernel behind Beta and Intercept; the slope b is the one Beta gives with the lagged series as the regressor. The mean-reversion rate per bar is theta = b - 1 (OUTheta), and the output is -math.Ln2 / theta when theta < 0 and 0 otherwise, evaluated in exactly that order. This is the formulation of Ernest Chan, Quantitative Trading, second edition (Wiley 2021), chapter 7, “What Is Your Exit Strategy?” (pages 170-173), Example 7.5: Chan regresses the change on the centred lagged level, ols(dz, prevz - mean(prevz)), and takes halflife = -log(2)/theta; in exact arithmetic his theta equals b - 1, and his example gives about 7.8 bars (he calls it about 10 days). The discrete-exact half-life -ln 2/ln(b), defined for 0 < b < 1, is a documented alternative, not the output.
The first valid index is n (the first bar with n lag pairs, n + 1 values), so the minimum length is n + 1; the outputs before index n are 0. When n < 2 or the series holds fewer than n + 1 values, however large or negative n is, the result is all zeros, with nothing but the output allocated: the test is n >= len(s), which never forms n + 1, so every int n, math.MaxInt and math.MinInt included, is safe. Each output depends only on the n + 1 values s[t-n .. t], so the result is window-local and start-invariant: dropping leading values leaves the remaining outputs unchanged bit for bit.
A non-finite value at index p makes the outputs at t = max(p, n) .. min(p+n, len(s)-1) NaN (every fit whose window holds it is not finite), and later outputs recover. A flat regressor window (every s[i-1] equal, so the slope is undefined) gives 0, as OUTheta and OUMean do, rather than the ln 2 that the slope of 0 reported by Beta would imply. A theta of 0 or more (a random walk or a trending or explosive series) also gives 0, which must not be read as instant reversion: consumers gate on OUTheta < 0, which is why the rate, the half-life and the level are all exported. A theta of -1 or below (an overshooting series) gives a half-life of ln 2 or less. As theta rises towards 0 the half-life grows without bound (about 6.2e15 bars at b = 1 - 2^-53) and then drops to 0 at theta = 0, so a window near a unit root reads as either a vast half-life or none; the gate HalfLife < n/2 rejects both. A window of finite values never gives NaN or an infinity at any magnitude: a slope that overflows to +Inf gives 0 and one that overflows to -Inf gives +0 ({1e-300, -1e-300, 1e300} with n = 2), although OUTheta is then infinite.
Uses: a time stop or holding period; the lookback of Standardise (in Algorithmic Trading, Wiley 2013, chapter 2, Chan sets it to the rounded half-life); a regime gate (trade only while OUTheta < 0 and HalfLife < n/2); and OUMean as the exit target. On a pair spread, form the spread first and drop its warm-up zeros, or the fit includes them: x, y := s.Overlap(s2); spread := x.RegressionSpread(y, w).Window(-(w-1)); h := spread.HalfLife(n), whose first valid index is (w-1) + n relative to the overlap. Two identities hold exactly: s[i] = 2^-i gives b = 0.5, so the output is 2*math.Ln2 and the level is 0; and the period-4 pattern +1, +1, -1, -1 with n a multiple of 4 gives b = 0, so the output is math.Ln2 and the level is 0.
Verification: two independent references, a Python reference in exact rational arithmetic (ln 2 to 100 digits or more) and a Julia translation of Chan’s formulation (which regresses the change on the centred lagged level), agree with it; -math.Ln2 / (u.Beta(u.Shift(1), n) - 1) equals it bit for bit wherever that rate is negative, off flat windows; the powers-of-two and period-4 series give their exact values; and the scaling and sign identities HalfLife(2^ku) == HalfLife(u), for every k that keeps 2^ku normal and finite, and HalfLife(-u) == HalfLife(u) hold bit for bit.
Intercept
func (s Series) Intercept(s2 Series, n int) Series
Intercept returns the rolling least-squares intercept of s regressed on s2 over the trailing n right-aligned bars (the hedge intercept, or alpha when both inputs are returns).
The response is s and the regressor s2. The regression is the window’s ordinary least squares with an intercept, and each value is the constant of the line fitted to the n paired bars: the mean of s less Beta times the mean of s2, computed as ybar - beta*xbar with the product rounded before the subtraction, a barrier against a fused multiply-add. William Brown’s “price regression line” (Stocks & Commodities, October 2002) is Beta and Intercept on levels; his RYURX against the S&P 500 over twelve monthly closes gives an intercept of 21.07013 with a slope of -0.00912. The alpha indicator of Frank Ritchie (Stocks & Commodities, July 1996) is the 9-return intercept of bond log returns on a chemical-stock fund’s, averaged over 12 bars, a composition listed under “Compositions”.
s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n bars. The warm-up reads 0, not NaN, so a threshold test (for example an alpha filter a > 0) must be masked before the first valid index.
Intercept is in units of s. On levels it is ill-conditioned: a small change in the slope moves it by that change times the mean of s2, so a hedge intercept between prices far from zero is unstable. On returns it is well conditioned and is the alpha, the return of s when the return of s2 is zero. Any finite data are valid.
The kernel is window-local and power-of-two scaled, each variable anchored at its first value and scaled by a power of two, so levels near 1e9 are safe and the result is bitwise start-invariant. The result is NaN on the windows containing a NaN or an infinity, and later windows recover; a slope beyond the float64 range makes Intercept ±Inf, or NaN when the window mean of s2 is exactly 0. A flat s2 gives the window mean of s: the slope is set to 0 (the minimum-norm slope of the centred fit) and the intercept is then ybar. s.Intercept(s, n) is 0 on windows that are not flat. Invalid parameters (no overlap, n < 2, or n greater than the overlap) give len(s) zeros.
Uses: the alpha of a fund or stock against an index when both inputs are returns, as in Ritchie’s alpha indicator, and the constant of a pairs spread, s less Intercept less Beta times s2. The residuals of one window’s fit have zero mean over that window, but the rolling series of current residuals, each from its own window’s fit, does not.
Deviations from the published method: Ritchie’s alpha is a composition of this method rather than a separate calculation, and the warm-up reads 0 rather than being absent.
Compositions that start from it are listed under “Compositions” in the “Relative: Regression and Spread” category.
Verification: a Python reference in exact rational arithmetic and a Julia reference, which also translates Markos Katsanos’s published MetaStock regression code for ReturnBeta and the Excel formulas published with Ritchie’s article, agree with Intercept; the recipe reproduces the alpha indicator printed with the article to its five decimals; s.Intercept(s, n) is 0 on windows that are not flat; and the integer line 5 + 2*ramp against ramp (ramp 0, 1, 2 and so on) gives 5 for every n from 2 to 120.
OUMean
func (s Series) OUMean(n int) Series
OUMean returns the equilibrium level of the series implied by its rolling n-pair autoregression.
At each bar t the n lag pairs (s[i-1], s[i]) for i = t-n+1 .. t are fitted by least squares, s[i] = a + bs[i-1], with the shared co-moment kernel behind Beta and Intercept, and the output is a / (1 - b), evaluated in exactly that order, where b is the slope that Beta forms and a the intercept that Intercept forms, ybar - bxbar with the product rounded before the subtraction. It is the fixed point of the fitted recursion, the level at which the expected change is 0: for the discretised Ornstein-Uhlenbeck process s[i] - s[i-1] = theta*(s[i-1] - mu) + noise with theta = b - 1 (OUTheta), it is mu, in the units of s. Ernest Chan, Quantitative Trading, second edition (Wiley 2021), chapter 7, Example 7.5, fits no level (he regresses the change, without an intercept, on the lagged level centred on its sample mean) and takes the historical mean as the target; this method reports instead the level the fit itself implies, from the same window as the rate.
The first valid index is n (the first bar with n lag pairs, n + 1 values), so the minimum length is n + 1; the outputs before index n are 0. When n < 2 or the series holds fewer than n + 1 values, however large or negative n is, the result is all zeros, with nothing but the output allocated: the test is n >= len(s), which never forms n + 1, so every int n, math.MaxInt and math.MinInt included, is safe. Each output depends only on the n + 1 values s[t-n .. t], so the result is window-local and start-invariant: dropping leading values leaves the remaining outputs unchanged bit for bit.
A non-finite value at index p makes the outputs at t = max(p, n) .. min(p+n, len(s)-1) NaN (every fit whose window holds it is not finite), and later outputs recover. A flat regressor window (every s[i-1] equal, so the slope is undefined) gives 0, as OUTheta and HalfLife do, and a slope b of exactly 1 (a random walk, which has no equilibrium) gives 0 too: both mark an undefined level, so consumers gate on OUTheta < 0 before using the output as a target. A slope just below 1 gives a large and unstable level, and HalfLife says how far it can be trusted. Inputs whose magnitudes lie within about 1e-140 to 1e140 never give NaN from a window of finite values (the flat regressor and b == 1 are excluded before the division); beyond that the slope overflows when the lagged values are nearly equal and the responses are not ({1e-150, math.Nextafter(1e-150, 1), 1e150} with n = 2 gives NaN), and the output can be NaN or infinite.
Uses: the exit target of a mean-reversion trade, with HalfLife as the time stop and OUTheta < 0 as the regime gate, and the centre of SScore. On a pair spread, form the spread first and drop its warm-up zeros, or the fit includes them: x, y := s.Overlap(s2); spread := x.RegressionSpread(y, w).Window(-(w-1)); level := spread.OUMean(n), whose first valid index is (w-1) + n relative to the overlap. Both the powers-of-two series s[i] = 2^-i (b = 0.5 and a = 0) and the period-4 pattern +1, +1, -1, -1 with n a multiple of 4 (b = 0 and a = 0) give 0 exactly.
Verification: two independent references, a Python reference in exact rational arithmetic and a Julia translation of Avellaneda and Lee’s appendix, m = a/(1 - b), agree with it; it equals u.Intercept(u.Shift(1), n) / (1 - u.Beta(u.Shift(1), n)) bit for bit where the regressor window is not flat and the slope is not 1; the powers-of-two and period-4 series give their exact values; and the scaling and sign identities OUMean(2^ku) == 2^kOUMean(u), for every k that keeps 2^k*u and its fitted means normal and finite, and OUMean(-u) == -OUMean(u) hold bit for bit, except that a zero has the same sign for u and -u (+0, or -0 where b > 1 and a is exactly 0).
OUTheta
func (s Series) OUTheta(n int) Series
OUTheta returns the mean-reversion rate per bar of the series, the rolling n-pair autoregression slope minus one.
At each bar t the n lag pairs (s[i-1], s[i]) for i = t-n+1 .. t are fitted by least squares, s[i] = a + bs[i-1], with the shared co-moment kernel behind Beta and Intercept, and the output is b - 1: the slope of the change s[i] - s[i-1] on the lagged level s[i-1]. For a discretised Ornstein-Uhlenbeck process s[i] - s[i-1] = theta(s[i-1] - mu) + noise it estimates theta per bar, typically in (-1, 0) for a mean-reverting series: a value of 0 is a random walk, a positive value a trending or explosive series and a value below -1 an overshooting one. Off flat regressor windows it equals u.Beta(u.Shift(1), n) - 1 bit for bit for t >= n (the Shift puts a 0 at index 0, which only the fits before bar n see). It is the theta of Ernest Chan, Quantitative Trading, second edition (Wiley 2021), chapter 7, Example 7.5, who regresses the change on the centred lagged level: in exact arithmetic that slope equals b - 1.
The first valid index is n (the first bar with n lag pairs, n + 1 values), so the minimum length is n + 1; the outputs before index n are 0. When n < 2 or the series holds fewer than n + 1 values, however large or negative n is, the result is all zeros, with nothing but the output allocated: the test is n >= len(s), which never forms n + 1, so every int n, math.MaxInt and math.MinInt included, is safe. Each output depends only on the n + 1 values s[t-n .. t], so the result is window-local and start-invariant: dropping leading values leaves the remaining outputs unchanged bit for bit.
A non-finite value at index p makes the outputs at t = max(p, n) .. min(p+n, len(s)-1) NaN (every fit whose window holds it is not finite), and later outputs recover. A flat regressor window (every s[i-1] equal, so the slope is undefined) gives 0, not -1: Beta reports a slope of 0 on a flat regressor, so the composition with Beta gives -1 there, whereas the 0 of this method marks an undefined rate, the same 0 that HalfLife and OUMean give on that window. A window of finite values never gives NaN; when the lagged values are nearly equal and the responses are not, the slope overflows and the output is infinite, which can happen for magnitudes within 1e-150 to 1e150 ({1e-150, math.Nextafter(1e-150, 1), 1e150} with n = 2 gives +Inf).
Uses: the regime gate of a mean-reversion trade (trade only while OUTheta < 0 and HalfLife < n/2), which also tells the 0 that HalfLife gives for a random walk, a trending series or a flat window from a true half-life; the speed test of Avellaneda and Lee, who trade only when b = 1 + theta is below e^(-1/30), about 0.967, for 60-bar daily windows; and the rate whose sign qualifies OUMean and SScore. The powers-of-two series s[i] = 2^-i gives b = 0.5 exactly, so the output is -0.5, and the period-4 pattern +1, +1, -1, -1 with n a multiple of 4 gives b = 0, so the output is -1.
Verification: two independent references, a Python reference in exact rational arithmetic and a Julia translation of Chan’s formulation (which regresses the change on the centred lagged level), agree with it; it equals u.Beta(u.Shift(1), n) - 1 bit for bit off flat windows; the powers-of-two and period-4 series give their exact values; and the scaling and sign identities OUTheta(2^ku) == OUTheta(u), for every k that keeps 2^ku normal and finite, and OUTheta(-u) == OUTheta(u) hold bit for bit.
RegressionSpread
func (s Series) RegressionSpread(s2 Series, n int) Series
RegressionSpread returns the residual of the current bar of s from its rolling least-squares fit on s2 over the trailing n right-aligned bars, in the units of s.
The response is s and the regressor s2. Each window of n bars is fitted by ordinary least squares with an intercept, s = alpha + betas2 + e, the slope being that of Beta and the intercept that of Intercept, and the output at the window’s last bar is that bar’s residual e: s less the fitted alpha + betas2. One slope scores the whole window, so a drifting hedge ratio does not pollute the result. On levels the spread is a hedge residual; on returns it is the return of s not explained by s2. Markos Katsanos’s regression divergence with the intercept (Intermarket Trading Strategies, 2008, section 9.5, equation 9.11, and the second listing of Appendix A.1) is the predicted return less the actual return of s, which is minus RegressionSpread on returns (then smoothed); his headline form, equation 9.9, drops the intercept and so differs from it by the fitted intercept. Katsanos warns that the regression suits price differences or yields, not raw price levels. Stéphane Reverre’s CAPM delta (Stocks & Commodities, January 2000) is a one-bar, beta-implied residual in price units, and Arthur A. Merrill’s advance-decline divergence oscillator (Stocks & Commodities, September 1988) is this spread in percent of the fitted level; both are compositions listed under “Compositions”. Several published fair-value regressions fit the whole sample, which uses the future; this method is the rolling form of those with one regressor.
s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n bars.
The result is in the units of s: a price residual on prices, a return residual on returns. Any finite data are valid, positive, negative or zero, so the spread is the family’s answer for zero-crossing series (spreads, rates, back-adjusted futures), where log-ratio lines are NaN on non-positive values and ratio lines flip direction when the denominator changes sign (NaN only at a zero denominator). The kernel is window-local and power-of-two scaled, so levels near 1e9 are safe and the result is bitwise start-invariant: the value at a bar depends only on its own window, and a series sliced to start later gives the same bits there.
Exactly collinear pairs whose ratio is not a power of two leave residuals that are pure rounding noise; without a guard RegressionZScore would report that noise at full scale (|z| above 6 on 3*x + 11 of integer data, beyond the Samuelson bound), and Ernest Chan’s entry at |z| >= 2 would fire on a fixed-ratio share class or a rescaled fund. When the sum of squared residuals is at most 1e-24 of the response’s sum of squared deviations, the fit is treated as perfect and both this method and RegressionZScore return exactly 0. The threshold is scale-free, and a genuine fit that close to perfect has no meaningful residual. A flat s2 gives a slope of 0, so the output is the deviation of the current bar of s from its window mean; a flat s is a perfect fit and gives 0. A window containing a NaN or an infinity gives NaN, and only such windows do, even where the rest of the window is collinear. The warm-up reads 0, so a threshold test (such as Chan’s entry, on RegressionZScore) must be masked before the first valid index. n must be at least 3, since a two-point fit is exact; invalid parameters (no overlap, n < 3, or n greater than the overlap) give len(s) zeros.
In trading, the spread on levels is the mark-to-market of the hedged pair, long one unit of s and short beta units of s2 net of the intercept, and a pairs trade fades it towards zero; RegressionZScore is its standardised form for threshold rules. On returns it is positive when s outran what s2 implied and negative when it lagged, read either as relative strength or as a warning of a pending catch-up; Katsanos’s with-intercept divergence is its negative, smoothed. In-sample, the current bar is part of its own fit, so its residual shrinks by its leverage; the out-of-sample composition (listed under “Compositions”) scores each bar against the previous window’s fit.
Deviations from the published methods: Chan fits once on a training set, without an intercept, and scores later bars against that fit with the training spread’s mean and standard deviation (n-1 in his MATLAB and R listings, n in the Python one); this method refits every window with an intercept, scores each bar against its own window, and gives 0 on a perfect fit. Katsanos’s with-intercept divergence is minus RegressionSpread on returns, afterwards smoothed, and his headline form omits the intercept; Reverre’s delta prices one bar’s return with a 10-day return beta and no intercept, rather than taking a window’s residual; and Merrill’s oscillator is divided by the fitted level.
Compositions that start from it are listed under “Compositions” in the “Relative: Regression and Spread” category.
Verification: the tests compare the result on a fixed grid of fixtures with a Python reference in exact rational arithmetic, which a Julia reference that fits each window by QR factorisation in high precision reproduces, and check the identities that s2 == s gives 0 at every valid index, that 3*s2 + 7 on integer data gives 0 through the guard, and that negating s negates the result, bit for bit.
RegressionZScore
func (s Series) RegressionZScore(s2 Series, n int) Series
RegressionZScore returns the residual of the current bar of s from its rolling least-squares fit on s2, divided by the standard deviation of that window’s residuals.
The response is s and the regressor s2. Each window of n bars is fitted by ordinary least squares with an intercept, as RegressionSpread, and the output at the window’s last bar is that bar’s residual divided by the population standard deviation of the window’s residuals, sqrt(SSR/n), where SSR is the window’s sum of squared residuals. One slope scores the whole window, so a drifting hedge ratio does not pollute the result. This is the rolling pairs z-score. Ernest Chan’s pairs z-score (Quantitative Trading, 2nd edition, 2021, Example 3.6: enter at |z| >= 2; exit a short at z <= 1 and a long at z >= -1) divides by the n-1 standard deviation in his MATLAB and R listings (the Python listing divides by n), so those thresholds are 2*sqrt(n/(n-1)) and sqrt(n/(n-1)) here. Evan Gatev, William Goetzmann and Geert Rouwenhorst’s pairs trading (2006) is the published background. Clifford J. Sherry’s z-score difference (Stocks & Commodities, April 1995) hedges with the ratio of standard deviations, sd(s)/sd(s2) = Beta/r, positive whatever the sign of r, so it matches this residual only when r = 1, where both are zero; at r = -1 the residual is zero but Sherry’s difference is not.
s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n bars.
The result is dimensionless and scale-free: scaling either input by a positive constant leaves it unchanged (bit for bit for a power of two), negating s2 leaves it unchanged, and negating s negates it. In-sample it is bounded by sqrt(n-1) in exact arithmetic, since the residuals of a fit with an intercept have zero mean (the Samuelson bound); on a nearly collinear window that the guard lets through, rounding can take the computed value past the bound, by about 1e-16*sqrt(n)*sqrt(Syy/SSR) (5e-5 at 1 - R^2 = 1e-23). Any finite data are valid, positive, negative or zero, so the method serves zero-crossing series (spreads, rates, back-adjusted futures), where log-ratio lines are NaN on non-positive values and ratio lines flip direction when the denominator changes sign (NaN only at a zero denominator). The kernel is window-local and power-of-two scaled, so levels near 1e9 are safe and the result is bitwise start-invariant: the value at a bar depends only on its own window, and a series sliced to start later gives the same bits there.
Exactly collinear pairs whose ratio is not a power of two leave residuals that are pure rounding noise; without the guard the z-score reports that noise at full scale (|z| above 6 on 3*x + 11 of integer data, beyond the Samuelson bound), and Chan’s entry at |z| >= 2 would fire on a fixed-ratio share class or a rescaled fund. When the sum of squared residuals is at most 1e-24 of the response’s sum of squared deviations, the fit is treated as perfect and both this method and RegressionSpread return exactly 0. The threshold is scale-free, and a genuine fit that close to perfect has no meaningful residual. A flat s2 gives a slope of 0, so the residuals are the deviations of s and the output is the population z-score of the current value of s in its window, as Standardise; a flat s is a perfect fit and gives 0. A window containing a NaN or an infinity gives NaN, and only such windows do, even where the rest of the window is collinear. The warm-up reads 0, so a threshold test (Chan’s entry at |z| >= 2) must be masked before the first valid index, where the zeros would otherwise read as an exit. n must be at least 3, since a two-point fit is exact; invalid parameters (no overlap, n < 3, or n greater than the overlap) give len(s) zeros.
In trading, this is the pairs signal: with Chan’s thresholds moved to the population scale, short s and long beta units of s2 when z is at or above 2*sqrt(n/(n-1)), the mirror when it is at or below the negative of that; exit a short when z is at or below sqrt(n/(n-1)) and a long when it is at or above -sqrt(n/(n-1)); the sign says which leg is rich. In-sample, the current bar is part of its own fit, so its residual shrinks by its leverage. The out-of-sample residual, a composition of Intercept and Beta listed under “Compositions” in the “Relative: Regression and Spread” category, scores each bar against the previous window’s fit; it is in the units of s, not a z-score, and these thresholds apply to it only once it is divided by that window’s residual standard deviation, sqrt(SSR/n), which neither Intercept nor Beta supplies.
Deviations from the published methods: Chan fits once on a training set, without an intercept, and scores later bars against that fit with the training spread’s mean and standard deviation (n-1 in his MATLAB and R listings, n in the Python one); this method refits every window with an intercept, divides by n, scores each bar against its own window, and gives 0 on a perfect fit. Sherry differences two separately standardised inputs, so his hedge ratio is the ratio of standard deviations rather than the slope. Markos Katsanos’s with-intercept regression divergence, the published relative of RegressionSpread, has the opposite sign and is smoothed.
Verification: the tests compare the result on a fixed grid of fixtures with a Python reference in exact rational arithmetic (exact up to the final square root, rounded once), which a Julia reference that fits each window by QR factorisation in high precision reproduces, and check the identities that s2 == s gives 0 at every valid index, that 3*s2 + 7 on integer data gives 0 through the guard, and that negating s negates the result and negating s2 leaves it unchanged, bit for bit.
ReturnBeta
func (s Series) ReturnBeta(s2 Series, n, k int) Series
ReturnBeta returns the rolling least-squares slope of the k-bar percentage returns of s regressed on those of s2 over the trailing n returns.
The response is s and the regressor s2: each value is Beta over the n most recent k-bar signed percent returns of the two inputs, where a return is (v/v0 - 1)*100 on a positive base, the expression of RateOfChange, and (v/(-v0) + 1)*100 on a negative base, so that a rise is always positive. William Brown’s BGEIX against the S&P 500 on eleven monthly gains gives a beta of 0.227. The regression code of Markos Katsanos in Intermarket Trading Strategies (2008) computes b := Correl(RS1,RS2,D1,0) * Stdev(RS1,D1)/Stdev(RS2,D1) on ROC(C,D2,%) returns, which on positive prices is s.ReturnBeta(s2, D1, D2) up to the subtract-first order of MetaStock’s ROC; its intercept Mov(RS1,D1,S) - b*Mov(RS2,D1,S) is the returns’ intercept.
s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.
The first valid index is off + n + k - 1, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar: the first k paired bars have no k-bar return and the first window takes n returns, so the minimum overlap is n + k bars. The warm-up reads 0, not NaN, so a threshold test (for example a beta filter b > 1) must be masked before the first valid index.
ReturnBeta is dimensionless, percent of s per percent of s2: the market beta when s2 is an index. Scott Silver and Gary Wingens noted, citing Meir Statman’s comparison of weekly and monthly betas, that a stock’s beta depends on the return interval (Stocks & Commodities, 1985): s.ReturnBeta(s2, n, k) uses overlapping k-bar returns, and a true monthly beta needs true monthly bars, not a strided daily series. The valid data are prices that do not cross zero, normally positive ones; on back-adjusted futures, spreads, rates and other zero-crossing series use DeltaBeta.
The kernel is window-local and power-of-two scaled, so levels near 1e9 are safe and the result is bitwise start-invariant. The result is NaN on exactly the windows containing a non-finite return: a NaN or an infinity at pair p makes returns p and p + k NaN, so the outputs at pairs p to p+n-1 and p+k to p+k+n-1 are NaN from the first valid index on, and a zero price at pair p makes only return p + k NaN. The negative-base leg negates the returns exactly, so s.Mul(-1).ReturnBeta(s2, n, k) is the exact negation of s.ReturnBeta(s2, n, k). Invalid parameters (no overlap, n < 2, k < 1, k not less than the overlap, or n greater than the overlap less k) give len(s) zeros, in time proportional to len(s) however large n or k.
Uses: a beta filter, as when Donald W. Pendergast Jr. admits only stocks whose beta to the S&P 500 exceeds 1.00 (Stocks & Commodities, 2014); up and down betas; and the beta-adjusted stops of Thomas Bulkowski.
Deviations from the published code: the signed percent return in place of the subtract-first (v - v0)/v0100 of MetaStock’s ROC, the same number up to rounding on positive bases (the two orders differ by up to about 6e-14max(1, |v/v0|)) and of the opposite sign on negative ones; NaN at a zero base; and zeros before the first valid index.
Compositions that start from it are listed under “Compositions” in the “Relative: Regression and Spread” category.
Verification: a Python reference in exact rational arithmetic and a Julia reference, which also translates Katsanos’s published MetaStock regression code, agree with ReturnBeta, and s.ReturnBeta(s2, n, k) equals s.RateOfChange(k).Beta(s2.RateOfChange(k), n) bit for bit from the first valid index on equal-length finite positive inputs.
SScore
func (s Series) SScore(n int) Series
SScore returns the Avellaneda-Lee s-score of the series, its distance from the rolling Ornstein-Uhlenbeck equilibrium in units of the equilibrium standard deviation.
At each bar t the n lag pairs (s[i-1], s[i]) for i = t-n+1 .. t are fitted by least squares, s[i] = a + bs[i-1], with the shared co-moment kernel behind Beta and Intercept, and the residuals of that fit are formed from its centred deviations. With ssr their compensated sum of squares in the kernel’s scaled units (math.Ldexp(ssr, 2fit.ey) is the sum in the squared units of s) the output is, in exactly this order:
sigmaZeta := math.Ldexp(math.Sqrt(ssr/float64(n)), fit.ey)
mu := a / (1 - b)
sigmaEq := sigmaZeta / math.Sqrt(1-float64(b*b))
S := (s[t] - mu) / sigmaEq
Here sigmaZeta is the residuals’ standard deviation with divisor n, in the units of s; mu is the equilibrium, OUMean; sigmaEq is the stationary standard deviation of the fitted process, sigmaZeta/sqrt(1 - b^2), with b*b rounded before the subtraction so that this step cannot fuse into a multiply-add (the kernel’s co-moment sums still fuse on arm64, so the last bits can differ between arm64 and amd64); and S is the score. It is the s-score of Avellaneda and Lee (2010), “Statistical arbitrage in the US equities market”, Quantitative Finance 10(7):761-782, section 4 and Appendix A, and it is approximately N(0, 1) for a stationary Ornstein-Uhlenbeck process. Compared with the plain z-score of Standardise, the target is the equilibrium rather than the window mean, and the scale is the stationary standard deviation rather than the window standard deviation.
The first valid index is n (the first bar with n lag pairs, n + 1 values), so the minimum length is n + 1; the outputs before index n are 0. When n < 3 (two pairs with distinct lagged values fit exactly) or the series holds fewer than n + 1 values, however large or negative n is, the result is all zeros, with nothing but the output allocated: the test is n >= len(s), which never forms n + 1, so every int n, math.MaxInt and math.MinInt included, is safe. Each output depends only on the n + 1 values s[t-n .. t], so the result is window-local and start-invariant: dropping leading values leaves the remaining outputs unchanged bit for bit.
A non-finite value at index p makes the outputs at t = max(p, n) .. min(p+n, len(s)-1) NaN (every fit whose window holds it is not finite), and later outputs recover. The perfect-fit guard comes first: when the responses are flat, or the residual sum of squares is at most 1e-24 times the responses’ centred sum of squares (the residuals are rounding noise), the output is 0, since the scale would vanish and the score would be infinite or undefined. Just outside the guard the score can be far beyond any trading threshold, which marks a nearly deterministic window rather than a signal: {1, 0.5, 0.25, 0.125 + 2^-40} with n = 3 gives about 3.9e11, while 2^-41 falls inside the guard and gives 0. Otherwise the score is formed only when the regressor window is not flat and -1 < b < 1, so that the fitted process is stationary; a flat regressor window (every s[i-1] equal, an undefined slope) and a slope of magnitude 1 or more (a unit root, b = ±1, or an explosive fit, with no stationary distribution) give 0. OUTheta < 0 rules out b >= 1 but not b <= -1 (OUTheta <= -2) or the guard (u[i] = 3^i*4^(33-i) has OUTheta within 2^-53 of -0.25, b being 3/4 to within an ulp, and a score of 0), so consumers treat every 0 as no signal. Inputs whose magnitudes lie within about 1e-150 to 1e150 never give NaN from a window of finite values (the branch order excludes every 0/0: the flat regressor, b == 1 and the guard come first); outside that range mu and the stationary standard deviation can overflow and the output can be NaN or infinite.
Uses: the trading rules of Avellaneda and Lee, who open a long at S < -1.25 and a short at S > +1.25, close a short at S < +0.75 and a long at S > -0.50, and trade only when mean reversion is fast, b < e^(-1/30), about 0.967, for 60-bar daily windows (check with OUTheta, which is b - 1). Their kappa = -ln(b)252 is undefined for -1 < b <= 0; this method still gives a score there. They do not say whether the residual variance divides by n or n - 2; this method divides by n (the n - 2 divisor would scale the score by sqrt((n-2)/n), 1.7% at n = 60). They apply the score to cumulative factor residuals and centre mu cross-sectionally; applying it to a pair spread directly, as x, y := s.Overlap(s2); spread := x.RegressionSpread(y, w).Window(-(w-1)); z := spread.SScore(n), is an adaptation. With n a multiple of 4, the period-4 pattern +1, +1, -1, -1 gives b = 0, a = 0, mu = 0 and sigmaEq = 1, so the score equals the series exactly (u.Standardise(n) is within a few ulps of it, 62^-53 at n = 60 and n = 100, not equal: the rolling variance leaves a residual mean).
Verification: two independent references, a Python reference in exact rational arithmetic (the square roots to 100 digits or more) and a Julia translation of Avellaneda and Lee’s appendix with the divisor n, agree with it; on the period-4 pattern it equals the series exactly; its mean and its slope minus one are OUMean and OUTheta bit for bit off flat windows; and the sign and scaling identities SScore(-u) == -SScore(u) and SScore(2^ku) == SScore(u), for every k that keeps 2^ku and its fitted means and scales normal, hold bit for bit, except that a zero output is +0 for both u and -u.
Compositions
Ritchie’s alpha indicator: the intercept of log returns as a timing signal
Frank Ritchie (“An Alpha Indicator For Bonds”, Stocks & Commodities, July 1996) regresses the 9 most recent daily log returns of Treasury bond futures on those of a cyclical-stock fund (Fidelity Select Chemicals) and averages the intercept over 12 bars: above zero, bonds have a positive bias of their own. With the traded market as s and the comparison as s2:
a, b := s.Overlap(s2)
ra := a.Log().Delta(1).Window(-1)
rb := b.Log().Delta(1).Window(-1)
alpha := ra.Intercept(rb, 9).Window(-8).Average(12)
valid := alpha.Window(-11) // the recipe's valid bars
- First valid index: 1 + 8 + 11 = 20, counted from the start of the overlap: one bar for the log return,
n - 1 = 8for the intercept ofn = 9returns and12 - 1 = 11for the average. In general, withnreturns and an average ofwbars, it isn + w - 1.alphahas lengthm - 9for an overlap ofmbars; its first 11 elements areAverage’s pass-through values, not zeros, so readvalid. - Minimum overlap: 21 bars (
n + w), which give one valid bar. - Domain: positive prices. A zero, negative or non-finite price gives an infinite or NaN log return (the log of 0 is -Inf), and
Intercept’s non-finite rule turns every window containing it into NaN: one bad price enters two log returns and ten intercepts, so it makes the nextn + w = 21values ofalphaNaN, after which the recipe recovers.Deltakeeps its legacy semantics, but the legacy percent rules do not arise here: the returns are differences of logs. - Anchors: with
s2 == s, each intercept is exactly 0 on every window whose nine log returns are not all equal, becauseIntercept(s, s)is 0 on windows that are not flat, so the recipe is exactly 0 on every valid bar whose twelve intercept windows are not flat (tag E). A window of nine identical log returns has a flat regressor, and its intercept is that return (the window mean ofs): a series growing by a constant factor gives such windows, and a constant price gives 0. The chain on theOverlapresults equals the chain on inputs trimmed to the overlap (the composition property of RELATIVE-INDICATORS-SPEC.md §3.5, E). References A (Python) and B (Julia) compute it exactly, and B also translates the Excel formulas printed in the sidebar to Ritchie’s article, which is signed by the magazine’s editor (D3 = LN(B3/B2),F11 = INDEX(LINEST(D3:D11,E3:E11,TRUE,FALSE),2),G22 = AVERAGE(F11:F22)); they agree to about 1e-16. - Published vector: on the sidebar’s 30 printed closes of the bond future and the fund, the recipe reproduces the printed alpha (column F, rows 11 to 31) and the printed indicator (column G, rows 22 to 31, from -0.00098 to -0.00200) to their five decimals (testdata/ref/inputs/tasc-ritchie-14-7.csv).
- Pinned by
TestRecipeRitchieIntercept; runnable exampleExampleSeries_Intercept_ritchie.
The out-of-sample regression spread
RegressionSpread scores each bar against a fit that includes that bar, so the current residual shrinks by its leverage, 1 - h, where h is the bar’s weight in its own fit. Scoring each bar against the previous window’s fit removes the shrinkage:
a, b := s.Overlap(s2)
oos := a.Sub(a.Intercept(b, n).Shift(1)).Sub(a.Beta(b, n).Shift(1).Mul(b))
valid := oos.Window(-n) // the recipe's valid bars
- First valid index:
n, counted from the start of the overlap:InterceptandBetaare valid fromn - 1, andShift(1)moves each window’s fit to the next bar.ooshas lengthmfor an overlap ofmbars; its firstnelements subtract warm-up zeros and are not residuals, so readvalid. - Minimum overlap:
n + 1bars, which give one valid bar. - Domain: any finite data. A NaN or an infinity in the previous window makes the bar NaN; in the bar’s own pair it passes through IEEE arithmetic, so an infinite price gives an infinite spread there and a NaN gives NaN.
- Anchors: with
s2 == s,Beta(s, s)is exactly 1 andIntercept(s, s)exactly 0 on windows that are not flat, so the recipe is exactly 0 on every valid bar whose previous window is not flat (tag E); the chain on theOverlapresults equals the chain on inputs trimmed to the overlap (the composition property of RELATIVE-INDICATORS-SPEC.md §3.5, E). Reference A computes it in exact rationals, and reference B, by QR in 512-bit floating point, reproduces it. - Pinned by
TestRecipeOutOfSampleSpread; runnable exampleExampleSeries_RegressionSpread_outOfSample.
Reverre’s CAPM delta
Stéphane Reverre (“Are Two Channels Better Than One?”, Stocks & Commodities, January 2000) measures how far today’s price of s has moved from the price that yesterday’s price and a 10-day return beta imply, given today’s move in s2, and trades it on a one-standard-deviation Bollinger channel around its 10-day mean:
a, b := s.Overlap(s2)
g := b.Div(b.Shift(1)).Sub(1)
delta := a.Sub(a.Shift(1).Mul(a.ReturnBeta(b, 10, 1).Mul(g).Add(1)))
valid := delta.Window(-10) // the recipe's valid bars
- First valid index: 10, counted from the start of the overlap (the first valid index of
ReturnBeta(b, 10, 1),n + k - 1). - Minimum overlap: 11 bars.
- Domain: positive prices.
ReturnBetauses the signed percent return andgthe plain ratio ofDiv, which agree only on positives2, andDivgives 0 for a zero divisor. The factor of 100 inReturnBeta’s percent returns cancels in the beta. - Anchors: a constant positive
s2gives exactlya.Momentum(1)on valid bars, because the beta of a flat regressor is 0 andgis 0 (tag E, throughMomentum); the composition property (E). - Pinned by
TestRecipeCAPMDelta; runnable exampleExampleSeries_ReturnBeta_capmDelta.
Merrill’s ADDO
Arthur A. Merrill’s advance-decline divergence oscillator (“Advance-Decline Divergence As An Oscillator”, Stocks & Commodities, September 1988) is the percent deviation of an index from its rolling regression on a cumulative advance-decline line: positive when price runs ahead of breadth, which he reads as bearish. He used weekly bars and a 52-week window:
a, b := s.Overlap(s2)
sp := a.RegressionSpread(b, n)
addo := sp.Div(a.Sub(sp)).Mul(100)
valid := addo.Window(-(n - 1)) // the recipe's valid bars
- First valid index:
n - 1, counted from the start of the overlap. - Minimum overlap:
nbars. - Domain: positive levels:
a.Sub(sp)is the fitted level, andDivgives 0 where it is 0. The fit’s intercept absorbs the origin of a cumulative line, so where the line starts changes only the rounding. The fit includes the current bar; Merrill’s “preceding year” may exclude it, which the out-of-sample spread above does. - Anchors:
s2 == sgives exactly 0 (tag E, through the perfect-fit guard’s exact zero); the composition property (E). - Pinned by
TestRecipeADDO; runnable exampleExampleSeries_RegressionSpread_addo.
Chan’s rolling-hedge spread and its position
The spread of s against s2 with a rolling hedge ratio, and the linear mean-reversion position on it: Ernest Chan’s PriceSpread.m (Algorithmic Trading, 2013, chapter 3), which fits each window’s hedge ratio by least squares with a constant and forms yport = y - hedgeRatio*x without subtracting the intercept, so the spread is not mean-zero; the position is minus its z-score over m bars.
a, b := s.Overlap(s2)
spread := a.Sub(a.Beta(b, n).Mul(b)).Window(-(n - 1))
position := spread.Standardise(m).Mul(-1).Window(-(m - 1))- First valid index:
n - 1for the spread andn + m - 2for the position, counted from the start of the overlap; elementeof each is overlap barn - 1 + eorn + m - 2 + e. Minimum overlap:n + m - 1bars for the position. - Parameters:
n >= 2andm >= 2; a window of 1 makes a trimWindow(0), which returns an empty series. - Hedge ratios: each bar’s spread uses its own window’s hedge ratio, so the z-score window mixes hedge ratios; that is Chan’s published behaviour. The single-fit alternative is
RegressionZScore, which scores each window’s residuals against one fit; its spread, net of the intercept, isRegressionSpread. - Convention: the position uses the population deviation; Chan’s
movingStdis the sample deviation, which scales it bysqrt((m-1)/m). - Domain: a non-finite price gives NaN for the windows holding it; a flat
s2window gives a hedge ratio of 0, so the spread issthere. - Anchors: on windows where
s2is not flat,s2 == sands = 2^k*s2give a spread of exactly 0 (E); on a flats2window the spread iss. Other exactly collinear pairs, such ass = 3*s2 + 7, leave rounding noise that the position reports at full scale (|z|reaches 3 on a random walk);RegressionZScoreguards against this. - Pinned by
TestRecipeT3HedgedSpread.