Relative series

All functions · nseries package

Sections: Relative: Alignment · Relative: Ratio and Relative Strength · Relative: Channels · Relative: Correlation · Relative: Regression and Spread · Relative: Divergence · Relative: Lead-Lag and Phase · Relative: Cross-Section · Relative: Timeframes

Relative: Alignment

Time alignment

Every two-series method pairs values by position only, right-aligned at the most recent bar. With m = min(len(s), len(s2)) and off = len(s) - m, bar off+j of the receiver is paired with bar len(s2)-m+j of the other series; the output has the receiver’s length and is zero before off. Overlap and the relative methods have always paired this way, and the older two-series methods now do too: Add, Sub, Mul and Div with a Series argument, the comparisons (Equal, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual and their aliases), Ratio, SAverage, SpearmanRankCorrelation, CrossesAboveS, CrossesBelowS, If with a Series branch and the FloorTrader levels. New two-series methods, such as the volume-weighted averages VWMA and VWEMA, the per-bar-alpha XAverageAlphaS and the exponentially weighted XCovariance and XBeta, pair this way from the start. QStick and SuperTrend inherit it through Sub and Add. An empty Series operand is the m = 0 case and gives zeros. The exceptions are the methods whose first valid bar depends on the data: with valid parameters they read NaN, not 0, before the overlap, and on every bar for an empty operand. These are the event operators ValueWhen, HighestSince, LowestSince, SumSince and ChangeSince, and the variable-length windows AverageVariable, SumVariable, WAverageVariable, HighestVariable, LowestVariable and StdDevVariable. BarsSince takes no second series, but it too reads NaN, not 0, until the first event in its receiver.

nseries has no timestamps, so position is all it can pair. It cannot see that two series keep different calendars (exchange holidays, half days, a session one market trades and the other does not), that one of them has missing bars, or that one is released on its own schedule, as FRED economic series and the CFTC’s weekly Commitments of Traders are. A bar that one input has and the other lacks shifts every earlier pairing by one bar, and the output still looks plausible. Line such series up in time, by their timestamps, before they reach nseries, so that equal positions refer to equal times.

For a slow or irregular series, compute paired-window statistics (correlations, betas, regression spreads, channels on a relationship line) on that series’ own release clock, sampling both legs at its releases. Do not compute them on base bars with the slow series carried forward: carrying a value forward makes most of its bar-to-bar changes zero, which biases every paired-window statistic towards zero (the Epps effect; RELATIVE-INDICATORS-SPEC.md §3.15, rule 5, and §3.4).

Sign conventions

In every relative method s is the base and s2 the comparison. The relationship lines and the relative-strength quantities are base against comparison, so on positive prices they rise when s outperforms s2: PriceRelative is s/s2, LogRatio is ln s - ln s2, and RSMK is the log return of s less that of s2; RegressionSpread and RegressionZScore are positive when s stands above its fit on s2, and DistanceScore when s has risen more than s2 since the start of its window; and the relative momentum recipe is the return of s less that of s2. The divergence family is the other way round, comparison less base, so a positive value means that s has lagged s2: IntermarketDisparity, IntermarketDisparityX, BollingerDivergence, ZScoreDivergence, IntermarketLRSDivergence and IntermarketLRSDivergenceCV take the leg of s2 (scaled or sign-adjusted where the method says so) less the leg of s; IntermarketRegressionDivergence takes the return of s that s2 predicts less its actual return; and the recipes for Katsanos’s relative rate of change and the per-leg channel-position divergence follow the same sign. Stress keeps Kaufman’s sign: its difference is the position of s less that of s2, so a high reading means that s is high in its own range relative to s2. The lead-lag entries take Shift’s sign: a positive lag of CrossCorrelation or LeadLag, or a positive LeadLagBias, means that s2 leads, and LeadLagCorrelation carries the sign of the correlation.

Overlap

func (s Series) Overlap(s2 Series) (Series, Series)

Overlap returns copies of the trailing bars that s and s2 have in common, right-aligned at the most recent bar, so both results have length min(len(s), len(s2)).

s is the base and s2 the comparison. The inputs are right-aligned at their most recent bar and only the trailing m = min(len(s), len(s2)) bars are paired: the results are fresh copies of s[len(s)-m:] and s2[len(s2)-m:], with separate storage, so writing to or appending to either result never changes s, s2 or the other result. An empty input gives two empty, non-nil Series. Values are copied bit for bit, NaN and ±Inf included.

The first valid index is 0 and the minimum overlap is 1: every returned bar is a paired input bar, and there is no warm-up.

Overlap is the alignment primitive of the relative indicators. Any single-series method, and the arithmetic methods with a Series argument, become correct two-series building blocks when they are applied to its results, whatever the lengths of s and s2. The composition rule is:

  1. compose on the results, a, b := s.Overlap(s2), which have equal lengths and pair bars from the most recent one;
  2. trim each stage’s warm-up with Window(-f), where f > 0 is that stage’s first valid index (the recipe named below tabulates f for common stages); a stage whose first valid index is 0 needs no trim, and Window(0) would return an empty Series;
  3. the first valid index of a chain of trimmed stages is the sum of the stages’ first valid indices, and the minimum overlap is one more;
  4. a binary stage (Add, Sub, Mul, Div with a Series argument, and the comparisons) right-aligns its operands at the most recent bar, so operands trimmed by different amounts are still paired correctly, but its result is zero on the receiver’s leading bars that have no partner, and a later stage reads those zeros as data; trim both operands by the same amount, or combine at full length and trim once.

For example, a.Div(b).XAverage(3).Window(-2).Highest(300) has first valid index 2 + 299 = 301 and minimum overlap 302, counted from the start of the overlap. The result is right-aligned at the most recent bar, so once the overlap has at least the minimum overlap (plus k for LookBack(k)), Value() and LookBack(k).Value() read valid bars; a shorter overlap still gives numbers, so check its length first. Written on s and s2 directly, s.Div(s2) pairs the right bars (it right-aligns, as every two-series method does), but its first valid index is counted from the start of s rather than from the start of the overlap, and when s is the longer input its result is zero before the overlap: a following stage with a long memory, such as XAverage(150), takes those zeros as data and carries them into valid bars. The equivalent idiom m := min(len(s), len(s2)); s.Right(m), s2.Right(m) gives aliasing views with the same values. The worked recipe is listed under “Compositions” in the “Relative: Alignment” category.

Overlap cannot repair temporal misalignment: bars are paired by position from the most recent bar, so a session one input has and the other lacks shifts every earlier pairing.

Verification: a Python reference and a Julia reference slice the three pairs of testdata/ref/inputs/mismatch.csv, and the Go results equal them bit for bit. For every pair of lengths the first result equals s.Right(len(s2)) and the second equals s2.Right(len(s)) bit for bit (Right is the existing method; Overlap copies where Right aliases), s.Overlap(s) returns two separate copies equal to s, and Overlap is idempotent.

Compositions

Composing a relative indicator over Overlap

Any single-series method becomes a correct two-series building block when it runs on the Overlap results. Trim each stage’s warm-up with Window(-f), where f is that stage’s first valid index (see the table below); a stage with f = 0 needs no trim, and Window(0) would return an empty series, so a parameterised trim such as Window(-(n-1)) must skip n = 1. The first valid index of the chain is the sum of the stages’ first valid indices, and the minimum overlap is one more. A binary stage (Add, Sub, Mul, Div with a Series argument, and the comparisons) right-aligns its operands at the most recent bar, so operands of different lengths are still paired correctly, but its result is zero on the receiver’s leading bars that have no partner, and a later stage reads those zeros as data. Trim both operands of a binary stage by the same amount, or combine them at full length and trim once.

a, b := s.Overlap(s2)
x := a.Div(b).XAverage(3).Window(-2).Highest(300)
valid := x.Window(-299) // empty unless the overlap has at least 302 bars
  • First valid index: 2 + 299 = 301, counted from the start of the overlap. x has length m - 2 for an overlap of m bars, and its element 299 is the first valid one. Its earlier elements are maxima of shorter windows, not zeros, and an overlap shorter than 302 bars still gives numbers, so check len(a) >= 302, or read valid, before using x.Value().
  • Minimum overlap: 302 bars, which give exactly one valid bar.
  • Anchors: the chain on the Overlap results equals, bit for bit, the same chain on inputs trimmed to the overlap without Overlap (tag E, exact; the composition property of RELATIVE-INDICATORS-SPEC.md §3.5), and each valid bar is the maximum of exactly the 300 smoothed ratios ending at that bar, its own included. Overlap itself equals Right of the other input’s length on both results (E, through the existing Right).
  • Why the rule matters: Div with a Series argument once paired bars from the start of each series and padded the shorter one with its last value, so s.Div(s2).XAverage(3).Highest(300) on inputs of different lengths differed from the correct value at every valid bar of the walk3 fixtures (testdata/ref/inputs/walk3.csv). Div now right-aligns, and on those fixtures the one-liner equals the recipe at every valid bar, but only because XAverage(3) forgets its seed within a few bars. Its result is as long as s, so its first valid index counts from the start of s, not of the overlap; and when s is the longer input, its first off values are zeros that seed the next stage. A stage with a long memory carries them into valid bars: s.Div(s2).XAverage(150) differs from a.Div(b).XAverage(150) at all 351 valid bars on (w1, w2[100:]), by up to 19%. Composing over Overlap keeps every stage on paired bars. In this chain the Window(-2) trim happens to change no valid bar, because Highest’s window at bar 301 starts after XAverage’s warm-up. The trim matters when a later stage reads warm-up values: in StdDev(n) followed by XAverage, the untrimmed leading zeros enter XAverage’s seed and change valid bars.
  • Pinned by TestRecipeCompositionRule; runnable example ExampleSeries_Overlap_compositionRule.

Div keeps its legacy semantics: a zero divisor gives 0, not NaN.

First valid index of common stages

StageFirst valid index fBefore f
StdDev(n), ZScore(n, v), R2(n), Slope(n), Standardise(n)n - 1zeros
Average(n), Bollinger(n, k)n - 1the input, passed through
XAverage(n)n - 1the input, passed through; it then seeds with the average of the first n inputs, so run it only on trimmed input
Highest(n), Lowest(n)n - 1the maximum or minimum of the bars so far
RateOfChange(k), Delta(k), Momentum(k)kzeros
Add, Sub, Mul, Div0—

Pinned by TestRecipeStageFirstValid.

Relative: Ratio and Relative Strength

DownCapture

func (s Series) DownCapture(s2 Series, n int) Series

DownCapture returns the percentage of the one-bar losses of s2 matched by s over the trailing n bars in which s2 fell.

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, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n + 1 bars.

Overlap index j is result index off + j. On the paired bars, r_a and r_b are the one-bar percent changes of s and s2, 0 on the first paired bar, and a bar qualifies when s2 fell on it. DownCapture is, over the qualifying bars among the last n, the arithmetic form of Morningstar’s capture ratio:

DownCapture = 100*Σ r_a/Σ r_b over the bars with r_b < 0

Each return is multiplied by a mask, 1 on a qualifying bar and 0 on any other (a plain product, so a NaN return stays NaN); the masked returns of s and of s2 are each summed over the last n bars with Sum, and the result is the first sum divided by the second, times 100. It is NaN when either sum is NaN and +0 when no bar of the window qualifies. It is in percent: 100 means the summed returns of s on those bars equal those of s2, a value above 100 that s lost more than s2 on them, one below 100 that it lost less, and a negative value that it rose on them overall.

Morningstar compounds the qualifying returns and takes the ratio of their geometric means (the compound form), so its published values differ from this arithmetic form. No minimum count of qualifying bars is applied: few or tiny falls of s2 in the window give extreme values, and a single tiny fall can give a very large ratio.

On positive finite inputs, unless a one-bar percent change overflows (|v/v0| above about 1.8e306; DownCapture then gives NaN where RateOfChange gives an infinity), DownCapture equals, bit for bit at overlap index j >= n, this composition of RateOfChange, LT, Mul, Sum and Div on the paired views a and b of s and s2:

rb := b.RateOfChange(1)
dn := rb.LT(0)
a.RateOfChange(1).Mul(dn).Sum(n).Div(rb.Mul(dn).Sum(n)).Mul(100)

A negative base gives a signed return, (v/(-v0) + 1)*100 for a move from v0 to v, so a move from -2 to -1 counts as a rise of 50 percent where RateOfChange gives a fall of 50 percent; on such data the method and the composition differ. Negative and sign-crossing values give no NaN.

NaN footprint: a NaN or an infinity at overlap index p of either input makes the returns at p and p+1 NaN, so the result is NaN at overlap indices j in [max(p, n), p+n], and later values recover. A zero of either sign in s2 at p makes only the return at p+1 NaN, giving NaN on [max(p+1, n), p+n], and when the value before it is positive the bar at p qualifies with a return of -100 percent; a zero in s at p gives NaN on the same bars whatever the mask, since NaN*0 is NaN. A one-bar percent change that overflows at return index p gives NaN on [max(p, n), p+n-1]. Sums of qualifying returns that overflow (returns near 1e308) give NaN where both sums overflow, and otherwise ±0 or a tiny value. A bar on which s2 is unchanged qualifies for neither ratio.

If n < 1 or n >= min(len(s), len(s2)), so that the overlap is shorter than n + 1 bars, every element of the result is zero. DownCapture is not start-invariant: Sum regroups its window in blocks counted from the first paired bar, so trimming or extending the start of the inputs can change the last bits of later values.

Warm-up hazard: before the first valid index the result is +0, which reads as “captured nothing” (a capture of 0, as if s lost nothing while s2 fell). A rule such as DownCapture > 100 or UpCapture > 100 does not fire there, but DownCapture < 100 does, and so do UpCapture < 100 and RelativeEfficiency < x for any x > 0 on their own warm-ups. To avoid this, trim with Window or mask with SetN. The same +0 appears after the warm-up wherever no bar of the window qualifies: during a steady rise of s2, DownCapture is 0, so DownCapture < 100 fires throughout.

Verification: a Python reference in exact rational arithmetic and an independent Julia reference agree with the method; on positive finite inputs DownCapture equals the composition above bit for bit, and s.DownCapture(s, n) is exactly 100 wherever the window has a bar in which s fell and the window’s sums are finite (0 where it has none; returns near 1e308 can overflow the sums and give NaN).

LogRatio

func (s Series) LogRatio(s2 Series) Series

LogRatio returns ln(s) - ln(s2) at each right-aligned bar, the natural log of the relative strength of s against s2, or NaN where either price is not finite and positive.

It is the log of the relative strength: the logarithm of John Murphy’s price relative and of Markos Katsanos’s Intermarket Relative Strength (see PriceRelative), taken as a difference of logs, ln(s) - ln(s2), and never as the log of the quotient. On this scale equal percentage outperformance moves the line by equal distances, and its n-bar change is the n-bar log return of s less that of s2 (see RSMK).

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, counted from the start of the overlap, is 0 (result index len(s) - min(len(s), len(s2))), and the minimum overlap is 1: there is no warm-up, and every paired bar has a value.

With m = min(len(s), len(s2)) and off = len(s) - m, pair j (0 <= j < m) is s[off+j] against s2[len(s2)-m+j], and its value is written at off+j. The valid-data class is prices on a scale that stays positive: LogRatio is invalid on Panama back-adjusted futures and on series loaded with a detrending option, instruments quoted in different currencies must be put in one currency first, and total-return series are preferred for long equity windows, where dividends would otherwise bias the line. It is pointwise and start-invariant: each value depends only on its own pair, so trimming earlier history changes no later value.

LogRatio is antisymmetric: s.LogRatio(s2) == -s2.LogRatio(s), bit for bit, at every pair where both are numbers, apart from the sign of a zero result (equal prices give +0 both ways). Its magnitude is at most about 1454.2, the log range of float64 (from the smallest subnormal to the largest finite value). It differs from Log() outside the positive domain: a zero price gives NaN rather than -Inf, and a negative, NaN or infinite price gives NaN. A subnormal price is scaled before its logarithm is taken, so it is correct on every platform.

Deviations from the published code: the listings write log(C/SEC2), the log of a quotient, whereas LogRatio takes ln(s) - ln(s2), so no quotient can overflow or underflow and the antisymmetry is exact; and a non-positive price gives NaN rather than an undefined value.

Compositions that start from it are listed under “Compositions” in the “Relative: Ratio and Relative Strength” and “Relative: Channels” categories.

Verification: two independent references, a Python reference in exact decimal arithmetic and a Julia reference in BigFloat, which also translates the published listings, agree with it; and there is an exact identity with an existing method: LogRatio of an all-ones s2 equals s.Log() bit for bit on normal positive values.

PriceRelative

func (s Series) PriceRelative(s2 Series, smooth int) Series

PriceRelative returns the price ratio s/s2 at each right-aligned bar (Murphy’s price relative and Katsanos’s Intermarket Relative Strength), optionally smoothed by an exponential moving average of length smooth.

It is the price relative of John Murphy and the Intermarket Relative Strength of Markos Katsanos, each from a book on intermarket analysis: the price of s divided by the price of s2, which rises while s outperforms s2. Both books warn that it is not the Relative Strength Index, Welles Wilder’s momentum oscillator of a single series. Katsanos smooths the ratio with a 3-bar exponential moving average (smooth = 3), and with a 150-bar one in his futures system, while authors in Stocks & Commodities read the raw ratio unsmoothed (smooth = 1), often on a log scale, which LogRatio gives directly. With smooth > 1 the average is XAverage over the ratios.

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, counted from the start of the overlap, is max(0, smooth-1), that is smooth-1 for smooth > 1 and 0 otherwise, and the minimum overlap is max(1, smooth) bars. Any smooth <= 1 (zero, negative and math.MinInt included) is valid and gives the raw ratio at every paired bar; a smooth greater than the overlap gives aligned zeros, a fresh all-zero series of len(s).

With m = min(len(s), len(s2)) and off = len(s) - m, pair j (0 <= j < m) is s[off+j] against s2[len(s2)-m+j], and its value is written at off+j. The valid-data class is prices on scales that stay away from zero: PriceRelative is invalid on Panama back-adjusted futures and on series loaded with a detrending option, where it returns numbers that are meaningless across a zero crossing; instruments quoted in different currencies must be put in one currency first; and total-return series are preferred for long equity windows. The raw ratio is pointwise and start-invariant; the smoothing is recursive, so the smoothed outputs depend on where the history starts and are not start-invariant.

A yield as the denominator is valid only while the yield is positive, and a CPI-deflated price is PriceRelative against a monthly index, which must first be sampled at each daily bar so that every daily price has its own divisor. The ratio is signed: a negative s2 flips its direction, so a rising s makes the line fall, and instruments that cross zero should use the difference-based measures (DeltaCorrelation, DeltaBeta and RegressionSpread) instead. A zero or non-finite s2, a non-finite s or a non-finite quotient gives NaN, and a zero s with a valid s2 gives 0. Smoothed, a NaN at pair p makes every output from max(p, smooth-1) on NaN, because the recursion never recovers.

Deviations from Katsanos’s MetaStock code: the average is seeded with the simple average of the first smooth ratios, where MetaStock’s Mov(…, E) seeds with the first value, so the two converge after a few times smooth bars; outputs before the first valid index are zero rather than undefined; and a zero denominator gives NaN.

Compositions that start from it are listed under “Compositions” in the “Relative: Ratio and Relative Strength” and “Relative: Channels” categories.

Verification: two independent references, a Python reference in exact decimal arithmetic and a Julia reference in BigFloat, which also translates the published listings, agree with it; and there are exact identities with existing methods, bit for bit from the first valid index on: an all-ones s2 gives s.XAverage(smooth), and equal-length valid inputs give s.Ratio(s2).XAverage(smooth).

RelativeEfficiency

func (s Series) RelativeEfficiency(s2 Series, n int) Series

RelativeEfficiency returns the signed efficiency ratio of the log ratio of s to s2 over n bars, its net change divided by the sum of its absolute one-bar changes.

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, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n + 1 bars.

It is Perry Kaufman’s efficiency ratio (Trading Systems and Methods) applied, signed, to the log ratio of the paired bars, L = ln(s) - ln(s2), which is LogRatio on the paired views a and b of s and s2 (NaN unless both values are finite and positive). At overlap index i >= n (result index off + i) the result is the net change L[i] - L[i-n] divided by the sum of the n absolute one-bar changes |L[j] - L[j-1]| for i-n < j <= i (kept as a rolling compensated sum), clamped to [-1, 1]; a sum of zero gives +0. It is unitless and lies in [-1, 1]: 1 means that, up to rounding, the log ratio never fell over the window (s never underperformed s2), -1 that it never rose, and a value near 0 a choppy relationship with little net relative move; a move that is tiny beside the net move, or rounding jitter, can still give ±1.

The identity |RelativeEfficiency| == L.EfficiencyRatio(n) holds bit for bit on every paired bar, where L = a.LogRatio(b): the magnitude is Kaufman’s unsigned ratio, and both are +0 before overlap index n. Swapping s and s2 negates L, and so the result on the paired bars, exactly, except that an exact zero stays +0; for positive finite s, s.RelativeEfficiency(s, n) is +0 everywhere.

NaN footprint: a non-positive or non-finite value at overlap index p of either input makes L[p] NaN, so the result is NaN at overlap indices j in [max(p, n), p+n], and later values recover.

If n < 1 or n >= min(len(s), len(s2)), so that the overlap is shorter than n + 1 bars, every element of the result is zero. RelativeEfficiency is not start-invariant in principle: the efficiency ratio’s rolling sum regroups its window in blocks counted from the first paired bar, so trimming or extending the start of the inputs can change the last bits of later values (no such case has been found for this method).

Warm-up hazard: before the first valid index the result is +0, which reads as “no net relative move”. A rule such as RelativeEfficiency > x for any x > 0 does not fire there, but RelativeEfficiency < x for any x > 0 does, and so do RelativeEfficiency > x for x < 0 and |RelativeEfficiency| < x, as do UpCapture < 100 and DownCapture < 100 on their own warm-ups (where UpCapture > 100 does not fire). To avoid this, trim with Window or mask with SetN. The same +0 appears after the warm-up wherever L does not move over the window. Series in exact proportion need not give it: the two logarithms are rounded separately, so L can jitter in its last bits and the result can then be anywhere in [-1, 1], ±1 included. No minimum movement is applied.

Verification: a Python reference in exact rational arithmetic with high-precision logarithms and an independent Julia reference agree with the method, and the identities above hold bit for bit: the EfficiencyRatio identity, the leg swap and the self zero.

RotationMomentum

func (s Series) RotationMomentum(s2 Series, n, nm int) Series

RotationMomentum returns the nm-bar change of RotationRatio as a ratio times 100.

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 len(s) - min(len(s), len(s2)) + n - 1 + nm and the minimum overlap is n + nm. For n < 2, nm < 1 or an overlap shorter than n + nm, the result is all zeros.

At a valid index the output is 100 times s.RotationRatio(s2, n) there, divided by its value nm bars earlier, so 100 means no change; a result that is not finite becomes NaN. A zero or non-finite value in s2, or a non-finite value in s, at overlap index p makes RotationRatio NaN at overlap indices max(p, n-1) to p+n-1, so the output is NaN on two windows: where that span holds the numerator and, nm bars later, where it holds the denominator. Later outputs recover. By Samuelson’s bound, RotationRatio lies within 100 ± sqrt(n-1) given an accurate z-score, so the denominator is at least 100 - sqrt(n-1): positive for n up to 10,000, though only 0.005 at n = 10,000 (a momentum near 2,000,000); from n = 10,001 on it can be zero (NaN), tiny (a huge value) or negative (a flipped sign).

Before the first valid index the output is 0, far below the 100 that marks no change, so a rule such as RM < 100 is true on every warm-up bar; trim the warm-up with Window or mask it with SetN.

Verification: the identity with RotationRatio holds bit for bit: from the first valid index on, the output is 100 times the signed ratio of s.RotationRatio(s2, n) at that index to its value nm bars earlier, or NaN where that is not finite. A Python reference and a Julia reference, each translated independently from this definition, agree with this method on reference data.

RotationRatio

func (s Series) RotationRatio(s2 Series, n int) Series

RotationRatio returns an open approximation of a relative-rotation ratio, 100 plus the n-bar z-score of the price ratio of s to s2.

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 len(s) - min(len(s), len(s2)) + n - 1 and the minimum overlap is n. For n < 2, or an overlap shorter than n, the result is all zeros.

On the overlap the ratio is that of s.PriceRelative(s2, 1), s divided by s2 bar by bar, and the output is its Standardise(n) z-score plus 100: the distance of the latest ratio from the mean of the trailing n ratios, in population standard deviations, so that 100 is the window mean and 101 one standard deviation above it. A flat window gives exactly 100. A zero or non-finite value in s2, a non-finite value in s or an overflowing quotient gives a NaN ratio, and the output is NaN on the windows that hold it and recovers after them; a negative s2 flips the ratio’s sign. Standardise carries a rolling variance, so the output is not bitwise start-invariant; the differences are at rounding level.

Julius de Kempenaer has not disclosed the construction of his relative rotation graphs, so the values will not match StockCharts or Bloomberg, and to stay clear of trademark use the method names avoid RRG and JdK. His interview in Stocks & Commodities 38:13 and Masonson’s use of the graphs with 15-week trails in Stocks & Commodities 44:4 give readings but no formulas. EhlersLoop, Ehlers’s published loop, is the per-leg alternative, and LoopRotation reduces it to one series.

Before the first valid index the output is 0, far below the 100 that marks no relative strength, so a rule such as RR < 100 is true on every warm-up bar; trim the warm-up with Window or mask it with SetN.

RotationMomentum is its momentum. With the ratio across and the momentum up, the pair (RotationRatio, RotationMomentum) places s in one quadrant of four around (100, 100): leading at the top right, weakening at the bottom right, lagging at the bottom left and improving at the top left.

Verification: the identity with Standardise holds bit for bit: with a and b the overlap views of s and s2, the output at each overlap index j from n-1 on is element j of a.PriceRelative(b, 1).Standardise(n) plus 100. A Python reference and a Julia reference, each translated independently from this definition, agree with this method on reference data.

RSMK

func (s Series) RSMK(s2 Series, n, smooth int) Series

RSMK returns Katsanos’s relative-strength indicator, the n-bar change in the log ratio of s to s2 smoothed by a smooth-bar exponential moving average and multiplied by 100.

Markos Katsanos defined it in Stocks & Commodities, March 2020, as EMA(log(C/SEC2) - log(Ref(C/SEC2, -RSBARS)), 3)*100, with C the close of the security, SEC2 the close of the comparison and RSBARS the n here: the n-bar momentum of the log relative strength, which is the n-bar log return of s less that of s2. The defaults are n = 90 and smooth = 3 on daily bars, for which Katsanos suggests n from 50 to 130; n = 15 and smooth = 2 in the December 2023 weekly VUG/VTV system; and n = 13 and smooth = 3 in the February 2026 weekly exploration column, which nothing ranks on.

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, counted from the start of the overlap, is n for smooth <= 1 and n+smooth-1 for smooth > 1, and the minimum overlap is n+1 and n+smooth bars respectively. Aligned zeros, a fresh all-zero series of len(s), are returned, tested in this order, for an empty overlap, for n < 1, for n at or above the overlap length m, and for smooth > 1 with smooth > m-n. The history budget follows: the published 90/3 needs a minimum overlap of 93 bars, plus about 15 bars of burn-in for the exponential average to forget its seed.

With m = min(len(s), len(s2)) and off = len(s) - m, pair j (0 <= j < m) is a_j = s[off+j] against b_j = s2[len(s2)-m+j], and its value is written at off+j. The log ratio L_j = ln(a_j) - ln(b_j) is computed first, then the change L_j - L_{j-n} for j >= n, then its exponential moving average over smooth bars, and last the factor 100, which never enters the average. One n is shared by both legs, so both must be on the same bar interval; legs on different bar intervals use the per-leg composition 100*(s.Log().Momentum(n1).Value() - s2.Log().Momentum(n2).Value()), never a daily series against a weekly one. The valid-data class is prices on a scale that stays positive: RSMK is invalid on Panama back-adjusted futures and on series loaded with a detrending option; instruments quoted in different currencies must be put in one currency first; and total-return series are preferred for long equity windows. The unsmoothed RSMK is the difference of two pointwise log ratios and is start-invariant; the smoothing is recursive, so the smoothed outputs are not start-invariant.

A NaN, infinite, zero or negative price at pair p gives NaN. Unsmoothed, it reaches the outputs at p and p+n where they are at or past n. Smoothed, it makes every output from p on NaN if p >= n, and every output from p+n on if p < n (none if p+n >= m), never before the first valid index. The signal line MARS is a composition, rs.Window(-f).Average(20), with rs the result and f its first valid index. The 2023 system buys when RSMK1 > RSMK2 (VUG and VTV each against the same benchmark) and RSMK1 is above its 20-bar average, gated by a volume-flow filter; RS > 0 AND RS > MARS is only the colour rule of its plot. Because the benchmark cancels, vug.RSMK(vtv, 15, 2) > 0 matches RSMK1 > RSMK2 except within about 1e-12 of a crossing. The log relative momentum, 100 times the n-bar log return of s less that of s2 (the descriptive name LogRelativeMomentum), is RSMK(s2, n, 1); the library has no separate method for it.

Deviations from Katsanos’s AmiBroker code: ln a - ln b rather than log(a/b); the average seeded with the simple average of the first smooth changes, as AmiBroker’s own EMA is; zeros rather than undefined values before the first valid index; and NaN for a non-positive price.

Compositions that start from it are listed under “Compositions” in the “Relative: Ratio and Relative Strength” and “Relative: Timeframes” categories.

Verification: two independent references, a Python reference in exact decimal arithmetic and a Julia reference in BigFloat, which also translates the published listings, agree with it; and there is an exact identity with an existing method: for an all-ones s2 and a finite, normal, positive s, s.RSMK(s2, n, 1) equals s.Log().Momentum(n).Mul(100) bit for bit, and with smooth > 1 and equal lengths, output j from n+smooth-1 on equals 100*s.Log().Momentum(n).Window(-n).XAverage(smooth)[j-n]. For any inputs, s.RSMK(s2, n, 1) is a.LogRatio(b).Momentum(n).Mul(100) placed at off, with a, b := s.Overlap(s2).

UpCapture

func (s Series) UpCapture(s2 Series, n int) Series

UpCapture returns the percentage of the one-bar gains of s2 captured by s over the trailing n bars in which s2 rose.

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, where off = len(s) - min(len(s), len(s2)) is the index of the first paired bar, and the minimum overlap is n + 1 bars.

Overlap index j is result index off + j. On the paired bars, r_a and r_b are the one-bar percent changes of s and s2, 0 on the first paired bar, and a bar qualifies when s2 rose on it. UpCapture is, over the qualifying bars among the last n, the arithmetic form of Morningstar’s capture ratio:

UpCapture = 100*Σ r_a/Σ r_b over the bars with r_b > 0

Each return is multiplied by a mask, 1 on a qualifying bar and 0 on any other (a plain product, so a NaN return stays NaN); the masked returns of s and of s2 are each summed over the last n bars with Sum, and the result is the first sum divided by the second, times 100. It is NaN when either sum is NaN and +0 when no bar of the window qualifies. It is in percent: 100 means the summed returns of s on those bars equal those of s2, a value above 100 that s gained more than s2 on them, one below 100 that it gained less, and a negative value that it fell on them overall.

Morningstar compounds the qualifying returns and takes the ratio of their geometric means (the compound form), so its published values differ from this arithmetic form. No minimum count of qualifying bars is applied: few or tiny rises of s2 in the window give extreme values, and a single tiny rise can give a very large ratio.

On positive finite inputs, unless a one-bar percent change overflows (|v/v0| above about 1.8e306; UpCapture then gives NaN where RateOfChange gives an infinity), UpCapture equals, bit for bit at overlap index j >= n, this composition of RateOfChange, GT, Mul, Sum and Div on the paired views a and b of s and s2:

rb := b.RateOfChange(1)
up := rb.GT(0)
a.RateOfChange(1).Mul(up).Sum(n).Div(rb.Mul(up).Sum(n)).Mul(100)

A negative base gives a signed return, (v/(-v0) + 1)*100 for a move from v0 to v, so a move from -2 to -1 counts as a rise of 50 percent where RateOfChange gives a fall of 50 percent; on such data the method and the composition differ. Negative and sign-crossing values give no NaN.

NaN footprint: a NaN or an infinity at overlap index p of either input makes the returns at p and p+1 NaN, so the result is NaN at overlap indices j in [max(p, n), p+n], and later values recover. A zero of either sign in s2 at p makes only the return at p+1 NaN, giving NaN on [max(p+1, n), p+n], and when the value before it is negative the bar at p qualifies with a return of +100 percent; a zero in s at p gives NaN on the same bars whatever the mask, since NaN*0 is NaN. A one-bar percent change that overflows at return index p gives NaN on [max(p, n), p+n-1]. Sums of qualifying returns that overflow (returns near 1e308) give NaN where both sums overflow, and otherwise ±0 or a tiny value. A bar on which s2 is unchanged qualifies for neither ratio (Morningstar Direct counts a zero benchmark return as an up period).

If n < 1 or n >= min(len(s), len(s2)), so that the overlap is shorter than n + 1 bars, every element of the result is zero. UpCapture is not start-invariant: Sum regroups its window in blocks counted from the first paired bar, so trimming or extending the start of the inputs can change the last bits of later values.

Warm-up hazard: before the first valid index the result is +0, which reads as “captured nothing” (a capture of 0). A rule such as UpCapture > 100 does not fire there, but UpCapture < 100 does, and so do DownCapture < 100 and RelativeEfficiency < x for any x > 0 on their own warm-ups. To avoid this, trim with Window or mask with SetN. The same +0 appears after the warm-up wherever no bar of the window qualifies: during a steady fall of s2, UpCapture is 0, so UpCapture < 100 fires throughout, and during a steady rise, DownCapture is 0, so DownCapture < 100 fires throughout.

Verification: a Python reference in exact rational arithmetic and an independent Julia reference agree with the method; on positive finite inputs UpCapture equals the composition above bit for bit, and s.UpCapture(s, n) is exactly 100 wherever the window has a bar in which s rose and the window’s sums are finite (0 where it has none; returns near 1e308 can overflow the sums and give NaN).

Compositions

Percentage premium

How far s stands above or below s2, in percent of s2, and the symmetric form in percent of the legs’ average, which is bounded to ±200 on non-negative prices and treats the legs alike.

a, b := s.Overlap(s2)
premium := a.Div(b).Sub(1).Mul(100)
symmetric := a.Sub(b).Div(a.Add(b)).Mul(200)
  • First valid index: 0. Minimum overlap: 1 bar.
  • Domain: these use the older Div, Sub and Mul, whose semantics they keep: a zero divisor gives 0, so a zero s2 gives a premium of -100 and a zero a + b a symmetric premium of 0, not NaN; and the sign is not the signed percent of the relative methods, so on a negative s2 the premium of s = 2*s2 is +100 (the signed percent would give -100). Use them on positive prices. The operands have equal lengths because they are the Overlap views.
  • Anchors: s = 2*s2 with s2 > 0 gives exactly 100 (E); s == s2 gives 0 for non-zero s2 (E; at a zero price the zero-divisor rule gives a premium of -100, while the symmetric form still gives 0); the symmetric form lies within ±200 on non-negative prices.
  • Pinned by TestRecipePercentPremium.

Relative performance and relative return

The ratio line’s change over n bars: as a return in percent, and as an index that starts at 100 n bars back. Knight-Ridder TRADECENTER’s Relative Ratio, which John J. Murphy and David J. Hirschfeld use to rank commodities against the CRB Index (Stocks & Commodities 6:5, 1988), rebases the ratio line to 100 at a chosen start; its reading 100 days after that start, on which they rank, is the index form at n = 100. Fixing the base at one bar in this way, as that line and the sidebar to Robert L. Hand Jr.’s “Calculating Relative Strength Of Stocks” (Stocks & Commodities 10:5, 1992) do, makes the line a positive multiple of the ratio line (less a constant for the return form), for a positive ratio at the base bar: the signs of its slopes, its trendline breaks and its crossings of its own moving averages do not depend on the base bar; its level, the size of its slopes and its zero line do. Phil Doyle’s relative performance charts (Stocks & Commodities 19:5, 2001) instead rebase each instrument at the first bar (his Figure 4 is this recipe against an all-ones s2); his baseline reading, Figure 9, is the difference of the rebased lines, the relative momentum below, and it is not a function of the ratio line.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
ret := r.RateOfChange(n).Window(-n)
index := r.Div(r.Shift(n)).Mul(100).Window(-n)
  • First valid index: n, counted from the start of the overlap. Minimum overlap: n + 1 bars.
  • Domain: the ratio line’s, then the older RateOfChange and Div: a zero ratio n bars back gives 0 rather than NaN.
  • Printed values: Doyle’s Figure 4 rebases the Nasdaq Composite at its first bar (s2 all ones, n the distance from that bar), and the editor’s sidebar to Hand’s article anchors Pepsico against the S&P 500 at its first row (the return divided by 100). The return reproduces Doyle’s six values after the anchor and Hand’s six values at their printed precision; the anchor’s 0.00 is the untrimmed chain’s, because Window(0) is empty (testdata/ref/inputs/tasc-doyle-19-5.csv and tasc-hand-10-5.csv; checked by TestRecipeDoylePrinted and TestRecipeHandPrinted).
  • Anchors: on equal-length valid inputs the return equals a.Ratio(b).RateOfChange(n) (E); the index equals the return plus 100 to T(1e-13·max(1, |index|)) away from zero bases.
  • Pinned by TestRecipeRelativeReturn.

Relative momentum

The difference of the legs’ n-bar returns in percent, a return spread that reads 0 when the legs move alike. Doyle’s Figure 9 (“Net XOI RPC”, the AMEX Oil Index against the S&P 500 from a common base bar) is this difference, and the recipe reproduces its six printed values. The log form, 100 times the difference of the legs’ n-bar log returns, is RSMK(s2, n, 1).

a, b := s.Overlap(s2)
mom := a.RateOfChange(n).Sub(b.RateOfChange(n)).Window(-n)
  • First valid index: n, counted from the start of the overlap. Minimum overlap: n + 1 bars.
  • Domain: the older RateOfChange’s: a zero base gives 0, and the sign is not the signed percent’s on a negative base.
  • Anchors: a constant non-zero s2 gives s.RateOfChange(n) (E); s2 == s and s = 2*s2 give 0 (E); swapping the legs negates it (E); on positive prices (index - 100)*(1 + b.RateOfChange(n)/100) equals it to T(1e-13·max(1, |index|, 100·max(a_j/a_{j-n}, b_j/b_{j-n}))), a tolerance that grows with the legs’ growth over the n bars.
  • Pinned by TestRecipeRelativeMomentum; runnable example ExampleSeries_RateOfChange_relativeMomentum.

Katsanos’s relative rate of change

Katsanos’s form (Intermarket Trading Strategies, 2008) takes the intermarket’s change over k1 bars less the base’s over k2, and smooths the difference with an m-bar average. Note the order: k1 belongs to the comparison s2 and k2 to the base s, the reverse of the divergence methods, whose n1 belongs to s and n2 to s2; the sign is theirs, comparison less base.

a, b := s.Overlap(s2)
d := b.RateOfChange(k1).Sub(a.RateOfChange(k2)).Window(-max(k1, k2))
rroc := d.Average(m).Window(-(m - 1)) // leave out the trim when m == 1
  • First valid index: max(k1, k2) + m - 1, counted from the start of the overlap. Minimum overlap: max(k1, k2) + m bars.
  • Domain: the older RateOfChange’s.
  • Anchors: with k1 == k2 == k, s2 == s gives 0 (E), and it equals the negated relative momentum averaged over m bars (E).
  • Pinned by TestRecipeKatsanosRelativeROC.

AccuTrack

AccuTrack, FastTrack’s switching indicator as Gary J. Harloff defines it (Stocks & Commodities 16:1, 1998): each fund’s daily percent change smoothed by a 48-bar exponential average, the reference’s subtracted, and the difference smoothed by a 12-bar one; by linearity the recipe subtracts first and smooths the difference twice, which agrees to rounding (the last anchor). Positive favours the first fund. Norman J. Brown (Stocks & Commodities 21:6, 2003) runs it at 1 and 1, the raw difference, against a slightly negative threshold.

a, b := s.Overlap(s2)
d := a.RateOfChange(1).Sub(b.RateOfChange(1)).Window(-1)
e := d.XAverage(n1).Window(-(n1 - 1)) // leave out the trim when n1 == 1
accutrack := e.XAverage(n2).Window(-(n2 - 1)) // leave out the trim when n2 == 1
  • First valid index: n1 + n2 - 1, counted from the start of the overlap. Minimum overlap: n1 + n2 bars.
  • Domain: the older RateOfChange’s. A NaN price, or a one-bar change that is not finite (an overflowing quotient, for example), makes that bar’s difference non-finite; the exponential averages never recover from it (XAverage(1) computes d*1 + prev*0, and NaN0 and Inf0 are NaN), so every later reading is NaN, while the relative momentum recovers. The averages seed with the mean of their first inputs, where FastTrack’s start-up is not published.
  • Anchors: at (1, 1), while every one-bar change of both legs is finite, it equals the relative momentum at n = 1 (E); s2 == s gives 0 (E); it equals the difference of the legs’ own doubly smoothed changes to T(1e-12·max(1, |ea| + |eb|)).
  • Pinned by TestRecipeAccuTrack.

Pring’s Money Flow Indicator

Martin Pring’s Money Flow Indicator (Stocks & Commodities 15:5, 1997) on monthly bars: the six-month average of the nine-month rate of change of the S&P 500 divided by the three-month commercial-paper yield, less the same average for the S&P 500 itself. Pring’s article is inconsistent on the sign: his theory, his “crosses above zero” reading and the captions of Figures 4 and 5 take money flow less the S&P 500, while his subtraction sentence and the caption of Figure 2 take the reverse; this follows the theory and Figures 4 and 5. With s that ratio and s2 the S&P 500, the log form of the difference is minus the log change of the yield: it has no S&P 500 content.

a, b := s.Overlap(s2)
ma := a.RateOfChange(n).Window(-n).Average(m)
mb := b.RateOfChange(n).Window(-n).Average(m)
mf := ma.Sub(mb).Window(-(m - 1)) // leave out the trim when m == 1
  • First valid index: n + m - 1 (14 for Pring’s 9 and 6), counted from the start of the overlap. Minimum overlap: n + m bars.
  • Domain: the older RateOfChange’s.
  • Anchors: s2 == s gives 0 (E).
  • Pinned by TestRecipePringMoneyFlow.

Excess over a universe

A stock’s return less that of an equal-weight buy-and-hold index of its peers (the form RELATIVE-INDICATORS-SPEC.md defines): each member rebased at the first common bar, averaged with SAverage, and the relative momentum taken against that index. Because of the rebase, the members’ weights drift from the first common bar, so the value depends on where the common history starts; adding a member with a shorter history moves every value. Anatoly B. Schmidt’s equal-weight benchmark portfolios (Stocks & Commodities 40:2, 2022) are baskets formed with equal weights at each date, whose return over a window is the average of the members’ returns, a different quantity: a.RateOfChange(n) less the mean of the members’ RateOfChange(n). Against a sector fund, as in Rob Friesen’s rankings against the sector ETF (39:5, 2021), use the relative momentum directly.

m := min(len(s), len(u1), len(u2))
a, y1, y2 := s.Right(m), u1.Right(m), u2.Right(m)
index := y1.Div(y1[0]).SAverage(y2.Div(y2[0]))
excess := a.RateOfChange(n).Sub(index.RateOfChange(n)).Window(-n)
  • First valid index: n, counted from the start of the common bars. Minimum common length: n + 1 bars (the rebase reads the first common bar, so an empty common length panics). Add members to SAverage as further arguments; all must be trimmed to the common length. SAverage right-aligns members of different lengths at the most recent bar, but the rebase y.Div(y[0]) reads each member’s own first bar, so untrimmed members would be rebased at different bars.
  • Domain: positive prices; a member whose first common value is 0 is rebased to zeros.
  • Anchors: two members both equal to s give exactly 0 when the rebasing is exact (a first value that is a power of two), because (x + x)/2 == x (E); with three or more members, or an inexact rebase, they give 0 to rounding, since SAverage’s running sum is not exact. This is the return of the average price, as the specification defines it; Schmidt’s average of the members’ returns (a rebalanced index) is a different quantity.
  • Pinned by TestRecipeExcessOverUniverse.

Two-asset switching and yield comparisons

Robert Tang’s gold momentum strategy (Stocks & Commodities 37:13, 2019) holds, for the next year, whichever of the S&P 500 and gold returned more over the last: the sign of the relative momentum over a year of bars, sampled at the year’s end by the consumer. Massoud Metghalchi’s variations (38:6, 2020) apply the same switch every six months over half a year of bars (his best result), and against an emerging-market ETF (EEM) in place of gold; his first-five-days and first-month variants switch on the S&P 500’s own return, not on relative momentum, and are calendar rules for the consumer. Tushar Chande’s comparison of stock and bond yields (10:10, 1992) compares annual percentage changes of two yields; a yield near zero makes a percentage change unbounded, so compare yields by their differences, or with DeltaCorrelation, as they approach zero.

  • First valid index: n, counted from the start of the overlap, with n a year (or half a year) of bars. Minimum overlap: n + 1 bars.
  • Domain and anchors: those of the relative momentum above, whose sign the switch reads.
  • Pinned by TestRecipeRelativeMomentum.

Mansfield relative strength

The ratio line’s percentage above or below its own n-bar simple average: Stan Weinstein’s Mansfield relative strength (Secrets for Profiting in Bull and Bear Markets, 1988), on weekly bars with n = 52, the form TrendSpider publishes. In his 2021 Stocks & Commodities interview (39:12) Weinstein confirms that he works from weekly charts, without disputing the interviewer’s description of the book’s Mansfield charts, and ranks relative strength lower than he did.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
rs := r.Div(r.Average(n)).Sub(1).Mul(100).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of the overlap; element e is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2; at n = 1 the trim is Window(0), which returns an empty series.
  • Order: the library’s percent leg, (r/ma - 1)*100, which is RateOfChange’s order; Markos Katsanos writes (r - ma)/ma*100, equal in exact arithmetic and within 1e-13*max(1, |r/ma|) in binary64.
  • Domain: the ratio line’s (NaN for a zero or non-finite s2, a non-finite s or an overflowing quotient, kept by the average for the n bars whose windows hold it); Div gives 0 where the average is exactly zero, so the reading is then -100. On a negative ratio line the average is negative and the reading is inverted.
  • Anchors: with s2 all ones on a positive s it is the price’s own disparity from its average, s.Div(s.Average(n)).Sub(1).Mul(100), trimmed (E); s2 == s gives exactly 0 (E).
  • Pinned by TestRecipeT3RatioRecipes; runnable example ExampleSeries_PriceRelative_mansfieldRS.

Katsanos’s RS-MA% (the exponential variant)

The same reading against an exponential average. Katsanos’s “RS-MA%” column (his chapter 16 MetaStock exploration, Intermarket Trading Strategies, 2008, Appendix A.7, page 343) applies it, with a 120-bar average, to his smoothed relative strength, a 150-bar exponential average of the ratio; on the raw ratio, as here, it is the unsmoothed form of his column. His column itself is rs := a.PriceRelative(b, 150).Window(-149) followed by rs.Div(rs.XAverage(120)).Sub(1).Mul(100).Window(-119), in the library’s order, and agrees with his MetaStock values, within rounding, once both averages have forgotten their seeds.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
rsma := r.Div(r.XAverage(n)).Sub(1).Mul(100).Window(-(n - 1))
  • First valid index: n - 1; element e is overlap bar n - 1 + e. Minimum overlap: n bars, plus four to five times n of burn-in before the average forgets its seed (the average seeds on its first n inputs).
  • Parameters: n >= 2; at n = 1 the trim is Window(0), which returns an empty series.
  • Order: the library’s percent leg; Katsanos’s MetaStock formula is (RS - Mov(RS,120,E))/Mov(RS,120,E)*100, seeded as MetaStock seeds its average, so a literal translation agrees with this one only after the seeds have decayed.
  • Pinned by TestRecipeT3RatioRecipes.

Z-score of the log ratio

The log ratio’s position in its own window, in standard deviations: Ernest Chan’s pair rules read the z-score of a ratio (Algorithmic Trading, 2013, chapter 3), and the log form treats the legs symmetrically.

a, b := s.Overlap(s2)
L := a.LogRatio(b)
z := L.Standardise(n).Window(-(n - 1))
  • First valid index: n - 1; element e is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2; at n = 1 the trim is Window(0), which returns an empty series.
  • Domain: a price that is not finite and positive gives NaN on the n windows containing it; never map it to 0, which means “at the mean” and would trigger exits. A window of equal log ratios gives exactly 0. A constant price ratio with moving legs need not give equal log ratios: ln(s) - ln(s2) rounds differently from bar to bar, and Standardise reports that noise at full scale (|z| above 2 on s = 2*s2 over 20 bars of a random walk). Test the ratio for constancy first where fixed-ratio pairs can occur.
  • Convention: the population deviation; Chan’s MATLAB and R listings (Quantitative Trading, 2nd edition, 2021, Example 3.6) use the sample deviation, so their z is this one times sqrt((n-1)/n); his Python listing uses the population deviation, as here.
  • Anchors: antisymmetric under a leg swap (E).
  • Pinned by TestRecipeT3RatioRecipes.

Z-score of the raw ratio

The same on the price ratio itself: Di Prima and Baruffa’s credit vote is the 100-bar z-score of HYG against IEF, with credit “favourable” while it is above -2 (Stocks & Commodities 44:5; reconstructed from their prose, since no code is printed).

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
z := r.Standardise(n).Window(-(n - 1))
  • First valid index: n - 1; element e is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2; at n = 1 the trim is Window(0), which returns an empty series.
  • Symmetry: unlike the log form, it is not antisymmetric under a leg swap (the ratio and its inverse have different spreads), so prefer the log form unless the published values must be reproduced.
  • Other readings: Andrew Sachais holds cash while the inverted IEF/JNK line’s 10-week average is above its 40-week one (34:7), the EMA regime of a relationship line, below; Chande’s zones at the sample mean ±0.5, 1 and 2 standard deviations of the dividend-yield to bond-yield ratio (10:10) and Lincoln’s ratio less its “historical mean” (9:9) are full-sample statistics, hence look-ahead; this recipe is their causal form.
  • Anchors: with s2 all ones it is s.Standardise(n), trimmed (E).
  • Pinned by TestRecipeT3RatioRecipes.

Relative Strength Oscillator (Martin and McCann)

The percentage by which the fast exponential average of the ratio line stands above or below its slow one: Martin and McCann’s 1989 sector-timing oscillator, 100 × [(Fast MA/Slow MA) − 1], as Michael Parzen restates it (“How Does A 1989 Sector Timing Model Hold Up 35 Years Later?”, Stocks & Commodities 44:7, 2026). It is a percentage price oscillator of the price relative, although Parzen’s prose calls it a difference.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
oscillator := r.XAverage(fast).Div(r.XAverage(slow)).Sub(1).Mul(100).Window(-(slow - 1))
  • First valid index: slow - 1, counted from the start of the overlap; element e is overlap bar slow - 1 + e. Minimum overlap: slow bars, plus four to five times slow of burn-in before the averages forget their seed (after three times slow the seed still weighs about 0.25%).
  • Parameters: 1 <= fast < slow; fast > slow inverts the reading and fast == slow gives 0 at every bar; both are consumer errors. The published smoothing constants are XAverage lengths exactly, because 2/(1+float64(n)) is the same double as the decimal constant: Martin and McCann’s 0.4 and 0.1 are fast = 4 and slow = 19; Parzen’s retune to 0.20 and 0.05 (weekly, about 3- and 13-week half-lives) is fast = 9 and slow = 39. His “tuned alphas” 0.15 and 0.03 have no integer length and cannot be reproduced.
  • Domain: the ratio line’s: a zero or non-finite price of s2, a non-finite s, or a quotient that overflows gives NaN, which both averages keep from that bar on. On a positive ratio line the averages are positive and the division is safe; a negative ratio line is signed (a negative leg), and its oscillator reads inverted. Where the slow average is exactly zero, Div gives 0 and the oscillator reads -100.
  • Anchors: with s2 all ones on a finite positive s it is s.XAverage(fast).Div(s.XAverage(slow)).Sub(1).Mul(100), trimmed (E); s2 == s gives exactly 0, because the average of a constant 1 stays 1 (E); scaling either leg by a power of two leaves it unchanged (E); on a positive ratio line its sign is exactly the EMA regime’s below, since the quotient of two distinct doubles never rounds to 1; on a negative ratio line it is the opposite (s = {-1, -2} against s2 = {1, 1} with lengths 1 and 2 gives an oscillator of +33.3 and a regime of -1).
  • Trading use: each week, Parzen holds the ETF (or the top three) with the highest oscillator above a minimum threshold, otherwise a T-bill proxy; he standardises the oscillators across the funds first and trades on the previous week’s values, and excludes funds with fewer than 104 weeks of history. His fix for the 1989 system’s normalisation of each fund’s ratio to 1.0 at its start is the raw ratio at each bar, which is why this recipe has no fixed start.
  • Pinned by TestRecipeT3TASCRelativeStrengthOscillator; runnable example ExampleSeries_PriceRelative_relativeStrengthOscillator.

EMA regime of a relationship line

The state of the ratio line’s fast exponential average against its slow one: +1 above, -1 below, 0 equal. nseries has no Sign, so the regime is GT(0) less LT(0) of the MACD of the ratio line.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
macd := r.MACD(fast, slow)
regime := macd.GT(0).Sub(macd.LT(0)).Window(-(slow - 1))
  • First valid index: slow - 1; element e is overlap bar slow - 1 + e. Minimum overlap: slow bars; allow four to five times slow before trusting a weekly regime, because the averages seed on their first slow inputs. A regime read during warm-up is meaningless, which is why the stage is trimmed.
  • Domain: as the oscillator’s. Where the MACD is NaN, both comparisons are false, so the regime is 0, not NaN. A negative ratio line reads inverted. fast > slow inverts the regime and fast == slow gives 0 at every bar; both are consumer errors.
  • Anchors: values in {-1, 0, +1}; s2 == s gives 0 (E); scaling either leg by a power of two leaves it unchanged (E); its sign is invariant under any positive rescaling of r in exact arithmetic.
  • Trading use: three authors read ratio regimes on weekly or monthly bars: Andrew Sachais, Domenico D’Errico (below) and Leslie N. Masonson. Sachais reads a TIP/IEI line with weekly EMAs of 9 and 36 for inflation expectations (Stocks & Commodities 34:10, where the legend’s EMA 9 of 0.9119 against EMA 36 of 0.9053 is the documented check), and an IEF/JNK line with 10 and 40 for credit, holding cash while the fast average is above the slow (34:7). Masonson’s monthly SPY:RSP (and IBB:XBI) ratio line against its 5-month exponential average (44:4) is a price against an average, r.GT(r.XAverage(5)) with the same trim, rather than a fast-against-slow crossing; he confirms it with RSI(7) and the MACD of the ratio and trades when at least two of the three agree. His text gives the MACD as (6,13,5) and his figure 11 caption as (6,13,9).
  • Pinned by TestRecipeT3TASCRegimesAndMACD.

SMA regime of a relationship line

The same state from simple averages. Domenico D’Errico reads a stock against SPY on weekly averages of 4 and 40, of unstated type (Stocks & Commodities 34:13); this is their simple form.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
diff := r.Average(fast).Sub(r.Average(slow))
regime := diff.GT(0).Sub(diff.LT(0)).Window(-(slow - 1))
  • First valid index: slow - 1; element e is overlap bar slow - 1 + e. Minimum overlap: slow bars.
  • Domain: a NaN ratio makes the difference NaN, and the regime 0, for the slow bars whose windows contain it; the regime then recovers, unlike the exponential forms.
  • Anchors: values in {-1, 0, +1}; s2 == s gives 0 (E); scaling either leg by a power of two leaves it unchanged (E).
  • Pinned by TestRecipeT3TASCRegimesAndMACD.

MACD of the log ratio

The scale-free MACD of the relationship line: the MACD of ln(s) - ln(s2), which a change of either leg’s (positive) price scale only shifts. Its sign can differ from the EMA regime’s near a crossing, in a band about as wide as the ratio’s variance over the averaging windows (the gap between geometric and arithmetic means): on random walks with 2% volatility per bar, about 1% of bars disagree.

a, b := s.Overlap(s2)
line := a.LogRatio(b)
macd := line.MACD(fast, slow).Window(-(slow - 1))
  • First valid index: slow - 1; element e is overlap bar slow - 1 + e. Minimum overlap: slow bars.
  • Domain: the log ratio needs finite prices > 0; any other price gives NaN, kept by both averages from that bar on.
  • Pinned by TestRecipeT3TASCRegimesAndMACD.

Ratio MACD and its signal line (Raff)

Gilbert Raff’s MACD of the fund-against-index ratio, with its 9-bar signal line (Stocks & Commodities 12:11): he buys a fund while the ratio’s MACD is above its signal, and a stock only when the MACD is also above zero, and he counts the share of a universe on buy signals as an allocation (a breadth count over these signals). His lengths are apparently 12 and 26 on weekly bars; the sidebar that gives them is not in the archive.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
macd := r.MACD(fast, slow).Window(-(slow - 1))
sig := macd.XAverage(9).Window(-8)
  • First valid index: slow - 1 for the MACD and slow + 7 for the signal; element e of each is overlap bar slow - 1 + e or slow + 7 + e. Minimum overlap: slow + 8 bars for the signal.
  • Units: the MACD is in ratio units, so compare or threshold it across pairs only in the PPO form below or on the log ratio.
  • Anchors: with s2 all ones it is s.MACD(fast, slow), trimmed (E); scaling s by 2^k scales the MACD and its signal by exactly 2^k (E).
  • Pinned by TestRecipeT3TASCRegimesAndMACD.

PPO of the ratio

The percentage price oscillator of the ratio line in difference form, (EF - ES)/ES*100. It is the oscillator’s quantity, which uses the ratio form (EF/ES - 1)*100; the two agree in exact arithmetic and within 1e-13*max(1, |EF/ES|) in binary64, except where the slow average is exactly zero (here 0, there -100).

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
es := r.XAverage(slow)
ppo := r.XAverage(fast).Sub(es).Div(es).Mul(100).Window(-(slow - 1))
  • First valid index: slow - 1; element e is overlap bar slow - 1 + e. Minimum overlap: slow bars.
  • Anchors: scaling either leg by a power of two leaves it unchanged (E); against the oscillator within 1e-13*max(1, |EF/ES|) absolute.
  • Pinned by TestRecipeT3TASCRegimesAndMACD.

Per-leg PPO difference

Each leg’s own PPO on its own history, read at “now”, and their difference. It is this library’s reduction of Vitali Apirine’s side-by-side reading, not a published formula: Apirine plots the W&D PPO(60,130,1,1), which is the weekly PPO alone (his prose prints PPO(130,60,1,1)), of the Nasdaq 100 and the S&P 500 in separate panes beside their price relative and compares their peaks (4.83 and 3.25 in April 2012; Stocks & Commodities 36:2, figure 4). Being per leg, it tolerates legs on different calendars or bar intervals, each with its own lengths.

ea := s.XAverage(slow)
eb := s2.XAverage(slow)
pa := s.XAverage(fast).Sub(ea).Div(ea).Mul(100).Window(-(slow - 1))
pb := s2.XAverage(fast).Sub(eb).Div(eb).Mul(100).Window(-(slow - 1))
diff := pa.Value() - pb.Value()
  • History: each leg needs slow bars of its own; a shorter leg’s PPO is empty and Value reads 0.
  • Pinned by TestRecipeT3TASCPerLeg.

Price Momentum Oscillator (DecisionPoint)

DecisionPoint’s PMO of one series: the one-bar rate of change smoothed by a “custom” exponential average with multiplier 2/n1, times 10, smoothed again with multiplier 2/n2, and a true 10-bar exponential signal line, as Vitali Apirine prints the definition (Stocks & Commodities 38:9). The custom multiplier 2/n is exactly XAverage(n - 1)’s factor, because 1 + float64(n-1) == float64(n) for every integer below 2^53; the factor 10 comes before the second average, in DecisionPoint’s order.

q := x.RateOfChange(1).Window(-1)
e1 := q.XAverage(n1 - 1).Mul(10).Window(-(n1 - 2))
p := e1.XAverage(n2 - 1).Window(-(n2 - 2))
sig := p.XAverage(10).Window(-9)
  • First valid index: n1 + n2 - 3 for the PMO (52 at DecisionPoint’s 35 and 20) and n1 + n2 + 6 for the signal, counted from the start of the series; element e of each is bar n1 + n2 - 3 + e or n1 + n2 + 6 + e. Minimum length: n1 + n2 - 2 bars for the PMO and n1 + n2 + 7 for the signal.
  • Parameters: n1 >= 3, n2 >= 3; DecisionPoint uses 35, 20 and a 10-bar signal.
  • Domain: the legacy RateOfChange: a zero previous price gives 0, and the sign follows the signed base, which differs from the signed percent of the relative methods on a negative base. A NaN price gives NaN, and an infinite price an infinite PMO, which both averages keep from that bar on; a zero price itself gives a rate of change of -100.
  • Published versions: Apirine’s MetaStock code seeds each average with a simple average but switches to its PREV recursion before that average exists (one bar early in the first stage, before any input in the second), so a literal translation agrees only after convergence. The Traders’ Tips versions differ: the EasyLanguage version starts both recursions from 0; Zorro and NeuroShell use average lengths 34 and 19, which is this composition up to the position of the factor 10; TradersStudio writes XAverage(10*XAverage(RC1, len2-1), len1-1).
  • Pinned by TestRecipeT3TASCPMO.

PMO spread (Apirine’s CPMO)

The two legs’ PMOs read at “now” and their difference: Vitali Apirine’s Compare Price Momentum Oscillator plots the two PMOs on one chart (Stocks & Commodities 38:9). A bullish crossover is one PMO turning up through the other; centreline and signal-line crossovers count too. Richard Denning’s TradersStudio system in the Traders’ Tips goes long the traded index fund (QQQ or IWM) while its PMO, with lengths 20 and 40, exceeds SPY’s and both closes are above their 300-bar simple averages. In part 2 (38:10), bearish centreline crossovers of the PMOs of SPX, NDX and DJI marked the 2000 and 2008 bear markets, and NDX’s PMO crossing above SPX’s marked their ends; in part 3 (38:11), the PMO of an index against that of its own volatility index gives an opposing-swing divergence.

pa := s.RateOfChange(1).Window(-1).XAverage(n1 - 1).Mul(10).Window(-(n1 - 2)).XAverage(n2 - 1).Window(-(n2 - 2))
pb := s2.RateOfChange(1).Window(-1).XAverage(n1 - 1).Mul(10).Window(-(n1 - 2)).XAverage(n2 - 1).Window(-(n2 - 2))
spread := pa.Value() - pb.Value()
  • History: each leg needs n1 + n2 - 2 bars of its own; it is per leg, so legs on different bar intervals are allowed, each with its own lengths.
  • Anchors: s2 == s gives exactly 0 (E).
  • Pinned by TestRecipeT3TASCPMO.

Relative hit rate

The share of the last n bars on which s outperformed s2, less the share on which it underperformed, each bar’s performance being its k-bar rate of change: a windowed count of signs, in steps of 1/n from -1 to 1. It is derived from Parent’s ranked relative strength (Stocks & Commodities 22:5), which ranks each day’s one-day changes across about 1,500 stocks, scores the ranks on a tanh curve, averages the scores by sector and adds each sector’s daily excess over an equal share to a running total. With two members and without the curve, each bar’s score is the sign of the difference of the two changes, and a windowed sum takes the place of the running total.

a, b := s.Overlap(s2)
ra := a.RateOfChange(k)
rb := b.RateOfChange(k)
rate := ra.GT(rb).Sub(ra.LT(rb)).Window(-k).Sum(n).Div(float64(n)).Window(len(a) - (n + k - 1))
  • First valid index: n + k - 1, counted from the start of the overlap; element e is overlap bar n + k - 1 + e. Minimum overlap: n + k bars.
  • Parameters: n >= 1 and k >= 1; Parent’s changes are one-day ones, k = 1.
  • Values: multiples of 1/n in [-1, 1]: +1 where s outperformed on every bar of the window and -1 where it underperformed on every one; a tie counts 0.
  • Reading: a persistence gauge beside the relative momentum (“Relative momentum”) and the capture ratios (UpCapture and DownCapture): leadership held on most bars reads near ±1, while leadership built on a few large bars shows a large relative momentum with a hit rate near 0.
  • Domain: positive prices. RateOfChange gives 0, not NaN, at a zero base, and GT and LT read a comparison with NaN as false, so a NaN return counts 0, as a tie, where a method with a strict contract would give NaN for the windows that hold it.
  • Not offered: the running count, a “relative advance line”, is not offered: its level depends on the bar it starts from, which moves when the series is a sliding window of recent bars. Parent’s running totals share that property.
  • Calendar version: Masonson’s “batting average” (Stocks & Commodities 44:4), the share of calendar years in which one fund of a pair beat the other, needs timestamps, which a Series does not carry.
  • Anchors: on finite inputs s2 == s gives exactly 0 (E); swapping the legs negates it exactly (E).
  • Pinned by TestRecipeT4ATASCHitRate.

Normalised difference and share

Two bounded forms of a pair of non-negative counts a and b: the normalised difference (a - b)/(a + b), from -1 to 1, and the share a/(a + b), from 0 to 1. Breadth uses them on advancing and declining issues, new highs and new lows, and bullish and bearish advisers; for advances and declines McClellan divides by the total because the number of issues traded has grown (Stocks & Commodities 22:12). His A/D ratio is the normalised difference times 1,000; Meyers’s advance/decline ratio (ai-di)/(ai+di) is the normalised difference itself (15:1); Hayes smooths the share of new highs in new highs plus new lows with a 55-day exponential average (11:12); and Flanagan’s advisory sentiment, from Investors Intelligence data, is the bullish percentage over the sum of the bullish and bearish percentages (12:11), which he reads contrarily.

a, b := s.Overlap(s2)
total := a.Add(b)
nd := a.Sub(b).Div(total)
share := a.Div(total)
  • First valid index: 0 for both, counted from the start of the overlap; element e is overlap bar e. Minimum overlap: 1 bar.
  • Domain: non-negative counts, which can be zero. On them the difference lies in [-1, 1] exactly, because a - b <= a + b holds before rounding and rounding is monotone. Div gives 0 where the total is 0, so both forms read 0 when both counts are zero (the share does not read 1/2 there); a negative input gives a meaningless value, which can fall outside [-1, 1], and a NaN or infinite input gives NaN (the share gives 0 where only b is infinite).
  • Published scales: the difference times 1,000 is McClellan’s ratio; the share smoothed by XAverage(55), trimmed by 54, is Hayes’s line (times 100 for his chart’s percent scale).
  • Anchors: on finite inputs s2 == s gives a difference of exactly 0 (E), and a share of exactly 1/2 where s is non-zero and 2s is finite (E); swapping the legs negates the difference exactly (E). The difference times 200 is the symmetric form of “Percentage premium” in this category, a.Sub(b).Div(a.Add(b)).Mul(200) (E). On positive inputs the difference equals tanh(L/2), with L the log ratio a.LogRatio(b), in exact arithmetic (in floating point to about 1e-14 absolute, but not in relative terms near a = b, where the log ratio cancels), so it is a bounded, increasing transform of the log ratio. The share equals (1 + d)/2, with d the difference, only in exact arithmetic, because 1 + d cancels for small shares.
  • Pinned by TestRecipeT4ATASCNormalisedDifference.

Cumulative line against its own average (McEwan)

McClellan’s cumulative A/D ratio is the running sum of the daily normalised difference times 1,000 (Stocks & Commodities 22:12), and McEwan’s low-frequency rule holds the Dow Jones Industrial Average while that cumulative line is above its simple one-year moving average (31:5). A running sum depends on the bar it starts from, which moves when the series is a sliding window of recent bars; its distance from its own n-bar simple average does not. With C the running sum of the normalised difference d, C at bar i less the mean of C over bars i - n + 1 to i is 1/n times the sum, over l = 1 to n - 1, of the sum of the last l values of d. Those sums weight the last n - 1 increments only, n - 1 times the newest down to once the oldest, so the gap is (n - 1)/2 times d.WAverage(n - 1).

a, b := s.Overlap(s2)
nd := a.Sub(b).Div(a.Add(b))
gap := nd.WAverage(n - 1).Mul(float64(n-1) / 2).Window(len(a) - (n - 2))
  • First valid index: n - 2, counted from the start of the overlap; element e is overlap bar n - 2 + e. Minimum overlap: n - 1 bars.
  • Parameters: n >= 2; at n = 2 the gap is half the latest normalised difference.
  • Reading: the sign is McEwan’s rule: positive while the cumulative line is above its n-bar average. He gives the length only as one year (about 252 daily bars). His factor 1,000 scales the gap and leaves its sign unchanged.
  • Domain: the normalised difference’s: non-negative counts, with 0 where both are zero (Div).
  • Anchors: on finite inputs s2 == s gives exactly 0 (E). It agrees with a running sum of the normalised difference less that sum’s own n-bar average, the form that drifts with the start, within 1e-12·(1 + |C|), with C the running sum at that bar; the two differ only in rounding, which grows with the running sum’s level.
  • Pinned by TestRecipeT4ATASCCumulativeGap.

Hurst exponent of the log ratio

The rescaled-range (R/S) Hurst exponent of the log ratio L, a.LogRatio(b), over its last n bars, twice: on the level of L, and on its one-bar changes, the log return of s less that of s2. Hurst’s exponent comes from his study of reservoir storage on the Nile (1951); Peters (Fractal Market Analysis) and, in Stocks & Commodities, Panini (25:2, “From Nile To NYSE”) apply R/S to logarithmic returns, and Hurst’s own documentation advises increments rather than levels. On the changes, a value above 0.5 suggests that relative strength persists and one below 0.5 that it reverts, the case a pairs trader wants. On the level the estimate reads high for any wandering ratio: on simulated random-walk pairs at n = 50 the level form averaged about 0.70 and the changes form about 0.46, the latter below 0.5 because Hurst is biased low at small n (HurstCorrected is centred near 0.5).

a, b := s.Overlap(s2)
L := a.LogRatio(b)
level := L.Hurst(n).Window(-(n - 1))
changes := L.Delta(1).Window(-1).Hurst(n).Window(-(n - 1))
  • First valid index: n - 1 for the level and n for the changes, counted from the start of the overlap; element e of each is overlap bar n - 1 + e or n + e. Minimum overlap: n + 1 bars (n for the level alone).
  • Parameters: n >= 2, and in practice n of 50 or more: on white noise Hurst averages about 0.37 at n = 10 and 0.46 at n = 50.
  • Detrended fluctuation analysis: L.DFAAlpha(n).Window(-n) is the DFA exponent of the log ratio’s one-bar changes (DFAAlpha takes the differences itself), trimmed so that its first window excludes the differences’ warm-up 0: first valid index n, n >= 20 (a smaller n gives zeros), 0.5 for a random walk of the ratio. Like Hurst, it gives 0, not NaN, on a window holding a NaN.
  • Domain: a zero, negative or non-finite price makes L NaN, and both outputs are then 0, not NaN, on the windows holding it, which reads as strong mean reversion: mask those windows. A window of equal finite values of L gives exactly 0, so s2 == s gives 0; a constant ratio other than 1, even a power of two, gives an L that varies in its last bits, which Hurst reads as data (about 0.87 on the level and 0.23 on the changes for s = 2*s2 at n = 50).
  • Anchors: with s2 all ones on positive finite s, both outputs are the same compositions on a.Log() (E); s2 == s gives exactly 0 in both (E); a NaN at overlap bar p gives exactly 0 in both outputs on every window holding it (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E).
  • Pinned by TestRecipeT4ALineOscillators; runnable example ExampleSeries_Hurst_logRatio.

Roofed relative-strength oscillator (Ehlers)

John Ehlers’s Ultimate Oscillator (Stocks & Commodities 43:5), applied to the log ratio a.LogRatio(b) rather than to a close: the difference of two high-pass filters, at width*edge and at edge bars, which passes cycles with periods between those lengths and removes the trend, of longer periods, and the noise, of shorter ones, divided by its own root mean square over 100 bars so that it reads in standard deviations. Ehlers reads it as momentum with little lag: above zero the market is likely trending up, and a zero crossing of its rate of change marks a turn. On the log ratio it is the momentum of relative strength, and swapping the legs negates it. His code’s defaults are a band edge of 20 and a bandwidth of 2 (his Figure 1 uses 30 and 2).

a, b := s.Overlap(s2)
osc := a.LogRatio(b).UltimateOscillator(edge, width).Window(-99)
  • First valid index: 99, counted from the start of the overlap; element e is overlap bar 99 + e. Minimum overlap: 100 bars. The trim is fixed because the RMS length is the default 100.
  • Parameters: edge >= 1 and width >= 1, both whole numbers of bars: Ehlers allows a fractional bandwidth (he advises at least 1.4), which UltimateOscillator does not. At width = 1 the two filters coincide and the oscillator is 0.
  • Transient: the high-pass recursions start from zero history, so early values carry a start-up transient; compare values only after a few times edge*width bars, about 120 to 200 at Ehlers’s defaults.
  • Domain: a zero, negative or non-finite price makes L NaN, and the oscillator NaN from that bar on for ever (from bar 1 on; a bad price at bar 0 alone never enters the filters), because the filters are recursive. An exactly flat log ratio gives 0 (0/0 by Div); a constant price ratio other than 1, even a power of two, gives a log ratio that varies in its last bits, which the RMS scaling turns into readings of order ±1 to ±3.
  • Anchors: with s2 all ones on positive finite s it is the same composition on a.Log() (E); s2 == s gives exactly 0 (E); a NaN at overlap bar p >= 1 makes the oscillator NaN from p on, and only there (E); a NaN at bar 0 alone leaves it finite, because the high-pass filters read bars 1 on.
  • Pinned by TestRecipeT4ALineOscillators.

KST with simple averages (Pring)

Martin Pring’s KST (“know sure thing”, Stocks & Commodities 26:12), which he introduced in Stocks & Commodities 10:9: four rates of change in percent, over the spans r[0] to r[3], each smoothed by an m[k]-bar average, weighted 1, 2, 3 and 4 and summed. It is built to turn when several price cycles turn together, and Pring reads its crossings of its own moving average as signals (10:10). This is the simple-average form; the next section is the exponential one. His 1992 presets are the table “Suggested KST formulas” of the editor’s sidebar “Calculating the KST” (Stocks & Commodities 10:9; the same sidebar is in 10:10, 10:11 and 18:4), where E marks an exponential average (use kstEMA); F is the first valid index, and the signal lines are those of his figure captions:

PresetSpans rAverages mFSignal line
Short-term, daily10, 15, 20, 3010, 10, 10, 1544—
Short-term, weekly3, 4, 6, 103E, 4E, 6E, 8E178-week simple (10:10)
Intermediate-term, weekly10, 13, 15, 2010, 13, 15, 20, simple or E3910-week (10:10)
Long-term, monthly9, 12, 18, 246, 6, 6, 9329-month (10:9)
Long-term, weekly39, 52, 78, 10426E, 26E, 26E, 39E14226-week (10:10)
f := max(r[0]+m[0], r[1]+m[1], r[2]+m[2], r[3]+m[3]) - 1
keep := len(x) - f
t1 := x.RateOfChange(r[0]).Window(-r[0]).Average(m[0]).Window(keep)
t2 := x.RateOfChange(r[1]).Window(-r[1]).Average(m[1]).Window(keep).Mul(2)
t3 := x.RateOfChange(r[2]).Window(-r[2]).Average(m[2]).Window(keep).Mul(3)
t4 := x.RateOfChange(r[3]).Window(-r[3]).Average(m[3]).Window(keep).Mul(4)
kst := t1.Add(t2).Add(t3).Add(t4)
  • First valid index: max_k(r[k] + m[k]) - 1, counted from the start of x; element e is bar max_k(r[k] + m[k]) - 1 + e of x. Term k is valid from bar r[k] + m[k] - 1, and every term is trimmed to the latest term’s start before the sum, so no term adds a warm-up value. Minimum history: max_k(r[k] + m[k]) bars (45 for the daily short-term preset).
  • Parameters: r[k] >= 1 and m[k] >= 1, and at least the minimum history; the weights 1, 2, 3 and 4 are fixed, as in every row of Pring’s preset tables.
  • ROC lag: RateOfChange(r[k]) compares each close with the close r[k] bars back, as ROC(C,n,%) does in Pring’s MetaStock code (26:12 and 27:1). The sidebar’s worked example, “Spreadsheet for short-term weekly KST” (10:9, also in 10:10, 10:11 and 18:4), labels its columns “3 week ROC” and so on but divides by the close n - 1 rows back (its four-week rate of change is row 20 over row 17), rounds its smoothing constants (0.29 for 2/7 and 0.22 for 2/9) and starts each exponential average from a single value. The references reproduce that sheet in a literal mode from testdata/ref/inputs/tasc-pring-18-4.csv, down to its printed KST values 6.26 and 10.71; the recipe does not follow it.
  • Domain: positive series. RateOfChange gives 0, not NaN, at a zero base, and its sign is not the signed percent’s on a negative base. A NaN makes a term NaN on the bars whose rate-of-change or averaging window reaches it, after which the simple averages recover. An infinite price gives an infinite rate of change at its own bar but a finite -100 where it is the base, r[k] bars later.
  • Relative form: on the ratio line a.PriceRelative(b, 1) of the overlap views, as in “Relative KST (Pring)” below, the first valid index counts from the start of the overlap.
  • Anchors: with s2 all ones on finite s, kstSMA(a.PriceRelative(b, 1), r, m) equals kstSMA(a, r, m) on s’s overlap view a (E). On a ratio line each term smooths RateOfChange(r[k]) trimmed by r[k], which is the relative return of “Relative performance and relative return” (E), so the relative KST is a weighted sum of smoothed relative returns.
  • Pinned by TestRecipeT4ATASCKST.

KST with exponential averages (Pring)

The same sum with exponential averages, which Pring uses on weekly bars (Stocks & Commodities 10:10): the short- and long-term weekly presets of the table above, and the intermediate one in its E form.

f := max(r[0]+m[0], r[1]+m[1], r[2]+m[2], r[3]+m[3]) - 1
keep := len(x) - f
t1 := x.RateOfChange(r[0]).Window(-r[0]).XAverage(m[0]).Window(keep)
t2 := x.RateOfChange(r[1]).Window(-r[1]).XAverage(m[1]).Window(keep).Mul(2)
t3 := x.RateOfChange(r[2]).Window(-r[2]).XAverage(m[2]).Window(keep).Mul(3)
t4 := x.RateOfChange(r[3]).Window(-r[3]).XAverage(m[3]).Window(keep).Mul(4)
kst := t1.Add(t2).Add(t3).Add(t4)
  • First valid index: max_k(r[k] + m[k]) - 1, as for simple averages, counted from the start of x; element e is bar max_k(r[k] + m[k]) - 1 + e of x (17, 39 and 142 for the weekly short-, intermediate- and long-term presets). Minimum history: max_k(r[k] + m[k]) bars, plus four to five times the longest m[k] of burn-in before the averages forget their seeds.
  • Seeds: each XAverage sees only its trimmed rate of change and seeds with the mean of its first m[k] values, at bar r[k] + m[k] - 1. The sidebar’s sheet starts each average from a single value instead (“The first cell value for each EMA column is the cell value directly to the left”, 10:9, 10:10 and 10:11), so the first values differ; that difference shrinks by the factor (m[k] - 1)/(m[k] + 1) a bar, but the sheet’s rounded constants (0.29 and 0.22) leave a lasting difference in its six- and eight-week averages.
  • Domain: as for simple averages, except that a NaN or infinite rate of change stays in its exponential average from that bar on.
  • Anchors: with s2 all ones on finite s, kstEMA(a.PriceRelative(b, 1), r, m) equals kstEMA(a, r, m) on s’s overlap view a (E).
  • Pinned by TestRecipeT4ATASCKST.

Relative KST (Pring)

Pring’s KST of a relative-strength line, “calculated by dividing the stock price by the market index value” (Stocks & Commodities 10:11). He finds the KST in many ways better suited to relative than to absolute prices, because group rotation through the business cycle makes relative prices the more cyclic. The block is the long-term relative KST on weekly bars, the series he compares across sectors (his figure 4), with a 26-week signal line; kstEMA is the composition of the section above, and any preset of the table can take its place, with kstSMA for the simple ones. The KST takes the rates of change of the ratio line itself, as Pring does, not of the log ratio, whose values cross zero, where a percent change is unbounded.

a, b := s.Overlap(s2)
line := a.PriceRelative(b, 1)
rkst := kstEMA(line, [4]int{39, 52, 78, 104}, [4]int{26, 26, 26, 39})
signal := rkst.Average(26).Window(-25)
  • First valid index: 142 for rkst and 167 for signal, counted from the start of the overlap; element e of each is overlap bar 142 + e or 167 + e. Minimum overlap: 143 and 168 bars, plus four to five times 39 bars of burn-in for the exponential seeds.
  • Signal line: Pring’s caption gives the weekly long-term KST a 26-week moving average for its crossovers without naming its kind (10:10, figure 2), and his text prefers crossings of an exponential average; the block uses a simple one, and rkst.XAverage(26).Window(-25) is the exponential alternative, with the same first valid index.
  • Reading (10:11): the best buying opportunities come when the long-term relative KST is below zero and starts to cross above its moving average while the shorter relative KSTs are not overextended, and when the ratio line itself has given a buy signal, such as a crossing of its exponential average or a trendline break; a long-term KST far above zero marks a trend that is well advanced. The long-term relative KSTs of two groups that lead at different stages of the cycle tend to move in opposite directions, with financials leading early and energy late. Pring applies the Special K below to sector ratio lines too (the S&P Financials and Materials against the S&P, 27:1).
  • Timeframes: Pring reads the short-, intermediate- and long-term KSTs together, his “market cycle model” (10:10, 10:11 and 18:4): the long-term KST gives the direction and maturity of the primary trend, and a short-term buy while it is declining “tells us that a bear market rally is underway” (10:11). Because plotting monthly, weekly and daily KSTs on one chart is onerous, he simulates the three trends on weekly bars (10:10); his weekly long-term spans and averages match the monthly ones times about 4.33, a relation he does not state (see “Timeframe emulation by length” under Relative: Timeframes). When the KST turns up and the Special K barely responds, chances are that the bearish longer cycles dominate (27:1).
  • Domain: the ratio line’s: a zero or non-finite price of s2, or a non-finite s, gives NaN, which the exponential terms keep from that bar on; a zero ratio as a rate-of-change base gives 0, not NaN. Use positive prices.
  • Anchors: with s2 all ones on finite s, rkst is kstEMA(a, [4]int{39, 52, 78, 104}, [4]int{26, 26, 26, 39}) on s’s overlap view (E); s2 == s gives exactly 0 when s is finite and non-zero throughout, because the ratio line is then exactly 1 (E).
  • Pinned by TestRecipeT4ATASCRelativeKST; runnable example ExampleSeries_PriceRelative_relativeKST.

Special K, daily (Pring)

Pring’s Special K adds the short-, intermediate- and long-term KSTs into one series of twelve terms, “true summed cyclicity” (Stocks & Commodities 26:12). His favourite form is daily and uses simple averages only; its terms are those of his MetaStock code (26:12, printed again in 27:1): the daily short-term KST (10/10, 15/10, 20/10 and 30/15), then 50/50, 65/65, 75/75 and 100/100, then 195/130, 265/130, 390/130 and 530/195, each group weighted 1 to 4. The signal line is a 100-bar simple average of a 100-bar simple average of the Special K, as in the code and his “100-day smoothing of a 100-day simple moving average” (27:1).

short := kstSMA(x, [4]int{10, 15, 20, 30}, [4]int{10, 10, 10, 15}).Window(len(x) - 724)
medium := kstSMA(x, [4]int{50, 65, 75, 100}, [4]int{50, 65, 75, 100}).Window(len(x) - 724)
long := kstSMA(x, [4]int{195, 265, 390, 530}, [4]int{130, 130, 130, 195})
sk := short.Add(medium).Add(long)
signal := sk.Average(100).Window(-99).Average(100).Window(-99)
  • First valid index: 724 for the Special K and 922 for the signal line, counted from the start of x; element e of each is bar 724 + e or 922 + e. The short and intermediate KSTs, valid from bars 44 and 199, are trimmed to the long one’s length before the sum. Minimum history: 725 and 923 bars, about 3.7 years of daily bars for the signal line; Pring plots the Special K over “three or more years of data” (27:1).
  • Reading (27:1): primary reversals show as trendline breaks of the Special K, as a reversal in its series of rising or falling peaks and troughs, and as crossings of its 100/100 smoothing. Pring plots the daily short-term KST beneath it for overbought and oversold readings; a KST rally with little or no Special K response suggests that the bearish intermediate- and long-term forces dominate.
  • Published versions: a literal translation of the MetaStock code adds the twelve products in the code’s order, which differs from the recipe’s grouping into three KSTs only in rounding.
  • Domain: as for the KST with simple averages; a NaN price reaches up to 724 bars ahead, through the 530-bar rate of change and its 195-bar average.
  • Pinned by TestRecipeT4ATASCSpecialK.

Special K, weekly (Pring)

The weekly Special K uses exponential averages (Stocks & Commodities 26:12): the terms 4/4, 5/5, 6/6 and 8/8, then 10/10, 13/13, 15/15 and 20/20, then the weekly long-term KST 39/26, 52/26, 78/26 and 104/39, each group weighted 1 to 4. Its short group differs from the weekly short-term preset of the KST table (3/3E, 4/4E, 6/6E, 10/8E). The signal line is “a 26-week simple moving average of a 52-week simple moving average of the Special K itself”.

short := kstEMA(x, [4]int{4, 5, 6, 8}, [4]int{4, 5, 6, 8}).Window(len(x) - 142)
medium := kstEMA(x, [4]int{10, 13, 15, 20}, [4]int{10, 13, 15, 20}).Window(len(x) - 142)
long := kstEMA(x, [4]int{39, 52, 78, 104}, [4]int{26, 26, 26, 39})
sk := short.Add(medium).Add(long)
signal := sk.Average(52).Window(-51).Average(26).Window(-25)
  • First valid index: 142 for the Special K and 218 for the signal line, counted from the start of x; element e of each is bar 142 + e or 218 + e. Minimum history: 143 and 219 bars, plus four to five times 39 bars of burn-in for the exponential seeds.
  • Code and table: Pring’s MetaStock code (26:12, printed again in 27:1) writes the eight-week term as Mov(ROC(C,8,%),6, E)*5, a 6-week average with weight 5, while his table “Special K formula for weekly charts” gives 8/8 with weight 4, and the table’s total, 537, is the sum of its average lengths times their weights with that term. The recipe follows the table, so it differs from a literal translation of the code.
  • Daily emulation: the daily form simulates this one on daily bars with simple averages and “the parameters are multiplied by 5” (26:12), exactly so only in the intermediate group (see “Timeframe emulation by length” under Relative: Timeframes).
  • Domain: as for the KST with exponential averages: a NaN or infinite value stays in the exponential terms from that bar on.
  • Pinned by TestRecipeT4ATASCSpecialK.

Relative volatility, tracking error and information ratio

Three statistics of the legs’ one-bar percent returns over the last n bars, ra := a.RateOfChange(1).Window(-1) and rb likewise from b, each trimmed of its warm-up 0: the relative volatility, the standard deviation of ra over that of rb; the tracking error, the standard deviation of the active return ra - rb; and the information ratio, the mean active return over the tracking error. With s a portfolio and s2 its benchmark these are the ex-post statistics of active management (Grinold and Kahn, Active Portfolio Management); with s2 a cash index the information ratio is a Sharpe ratio, which Jerome M. Lederman (Stocks & Commodities 14:8) computes as the return in excess of the three-month Treasury bill yield over its standard deviation.

a, b := s.Overlap(s2)
ra := a.RateOfChange(1).Window(-1)
rb := b.RateOfChange(1).Window(-1)
active := ra.Sub(rb)
sd := active.StdDev(n)
rv := ra.StdDev(n).Div(rb.StdDev(n)).Window(-(n - 1))
te := sd.Window(-(n - 1))
ir := active.Average(n).Div(sd).Window(-(n - 1))
  • First valid index: n for all three, counted from the start of the overlap; element e of each is overlap bar n + e. Minimum overlap: n + 1 bars.
  • Parameters: n >= 2.
  • Conventions: population standard deviations (divisor n) of one-bar returns, per bar; to annualise, multiply the tracking error and the information ratio by the square root of the bars in a year (about 15.9 for 252 daily bars). The mean and the deviation are of the same one-bar returns: an n-bar relative return over the one-bar tracking error would be about n times larger and would mix horizons. Grinold and Kahn define the information ratio on residual return, net of the portfolio’s beta to the benchmark; the active return here takes that beta as 1.
  • Reading: the absolute beta of ra on rb is the absolute correlation times the relative volatility, so a relative volatility of 1.6 at a correlation of 0.5 is a beta of 0.8.
  • Domain: RateOfChange gives a 0 return at a zero base, not NaN. A flat window of s2’s returns gives a relative volatility of 0 (Div), even where s’s window holds a NaN, and a zero tracking error an information ratio of 0. Otherwise a window holding a NaN or an infinite return gives NaN; an infinite price gives an infinite return at its bar and -100 where it is the base.
  • Anchors: the tracking error equals a.RateOfChange(1).Sub(b.RateOfChange(1)).Window(-1).StdDev(n), trimmed by n - 1 (E); s2 = 2^p*s on finite s gives a relative volatility of exactly 1 on windows where rb is not flat, 0 where it is (E), and at p = 0 a tracking error of 0 and an information ratio of +0 (E). With sa, sb and r the n-bar deviations and correlation of ra and rb, the squared tracking error equals sa^2 + sb^2 - 2*r*sa*sb within 1e-10*(sa^2 + sb^2), and |ra.Beta(rb, n)| equals |r| times the relative volatility within 1e-12*|Beta|.
  • Pinned by TestRecipeT4ARelativeVolatility; runnable example ExampleSeries_StdDev_relativeVolatility.

Relative volatility over k-bar returns

The relative volatility on k-bar percent returns: the standard deviation over the last n bars of s’s k-bar returns, trimmed of their k warm-up zeros, over that of s2’s. Katsanos compares the correlations of markets across return horizons from a day to a quarter (Intermarket Trading Strategies, Table 5.7); this horizon form of relative volatility is derived, with no published source. For two random walks the ratio is about the same at every horizon, so a ratio that drifts with k points to trending or mean reversion in one leg relative to the other.

a, b := s.Overlap(s2)
rv := a.RateOfChange(k).Window(-k).StdDev(n).Div(b.RateOfChange(k).Window(-k).StdDev(n)).Window(-(n - 1))
  • First valid index: k + n - 1, counted from the start of the overlap; element e is overlap bar k + n - 1 + e. Minimum overlap: k + n bars.
  • Parameters: n >= 2 and k >= 1. The k-bar returns at consecutive bars share k - 1 bars, so a window of n of them carries less information than n one-bar returns; take n several times k.
  • Domain: the relative volatility’s, on k-bar returns.
  • Anchors: at k = 1 it is the relative volatility above, bit for bit (E); s2 = 2^p*s on finite s gives exactly 1 on windows where s2’s k-bar returns are not flat, 0 where they are (E).
  • Pinned by TestRecipeT4ARelativeVolatility.

Level volatility ratios (Katsanos)

Two ratios of price levels over m bars: the standard deviation of s over that of s2, and the coefficient of variation of s, its standard deviation over its simple average, over that of s2. Katsanos uses both as scale factors (Intermarket Trading Strategies, Appendix A.1): his intermarket LRS divergence multiplies one security’s percent slope by the ratio of the two securities’ 200-day standard deviations of price or, for securities at different price levels such as gold and silver, by that ratio times the ratio of their 200-day averages, which is the coefficient-of-variation ratio; his double LRS divergence weights each predictor by a ratio of coefficients of variation, over 300 days by default. The first ratio is in units of s per unit of s2; the second has no units.

a, b := s.Overlap(s2)
sdRatio := a.StdDev(m).Div(b.StdDev(m)).Window(-(m - 1))
cvRatio := a.StdDev(m).Div(a.Average(m)).Div(b.StdDev(m).Div(b.Average(m))).Window(-(m - 1))
  • First valid index: m - 1 for both, counted from the start of the overlap; element e of each is overlap bar m - 1 + e. Minimum overlap: m bars.
  • Parameters: m >= 2; Katsanos’s 200 and 300, on daily bars.
  • Levels, not returns: the standard deviation of a trending price grows with the trend and with m, so these ratios mix trend with volatility; the relative volatility above compares returns.
  • Order: the coefficient-of-variation ratio divides one coefficient of variation by the other, the order of the double LRS divergence; the price-level form multiplies the standard-deviation ratio by the average of s2 and divides by that of s, which agrees within 4 units of 2^-52, relative.
  • Domain: the relative volatility’s, on levels: a flat s2 window or a zero average gives 0 (Div), and a window holding a NaN or an infinity gives NaN.
  • Anchors: s2 = 2^p*s on finite s gives a standard-deviation ratio of exactly 2^-p and a coefficient-of-variation ratio of exactly 1 on windows where s2 is not flat, both 0 where it is (E). The coefficient-of-variation ratio equals the price-level form ((sdA/sdB)*maB)/maA within 4*2^-52 relative.
  • Pinned by TestRecipeT4ARelativeVolatility.

Percentile volatility ratio (Katsanos)

The q-quantile of s’s signed one-bar percent returns over the last n bars, over the same quantile of s2’s, by Quantile (Hyndman and Fan’s type 7). Katsanos tabulates percentiles of the daily percentage changes of stock indices and commodities from 1992 to 2006 (Intermarket Trading Strategies, Table 4.3), and sizes futures positions by the daily change exceeded only 10% of the time, in each tail, so that such a move gives each position the same profit or loss (Table 4.5). The ratio compares two legs’ tail moves per unit of price: q = 0.1 for the lower tail and q = 0.9 for the upper.

a, b := s.Overlap(s2)
ratio := a.RateOfChange(1).Window(-1).Quantile(n, q).Div(b.RateOfChange(1).Window(-1).Quantile(n, q)).Window(-(n - 1))
  • First valid index: n, counted from the start of the overlap; element e is overlap bar n + e. Minimum overlap: n + 1 bars.
  • Parameters: n >= 2 and 0 <= q <= 1. Katsanos’s percentiles span whole histories (15 years in Table 4.3, five in Table 4.5); a year of daily bars, n = 252, holds about 25 returns beyond each 10% tail.
  • Signs: the returns are signed, so both lower-tail quantiles are normally negative and their ratio positive; the ratio is negative where the two quantiles have opposite signs, as can happen near the median.
  • Domain: RateOfChange gives a 0 return at a zero base; a window holding a NaN or an infinite return gives NaN; a zero quantile of s2’s returns gives 0 (Div).
  • Anchors: s2 = 2^p*s on finite s gives exactly 1 on windows where s2’s quantile is not 0, and 0 where it is (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E).
  • Pinned by TestRecipeT4ARelativeVolatility.

A relative-volatility cap (Huck)

Ken and Nelson Huck’s hedge rotation (Stocks & Commodities 38:13, restated in 43:13) buys a hedging fund only if its daily standard deviation is no more than 1.6 times that of the S&P 500 “over the previous 53 market days”; if no fund qualifies, it holds a money-market fund. The articles do not say whether the deviations are of prices or of returns; the recipe takes one-bar returns, which compare funds at different prices. spx is the S&P 500 and s the candidate fund.

rv, _, _ := relativeVolatility(s, spx, 53)
eligible := rv.LTE(1.6)
  • First valid index: 53, counted from the start of the overlap of s and spx; element e of eligible is overlap bar 53 + e. Minimum overlap: 54 bars.
  • Reading: 1 where the fund’s 53-bar return deviation is at most 1.6 times the index’s, 0 elsewhere.
  • Domain: LTE reads a comparison with NaN as false, so a window holding a non-finite return shuts the gate, unless the index window is flat: that gives a relative volatility of 0 (Div), which opens it whatever the fund’s returns. The helper’s result is already trimmed; an untrimmed ratio would open the gate on its warm-up zeros.
  • Other uses: Perry Kaufman (Stocks & Commodities 32:3) hedges a long stock with index shares in the amount of the stock position times the ratio of the two 60-day standard deviations of returns times a hedge ratio (he recommends 0.5), adding the hedge when the index’s 60-day simple average turns down and removing it when the average turns up; that ratio is the first result of relativeVolatility(stock, index, 60). In 42:2 he sizes a commodity against a stock for equal volatility by the ratio of their 20-day average absolute returns. Rob Friesen (39:3) balances the legs of a pair by their average true ranges, and John Forman (24:5) compares volatility across markets with the 14-bar average true range as a percentage of the close; both need high and low prices.
  • Pinned by TestRecipeT4AVolatilityCap.

Cross rates, re-denomination and quote inversion

Exchange rates compose by multiplication: with eurusd in dollars per euro and usdjpy in yen per dollar, their product is yen per euro, the EUR/JPY cross. (Katsanos writes that the EUR/JPY is the EUR/USD divided by the USD/JPY, Intermarket Trading Strategies, section 17.1; it is their product.) A price re-denominated in another currency divides by the exchange rate: Katsanos’s gold in euros (section 7.1, Figure 7.2), gold in dollars an ounce over dollars per euro, moved sideways from 2002 to 2005 while gold in dollars rallied, which shows that rally as a fall of the dollar. An inverted quote is the reciprocal, Pow(-1), which keeps the infinity at a zero price where Div would give 0.

a, b := eurusd.Overlap(usdjpy)
eurjpy := a.Mul(b)
g, f := goldUSD.Overlap(eurusd)
goldEUR := g.PriceRelative(f, 1)
jpyusd := usdjpy.Pow(-1)
  • First valid index: 0 for all three, counted from the start of each overlap (of usdjpy itself for the inversion); element e is bar e. Minimum overlap: 1 bar.
  • Correlation under inversion: inverting a leg negates its log-return correlation, LogReturnCorrelation, up to the rounding of 1/x, but not its percent-return correlation, ReturnCorrelation, because the percent change of a reciprocal is not minus the percent change; on volatile daily walks the two can differ by a few hundredths. Katsanos’s yen systems trade USD/JPY, and for yen futures, quoted the other way, he negates the disparity term and adjusts its thresholds (section 17.4 and Appendix A.8).
  • Alignment: the legs are paired on Overlap views. Mul with a Series operand also right-aligns the two at their most recent bar, but keeps the receiver’s length, with zeros before the overlap, so a longer eurusd would give a cross rate of 0 on its earlier bars.
  • Domain: Mul’s for the cross rate (NaN and infinities propagate); PriceRelative’s for the re-denominated price (NaN at a zero or non-finite rate or a non-finite price); Pow(-1) gives ±Inf at ±0.
  • Anchors: Pow(-1) equals 1/x bit for bit wherever the reciprocal is normal, and gives ±Inf at ±0 (E); with eurusd all ones, goldEUR is goldUSD’s overlap view (E); 1.47 dollars per euro times 104.9 yen per dollar gives exactly the float64 154.203 yen per euro (E); s.LogReturnCorrelation(s2.Pow(-1), n, k) is minus s.LogReturnCorrelation(s2, n, k) within 1e-13.
  • Pinned by TestRecipeT4ACrossRates.

Relative: Channels

Breakout

func (s Series) Breakout(n, lag int) Series

Breakout returns +1 when a value exceeds the highest of the n values that end lag bars earlier, -1 when it falls below their lowest, and 0 otherwise.

With m = len(s), the first valid index is F = n + lag - 1, and the minimum length is n + lag. Invalid conditions are tested in this order: m == 0, n < 1, lag < 1, lag >= m, and n > m - lag. They give aligned zeros. Validation uses comparisons and subtraction before allocating the deques; huge invalid parameters allocate only the output. The result is a fresh Series of length m, never aliases or modifies the input, and contains exactly +0 before F. No domain rule applies: any finite values are accepted.

For j >= F, the reference window P is s[j-lag-n+1..j-lag]. Two rollExtreme deques supply its maximum and minimum. Strictly exceeding the maximum gives +1, strictly falling below the minimum gives -1, and ties or values between the extremes give +0.

If s[j] or any value of P is NaN or an infinity, the output is NaN; +Inf never produces +1. A nonFiniteWindow of width n tracks P, so values in the gap between P and j have no effect. A non-finite value at index p produces NaN at p if p >= F, and at max(p+lag, F)..min(p+lag+n-1, m-1). These sets need not be contiguous when lag > 1. Later outputs recover; results are window-local and bitwise start-invariant.

Markos Katsanos’s Chapter 16 filter in Intermarket Trading Strategies (2008) is RS > Ref(HHV(RS,250),-20). His text says “a new 250-day high”; the code determines the lag. On overlap views, the composition is:

x, y := s.Overlap(s2)
x.PriceRelative(y, 150).Window(-149).Breakout(250, 20)

Its first valid index is 418 in the overlap and its minimum overlap is 419 bars, plus about 600 to 750 bars of burn-in for the 150-bar average.

Rob Friesen’s multi-year breakouts of a ratio line (Stocks & Commodities, March 2021) use Breakout(n, 1) on the trimmed line. John Murphy’s six-month high of a relative-strength line is another application. With lag = 1 and a finite channel range, +1 implies ChannelPosition(n+1, 1) == 100.

Deviations from Katsanos’s upside-only code are the -1 branch, zeros before the first valid index, and NaN for a non-finite current or reference value. Processing takes amortised O(1) per bar with at most three allocations per call and no package-level mutable state.

Single-series compositions of it are also listed under “Compositions” in the “Donchian Channels”, “Point and Figure” and “Moving Averages” categories: the classic breakout systems, OXXO’s latched state and Dukas’s channel highs and lows. Crabel’s narrow-range and wide-spread bars under “Bar Classification”, the NR4 and wide-range days of the historical-volatility ratios under “Volatility Estimators” and the new BandWidth low under “Bollinger Bands” use it too.

The compositions that use it are listed under “Compositions” in the “Relative: Channels” category.

Verification: Compare against two independent references: a Python reference in exact rational arithmetic and a Julia reference. On finite input at j >= n + lag - 1, the bit-for-bit identity is s.GT(s.Highest(n).Shift(lag)).Sub(s.LT(s.Lowest(n).Shift(lag))).

ChannelPosition

func (s Series) ChannelPosition(n, smooth int) Series

ChannelPosition returns where each value sits in its trailing n-bar high-low channel on a 0 to 100 scale, slowed by summing smooth bars, as in Katsanos’s Intermarket Momentum Oscillator.

Markos Katsanos describes this Intermarket Momentum Oscillator in Intermarket Trading Strategies (2008), applying n of 200 or 300 and 3-bar slowing to an intermarket divergence or a relative-strength line. With HH and LL the trailing n-bar highest and lowest values, including the current bar, its formula is (sum of (x - LL) / sum of (HH - LL)) * 100 over the last smooth bars, treating smooth <= 1 as 1. Each subtraction is rounded as a float64; the sums are recomputed left to right in index order at every bar, and the division precedes the multiplication.

The first valid index is F = n + max(smooth, 1) - 2, and the minimum length is n + max(smooth, 1) - 1. The result is a fresh Series of length len(s), never aliases or modifies the input, and contains exactly +0 before F. An empty input, n < 1, n > len(s), or max(smooth, 1) > len(s) - n + 1 gives aligned zeros. Parameter validation uses comparisons and subtraction before any parameter-sized allocation; huge invalid parameters allocate only the output. No domain rule applies: any finite values are accepted.

A flat channel gives exactly 50. The reflection identity ChannelPosition(-X) == 100 - ChannelPosition(X) holds on flat windows only with 50; returning 0 would fire Katsanos’s “lowest of the last 4 values below 20” buy rule after a flat stretch. The warm-up 0 before the first valid index F fires the same rule on the first bars, so trim to F (Window(-F)) or mask it (SetN(F, math.NaN())) before such a rule. Existing methods differ on flat channels: Stochastic returns 0 after its warm-up, PercentR returns 100, and CaseyPercentC returns 0. With n = 1, every valid finite window gives 50. With n >= 2 and finite denominators, strictly increasing data gives exactly 100 and strictly decreasing data exactly 0. Power-of-two scaling preserves results bit for bit when it causes no overflow or underflow, and reflection satisfies the identity above up to rounding. No clamping is performed; finite inputs with finite denominators nevertheless stay exactly in [0, 100].

A NaN or +Inf or -Inf at index p produces NaN at max(p, F)..min(p + n + max(smooth, 1) - 2, len(s)-1); later outputs recover. A non-finite denominator, including a range wider than the largest float64 or an overflowing sum of ranges, also produces NaN. Results are window-local and bitwise start-invariant.

Two-series use always starts on overlap views so that pre-overlap zeros cannot enter the channel. The recommended form is:

x, y := s.Overlap(s2)
x.LogRatio(y).ChannelPosition(n, smooth)

Katsanos’s relative-strength form is:

x, y := s.Overlap(s2)
x.PriceRelative(y, 3).Window(-2).ChannelPosition(300, 3)

Its first valid index is 301 in the trimmed series, overlap bar 303, and its minimum overlap is 304 bars.

Katsanos buys near 80 and sells near 20–30 on divergences. On a relative-strength line the levels are reversed: buy when it drops below 20 and rises again. A signal line is .Average(4). His Chapter 16 “new 250-day high” rule is Breakout(250, 20), not ChannelPosition == 100.

CaseyPercentC places the one-bar return in its channel and averages the positions, an average of ratios. ChannelPosition places the level in its channel and divides smoothed sums, a ratio of averages.

Published relationship uses in Stocks & Commodities include Mark Vakkur’s 36-month position of the earnings yield minus the 10-year yield, with results by percentile band (December 2001); Perry Kaufman’s inverted crack-spread position, 100 - ChannelPosition(n, 1) up to rounding, with n of 5, 10 or 15 days (April 2026); Mike Flanagan’s five-year normalisation of fund cash (November 1994); and Katsanos’s ChannelPosition(50, 2) of a 50-bar divergence (July 2017). Each published code either divides by zero on a flat window or adds a guard in data units, such as Katsanos’s +.01, which also biases every value. The flat value 50 replaces both behaviours.

Katsanos’s MetaStock expression is (Mov(X - LLV(X,n),3,S)*100)/(Mov(HHV(X,n) - LLV(X,n),3,S)). Deviations are a ratio of sums rather than divided simple averages, equal in exact arithmetic; the order (num/den)*100; smooth as a parameter; 50 on a flat channel; zeros before the first valid index; and NaN for a non-finite window.

Katsanos’s pre-breakout squeeze applies it to a normalised standard deviation, StdDev(30) over Average(30), with n = 150 and smooth = 3, a recipe listed under “Compositions” in the “Bollinger Bands” category.

Extremes take amortised O(1) per bar and sums take O(max(smooth, 1)) per bar. There are at most four allocations per call and no package-level mutable state.

The compositions that use it are listed under “Compositions” in the “Relative: Channels” category.

Verification: Compare against two independent references: a Python reference in exact rational arithmetic and a Julia reference that also translates Katsanos’s MetaStock code. With smooth = 1 on windows that are not flat, after the first valid index and subject to the non-finite rules above, the exact identities are s.Sub(s.Lowest(n)).Div(s.Highest(n).Sub(s.Lowest(n))).Mul(100) and s.Stochastic(s, s, n).

Quantile

func (s Series) Quantile(n int, q float64) Series

Quantile returns the rolling q-quantile of the last n bars, Hyndman and Fan’s type 7 (linear interpolation between order statistics).

With the window’s values sorted ascending as v[0] <= … <= v[n-1], h = (n-1)q, j = floor(h) and f = h - j, the value is v[j] + f(v[j+1]-v[j]), or v[j] when f is 0. It is the definition of numpy’s quantile(method=“linear”), pandas’s rolling(n).quantile(q, interpolation=“linear”), R’s quantile(type = 7) and Excel’s PERCENTILE.INC. The comparison holds on finite data only (pandas’s warm-up is NaN, and numpy and pandas treat an infinity differently), and there the bits may differ: numpy and R interpolate in other forms, pandas’s compiled code may fuse the same form into a multiply-add, none of the three snaps h to an integer, and numpy and pandas overflow on huge values of opposite signs, where Quantile stays finite.

The warm-up reads 0, not NaN: bars 0 to n-2 are 0 whatever the data, the first valid index is n-1 and the minimum length is n. Trim those bars, or mask them with SetN(n-1, math.NaN()), before using the values.

Invalid parameters (n < 1, or q NaN, infinite, below 0 or above 1) give a series of zeros, as does n > len(s), where every bar is in the warm-up. A valid bar is NaN while its window holds a NaN or an infinity. The rule is window-local: each valid bar depends only on its own window, so the values recover once the non-finite bar has left it, and the method is start-invariant.

On finite data, Quantile(n, 0) is the window minimum, as Lowest(n), and Quantile(n, 1) the maximum, as Highest(n). Quantile(n, 0.5) is the rolling median: an odd n gives the middle value exactly, and an even n interpolates halfway between the two middle values as v[j] + 0.5*(v[j+1]-v[j]), which can differ by an ulp from the correctly rounded (v[j]+v[j+1])/2. For n > 1 and 0 <= j <= n-1, Quantile(n, float64(j)/float64(n-1)) is exactly order statistic j (the j-th smallest value of the window, from 0), because an h within rounding error of an integer is snapped to it. Both exact cases hold apart from a -0 statistic, which comes back as +0. s.Quantile(n, 0.9) and s.Quantile(n, 0.1) make a robust channel, less sensitive to outliers than Highest and Lowest: from n >= 11 the window’s maximum has no weight in Quantile(n, 0.9), nor its minimum in Quantile(n, 0.1), while for n <= 10 the extreme keeps weight 0.9(n-1)-(n-2).

The value is monotone in q (raising q does not lower it), and an interpolated value lies between v[j] and v[j+1], also for huge values of opposite signs whose difference overflows. A zero is returned as +0. Each bar costs O(n) time, and a call allocates the result and one window buffer.

RankPosition is its inverse at the window’s own values: with q := s.RankPosition(n), s.Quantile(n, q[t]) at bar t is s[t] (+0 for -0) wherever the window ending at bar t is finite. QuantileWhere takes the same statistic over the last k bars on which a condition fires, or over all of them so far.

Verification: two clean-room references (Python and Julia, in testdata/gle257-wp010), which agree byte for byte, evaluate the same binary64 program, and Quantile matches them bit for bit; on the reference inputs it lies within 2^-50 relative of the exact type 7 quantile. From bar n-1 on finite data, Quantile(n, 1) equals Highest(n).Add(0.0) and Quantile(n, 0) equals Lowest(n).Add(0.0) bit for bit, and on positive data, from bar n-1, s.Log().Quantile(n, q) equals s.Quantile(n, q).Log() at q = j/(n-1).

RankPosition

func (s Series) RankPosition(n int) Series

RankPosition returns the percentile rank of each value within its own trailing n-bar window, on the 0 to 1 scale of PercentRank.

For bar i, with the window s[i-n+1 .. i] held in ascending order, j is the index of the first value equal to s[i], which is the number of values in the window strictly less than s[i], and the output is the binary64 product interval * float64(j), where interval = 1 / (float64(n) - 1) for n > 1 and interval = 0 for n = 1. These are the expressions PercentRank evaluates, so on finite inputs s.RankPosition(n)[i] == s.PercentRank(s[i], n)[i] bit for bit at every i >= n - 1. The output lies in [0, 1], since j runs from 0 to n - 1: it is 0 when no value of the window is below s[i] and, for n > 1, interval * (n - 1) when all the other values are: 1 for most n, but 1 - 2^-53 for some, such as n = 50 and n = 99, exactly as in PercentRank. Only the order of the values counts, so the result is unchanged by any transform that is strictly increasing on the values present, keeps finite values finite, and keeps equal values equal and distinct values distinct in binary64, for example s.Mul(2^k) while the values stay normal, or s.Exp() while the exponentials stay finite and distinct.

The first valid index is n - 1 and the minimum length is n; the outputs before index n - 1 are 0. If n < 1 or n > len(s) the result is all zeros: an invalid n, however large or negative (math.MaxInt and math.MinInt included), is rejected before any loop, in O(len(s)) time and with only the result allocated. The result is always a fresh series of length len(s), and s is never modified. A valid call makes two allocations, the result and a sorted buffer of capacity n, whatever the length of s, and none per bar.

Each output depends only on its own window s[i-n+1 .. i], so the method is window-local and start-invariant: dropping leading values of s leaves the remaining outputs unchanged bit for bit. From bar to bar the sorted buffer loses the value that leaves the window, the oldest of the values equal to it, and gains the value that enters, after the values equal to it, so it always equals, bit for bit, a buffer built afresh by inserting the window’s values that are not NaN in window order (a stable sort; an unstable sort may order -0 and +0 differently).

A window that contains a NaN or an infinity gives NaN, the library’s shared non-finite rule, and the output recovers once that value has left the window: a non-finite value at index p makes the outputs at indices p to p + n - 1 NaN, clipped to the valid bars. Ties take the lowest rank, the first match, so a flat window gives 0, not the neutral 50 of ChannelPosition; mid-ranks, which would average the tied positions, are a documented alternative that is not provided. With n = 1 every bar holding a finite value gives 0, since interval is 0, and a bar holding a non-finite value gives NaN.

RankPosition is the percentile sibling of Standardise: PercentRank(val, n), like ZScore(avgLen, val), scores a constant against each window, whereas this method, like Standardise, scores each bar’s own value. To compare it with ChannelPosition, the 0 to 100 position in the high-low channel, multiply it by 100. For a relationship line L := a.LogRatio(b), L.RankPosition(n) is a robust channel position, insensitive to outliers, since one finite extreme value moves the rank of any other value by at most one place, and to the scale and origin of the line in exact arithmetic; in binary64 a rescaling or shift can merge values that differ in the last bits and so change their ranks, whereas multiplication by a power of two leaves every rank unchanged while the values stay normal.

Published percentile readings map onto this method. McEwan ranks a pair’s spread, the ratio U of the two legs’ cumulative midpoint-return indices (the midpoint return is 2(P_t - P_{t-1})/(P_t + P_{t-1})), by COUNTIF(U range <= U_t)/COUNT against a 0.5 signal line (Stocks & Commodities 30:5); that count runs over the whole sample, which is look-ahead, and its causal form is RankPosition(n) over a trailing window on U or its log; the relationship line of the two prices is a close substitute, not the same series, since a cumulative midpoint-return index is not the price relative. (His count includes the current value and its ties and divides by the sample size, where RankPosition counts the values strictly below and multiplies by 1/(n - 1).) Lewis, Moody, Parker and Hyer, of Dorsey Wright Money Management, score each member of a universe weekly by its percentile among the members on its 52-week return, or on its price’s deviation from its six-month simple moving average, buying above one threshold and selling below another (Stocks & Commodities 23:9); their percentile runs across the universe, not across time, so this method is its time-series analogue, on which such thresholds are a consumer’s latch. Snead’s full-sample extreme bands of an intermarket series, the largest 1%, 2%, 5%, 10% and 20% of 20-day declines in a smoothed Treasury bill yield, used to time the S&P 500 (Stocks & Commodities 12:11), become causal as thresholds on this rolling self-percentile. The cost is O(n) per bar, a binary search and one memmove, against PercentRank’s O(n log n) sort per bar; neither allocates per bar.

With n previous bars and the current one, s.RankPosition(n+1) is the share of the previous n values strictly below the current one, the implied-volatility percentile, to one ulp (the product interval * j, with interval the rounded 1/n, misses j/n in the last bit for some j: 80 of the 253 values at n = 252), and s.RankPosition(n+1).Mul(n).Round().Div(n) is exactly j/n; the recipes that use it are listed under “Compositions” in the “Volatility Estimators” category.

Verification: two independent references agree with it exactly, a Python reference that ranks each window in a sorted list by the first match of the current value and a Julia reference that counts the window values strictly below the current value, which is R’s rank(ties.method = “min”) less one, both forming the value as the binary64 product interval * j, with interval equal to 0 when n = 1; on finite inputs it equals PercentRank of the current value, s.PercentRank(s[i], n)[i], bit for bit; and it is unchanged by the strictly increasing transforms of multiplication by a power of two (Mul, while the values stay normal) and Exp (while the exponentials stay finite and distinct).

RegressionChannel

func (s Series) RegressionChannel(s2 Series, n int, k float64) Series

RegressionChannel returns the fitted value of s from its rolling n-bar least-squares fit on s2, shifted by k standard deviations of that window’s residuals.

Each window of n bars is fitted by ordinary least squares with an intercept, s = alpha + betas2 + e, the response being s and the regressor s2, the slope being that of Beta and the intercept that of Intercept (the same co-moment kernel as RegressionSpread). At the window’s last bar the fitted value mid is the current bar of s less its residual, and sigma is the standard deviation of the window’s n residuals with the population divisor n, the square root of their compensated sum of squares over n. The output is mid + ksigma: k = 0 gives the fitted line, k > 0 an upper band and k < 0 a lower band, Any finite k is valid. A huge k can overflow sigma*k, and levels near the overflow threshold can overflow the fitted value, to an infinity, which is then the output (or NaN where two infinities of opposite sign meet); wherever neither output is NaN, the output for k1 is at least the output for k2 whenever k1 >= k2.

The Go operation order is part of the contract, because the identities below hold bit for bit. The fit has s2 as its regressor window and s as its response window; with e the residual of the current bar and ssr the residual sum of squares, both in the kernel’s scaled units, and ey the response’s scaling exponent, the output at bar j is

spread := math.Ldexp(e, ey)                        // the residual of bar j, in the units of s
mid := s[j] - spread                               // the fitted value
sigma := math.Ldexp(math.Sqrt(ssr/float64(n)), ey) // the residual standard deviation, divisor n
out[j] = mid + float64(sigma*k)

The conversion rounds the product before the addition, so the compiler cannot fuse the two into a multiply-add.

Sources: Markos Katsanos, Intermarket Trading Strategies (Wiley 2008), chapter 3, section 3.1, pages 33-34, equations 3.1-3.4, and section 9.5, page 128, equations 9.9-9.11, for the regression of one market on another; Ernest Chan, Quantitative Trading, 2nd edition (Wiley 2021), Examples 3.6 and 7.2, whose GLD-GDX spread is fitted once, without an intercept, on a training year and traded on its z-score (enter at 2, exit at 1), where this method refits with an intercept at every bar; and Jon Andersen, “Standard Error Bands”, Stocks & Commodities 14:9 (September 1996), pages 375-379, whose bands lie two standard errors (divisor n - 2) about the end of a 21-bar regression of price on time, both smoothed over three bars; with s2 a ramp and k scaled by sqrt(n/(n-2)) this method gives his unsmoothed band.

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 < 3 (two bars fit any line exactly, leaving no residual), when n exceeds the overlap, or when k is NaN or an infinity; 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 of n bars, so the method is window-local and start-invariant: dropping leading values leaves the remaining outputs unchanged bit for bit. A window costs O(n), and a valid call makes two allocations, the result and the kernel’s scratch buffer of 2n values, whatever the input length.

A NaN or an infinity at aligned bar p of either input makes the outputs for bars p to p+n-1 (clipped to the valid bars) NaN, and later outputs recover. Any finite data are valid, positive, negative or zero, so the channel also serves spreads and returns. A flat s2 window, whose centred sum of squares is zero, has a slope of 0, so the fitted value is the window mean of s and sigma is its population standard deviation; the result then approximates s.Bollinger(n, k), that is Average(n) + k*StdDev(n), differing only by rounding. A flat s window is a perfect fit and gives s.

The perfect-fit guard protects exact fits, whose computed residuals are pure rounding noise that would otherwise become a spurious sigma. When the window’s residual sum of squares is at most 1e-24 of its centred sum of squares of s, or that sum is zero, there is no residual scale to shift by, and the output is the current bar of s for every k, so every band equals s. RegressionSpread returns 0 under the same guard.

These identities hold bit for bit at every valid bar that is not NaN. The k = 0 line equals s[i] - s.RegressionSpread(s2, n)[i], the same fit, residual and rounding, and the two differ only in the sign of a zero when s[i] is -0 on a window that is not perfect. The call with s2 == s gives s, and so does a line s = 2^ps2 + d that is exact in binary64, whose residuals are exactly 0; s = 3s2 + 7 on integer data gives s through the guard. Scaling s by 2^p and s2 by 2^q scales the output by 2^p while every value stays normal; negating s negates the output and the sign of k, up to the sign of a zero; negating s2 leaves the output unchanged.

To tolerance, on a flat s2 the result equals s.Bollinger(n, k) to 1e-12 times max(1, |k|) relative to the level. On a ramp s2, each value being its bar index, the k = 0 line equals s.Average(n) + s.Slope(n)*float64(n-1)/2 to 1e-15 relative to the level (the largest magnitude of s in the window), and the residual standard deviation, the spacing of the bands, equals s.StdDev(n)*math.Sqrt(1 - s.R2(n)) to a tolerance scaled by the level, because the rolling variance of StdDev carries rounding of order machine epsilon times the level.

With a, b := s.Overlap(s2), the log-log variant is the composition a.Log().RegressionChannel(b.Log(), n, k).Exp() (positive inputs; its warm-up bars read 1, not 0), which gives multiplicative bands about a power-law fair value. Andersen’s degrees-of-freedom variant multiplies the residual standard deviation by sqrt(n/(n-2)), which callers get by scaling k by that factor; this method uses the population divisor n. The through-origin variant is not provided.

When s leaves mid +/- 2*sigma it has decoupled from s2 by more than usual, given the current slope (Chan: enter at +/-2, exit at +/-1); the line is a fair-value line for s, such as GLD against GDX (Chan). Katsanos warns that raw prices violate the two basic assumptions of regression, linearity and normality, and uses the regression only on price differences or percentage yields; on levels, prefer the log-log variant or a cointegrated pair.

Verification: two independent references agree with it to a tolerance scaled by each window’s condition number, a Python reference in exact rational arithmetic and a Julia reference that fits each window by QR factorisation in high precision; s2 == s and exact dyadic lines give s, and 3*s2 + 7 on integer data gives s through the guard; the scaling and sign identities hold bit for bit; the k = 0 line is s minus RegressionSpread bit for bit; and on a flat s2 it agrees with Bollinger to tolerance.

RelativeBollinger

func (s Series) RelativeBollinger(s2 Series, n int, k float64) Series

RelativeBollinger returns a Bollinger band of the ratio of s to s2 over the trailing n right-aligned bars, k standard deviations from its mean, projected onto the price scale of s.

The ratio line s/s2, the line PriceRelative draws, is given the band that John Bollinger draws on a price, its n-bar mean plus k population standard deviations, and the band is multiplied by the current value of s2, which projects it back onto the chart of s: a relative-strength band drawn in the price units of s, so that a stop or a target on s can be read from it. With k = 0 it is the projected mean, the fair value of s implied by the ratio’s average, on windows whose standard deviation is finite. Each value is s2 times float64(mu + float64(sigmak)), where mu and sigma are the ratio’s Average(n) and StdDev(n), with sigmak rounded before the sum and the sum rounded before the product: the rounding boundaries of Bollinger, so that no multiply-add can fuse. Stéphane Reverre’s ratio mispricing (Stocks & Commodities, March 2001) is s less this mean, standardised over thirty days, and John J. Murphy (Intermarket Technical Analysis, 1991) applies support and resistance levels, trendlines and trading bands to ratio lines.

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, so a threshold test must be masked before the first valid index. The result is not start-invariant: Average and StdDev carry their rolling state from the first paired bar, so the same window can differ in its last bits between two starts.

The result is in the price units of s, and k is in population standard deviations of the ratio. The valid data are finite values of s and finite, non-zero values of s2 whose quotient s/s2 is finite, on every bar of the window; the method is meant for positive prices, and a negative s2 is handled as below.

For a negative current s2 the multiplication reverses the order and the band is not swapped, so a positive k gives the band below the projected mean. A negative s2 flips the direction of the ratio line; for zero-crossing instruments use the difference-based methods (DeltaCorrelation, DeltaBeta, RegressionSpread) instead. A zero or non-finite value of s2, a non-finite value of s, or a quotient s/s2 that overflows makes that bar’s ratio invalid, and every window containing it gives NaN, in the mean and the standard deviation alike; later windows recover. A zero s with a valid s2 gives a valid ratio of 0. Invalid parameters (no overlap, n less than two, n greater than the overlap, or a non-finite k) give a fresh series of zeros of length len(s). Products of large ratios and large values of s2 can overflow to ±Inf; a window whose standard deviation overflows gives an infinite band for a non-zero k, and NaN for k = 0 or where the mean overflows too, bar for bar as Bollinger does.

In trading, s above the projected mean is rich relative to s2 and s below it cheap, by the distance between them in the units of s, and bands at plus and minus k standard deviations give entry and exit levels on the chart of s, in the manner of the bands Ernest Chan (Quantitative Trading, 2nd edition, 2021, Example 7.1) trades one leg against. Causality: the band at a bar uses that bar’s own ratio and that bar’s s2, so it is known only at that bar’s close; an order resting for the next bar must use this bar’s band, stale by one bar of s2’s move, and using the next bar’s s2 in a backtest is look-ahead.

Deviations from the published methods: the band is computed on the ratio and projected onto s by the current s2, rather than drawn on s or on the ratio chart; the standard deviation is the population one, as in Bollinger; for a negative s2 the bands are not swapped; Chan’s bands are an exponential moving average and variance of a difference spread, where this method takes the n-bar simple mean and population standard deviation of the ratio; and Reverre’s mispricing is not computed here but is a composition, s less the band with k = 0, standardised over thirty days, listed under “Compositions”.

Compositions that start from it are listed under “Compositions” in the “Relative: Channels” category.

Verification: two independent references, one in Python using exact rational arithmetic and one in Julia, agree with the result on every bar to within rounding, including the NaN footprint and the band below the projected mean for a negative s2 and a positive k; with s2 all ones of the length of s, on finite s, the result equals s.Bollinger(n, k) bit for bit from index n-1 on, on arm64 as well; and on any pair it equals b.Mul(r.Bollinger(n, k)) bit for bit, where r is the ratio line and b is s2 on the overlap, which checks that the rounding boundaries are kept.

RelativeChannelLower

func (s Series) RelativeChannelLower(s2 Series, n int) Series

RelativeChannelLower returns the lower edge of the channel of the ratio of s to s2 over the trailing n right-aligned bars, projected onto the price scale of s by the current value of s2.

The ratio line s/s2, the line PriceRelative draws, has a channel whose upper edge is the highest ratio of the trailing n bars and whose lower edge is the lowest; multiplying the lower edge by the current value of s2 projects it back onto the chart of s, giving the lower edge of a relative-strength channel drawn in the price units of s, so that a stop or a target on s can be read from it. RelativeChannelUpper gives the upper edge. John J. Murphy (Intermarket Technical Analysis, 1991) applies support and resistance levels, trendlines and trading bands to ratio lines, and Ernest Chan (Quantitative Trading, 2nd edition, 2021, Example 7.1) trades one leg of a pair against bands derived from a relationship between the legs (a difference spread there).

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, so a threshold test must be masked before the first valid index. The extremes are window-local, so the result is bitwise start-invariant.

The result is in the price units of s. The valid data are finite values of s and finite, non-zero values of s2 whose quotient s/s2 is finite, on every bar of the window; the method is meant for positive prices, and a negative s2 is handled as below.

For a positive current s2 the lower edge is s2 times the lowest ratio of the window. For a negative current s2 the multiplication reverses the order, so the edges swap: the lower edge is s2 times the highest ratio, and Lower <= s <= Upper still holds. A negative s2 flips the direction of the ratio line; for zero-crossing instruments use the difference-based methods (DeltaCorrelation, DeltaBeta, RegressionSpread) instead. The containment rule makes s sit exactly on the edge its ratio touches: on a bar whose ratio equals the extreme the lower edge is projected from, the edge is s itself, because s2 times s/s2 differs from s by one ulp on about one bar in ten, which can leave s one ulp outside its own channel. A zero or non-finite value of s2, a non-finite value of s, or a quotient s/s2 that overflows makes that bar’s ratio invalid, and every window containing it gives NaN; later windows recover. A zero s with a valid s2 gives a valid ratio of 0. Invalid parameters (no overlap, n less than one, or n greater than the overlap) give a fresh series of zeros of length len(s). Products of large ratios and large values of s2 can overflow to ±Inf.

In trading, the lower edge is the level on the chart of s at which s is at its weakest relative to s2 over the window: a stop for a long position in s or a target for a short one. Causality: the edge at a bar includes that bar’s own ratio, so s can touch it but never close below it, and a relative breakdown compares s with the edge of the previous n bars. For a positive s2, the level s must fall below at the next bar to make a new n-bar relative low is s2’s next price times this bar’s lowest ratio, which is unknown at this bar’s close; a resting order must use this bar’s s2 (stale by one bar of s2’s move), and using the next bar’s s2 in a backtest is look-ahead. For a positive s2 the breakdown levels are a composition, b.Mul(r.Lowest(n).Shift(1)), where r is the ratio line and b is s2 on the overlap, valid from overlap index n; for a negative s2 they are b.Mul(r.Highest(n).Shift(1)).

Deviations from the published methods: Murphy draws his levels and trendlines by eye on the ratio chart, and Chan’s bands are an exponential moving average and variance of a difference spread, whereas this method takes the exact n-bar extremes of the ratio, projects them onto s by the current s2, swaps the edges for a negative s2 and applies the containment rule.

Verification: two independent references, one in Python using exact rational arithmetic and one in Julia as an alternative oracle, compute the channel exactly, including the containment rule and the swap for a negative s2, and agree with the result on every bar; the exact containment property, Lower <= s <= Upper with s on the edge its ratio touches, checked over tens of thousands of random-walk bars for either sign of s2, covers the rule, which no tolerance can see; and with s2 all ones of the length of s, on finite s, the result equals s.Lowest(n) bit for bit from index n-1 on, apart from the sign of a zero result: where the window holds both +0 and -0, the containment rule returns s’s own zero.

RelativeChannelUpper

func (s Series) RelativeChannelUpper(s2 Series, n int) Series

RelativeChannelUpper returns the upper edge of the channel of the ratio of s to s2 over the trailing n right-aligned bars, projected onto the price scale of s by the current value of s2.

The ratio line s/s2, the line PriceRelative draws, has a channel whose upper edge is the highest ratio of the trailing n bars and whose lower edge is the lowest; multiplying the upper edge by the current value of s2 projects it back onto the chart of s, giving the upper edge of a relative-strength channel drawn in the price units of s, so that a stop or a target on s can be read from it. RelativeChannelLower gives the lower edge. John J. Murphy (Intermarket Technical Analysis, 1991) applies support and resistance levels, trendlines and trading bands to ratio lines, and Ernest Chan (Quantitative Trading, 2nd edition, 2021, Example 7.1) trades one leg of a pair against bands derived from a relationship between the legs (a difference spread there).

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, so a threshold test must be masked before the first valid index. The extremes are window-local, so the result is bitwise start-invariant.

The result is in the price units of s. The valid data are finite values of s and finite, non-zero values of s2 whose quotient s/s2 is finite, on every bar of the window; the method is meant for positive prices, and a negative s2 is handled as below.

For a positive current s2 the upper edge is s2 times the highest ratio of the window. For a negative current s2 the multiplication reverses the order, so the edges swap: the upper edge is s2 times the lowest ratio, and Lower <= s <= Upper still holds. A negative s2 flips the direction of the ratio line; for zero-crossing instruments use the difference-based methods (DeltaCorrelation, DeltaBeta, RegressionSpread) instead. The containment rule makes s sit exactly on the edge its ratio touches: on a bar whose ratio equals the extreme the upper edge is projected from, the edge is s itself, because s2 times s/s2 differs from s by one ulp on about one bar in ten, which can leave s one ulp outside its own channel. A zero or non-finite value of s2, a non-finite value of s, or a quotient s/s2 that overflows makes that bar’s ratio invalid, and every window containing it gives NaN; later windows recover. A zero s with a valid s2 gives a valid ratio of 0. Invalid parameters (no overlap, n less than one, or n greater than the overlap) give a fresh series of zeros of length len(s). Products of large ratios and large values of s2 can overflow to ±Inf.

In trading, the upper edge is the level on the chart of s at which s is at its strongest relative to s2 over the window: a target for a long position in s or a stop for a short one. Causality: the edge at a bar includes that bar’s own ratio, so s can touch it but never close above it, and a relative breakout compares s with the edge of the previous n bars. For a positive s2, the level s must exceed at the next bar to make a new n-bar relative high is s2’s next price times this bar’s highest ratio, which is unknown at this bar’s close; a resting order must use this bar’s s2 (stale by one bar of s2’s move), and using the next bar’s s2 in a backtest is look-ahead. For a positive s2 the breakout levels are a composition, b.Mul(r.Highest(n).Shift(1)), where r is the ratio line and b is s2 on the overlap; from overlap index n on, s exceeds them exactly when r.Breakout(n, 1) is +1, except on near-tie bars. For a negative s2 the order reverses: the level is b.Mul(r.Lowest(n).Shift(1)), and s exceeds it exactly when r.Breakout(n, 1) is -1.

Deviations from the published methods: Murphy draws his levels and trendlines by eye on the ratio chart, and Chan’s bands are an exponential moving average and variance of a difference spread, whereas this method takes the exact n-bar extremes of the ratio, projects them onto s by the current s2, swaps the edges for a negative s2 and applies the containment rule.

Verification: two independent references, one in Python using exact rational arithmetic and one in Julia as an alternative oracle, compute the channel exactly, including the containment rule and the swap for a negative s2, and agree with the result on every bar; the exact containment property, Lower <= s <= Upper with s on the edge its ratio touches, checked over tens of thousands of random-walk bars for either sign of s2, covers the rule, which no tolerance can see; and with s2 all ones of the length of s, on finite s, the result equals s.Highest(n) bit for bit from index n-1 on, apart from the sign of a zero result: where the window holds both +0 and -0, the containment rule returns s’s own zero.

SpreadBollingerX

func (s Series) SpreadBollingerX(s2 Series, n int, h, k float64) Series

SpreadBollingerX returns an exponentially weighted Bollinger band, k standard deviations from the mean, of the spread s - h*s2 over an n-bar smoothing length.

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.

On the paired bars, counted by t from 0 at the first, the spread is d = s - hs2, the product hs2 rounded before the subtraction. Its mean E is the XAverage of d over n bars, seeded at t = n-1 with the simple average of the first n spreads, and its variance V is seeded at t = n-1 with the population variance of the first n spreads. From t = n both follow the recursion with alpha = 2/(n+1):

E(t) = alpha*d(t) + (1 - alpha)*E(t-1)
V(t) = alpha*(d(t) - E(t))^2 + (1 - alpha)*V(t-1)

The band is E + k*sqrt(V), at index off + t of the result for every t from n-1. It is in the price units of s; h is the hedge ratio in units of s per unit of s2, and k is in exponentially weighted standard deviations of the spread, so a positive k gives the upper band, a negative k gives the lower band and k = 0 gives the mean E wherever V is finite.

The method is recursive: E and V start at the first paired bar and each later value depends on every bar before it, so the result is not start-invariant, and dropping bars from the front of the inputs changes later outputs. A NaN or an infinity is permanent: one in s or s2 at paired bar p, or a spread d that overflows to an infinity at p, makes the result NaN at every index off + t with t >= max(p, n-1).

Bars before the first valid index hold the warm-up value 0, which is not a band value, and reading it as one gives false signals: a rule “spread above the upper band” compares a positive spread with a zero band on every warm-up bar and so fires on each of them, as “spread below the lower band” does for a negative spread. Trim the warm-up with Window or mask it with SetN before such rules.

If the overlap is empty, n < 2, n exceeds the overlap, or h or k is NaN or an infinity, the result is all zeros. A spread whose squared deviation from E overflows, or whose first n squared deviations overflow in sum, makes V infinite for good: where E is finite, the band is then +Inf for a positive k, -Inf for a negative k and NaN for k = 0. Spreads of one sign near MaxFloat64/n overflow the mean’s seed instead (XAverage sums before it divides): E is then infinite, the band equals E at the first valid index for every k, and afterwards it keeps that infinity for a k of the same sign and is NaN otherwise. Both are accepted, not special-cased. The inputs are never modified, and the result is always fresh.

The source is Ernest Chan, Quantitative Trading (2nd edition, 2021), Example 7.1 and its endnote. His spread is GLD - GDX*GDX_weight on 1-minute GLD/GDX bars, his z-score is (Spread - Spread_EMA)/sqrt(Spread_VAR), and, with L his lookback, the endnote gives:

Spread_EMA(0) = Spread(0)
Spread_EMA(t+1) = (2/L)*Spread(t+1) + (1 - 2/L)*Spread_EMA(t)
Spread_VAR(1) = (Spread(1) - Spread(0))^2
Spread_VAR(t+1) = (2/L)*(Spread(t+1) - Spread_EMA(t+1))^2 + (1 - 2/L)*Spread_VAR(t)

He tries lookbacks of 30 to 720 bars, entries at 0.2 to 2.5 standard deviations and exits at -0.6 times the entry. The library departs from him in its seeds: it seeds the mean with the simple average of the first n spreads and the variance with their population variance, both at bar n-1, where Chan seeds them at bars 0 and 1, so the two agree only after the seeds decay. Chan’s 2/L equals the library’s 2/(n+1) when n = L - 1.

The z-score Chan trades is (d - E)/sqrt(V) (undefined where V is 0, as on a constant spread), which in exact arithmetic is (spread - band(k=0))/(band(k=1) - band(k=0)), with the spread s - h*s2 on the paired bars. The simple (windowed) spread Bollinger, from a simple average and a windowed standard deviation of the spread, is the composition a.Sub(b.Mul(h)).Bollinger(n, k), where a, b := s.Overlap(s2) are the overlap copies of s and s2; it is listed under “Compositions” in the “Relative: Channels” category. BollingerX of the spread is a different band again: XAverage plus a windowed StdDev.

Verification: the method has no exact identity with an existing method, so two references form its alternative oracle: a Python reference that carries the spreads and seeds as exact fractions and the recursions and square root at 120 decimal digits, rounding once per output, with the library’s seeds, and a Julia reference that translates Chan’s endnote, in literal mode (his seeds, compared after they decay) and in library mode (the library’s seeds), which agrees with the Python reference after rounding. The tests also check identities that follow from the definition. For finite inputs, h = 0 makes the mean E the XAverage of the paired bars of s bit for bit, apart from the sign of a zero (a warm-up bar of -0 in s passes through as +0). For a finite s, s2 == s with h = 1 gives a spread of exactly zero and so a result of exactly +0 from the first valid index. By the negation identity, negating s, s2 and k negates the result bit for bit, apart from the sign of a zero, and by the power-of-two identity, scaling s and s2 by a power of two (without overflow or underflow) scales the result by exactly that power.

Stress

func (s Series) Stress(s2 Series, n int) Series

Stress returns Kaufman’s stress indicator, the position within its own trailing n-bar range of the difference between the n-bar channel positions of s and s2, on a 0 to 100 scale.

Let a and b be the right-aligned overlap views of s and s2, of length m. This is the close-only form, in two stages. The first places each leg in its own trailing n-bar channel of closes: the first leg’s position, k1 = a.ChannelPosition(n, 1), is ((a - LL)/(HH - LL))*100, with HH and LL the highest and lowest of the trailing n closes of a, and the second leg’s position, k2 = b.ChannelPosition(n, 1), is the same for b. Their difference D = k1 - k2, Kaufman’s D, is trimmed of the first stage’s n - 1 warm-up bars, so that no warm-up zero enters a window extreme of the second stage, which returns the position of D in its own trailing n-bar range, D.ChannelPosition(n, 1): 100 when D is at its n-bar high and 0 when it is at its n-bar low, unless the window is flat. D keeps Kaufman’s sign, so a high D means that s is high in its own range relative to s2.

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. With off = len(s) - m, the first valid index is off + 2n - 2 and the minimum overlap is 2n - 1 bars, n for the first position of each leg and n - 1 more for the first full window of D; every earlier output is exactly +0. An empty overlap, n < 2, n > m or n - 1 > m - n (an overlap shorter than 2n - 1 bars, tested without forming a sum, so that n = math.MaxInt is safe) returns a fresh all-zero series of length len(s). Validation takes O(1) and precedes any allocation other than the result, so every huge or negative n returns zeros in O(len(s)) time without allocating anything proportional to n.

Each output, at overlap bar j, depends only on the overlap bars j - 2n + 2 .. j, so the results are window-local and bitwise start-invariant: dropping leading values of either input leaves every output whose window survives unchanged, bit for bit. The result is a fresh Series that never aliases or modifies s or s2, concurrent calls on shared inputs are safe, a valid call makes a fixed number of allocations, independent of the input length, and no input panics.

On finite inputs whose n-bar ranges stay finite, the result lies exactly in [0, 100], since both stages place a series in its own channel. On such inputs, a flat leg window, n equal closes, gives that leg’s position exactly 50, ChannelPosition’s flat value, so D follows the other leg alone; a flat window of D gives exactly 50; and s2 == s gives D = 0 everywhere and so exactly 50 at every valid bar. Swapping the legs negates D, so at every valid bar of the overlap s2.Stress(s, n) is 100 - s.Stress(s2, n) to within 1e-12 but not bit for bit, because (x - LL)/(HH - LL) and (HH - x)/(HH - LL) round differently; both are exactly 50 on flat windows. Scaling either input by a power of two leaves the result unchanged, bit for bit, while its non-zero values stay normal and its n-bar ranges stay finite. A non-finite value (NaN, +Inf or -Inf) at overlap index p of either input makes that leg’s position NaN on max(p, n-1)..min(p+n-1, m-1), so D is NaN there and the output is NaN on max(p, 2n-2)..min(p+2n-2, m-1), all overlap indices; later outputs recover. A leg window of finite values whose range overflows float64 makes that leg’s position NaN at that bar, as in ChannelPosition, and so the output NaN wherever that bar is in the window of D.

The method deviates from Kaufman’s published code, his EasyLanguage function PJK_Stress, in three ways. A flat leg window gives that leg’s position 50, ChannelPosition’s flat value, so that D follows the other leg, where his code returns 50 for that bar and carries the previous difference into later windows of D; a carried value depends on the start of the data and would break start-invariance. The first stage works in percentages, 0 to 100, as in his printed formulas, instead of his code’s fractional stochastics, 0 to 1, so D runs from -100 to 100 instead of -1 to 1; the scale cancels in the second stage only in exact arithmetic, and both forms return 0 to 100. Finally, each leg’s channel is formed from its closes instead of its highs and lows; his code’s form is the high-low one, which is not provided.

With s the stock and s2 the index, Kaufman’s reading is that “the stock is overbought relative to the index when D is high, and it’s oversold when D is low”, and he sets thresholds of 90 and 10 on the stress value. A traditional pairs trade buys the stock and sells the index below 10, and does the reverse above 90. His long-bias version buys only the stock below 10 and exits above 50, holding for about eight days, and hedges by selling the index when its 60-day moving average turns down, in a size equal to the stock position times the ratio of the stock’s to the index’s 60-day standard deviation of returns, times a hedge ratio of 0.5. His Pairs1 inputs include 60 for n, 10 and 50 for entry and exit, a $3 minimum price, a 10% stop, 60 for the hedge period and 0.50 for the hedge ratio, and declare a correlation filter of 0.20 that the printed code never uses; his text puts the crisis stop at 20%. The hedge and the positions are consumer logic. Kaufman sets n at 60 (“set at 60 to maximize the returns”), so the minimum overlap is 119 bars.

Each stage is ChannelPosition(n, 1), and on the overlap views the result equals, bit for bit and placed at offset n - 1, the composition a.ChannelPosition(n, 1).Sub(b.ChannelPosition(n, 1)).Window(-(n-1)).ChannelPosition(n, 1). The per-leg channel-position divergence b.ChannelPosition(n2, 1).Sub(a.ChannelPosition(n1, 1)) has the opposite sign: with n1 = n2 = n it is -D, the first stage negated and without the second. Across bar intervals Stress needs a common clock, since it shares one n between the legs and its second stage pairs the two legs’ positions bar by bar.

Source: Perry J. Kaufman, “Timing The Market With Pairs Logic”, Stocks & Commodities 32:3 (March 2014), formulas on page 11 and the EasyLanguage function PJK_Stress on page 13; Kaufman introduced the indicator in his book Alpha Trading (Wiley 2011).

The warm-up 0 is below Kaufman’s buy threshold of 10, so an untrimmed rule buys on every warm-up bar; trim to the first valid index, off + 2n - 2, before applying one.

Verification: Two independent references agree with this method, a Python reference in exact rational arithmetic and a Julia reference that translates Kaufman’s formulas and his EasyLanguage function, the Julia one in a library mode with this method’s rules and in a literal mode with his fractional stochastics, his 50 on a flat leg and his carry, taking each leg’s closes as its highs and lows; where no leg window in the second stage’s window is flat and the difference’s range is at least one point, the literal mode agrees with this method to within rounding. On finite inputs whose n-bar ranges stay finite, on windows where neither leg nor the difference is flat, the result equals the Highest/Lowest composition X.Sub(X.Lowest(n)).Div(X.Highest(n).Sub(X.Lowest(n))).Mul(100), applied to each leg and then to k1 - k2 trimmed by n - 1, bit for bit, and swapping the legs gives 100 - Stress to within 1e-12.

Compositions

First valid index of the relative stages

The table under “Relative: Alignment” lists the older stages. The relative stages used here start as follows, counted from the start of the series they are applied to. The two-series stages (PriceRelative, LogRatio, Correlation and ReturnCorrelation) are applied to the Overlap views a, b, so for them this is the start of the overlap; on inputs of unequal length the absolute index is off plus the value, with zeros before the overlap.

StageFirst valid index fBefore f
PriceRelative(b, k)k - 1 (0 for k <= 1)zeros
LogRatio(b)0—
ChannelPosition(n, smooth)n + max(smooth, 1) - 2zeros
Breakout(n, lag)n + lag - 1zeros
Shift(k)kzeros
DonchianMiddle(n)n - 1the middle of the bars so far
BollingerX(n, k)n - 1the input, passed through; its average then seeds on the first n inputs, so run it only on trimmed input
Correlation(b, n)n - 1zeros
ReturnCorrelation(b, n, k)n + k - 1zeros

A trim by 0 bars is left out: Window(0) returns an empty series, so a chain whose stage starts at 0 (for example n = 1, k <= 1, or smooth 1 with n 1) skips that Window. Pinned by TestRecipeStageFirstValidT2.

The IMO of the log ratio

The Intermarket Momentum Oscillator, which Markos Katsanos (Intermarket Trading Strategies, 2008, Section 9.6 and Fig. 9.1) applies to his intermarket divergences and to the relative-strength line, here applied to the log ratio of two series: where the log ratio sits in its own n-bar channel, 0 at the channel’s low and 100 at its high, slowed over smooth bars by dividing the summed distances above the low by the summed ranges, as Katsanos does (a ratio of averages, not an average of the positions). The log ratio makes it symmetric in the legs: swapping s and s2 gives 100 less the reading, where the ratio line would not.

a, b := s.Overlap(s2)
f := n + max(smooth, 1) - 2
imo := a.LogRatio(b).ChannelPosition(n, smooth).Window(-f) // no trim when f == 0
  • First valid index: n + max(smooth, 1) - 2, counted from the start of the overlap (a smooth of 1 or less is no smoothing). Minimum overlap: n + max(smooth, 1) - 1 bars.
  • Domain: finite, positive prices. A non-positive or non-finite price makes that bar’s log ratio NaN, and every reading whose window contains it NaN; later readings recover. A flat channel reads 50.
  • Anchors: with smooth = 1, on windows that are not flat, the reading is L.Sub(L.Lowest(n)).Div(L.Highest(n).Sub(L.Lowest(n))).Mul(100) with L := a.LogRatio(b) (E, ChannelPosition’s Highest/Lowest identity); swapping the legs gives 100 less the reading (T(1e-12) for smooth up to 5; the rounding of the smoothed sums grows with smooth).
  • Pinned by TestRecipeLogRatioIMO; runnable example ExampleSeries_LogRatio_imo.

Katsanos’s IMO of the relative-strength line

Katsanos’s relative-strength oscillator: the IMO of the 3-bar exponential average of the ratio line, over 300 bars (Chapter 11, with the XAU as the comparison: code RS:=MOV((C/SEC2),3,E) with IM over 300 bars). The smoothed line is trimmed before the channel, so no warm-up zero enters it.

a, b := s.Overlap(s2)
rs := a.PriceRelative(b, 3).Window(-2)
imo := rs.ChannelPosition(300, 3).Window(-301)
  • First valid index: 303, counted from the start of the overlap (2 for the average and 301 for the channel). Minimum overlap: 304 bars. For other parameters, max(k, 1) - 1 + n + max(smooth, 1) - 2 with k the average’s length (the average’s trim is left out when k <= 1).
  • Seed: PriceRelative seeds its average with the mean of the first 3 ratios. The first-value seed (MetaStock’s, per a forum formula; uncertain, because AmiBroker’s author says that MetaStock seeds with the simple average, as PriceRelative does) differs from it by an amount that halves each bar: on this project’s test pairs it moves only the first dozen valid readings at n = 300 by more than 1e-4 points, and none after the sixteenth at all. It lasts longer where the first smoothed values stay the channel’s extreme, as on a nearly flat ratio after a large move in the seed’s bars, where it can still move a reading by points well after the sixteenth.
  • Domain: as PriceRelative: NaN at a zero or non-finite s2 or a non-finite s, and because the average never recovers from a NaN, every later reading is NaN. A negative s2 flips the ratio’s direction.
  • Signal line and rules (applied by the consumer): in Chapter 9 (the text and the Fig. 9.1 caption; no listing has a signal line) the signal line is his “4-day moving average”, taken here as the simple imo.Average(4) (the book does not say which kind; his stochastic signal lines are simple), valid 3 bars after imo’s first. There, on a 200-bar oscillator of the 3-bar average of gold against the dollar index, he buys when the oscillator crosses above its signal line below 50 and sells when it crosses below it above 80. His Chapter 11 countertrend rules (Appendix A code) keep that orientation with more extreme levels (the inversion he notes in Section 11.4 is against his divergence indicators, where a high reading is a buy): buy when imo.Lowest(4) is below 20 and the oscillator turns up by 10% from it, confirmed by the base’s 5-bar stochastic with 3-bar slowing rising above its 3-bar average (LLV(IM,4)<20 AND IM>REF(IM,-1) AND IM>(1+10/100)*LLV(IM,4) AND STOCH(5,3)>MOV(STOCH(5,3),3,S), the text’s “below 20 and then rose above 22”), and short the mirror above 85, with the stochastic below its average. His Chapter 15 sector filter (Appendix A code) compares averages of the smoothed line itself, rs.Average(50).GT(rs.Average(200)), read from bar 199 of rs.
  • Anchors: with s2 all ones the oscillator equals the same chain on a.XAverage(3).Window(-2) (E; this repeats PriceRelative’s all-ones anchor and does not test ChannelPosition itself); the chain on the Overlap views equals the chain on inputs trimmed to the overlap (E).
  • Pinned by TestRecipeRSLineIMO.

Katsanos’s Chapter 16 filter

The relative-strength breakout of Katsanos’s dynamic asset allocation (Chapter 16): the 150-bar exponential average of the ratio line against its 250-bar high of 20 bars ago, RS > Ref(HHV(RS,250),-20) in his code. A reading of +1 is a new relative high by that rule, -1 a new relative low, and 0 neither.

a, b := s.Overlap(s2)
rs := a.PriceRelative(b, 150).Window(-149)
filter := rs.Breakout(250, 20).Window(-269)
  • First valid index: 418, counted from the start of the overlap (149 for the average and 269 for the breakout). Minimum overlap: 419 bars, plus about 600 to 750 bars of burn-in for the 150-bar average: the first-value seed moves the line by about 5e-4 relative at bar 250, which can flip a reading near a tie.
  • Entry and exit (Katsanos’s code, applied by the consumer): enter when the filter is +1 and rs.GT(rs.XAverage(120)), s.XAverage(25) rising over 2 bars, rs.Slope(20) positive, the 130-bar correlation of the legs below 0.5 (see “Relative: Correlation”) and the 100-bar stochastic with 3-bar slowing (Stoch(100,3), on the security’s highs, lows and closes) below 80 (Stochastic has no slowing); exit when rs crosses below rs.XAverage(90) or rs.RateOfChange(3) is below -0.5.
  • Domain: as the IMO of the relative-strength line.
  • Anchors: with lag = 1 and a finite channel range, +1 implies that rs.ChannelPosition(n + 1, 1) is 100 (E, as Breakout documents); the chain on the Overlap views equals the chain on trimmed inputs (E).
  • Pinned by TestRecipeRSLineBreakout.

The ratio Donchian channel

The Donchian channel of the ratio line: its highest, lowest and middle values over the trailing n bars. John J. Murphy (Intermarket Technical Analysis, 1991) draws support and resistance on ratio lines, and Rob Friesen’s multi-year breakouts of a ratio line (Stocks & Commodities, March 2021) are breaks of its upper channel; Breakout(n, 1) on the trimmed line flags them (Katsanos’s Chapter 16 filter, under “Compositions” in this category, is a lagged form). The channel is in ratio units; RelativeChannelUpper and RelativeChannelLower project it onto the price scale of s.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
upper := r.Highest(n).Window(-(n - 1))
lower := r.Lowest(n).Window(-(n - 1))
middle := r.DonchianMiddle(n).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of the overlap (with n = 1 the trim is left out, the upper and lower lines equal r, and the middle equals r where |r| is at most MaxFloat64/2: DonchianMiddle adds the extremes before halving, so a larger ratio gives an infinite middle). Minimum overlap: n bars.
  • Domain: the ratio line’s (signed ratio): NaN at a zero or non-finite s2, a non-finite s or an overflowing quotient, and every channel whose window contains it NaN; later windows recover. A negative s2 flips the ratio’s direction, so the upper channel is then the leg’s weakest point.
  • Anchors: with s2 all ones, on finite s, the three equal s.Highest(n), s.Lowest(n) and s.DonchianMiddle(n) from index n - 1 (E); s2 == s gives exactly 1 on non-zero s (E); scaling s2 by 2^k scales them by exactly 2^-k when no ratio or channel overflows or rounds in the subnormal range (E); on positive ratios Log commutes with Highest and Lowest (E); a bitwise-flat ratio gives three equal channels (E) where its magnitude is at most MaxFloat64/2.
  • Pinned by TestRecipeRatioDonchian; runnable example ExampleSeries_PriceRelative_ratioDonchian.

The log-ratio channel and band

The same channel, and John Bollinger’s band, on the log ratio ln s - ln s2, which treats the legs symmetrically: swapping them negates every level. A level x of the log ratio projects onto the price scale of s as b.Mul(x.Exp()).

a, b := s.Overlap(s2)
L := a.LogRatio(b)
upper := L.Highest(n).Window(-(n - 1))
lower := L.Lowest(n).Window(-(n - 1))
band := L.Bollinger(n, k).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of the overlap (with n = 1 the trim is left out). Minimum overlap: n bars.
  • Domain: finite, positive prices; a non-positive or non-finite price gives NaN, and every channel or band whose window contains it NaN.
  • Anchors: a.LogRatio(b).Highest(n) == -(b.LogRatio(a).Lowest(n)) and Bollinger(-L, n, -k) == -Bollinger(L, n, k) (E); with s2 all ones, on finite, normal, positive s, s.Log().Highest(n) and s.Log().Bollinger(n, k) (E); s2 == s gives 0 (E); on prices within 1e±150 the log of the ratio’s Highest agrees with L.Highest(n) to T(1e-13) absolute. Outside that domain the error grows with |ln price|, and the log of a quotient that underflows or overflows does not agree at all, which is why LogRatio takes ln s - ln s2.
  • Pinned by TestRecipeLogRatioChannel.

The ratio Bollinger band

John Bollinger’s band on the ratio line, k population standard deviations from its n-bar mean, and the variant whose centre is the exponential average (BollingerX). The band is in ratio units; RelativeBollinger projects it onto the price scale of s. A re-entry of the ratio into the band (+1 when the ratio crosses from below the lower band to at or above it, -1 from above the upper band to at or below it) compares equal-length trimmed views: with f := n - 1, rt := r.Window(-f), lo := r.Bollinger(n, -k).Window(-f) and hi := r.Bollinger(n, k).Window(-f), rt.CrossesAboveS(lo) marks the +1 events and rt.CrossesBelowS(hi) the -1 events. CrossesAboveS tests “at or below, then above”, so the composition differs from that rule only at ties. After a bitwise-flat stretch the ratio can lie strictly outside its own band (see the anchors), so the next move can fire both the composition and the rule.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
band := r.Bollinger(n, k).Window(-(n - 1))
xband := r.BollingerX(n, k).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of the overlap. Minimum overlap: n bars. BollingerX seeds its average with the mean of the first n ratios of the overlap, so it must run on the overlap views, never on a line with a zero prefix.
  • Domain: the ratio line’s; the standard deviation is the population one, as in Bollinger.
  • Anchors: with s2 all ones, s.Bollinger(n, k) and s.BollingerX(n, k) (E); k = 0 gives r.Average(n) on windows whose standard deviation is finite (E); scaling s2 by 2^j scales the bands by exactly 2^-j when no ratio, mean or band overflows or rounds in the subnormal range (E); on windows whose sum does not overflow, a bitwise-flat ratio gives a standard deviation of exactly 0, so each band equals its centre, r.Average(n) for Bollinger and r.XAverage(n) for BollingerX (E). The centre equals the ratio only when the averaging is exact, as for a power-of-two ratio; on a ratio such as 0.1 it can sit an ulp or more away, so on a flat stretch the ratio can lie strictly outside its own band.
  • Pinned by TestRecipeRatioBollinger.

Reverre’s ratio mispricing

Stéphane Reverre (“Pairs Trading”, Stocks & Commodities, March 2001) prices a stock against its partner through the 30-day mean of their ratio: the mispricing Delta is the stock’s price less the partner’s price times that mean, and his signal, Delta_norm, is Delta standardised over its own last 30 days. He sells one share of the stock and buys the mean ratio in shares of the partner (0.7698 share of Royal Dutch per share of Shell on 17 July 1998; he notes that the position is not hedged in dollars) when Delta_norm is above 1.5, does the opposite below -1.5, and exits at the next close:

a, b := s.Overlap(s2)
d := a.Sub(a.RelativeBollinger(b, 30, 0))
z := d.Window(-29).Standardise(30)
valid := z.Window(-29) // the recipe's valid bars
  • First valid index: 58, counted from the start of the overlap: RelativeBollinger(b, 30, 0) is valid from 29, and Standardise(30) on the trimmed d from 29 more. z has length m - 29 for an overlap of m bars; its first 29 elements are Standardise’s warm-up zeros, so read valid.
  • Minimum overlap: 59 bars, which give one valid bar.
  • Domain: positive prices. A non-finite price of either leg, a zero partner price, or a quotient that overflows makes that bar’s ratio invalid and every window containing it NaN: 30 bars of the mean and of d, and 29 more in the z-score, 59 bars in all. A zero price of the stock gives a valid ratio of 0. Standardise is the population z-score; Reverre does not state his standard deviation’s convention, and the n-1 convention of Ernest Chan’s MATLAB and R listings reads z*sqrt((n-1)/n) against the same thresholds.
  • Anchors: s2 == s gives exactly 0, because the ratio is exactly 1 and d is exactly 0 (tag E); s2 all ones on finite s whose 30-bar standard deviation does not overflow gives d == s.Sub(s.Average(30)) from bar 29, because RelativeBollinger(ones, 30, 0) equals s.Bollinger(30, 0), which is Average plus a zero term (tag E; where the standard deviation overflows, both are NaN); the chain on the Overlap results 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 the recipe in exact rational arithmetic.
  • Published figure: Reverre’s Figure 3 prints six days of Shell and Royal Dutch closes, 17 to 24 July 1998, with his column headed “MA30(Delta)”, Delta and Delta_norm (testdata/ref/inputs/tasc-reverre-19-3.csv). The implied ratio means (sc - Delta)/rd round to his printed column, 0.7698 to 0.7681, which shows that the column is the ratio’s 30-day mean and that Delta is in dollars (his sidebar words Delta as a difference of ratios). The 29 earlier closes are not printed, so the figure is a reading check, not a test of the recipe’s values.
  • Pinned by TestRecipeReverreMispricing; runnable example ExampleSeries_RelativeBollinger_reverre.

%b and bandwidth of the log ratio

John Bollinger’s %b and bandwidth (Bollinger on Bollinger Bands, 2001) applied to the log ratio. Markos Katsanos uses the band position to compare two markets instead (Intermarket Trading Strategies, 2008, section 9.2: each market’s 1 + %b at k = 2, his SEC1BOL and SEC2BOL in equations 9.1 to 9.5, enters his Bollinger divergence, equation 9.6). %b is written through the z-score, 0.5 + z/(2k), which equals the band form (x - lower)/(upper - lower) in exact arithmetic.

a, b := s.Overlap(s2)
L := a.LogRatio(b)
pctB := L.Standardise(n).Div(2 * k).Add(0.5).Window(-(n - 1))
bandwidth := L.Bollinger(n, k).Sub(L.Bollinger(n, -k)).Div(L.Average(n)).Window(-(n - 1))
  • First valid index: n - 1 for both, counted from the start of the overlap; element e is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2 and a finite k > 0; k <= 0 is invalid (the band collapses or inverts).
  • Range: in exact arithmetic |%b - 0.5| <= sqrt(n-1)/(2k) (Samuelson’s inequality); rounding can exceed it, slightly on ordinary windows and by about half the bound again on nearly flat ones. A window of equal log ratios gives exactly 0.5; the band form, written with Div, gives 0 there (plain division gives NaN), and agrees with this one to about 2e-16*|mean|/(k*sd) elsewhere, so within 1e-12 unless the window’s deviation is below about 1e-4 of its mean.
  • Bandwidth: (upper - lower)/average of the log ratio, which is 2k*sd/mean in exact arithmetic. The log ratio’s mean is near zero for legs of similar level, where the bandwidth explodes and changes sign (an exactly zero mean gives 0 by Div); read it on legs of clearly different level, or read the band width 2k*sd itself.
  • Uses: Gardner gates entries in one fund on a high-yield bond fund’s Bollinger position above 1 (Stocks & Commodities 27:4), a reading of a second market’s %b.
  • Anchors: at the last bar %b equals 0.5 + L.ZScore(n, L.Value()).Value()/(2*k) (E).
  • Pinned by TestRecipeT3RatioRecipes.

Relative drawdown and run-up

How far the ratio line stands below its n-bar high and above its n-bar low, in percent: a relative trailing stop exits a rotation holding after it gives back a set share of its relative performance, even while its price still rises. Katsanos’s exit is the qualitative form: relative strength falling below its moving average while the price still rises (section 9.1, page 123). John Murphy’s gasoline futures “outperformed the CRB Index by 11 percent in the previous 100 days but lost 10 percent from their January peak relative to the CRB Index” (Intermarket Technical Analysis, 1991, chapter 11, page 193).

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
drawdown := r.Div(r.Highest(n)).Sub(1).Mul(100).Window(-(n - 1))
runup := r.Div(r.Lowest(n)).Sub(1).Mul(100).Window(-(n - 1))
  • First valid index: n - 1 for both; element e is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2; at n = 1 the trim is Window(0), which returns an empty series.
  • Domain: on a positive ratio line the drawdown is at most 0 and the run-up at least 0; a negative ratio line inverts both, because the composition uses Div rather than the signed percent leg.
  • Anchors: s2 == s gives exactly 0 (E); with s2 all ones it is s.Div(s.Highest(n)).Sub(1).Mul(100), trimmed (E); the drawdown is exactly 0 where the ratio is its window’s high, that is where ChannelPosition(n, 1) is 100 on a non-flat window (a flat window gives a drawdown of 0 and a position of 50).
  • Pinned by TestRecipeT3RatioRecipes.

Relative drawdown in log form

The same distance below the high on the log ratio. A leg swap turns it into the negated log run-up, L - L.Lowest(n), exactly.

a, b := s.Overlap(s2)
L := a.LogRatio(b)
drawdown := L.Sub(L.Highest(n)).Window(-(n - 1))
  • First valid index: n - 1; element e is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2; at n = 1 the trim is Window(0), which returns an empty series.
  • Domain: both legs must be finite and positive; a bad price gives NaN on the n windows holding it.
  • Pinned by TestRecipeT3RatioRecipes.

Relative Ulcer index

The Ulcer index of the ratio line: the root mean square of its percentage falls below its running high within the window (Peter Martin and Byron McCann, 1989).

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
ulcer := r.Ulcer(n).Window(-(n - 1))
  • First valid index: n - 1; element e is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2; at n = 1 the trim is Window(0), which returns an empty series.
  • Domain: a NaN ratio resets the running high and drops out of the sum, so the Ulcer index of a window holding it is finite (the divisor stays n); screen the legs for invalid prices first.
  • Pinned by TestRecipeT3RatioRecipes.

Per-leg channel-position divergence

Each leg’s position in its own n-bar channel, and the second leg’s less the first’s: the bounded, continuous form of “each series in its own channel, then compared”. It is per leg, so each leg may have its own length (a daily leg against a weekly one, with calendar-matched lengths), and it is the Donchian analogue of the Bollinger divergence, with no assumption of normality. The sign is the Katsanos divergence family’s: positive means s has lagged s2.

a, b := s.Overlap(s2)
divergence := b.ChannelPosition(n2, sm).Sub(a.ChannelPosition(n1, sm)).Window(-(max(n1, n2) + sm - 2))
  • First valid index: max(n1, n2) + sm - 2, counted from the start of the overlap; element e is overlap bar max(n1, n2) + sm - 2 + e. Minimum overlap: max(n1, n2) + sm - 1 bars.
  • Parameters: n1, n2, sm >= 1 with max(n1, n2) + sm >= 3; a smaller sum makes the trim Window(0), which returns an empty series.
  • Range: within ±100; a flat leg window gives that leg 50, and a leg with n = 1 is always flat.
  • Published uses: Perry Kaufman’s crossover arbitrage between cash gold and Newmont reads the 20-day close-only stochastic difference on a ±1 scale, entering beyond ±0.7 and exiting on the sign change (Stocks & Commodities 42:2); with his sign (the first leg less the second, cash gold less Newmont) it is the negation of this recipe. Meyers’s ADRrs is the 10-day stochastic position of a 10-day exponential average of (A - D)/(A + D) less the DJIA’s (15:1, 15:8); Mason rescales two series to 0 to 100 by their whole-sample ranges and compares them (7:9), a look-ahead form; Katsanos compares the VIX’s and SPY’s stochastics over 10 and 25 bars (40:9), high-low stochastics with %K averaged over 3 bars, which this close-only recipe approximates; they agree only at sm = 1 on close-only data. Meyers documents a failure mode: in a sustained move one leg pins at its extreme first, the other follows, and the difference locks at zero, which delays signals (15:8).
  • Stress: Kaufman’s stress indicator applies the channel position once more to the per-leg difference, with the opposite sign (Stress).
  • Anchors: at sm = 1, on non-flat windows, the difference of the two Highest/Lowest compositions (E); s2 == s with n1 == n2 gives exactly 0 (E).
  • Pinned by TestRecipeT3ChannelPositionDivergence.

Per-leg channel-position divergence at “now”

The same reading at the last bar, each leg on its own whole history, so the legs may have any lengths and calendars.

divergence := s2.ChannelPosition(n2, sm).Value() - s.ChannelPosition(n1, sm).Value()
  • History: each leg needs n + sm - 1 bars of its own. The channel position sums each window afresh, so where the overlap holds max(n1, n2) + sm - 1 bars this is the last element of the series form above, bit for bit; it also reads a pair whose overlap is shorter, where the series form is empty.
  • Pinned by TestRecipeT3ChannelPositionDivergence.

Regression channel of the ratio line

The least-squares line of the ratio line over its last n bars, read at the current bar, and a band k residual standard deviations from it. With r := a.PriceRelative(b, 1), the line is r.LinRegValue(n), and the residual standard deviation of each window’s fit against time is the population standard deviation of r times sqrt(1 - R-squared), R-squared being the window’s coefficient of determination. Jon Andersen’s standard error bands (Stocks & Commodities 14:9) put lines two standard errors above and below the ending value of a 21-bar regression of price; Domenico D’Errico confirms reversals of relative strength with regression channels on ratio charts such as XLE over SPY (34:13); and John Murphy applies trendlines to ratio lines (Intermarket Technical Analysis, chapters 11 and 12). The recipe is the rolling form of such a line on the ratio.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
fit := r.LinRegValue(n)
width := r.StdDev(n).Mul(r.R2(n).Mul(-1).Add(1).Sqrt())
line := fit.Window(-(n - 1))
band := fit.Add(width.Mul(k)).Window(-(n - 1))
  • First valid index: n - 1 for the line and the band, counted from the start of the overlap; element e of each is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 3, since a 2-bar line passes through both points and leaves no residual; k finite and signed: k < 0 gives the lower band and k = 0 the line itself. Andersen uses n = 21 and k = 2, and suggests averaging the 8-, 13- and 21-bar versions.
  • Andersen’s form: the Excel sidebar to his article, by the editor Thom Hartle, computes the standard error with STEYX, whose divisor is n - 2, so that band is this one with k scaled by sqrt(n/(n - 2)); Andersen also smooths the line and the standard error with 3-bar simple averages before adding them, which the recipe does not. Gilbert Raff’s channel (Stocks & Commodities 9:10) fits a least-squares line to the prices between two swing points he chooses, with parallel lines at the distance of the price farthest from it.
  • Katsanos’s line: the relative-strength test for gold in Katsanos’s Intermarket Trading Strategies (Appendix A.2) buys when, among other conditions, its relative strength against the dollar index, a 3-bar exponential average of their ratio, has crossed above the 215-bar regression line of 15 bars earlier and stands more than 3% above it. With rs := a.PriceRelative(b, 3).Window(-2) that line is rs.LinRegValue(215).Shift(15), valid from bar 229 of rs (see LinRegValue).
  • Domain: the ratio line is NaN where s2 is zero or not finite or s is not finite, which makes the line and the band NaN on the n windows holding that bar; later windows recover. A flat window gives a width of 0, so the band is the line, and the line is the window’s computed mean: exactly the ratio for a power-of-two ratio, but not for every constant (three copies of 0.1 average to 0.10000000000000002). A negative ratio line inverts the reading. The rolling sums make the last bits depend on where the series starts.
  • Anchors: with r as above, the line and the band equal the same composition with r.Average(n).Add(r.Slope(n).Mul(float64(n-1) / 2)) in place of r.LinRegValue(n), bit for bit (E); s = 2^p*s2 on a finite non-zero s2 (s2 == s at p = 0) gives a line and a band of exactly 2^p for every finite k (E); with s2 all ones on finite s it is the same composition on s’s overlap view (E). The line less r.Average(n) is r.Slope(n) times (n - 1)/2 within 6e-16*max(|line|, |average|). Against r.RegressionChannel(ramp, n, k), with ramp the bar numbers 0, 1, 2 and so on, the line equals the k = 0 channel within 1e-15*n*level, and the band less the line at k = 1 equals the channel’s spacing within 1e-13*level, level being the window’s largest |r|, on windows whose coefficient of determination is below 1 - 1e-8.
  • Pinned by TestRecipeT4ARatioChannel; runnable example ExampleSeries_LinRegValue_ratioChannel.

Slope of the ratio line (D’Errico)

Domenico D’Errico (Stocks & Commodities 44:7) divides each of the “Magnificent 7” stocks by SPY and ranks the ratio lines by their least-squares slope over 50 bars, in ratio units per bar; his loop fits the slope stored at each bar to the 50 bars before it, leaving that bar out. The recipe gives both readings of Slope(n) on the ratio line a.PriceRelative(b, 1): the slope at the current bar, and D’Errico’s slope at the previous bar, which is the current-bar slope delayed by one bar. He ranks all seven and asks a language model to “Analyze these results and propose possible trading actions”; he states no trading rule himself. Robert L. Hand Jr. (Stocks & Commodities 9:5) holds the Dow stocks with the best relative strength, his ratio of a stock’s percentage change to the Dow’s, and buys rising and sells falling relative strength: “It is the slope of the relative strength line that is important, not the position.”

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
slope := r.Slope(n)
current := slope.Window(-(n - 1))
previous := slope.Shift(1).Window(-n)
  • First valid index: n - 1 for the current-bar slope and n for the previous-bar slope, counted from the start of the overlap; element e of each is overlap bar n - 1 + e or n + e. Minimum overlap: n + 1 bars (n for the current-bar slope alone).
  • Parameters: n >= 2; D’Errico’s n = 50, on daily bars.
  • Units: ratio units per bar, which scale with the ratio’s level, so the slopes of members at different price levels against one benchmark do not compare. The percent slope below, or the slope of the log ratio, a.LogRatio(b).Slope(n), does.
  • Domain: a window holding a NaN or an infinite ratio gives NaN; a flat window gives exactly 0.
  • Anchors: the previous-bar slope is the current-bar slope without its last element, its LookBack(1), bit for bit (E); a flat ratio line (s2 == s, or s = 2^p*s2) gives exactly 0 in both (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E).
  • Pinned by TestRecipeT4ARatioSlopes.

Percent slope of a regression line (Katsanos)

Markos Katsanos’s soybean system (Stocks & Commodities 27:11) measures a trend by the rise of its n-bar regression line, per bar, as a percentage of the line’s value n bars back: the line read at the current bar, less the same line extended n bars into the past (TradeStation’s LinearRegValue read n bars ago, one bar before the window), divided by that extended value and by n, times 100. He computes it on two series: his relative strength, a 3-bar exponential average of the soybean close over its seasonal pattern (the average seasonal prices), over 12 bars, and the dollar index over 25 bars. Long trades require the dollar’s percent slope below 0.3% a day and the relative strength’s above -0.8%; once the close is below its 15-day exponential average, a long is closed when the relative strength’s percent slope falls below -0.8%, or when the dollar’s rises above 0.3% while its 50-day exponential average rises. His text says the short rules are the opposite ones; his code mirrors the short entry and the relative-strength exit, but its short dollar exit has no dollar-slope test (it requires the close above its 15-day exponential average, a falling 50-day exponential average of the dollar and the seasonal series above its own 15-day exponential average). The helper works on one series, so a ratio line is passed in: linRegPercentSlope(a.PriceRelative(b, 3).Window(-2), n) for his relative strength.

fit := x.LinRegValue(n)
back := fit.Sub(x.Slope(n).Mul(float64(n)))
pct := fit.Sub(back).Div(back).Div(float64(n)).Mul(100).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of x; element e is bar n - 1 + e of x. On Katsanos’s relative strength above, trimmed by 2, element e is overlap bar n + 1 + e. Minimum history: n bars.
  • Parameters: n >= 2; Katsanos’s 12 and 25, on daily bars, with thresholds in percent per bar.
  • Against SlopePercent: SlopePercent(n) divides the n-bar slope by the absolute value of the window’s first value. This form divides by the fitted line one bar before the window, keeping its sign, so on an exactly linear positive series it equals x.SlopePercent(n + 1) from bar n, which is 100 times Rafter’s MSROC (“Moving-slope rate of change (Rafter)” under Relative: Divergence), not SlopePercent(n).
  • Domain: a flat positive series gives exactly +0, also where Average does not return the constant; a zero fitted base gives 0 (Div), not an infinity; a negative fitted base flips the sign; a window holding a NaN gives NaN.
  • Anchors: it equals the same composition with x.Average(n).Add(x.Slope(n).Mul(float64(n-1) / 2)) in place of x.LinRegValue(n), bit for bit (E); scaling x by a power of two leaves it unchanged (E); a flat positive x gives exactly +0 (E). On the exact line 100 + 0.5*i it equals x.SlopePercent(n + 1) from bar n within 1e-14*max(1, |value|).
  • Pinned by TestRecipeT4ARatioSlopes.

Angle of a rank’s slope (Behar)

Jaime V. Behar (Stocks & Commodities 12:7) follows industry groups by the relative-strength rank that Investor’s Business Daily publishes, from 1 to 99 with 99 the best, read weekly. A group’s “speed” is the angle, in degrees, whose tangent is the least-squares slope of its rank readings, over 20 weeks for the long term and 6 weeks for the short term, and its “acceleration” is the short-term speed less the long-term one. On a rank series rank, one reading per bar, the speed over n readings is rank.Slope(n).Atan().Mul(180 / math.Pi).Window(-(n - 1)), and the acceleration is rank.Slope(6).Atan().Sub(rank.Slope(20).Atan()).Mul(180 / math.Pi).Window(-19). The arctangent is increasing, so speeds rank groups as their slopes do, and it compresses steep slopes towards 90 degrees. An angle depends on the units of both axes: Behar’s time axis counts the days elapsed between readings, so his angles come from the per-bar slope divided by the days per bar, and a slope per weekly bar gives larger angles.

  • First valid index: n - 1 for a speed over n readings and 19 for the acceleration, counted from the start of the rank series; element e is reading n - 1 + e or 19 + e. Minimum history: n readings (20 for the acceleration).
  • Domain: Slope’s: a window holding a NaN gives NaN, and a flat window a speed of 0 degrees.
  • Not pinned: prose only; no helper or test holds this form.

Deviation from a log-linear trend (Hayes)

Tim Hayes (Stocks & Commodities 12:2) compares the ratio of Moody’s Aaa bond yields to the S&P 500’s earnings yield with its trendline, a linear regression on a log scale over monthly data from 1926 to 1993, and plots the ratio’s “% Over Or Under Trend Line Growth”. The recipe fits that line over a rolling window: the log ratio L, a.LogRatio(b), its least-squares line read at the current bar, L.LinRegValue(n), and the percentage by which the ratio stands above the exponential of the line, 100*(exp(L - fitted) - 1), computed with Expm1 for accuracy near 0. A whole-sample line, as Hayes’s, uses data that come after each bar (he notes that his line changes slightly with each update); the rolling line uses only the last n bars. Matt Blackman (Stocks & Commodities 33:4, the bonus issue) fits straight lines on an arithmetic scale to Buffett’s ratio of market capitalisation to GDP, separately over three periods: linear, not log-linear, trends, also fitted with hindsight.

a, b := s.Overlap(s2)
L := a.LogRatio(b)
deviation := L.Sub(L.LinRegValue(n)).Expm1().Mul(100).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of the overlap; element e is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2, and n >= 3 in practice: a 2-bar line passes through both log values, so the deviation reads rounding only.
  • Units: percent of the trend value. L.Sub(L.LinRegValue(n)) alone is the deviation in natural-log units (100 times it is log points), which swapping the legs negates.
  • Domain: a zero, negative or non-finite price makes L NaN, and the deviation NaN on the n windows holding it; a deviation of more than about 709.78 in natural-log units overflows to +Inf.
  • Anchors: s2 == s gives exactly 0 (E); with s2 all ones it is the same composition on ln of s’s overlap view, a.Log() (E); on an exact exponential trend 100*e^(0.003*i) against a constant s2 its magnitude is at most 1e-12. A power-of-two ratio does not give exactly 0: the log ratio of such legs varies in its last bits.
  • Pinned by TestRecipeT4ALogTrendDeviation.

Relative-performance channel (Murphy)

John Murphy’s relative ratio (Intermarket Technical Analysis, chapter 11) divides one market by another and gives the quotient “a starting value of 100” at the start of a chosen span, 25 or 100 trading days in his examples, so that its value reads as relative performance (110 is a 10% gain on the benchmark) and ranks markets. The recipe rolls the span: at each bar the ratio line a.PriceRelative(b, 1) is rebased to 100 at the window’s first bar, n - 1 bars back, and the channel is the highest and lowest rebased values over the window (upper and lower) around the current one (the line). The line is the index of “Relative performance and relative return” (Relative: Ratio and Relative Strength) with its base n - 1 bars back. The upper and lower bounds are the recipe’s own: Murphy reads the relative ratio’s level and draws trendlines on it.

a, b := s.Overlap(s2)
r := a.PriceRelative(b, 1)
base := r.Shift(n - 1)
upper := r.Highest(n).Div(base).Mul(100).Window(-(n - 1))
lower := r.Lowest(n).Div(base).Mul(100).Window(-(n - 1))
line := r.Div(base).Mul(100).Window(-(n - 1))
  • First valid index: n - 1 for the upper, the lower and the line, counted from the start of the overlap; element e of each is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2. Murphy’s spans of 25 and 100 days are n = 25 and n = 100 if a span counts its starting bar, one more if it does not.
  • Reading: on a positive ratio line lower <= 100 <= upper and lower <= line <= upper, and the line’s place in the channel, (line - lower)/(upper - lower), is the ratio line’s ChannelPosition(n, 1) divided by 100.
  • Domain: a NaN ratio (a zero or non-finite s2, or a non-finite s) makes the upper and the lower NaN on the windows holding it, and the line NaN at its bar and where it is the base; a zero base gives 0 for all three (Div); a negative ratio line inverts the channel, with the upper below 100.
  • Anchors: s2 = 2^p*s on finite non-zero s (s2 == s at p = 0) gives exactly 100 for all three (E); with s2 all ones it is the same composition on s’s overlap view (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E). Where the upper and the lower differ, the line’s place in the channel equals the ratio line’s ChannelPosition(n, 1), trimmed by n - 1, divided by 100, within 1e-13.
  • Pinned by TestRecipeT4APerformanceChannel.

Rebased overlay (Murphy)

Murphy compares markets on overlay charts, two price lines on one chart, beside the ratio chart. Rebasing s2 to the value of s k bars back puts both on the price scale of s: the overlay at bar j is s2[j]*s[j-k]/s2[j-k], the price s would have now had it moved as s2 did over the last k bars. Where s is above the overlay it has outperformed over those bars, and s divided by the overlay, times 100, is the line of the channel above with n = k + 1, in exact arithmetic. Phil Doyle’s relative performance charts (Stocks & Commodities 19:5) rebase both instruments at one fixed bar instead (see “Relative performance and relative return”).

a, b := s.Overlap(s2)
overlay := b.Mul(a.PriceRelative(b, 1).Shift(k)).Window(-k)
  • First valid index: k, counted from the start of the overlap; element e is overlap bar k + e. Minimum overlap: k + 1 bars.
  • Parameters: k >= 1.
  • Domain: a NaN ratio k bars back (a zero or non-finite s2, or a non-finite s, there) gives NaN; a zero s2 at the current bar gives 0 where the ratio k bars back is a number. Use positive prices.
  • Anchors: s2 = 2^p*s on finite non-zero s (s2 == s at p = 0) gives exactly s’s overlap view, trimmed by k (E); with s2 all ones it is s delayed by k bars, a.Shift(k).Window(-k) (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E).
  • Pinned by TestRecipeT4APerformanceChannel.

Simple spread Bollinger band

The windowed counterpart of SpreadBollingerX: a Bollinger band of the hedged spread s - h*s2, with the n-bar simple mean and population standard deviation of the spread. Ernest Chan’s Example 3.6 (Quantitative Trading, 2nd edition, 2021) trades the same GLD/GDX spread, with the hedge ratio from a regression of GLD on GDX over a 252-day training set, but standardises it with that set’s fixed mean and standard deviation rather than a moving window.

a, b := s.Overlap(s2)
spread := a.Sub(b.Mul(h))
band := spread.Bollinger(n, k).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of the overlap; element e of band is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 2, a finite hedge ratio h and a finite k; a negative k gives the lower band.
  • Units: the price units of s; h is in units of s per unit of s2, and k in population standard deviations of the spread.
  • Warm-up: untrimmed, Bollinger passes the spread through on its first n - 1 bars, where the band equals the spread itself, so a rule comparing the spread with its band reads those bars as “on the band”. The Window trim removes them.
  • Anchors: with h = 0 and a finite s2 the band is a.Bollinger(n, k) bit for bit (E, an exact identity); with a finite s, s2 == s and h = 1 the spread is exactly 0, so the band is exactly 0 (E).
  • Against SpreadBollingerX: the windowed band weights the last n spreads equally and drops each at once, so it jumps when a large spread leaves the window; the exponential band, with alpha = 2/(n+1), has the same average lag of (n-1)/2 bars but moves about twice as far on the first bar after a change in level and completes the move more slowly. A NaN spoils the windowed band for n bars and the exponential band for good.

Pinned by TestRecipeSimpleSpreadBollinger, with its runnable example ExampleSeries_SpreadBollingerX_simple.

Relative: Correlation

AverageCorrelation

func (s Series) AverageCorrelation(n int, ss ...Series) Series

AverageCorrelation returns the mean of the rolling n-bar pairwise correlations among s and the other series.

s is the base and the other series are the members; all the inputs are right-aligned at their most recent bar, only the trailing bars common to every input are paired, the result has len(s), and bars before the overlap are zero. The first valid index is len(s) - m + n - 1, where m is the length of the shortest input, and the minimum overlap is n.

The result is the mean over all pairs: each pair’s Correlation, summed in the fixed pair order and divided by the pair count K*(K-1)/2, with K the number of series. The pairs run (0,1), (0,2), …, (0,K-1), (1,2), …, (K-2,K-1), with s numbered 0 and the members 1 to K-1 in order; in each pair the earlier series is the response and the later one the regressor.

K = 1 (no other series), n < 2 or n above the overlap give zeros. The result is at most 1 and, in exact arithmetic, at least -1/(K-1); rounding can take it an ulp or so below -1/(K-1). The result is start-invariant: a bar’s value depends only on its own window. The inputs are paired by position only, and the cost is O(len(s)KK*n): with K = 29, 406 pairs per bar.

A NaN or an infinity in any input makes NaN every output whose window contains it; later windows recover. A member that is flat over a window contributes 0 to each of its pairs there. It still counts in the pair count, so it pulls the mean towards 0.

The method correlates whatever it is given: for return correlations pass return series. The return form and the hub variant are recipes in the “Relative: Correlation” category, not further methods.

Uses: Wang holds SPY while the 8-week simple average of the 13-week average pairwise correlation of 29 ETFs (SPY among them) rises and holds cash while it falls (Stocks & Commodities 35:2); Parzen picks, from six momentum leaders, the three-asset subset with the lowest average pairwise correlation of recent daily returns (Stocks & Commodities 44:4). Two published one-to-many averages are the hub variant, not this method: Landazabal shorts SPY when the mean of its 34-day correlations with HYG and JNK drops below 0.3 while its 34-day regression slope is positive (Stocks & Commodities 35:13), and the Updata Traders’ Tip accompanying Cline’s article averages the 14-bar R-squared of an instrument’s closes against each member of a watch list as a gate on Donchian entries (Stocks & Commodities 33:5). Inputs filled forward over holidays or carrying stale prices bias the correlation of returns towards zero (the Epps effect); align them by date before calling.

Warm-up hazard: before the first valid index the output is 0, which passes a gate such as a correlation below 0.3; trim the warm-up with Window or mask it with SetN.

Verification: two references, a Python reference with exact rational sums and a Julia reference, agree with it; with one member it equals Correlation bit for bit, and identical inputs give exactly 1 on windows that are not flat.

BandCorrelation

func (s Series) BandCorrelation(s2 Series, n, period int, bw float64) Series

BandCorrelation returns the rolling n-bar correlation of s and s2 after each is passed through a band-pass filter centred on period bars with fractional bandwidth bw.

Each input’s overlap is passed through Ehlers’s band-pass filter, BandPass(period, bw), as printed in his Stocks & Commodities articles (for example “Truncated Indicators”, July 2020). The value at a bar is the Pearson correlation of the trailing n filtered values. It uses the shared co-moment kernel, with the filtered s2 as the regressor and the filtered s as the response. Take the overlap views a of s and b of s2, and let fy = a.BandPass(period, bw) and fx = b.BandPass(period, bw). From overlap index n+1 on, the value equals fy.Correlation(fx, n) bit for bit.

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, counted from the start of the overlap, is n+1, and the minimum overlap is n+2. The first valid index is the first window that excludes indices 0 and 1, where BandPass is structurally zero. Any of the following gives a series of len(s) zeros, in O(len(s)) time whatever the size of n: an empty overlap, n < 2, period < 1, a NaN or infinite bw, or an overlap shorter than n+2. Every period of at least 1 is valid, however large, and costs O(len(s)) time.

The filter is recursive, so it is not start-invariant. Each value depends on every bar of the overlap before it, and moving the start of the overlap changes it.

The output lies in [-1, 1], and a window in which either filtered series is flat gives exactly 0. The start-up transient of the filter is not masked; it decays by about exp(-pibw) per period, so discard about 2period/bw bars (about 7 periods at bw = 0.3, 20 at bw = 0.1).

BandPass reads every input from index 0 on. A NaN or an infinity at overlap index p therefore makes every output from max(p, n+1) on NaN, because the filter carries it forever.

s.BandCorrelation(s, n, period, bw) is exactly 1 wherever the filtered window is not flat. The bandwidth bw is a fraction of the centre frequency, typically 0.1 to 0.5 (Ehlers’s Stocks & Commodities listings use 0.1 to 0.25). Where cos(2pibw/period) < 0, that is for period/4 < bw < 3*period/4 give or take whole periods, the coefficient is below -1 and the filter is unstable; the outputs there are NaN or meaningless.

Correlating two markets within one cycle band isolates their co-movement at that period. Use it to compare them at a chosen cycle length, free of the trends and noise outside the band.

Verification: two independent references agree with it; it equals Correlation of the BandPass series from the first valid index on, bit for bit; and it is exactly 1 for s2 = s.

Correlation

func (s Series) Correlation(s2 Series, n int) Series

Correlation returns the rolling Pearson correlation of s with s2 over the trailing n right-aligned bars.

It is the correlation coefficient of Markos Katsanos (Intermarket Trading Strategies, 2008, equations 2.1 to 2.3) and MetaStock’s Correl: on each bar, the Pearson correlation Sxy/sqrt(Sxx*Syy) of the n most recent paired values of s and s2, with the sums centred within the window.

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 len(s) - min(len(s), len(s2)) + n - 1 and the minimum overlap is n. An empty overlap, n < 2 or n > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n.

Pair j of the overlap is s[len(s) - min(len(s), len(s2)) + j] with s2[len(s2) - min(len(s), len(s2)) + j], and the window ending at pair j holds pairs j-n+1 to j. Any finite level of either sign is valid. The kernel uses centred, power-of-two-scaled, window-local sums: each variable is anchored at the first value of its window and scaled by a power of two before the deviations are formed, so levels near 1e9 or 1e300 are safe, the powers of two cancel in the ratio, and the result is bitwise start-invariant (a bar’s value does not depend on where the series begins). The correlation is symmetric, so s.Correlation(s2, n) equals s2.Correlation(s, n) bit for bit on equal lengths, and it is invariant under multiplying either input by a power of two, provided no product overflows or rounds in the subnormal range.

The result lies in [-1, 1]. A flat window, in which either variable is constant over the n bars, gives exactly 0. At n = 2 the value is the product of the signs of the two one-bar changes, exactly 1, -1 or 0, where the general formula rounds to ±0.9999999999999999 on about one random window in nine. A NaN or an infinity at pair p gives NaN at exactly the windows containing it, pairs p to p+n-1 from the first valid pair on (fewer than n near either end of the overlap), and later outputs recover. Before the first valid index the output is 0, so a fixed-threshold gate such as r < 0.5 or r < 0.8 is true on every warm-up bar and must be masked, for example with Window(-f) where f is the first valid index, or by starting the test there. Forward-filled bars (the Epps effect) bias the value towards 0; align the two inputs by date before calling.

Correlations of trending levels can be spurious (George Udny Yule; Clive Granger and Paul Newbold): Katsanos’s Table 6.1 shows the DAX against the lagged S&P 500 at 0.913 on prices but 0.134 on daily returns and 0.661 on weekly returns, so ReturnCorrelation is the recommended default and Correlation is for readers who want the level relationship itself. His Chapter 16 filter Correl(C,SEC2,130,0)<.5 is Correlation(s2, 130), which needs a minimum overlap of 130, and the Correlation(C,IND,126) of his RSMK system needs 126. Published level windows and gates in Stocks & Commodities: 30 days (Thom Hartle, and the editor’s sidebar to Michael Turner’s 1999 article), 40 bars with a gate below -0.40 (Katsanos, gold against European bank futures), 52 weeks with ±0.5 as “strong” (Katsanos), 63 days and two years (Jim Bianco), and 50 months of monthly averages (Eric Sharp); Cassandra Wang plots a 13-week correlation of XLE with SPY on adjusted weekly closes (January 2018), s.Correlation(spy, 13) on true weekly bars, not a strided daily series. Hartle’s point is that a turn in the correlation is the signal, not its level. The sign of a correlation may choose the orientation of a spread (long/long for inverse pairs, John Murphy and David Hirschfeld) or the inverted scale of an overlay (Roger Pilloton), but the library does not switch automatically.

Deviations from the published code: the published raw-sum formula (SumXY - nXbarYbar)/sqrt(…) cancels at price levels (Bianco’s ten bond and CRB closes lose about four of their digits to it) and the centred kernel does not; the n = 2 sign rule replaces the formula on two-bar windows; and the output before the first valid index is zero rather than empty.

Compositions that start from it are listed under “Compositions” in the “Relative: Correlation” category.

Verification: two independent references, a Python reference in exact rational arithmetic and a Julia reference, and the exact identities with existing methods: s.Correlation(s, n) is 1 and s.Correlation(s.Mul(-1), n) is -1 on windows that are not flat, the Pearson correlation of each window’s fractional ranks equals SpearmanRankCorrelation(n, s2) on equal-length finite inputs for n up to about 300,000, where Spearman’s plain sums of ranks are exact, and sign(s.Correlation(ramp, n)) equals sign(s.Slope(n)) for the ascending unit-step ramp c, c+1, c+2 and so on, wherever Slope’s result does not underflow (it is normal, or the exact slope is zero).

CorrelationBreak

func (s Series) CorrelationBreak(s2 Series, n, n2 int) Series

CorrelationBreak returns a Fisher z test statistic comparing the correlation of s and s2 over the latest n bars with that over the n2 bars before them.

At each bar the Pearson correlation ra of the latest n paired bars and the correlation rb of the n2 paired bars before them are computed exactly as Correlation computes them, with one kernel of capacity max(n, n2); each is transformed to Fisher’s z with the Fisher method’s expression, the correlation clamped to [-0.999, 0.999] and then ln((1+r)/(1-r))*0.5 taken; and the output is the difference of the two z values divided by the standard error se = sqrt(1/(n-3) + 1/(n2-3)) of Fisher (1921), evaluated as (float64(fisherZ(ra)) - float64(fisherZ(rb)))/se with se computed once per call. Under a constant correlation of independent bivariate normal pairs the statistic is approximately standard normal, so a magnitude beyond about 2 flags a change in the relationship at roughly the five per cent level; heavy-tailed returns inflate its variance, about threefold for Student’s t with three degrees of freedom.

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, counted from the start of the overlap, is n+n2-1, and the minimum overlap is n+n2. An empty overlap, n < 4, n2 < 4 or n+n2 > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the sizes of n and n2; the last test never forms the sum n+n2, so no argument, math.MaxInt included, can overflow.

Each output depends only on its own paired window of n+n2 bars, the latest n and the n2 before them, so the series is start-invariant: dropping leading bars of both inputs leaves the later outputs unchanged bit for bit. Any finite data is valid, and both correlations are centred within their windows, so levels, returns and back-adjusted prices all serve.

A window holding a NaN or an infinity in either input gives a NaN correlation and a NaN output, so a non-finite value at overlap index p makes NaN exactly the outputs from p to p+n+n2-1 that lie at or after the first valid index, and later outputs recover. A flat window gives a correlation of 0 and a z of 0. CorrelationBreak(s, s, n, n2) is exactly 0 wherever neither window is flat, both correlations being 1 and clamped to 0.999 by the transform, and wherever both are; with one flat window it is 0.5ln(1999)/se, about 3.8002/se, or its negation. The transform’s clamp to [-0.999, 0.999] bounds each z by 0.5ln(1999), so no output overflows.

With overlapping k-bar returns the bars are not independent and the effective sample size is smaller than n, about n_eff = 3kn/(2k*k+1) for uncorrelated returns, so multiply the output by se/sqrt(1/(n_eff-3) + 1/(n2_eff-3)) before reading it as a z score. Murphy, Intermarket Analysis (Wiley 2004), chapter 15, pages 238 to 239, warns that intermarket correlations lapse and sometimes change for good, as bonds and stocks decoupled after 1997; the test itself is original to this library. The fast-in-slow nested form is the composition s.Correlation(s2, nf).Fisher().Sub(s.Correlation(s2, ns).Fisher()).Div(se): its windows share bars, so the two z values are correlated and se overstates the spread of their difference, and it reads as a scale rather than a test. Published practice judges a relationship by the stability of block correlations rather than by a break test: Saitta’s mean and mean absolute deviation of five four-year block correlations (Stocks & Commodities 18:7), Katsanos’s calendar-year tables and nested trailing windows (Stocks & Commodities volumes 30:1 and 38:9), and Lederman’s whole-period return correlation of 0.823 hiding calendar-year values from 0.225 to 0.941 (Stocks & Commodities 14:8). The rolling form of that practice is a composition: sample s.Correlation(s2, n) with Value(0), Value(n), Value(2n) and so on and take the mean and the mean absolute deviation in the consumer; calendar-year blocks need timestamps and belong upstream of nseries.

Verification: two independent references, a Python reference and a Julia reference, each in exact rational arithmetic with a high-precision square root and logarithm, agree with it; and it equals s.Correlation(s2, n).Fisher().Sub(s.Correlation(s2, n2).Fisher().Shift(n)).Div(se) bit for bit after warm-up.

CorrelationLowerBound

func (s Series) CorrelationLowerBound(s2 Series, n int, zcrit float64) Series

CorrelationLowerBound returns the rolling correlation of s with s2 shrunk towards zero by zcrit standard errors of its Fisher transform.

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 len(s) - min(len(s), len(s2)) + n - 1 and the minimum overlap is n.

With r = s.Correlation(s2, n), a = |r| and c = zcrit/sqrt(n-3), each valid bar is LB = min(a, max(0, Tanh(Atanh(a) - c))) with the sign of r, or +0 where the magnitude is zero. LB is a one-sided lower confidence bound on the population correlation in the direction of r’s sign. The outer min keeps |LB| <= |r|: for about 8% of magnitudes Tanh(Atanh(a)) alone exceeds a, almost always by one ulp and rarely by two.

Invalid input gives zeros: n < 4, a non-finite or negative zcrit, or n above the overlap. The sign of r is kept unless the output is +0; a correlation of 1, -1 or 0 is kept exactly (Atanh(1) is +Inf); |LB| is non-increasing in zcrit; and zcrit = 0 gives r within 2 ulp. zcrit = 1.645 gives a one-sided 95% bound for a signed gate such as LB > 0.5, and 1.96 the lower end of the two-sided 95% interval. Read as a bound on the magnitude, either sign can clear it, so the coverage falls towards 90% at 1.645 as the population correlation nears 0. A NaN or an infinity at overlap index p gives NaN on [max(p, n-1), p+n-1], the footprint of Correlation. The result is bitwise start-invariant.

The standard error 1/sqrt(n-3) assumes independent bivariate-normal pairs. Overlapping returns, trending levels and serial correlation make the true error larger, so the bound is a heuristic.

Published fixed-threshold gates each ignore n; given the same inputs, this method is their n-aware form. Like Correlation it correlates s and s2 as given, so a gate on returns needs return series, with their warm-up bars masked or dropped. Bianco goes long bonds when the 63-day correlation of the CRB index with bonds reaches +0.55 and exits when it returns to -0.50 (Stocks & Commodities 9:12). Murphy and Hirschfeld call correlations between -0.5 and +0.5 unreliable and suggest spreading against the CRB index only markets with positive correlations above 0.5, citing correlations over 12 recent months (Stocks & Commodities 6:5). Katsanos gates below -0.40 over 40 bars of prices (Stocks & Commodities 30:1) and skips trades above 0.8 over 50 bars of 3-day returns (Stocks & Commodities 35:8). Kaufman requires more than 0.50 over 40 days of daily returns (Stocks & Commodities 39:11).

Bianco’s two thresholds are a hysteresis latch: r.Hysteresis(0.55, -0.50) on the correlation r is +1 from a bar above 0.55 until one below -0.50 and -1 (his exit to flat) after it, so its GT(0) is the long flag. The same latch on this lower bound is an n-aware gate: at n = 63 and zcrit = 1.645 it enters only when r exceeds about 0.681 and exits only when r falls below about -0.642.

Warm-up hazard: before the first valid index the output is 0, which passes a gate such as r < 0.8; mask the warm-up with Window or SetN.

Verification: the bound has no exact identity with an existing method, so the alternative oracle stands in: a Python reference, a Julia reference, and the closeness of the output at zcrit = 0 to Correlation.

CorrelationTrend

func (s Series) CorrelationTrend(n int) Series

CorrelationTrend returns Ehlers’s Correlation Trend Indicator, the correlation of the trailing n values with a straight line rising over the same bars.

It is the Pearson correlation of each n-bar window of s with the ramp 0, 1, …, n-1. It uses the shared co-moment kernel of Correlation, with the ramp as the regressor and the window of s as the response. The source is Ehlers, “Correlation As A Trend Indicator”, Stocks & Commodities, May 2020. His code correlates the closes with a line. Its Y = -count over Close[count] pairs the same values in reverse order, with the line shifted by a constant, which leaves the correlation unchanged. Let t be the unit ramp t[i] = i, of the same length as s. The value equals s.Correlation(t, n) bit for bit, because the kernel anchors each window of t at its first value and so sees exactly 0 to n-1.

The first valid index is n-1, and the minimum length is n. An empty s, n < 2 or n > len(s) gives a series of len(s) zeros. That takes O(len(s)) time whatever the size of n.

It is window-local and start-invariant: each value depends only on the n bars of its own window.

The output lies in [-1, 1]. A flat window gives exactly 0, which is the rule of the correlation family; Ehlers’s code holds the previous value instead where its raw sums show no variance; in floating point they often do show a little, and it then prints a value near 0. A rising line gives exactly 1, and a falling one exactly -1, wherever the kernel reads the window as a line, for example on integer ramps. At n = 2 the value is the sign of the latest change. A NaN or an infinity at index p makes the outputs on [p, p+n-1] NaN, clipped to the valid bars from n-1 to len(s)-1, and later outputs recover.

Values near +1 mean a steady rise, values near -1 a steady fall and values near 0 no linear trend. Unlike Slope, it is scale-free. Its sign is that of Slope(n) wherever Slope’s result does not underflow, and its square is R2(n) up to rounding. For a pair of series, s.LogRatio(s2).CorrelationTrend(n) gives the trend of the relationship line.

Verification: two independent references agree with it, and a translation of Ehlers’s published code agrees to within 1e-9 off flat windows; it equals s.Correlation(t, n) against a unit ramp t bit for bit; and its sign is that of Slope(n) and its square is R2(n) up to rounding.

Covariance

func (s Series) Covariance(s2 Series, n int) Series

Covariance returns the rolling population covariance of s and s2 over the trailing n right-aligned bars.

It is the mean product of the centred deviations of the n most recent paired values, the sum over the window of (s - mean(s))*(s2 - mean(s2)) divided by n, computed by the shared co-moment kernel behind Correlation: each variable is anchored at the window’s first value and scaled by a power of two before its deviations are formed, and the output is the kernel’s scaled cross sum restored to real units with math.Ldexp and then divided by float64(n), in that order. The anchoring spares levels near 1e9 or 1e300 the cancellation of a raw-sum formula, and the result is within a few ulps of the window’s absolute co-moment scale. Katsanos, Intermarket Trading Strategies (Wiley 2008), equation 2.2 on page 17, divides by n-1 instead, the sample covariance; multiply by n/(n-1) for it. RollSpread is 2 sqrt(max(0, -cv)) with cv this covariance, over n pairs, of the one-bar changes and their lag.

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, counted from the start of the overlap, is n-1, and the minimum overlap is n. An empty overlap, n < 2 or n > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n.

Each output depends only on its own paired window of n bars, so the series is start-invariant: dropping leading bars of both inputs leaves the later outputs unchanged bit for bit. Any finite data is valid, including negative levels and back-adjusted prices, because the deviations are centred within the window. The method is symmetric: s.Covariance(s2, n) equals s2.Covariance(s, n) bit for bit on inputs of equal length.

A window holding a NaN or an infinity in either input gives NaN, so a non-finite value at overlap index p makes NaN exactly the outputs from p to p+n-1 that lie at or after the first valid index, and later outputs recover. A window in which either variable is flat gives exactly 0. The co-moment sum is restored to real units before the division by n, so a window whose sum of products exceeds the binary64 range, n times the covariance, gives an infinity of its sign even where the covariance itself would fit: deviations of ±1e154 at n = 2 give +Inf.

The covariance is the unnormalised correlation, in the product of the two inputs’ units, and the numerator of the slope of s regressed on s2 and of the beta of s against s2. Katsanos’s Table 2.2, fifteen days of percentage changes of gold and the dollar index, gives a population covariance of -0.1931488, printed there as the sample value -0.207.

Verification: two independent references, a Python reference and a Julia reference, both in exact rational arithmetic, agree with it; it equals the shared kernel’s covariance, the kernel behind Correlation, whose correlation and slope are tied to the existing R2 and Slope; the ramp t = 0, 1, …, n-1 gives exactly float64(nn-1)/12 against itself and float64(nn-1)/4 against 3t+7; and Katsanos’s Table 2.2 gives -0.1931488.

DeltaCorrelation

func (s Series) DeltaCorrelation(s2 Series, n, k int) Series

DeltaCorrelation returns the rolling Pearson correlation of the k-bar price changes of s and s2 over the trailing n changes, which is valid for spreads, rates and back-adjusted futures.

It is the difference form of the return correlation of Markos Katsanos (Intermarket Trading Strategies, 2008): each input is first reduced to its k-bar change v[j] - v[j-k], the expression of Momentum and Delta, and the two change series are then correlated as Correlation does, over n right-aligned changes, with Sxy/sqrt(Sxx*Syy) on sums centred within the window. After the warm-up it is the Momentum(k) composition, s.Momentum(k).Correlation(s2.Momentum(k), n), which untrimmed contaminates the first windows with Momentum’s leading zeros.

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 len(s) - min(len(s), len(s2)) + n + k - 1 and the minimum overlap is n + k, so the earliest change in the first window is a real one. An empty overlap, n < 2, k < 1, k >= min(len(s), len(s2)) or n > min(len(s), len(s2)) - k gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or k.

Pair j of the overlap is s[len(s) - min(len(s), len(s2)) + j] with s2[len(s2) - min(len(s), len(s2)) + j]; the change at pair j is the difference from pair j-k, and the window ending at pair j holds the changes of pairs j-n+1 to j. Any finite values are valid, of either sign and through zero: the difference of an arbitrary additive level is well defined, so this is the form for Panama back-adjusted futures, spreads, rates and other zero-crossing series, where a percentage means nothing. A difference that overflows gives an infinity, as Delta does, and hence NaN. The kernel uses centred, power-of-two-scaled, window-local sums, so changes from levels near 1e9 or 1e300 are safe and the result is bitwise start-invariant (a bar’s value does not depend on where the series begins).

The result lies in [-1, 1]. Flat prices give all-zero changes, and a window in which either change series is flat gives exactly 0. At n = 2 the value is the product of the signs of the two one-bar changes in the changes, exactly 1, -1 or 0. A NaN or an infinity at pair p makes the changes at pairs p and p+k NaN, so from the first valid index on the output is NaN at exactly the windows containing them, pairs p to p+n-1 and p+k to p+k+n-1 (fewer near either end of the overlap; one run when k <= n, two when k > n), and later outputs recover; a zero price makes nothing NaN. Before the first valid index the output is 0, so a fixed-threshold gate such as r < 0.5 or r < 0.8 is true on every warm-up bar and must be masked, for example with Window(-f) where f is the first valid index, or by starting the test there. Forward-filled bars (the Epps effect) bias the value towards 0; align the two inputs by date before calling.

No source publishes windows or thresholds for difference correlation; the return-correlation windows, 52 weekly changes of one bar or 50 changes of three bars, are reasonable starting points, but thresholds such as Katsanos’s 0.8, which he fitted to percentage returns, should be fitted again. The changes are in the units of each input, but the correlation does not depend on those units, so a spread in points and a rate in basis points compare directly, and a correlation of trending levels can still be spurious, for the same reasons as for Correlation.

Deviations from the published code: the difference in place of the percentage return, so a zero or negative base is ordinary data rather than a division hazard; NaN only from a non-finite price or an overflowing difference; and zeros before the first valid index.

Verification: two independent references, a Python reference in exact rational arithmetic and a Julia reference, and the exact identity with existing methods: s.Momentum(k).Correlation(s2.Momentum(k), n) from the first valid index on equal-length finite inputs, with DeltaCorrelation(s, s.Mul(-1), n, k) at -1 on windows whose changes are not flat.

DetrendedCorrelation

func (s Series) DetrendedCorrelation(s2 Series, n int) Series

DetrendedCorrelation returns the rolling correlation of s and s2 over n bars after removing each series’ linear trend within the window.

Within each paired window of n bars it is the partial correlation of s and s2 given time: with rab the Pearson correlation of s2 and s, rat that of the ramp t = 0, 1, …, n-1 and s, and rbt that of the ramp and s2, each computed exactly as Correlation computes it, the output is (rab - ratrbt)/sqrt((1 - ratrat)(1 - rbtrbt)) clamped to [-1, 1]. Go evaluates it as num := rab - float64(ratrbt), then rad := (1 - float64(ratrat))(1 - float64(rbtrbt)), and then math.Max(-1, math.Min(1, num/math.Sqrt(rad))), with 0 when rad <= 0. In exact arithmetic this equals the Pearson correlation of the two series’ within-window least-squares residuals on time (Yule 1907; for detrending, Frisch and Waugh 1933 and Lovell 1963).

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, counted from the start of the overlap, is n-1, and the minimum overlap is n. An empty overlap, n < 3 or n > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n.

Each output depends only on its own paired window of n bars, so the series is start-invariant: dropping leading bars of both inputs leaves the later outputs unchanged bit for bit. Any finite data is valid, including negative levels and back-adjusted prices, because every correlation is centred within the window. The method is symmetric, s.DetrendedCorrelation(s2, n) equals s2.DetrendedCorrelation(s, n) bit for bit on inputs of equal length, and every output that is not NaN lies in [-1, 1].

The float64 conversions are rounding barriers: they make DetrendedCorrelation(s, s, n) exactly 1 on every window where the kernel’s correlation of s with time is not exactly 1 or -1, because there num equals the value q = 1 - float64(ratrat), rad is float64(qq) and math.Sqrt returns exactly q for it, on every architecture and with or without fused multiply-add. A window where the kernel’s correlation of either series with time is exactly 1 or -1 gives rad = 0 and the output 0, and a flat window gives 0. That set is not quite the set of exact lines: a window within rounding of a line can read as one (0 where the exact value is 1), and a long exact line whose slope has many significant bits can miss it (1 against itself where the exact value is 0). A window holding a NaN or an infinity in either input gives NaN, so a non-finite value at overlap index p makes NaN exactly the outputs from p to p+n-1 that lie at or after the first valid index, and later outputs recover. No output overflows: the correlations are clamped and the quotient is clamped again.

It removes each series’ linear trend, which inflates the plain correlation of two trending series, the drift that makes any two rising markets look related, and so measures the co-movement of their departures from trend. It is PartialCorrelation with time as the control: s.DetrendedCorrelation(s2, n) equals s.PartialCorrelation(s2, t, n) bit for bit for a unit-step ramp t at least as long as the overlap of s and s2 and below 2^53 in magnitude. Vigderhous’s SPRM swaps the roles, with time as x1 and the market as the control: s.PartialCorrelation(t, market, n).

Verification: two independent references agree with it, a Python reference that correlates explicit least-squares residuals in exact rational arithmetic with a high-precision square root, and a Julia reference from the three-correlation formula in exact rational arithmetic with a BigFloat square root; it equals PartialCorrelation with a unit-step ramp as the control bit for bit, DetrendedCorrelation(s, s, n) is exactly 1 where the kernel’s correlation of s with time is not 1 or -1, and exactly 0 through rad <= 0 where it is.

KendallTau

func (s Series) KendallTau(s2 Series, n int) Series

KendallTau returns the rolling Kendall tau-b rank correlation of s with s2 over the trailing n right-aligned bars.

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 len(s) - min(len(s), len(s2)) + n - 1 and the minimum overlap is n.

Over each window of n bars, a pair of bars is concordant when s and s2 move the same way between them and discordant when they move opposite ways; a pair tied in either variable is neither. S is the number of concordant pairs less the number of discordant pairs, n0 = n*(n-1)/2 is the number of pairs, and n1 and n2 count the pairs tied in s and in s2. Then tau-b is S divided by sqrt((n0-n1)*(n0-n2)), the denominator that corrects for ties (Kendall 1938, Biometrika 30:81-93; the tie correction is Kendall 1945, Biometrika 33:239-251). Without ties it is Kendall’s original tau, S / n0.

If n < 2 or n is above the overlap, the result is all zeros. A NaN or an infinity in either window gives NaN on exactly the windows that hold it, flat or not, and later outputs recover. A finite flat window gives exactly 0: a window is flat when every value of s in it, or every value of s2, is equal; tau-b is undefined there, and the 0 is a convention of the library’s correlation family that cannot be told apart from S = 0. Otherwise the output is tau-b evaluated as written, with no clamp; it lies in [-1, 1], is exactly 1 for s2 == s off flat windows, even with ties, and exactly -1 for s2 == s.Mul(-1). It is symmetric in s and s2: s.KendallTau(s2, n) equals s2.KendallTau(s, n) bit for bit on equal lengths, and at the same distance from the most recent bar otherwise. It is bitwise start-invariant: dropping bars from the front of both inputs leaves every later output unchanged. The counts S, n1 and n2 are exact integers updated as bars enter and leave the window, at O(n) per bar, so each output depends only on the contents of its window.

Warm-up hazard: the output before the first valid index f is 0, so a fixed-threshold gate such as tau < 0.5 is true on every warm-up bar and must be masked, for example with Window(-f) or SetN(f, math.NaN()). The same gate is also true on flat windows, such as stale or forward-filled data, and on every bar when n < 2 or n is above the overlap.

The output depends only on the order of the values, so it is unchanged by any transform of either input that keeps their order and keeps distinct values distinct in binary64: prices or their Log alike, unless two prices differ only by rounding (Log can map neighbouring floats to one value). An outlier moves it no more than any other bar. For continuous, untied data under bivariate normality, sin(pitau/2) estimates the Pearson correlation, and heavy ties bias that estimate. Against a strictly ascending series s2, such as a bar index, the window’s S is the Mann-Kendall trend statistic and the output is S/sqrt(n0(n0-n1)), the tie-corrected Mann-Kendall tau, a rank analogue of CorrelationTrend. SpearmanRankCorrelation is the other rank correlation in the library.

At n = 2 the window holds one pair, and the output equals Correlation bit for bit by the same sign rule: NaN for a non-finite value, 0 for a tie in either input, and otherwise the product of the signs of the two one-bar changes.

Verification: a Python reference in exact integer arithmetic and an independent Julia reference, both counting every pair of every window, reproduce the outputs on tie-heavy data with non-finite values, and the n = 2 identity with Correlation holds bit for bit.

LogReturnCorrelation

func (s Series) LogReturnCorrelation(s2 Series, n, k int) Series

LogReturnCorrelation returns the rolling Pearson correlation of the k-bar log returns of s and s2 over the trailing n returns.

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 len(s) - min(len(s), len(s2)) + n + k - 1 and the minimum overlap is n + k.

The k-bar log return of a series v at bar j is the log change ln(v[j]) - ln(v[j-k]), and each valid bar holds the Pearson correlation of the trailing n such returns of s and s2. The log return is NaN unless both prices are finite and > 0, so a price that is zero, negative or not finite at overlap index p makes the returns at p and p+k NaN and gives NaN on the windows holding them: [p, p+n-1] and [p+k, p+k+n-1] from the first valid index. Later outputs recover.

Any of n < 2, k < 1, k >= m or n > m - k, with m = min(len(s), len(s2)), gives zeros. A window in which the returns of either input are all equal, as when its prices are flat over the window’s n + k bars, gives 0; prices growing at a constant rate have log returns equal only up to rounding, so they give an arbitrary value rather than 0. At n = 2 the output is -1, 0 or +1. The output lies in [-1, 1], and it is bitwise start-invariant.

Log returns add over time and are symmetric for rises and falls, so inverting a quote (s2 replaced by 1/s2) negates the correlation up to the rounding of 1/s2. That rounding matters only for returns of an ulp or so: prices an ulp apart can share a reciprocal, so such a window can read 0 or any other value once inverted. ReturnCorrelation is the percent form and DeltaCorrelation the difference form for series that cross zero.

Warm-up hazard: before the first valid index the output is 0, which passes a gate such as r < 0.8; mask the warm-up with Window or SetN.

Verification: a Python reference and a Julia reference, and the identity with s.Log().Momentum(k) through Correlation: on positive finite inputs of equal length, the output equals s.Log().Momentum(k).Correlation(s2.Log().Momentum(k), n) bit for bit from index n + k - 1 on.

PartCorrelation

func (s Series) PartCorrelation(x1, x2 Series, n int) Series

PartCorrelation returns the rolling semi-partial correlation of s with x1 after removing from x1 the part explained by x2, over the trailing n bars.

With rY1 the correlation of s and x1 over the window, rY2 that of s and x2 and r12 that of x1 and x2, each exactly the window value of Correlation, the value is (rY1 - rY2r12)/sqrt(1 - r12r12) clamped to [-1, 1]. Go evaluates it as num := rY1 - float64(rY2r12), then rad := 1 - float64(r12r12), and then math.Max(-1, math.Min(1, num/math.Sqrt(rad))), with 0 when rad <= 0. The float64 conversions round each product before it is used, so no fused multiply-add enters this last step on any architecture; the correlations are Correlation’s, whose kernel rounds its products on their own too, so the value has the same bits on arm64 and amd64. It is dimensionless and lies in [-1, 1], or is NaN.

s is the base and x1 and x2 the comparisons; the three inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(x1), len(x2)) bars are used, the result has len(s), and bars before the overlap are zero. The first valid index is len(s) - min(len(s), len(x1), len(x2)) + n - 1 and the minimum overlap is n. An empty overlap, n < 3 or n greater than the overlap gives a series of len(s) zeros.

A NaN or an infinity in any input makes NaN every output whose window contains it, whatever the other rules give, and later outputs recover. A flat x2 window gives s.Correlation(x1, n) bit for bit, and x2 equal to x1 gives exactly 0. A window in which s or x1 is flat gives 0, and x2 equal to s gives exactly 0. x2 equal to x1, or a power-of-two multiple of it, gives r12 exactly 1 or -1 and so 0; other exactly collinear x1 and x2, such as x2 = 3x1 + 7, can leave r12 within rounding of 1 or -1, and the output is then rounding noise rather than 0. Measured on integer x1 with such x2, it was exactly 0 on about four windows in five and otherwise of order 1e-8: the numerator’s rounding, about 1e-16, divided by sqrt(1 - r12r12), about 1.5e-8. s.PartCorrelation(s, x2, n) is sqrt(1 - r*r) up to rounding wherever s is not flat, where r is s.Correlation(x2, n). Negating s or x1 negates the value, negating x2 leaves it unchanged, and scaling any input by a power of two leaves it unchanged.

Each value depends only on its own window, which the kernel anchors at its first value, so the result is bitwise start-invariant and invariant to prepending history before the overlap.

Warm-up hazard: before the first valid index the output is 0, which passes a gate such as a part correlation below 0.3; trim the warm-up with Window or mask it with SetN.

Uses: Katsanos weights each predictor market according to its part correlation with the security to be predicted, which takes account of the redundant cross correlation between the predictors; his example predicts gold from silver and the dollar index. TwoFactorBeta is the coefficient of the same regression, and PartialCorrelation removes x2 from s as well as from x1.

Sources: Markos Katsanos, Intermarket Trading Strategies (Wiley, 2008), section 9.8, equations 9.12 and 9.13 on page 132, defines the part correlation; his ry2(1), the part correlation with the second predictor, is s.PartCorrelation(x2, x1, n). His MetaStock listing for the Double LRS Divergence in Appendix A.1, pages 299 and 300, computes it as rY12:=(rY1-rY2r12)/Sqrt(1-r12r12), with a correlation window that defaults to 300 days.

Verification: two references, a Python reference in exact rational arithmetic and a Julia reference, agree with it; a flat x2 gives Correlation of s and x1 bit for bit, and x2 equal to x1 gives exactly 0.

PartialCorrelation

func (s Series) PartialCorrelation(x1, x2 Series, n int) Series

PartialCorrelation returns the rolling partial correlation of s with x1 controlling for x2 over the trailing n bars.

With rY1 the correlation of s and x1 over the window, rY2 that of s and x2 and r12 that of x1 and x2, each exactly the window value of Correlation, the value is (rY1 - rY2r12)/sqrt((1 - rY2rY2)(1 - r12r12)) clamped to the interval [-1, 1]. Go evaluates it as num := rY1 - float64(rY2r12), then rad := (1 - float64(rY2rY2))(1 - float64(r12r12)), and then math.Max(-1, math.Min(1, num/math.Sqrt(rad))), with 0 when rad <= 0. The float64 conversions round each product before it is used, so no fused multiply-add enters this last step on any architecture; the correlations are Correlation’s, whose kernel rounds its products on their own too, so the value has the same bits on arm64 and amd64. It is dimensionless and lies in [-1, 1], or is NaN.

s is the base and x1 and x2 the comparisons; the three inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(x1), len(x2)) bars are used, the result has len(s), and bars before the overlap are zero. The first valid index is len(s) - min(len(s), len(x1), len(x2)) + n - 1 and the minimum overlap is n. An empty overlap, n < 3 or n greater than the overlap gives a series of len(s) zeros.

A NaN or an infinity in any input makes NaN every output whose window contains it, whatever the other rules give, and later outputs recover. A flat x2 window gives s.Correlation(x1, n) bit for bit, and x2 equal to x1 gives exactly 0. A window in which s or x1 is flat gives 0, and x2 equal to s gives exactly 0. x2 equal to x1, or a power-of-two multiple of it, gives r12 exactly 1 or -1 and so 0; other exactly collinear x1 and x2, such as x2 = 3x1 + 7, can leave r12 within rounding of 1 or -1, and the output is then rounding noise rather than 0. Measured on integer x1 with such x2, it was exactly 0 on about four windows in five and otherwise of order 1e-8 (the numerator’s rounding, about 1e-16, divided by sqrt(1 - r12r12), about 1.5e-8), and up to about 4e-5 at n = 3.

It is symmetric in s and x1: s.PartialCorrelation(x1, x2, n) equals x1.PartialCorrelation(s, x2, n) bit for bit on inputs of equal length. s.PartialCorrelation(x1, x2, n) with x1 equal to s, or to s times a power of two, is exactly 1 on every window where s is not flat and its correlation with x2 is not exactly 1 or -1, on every architecture, because of the float64 conversions. With a unit-step ramp t as x2 (c, c+1, c+2 and so on, or a step of a power of two of either sign), at least as long as the overlap of s and s2 and below 2^53 in magnitude, s.PartialCorrelation(s2, t, n) equals s.DetrendedCorrelation(s2, n) bit for bit; a step of 3 agrees only up to rounding. Negating s or x1 negates the value, negating x2 leaves it unchanged, and scaling any input by a power of two leaves it unchanged.

Each value depends only on its own window, which the kernel anchors at its first value, so the result is bitwise start-invariant and invariant to prepending history before the overlap.

Vigderhous’s stock performance relative to the market (SPRM) is the partial correlation of a stock’s prices with time controlling for the S&P 500, s.PartialCorrelation(t, market, n) with t a unit-step ramp such as 1, 2, 3 and so on; he reports it over the last 5 to 30 days (Stocks & Commodities, bonus issue 2017). Any unit-step ramp gives the same value bit for bit, because each window is anchored at its first value. DetrendedCorrelation swaps the roles of time and the control.

Warm-up hazard: before the first valid index the output is 0, which passes a gate such as a partial correlation below 0.3; trim the warm-up with Window or mask it with SetN.

Verification: two references, a Python reference in exact rational arithmetic and a Julia reference, agree with it; a flat x2 gives Correlation of s and x1 bit for bit, a unit-step ramp as x2 gives DetrendedCorrelation bit for bit, x1 equal to s gives exactly 1 where the correlation of s and x2 is not 1 or -1, and x2 equal to x1 gives exactly 0.

ReturnCorrelation

func (s Series) ReturnCorrelation(s2 Series, n, k int) Series

ReturnCorrelation returns the rolling Pearson correlation of the k-bar percentage returns of s and s2 over the trailing n returns.

It is the return correlation of Markos Katsanos (Intermarket Trading Strategies, 2008): each input is first reduced to its k-bar signed percent change, (v[j]/v[j-k] - 1)*100 on a positive base, which is RateOfChange’s exact expression, and (v[j]/(-v[j-k]) + 1)*100 on a negative one, the change over the magnitude of the base; the two return series are then correlated as Correlation does, over n right-aligned returns, with Sxy/sqrt(Sxx*Syy) on sums centred within the window.

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 len(s) - min(len(s), len(s2)) + n + k - 1 and the minimum overlap is n + k, so the earliest return in the first window is a real one. An empty overlap, n < 2, k < 1, k >= min(len(s), len(s2)) or n > min(len(s), len(s2)) - k gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or k.

Pair j of the overlap is s[len(s) - min(len(s), len(s2)) + j] with s2[len(s2) - min(len(s), len(s2)) + j]; the return at pair j is the change from pair j-k, and the window ending at pair j holds the returns of pairs j-n+1 to j. The valid data are prices that do not cross zero, normally positive ones: the signed percent change gives numbers on negative prices, a zero or non-finite base gives NaN, never a fake zero return, and near a zero crossing the return is unbounded. A percentage of an arbitrary additive level means nothing, so Panama back-adjusted futures, spreads, rates and other zero-crossing series should use DeltaCorrelation. The kernel uses centred, power-of-two-scaled, window-local sums, so returns from levels near 1e9 or 1e300 are safe and the result is bitwise start-invariant (a bar’s value does not depend on where the series begins).

The result lies in [-1, 1]. Flat non-zero prices give all-zero returns (flat zero prices give NaN, every base being zero), and a window in which either return series is flat gives exactly 0. At n = 2 the value is the product of the signs of the two one-bar changes in the returns, exactly 1, -1 or 0. A NaN or an infinity at pair p makes the returns at pairs p and p+k NaN, so from the first valid index on the output is NaN at exactly the windows containing them, pairs p to p+n-1 and p+k to p+k+n-1 (fewer near either end of the overlap; one run when k <= n, two when k > n), and later outputs recover; a zero price at pair p makes only the return at pair p+k NaN, where it is the base. Before the first valid index the output is 0, so a fixed-threshold gate such as r < 0.5 or r < 0.8 is true on every warm-up bar and must be masked, for example with Window(-f) where f is the first valid index, or by starting the test there. Forward-filled bars (the Epps effect) bias the value towards 0; align the two inputs by date before calling.

Returns rather than levels are the recommended basis, because trending prices “can produce deceptively high correlations” (Katsanos, Stocks & Commodities, February 2026), where his weekly ReturnCorrelation(spy, 52, 1) is the measure. Other published parameterisations: his decoupling gate ReturnCorrelation(spy, 50, 3).Value() < 0.8 (July 2017); his book’s fifteen-day returns over 300 days in the regression divergence indicator; Perry Kaufman’s recommended band of roughly 0.30 to 0.70 for pairs (March 2014) and his 20- or 40-day return correlation above 0.50 (October 2021); on true weekly bars, ReturnCorrelation(spy, 13, 1) as the return form of Cassandra Wang’s 13-week level correlation of XLE with SPY; and Jerome Lederman’s level correlations of 0.83 to 0.955 against return correlations of 0.099 to 0.347 for the same indices. The composition a.RateOfChange(k).Window(-k).Correlation(b.RateOfChange(k).Window(-k), n) over Overlap differs in its domain policy (0 at a zero base, the signed base on a negative one) and, untrimmed, contaminates the first windows.

Deviations from the published code: the signed percent change in place of MetaStock’s and AmiBroker’s subtract-first ROC, the same number up to rounding on positive bases and 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: Correlation” category.

Verification: two independent references, a Python reference in exact rational arithmetic and a Julia reference that also translates Katsanos’s published return-correlation code, and the exact identity with existing methods: s.RateOfChange(k).Correlation(s2.RateOfChange(k), n) from the first valid index on equal-length inputs whose bases are all finite and positive (at an infinite base RateOfChange gives -100 where this method gives NaN), with ReturnCorrelation(s, s.Mul(2), n, k) at 1 where s.Mul(2) is finite and self-correlation at 1, and ReturnCorrelation(s, s.Mul(-1), n, k) at -1, on windows whose returns are not flat.

RoofingCorrelation

func (s Series) RoofingCorrelation(s2 Series, n, hpLen, ssLen int) Series

RoofingCorrelation returns the rolling n-bar correlation of s and s2 after each is passed through a hpLen-bar high-pass filter and an ssLen-bar SuperSmoother.

Each input’s overlap is passed through a roofing filter in Ehlers’s sense (Cycle Analytics for Traders, Wiley, 2013): a two-pole high-pass filter, here HighPass(hpLen) in the Butterworth form of his later articles, then SuperSmoother(ssLen). Together they pass cycles between about ssLen and hpLen bars; his printed defaults are hpLen = 48 and ssLen = 10. The value at a bar is the Pearson correlation of the trailing n filtered values. It uses the shared co-moment kernel, with the filtered s2 as the regressor and the filtered s as the response.

Take the overlap views a of s and b of s2, and let fy = a.HighPass(hpLen).SuperSmoother(ssLen) and fx = b.HighPass(hpLen).SuperSmoother(ssLen). From overlap index n+2 on, the value equals fy.Correlation(fx, n) bit for bit. Correlating trending levels gives spurious correlation (Granger and Newbold, “Spurious regressions in econometrics”, Journal of Econometrics 2(2):111-120, 1974), and the high-pass stage removes those trends.

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, counted from the start of the overlap, is n+2, and the minimum overlap is n+3. The first valid index is the first window that excludes indices 0 to 2, where the filtered series are structurally zero. Any of the following gives a series of len(s) zeros, in O(len(s)) time whatever the size of n: an empty overlap, n < 2, hpLen < 1, ssLen < 1, or an overlap shorter than n+3. Every hpLen and ssLen of at least 1 is valid, however large, and costs O(len(s)) time.

The filters are recursive, so it is not start-invariant. Each value depends on every bar of the overlap before it, and moving the start of the overlap changes it.

The output lies in [-1, 1] for finite inputs whose second differences do not overflow, and a window in which either filtered series is flat gives exactly 0. The start-up transient of the filters is not masked: discard about 2hpLen bars when ssLen is well below hpLen, as with the defaults, and about 2(hpLen+ssLen) bars otherwise.

HighPass never reads the first value of its input. A NaN or an infinity at overlap index 0 of either input therefore changes nothing. One at overlap index p >= 1 makes every output from max(p, n+2) on NaN, because the filters carry it forever.

s.RoofingCorrelation(s, n, hpLen, ssLen) is exactly 1 wherever the filtered window is not flat. Take an integer-valued s and s2 = s + u*i + v, with integers u and v of moderate magnitude. The high-pass filter removes the line exactly, and the value is exactly 1 wherever the filtered window is not flat. An integer-valued ramp filters to exactly 0 and so gives 0.

Correlations of series that the filter has removed are meaningless, because only rounding residue is left. For example, a non-integer ramp such as 0.1*i+0.3 leaves a residue of about 1e-15 after HighPass. Two such residues can correlate at nearly -1 or 1, and at n = 2 exactly, by the sign rule.

Use it to measure how two markets move together within a band of cycles, free of the trends that dominate the correlation of their levels.

Verification: two independent references, which implement the library’s HighPass and SuperSmoother recursions, agree with it; it equals Correlation of the HighPass then SuperSmoother series from the first valid index on, bit for bit; and it is exactly 1 for s2 = s and for an integer series against itself plus an integer line.

SignCorrelation

func (s Series) SignCorrelation(s2 Series, n, k int) Series

SignCorrelation returns the rolling average over n bars of the product of the signs of the k-bar changes of s and s2, a concordance measure between -1 and 1.

At each pair j of the overlap from k onwards the k-bar changes are the differences between pair j and pair j-k of each input, the changes that Momentum(k) returns, and each is reduced to its sign, 1, -1 or 0. The output is the integer sum of the products of the two signs over the n most recent changes divided by n, float64(sum)/float64(n) in that order, so it lies in [-1, 1] in steps of 1/n: 1 when the two series moved the same way on every one of the n bars and -1 when they always moved opposite ways. A zero k-bar change in either series contributes 0, which shrinks the value on illiquid or forward-filled data.

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, counted from the start of the overlap, is n+k-1, and the minimum overlap is n+k. An empty overlap, n < 2, k < 1 or n+k > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the sizes of n and k; the last test never forms the sum n+k, so no argument, math.MaxInt included, can overflow.

Each output depends only on its own paired window of n+k bars, so the series is start-invariant: dropping leading bars of both inputs leaves the later outputs unchanged bit for bit. Any finite data is valid: the changes are differences, whose sign on positive levels matches that of simple and logarithmic changes, so the method is safe on back-adjusted futures and on negative levels, and scaling either input by a power of two leaves it unchanged unless a scaled value overflows or underflows. The n products are kept in an integer ring with their running sum and a count of the unusable changes in the window, so each bar costs O(1) and the sum is exact and does not drift.

A change is unusable when either of its two operands is not finite, and an output is NaN when any of its n changes in either series is unusable, so a non-finite value at overlap index p makes NaN exactly the outputs from p to p+n-1 and from p+k to p+k+n-1 that lie at or after the first valid index, and later outputs recover. An overflowing difference of finite values is an infinity with a sign and counts normally. A window in which either series never changes gives exactly 0.

For jointly normal changes with zero mean the Pearson correlation of the changes is approximately sin(pi*v/2), with v this value (Sheppard 1899 and Blomqvist 1950); a drift in either series biases the mapping upwards. With no zero changes it equals 2I - 1, where I is the concordance index with which Harding and Pagan (2002) compare business cycles, with the signs of k-bar changes in place of their dated cycle phases. The percent-agreement variant, the number of positive products divided by the number of non-zero products, is a composition: Sum(n) of the positive products divided by Sum(n) of the non-zero ones, which gives 0 when there are none. The phi coefficient of up and down changes, which corrects for unbalanced margins when one series rises far more often than the other, is SignPhi.

Verification: two independent references, a Python reference in exact integers and a Julia reference, agree with it exactly; on finite inputs it equals, bit for bit after warm-up, the composition that takes the signs of s.Momentum(k) and s2.Momentum(k) through GT(0) and LT(0), multiplies them, sums the products with Sum(n) and divides by float64(n); s2 = s gives the share of non-zero changes in the window and s2 = s.Mul(-1) its negation.

SignPhi

func (s Series) SignPhi(s2 Series, n, k, lag int) Series

SignPhi returns the phi coefficient of the up and down k-bar changes of s against those of s2 lag bars earlier over the trailing n pairs, a concordance measure between -1 and 1 that corrects for unbalanced up and down counts.

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 len(s) - min(len(s), len(s2)) + n + k + |lag| - 1 and the minimum overlap is n + k + |lag|.

The sign of lag is that of Shift: a positive lag pairs s with earlier changes of s2, a negative lag s2 with earlier changes of s, and both directions are causal.

The k-bar change of a series is its bar minus the bar k earlier, and only its sign is used: up, down or zero. In Sherry’s and Tarkany’s notation, with s the first series, a pair of non-zero changes enters one of four cells: A (s up and s2 up), B (s down and s2 up), C (s up and s2 down) or D (s down and s2 down). A pair with a zero change in either series, a -0 included, enters no cell (the tie rule). Sherry suggests resolving ties by a coin toss; counting ties as down is another possible rule, not built.

Over the n pairs of the window, phi is (AD - BC)/sqrt((A+B)(C+D)(A+C)(B+D)), and it is 0 when any margin A+B, C+D, A+C or B+D is zero. So 0 also appears after the warm-up, on a window with a zero margin or with AD = B*C.

Parameters with n < 2, k < 1, or n + k + |lag| above the overlap give zeros; lag = math.MinInt and math.MaxInt also give zeros, without overflow. A non-finite value at overlap index p of s or s2 makes NaN the outputs from p+e to p+e+n-1 and from p+e+k to p+e+k+n-1 that lie at or after the first valid index, with e = 0 for s and |lag| for s2 when lag >= 0, and the reverse when lag < 0; later outputs recover.

SignPhi accepts any finite data, and for positive prices the sign of a change is the same for simple, log and difference returns, up to rounding near a tie; a change that overflows to an infinity keeps its sign. The cells are exact counts, so SignPhi is start-invariant, bit for bit. Up to n = 9,741, where every margin product is exact, the swap identity holds bit for bit on inputs of equal length (s.SignPhi(s2, n, k, lag) equals s2.SignPhi(s, n, k, -lag)) and so does the self identity, 1 for s against itself and -1 against its negation at lag 0 on windows with both an up and a down change. Beyond that the margin product can round: a search found the self identity exact up to n = 393,217, and from n = 393,218 the identities, and the bound |phi| <= 1, can be off by an ulp.

Relation and uses: SignCorrelation is the average sign product S/n over the n bars of its window; phi corrects for unbalanced margins (a series that rises on most bars), which SignCorrelation does not. Tarkany’s approximation (SA - DI)/(SA + DI), that is (A + D - B - C)/(A+B+C+D), is SignCorrelation scaled by n over the number of non-tied pairs, and is not phi. The chi-square statistic is Nphiphi with N = A+B+C+D, which excludes ties; 3.84 and 6.635 are its 5% and 1% points for one degree of freedom, gauges rather than sequential tests: with k > 1 overlapping changes make the pairs dependent, and a scan over many lags finds chance hits.

Sources: Sherry described the two-by-two method (Stocks & Commodities 3:6); his example pairs a change with the other series’ change two bars earlier, lag +2 here. Tarkany applied it to weekly DJIA price and NYSE volume changes at lags of 0 to 2,311 weeks (Stocks & Commodities 5:3), to price and volume changes between Price Percent Filter swings (5:11), and to weekly common stock indicators at lags of up to about 236 weeks (8:4), shifting the second indicator forward (lag -1 here for a shift of one), where his cells A = 736, B = 55, C = 31, D = 465 give phi 0.8608616. Saitta’s quarterly “same or opposite” share (Stocks & Commodities 16:10) is the percent-agreement form of SignCorrelation on non-overlapping quarters, which needs true quarterly bars.

Warm-up hazard: before the first valid index the output is 0, which reads as no concordance; trim the warm-up with Window or mask it with SetN.

Verification: a Python reference and a Julia reference agree with SignPhi; on finite inputs the four cells equal Sum(n) of products of the GT(0) and LT(0) indicator series of the two Momentum(k) legs, with the lag applied by Shift, bit for bit; and Tarkany’s cells give his phi.

XCorrelation

func (s Series) XCorrelation(s2 Series, n int) Series

XCorrelation returns an exponentially weighted correlation of s with s2, seeded from the first n-bar window and then updated with smoothing factor 2/(n+1).

It is an exponentially weighted Pearson correlation: it applies the exponential weighting of RiskMetrics (RiskMetrics Technical Document, 4th edition, J.P. Morgan/Reuters, 1996, section 5.2, on exponentially weighted covariances and correlations) to deviations from exponentially weighted means, rather than to returns taken as zero-mean. The means and variances are updated as in West, “Updating mean and variance estimates: an improved method”, Communications of the ACM 22(9):532-535, 1979. Let alpha = 2/(n+1). Let x be the overlap of s2, which is the regressor, and y the overlap of s, which is the response.

At overlap index n-1, the seed bar, the shared co-moment kernel fits the first n-bar window, f := k.fit(x[:n], y[:n]). The means mx and my of the state are the fit’s real-unit means, f.xbar and f.ybar. The values vx, vy and c are its centred sums of squares and of cross-products, scaled back to real units, divided by n, that is, its population variances and covariance: vx = math.Ldexp(f.sxx/float64(n), 2f.ex), vy = math.Ldexp(f.syy/float64(n), 2f.ey) and c = math.Ldexp(f.sxy/float64(n), f.ex+f.ey). Dividing by n before scaling back gives the same bits as dividing afterwards wherever the result is a normal number, and keeps the moments finite for deviations up to the top of the float64 range, where the rebuilt centred sums would overflow; where the result is subnormal the last bits can differ, and accuracy is not claimed there. The seed bar’s value is the kernel’s correlation of that window, which is bit for bit the value of Correlation there. The seed bar emits that value directly because c/sqrt(vx*vy), formed from the seeded state, would not reproduce it bit for bit.

Each later bar updates the state in the Go operation order below. Each product dxdx, dydy and dxdy is formed on its own and only then scaled by alpha, so exchanging x and y, or negating one of them, gives every operation the same operands up to order and sign; this keeps the update symmetric in x and y where multiply-adds are fused, as on arm64, where each alpha(…) is fused with the addition before it. The float64 conversions only mark those roundings; no addition takes the products they wrap:

dx := x - mx
dy := y - my
mx += alpha * dx
my += alpha * dy
vx = (1 - alpha) * (vx + alpha*float64(dx*dx))
vy = (1 - alpha) * (vy + alpha*float64(dy*dy))
c = (1 - alpha) * (c + alpha*float64(dx*dy))

The value is 0 if vx <= 0 or vy <= 0. Otherwise it is c/sqrt(vxvy), clamped to [-1, 1]. When the product vxvy falls below the smallest normal number, 0x1p-1022 (a subnormal product has too few bits), or overflows to +Inf, the value is computed with one square root per variance instead, larger first, (c/sqrt(max(vx, vy)))/sqrt(min(vx, vy)), which does not depend on the order of the inputs, or as c/vx when the two variances are equal. At overlap index t, each bar after the seed bar carries the weight alpha*(1-alpha)^k, where k is its distance back from t. The seed window as a whole carries the remaining weight, (1-alpha)^(t-n+1).

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, counted from the start of the overlap, is n-1, and the minimum overlap is n. An empty overlap, n < 2 or n > min(len(s), len(s2)) gives a series of len(s) zeros. That takes O(len(s)) time whatever the size of n.

It is recursive, with infinite memory, so it is not start-invariant. Each value depends on every bar of the overlap before it, and moving the start of the overlap changes it. After the seed it costs O(1) time per bar.

The output lies in [-1, 1] while the deviations from the means stay between about 1e-162 and 1e154 in magnitude; below that the variances underflow and the output is 0, and above it a variance or the covariance overflows and later outputs are 0 or NaN. The clamp keeps [-1, 1] over that whole range, but accuracy to a few units in the last place holds only while both variances are normal numbers (deviations above about 1e-154). A NaN or an infinity at overlap index p makes every output from max(p, n-1) on NaN. Inside the seed window it spoils the seed; after the seed window it spoils the state, which never recovers.

The output is +0 at every bar, from the seed bar on, where the trailing n values of either input in the overlap are all equal. This is Correlation’s rule for a flat window, applied to the same n bars, so XCorrelation and s.Correlation(s2, n) are 0 by flatness on the same bars. As there, +0 and -0 count as equal, and a NaN takes precedence. The rule changes the output only: the state goes on updating through a flat stretch, and the value follows it again from the first bar where neither input’s trailing n values are all equal. When the rule first applies to a run that starts after the seed window, the flat bars carry 1-(1-alpha)^n of the weight, from about 89 per cent at n = 2 down to 1-e^-2, about 86.5 per cent, for large n. The value steps to 0 there from the recursion’s value, as Correlation’s does at a flat window: at n = 2 a single unchanged value gives 0, and a single move inside a long halt gives n-1 bars computed from it, then 0 again.

Without the rule, a flat stretch would show rounding noise in float64. The flat input’s deviation from its mean, its variance and the covariance shrink by the factor 1-alpha per bar at first, but the mean stops moving once alpha times the deviation is below half a unit in the last place of the mean, after about 15*(n+1) bars at ordinary levels, and the deviation freezes at up to about (n+1)/4 units in the last place. The rule applies once a value has repeated n-1 times, long before that. An input that is not exactly flat but moves only in its last bits, such as a derived series, or that decays towards 0 into the subnormal range, is outside the rule; there the value can become rounding noise anywhere in [-1, 1], so do not read meaning into it. After a long halt of both inputs the state holds little but the halt (after about 15*(n+1) bars, only rounding residuals), so the first bars after both resume are dominated by those bars alone: the first reads near -1 or 1, as Correlation’s first window after a halt of n-1 bars reads -1 or 1. Read the value from about n bars after a long halt.

s.XCorrelation(s, n) is exactly 1, and s.XCorrelation(s.Mul(-1), n) exactly -1, at every valid bar where the variances are positive and finite and the trailing n values of s are not all equal (both are 0 there), because the two states are the same expressions and equal variances give c/vx. s.XCorrelation(s2, n) equals s2.XCorrelation(s, n) bit for bit on inputs of equal length.

The X marks the exponential form, at the front of the name as in XAverage and XMA (BollingerX and IntermarketDisparityX put it at the end). Take the overlap views a of s and b of s2. Up to rounding, the means mx and my track b.XAverage(n) and a.XAverage(n), since all of them start from the first window’s mean. The centre of mass of the weights is (1-alpha)/alpha = (n-1)/2 bars back, so the effective memory is about n bars. Use it for a smooth correlation over long histories: after the seed each bar costs O(1) within a call, but each call recomputes the whole overlap. Compare Correlation for a hard n-bar window.

Verification: two independent references, a Python reference in exact rational arithmetic with high-precision square roots and a Julia reference, agree with it; it equals Correlation at the seed bar, and at every bar where the trailing n values of either input are all equal (both are 0); at ordinary levels self-correlation is 1, negation gives -1 and it is symmetric, bit for bit; and it tracks the closed-form exponentially weighted Pearson correlation.

Compositions

Katsanos’s correlation gates

Markos Katsanos uses a rolling correlation as a gate rather than a signal. His Chapter 16 asset-allocation entry (Intermarket Trading Strategies, 2008) requires the 130-bar correlation of the asset with the S&P 500 to be below 0.5 (Correl(C,SEC2,130,0) < .5), so that the rotation buys what moves independently of the benchmark. In “Trading The Nikkei” (Stocks & Commodities 35:8, 2017) his decoupling gate requires the 50-bar correlation of the 3-bar returns of DXJ and SPY to be below 0.8. Each is a comparison of the trimmed correlation, 1 where the gate is open and 0 where it is shut.

a, b := s.Overlap(s2)
ch16 := a.Correlation(b, 130).Window(-129).LT(0.5)
decouple := a.ReturnCorrelation(b, 50, 3).Window(-52).LT(0.8)
  • First valid index: 129 for the Chapter 16 gate and 52 for the decoupling gate, counted from the start of the overlap (in general n - 1 and n + k - 1). Minimum overlap: 130 and 53 bars.
  • Trim first: the untrimmed comparison reads the warm-up zeros as a correlation of 0 and opens the gate on every one of those bars.
  • Domain: the correlation’s. A window with a non-finite value gives a NaN correlation, as does, for the decoupling gate, a zero price, which is a zero base for its return; LT returns 0 for a comparison with NaN, so the gate is shut there. A correlation exactly equal to the threshold shuts the gate too: LT is strict, as Katsanos’s < .5 is. A flat window has a correlation of exactly 0, which opens it, except that flat zero prices give NaN in the decoupling gate and shut it.
  • Anchors: each gate equals the untrimmed method’s LT from the first valid index on (E); with s2 == s, the Chapter 16 gate is shut on every window that is not flat (the correlation is 1).
  • Pinned by TestRecipeCorrelationGates; runnable example ExampleSeries_Correlation_gates.

Percent agreement of signs

The share of bars on which the two legs moved the same way, among the bars on which both moved: the percent-agreement form of SignCorrelation. Saitta’s quarterly “same or opposite” direction share (Stocks & Commodities 16:10) is this reading on non-overlapping quarters.

a, b := s.Overlap(s2)
p := a.Momentum(k).Mul(b.Momentum(k)).Window(-k)
agree := p.GT(0)
agreement := agree.Sum(n).Div(agree.Add(p.LT(0)).Sum(n)).Window(-(n - 1))
  • First valid index: n + k - 1, counted from the start of the overlap; element e is overlap bar n + k - 1 + e. Minimum overlap: n + k bars.
  • Parameters: n >= 2 and k >= 1; n = 1 or k = 0 makes a trim Window(0), which returns an empty series.
  • Counting: a bar with a zero change in either leg counts in neither total, and a NaN change compares false both ways and also counts in neither, unlike SignCorrelation, which keeps zero changes in its n and is NaN while a non-finite change is in its window. A window with no non-zero product gives 0, because Div by zero gives 0.
  • Anchors: s2 == s gives exactly 1 on windows with a change of at least about 1.6e-162 in size (E); a product of changes smaller than about 2.5e-324 underflows to 0 and counts in neither total; s2 = -s gives 0 (E); on windows without zero changes or underflowing products it is (1 + SignCorrelation(s2, n, k))/2 within 1e-14; it lies in [0, 1].
  • Pinned by TestRecipeT3SignAgreement; runnable example ExampleSeries_Momentum_signAgreement.

Average correlation of returns, and the hub variant

AverageCorrelation correlates whatever it is given. Parzen correlates daily returns, and returns are the recommended basis (see ReturnCorrelation), so pass trimmed return series. The one-to-many “hub” average of Landazabal, the mean of one series’ correlations with each member, is a composition of Correlation (the Updata Traders’ Tip for Cline’s article averages R-squared instead).

// The return form: the mean pairwise correlation of k-bar percent returns.
ra := a.RateOfChange(k).Window(-k)
avg := ra.AverageCorrelation(n, b.RateOfChange(k).Window(-k), c.RateOfChange(k).Window(-k))

// The hub variant: the mean of a's correlations with each member.
hub := a.Correlation(b, n).Add(a.Correlation(c, n)).Div(2.0)
  • First valid index: n + k - 1 for the return form, counted from the start of the common overlap (element e of the trimmed result is bar e + k of a; on equal lengths the first valid element is n - 1); n - 1 for the hub variant, counted from the start of the latest-starting pair overlap. Minimum overlap: n + k and n bars.
  • The return form equals, bit for bit on positive inputs of equal length, the mean of the members’ pairwise ReturnCorrelation values summed in the method’s pair order, (a, b), (a, c), (b, c), each with the earlier series as the base, divided by the pair count; RateOfChange gives 0 at a zero base, so use it on positive prices.
  • The hub variant with K - 1 members divides by K - 1, not by the pair count of the full set: it averages only the hub’s pairs. With one member it is Correlation (E). With members of unequal lengths each term starts n - 1 bars into its own pair’s overlap, and until every term is valid the sum mixes in warm-up zeros; the hub is valid from the latest such index, so give the members equal lengths or mask to it.
  • Pinned by TestRecipeAverageCorrelationReturnForm and TestRecipeAverageCorrelationHub.

Significance-scaled correlation (Fisher z and t)

The rolling correlation r of a and b over n bars, rescaled two ways for reading against a table. Fisher’s z, atanh(r) times sqrt(n - 3), is close to a standard normal variable when the true correlation is 0 (Fisher’s transformation of 1921, whose standard error is 1/sqrt(n - 3)). The t statistic, r*sqrt((n - 2)/(1 - r^2)), follows Student’s t with n - 2 degrees of freedom under the same hypothesis, for independent pairs from a normal distribution. The z uses Fisher, which clamps its input to ±0.999 (Ehlers’s code sets values beyond ±0.99 to ±0.999), not Atanh, so it saturates at 3.8002011672501999 times sqrt(n - 3). The t statistic is written as r times ((1 - r^2)/(n - 2)) to the power -1/2, so that |r| = 1 gives ±Inf; written with Div, the quotient would read 0 there. Bartlett (1935) showed that autocorrelation shared by both series widens the spread of r, so these scales assume independent pairs: pass returns, not price levels, whose trends correlate by chance. Katz and McCormick (Stocks & Commodities 15:7) report t statistics for the serial correlation of a system’s trades; no article found in the journal uses Fisher’s z.

a, b := s.Overlap(s2)
corr := a.Correlation(b, n)
z := corr.Fisher().Mul(math.Sqrt(float64(n - 3))).Window(-(n - 1))
tstat := corr.Mul(corr.Mul(corr).Mul(-1).Add(1).Div(float64(n - 2)).Pow(-0.5)).Window(-(n - 1))
  • First valid index: n - 1 for both, counted from the start of the overlap; element e of each is overlap bar n - 1 + e. Minimum overlap: n bars. On trimmed k-bar returns, as a.RateOfChange(k).Window(-k), add k.
  • Parameters: n >= 4: at n = 3 the z is identically 0, and below it NaN.
  • Reading: at the 5% level, two-sided, |z| above 1.96 rejects a zero correlation, as does |t| above Student’s critical value for n - 2 degrees of freedom (2.01 at n = 50). Overlapping k-bar returns sampled every bar hold fewer independent pairs, about 3kn/(2k^2 + 1) for uncorrelated returns (see CorrelationBreak), so put that in place of n before reading a table; and successive bars test overlapping windows, not independent samples.
  • Domain: a flat window in either input gives a correlation of 0, so z = t = 0; |r| = 1 gives z at its clamp and t = ±Inf; a window holding a NaN or an infinity gives NaN.
  • Anchors: s2 = f*s with f a positive power of two (s2 == s at f = 1) gives z = Series{0.999}.Fisher()[0]*math.Sqrt(float64(n-3)) and t = +Inf on windows that are not flat, both 0 on flat ones, and f = -1 gives their negatives (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E). The t statistic equals r*math.Sqrt(float64(n-2)/(1-r*r)) within 1e-15*|t|/(1 - r*r).
  • Pinned by TestRecipeT4ACorrelationSignificance; runnable example ExampleSeries_Fisher_correlationSignificance.

Fast-against-slow correlation shift

The Fisher z of the fast correlation over nf bars less that of the slow one over ns bars, divided by sqrt(1/(nf - 3) + 1/(ns - 3)), the standard error of the difference of two independent z values: positive where the recent correlation runs above the longer one. Both windows end at the current bar, so the shorter lies inside the longer, the two z values are correlated, and that standard error overstates the spread of their difference; the shift is a scale, not a test. CorrelationBreak is the tested break test: it compares the fast window with a slow window that ends just before the fast one begins. Published practice judges a relationship by the stability of block correlations instead: Saitta (Stocks & Commodities 18:7) averages the correlations of five four-year periods and takes their average absolute deviation, and Katsanos (38:9) tabulates correlations over trailing windows of seven years, two years, one year and three months.

a, b := s.Overlap(s2)
fast := a.Correlation(b, nf).Fisher()
slow := a.Correlation(b, ns).Fisher()
shift := fast.Sub(slow).Div(math.Sqrt(1/float64(nf-3) + 1/float64(ns-3))).Window(-(max(nf, ns) - 1))
  • First valid index: max(nf, ns) - 1, counted from the start of the overlap; element e is overlap bar max(nf, ns) - 1 + e. Minimum overlap: max(nf, ns) bars.
  • Parameters: nf >= 4 and ns >= 4; normally nf < ns.
  • Domain: a window holding a NaN gives NaN; each z saturates at about 3.8 (the clamp of Fisher), which bounds the shift.
  • Anchors: nf == ns gives exactly +0 wherever the correlation is a number (E); swapping nf and ns negates the shift (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E).
  • Pinned by TestRecipeT4ACorrelationSignificance.

Correlation stability statistics

Four summaries of a correlation series c over its last m values: its standard deviation (the correlation volatility), its range, and its 10th and 90th percentiles (Quantile, type 7). They are rows of Katsanos’s Table 5.1 (Intermarket Trading Strategies), which summarises the yearly correlations of the S&P 500’s daily prices with ten world indices from 1992 to 2007 by their average, median, standard deviation, range and 10th and 90th percentiles. He gives the standard deviation of calendar-year correlations again in Stocks & Commodities 30:1, and percentiles too in 38:9. Here they summarise m consecutive values of a rolling correlation, whose windows overlap, rather than disjoint years. Pass c already trimmed to its valid bars, as a.Correlation(b, n).Window(-(n - 1)): warm-up zeros would enter the statistics as correlations of 0.

dispersion := c.StdDev(m).Window(-(m - 1))
span := c.Highest(m).Sub(c.Lowest(m)).Window(-(m - 1))
low := c.Quantile(m, 0.1).Window(-(m - 1))
high := c.Quantile(m, 0.9).Window(-(m - 1))
  • First valid index: m - 1 for all four, counted from the start of c; element e of each is bar m - 1 + e of c. Minimum history: m values of c. On a.Correlation(b, n).Window(-(n - 1)) element e is overlap bar n + m - 2 + e.
  • Parameters: m >= 2. From m = 11 on, a single extreme value has no weight in the 10th or 90th percentile, which makes them steadier than the range.
  • Domain: a window holding a NaN gives NaN in all four; otherwise, on a correlation, 0 <= span <= 2 and the 10th percentile is at most the 90th.
  • Anchors: on c as above the dispersion is the correlation volatility c.StdDev(m), trimmed by m - 1 (E); a flat c gives a dispersion and a range of 0 and both percentiles equal to c (E).
  • Pinned by TestRecipeT4ACorrelationStability.

Correlation regimes (Katsanos)

The sign of a correlation and its change mark regimes: Katsanos shades the alternating periods of positive and negative correlation between the S&P 500 and the yen, which turned negative when the carry trade unwound (Intermarket Trading Strategies, Figure 5.6). The block takes the return correlation, the recommended basis (see ReturnCorrelation), trims it to its valid bars, summarises it with correlationStability from the section above, and adds the positive regime (1 where the correlation is above 0) and the change of the correlation over h bars. Katsanos’s Table 5.1 correlates price levels; returns avoid the chance correlation of two trends.

a, b := s.Overlap(s2)
c := a.ReturnCorrelation(b, n, k).Window(-(n + k - 1))
dispersion, span, low, high := correlationStability(c, m)
positive := c.GT(0)
change := c.Delta(h).Window(-h)
  • First valid index: n + k - 1 for c and positive, n + k + m - 2 for the four statistics and n + k - 1 + h for change, counted from the start of the overlap; element e of each is overlap bar F + e for its index F. Minimum overlap: n + k bars (n + k + m - 1 for the statistics, n + k + h for change).
  • Parameters: n >= 2, k >= 1, m >= 2 and h >= 1; a calendar year of daily one-bar returns is about n = 252, k = 1.
  • Domain: ReturnCorrelation’s: a non-finite price at overlap bar p gives NaN on the windows holding the return at p or p + k, and a zero price on those holding the return at p + k, where it is the base. GT reads a comparison with NaN as false, so positive is 0 there, and change is NaN.
  • Anchors: the four statistics are those of the section above on this c (E).
  • Pinned by TestRecipeT4ACorrelationRegimes.

Return-horizon profile (Katsanos)

The correlation of two markets’ percent returns at several return horizons, read at the last bar. Katsanos tabulates such profiles: the S&P 500 against commodity and financial indices on daily to quarterly returns (Intermarket Trading Strategies, Table 5.7); gold against the XAU and the dollar index over return intervals of one day upwards (Table 7.6 and Figure 7.12, where he suggests nine days for short- to medium-term systems); and index futures on intraday changes over up to 1,500 minutes (Tables 6.7 and 8.1). horizons lists the horizons in bars.

profile := make([]float64, len(horizons))
for i, k := range horizons {
    profile[i] = s.ReturnCorrelation(s2, n, k).Value()
}
  • First valid index: n + k - 1 for horizon k, counted from the start of the overlap; element i of the profile is the correlation at horizon horizons[i] at the overlap’s last bar. Minimum overlap: n + max(horizons) bars; with a shorter overlap the longer horizons read 0, which looks like no correlation.
  • Parameters: n >= 2 and every horizon >= 1.
  • Power law: Katsanos fits his intraday profiles with power laws in the horizon T in minutes, 0.677*T^0.04 for the e-mini S&P 500 against the DAX and 0.87*T^0.01 for the DAX against the Euro Stoxx, and converts a correlation at one horizon to another with r_i = r_j*(T_i/T_j)^0.04: both give 0.797 for 60 minutes, from the fit and from the 15-minute value 0.754 (0.79747 and 0.79699 unrounded). The fits are of one pair over three months of 5-minute bars, exceed 1 at long enough horizons (beyond about 17,000 minutes for the first) and mean nothing for a negative correlation, so they are not built.
  • Domain: ReturnCorrelation’s, at each horizon.
  • Anchors: each element equals ReturnCorrelation at the last bar for its horizon, by construction.
  • Pinned by TestRecipeT4AHorizonProfile.

Session-masked correlation

Katsanos correlates US and European index futures on 30-minute changes (Intermarket Trading Strategies, section 8.4). He deletes the US bars while the European futures are closed, so that bars on which one contract was not trading do not enter the correlation, and splits the day into five segments, such as the data until 14:30 or after 15:30 Central European time; his segments read as cumulative from the open. A Series carries no timestamps, so the session is a 0/1 mask aligned bar for bar with the overlap, built upstream: the correlation runs on the kept bars only, joined end to end, and a signal of the same length is gated by multiplying it by the mask.

a, b := s.Overlap(s2)
var sa, sb Series
for j, keep := range mask {
    if keep != 0 {
        sa = append(sa, a[j])
        sb = append(sb, b[j])
    }
}
corr := sa.Correlation(sb, n).Window(-(n - 1))
gated := signal.Mul(mask)
  • First valid index: n - 1 for corr, counted from the first kept bar; element e is kept bar n - 1 + e, the (n + e)th kept bar. gated has first valid index 0, overlap bar e. Minimum overlap: n kept bars.
  • Inputs: pass changes, as Katsanos does, computed on the full bar grid: the first change after a closed period then spans the closure, which the mask can drop by marking that bar 0.
  • Domain: Correlation’s on the kept bars: a flat window gives 0 and a window holding a NaN or an infinity NaN. A kept window can span a gap between sessions. Mul right-aligns mask with signal at their most recent bar and keeps the signal’s length, so a signal longer than the mask reads 0 before the mask’s first bar.
  • Anchors: a mask of all ones gives a.Correlation(b, n).Window(-(n - 1)) (E); for a finite 0/1 signal, gated equals signal where the mask is 1 and 0 where it is 0 (E).
  • Pinned by TestRecipeT4ASessionMask.

Absolute-return correlation

The rolling correlation of the two legs’ absolute one-bar percent returns: whether large moves, of either sign, come together. It discards the signs, so a pair whose returns move in opposite directions reads like one whose returns move together, and it complements the return correlation, which can be near 0 for markets whose volatility rises and falls together. It is derived, with no published source.

a, b := s.Overlap(s2)
corr := a.RateOfChange(1).Window(-1).Abs().Correlation(b.RateOfChange(1).Window(-1).Abs(), n).Window(-(n - 1))
  • First valid index: n, counted from the start of the overlap; element e is overlap bar n + e. Minimum overlap: n + 1 bars.
  • Parameters: n >= 2.
  • Domain: RateOfChange gives a spurious 0 return at a zero base (ReturnCorrelation gives NaN there); a window in which either leg’s absolute returns are flat gives 0; a window holding a NaN or an infinite return gives NaN.
  • Anchors: s2 == s, s2 = -s and s2 = 2^p*s give exactly 1 on windows whose absolute returns are not flat, and 0 on flat ones (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E).
  • Pinned by TestRecipeT4AAbsoluteReturnCorrelation.

Relative: Regression and Spread

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.

DownsideBeta

func (s Series) DownsideBeta(s2 Series, n int, tau float64) Series

DownsideBeta returns the rolling least-squares slope of s on s2 over the bars of the trailing n-bar window in which s2 is below tau.

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 len(s) - min(len(s), len(s2)) + n - 1 and the minimum overlap is n.

The bars of the window with s2 below tau form the subset. It is gathered in time order and fitted as Beta fits a window, with its own means rather than those of the window. A bar with s2 equal to tau is in neither subset (UpsideBeta takes the bars above tau). Fewer than two bars, or a flat subset (all its s2 values equal), give 0.

Every window holding a NaN or an infinity in either input gives NaN, whatever the subset, with recovery once the value has left the window. Invalid parameters give zeros: n < 2, a non-finite tau, or n above the overlap.

The threshold tau is in the units of s2 (0 on returns is the usual choice). The inputs are used as given: for a market beta pass returns, for example from RateOfChange(1), and the value is then dimensionless. The result is start-invariant: each value depends only on the bars of its own window. A nearly flat subset makes the slope explode, and one beyond the float64 range is ±Inf, as for Beta.

The fit uses only the subset’s bars, which number between 0 and n and vary from window to window, so the slope is noisier than Beta over the same n. Under a linear relation whose noise is independent of s2, the downside and upside slopes estimate the same beta, so a gap between them can be noise as well as asymmetry.

On finite data, tau = math.MaxFloat64 puts the whole window in the subset, so the result equals Beta bit for bit, except where the window holds an s2 value equal to math.MaxFloat64. For UpsideBeta the threshold with the same effect is tau = -math.MaxFloat64.

Zero hazard: the output is 0 before the first valid index and on every later window whose subset has fewer than two bars or is flat (at tau = 0, DownsideBeta is 0 throughout a steady rise of s2); a defensive screen such as DownsideBeta < 1 passes on all of them. Trim the warm-up with Window or mask it with SetN, and mask the small subsets separately, for example where s2.LT(tau).Sum(n) is below 2.

Uses: asymmetric hedging and defensive screening. A downside beta below the upside beta means s responded less to the size of the moves of s2 on the bars below tau than on those above it. Each slope has its own intercept, so it says nothing about how far s fell on average: a stock that loses the same amount on every bar below tau has a downside beta of 0. UpCapture and DownCapture are related ratio-of-sums measures, not slopes: they take levels, form one-bar percent changes, fix the threshold at 0 and report 100 times the ratio of the sums of the two inputs’ returns, with no intercept.

Sources: Ang, Chen and Xing, “Downside Risk”, Review of Financial Studies 19(4):1191-1239 (2006), condition on the market’s excess return below or above its mean and estimate a downside-risk premium of about 6% a year. Bawa and Lindenberg, Journal of Financial Economics 5(2):189-200 (1977), measure downside risk with co-lower partial moments about a fixed target; that beta is centred at the target, not at the subset’s means, so it is not this slope. Peterson asks for “separate measures of downside and upside performance” (Stocks & Commodities 19:13).

Verification: agrees with an independent Python reference and an independent Julia reference, and equals Beta bit for bit at tau = math.MaxFloat64.

DownsideBetaACX

func (s Series) DownsideBetaACX(s2 Series, n int) Series

DownsideBetaACX returns DownsideBeta with the threshold set to the window mean of s2, following Ang, Chen and Xing.

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 len(s) - min(len(s), len(s2)) + n - 1 and the minimum overlap is n.

A bar of the window is classified by the sign of the centred deviation of s2 from its window mean, as the co-moment kernel of Beta computes it: a negative deviation puts the bar in the downside subset, and a bar with zero deviation is in neither subset. The subset is gathered in time order and fitted as Beta fits a window, with its own means. The classification is exact when the differences from the window’s first value, their sum and the mean are exact in float64, as for windows of integers, or of dyadic fractions with a common power of two, whose span times n is below 2^52. Otherwise a bar can be classified differently from exact arithmetic only when it lies within a few units of 2^-52 times the window’s largest deviation from its first value; such a bar usually gets a zero deviation and falls in neither subset (on returns quoted to two decimals, about one bar in 500 sits on such a decimal tie with the mean). So the value need not equal the fixed-threshold method with tau set to the rounded window mean on quantised data. A flat s2 window gives 0, since it has no deviations and both subsets are empty; so do fewer than two bars in the subset.

Every window holding a NaN or an infinity in either input gives NaN, whatever the subset, with recovery once the value has left the window. Invalid parameters give zeros: n < 2 or n above the overlap. The result is start-invariant and, on returns, dimensionless. DownsideBeta gives the uses and sources.

Zero hazard: as for DownsideBeta, the output is 0 before the first valid index and on every later window whose subset has fewer than two bars or is flat, which passes a defensive screen such as DownsideBetaACX < 1; trim the warm-up with Window or mask it with SetN, and mask the small subsets separately.

Verification: agrees with an independent Python reference and an independent Julia reference, and each value equals the last value of the kernel beta applied to the gathered subset. On integer windows whose mean is an integer it equals DownsideBeta with that mean as tau.

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. DickeyFuller(n) is the t-statistic of this coefficient, b sqrt((n-2) Sxx/SSR) on the same kernel, so its sign is this one’s wherever neither is 0. 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 rounds its co-moment products on their own too, so the value has the same bits on 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.

TwoFactorBeta

func (s Series) TwoFactorBeta(x1, x2 Series, n int) Series

TwoFactorBeta returns the rolling coefficient on x1 in the least-squares regression of s on x1 and x2 with an intercept.

With M11, M22 and M12 the centred sums of squares and products of x1 and x2 over the window, and M1y and M2y those of each regressor with s, each variable anchored at its first value and scaled by its own power of two, a flat x2 gives s.Beta(x1, n) bit for bit, a flat x1 gives 0, and otherwise Go evaluates det := float64(M11M22) - float64(M12M12), gives 0 when det <= collinearKappafloat64(M11M22) with collinearKappa = 1e-9, and returns (float64(M1yM22) - float64(M2yM12))/det rescaled to units of s per unit of x1. The float64 conversions round each product of the determinant and the numerator before it is used, so no fused multiply-add enters them on any architecture; the co-moments come from the kernel, which rounds its products on their own too, so the value has the same bits on arm64 and amd64. Negating s or x1 negates the value, negating x2 leaves it unchanged, and scaling s by 2^p and x1 by 2^q scales it by 2^(p-q). The coefficient on x2 is s.TwoFactorBeta(x2, x1, n). On levels the value is in units of s per unit of x1; on returns it is dimensionless. A coefficient beyond the float64 range is ±Inf. One below the smallest subnormal underflows to a zero with its sign, as Beta’s slope does.

s is the response and x1 and x2 the regressors; the three inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(x1), len(x2)) bars are used, the result has len(s), and bars before the overlap are zero. The first valid index is len(s) - min(len(s), len(x1), len(x2)) + n - 1 and the minimum overlap is n. An empty overlap, n < 3 or n greater than the overlap gives a series of len(s) zeros.

A NaN or an infinity in any input makes NaN every output whose window contains it, whatever the other rules give, and later outputs recover. Collinear regressors give 0 through the determinant guard: x2 equal to x1, or to a multiple of x1 plus a constant, and any pair whose correlation over the window is within about 5e-10 of 1 or -1. An exact fit is not zeroed: it determines the coefficient. s.TwoFactorBeta(s, x2, n) is exactly 1, and x1 equal to s times 2^k gives exactly 2^-k, wherever s is not flat and the determinant guard passes, and s.TwoFactorBeta(x1, s, n) is exactly 0.

Each value depends only on its own window, in which each variable is anchored at its first value, so the result is bitwise start-invariant and invariant to prepending history before the overlap.

Warm-up hazard: before the first valid index the output is 0, which passes a gate such as a beta below 1; trim the warm-up with Window or mask it with SetN.

Uses: Schmidt hedges a stock with short positions of beta_MW in SPY and beta_IW in its industry or sector ETF, where W is the capital in the stock, with both betas estimated from daily returns over 252 trading days, re-estimated as the window moves (“Then I moved the trading window by one day”): on the stock’s returns r, the market’s rm and the industry’s ri, beta_M is r.TwoFactorBeta(rm, ri, 252) and beta_I is r.TwoFactorBeta(ri, rm, 252). A hedged return held over the next bar uses the previous bar’s betas, a composition with Shift(1).

Sources: Schmidt, “Improving Stock Hedging With Industry Beta”, Stocks & Commodities 43:3 (March 2025), equation 3 on page 15, r_i = alpha_i + beta_iMr_M + beta_iIr_I + eps_i, the two-factor extension of the market model; the article prints an estimator only for the one-factor beta, and this method is the standard least-squares fit of equation 3.

Compositions that start from it are listed under “Compositions” in the “Relative: Regression and Spread” category.

Verification: two references, a Python reference in exact rational arithmetic and a Julia reference that solves each window’s least-squares problem, agree with it; a flat x2 gives Beta of s on x1 bit for bit, a flat x1 gives 0, x2 equal to x1 gives 0 through the determinant guard, and s.TwoFactorBeta(s, x2, n) is exactly 1 wherever s is not flat and the guard passes.

UpsideBeta

func (s Series) UpsideBeta(s2 Series, n int, tau float64) Series

UpsideBeta returns the rolling least-squares slope of s on s2 over the bars of the trailing n-bar window in which s2 is above tau.

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 len(s) - min(len(s), len(s2)) + n - 1 and the minimum overlap is n.

It is DownsideBeta with the subset reversed: the bars of the window with s2 above tau are gathered in time order and fitted as Beta fits a window, with their own means rather than those of the window. A bar with s2 equal to tau is in neither subset. Fewer than two bars, or a flat subset, give 0.

Every window holding a NaN or an infinity in either input gives NaN, whatever the subset, with recovery once the value has left the window. Invalid parameters give zeros: n < 2, a non-finite tau, or n above the overlap. The threshold tau is in the units of s2, the result is start-invariant and, on returns, dimensionless. DownsideBeta gives the uses and sources.

On finite data, tau = -math.MaxFloat64 puts the whole window in the subset, so the result equals Beta bit for bit, except where the window holds an s2 value equal to -math.MaxFloat64.

Zero hazard: as for DownsideBeta, the output is 0 before the first valid index and on every later window whose subset has fewer than two bars or is flat; trim the warm-up with Window or mask it with SetN, and mask the small subsets separately.

Verification: agrees with an independent Python reference and an independent Julia reference, and equals Beta bit for bit at tau = -math.MaxFloat64.

UpsideBetaACX

func (s Series) UpsideBetaACX(s2 Series, n int) Series

UpsideBetaACX returns UpsideBeta with the threshold set to the window mean of s2, following Ang, Chen and Xing.

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 len(s) - min(len(s), len(s2)) + n - 1 and the minimum overlap is n.

A bar of the window is classified by the sign of the centred deviation of s2 from its window mean, as the co-moment kernel of Beta computes it: a positive deviation puts the bar in the upside subset, and a bar with zero deviation is in neither subset. The subset is gathered in time order and fitted as Beta fits a window, with its own means. The classification is exact when the differences from the window’s first value, their sum and the mean are exact in float64, as for windows of integers, or of dyadic fractions with a common power of two, whose span times n is below 2^52. Otherwise a bar can be classified differently from exact arithmetic only when it lies within a few units of 2^-52 times the window’s largest deviation from its first value; such a bar usually gets a zero deviation and falls in neither subset (on returns quoted to two decimals, about one bar in 500 sits on such a decimal tie with the mean). So the value need not equal the fixed-threshold method with tau set to the rounded window mean on quantised data. A flat s2 window gives 0, since it has no deviations and both subsets are empty; so do fewer than two bars in the subset.

Every window holding a NaN or an infinity in either input gives NaN, whatever the subset, with recovery once the value has left the window. Invalid parameters give zeros: n < 2 or n above the overlap. The result is start-invariant and, on returns, dimensionless. DownsideBeta gives the uses and sources.

Zero hazard: as for DownsideBeta, the output is 0 before the first valid index and on every later window whose subset has fewer than two bars or is flat; trim the warm-up with Window or mask it with SetN, and mask the small subsets separately.

Verification: agrees with an independent Python reference and an independent Julia reference, and each value equals the last value of the kernel beta applied to the gathered subset. On integer windows whose mean is an integer it equals UpsideBeta with that mean as tau.

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 = 8 for the intercept of n = 9 returns and 12 - 1 = 11 for the average. In general, with n returns and an average of w bars, it is n + w - 1. alpha has length m - 9 for an overlap of m bars; its first 11 elements are Average’s pass-through values, not zeros, so read valid.
  • 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 next n + w = 21 values of alpha NaN, after which the recipe recovers. Delta keeps 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, because Intercept(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 of s): a series growing by a constant factor gives such windows, and a constant price gives 0. The chain on the Overlap results 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 example ExampleSeries_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: Intercept and Beta are valid from n - 1, and Shift(1) moves each window’s fit to the next bar. oos has length m for an overlap of m bars; its first n elements subtract warm-up zeros and are not residuals, so read valid.
  • Minimum overlap: n + 1 bars, 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 and Intercept(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 the Overlap results 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 example ExampleSeries_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. ReturnBeta uses the signed percent return and g the plain ratio of Div, which agree only on positive s2, and Div gives 0 for a zero divisor. The factor of 100 in ReturnBeta’s percent returns cancels in the beta.
  • Anchors: a constant positive s2 gives exactly a.Momentum(1) on valid bars, because the beta of a flat regressor is 0 and g is 0 (tag E, through Momentum); the composition property (E).
  • Pinned by TestRecipeCAPMDelta; runnable example ExampleSeries_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: n bars.
  • Domain: positive levels: a.Sub(sp) is the fitted level, and Div gives 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 == s gives exactly 0 (tag E, through the perfect-fit guard’s exact zero); the composition property (E).
  • Pinned by TestRecipeADDO; runnable example ExampleSeries_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 - 1 for the spread and n + m - 2 for the position, counted from the start of the overlap; element e of each is overlap bar n - 1 + e or n + m - 2 + e. Minimum overlap: n + m - 1 bars for the position.
  • Parameters: n >= 2 and m >= 2; a window of 1 makes a trim Window(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, is RegressionSpread.
  • Convention: the position uses the population deviation; Chan’s movingStd is the sample deviation, which scales it by sqrt((m-1)/m).
  • Domain: a non-finite price gives NaN for the windows holding it; a flat s2 window gives a hedge ratio of 0, so the spread is s there.
  • Anchors: on windows where s2 is not flat, s2 == s and s = 2^k*s2 give a spread of exactly 0 (E); on a flat s2 window the spread is s. Other exactly collinear pairs, such as s = 3*s2 + 7, leave rounding noise that the position reports at full scale (|z| reaches 3 on a random walk); RegressionZScore guards against this.
  • Pinned by TestRecipeT3HedgedSpread.

Static spread and log spread

The spread of s against s2 with a fixed hedge ratio h, s - h*s2, and its log form ln(s) - h*ln(s2), with h fitted elsewhere. Ernest Chan’s pair trade of GLD against GDX (Quantitative Trading, Example 3.6) fits the hedge ratio by regressing GLD on GDX over a 252-day training set, forms the spread with it over the whole history, and trades the spread’s z-score against the training set’s mean and standard deviation, entering at two deviations and leaving within one; his Example 7.2 tests the same pair for cointegration. Chan’s spreads are of prices. The log spread’s hedge ratio is an elasticity, and at h = 1 the log spread is the log ratio. A rolling hedge ratio is “Chan’s rolling-hedge spread and its position” (this category), and a band around a fixed-ratio spread is “Simple spread Bollinger band” (Relative: Channels).

a, b := s.Overlap(s2)
spread := a.Sub(b.Mul(h))
logSpread := a.Log().Sub(b.Log().Mul(h))
  • First valid index: 0 for both, counted from the start of the overlap; element e of each is overlap bar e. Minimum overlap: 1 bar.
  • Parameters: h finite. The spread is in units of s, and h in units of s per unit of s2; the log spread has no units.
  • Domain: NaN propagates. The log form is -Inf at a zero price of s, +Inf at a zero price of s2 (for h > 0) and NaN at a negative price, whereas LogRatio gives NaN at every price that is not positive and finite.
  • Anchors: at h = 0 the spread is s wherever s2 is finite, and the log spread ln(s) wherever s2 is finite and positive (E); at h = 1 the log spread equals a.LogRatio(b) on finite positive prices (E), and is -Inf at a zero price of s, +Inf at a zero price of s2 and NaN at a negative price; s2 == s on finite positive prices gives both spreads exactly 0 at h = 1 (E); dropping leading bars from both inputs leaves the later values unchanged, bit for bit (E).
  • Pinned by TestRecipeT4ASpreads; runnable example ExampleSeries_Sub_staticSpread.

Futures dollar spread (Taucher)

Frank Taucher (Stocks & Commodities 5:9) values a futures spread in dollars: each leg’s price times its contract’s unit size, then the difference, so that a change in the dollar spread is the profit of one spread position. His spread history printout is long March against short November feeder cattle, whose contract of 44,000 pounds is priced in cents a pound, so each leg’s multiplier is 440 dollars a cent; it lists an entry row and an exit row for each of 1978, 1982 and 1986. bought and sold are the two legs’ prices.

a, b := bought.Overlap(sold)
spread := a.Mul(440).Sub(b.Mul(440))
profit := spread.Delta(1).Window(-1)
  • First valid index: 0 for the dollar spread and 1 for the profit, counted from the start of the overlap; element e of the spread is overlap bar e, and of the profit overlap bar 1 + e. Minimum overlap: 1 bar (2 for the profit).
  • Printed values: on Taucher’s six rows the dollar spreads are 924, 2915, 176, 473, -385 and -242, and elements 0, 2 and 4 of the profit, from each entry row to its exit row, are his profits of 1991, 297 and 143 dollars; the other elements span the gap from one year’s exit to the next year’s entry and mean nothing. The recipe gives them within half a dollar (924.0000000000036 and so on, since the prices are not binary fractions).
  • Unequal legs: give each leg its own multiplier, for example a.Mul(440).Sub(b.Mul(400)) for feeder cattle (44,000 pounds) against live cattle (40,000 pounds), an inter-commodity spread Taucher also mentions. Spreads of more than two legs, such as Perry Kaufman’s 3:2:1 crack spread (Stocks & Commodities 44:4: three crude oil contracts of 1,000 barrels against two gasoline contracts and one heating oil contract of 42,000 gallons), chain Mul, Add and Sub over overlap views of every leg; the library has no weighted-sum method yet.
  • Domain: Mul’s and Sub’s: NaN and infinities propagate. A dollar spread can be negative or zero, so it has no log form; use point changes, as the profit does, not percent changes.
  • Pinned by TestRecipeT4ADollarSpread.

Smoothed rate spread (Kaeppel)

Jay Kaeppel (Stocks & Commodities 8:2) times the stock market with the Federal Reserve discount rate less the three-month Treasury bill auction yield, smoothed by a 15-week exponential average: he buys when the average turns positive, with bills yielding less than the discount rate, and stays in until it falls back below zero. His worked update takes a previous average of -0.70 and a new spread of -0.95 (a 7% discount rate and a 7.95% bill yield, misprinted as 7.9% in the subtraction) to -0.70*0.875 + (-0.95)*0.125, which is -0.73125, printed -0.73; the weight 0.125 is 2/(15 + 1). Nelson Freeburg and Charles Skelley (10:10) test the same spread on weekly 13-week bill rates, initialise the 15-week average with “a simple 15-week average of the spread”, as XAverage does, and settle on a faster 9-week average (a weight of 20%). The recipe is the exponential average of s - s2, for two rates or yields in the same units.

a, b := s.Overlap(s2)
smoothed := a.Sub(b).XAverage(n).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of the overlap; element e is overlap bar n - 1 + e. Minimum overlap: n bars. The seed is the published one, the simple average of the first n spreads.
  • Parameters: n >= 2; Kaeppel’s 15 and Freeburg and Skelley’s 15 or 9, on weekly bars.
  • Units: the rates’ units, percentage points for yields.
  • Yield curve: the same recipe on a long yield and a short one reads the yield curve. Siligardos (Stocks & Commodities 31:13) takes the long yield less the short, with three months against ten years the most popular pair: positive for a normal curve, negative for an inverted one. Matt Blackman (38:6) watches the ten-year yield less the three-month, whose inversion for more than a quarter has signalled recessions. Kaeppel (24:6) holds the S&P 500 only while the 30-year bond yield less the 13-week bill yield lies between 0 and 3 points. Flanagan (12:11) instead calls the curve inverted when the bill rate exceeds 1.03 times the long bond yield, a ratio test, bill.Div(bond).GT(1.03) on overlap views.
  • Domain: a NaN is permanent, from its bar on (exponential average).
  • Anchors: s2 == s gives exactly 0 (E). Kaeppel’s update, fifteen spreads of -0.70 and then -0.95 at n = 15, gives -0.73125 within 1e-15.
  • Pinned by TestRecipeT4ASpreads.

Point-volatility hedge ratio

The number of contracts of s2 that matches the dollar volatility of one contract of s: the standard deviation over n bars of s’s one-bar point changes, over that of s2’s, times mult, the contract multiplier of s over that of s2. It uses point changes, because a futures position gains its multiplier per point, and point changes stay meaningful on back-adjusted contracts and on spreads that cross zero, where percent returns do not. The ratio is derived, with no published source; Taucher’s unit sizes (Stocks & Commodities 5:9) give multipliers such as 440 dollars a cent for feeder cattle. Kaufman’s hedge in “A relative-volatility cap (Huck)” (Relative: Ratio and Relative Strength) is the percent-return form for shares.

a, b := s.Overlap(s2)
ratio := a.Delta(1).Window(-1).StdDev(n).Div(b.Delta(1).Window(-1).StdDev(n)).Mul(mult).Window(-(n - 1))
  • First valid index: n, counted from the start of the overlap; element e is overlap bar n + e. Minimum overlap: n + 1 bars.
  • Parameters: n >= 2; mult finite, for example 440.0/400 for feeder cattle against live cattle.
  • Domain: a flat window of s2’s changes gives 0 (Div); a window holding a NaN or an infinite change gives NaN.
  • Anchors: s2 = 2^p*s on finite s with mult = 1 gives exactly 2^-p on windows where s2’s changes are not flat, and 0 where they are (E), so s2 == s gives 1.
  • Pinned by TestRecipeT4ASpreads.

Hedged residual return (Schmidt)

Anatoly Schmidt (“Improving Stock Hedging With Industry Beta”, Stocks & Commodities 43:3) hedges a stock with two short positions, one in the market (SPY) and one in the stock’s industry or sector ETF, each the capital in the stock times that factor’s beta in the two-factor model of his equation 3 on page 15, r_i = alpha_i + beta_iM*r_M + beta_iI*r_I + eps_i. Both betas come from daily returns over 252 trading days and are re-estimated as the window moves (“Then I moved the trading window by one day”). The hedged return of a bar is the stock’s return less each beta times its factor’s return, with the betas of the window that ends on the bar before, so they are out of sample. s is the stock’s price, market the market’s and industry the industry’s; the three are right-aligned at their most recent bar and trimmed to the shortest one’s length, and ra, r1 and r2 are their one-bar percent returns.

m := min(len(s), len(market), len(industry))
ra := s.Right(m).RateOfChange(1).Window(-1)
r1 := market.Right(m).RateOfChange(1).Window(-1)
r2 := industry.Right(m).RateOfChange(1).Window(-1)
h := ra.Sub(ra.TwoFactorBeta(r1, r2, n).Shift(1).Mul(r1)).Sub(ra.TwoFactorBeta(r2, r1, n).Shift(1).Mul(r2))
valid := h.Window(-n) // the recipe's valid bars
  • First valid index: n, counted from the start of the return views, which are one bar shorter than the common history (return bar i runs from common bar i to i + 1); element e of valid is return bar n + e. Minimum common history: n + 2 bars, which give one valid value.
  • Parameters: n >= 3, TwoFactorBeta’s minimum; Schmidt’s n = 252 on daily closes.
  • Units: percent, the returns’ units: h is the return of the hedged position per unit of capital in the stock. Each beta is dimensionless.
  • Rebalancing: the recipe re-estimates both betas every bar and holds each hedge for one bar. Schmidt holds a hedge for a rebalancing period of τ trading days (21, 63 and 126 in his tables on page 16) with its betas fixed when it starts, and starts a new period every day; on day d of such a period (d = 1 to τ) the hedged return uses the betas shifted by d rather than 1.
  • Hedging measures: for each rebalancing period Schmidt reports the annualised Sharpe ratio SR of the hedged daily returns (his equation 4), the losses L, the sum of the period’s negative daily returns, in basis points (equation 5), and Z = SR/L (equation 6), all on page 16. They are statistics of valid over each period for the caller to compute; the library has no method for them.
  • Warm-up hazard: on return bars 0 to n - 1 the shifted betas are TwoFactorBeta’s warm-up zeros, so h there is the unhedged return ra, not 0, and a statistic over h mixes unhedged returns in; use valid.
  • Collinear factors: on a window where the market’s and the industry’s returns are collinear (their correlation within about 5e-10 of 1 or -1, as when the industry’s price is a power-of-two multiple of the market’s), both betas are 0 through TwoFactorBeta’s determinant guard, and the next bar’s h is the unhedged return.
  • Domain: a NaN price at common bar p makes return bars p - 1 and p NaN, and, for p at least n, h NaN on return bars p - 1 to p + n (n + 2 values), after which it recovers; for p below n, h is NaN on return bars p - 1 and p and on bars n to p + n, with unhedged warm-up returns between. RateOfChange gives a 0 return out of a zero price.
  • Anchors: with a flat r2 (an industry whose returns are constant over the window, such as a constant price) the industry beta is 0 (the flat-x1 branch) and the market beta is ra.Beta(r1, n) (the flat-x2 branch), so h equals the one-factor hedge ra.Sub(ra.Beta(r1, n).Shift(1).Mul(r1)) up to the sign of a zero (G); if also market == s, h is exactly 0 on every bar from n whose n previous returns of ra are not flat (E, through the flat-x2 branch and Beta(s, s) == 1); with industry == s the market beta is exactly 0 and the industry beta exactly 1 wherever the determinant guard passes, so h is exactly 0 there (E). A Python reference in exact rational arithmetic evaluates the recipe on the grid of testdata/ref/multi/recipes_a.csv, and Go agrees with it within the per-row condition bound written there.
  • Pinned by TestRecipeHedgedResidual.

Hinge beta (Henriksson and Merton)

Daniel, Jagannathan and Kim (“A Hidden Markov Model of Momentum”, working paper draft of 17 February 2019) fit the Henriksson–Merton regression s = a + b0*m + bp*max(m, 0) + e (their equation 1, page 10), with m the market’s excess return: b0 is the down-market beta, b0 + bp the up-market beta and bp the convexity. Its least-squares coefficients are TwoFactorBeta calls, on the regressors m and max(m, 0), or on min(m, 0) and max(m, 0), which with the intercept span the same fits. s and m are excess returns per bar, paired from the most recent bar.

y, x := s.Overlap(m)
bp := y.TwoFactorBeta(x.Max(0), x, n).Window(-(n - 1))
b0 := y.TwoFactorBeta(x, x.Max(0), n).Window(-(n - 1))
b0Alt := y.TwoFactorBeta(x.Min(0), x.Max(0), n).Window(-(n - 1))
up := y.TwoFactorBeta(x.Max(0), x.Min(0), n).Window(-(n - 1))
  • First valid index: n - 1, counted from the start of the overlap; element e of each result is overlap bar n - 1 + e. Minimum overlap: n bars.
  • Parameters: n >= 3, TwoFactorBeta’s minimum, and in practice enough bars for several of each sign of m in every window (below).
  • Identities: on a full-rank window the two parametrisations are the same regression, so b0Alt equals b0 and up equals b0 + bp in exact arithmetic; in float64 they agree within 1e-12 of the largest of 1, |b0| and |bp| wherever each call’s 1 - r12*r12 is at least 0.01 (T). They differ on the degenerate windows below.
  • Degenerate windows: a window is full rank only when m takes both signs and at least three distinct values, so every window needs both signs, and on daily bars enough bars of each (with one bar of a sign, the fit passes through that bar exactly). On the others the calls take TwoFactorBeta’s degenerate branches (G):
    • every m <= 0, ties at zero included: x.Max(0) is flat, so bp and up are 0 (flat x1) and b0 and b0Alt are s.Beta(m, n)’s value (flat x2);
    • every m >= 0: x.Max(0) equals x, so bp and b0 are 0 through the determinant guard (x2 equal to x1); b0Alt is 0 (flat x1) and up is s.Beta(m, n)’s value, so up is not b0 + bp there;
    • two distinct values of opposite sign: max(m, 0) is an exact affine function of m, and all four calls are 0 through the determinant guard, whose computed ratio lies within 1e-15 of 0 there;
    • one tiny negative m = -δ among positive values: 1 - r12*r12 of the bp and b0 calls is at most δ*δ over the window’s centred sum of squares of max(m, 0), so they return 0 through the guard once that is below about 1e-9 (in the measurements every δ up to 1e-6 of a typical |m|, nearly every one at 1e-5, a growing share at 1e-4, at 3e-4 only the 252-bar windows, none from 1e-3), although their exact values are of order 1/δ; b0Alt and up stay on the general branch and give the correctly conditioned coefficients, within about 1e-12 relative, b0Alt of order 1/δ. Just above the threshold the bp and b0 calls are accurate only to about 3e-7 relative.
  • Reading: the kink is at zero excess market return; raw returns move it by the risk-free rate. The intercept a is not an alpha, because max(m, 0) is not an excess return (pages 10 and 11: it “is no longer a measure of the strategy’s abnormal return”).
  • Not the upside less the downside beta: bp is not s.UpsideBeta(m, n, 0) - s.DownsideBeta(m, n, 0), because those fit each side with its own intercept and the hinge has one. With m = (-2, -1, 1, 3), s = (0, 2, 0, 2) and n = 4, bp is 1/3 and b0 is 0, but the upside beta is 1 and the downside beta 2, a difference of -1 (E); the two readings differ on every noisy mixed-sign window of the tests.
  • Reference: a Python reference in exact rational arithmetic transcribes TwoFactorBeta’s branches on each call’s window (testdata/ref/multi/ recipes_a.csv); Go agrees with it within 1e-12 of the larger of 1 and the exact value on the full-rank windows whose exact 1 - r12*r12 is at least 0.01, and within the per-row condition bound elsewhere.
  • Pinned by TestRecipeHingeBeta; the degenerate windows by TestRecipeHingeBetaDegenerate and the counterexample by TestRecipeHingeBetaNotUpsideDownside.

Relative: Divergence

BollingerDivergence

func (s Series) BollingerDivergence(s2 Series, n1, n2, smooth int) Series

BollingerDivergence returns Katsanos’s Bollinger band divergence, the percentage difference between the positions of s2 and s within their own two-standard-deviation bands, smoothed over smooth bars.

It is the Bollinger band divergence of Markos Katsanos, Intermarket Trading Strategies (Wiley, 2008), section 9.2, pages 123-124, with the MetaStock listing in Appendix A.1, pages 297-298. The band position of a series v over n bars, with mu = v.Average(n) and sd = v.StdDev(n), is 1 + (((v[j] - mu[j]) + 2sd[j]) / (4sd[j])), in exactly that order, which is the order of the listing’s 1+((C- Mov(C,D1,S)+2Stdev(C,D1))/(4Stdev(C,D1))): 1 on the lower band, 1.5 on the average and 2 on the upper band, and 1.5 on a flat leg, where sd is 0. With bs the position of s over n1 bars and bs2 that of s2 over n2 bars, the raw value at overlap bar j is the signed percentage change signedPctChange(bs2[j], bs[j]), which is (bs2/bs - 1)*100 when bs > 0 and (bs2/(-bs) + 1)*100 when bs < 0. For smooth > 1 the output is the exponential moving average XAverage(smooth) of the raw values from the first raw bar on, bit for bit, and zero while it warms up.

The sign is intermarket minus base: a positive value means that s2 sits higher in its band than s does in its own, so s has lagged s2, which is bullish for s if the gap closes by mean reversion.

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, counted from the start of the overlap, is w = max(n1, n2) - 1 for smooth <= 1 and w + smooth - 1, that is max(n1, n2) + smooth - 2, for smooth > 1, and the minimum overlap is one bar more: max(n1, n2) bars unsmoothed and max(n1, n2) + smooth - 1 bars smoothed, so Katsanos’s 40/40/3 needs 42 bars. Every output before the first valid index is zero. Any smooth <= 1 (zero, negative and math.MinInt included) is valid and gives the raw divergence. Invalid parameters give aligned zeros, a fresh all-zero series of len(s): an empty overlap, n1 < 2 or n2 < 2, n1 or n2 greater than the overlap, or a smooth > 1 that leaves fewer than smooth raw values. The checks never add parameters together, so every int from math.MinInt to math.MaxInt is safe, and an invalid one returns at once without allocating anything beyond the zero result.

Pairing class: per leg, each band position seeing only its own series and window, with the legs combined bar by bar and a recursive tail when smoothed. In exact arithmetic an unsmoothed value depends only on the two trailing windows, but the rolling variance behind StdDev depends on earlier history, so it is not start-invariant bit for bit, and the smoothed values depend on where the history starts. Valid data: any finite data whose deviations from their window mean lie between about 1e-154 and 1e154/sqrt(n), the range of the rolling variance behind StdDev (outside it the band position is NaN or a silent 1.5), since a band position is unchanged by adding a constant to its series or multiplying it by a positive one; within that range the hazard below lies in the window length, not in the data.

Non-finite values: unsmoothed, a NaN or an infinity at overlap index p of s gives NaN on [p, p+n1-1] and one in s2 gives NaN on [p, p+n2-1], each clipped to [w, m) with m the overlap length, and later bars recover once the value has left the window; smoothed, with p the index of the first NaN raw value, every output from max(p, w + smooth - 1) on is NaN for ever, because the recursion never recovers. A flat leg sits at mid-band, 1.5, so a flat s gives (bs2/1.5 - 1)*100 and two flat legs give exactly 0. A base position of exactly 0 (the zero base) gives NaN, as does a quotient that overflows.

In exact arithmetic B = 1.5 + Z/4, with Z the population z-score of the bar in its window, so the raw value is 100*(Zs2 - Zs)/|6 + Zs|. By Samuelson’s inequality |Z| <= sqrt(n-1), so for n1 <= 36 the base position is at least 1.5 - sqrt(n1-1)/4 > 0 and |raw| is at most 100*(sqrt(n2-1) + sqrt(n1-1))/(6 - sqrt(n1-1)), which is 531.2 at n1 = n2 = 20 and 14,099 at n1 = n2 = 36. From n1 = 37, which includes the default of 40 in Katsanos’s Appendix A.1, the base position can reach or cross 0: the published quantity then spikes as the base approaches 0 (the signed leg keeps its sign across the crossing, where Katsanos’s formula flips it), and a base of exactly 0 gives NaN. The method keeps the published quantity; for long windows use ZScoreDivergence, which has no such hazard. Katsanos’s signal levels lie between 10 and 30 either side of 0, and for closely related markets most readings lie within about 30 either side of 0; less related pairs exceed it more often.

The value of this composite lies in its published, verifiable code, not in a demonstrated edge: Katsanos’s Chapter 11 tests have 9 to 38 trades over 1995 to 2007, with the levels optimised per system.

Katsanos’s defaults are 40/40/3 (Appendix A.1), 20 bars in his Figure 9.2 (GLD against SLV) and 100 bars in the Appendix A.2 systems; the systems of his articles in Stocks & Commodities use smooth = 1 with 20 bars (“Trading The Loonie”, 33:13, December 2015) and 30 bars (“Trading The Aussie”, 27:2, 2009), below the hazard. Trading use: buy when the divergence peaks above +10 to +30 and declines, and sell when it troughs below -10 to -30 and rises (15 either side of 0 in Figure 9.2); the Loonie article’s signal code is HHV(DIV1,3) > 20 AND DIV1 < REF(DIV1,-1) for longs and its mirror for shorts, and the Aussie article uses 10 and -10. Katsanos writes that the formula “doesn’t work so well” for negatively correlated markets; use ZScoreDivergence for those.

Against several intermarkets, as in “Trading The Aussie”, take the maximum, minimum or sum of the divergences against each of them, composed over a three-way overlap:

m := min(len(aud), len(xau), len(crb))
a, x, c := aud.Right(m), xau.Right(m), crb.Right(m)
d2 := a.BollingerDivergence(x, 30, 30, 1)
d3 := a.BollingerDivergence(c, 30, 30, 1)
maxDiv := d2.GT(d3).If(func(v float64) bool { return v != 0 }, d2, d3)
minDiv := d2.LT(d3).If(func(v float64) bool { return v != 0 }, d2, d3)
sumDiv := d2.Add(d3) // Katsanos's DIV2 + DIV3 > 80

GT and LT are false when either value is NaN, so the maximum and the minimum then take d3, which is NaN where d3 is.

Deviations from Katsanos’s MetaStock code: the smoothing average is seeded with the simple average of its first smooth values, where MetaStock’s Mov(…, E) is understood to seed with the first value (a MetaStock forum formula; Katsanos does not state it), so the two converge after a few times smooth bars; the raw value is the signed percentage change of the positions (signedPctChange) in place of the listing’s DIVERG1:=(sec2BOL-sec1BOL)/SEC1BOL100, which it matches up to rounding while the base position is positive, and a zero base gives NaN; a flat leg sits at 1.5 by an exact zero-divisor rule, with no +.0001 guard, where the article listings add .0001 to the denominator 4Stdev, a constant in price units that changes the position’s distance from 1, B - 1, by a relative amount of about 0.0001/(4*sd); the standard deviations are population ones, MetaStock’s Stdev being taken to be population; and outputs before the first valid index are zero rather than undefined.

Verification: two independent references, a Python reference in exact rational arithmetic (with 140-digit decimals for square roots and the smoothing) and a Julia reference in BigFloat that also translates Katsanos’s MetaStock listings, agree with it; and there are exact identities with existing methods: on non-flat windows each leg equals v.Sub(v.Average(n)).Add(v.StdDev(n).Mul(2)).Div(v.StdDev(n).Mul(4)).Add(1) bit for bit, and s2 == s and s2 = s.Mul(2), each with n1 == n2, give exactly 0 wherever the base position is not 0.

IntermarketDisparity

func (s Series) IntermarketDisparity(s2 Series, n1, n2, smooth int, c float64) Series

IntermarketDisparity returns Katsanos’s intermarket disparity, c times the percentage distance of s2 from its n2-bar average minus that of s from its n1-bar average, smoothed over smooth bars.

It is the intermarket disparity of Markos Katsanos, Intermarket Trading Strategies (Wiley, 2008), section 9.3, pages 124-127, with the MetaStock listing in Appendix A.1, page 298. The disparity of a series v is its signed percentage distance from its n-bar simple moving average M = v.Average(n), the listing’s Mov(C,D1,S): signedPctChange(v[j], M[j]), which is (v/M - 1)*100 when M > 0 and (v/(-M) + 1)*100 when M < 0. With ds1 the disparity of s over n1 bars and ds2 that of s2 over n2 bars, the raw value at overlap bar j is float64(c*ds2[j]) - ds1[j], in exactly that form: the conversion rounds the product before the subtraction, so that the compiler cannot fuse the two into one multiply-add on arm64. For smooth > 1 the output is the exponential moving average XAverage(smooth) of the raw values from the first raw bar on, bit for bit, and zero while it warms up; Katsanos smooths with MetaStock’s Mov(…, E).

The sign is intermarket minus base: a positive value means that s stands lower against its average than s2, scaled by c, stands against its own, so s has lagged s2, which is bullish for s if the gap closes by mean reversion. The multiplier c orients the intermarket leg, +1 for a market that moves with s and -1 for one that moves against it, and any other finite non-zero value also scales it.

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, counted from the start of the overlap, is w = max(n1, n2) - 1 for smooth <= 1 and w + smooth - 1, that is max(n1, n2) + smooth - 2, for smooth > 1, and the minimum overlap is one bar more: max(n1, n2) bars unsmoothed and max(n1, n2) + smooth - 1 bars smoothed, so Katsanos’s 30/30/3 needs 32 bars. Every output before the first valid index is zero. Any smooth <= 1 (zero, negative and math.MinInt included) is valid and gives the raw disparity. Invalid parameters give aligned zeros, a fresh all-zero series of len(s): an empty overlap, n1 < 1 or n2 < 1, a c that is zero, NaN or infinite, n1 or n2 greater than the overlap, or a smooth > 1 that leaves fewer than smooth raw values. The checks never add parameters together, so every int from math.MinInt to math.MaxInt is safe, and an invalid one returns at once without allocating anything beyond the zero result.

Pairing class: per leg, each disparity seeing only its own series and window, with the legs combined bar by bar and a recursive tail when smoothed. In exact arithmetic an unsmoothed value depends only on the two trailing windows and so is start-invariant, though the rolling sums behind Average are not promised to be start-invariant bit for bit; the smoothed values depend on where the history starts and are not start-invariant. Valid data: levels whose n-bar average is not zero, such as prices and index values. Scaling either input by a positive factor leaves the value unchanged, shifting it does not, and a negative level takes the signed branch, which keeps the sign of the distance. A zero average gives NaN and an average near zero makes the percentage ill-conditioned, so for spreads, rates and back-adjusted futures, which can cross zero, prefer ZScoreDivergence.

Non-finite values: unsmoothed, a NaN or an infinity at overlap index p of s gives NaN on [p, p+n1-1] and one in s2 gives NaN on [p, p+n2-1], each clipped to [w, m) with m the overlap length, and later bars recover once the value has left the window; smoothed, with p the index of the first NaN raw value, every output from max(p, w + smooth - 1) on is NaN for ever, because the recursion never recovers. A zero average (the zero base), a non-finite value or a non-finite quotient gives its leg NaN; a product c*ds2 that overflows stands as an infinity, and the smoothed values from it on stay infinite or NaN. A flat window gives its leg 0 up to the rounding of the simple average (Average of equal values need not return the value exactly), a flat window of zeros gives NaN, and levels above about math.MaxFloat64/n overflow the rolling sum and give NaN.

The value of this composite lies in its published, verifiable code, not in a demonstrated edge: Katsanos’s Chapter 11 tests have 9 to 38 trades over 1995 to 2007, with the levels optimised per system.

Katsanos’s defaults are 30/30/3 with c = -1, for gold against the dollar index (Appendix A.1); his Chapter 11 optima are 15/15 against the Philadelphia Gold and Silver Mining Index (XAU), and n1 = 20 for gold with n2 = 15 for the dollar index. His yen system (Appendix A.8) takes exponential averages, which IntermarketDisparityX provides. Trading use, as in his Figure 9.3: buy when the value tops above about +3% and turns down, and sell when it bottoms below about -3% and turns up. Unlike Ruggiero’s sign rule, it can signal when both markets are on the same side of their averages.

Deviations from Katsanos’s MetaStock code: the smoothing average is seeded with the simple average of its first smooth values, where MetaStock’s Mov(…, E) is understood to seed with the first value (a MetaStock forum formula; Katsanos does not state it), so the two converge after a few times smooth bars; each leg is the signed percentage change of v over M (signedPctChange) in place of the subtract-first ((C - Mov)/Mov)*100, which it matches up to rounding on a positive average and which reverses the direction of the distance on a negative one, and a zero average gives NaN; c is a multiplier on the intermarket leg in place of the listing’s correlation-sign input; and outputs before the first valid index are zero rather than undefined.

Verification: two independent references, a Python reference in exact rational arithmetic (with 140-digit decimals for square roots and the smoothing) and a Julia reference in BigFloat that also translates Katsanos’s MetaStock listings, agree with it; and there are exact identities with existing methods: where the average is positive, each leg equals v.Div(v.Average(n)).Sub(1).Mul(100) bit for bit from index n-1 on, an all-ones s2 gives, unsmoothed, minus that leg of s, and s2 == s with n1 == n2 and c = 1 gives exactly 0.

IntermarketDisparityX

func (s Series) IntermarketDisparityX(s2 Series, n1, n2, smooth int, c float64) Series

IntermarketDisparityX returns IntermarketDisparity computed against exponential rather than simple moving averages, as in Katsanos’s yen system.

It follows the yen system of Markos Katsanos, Intermarket Trading Strategies (Wiley, 2008), Appendix A.8, whose listing measures each market’s disparity from its exponential moving average, Mov(C,10,E), in place of the simple average of the intermarket disparity of section 9.3, pages 124-127, and Appendix A.1, page 298. Each leg is the signed percentage distance signedPctChange(v[j], M[j]) with M = v.XAverage(n), which is (v/M - 1)*100 when M > 0 and (v/(-M) + 1)*100 when M < 0. With ds1 the leg of s over n1 bars and ds2 that of s2 over n2 bars, the raw value at overlap bar j is float64(c*ds2[j]) - ds1[j], in exactly that form: the conversion rounds the product before the subtraction, so that the compiler cannot fuse the two into one multiply-add on arm64. For smooth > 1 the output is the exponential moving average XAverage(smooth) of the raw values from the first raw bar on, bit for bit, and zero while it warms up.

The sign is intermarket minus base: a positive value means that s stands lower against its average than s2, scaled by c, stands against its own, so s has lagged s2, which is bullish for s if the gap closes by mean reversion. The multiplier c orients the intermarket leg, +1 for a market that moves with s and -1 for one that moves against it, and any other finite non-zero value also scales it.

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, counted from the start of the overlap, is w = max(n1, n2) - 1 for smooth <= 1 and w + smooth - 1, that is max(n1, n2) + smooth - 2, for smooth > 1, and the minimum overlap is one bar more: max(n1, n2) bars unsmoothed and max(n1, n2) + smooth - 1 bars smoothed, so the yen system’s 10/10/3 needs 12 bars; as the exponential averages remember their seeds, allow a few times max(n1, n2) bars more history before relying on the values. Every output before the first valid index is zero. Any smooth <= 1 (zero, negative and math.MinInt included) is valid and gives the raw disparity. Invalid parameters give aligned zeros, a fresh all-zero series of len(s): an empty overlap, n1 < 1 or n2 < 1, a c that is zero, NaN or infinite, n1 or n2 greater than the overlap, or a smooth > 1 that leaves fewer than smooth raw values. The checks never add parameters together, so every int from math.MinInt to math.MaxInt is safe, and an invalid one returns at once without allocating anything beyond the zero result.

Pairing class: per leg, each disparity seeing only its own series, with the legs combined bar by bar and a recursive tail when smoothed. The legs are recursive too, each exponential average running from the start of the overlap, so even the unsmoothed values depend on where the history starts and are not start-invariant. Valid data: levels whose exponential average is not zero, such as prices and index values. Scaling either input by a positive factor leaves the value unchanged, shifting it does not, and a negative level takes the signed branch, which keeps the sign of the distance. A zero average gives NaN and an average near zero makes the percentage ill-conditioned, so for spreads, rates and back-adjusted futures, which can cross zero, prefer ZScoreDivergence.

Non-finite values: a NaN or an infinity at overlap index p of either input gives NaN from max(p, w) on unsmoothed, and from max(p, w + smooth - 1) on smoothed, for ever, because an exponential average never recovers. A zero average (the zero base), a non-finite value or a non-finite quotient gives its leg NaN; a product c*ds2 that overflows stands as an infinity, and the smoothed values from it on stay infinite or NaN. A flat stretch draws its leg towards 0 as the average converges on the flat level.

The value of this composite lies in its published, verifiable code, not in a demonstrated edge: Katsanos’s Chapter 11 tests have 9 to 38 trades over 1995 to 2007, with the levels optimised per system.

Katsanos’s USD/JPY yen system (Appendix A.8) uses 10/10/3 with c = +1: its listing forms the difference DIS3 - DISY, the ten-year Treasury yield’s leg minus the yen’s, as here, and it buys when that divergence has risen above 4 within four bars and turns down, and sells when it has fallen below -4 and turns up. His yen futures listing negates the intermarket leg (-DIS3 - DISY, that is c = -1) and uses 2 and -2, and his regression system takes a length it does not print in place of 10. Unlike Ruggiero’s sign rule, the divergence can signal when both markets are on the same side of their averages.

Deviations from Katsanos’s MetaStock code: the leg averages are seeded with the simple average of their first n1 or n2 values and the smoothing average with that of its first smooth values, where MetaStock’s Mov(…, E) is understood to seed with the first value (a MetaStock forum formula; Katsanos does not state it), so each pair converges after a few times its length; each leg is the signed percentage change of v over M (signedPctChange) in place of the subtract-first ((C - Mov)/Mov)*100, which it matches up to rounding on a positive average and which reverses the direction of the distance on a negative one, and a zero average gives NaN; c is a multiplier on the intermarket leg in place of the listing’s plain difference, which is c = 1; and outputs before the first valid index are zero rather than undefined.

Verification: two independent references, a Python reference in exact rational arithmetic (with 140-digit decimals for square roots and the smoothing) and a Julia reference in BigFloat that also translates Katsanos’s MetaStock listings, agree with it; and there are exact identities with existing methods: where the average is positive, each leg equals v.Div(v.XAverage(n)).Sub(1).Mul(100) bit for bit from index n-1 on, an all-ones s2 gives, unsmoothed, minus that leg of s, and s2 == s with n1 == n2 and c = 1 gives exactly 0.

IntermarketLRSDivergence

func (s Series) IntermarketLRSDivergence(s2 Series, n1, n2, volLen, smooth int, c float64) Series

IntermarketLRSDivergence returns Katsanos’s linear-regression-slope divergence, the volatility-scaled percentage slope of s2 minus that of s, smoothed over smooth bars.

It is the linear-regression-slope divergence of Markos Katsanos, Intermarket Trading Strategies (Wiley, 2008), section 9.4, pages 127-128, in the form of his published code (Appendix A.1, page 298: “D3:=D2Stdev(C,200)/Stdev(P,200); LRSI:=LinRegSlope(P,D1)/Abs(Ref(P,-D1+1))100; LRS:=LinRegSlope(C,D1)/Abs(Ref(C,-D1+1))100; DIVERG:=(D3LRSI-LRS)100; Mov(DIVERG,3,E)”), with C the base s, P the comparison s2, D2 the coefficient c and D1 the slope length, here n1 for s and n2 for s2. On the overlap, psA and psB are the percentage slopes of s over n1 bars and of s2 over n2 bars (SlopePercent), and sdA and sdB are the population standard deviations of s and s2 over volLen bars (StdDev). The scale is Katsanos’s default, the plain ratio of standard deviations, q = (csdA)/sdB, which is the scale of every published result of the method, and the unsmoothed divergence is (float64(qpsB) - psA) 100, his (D3*LRSI-LRS)*100, in that Go operation order: the conversion rounds the product before the subtraction, so the two are never fused into one multiply-add. The result is that divergence smoothed by an exponential moving average of length smooth. His price-level-adjusted scale, which he recommends for"a high and a low priced security" but published no test of, is IntermarketLRSDivergenceCV.

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, counted from the start of the overlap, is w = max(n1, n2, volLen) - 1 for smooth <= 1 and w + smooth - 1 for smooth > 1, and the minimum overlap is one bar more; every earlier output is 0. With m the overlap length, the result is all zeros for an empty overlap, for n1, n2 or volLen below 2, for a c that is zero or not finite, for n1, n2 or volLen greater than m and, when smooth > 1, for smooth > m - w (tested by subtraction); any smooth <= 1 means unsmoothed, and every int parameter may be math.MaxInt or math.MinInt. For smooth > 1 the outputs from w + smooth - 1 on are exactly d[w:].XAverage(smooth)[smooth-1:], with d the unsmoothed result on the overlap.

Pairing is per leg: each percentage slope and each standard deviation is computed on its own series over its own window, and the legs are combined bar by bar. Unsmoothed outputs are start-invariant up to rounding: the slopes are window-local, but the standard deviations come from a rolling aggregate whose last bits depend on where the series starts; smoothed ones are not, because the exponential average starts at the first valid bar. Valid data: price levels, as a percentage slope needs a non-zero first bar in each window. Units: percent per bar times 100, or basis points per bar, centred on 0, with the sign intermarket minus base.

Before smoothing, a NaN or an infinity at overlap index p of s gives NaN on [p, p + max(n1, volLen) - 1], and one in s2 on [p, p + max(n2, volLen) - 1], each clipped to [w, m), and later values recover. A zero first bar of a slope window gives NaN at that window’s end only. A zero standard deviation of s2 (a flat volLen-bar window) gives 0 when both percentage slopes are finite. With smooth > 1, a NaN unsmoothed value at p makes every output from max(p, w + smooth - 1) on NaN, because the exponential recursion never recovers. A result beyond the binary64 range is whatever binary64 gives.

Katsanos’s defaults (Appendix A.1) are n1 = n2 = 15, volLen = 200, smooth = 3 and c = +1, so the minimum overlap is 202. His trading use, on page 128, is to buy when it peaks above a level between +10 and +40 and declines, and to sell when it troughs below a level between -10 and -40 and rises; Figure 9.4, gold against the CRB, uses +15 and -30. The output is multiplied by 100 twice, once in the percentage slope and once in the divergence, which matters only for absolute thresholds. His gold systems (Appendix A.2) run it against the XAU with 40/50 and c = +1 and against the dollar index with 30/40 and c = -1; in Table 11.1 the dollar index system made $282,257 in 21 trades with 90% winners and a profit factor of 35.39, and “the single LRS system stood out” (section 11.9, page 186).

Deviations from the MetaStock code: the smoothing average is seeded with the simple average of its first smooth values, where MetaStock’s Mov(…, E) is taken to seed with the first value (the MetaStock forum’s formula); every output before the first valid index is 0; and a zero standard deviation of s2 gives 0, where his e-mini listing guards the denominator with +.01 (“Stdev(SEC2,200)+.01”).

Verification: two independent references, one in Python in exact rational arithmetic with high-precision square roots and smoothing, and one in Julia that also translates his Appendix A.1 listing for both scales and his Appendix A.2 gold systems for the default scale, agree with it. There are exact identities: s2 == s with n1 == n2 and c = +1 gives 0 on every valid bar; s2 = s.Mul(2^k) with n1 == n2 and c = +1 gives, unsmoothed, exactly (float64(2^-k*PS) - PS)*100, with PS the SlopePercent(n1) of s, wherever the volLen-bar window of s is not flat, because the default scale depends on the relative price level; the legs psA and psB equal SlopePercent of the overlap views; and the smoothed tail equals XAverage of the unsmoothed values.

IntermarketLRSDivergenceCV

func (s Series) IntermarketLRSDivergenceCV(s2 Series, n1, n2, volLen, smooth int, c float64) Series

IntermarketLRSDivergenceCV returns IntermarketLRSDivergence with Katsanos’s price-level-adjusted scale, the ratio of the coefficients of variation of s and s2, for pairs at very different price levels.

It is the price-level-adjusted linear-regression-slope divergence of Markos Katsanos, Intermarket Trading Strategies (Wiley, 2008), section 9.4, pages 127-128 and Appendix A.1, page 298, which he recommends for “a high and a low priced security” but published no test of, so no published result or threshold applies to it. Everything is as in IntermarketLRSDivergence, with psA and psB the percentage slopes of s over n1 bars and of s2 over n2 bars (SlopePercent) and sdA and sdB the population standard deviations of s and s2 over volLen bars (StdDev), except the scale, which also uses maA and maB, the volLen-bar moving averages of s and s2 (Average): q = (((csdA)/sdB)maB)/maA, exactly in that Go operation order, his “D3:=D2Stdev(C,200)/Stdev(P,200)Mov(P,200,S)/Mov(S,200,S)” read from left to right, and not the algebraically equal c times the quotient of the two coefficients of variation. The line is printed with Mov(S,…), read here as Mov(C,…), the only dimensionally consistent reading, which makes the adjustment exactly the ratio of the coefficient of variation of s, sdA/maA, to that of s2, sdB/maB. The unsmoothed divergence is (float64(qpsB) - psA) 100, his “DIVERG:=(D3*LRSI-LRS)*100”, in that Go operation order: the conversion rounds the product before the subtraction, so the two are never fused into one multiply-add. The result is that divergence smoothed by an exponential moving average of length smooth.

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, counted from the start of the overlap, is w = max(n1, n2, volLen) - 1 for smooth <= 1 and w + smooth - 1 for smooth > 1, and the minimum overlap is one bar more; every earlier output is 0. With m the overlap length, the result is all zeros for an empty overlap, for n1, n2 or volLen below 2, for a c that is zero or not finite, for n1, n2 or volLen greater than m and, when smooth > 1, for smooth > m - w (tested by subtraction); any smooth <= 1 means unsmoothed, and every int parameter may be math.MaxInt or math.MinInt. For smooth > 1 the outputs from w + smooth - 1 on are exactly d[w:].XAverage(smooth)[smooth-1:], with d the unsmoothed result on the overlap.

Pairing is per leg: each percentage slope, standard deviation and moving average is computed on its own series over its own window, and the legs are combined bar by bar. Unsmoothed outputs are start-invariant up to rounding: the slopes are window-local, but the standard deviations and averages come from rolling aggregates whose last bits depend on where the series starts; smoothed ones are not, because the exponential average starts at the first valid bar. Valid data: positive price levels, since a coefficient of variation needs a positive mean. Units: percent per bar times 100, or basis points per bar, centred on 0, with the sign intermarket minus base.

Before smoothing, a NaN or an infinity at overlap index p of s gives NaN on [p, p + max(n1, volLen) - 1], and one in s2 on [p, p + max(n2, volLen) - 1], each clipped to [w, m), and later values recover. A zero first bar of a slope window gives NaN at that window’s end only, and a volLen-bar average of s or s2 at or below zero gives NaN. A zero standard deviation of s2 (a flat volLen-bar window) gives 0 when both percentage slopes are finite and both averages are positive. With smooth > 1, a NaN unsmoothed value at p makes every output from max(p, w + smooth - 1) on NaN, because the exponential recursion never recovers. A result beyond the binary64 range is whatever binary64 gives.

Katsanos’s defaults for the divergence (Appendix A.1) are n1 = n2 = 15, volLen = 200, smooth = 3 and c = +1, so the minimum overlap is 202, and his rule of trading its peaks and troughs (page 128) applies. His thresholds, from 10 to 40 on either side of 0, and his gold systems were all found with the default scale, so none of them carries over to this one. The output is multiplied by 100 twice, once in the percentage slope and once in the divergence, which matters only for absolute thresholds.

Deviations from the MetaStock code: the smoothing average is seeded with the simple average of its first smooth values, where MetaStock’s Mov(…, E) is taken to seed with the first value (the MetaStock forum’s formula); every output before the first valid index is 0; and a zero standard deviation of s2 gives 0, where his e-mini listing guards the denominator with +.01 (“Stdev(SEC2,200)+.01”).

Verification: two independent references, one in Python in exact rational arithmetic with high-precision square roots and smoothing, and one in Julia that also translates his Appendix A.1 listing for both scales and his Appendix A.2 gold systems for the default scale, agree with it. There are exact identities: s2 == s with n1 == n2 and c = +1 gives 0 on every valid bar; on positive data s2 = s.Mul(2^k) with n1 == n2 and c = +1 gives 0 as well, unlike the default scale, because the adjustment removes the price level; the legs psA and psB equal SlopePercent of the overlap views; and the smoothed tail equals XAverage of the unsmoothed values.

IntermarketRegressionDivergence

func (s Series) IntermarketRegressionDivergence(s2 Series, n, rocLen, smooth int) Series

IntermarketRegressionDivergence returns Katsanos’s regression divergence, the rocLen-bar return of s predicted from s2 by a rolling n-bar regression minus the actual return, smoothed over smooth bars.

It is the regression divergence of Markos Katsanos, Intermarket Trading Strategies (Wiley, 2008), section 9.5, page 128, equations 9.9 to 9.11, in the form of his published code (Appendix A.1, page 299: “RS1:=ROC(C,D2,%); RS2:=ROC(SEC2,D2,%); b:=Correl(RS1,RS2,D1,0)Stdev(RS1,D1)/Stdev(RS2,D1); a:=Mov(RS1,D1,S)-bMov(RS2,D1,S); PRED:=b*RS2+a; DIVERG:=(PRED-RS1); Mov(DIVERG,3,E)”), with C the base s, SEC2 the comparison s2, D1 the regression length n and D2 the return length rocLen. With k = rocLen, rs1 and rs2 are the k-bar signed percent returns of s and s2 on the overlap: (v/v0 - 1)*100 on a positive base v0, the expression of RateOfChange, and (v/(-v0) + 1)100 on a negative one. At each bar, beta and alpha are the least-squares slope and intercept of the regression of the n returns of s ending there on those of s2 (the values of Beta and Intercept, with the returns of s2 the regressor), and the unsmoothed divergence is (float64(betars2) + alpha) - rs1, predicted minus actual, his PRED-RS1, in that Go operation order: the conversion rounds the product before the addition, so the two are never fused into one multiply-add. His b, a correlation times a ratio of standard deviations, equals the least-squares slope only in exact arithmetic. The result is that divergence smoothed by an exponential moving average of length smooth.

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, counted from the start of the overlap, is w = k + n - 1 for smooth <= 1 and w + smooth - 1 = k + n + smooth - 2 for smooth > 1, and the minimum overlap is one bar more; every earlier output is 0. With m the overlap length, the result is all zeros for an empty overlap, for n < 3, for k < 1, for k + n > m (tested without forming the sum) and, when smooth > 1, for smooth > m - w (tested by subtraction); any smooth <= 1 means unsmoothed, and every int parameter may be math.MaxInt or math.MinInt. For smooth > 1 the outputs from w + smooth - 1 on are exactly d[w:].XAverage(smooth)[smooth-1:], with d the unsmoothed result on the overlap.

Pairing is by paired window: the returns are per leg, and each regression pairs the n returns of s and s2 that end at the same bar. Unsmoothed outputs are start-invariant; smoothed ones are not, because the exponential average starts at the first valid bar. Valid data: price levels, whose k-bar returns it regresses, because Katsanos holds the regression valid on price differences or yields only, not on raw prices (linearity and normality). Units: percentage points of k-bar return, centred on 0.

Before smoothing, a NaN or an infinity at overlap index p of either input makes returns p and p + k NaN, so the NaN outputs are [p, p+n-1] and [p+k, p+k+n-1], clipped to [w, m), and later outputs recover; a zero at p makes return p + k NaN, as its base is zero. A window holding a NaN return gives NaN, as its slope and intercept are both NaN. A flat window of returns of s2 gives beta = 0 and alpha the mean of the returns of s, so the unsmoothed result is that mean minus rs1. With smooth > 1, a NaN unsmoothed value at p makes every output from max(p, w + smooth - 1) on NaN, because the exponential recursion never recovers.

The book’s defaults (Appendix A.1) are n = 300, k = 15 and smooth = 3, so the minimum overlap is 317; his gold systems (Table 11.1) use n = 400 with k = 12 against the XAU and k = 10 against the dollar index. His trading use (Figure 9.5) is to buy when it peaks above about +3 and turns down, and to sell when it troughs below about -4 and turns up. Gold against the XAU with n = 400 and k = 12 made $621,084 in 31 trades with 81% winners and a profit factor of 28.91 (Table 11.1), and Katsanos stresses that the regression system needs no calculation or optimisation of the regression coefficients. Chained with an oscillator such as ChannelPosition(200, 3) applied from the divergence’s first valid index (Window(-413)), the first oscillator value of the gold and XAU system (400, ROC 12, smoothing 3, oscillator 200 and 3) is at overlap index 614, so it needs 615 bars; Katsanos’s 617 adds the five lengths with an 11-day ROC (11 + 400 + 3 + 200 + 3). The 2017 Nikkei system (Stocks & Commodities 35:8) uses n = 50, k = 3 and smooth = 1 on DXJ against FXY with ChannelPosition(50, 2); its EasyLanguage code zero-fills the first returns, so that its first windows hold zeros, which this method never does. The no-intercept listing (Appendix A.1, page 299, “PRED:=b*RS2”) keeps the with-intercept b and drops only a, so in exact arithmetic it is this method’s unsmoothed value minus Intercept on the return tails, then smoothed; it is not BetaOrigin. The gold-stock listing’s “DIVERG:=(PRED-RS)*10” subtracts the price ratio RS, a typo for RS1; neither the typo nor the *10 is reproduced. RegressionSpread on returns is its negation up to rounding, except where RegressionSpread’s perfect-fit guard returns 0. Overlapping k-bar returns are autocorrelated, so each regression is about as precise as 3n/(2k) independent pairs of returns.

Deviations from the MetaStock code: the smoothing average is seeded with the simple average of its first smooth values, where MetaStock’s Mov(…, E) is taken to seed with the first value (the MetaStock forum’s formula); every output before the first valid index is 0; and a negative base gives the signed return (v/(-v0) + 1)*100 rather than that of ROC.

Verification: two independent references, one in Python at 130 significant digits with every exact-zero and flatness decision made in rational arithmetic, and one in Julia that also translates the Appendix A.1 and Appendix A.2 listings, agree with it. There are exact identities: s2 = s and s2 = s.Mul(2) give 0 on every valid bar, with beta exactly 1 and alpha exactly 0 wherever the window’s returns are not flat; with smooth <= 1 and positive inputs, over the overlap views a and b, with ra := a.RateOfChange(k).Window(-k) and rb likewise, it equals ra.Beta(rb, n).Mul(rb).Add(ra.Intercept(rb, n)).Sub(ra) bit for bit from index n - 1 of that composition on, a composition through RateOfChange, Beta, Intercept, Mul, Add and Sub; and the smoothed tail equals XAverage of the unsmoothed values.

NonConfirmation

func (s Series) NonConfirmation(s2 Series, n, k int) Series

NonConfirmation returns +1 when s makes a new n-bar high that s2 has not confirmed within k bars, -1 for an unconfirmed new low, and 0 otherwise.

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, and the minimum overlap is n + k bars.

On the paired bars, a series makes a new high when its value is strictly greater than the highest of the n bars before it, and a new low when it is strictly less than the lowest of them. A new high of s is confirmed when s2 made a new high on any of the last k bars, the current bar included, and a new low of s when s2 made a new low on any of them. The output is an event code, not a level: +1 marks a new high of s that s2 has not confirmed, which in Dow usage is a bearish warning for s (a high that the partner did not confirm), and -1 marks an unconfirmed new low, the bullish counterpart. Because +1 is bearish, the output stands outside the sign conventions of the divergence methods. Sum(m) of the output, the unconfirmed highs less the unconfirmed lows of the last m bars, gives a divergence-pressure gauge.

From the first valid index, the output is NaN when s holds a NaN or an infinity in the n + 1 bars ending at the current bar, the window of its breakout tests, or s2 holds one in the n + k bars ending there, the windows of its last k breakout tests. Overlap index t, counted from the first paired bar, is result index off + t, and a non-finite value at overlap index p gives NaN on the overlap indices [max(p, n+k-1), p+n] when it is in s and on [max(p, n+k-1), p+n+k-1] when it is in s2; later outputs recover. If n < 1, k < 1 or the overlap is shorter than n + k bars, the result is all zero. Every output is in {-1, 0, +1} or NaN, with zeros always +0, and the output is bitwise start-invariant: it depends only on the windows of its bar, so dropping bars from the front of both inputs leaves every later output unchanged.

Warm-up hazard: the +0 outputs before the first valid index read as “no event”, which is harmless for a rule that waits for +1 or -1, but a count such as Sum(m) over a window that reaches into the warm-up covers fewer than m live bars. Trim the warm-up with Window(-(off+n+k-1)) or mask it with SetN(off+n+k-1, math.NaN()). Sum(m) counts bars, not episodes: a run of unconfirmed new highs adds one per bar, and a NaN output makes Sum(m) NaN for the next m bars.

Confirmation only looks back: if s2 makes its new high after s made its own, the earlier +1 is not withdrawn, where a Dow theorist may accept the later confirmation. Both series use the same n.

Dow theory reads Industrials against Transports: the averages must confirm one another, and a new high in the Industrials that the Transports do not match is a warning, which industrials.NonConfirmation(transports, n, k) flags. John Murphy, Intermarket Analysis (2004), applies the same test between markets, with bonds failing to confirm the Dow’s summer-1990 highs. Martin Pring’s relative-strength non-confirmation (Stocks & Commodities 19:7) sets a price against its own relative-strength line: with a, b := s.Overlap(s2), a.NonConfirmation(a.PriceRelative(b, 1), n, k) flags a new high of a that its strength against b does not confirm. A partner expected to move inversely enters as s2.Mul(-1). The bars since the leader’s high are n - 1 - IndexOfHighestS(n), that is s.IndexOfHighestS(n).Mul(-1).Add(n - 1), because IndexOfHighestS counts from the oldest bar of its n-bar window. Breadth against an index is another use, with an advance-decline line as s2. Dow confirmation by swing highs and lows is not covered, because the library’s pivot methods return index lists, not causal series.

From bar n on, the breakout test is s.GT(s.Highest(n).Shift(1)) and the new-low test s.LT(s.Lowest(n).Shift(1)). With a constant s2, which makes no new highs or lows, the output equals s.Breakout(n, 1) at every paired bar from the first valid index on finite data; s.NonConfirmation(s, n, k) is zero there wherever the windows are finite; and s.NonConfirmation(s.Mul(-1), n, 1), the inverse partner at k = 1, equals s.Breakout(n, 1) from overlap index n on finite data, since every new high of s is a new low of its inverse and never a new high.

Verification: a Python reference evaluates the definition by brute force with exact comparisons of the inputs, an independent Julia reference reproduces it, and both match this method exactly; the tests also check the constant-s2 identity with Breakout(n, 1), the zero for s2 == s, the inverse-partner identity at k = 1 and the breakout test against Highest.

ZScoreDivergence

func (s Series) ZScoreDivergence(s2 Series, n, smooth int) Series

ZScoreDivergence returns Katsanos’s correlation-weighted z-score divergence of s2 minus s over n bars, smoothed by a smooth-bar exponential moving average.

It is the z-score divergence of Markos Katsanos, Intermarket Trading Strategies (Wiley, 2008), section 9.7, pages 130-131, with the MetaStock listing in Appendix A.2, page 312. On the overlap, za and zb are Standardise(n) of the overlap views of s and s2, each bar measured against the mean and population standard deviation of its own n-bar window, and r is the n-bar Pearson correlation of the pair, equal bit for bit to s.Correlation(s2, n) on the overlap. The raw value at overlap bar j is (zb[j]r[j]/math.Abs(r[j]) - za[j]) math.Abs(r[j]), the operation order of the published ZDIV:=(Z2*r/Abs(r)-Z1)*Abs(r), evaluated left to right as Go does, and 0 where r[j] == 0. For smooth > 1 the output is the exponential moving average XAverage(smooth) of the raw values from the first raw bar on, bit for bit, and zero while it warms up.

In exact arithmetic raw = r*Zs2 - |r|*Zs, with Zs and Zs2 the z-scores of s and s2: the code form, not the text form r*(Z2 - Z1) of page 130. For r < 0 the text form flips both legs, whereas the code sign-adjusts only the intermarket leg, as c does in the disparity methods; with s2 = s.Mul(-1), zb == -za bit for bit and r == -1, so the code form gives exactly 0 and the text form does not.

The sign is intermarket minus base: a positive value means that s stands lower in its window than s2, sign-adjusted by the correlation, stands in its own, so s has lagged s2, which is bullish for s if the gap closes by mean reversion.

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, counted from the start of the overlap, is n - 1 for smooth <= 1 and n + smooth - 2 for smooth > 1, and the minimum overlap is one bar more: n bars unsmoothed and n + smooth - 1 bars smoothed. That sets the history budget: Katsanos’s n = 500 with smooth = 3, against the dollar index, needs a minimum overlap of 502 bars. Every output before the first valid index is zero. Any smooth <= 1 (zero, negative and math.MinInt included) is valid and gives the raw divergence. Invalid parameters give aligned zeros, a fresh all-zero series of len(s): an empty overlap, n < 2, n greater than the overlap, or a smooth > 1 that leaves fewer than smooth raw values. The checks never add parameters together, so every int from math.MinInt to math.MaxInt is safe, and an invalid one returns at once without allocating anything beyond the zero result.

Pairing class: a paired window for r, which sees the same n bars of both series, and per leg for the z-scores, with a recursive tail when smoothed. It is not start-invariant: the z-scores use a rolling variance that depends on earlier history, and the smoothing is recursive. Valid data: any finite data whose deviations from their window mean lie between about 1e-154 and 1e154/sqrt(n), the range of the rolling variance behind Standardise (outside it the value is NaN or a silent 0, which reads as no divergence), since the value is unchanged by shifting either input or scaling it by a positive factor (a negative factor on s2 is absorbed by the correlation’s sign, and one on s negates the value), so it is the recommended divergence for spreads, rates and back-adjusted futures, and for long windows, where BollingerDivergence is ill-conditioned. The output is in standard-deviation units centred on 0, with |raw| <= 2*sqrt(n-1). With n = 2 both z-scores are plus or minus 1 and so is the correlation, so the value is 0 in exact arithmetic and rounding residue in binary64; use n >= 3.

Non-finite values: unsmoothed, a NaN or an infinity at overlap index p of either input gives NaN on [p, p+n-1], clipped to [n-1, m) with m the overlap length, and later bars recover once the value has left the window; smoothed, with p the index of the first NaN raw value, every output from max(p, n + smooth - 2) on is NaN for ever, because the recursion never recovers. A flat window in either series has r exactly 0 and gives 0, as does any other r == 0, so there is no zero-base NaN.

The value of this composite lies in its published, verifiable code, not in a demonstrated edge: Katsanos’s Chapter 11 tests have 9 to 38 trades over 1995 to 2007, with the levels optimised per system.

Katsanos’s defaults are n = 100 against the Philadelphia Gold and Silver Mining Index (XAU) and n = 500 against the dollar index, each with smooth = 3. On page 130 he suggests moving z-scores onto a scale centred on 50 (“adding 50 and multiplying by 10”); the method returns the unscaled value. Trading use: s2 can be a related market of either correlation sign; trade the extremes of a momentum oscillator of the divergence, as the Appendix A.2 systems do when they buy on the oscillator crossing below 80 (the XAU system) or 90 (the dollar index system) with ROC(SEC2,2) > 0. Katsanos (section 11.6) calls it “statistically unsound” because it assumes normality, yet in his Table 11.2 it beat every other single-intermarket test against the dollar index on net profit.

Deviations from Katsanos’s MetaStock code: the smoothing average is seeded with the simple average of its first smooth values, where MetaStock’s Mov(…, E) is understood to seed with the first value (a MetaStock forum formula; Katsanos does not state it), so the two converge after a few times smooth bars; the value is 0 where r == 0, where the listing would divide by Abs(r) = 0; the z-scores use population standard deviations, MetaStock’s Stdev being taken to be population; and outputs before the first valid index are zero rather than undefined.

Verification: two independent references, a Python reference in exact rational arithmetic (with 140-digit decimals for square roots and the smoothing) and a Julia reference in BigFloat that also translates Katsanos’s MetaStock listings, agree with it; and there are exact identities with existing methods: each z leg equals v.ZScore(n, v[i])[i] bit for bit, r equals s.Correlation(s2, n) on the overlap, and s2 = s, s.Mul(-1) and s.Mul(2) give exactly 0.

Compositions

Disparity, simple and exponential (Nison)

The percentage by which x stands above its n-bar simple or exponential average, (x/M - 1)*100: the disparity index, which Katsanos credits to Steve Nison’s Beyond Candlesticks (Intermarket Trading Strategies, section 9.3). Katsanos’s intermarket disparity subtracts one market’s disparity from the other’s, with 30-bar simple averages by default (IntermarketDisparity), and his forex systems use 10-bar exponential averages (Appendix A.8, IntermarketDisparityX). Dan Valcu’s disparity index, which he also calls an “elasticity index” (Stocks & Commodities 27:12), divides by the price instead: his formula is 100*(Price - EMA)/Price, and his chart’s reading of 31.78 for the Nasdaq Composite at 5046.86 against its 200-bar exponential average of 3443.04 holds only with that divisor (the average divisor gives 46.58). The two are increasing functions of each other: with d the recipe’s disparity, Valcu’s is 100*d/(100 + d). He reads it against each market’s own history, as a gauge of overextension that reverts to the mean.

simple := x.Div(x.Average(n)).Sub(1).Mul(100).Window(-(n - 1))
exponential := x.Div(x.XAverage(n)).Sub(1).Mul(100).Window(-(n - 1))
  • First valid index: n - 1 for both, counted from the start of x; element e of each is bar n - 1 + e of x. Minimum history: n bars, and four to five times n of burn-in before the exponential average forgets its seed.
  • Parameters: n >= 2. Katsanos’s section 9.3 formula uses 30 bars and his forex systems 10; Valcu’s chart shows 200, 50 and 10.
  • Order: the library’s percent leg, (x/M - 1)*100, which is RateOfChange’s order; Katsanos’s subtract-first form, (x - M)/M*100, is equal in exact arithmetic and agrees within 1e-13*max(1, |x/M|).
  • Domain: a zero average gives -100 (Div gives 0 for the quotient); a negative average reverses the sign. After a NaN the simple form recovers once the NaN has left its window, but the exponential form stays NaN.
  • Anchors: on a positive walk the simple and exponential forms are the negated legs of IntermarketDisparity and IntermarketDisparityX against all ones (n1 = n2 = n, smooth = 1, c = 1), trimmed by n - 1, compared with == (E); they are also the recipes “Mansfield relative strength” and “Katsanos’s RS-MA% (the exponential variant)” (Relative: Ratio and Relative Strength) with s2 all ones (E). Both agree with the subtract-first order within 1e-13*max(1, |x/M|).
  • Pinned by TestRecipeT4ADisparity; runnable example ExampleSeries_XAverage_disparity.

Katsanos’s RS-MA% column

Column D, “RS-MA%”, of the MetaStock exploration for Katsanos’s chapter 16 futures and ETF systems (Intermarket Trading Strategies, Appendix A.7): his relative strength, the 150-bar exponential average of the ratio of the security to the S&P 500, and its percentage above its own 120-bar exponential average. Among other conditions, the same systems enter only when that relative strength is above its 120-bar average, so the column shows by how much. Both lengths are fixed, as published; on the raw ratio, with any length, the reading is “Katsanos’s RS-MA% (the exponential variant)” (Relative: Ratio and Relative Strength).

a, b := s.Overlap(s2)
rs := a.PriceRelative(b, 150).Window(-149)
column := rs.Div(rs.XAverage(120)).Sub(1).Mul(100).Window(-119)
  • First valid index: 268 (149 for the smoothed ratio and 119 for its average), counted from the start of the overlap; element e is overlap bar 268 + e. Minimum overlap: 269 bars.
  • Seeds: both averages seed with the simple average of their first values, as XAverage does. MetaStock’s exponential average is understood to start from a single value, so a literal translation agrees only after both seeds have decayed: the 150-bar average’s start-up error falls by a factor of e about every 75 bars.
  • Order: the library’s percent leg; Katsanos’s column subtracts first, equal up to rounding, within 1e-13*max(1, |RS/MA|).
  • Domain: the ratio line’s NaN (a zero or non-finite s2, or a non-finite s) stays in both exponential averages from its bar on.
  • Anchors: with s2 all ones it is the exponential disparity at 120 of a.XAverage(150).Window(-149) (E); s2 == s gives exactly 0 (E).
  • Pinned by TestRecipeT4ADisparity.

Fixed-coefficient regression divergence (Katsanos)

Katsanos’s multiple regression divergence for gold (Intermarket Trading Strategies, section 9.9 and Appendix A.1): gold’s k-bar percent return as predicted by a fixed linear model of the k-bar returns of three related markets, a + b[0]*r1 + b[1]*r2 + b[2]*r3, less gold’s actual k-bar return, times 10, and then its 3-bar exponential average. Here a is the intercept, b the three coefficients and x the three predictors. His indicator uses k = 9, an intercept of 0.071 and coefficients of 0.216, 0.166 and -0.43 for the XAU, silver and the dollar index (his equation 9.15 rounds the intercept to 0.07); his gold system (Appendix A.2) uses k = 11 with 0.16, 0.2, 0.175 and -0.5 (his equation 11.1 prints 0.21 for silver) and reads the smoothed divergence through a 300-bar momentum oscillator. A positive divergence means that gold has lagged its prediction. The coefficients come from one regression, fitted offline; the rolling fit with one predictor is IntermarketRegressionDivergence. The four series are right-aligned at their most recent bar and trimmed to the shortest one’s length.

m := min(len(s), len(x[0]), len(x[1]), len(x[2]))
y := s.Right(m)
r1 := x[0].Right(m).RateOfChange(k)
r2 := x[1].Right(m).RateOfChange(k)
r3 := x[2].Right(m).RateOfChange(k)
pred := r1.Mul(b[0]).Add(a).Add(r2.Mul(b[1])).Add(r3.Mul(b[2]))
raw := pred.Sub(y.RateOfChange(k)).Mul(10).Window(-k)
smoothed := raw.XAverage(3).Window(-2)
  • First valid index: k for the raw divergence and k + 2 for the smoothed one, counted from the start of the four series’ common bars; element e of each is common bar k + e or k + 2 + e. Minimum history: k + 3 common bars (k + 1 for the raw divergence alone).
  • Parameters: k >= 1. The model has three predictors, as both published ones do; a model with fewer passes zero coefficients and a finite series in each unused slot, because a NaN times 0 is NaN.
  • Order: b[0]*r1 + a equals his intercept-first sum a + b[0]*r1 bit for bit, since floating-point addition commutes, and the other terms are added left to right, as in his code. The 3-bar average seeds with the mean of the first three raw values; a MetaStock average, understood to start from the first value, agrees once its seed has halved for a few dozen bars.
  • Domain: RateOfChange gives a 0 return at a zero base. A NaN reaches the raw divergence at its bar and k bars later, and the smoothed one from its bar on for ever.
  • Anchors: the smoothed divergence is the raw one’s XAverage(3), trimmed by 2 (E); with one predictor whose coefficients are the rolling regression’s at bar j (b = (Beta, 0, 0) and a = Intercept of the trimmed k-bar returns over n bars, at j), the raw divergence at j is 10 times IntermarketRegressionDivergence(..., n, k, 1) at j (E); with a and every coefficient 0 and finite predictors the raw divergence is -10 times s’s k-bar return, up to the sign of a zero (E); dropping leading common bars leaves the later raw values unchanged, bit for bit (E).
  • Pinned by TestRecipeT4AFixedRegression.

Price-space divergence (Katsanos)

Katsanos’s FTSE system (Intermarket Trading Strategies, chapter 14 and Appendix A.5) predicts the FTSE’s 10-bar percent return from those of the CAC 40, the S&P Bank Index (BIX) and the Amex Oil Index (XOI), with an intercept of -0.11 and coefficients of 0.551, 0.097 and 0.116 (his equation 14.1), turns the predicted return RG into a predicted price, the close k bars back grown by RG percent, and takes the divergence as the percentage by which that price stands above the current close, unsmoothed. The recipe writes the premium in the library’s percent leg, (pred/C - 1)*100 with C the close; his own order subtracts the close first and then divides by it, equal up to rounding. The arguments are those of the section above.

m := min(len(s), len(x[0]), len(x[1]), len(x[2]))
y := s.Right(m)
rg := x[0].Right(m).RateOfChange(k).Mul(b[0]).Add(a).Add(x[1].Right(m).RateOfChange(k).Mul(b[1])).Add(x[2].Right(m).RateOfChange(k).Mul(b[2]))
pred := y.Shift(k).Mul(rg.Div(100).Add(1))
div := pred.Div(y).Sub(1).Mul(100).Window(-k)
  • First valid index: k, counted from the start of the four series’ common bars; element e is common bar k + e. Minimum history: k + 1 common bars.
  • Parameters: k >= 1; Katsanos’s k = 10, on daily bars.
  • Return space: the premium equals 100*(RG - R)/(100 + R), with R the k-bar return of s, so it has the sign of the return-space divergence RG - R and nearly its size for small returns.
  • Domain: a zero current price gives -100 (Div gives 0 for the quotient), where Katsanos’s order gives 0; RateOfChange gives 0 at a zero base; NaN propagates.
  • Anchors: with a and every coefficient 0 and finite predictors it is (y[i-k]/y[i] - 1)*100 at finite non-zero prices, y being s on the common bars: the predicted price is then the price k bars back (E); dropping leading common bars leaves the later values unchanged, bit for bit (E); it equals 100*(RG - R)/(100 + R) within 1e-13*max(1, |pred/C|).
  • Pinned by TestRecipeT4AFixedRegression.

Relative-strength non-confirmation

Martin Pring’s relative-strength non-confirmation (Stocks & Commodities 19:7): a stock makes an n-bar high that its own relative-strength line against a benchmark does not confirm within k bars. Pring calls the stock against the market “the most common use of relative strength”; his Figure 4 shows a price reaching a new high after its relative-strength line has peaked.

a, b := s.Overlap(s2)
nc := a.NonConfirmation(a.PriceRelative(b, 1), n, k)
  • First valid index: n + k - 1, counted from the start of the overlap. Minimum overlap: n + k bars.
  • Reading: +1 where the stock made a new high that its ratio line did not confirm (relative strength is lagging the price: a warning), -1 where it made a new low that the ratio line did not confirm (relative strength is holding up), 0 otherwise. nc.Sum(m) counts the recent balance.
  • Domain: positive prices. A zero, non-finite or overflowing benchmark value makes the ratio line non-finite, and NonConfirmation gives NaN on the windows that hold it; a non-finite stock price gives NaN on the same footprint.
  • Anchor: with s2 all ones on finite positive s, the ratio line is s itself and the recipe is exactly 0 everywhere (E).
  • Related uses: an inverse partner is s2.Mul(-1) (Murphy’s bond prices against the CRB, Stocks & Commodities 10:5; Forest’s gold against the DJIA, Stocks & Commodities 11:1); bond yields, which trend with the CRB, enter directly. “Bars since the leader’s high” is s.IndexOfHighestS(n).Mul(-1).Add(n - 1), because IndexOfHighestS counts from the oldest bar of its window. An advance-decline line as the partner tests breadth against an index. Swing-based Dow confirmation needs a causal swing series, which the library does not provide.

Pinned by TestRecipeRSNonConfirmation, with the method’s example ExampleSeries_NonConfirmation.

Moving-slope rate of change (Rafter)

Rafter’s MSROC (Stocks & Commodities 23:9): the least-squares slope of the last n + 1 values divided by the value n bars back, his “N + 1 period movslope of price” over “(Price back N)”. It is a fraction per bar, not multiplied by 100, so lengths and instruments share a scale, and Rafter finds it smoother than a rate of change, which is “too jagged” for the correlation trade below. The composition divides by the absolute value of the base, which changes nothing on positive prices and keeps the slope’s sign on negative ones.

rate := x.Slope(n + 1).Div(x.Shift(n).Abs()).Window(-n)
  • First valid index: n, counted from the start of the series; element e is bar n + e. Minimum history: n + 1 bars.
  • Parameters: n >= 1, a window of n + 1 values.
  • Domain: positive prices. Div gives 0, not NaN, where the base n bars back is zero, even when the window holds a NaN; otherwise a non-finite value in the window gives NaN, and a flat window gives exactly 0.
  • Anchors: msroc(x, n).Mul(100) equals x.SlopePercent(n + 1), trimmed by n, bit for bit wherever the window’s first value is finite and non-zero, of either sign, and 100 times the quotient is finite (E); scaling x by a power of two leaves it unchanged (E); a flat window gives exactly 0 (E). On positive prices msroc(x, 1) equals x.RateOfChange(1) divided by 100 in exact arithmetic, Rafter’s “a two-period moving slope is identical to a one-period change”; in floating point the two differ by rounding, because the rate of change’s x_j/x_{j-1} - 1 cancels.
  • Uses: the three sections below. Rafter also averages the MSROCs of a sector’s stocks into “an equally weighted index of the sector’s slope” and ranks sectors by it: SAverage of the members’ MSROCs, ranked with CrossSectionalRank. He compares the quarterly MSROC of the monetary base with its annual MSROC and with the Fed’s target, two horizons on the same bars.
  • Pinned by TestRecipeT4ATASCMSROC; runnable example ExampleSeries_Slope_msroc.

MSROC spread

The two legs’ MSROCs at “now”, each on its own whole history, and their difference: positive where s is rising faster than s2 per unit of price. Being per leg, it allows legs of different lengths.

spread := msroc(s, n).Value() - msroc(s2, n).Value()
  • First valid index: n, per leg: element e of each leg’s MSROC is bar n + e of that leg, and the spread reads the last element of each. Minimum history: n + 1 bars per leg; a shorter leg’s MSROC is empty, and Value reads it as 0.
  • Domain: the MSROC’s, on each leg.
  • Anchors: on finite inputs s2 == s gives exactly 0 (E).
  • Pinned by TestRecipeT4ATASCMSROC.

Slope-matched leg sizing (Rafter)

Rafter matches the legs of a pair trade so that each leg’s dollar value times its MSROC is the same (Stocks & Commodities 23:9, where the legs are A and B). With prices P, quantities Q and MSROCs M at “now”, P_s*Q_s*M_s = P_s2*Q_s2*M_s2, so the quantity ratio is Q_s/Q_s2 = (P_s2*M_s2)/(P_s*M_s), which the recipe returns.

ms := msroc(s, n).Value()
ms2 := msroc(s2, n).Value()
ratio := (s2.Value() * ms2) / (s.Value() * ms)
  • First valid index: n, per leg: element e of each leg’s MSROC is bar n + e of that leg, and the ratio reads the last element of each. Minimum history: n + 1 bars per leg.
  • Domain: positive prices. The ratio is a plain floating-point division, not Div: a zero MSROC of s (a flat window) gives an infinite ratio, or NaN when the numerator is zero too, and a zero MSROC of s2 gives 0. MSROCs of opposite signs give a negative ratio.
  • Anchors: s2 == s gives exactly 1 wherever the last price times the MSROC is finite and non-zero (E).
  • Pinned by TestRecipeT4ATASCMSROC.

Correlation of MSROCs (Rafter)

Rafter takes “20-day MSROCs” of two related funds, the Biotech and Pharmaceutical HOLDRS (BBH and PPH), and “a 10-day moving correlation of their MSROCs” (Stocks & Commodities 23:9). It normally sits near +1; he suggests trading its dips into negative correlation, in anticipation of a return to the usual high correlation, and notes that four of the five dips on his chart would have been profitable. He does not say which leg is bought.

a, b := s.Overlap(s2)
corr := msroc(a, n).Correlation(msroc(b, n), c).Window(-(c - 1))
  • First valid index: n + c - 1, counted from the start of the overlap; element e is overlap bar n + c - 1 + e. Minimum overlap: n + c bars (30 for Rafter’s 20 and 10).
  • Parameters: n >= 1 and c >= 2; Rafter’s “20-day” MSROCs, read here as n = 20 (he does not say whether 20 counts N or the N + 1 values of the slope), and c = 10, on daily bars.
  • Domain: positive prices; then Correlation’s rules: a flat window gives 0 and a non-finite one NaN.
  • Pinned by TestRecipeT4ATASCMSROC.

Volatility-normalised momentum (Katsanos)

Katsanos’s long-only gold system, keyed to European bank futures (Stocks & Commodities 30:1), divides each market’s five-day percent change by the 40-day standard deviation of its price, and one condition of its buy rule requires the banks’ value to be below gold’s: his MetaStock ROC(SEC3,5,%)/STDEV(SEC3,40) < ROC(C,5,%)/STDEV(C,40), where C is the gold fund GLD and SEC3 the Dow Jones Eurostoxx Bank futures (FESB), on daily bars. The recipe is one side of that comparison.

momentum := x.RateOfChange(k).Div(x.StdDev(n)).Window(-max(k, n-1))
  • First valid index: max(k, n-1), counted from the start of the series; element e is bar max(k, n-1) + e. Minimum history: max(k, n-1) + 1 bars (40 for Katsanos’s 5 and 40).
  • Parameters: k >= 1 and n >= 2; Katsanos uses k = 5 and n = 40.
  • Units: a percent change over a standard deviation of price levels (StdDev, the population form, divisor n) has units of 1/price, so between instruments priced very differently the comparison depends on their price levels; use the return form below there.
  • Domain: positive prices. RateOfChange gives 0, not NaN, at a zero base, and Div gives 0 where the standard deviation is 0 (a flat window).
  • Pinned by TestRecipeT4ATASCVolatilityMomentum.

Return-volatility form

The scale-free alternative, which has no published source: the k-bar percent change over the standard deviation of one-bar percent changes over n bars, a dimensionless number that compares across price levels. The one-bar changes are trimmed of their warm-up 0 before StdDev, and the k-bar change by the same bar so that the operands have equal lengths.

momentum := x.RateOfChange(k).Window(-1).Div(x.RateOfChange(1).Window(-1).StdDev(n)).Window(-(max(k, n) - 1))
  • First valid index: max(k, n), counted from the start of the series; element e is bar max(k, n) + e. Minimum history: max(k, n) + 1 bars.
  • Parameters: k >= 1 and n >= 2.
  • Domain: positive prices. RateOfChange gives 0 at a zero base, and Div gives 0 where the one-bar changes are flat over the window.
  • Pinned by TestRecipeT4ATASCVolatilityMomentum.

Volatility-normalised momentum spread

The two legs’ volatility-normalised momenta at “now”, each on its own whole history, as v(s2) - v(s): positive means that s has lagged. With s the bank futures and s2 gold, Katsanos’s condition is a positive spread. Being per leg, it allows legs of different lengths.

spread := volatilityMomentum(s2, k, n).Value() - volatilityMomentum(s, k, n).Value()
  • First valid index: max(k, n-1), per leg: element e of each leg’s reading is bar max(k, n-1) + e of that leg, and the spread reads the last element of each. Minimum history: max(k, n-1) + 1 bars per leg; a shorter leg’s reading is empty, and Value reads it as 0.
  • Scale-free spread: subtract the legs’ returnVolatilityMomentum values in the same order; each leg then needs max(k, n) + 1 bars.
  • Domain: the volatility-normalised momentum’s, on each leg.
  • Anchors: on finite inputs s2 == s gives exactly 0 (E).
  • Pinned by TestRecipeT4ATASCVolatilityMomentum.

Relative: Lead-Lag and Phase

CrossCorrelation

func (s Series) CrossCorrelation(s2 Series, n, lag int) Series

CrossCorrelation returns the rolling Pearson correlation of s with s2 displaced by lag bars, where a positive lag pairs each bar of s with the value of s2 lag bars earlier.

At bar j it is the Pearson correlation, with the sums centred within the window, of the n bars s[j-n+1..j] against the n bars s2[j-n+1-lag..j-lag] when lag >= 0, and of the n bars s2[j-n+1..j] against s[j-n+1+lag..j+lag] when lag < 0, so the series displaced into the past is s2 for a positive lag and s for a negative one. The sign of lag is the sign of Shift: a positive lag pairs the current values of s with the earlier values of s2 and so measures how far s2 leads, a negative lag measures how far s leads, and both directions are causal, since every paired value is at or before bar j. Each pair of windows goes through the kernel of Correlation with the window of s2 as the regressor and the window of s as the response, so lag 0 is Correlation bit for bit, and with s2 = s the result is the rolling autocorrelation of s at lag bars. It is the displaced correlation of Katsanos (Intermarket Trading Strategies, Wiley 2008, section 7.2, pages 100 to 102), whose Table 7.3 on page 101 correlates weekly gold returns with the XAU, the dollar index, silver, the Venture index and the CRB at shifts of -10 to +10 days; his positive shift is a negative lag here when gold, the traded market, is s, so his shift of +k days with another market as s2 is s.CrossCorrelation(s2, n, -k). MetaStock’s Correl(DATA ARRAY, DATA ARRAY, PERIODS, SHIFT) shifts the second array to the right, so Correl(s, s2, n, k) pairs the same bars as s.CrossCorrelation(s2, n, k).

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, counted from the start of the overlap, is n+|lag|-1, and the minimum overlap is n+|lag|. An empty overlap, n < 2, n > min(len(s), len(s2)) or |lag| > min(len(s), len(s2))-n gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or lag; the lag is compared with the room left after n before its magnitude is taken, so every int value of n and lag, including math.MinInt and math.MaxInt, is accepted without overflow. Otherwise one kernel of capacity n (none at n = 2) serves every bar of the call.

Every output depends only on its two paired windows of n bars, so the value at a bar is the same whatever precedes those windows (start-invariant): dropping leading bars of both inputs leaves the later outputs unchanged bit for bit, and lengthening only the longer input adds leading zeros and changes nothing else. An output whose paired windows are finite is in [-1, 1], the centred co-moment ratio clamped to that range.

A flat paired window of either input gives exactly 0, and at n = 2 the result is the product of the signs of the two changes, -1, 0 or 1. An output whose paired windows hold a NaN or an infinity is NaN, and the outputs whose windows have moved past it recover: for lag >= 0 a non-finite value at overlap index p of s gives NaN on [p, p+n-1] and one in s2 gives NaN on [p+lag, p+lag+n-1]; for lag < 0 the roles swap, s on [p+|lag|, p+|lag|+n-1] and s2 on [p, p+n-1]; each range is clipped to the valid bars.

Use returns, such as RateOfChange, rather than prices, and prefer 1-bar or non-overlapping returns: overlapping k-bar returns smear a peak at one lag into a triangle of half-width k, as the XAU column of Katsanos’s Table 7.3 shows. For markets that close at different times, lag the later-closing market, or use synchronous proxies as Katsanos does with US-listed funds (Stocks & Commodities 35:8). His chapter 6 shifts the US indices by one day to predict the DAX (Table 6.1), and his section 8.4, pages 116 to 118, compares synchronous, leading and lagging 30-minute correlations of the Euro Stoxx against the S&P e-mini, 0.788 synchronous against 0.652 lagging; he reads the leadership as switching around the US open, from leading and lagging values that differ by 0.011 or less. Published leading indicators in Stocks & Commodities include Merrill’s Fed funds trend and free-reserves oscillator (7:7), Snead’s 26-week yield change as a regime filter (9:7), breadth momentum leading index momentum (Oliver and Esayian, 11:1) and the advance-decline line during Fed tightening (McClellan, 22:12). For k >= 1 the composition a.Window(-k).Correlation(b.Shift(k).Window(-k), n) of the overlap views a and b of Overlap is an alternative, and a negative lag -d composes causally as a.Shift(d).Window(-d).Correlation(b.Window(-d), n), which shifts s rather than s2; the method is justified by handling both signs in one call and by its warm-up, which stays aligned with s.

Verification: two independent references, a Python reference in exact rational arithmetic with high-precision square roots and a Julia reference, agree with it; and there are exact identities with existing methods: at lag 0 it equals s.Correlation(s2, n) bit for bit; for k >= 0 and inputs of equal length it equals s.Correlation(s2.Shift(k), n) from bar n-1+k on, before which the zero padding of Shift pollutes the windows of the composition (for a negative lag -d the matching identity shifts s instead, s.Shift(d).Correlation(s2, n) from bar n-1+d on, since a negative Shift of s2 would be look-ahead); s.CrossCorrelation(s2, n, k) equals s2.CrossCorrelation(s, n, -k) on inputs of equal length, the same pairs through a symmetric kernel; and a shifted copy s2 = s.Shift(D) with D >= 0 gives exactly 1 at lag -D on non-flat windows from bar n-1+D on.

LeadLag

func (s Series) LeadLag(s2 Series, n, maxLag int) Series

LeadLag returns the lag, in bars between -maxLag and maxLag, at which the rolling n-bar cross-correlation of s with s2 is strongest in absolute value.

At bar j it takes the value r(k) that s.CrossCorrelation(s2, n, k) gives at that bar for every lag k from -maxLag to maxLag, the same paired windows through the same kernel with the window of s2 as the regressor, and scans them in a fixed order: the incumbent starts at lag 0 with r(0); then for k = 1, 2, …, maxLag, first +k and then -k, the incumbent is replaced by lag k only when math.Abs(r(k)) exceeds math.Abs of the incumbent’s correlation by more than 1e-9; the lag of the final incumbent is the result, as a float64. The tolerance decides ties for the incumbent, so among equally strong lags the shorter wins and a positive lag beats the negative lag of the same length. The objective is the absolute correlation, so inverse relationships, gold against the dollar, are found, and LeadLagCorrelation carries the sign. The sign of the result is the sign of Shift and of CrossCorrelation: a positive lag means that the earlier values of s2 match the current values of s best, so s2 leads by that many bars, and a negative lag means that s leads. Katsanos (Intermarket Trading Strategies, Wiley 2008, section 7.2, Tables 7.3 to 7.5, pages 101 to 107) tabulates displaced correlations at shifts of -10 to +10 days and judges the lead from their lagging and leading averages (LeadLagBias) rather than from the strongest shift, which is the synchronous one in every column of those tables; the rolling scan is original to this library.

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, counted from the start of the overlap, is n-1+maxLag, and the minimum overlap is n+maxLag. A warm-up zero before the first valid index is indistinguishable from a chosen lag of 0, so make sure the overlap reaches the minimum before reading the result. An empty overlap, n < 2, maxLag < 0, maxLag > 64 or n+maxLag > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or maxLag; the span test subtracts each span from the room rather than adding the spans, so every int value of n and maxLag, including math.MinInt and math.MaxInt, is accepted without overflow. The cap of 64 on maxLag is a resource bound, at most 129 correlations per bar, where Katsanos scanned 10 each way; the cost is O((2*maxLag+1)*n) per bar with one kernel of capacity n (none at n = 2) per call.

Every output depends only on the 2*maxLag+1 pairs of n-bar windows ending at its bar or up to maxLag bars earlier, which together span the n+maxLag most recent bars of each input, so the value at a bar is the same whatever precedes them (start-invariant): dropping leading bars of both inputs leaves the later outputs unchanged bit for bit, and lengthening only the longer input adds leading zeros and changes nothing else. An output whose lag windows are all finite is an integer-valued float64 in [-maxLag, maxLag].

If the correlation at any scanned lag is NaN, because a window at that lag holds a NaN or an infinity, the output is NaN: the lag windows overlap, so the NaN is the window-local contract of the whole scan rather than of one lag. A non-finite value at overlap index p of either input therefore gives NaN on [p, p+n+maxLag-1], clipped to the valid bars, and the later outputs recover. Two ramps of small integers correlate exactly 1 at every lag (-1 if they run in opposite directions), and an input flat over the n+maxLag bars gives exactly 0 at every lag, so both keep lag 0; ramps with inexact steps come within a few ulps of 1, and the scan still keeps lag 0. Nearly linear windows keep lag 0 as well whenever |r(0)| is within 1e-9 of 1, even when a true displacement exists: s = t + 1e-6*t^2 against its own copy shifted by 3 bars, with n = 30 and maxLag = 5, returns 0. The scan is meant for white-noise-like returns, where a shifted copy is found at its true lag: s2 = s.Shift(D) with 0 < D <= maxLag gives -D, with LeadLagCorrelation exactly 1, wherever the window of s at lag -D is not flat and every other lag correlates below 1-1e-9 in absolute value. The result jumps between integer lags from bar to bar, so persistence rules belong downstream.

Katsanos scans shifts in Stocks & Commodities 38:9, where weekly percent changes computed daily and shifted by -7 to +7 days over two years find that bitcoin leads gold by one day and the S&P 500 by three; in 35:8 one-day leading and lagging correlations of one- and two-week changes ask whether the yen leads Japanese equities, on synchronous US-listed proxies that avoid non-synchronous closes. Saitta (17:5) lags gold by 0 to 20 months against bond yields, takes the signed maximum on one side, and trusts the lead only if it recurs in every 10-year and 5-year sub-period: a one-sided scan of the signed correlation over a lag range, the objective r rather than |r|, is a documented variant rather than a method of its own, and the sub-period check is the block practice described under CorrelationBreak, applied at the chosen lag.

Verification: two independent references, a Python reference and a Julia reference, both working from exact rational sums with high-precision square roots and deciding every comparison of the scan far above binary64 precision with the same tie rule, agree with it; at maxLag = 0 it is 0 wherever the window is finite and NaN elsewhere, and LeadLagCorrelation is Correlation; it equals the scan applied to explicitly computed CrossCorrelation values; and since no identity with an existing method exercises the scan itself, the alternative oracle is those references, which are partial entries on the alternative-oracle list, for the scan.

LeadLagBias

func (s Series) LeadLagBias(s2 Series, n, maxLag int) Series

LeadLagBias returns Katsanos’s time-weighted average of the correlations at which s2 leads minus those at which s leads, over lags 1 to maxLag.

At bar j, with r(k) the value that s.CrossCorrelation(s2, n, k) gives at that bar (the same paired windows through the same kernel with the window of s2 as the regressor), it is the sum, started at 0 and taken in ascending order of k = 1, 2, …, maxLag, of the terms w(k)(r(k) - r(-k)), where w(k) = (maxLag+1-k)/(maxLag(maxLag+1)/2) is Katsanos’s linear time weight: lag 1 has the largest weight, lag maxLag the smallest, and the weights sum to 1 in exact arithmetic. Each term is rounded to a float64 before it is added, an explicit conversion that stops the compiler fusing the multiplication into the addition, so the bias adds no architecture-dependent rounding of its own to the correlations it sums; the kernel rounds its products on their own too (GLE-485), so the bias has the same bits on arm64 and amd64. With maxLag = 0 there are no terms and the bias is 0, subject to the NaN rule below. A positive lag pairs the current values of s with the earlier values of s2, the sign of Shift, so a positive bias means that the past moves of s2 correlate more with the present moves of s than the reverse, that is, s2 tends to lead, and a negative bias means that s tends to lead. Katsanos (Intermarket Trading Strategies, Wiley 2008, Tables 7.3 to 7.5, pages 101 to 107) prints the two weighted averages as the “lag avg” and “lead avg” rows of his displaced-correlation tables: with gold, the traded market, as s, the weighted average of the correlations at positive lags, where gold lags the other market, reproduces his lag average, and the weighted average at negative lags, where gold leads, reproduces his lead average: in Table 7.3 they are 0.291 and 0.150 for the XAU against gold and -0.098 and -0.113 for the dollar index, whereas unweighted means give 0.189 and 0.065 for the XAU and do not match his rows. LeadLagBias is his lag average minus his lead average.

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, counted from the start of the overlap, is n-1+maxLag, and the minimum overlap is n+maxLag; a warm-up zero before the first valid index is indistinguishable from a bias of 0, so make sure the overlap reaches the minimum before reading the result. An empty overlap, n < 2, maxLag < 0, maxLag > 64 or n+maxLag > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or maxLag; the span test subtracts each span from the room rather than adding the spans, so every int value of n and maxLag, including math.MinInt and math.MaxInt, is accepted without overflow. The cap of 64 on maxLag bounds the work at 129 correlations per bar; the cost is O((2*maxLag+1)*n) per bar with one kernel of capacity n (none at n = 2) per call.

Every output depends only on the pairs of n-bar windows at the scanned lags, ending at its bar or up to maxLag bars earlier, which together span the n+maxLag most recent bars of each input, so the value at a bar is the same whatever precedes them (start-invariant): dropping leading bars of both inputs leaves the later outputs unchanged bit for bit, and lengthening only the longer input adds leading zeros and changes nothing else. An output whose lag windows are all finite lies in [-2, 2] up to one ulp of rounding (2.0000000000000004 is reachable at maxLag = 14), since every correlation is in [-1, 1] and the weights sum to one in exact arithmetic.

If the correlation at any scanned lag, lag 0 included, is NaN, because a window at that lag holds a NaN or an infinity, the output is NaN, the window-local contract shared with LeadLag and LeadLagCorrelation; a non-finite value at overlap index p of either input therefore gives NaN on [p, p+n+maxLag-1], clipped to the valid bars, and the later outputs recover. An input flat over the n+maxLag bars gives 0 at every lag and two ramps of small integers give 1 (or -1) at every lag, so both give a bias of 0; ramps with inexact steps give a bias of order 1e-16. With s2 = s the two lags of each term are the same pairs through a symmetric kernel, so every difference is +0 and the bias is +0 bit for bit; swapping s and s2 swaps r(k) and r(-k) bit for bit, so every term changes sign exactly and the bias is antisymmetric bit for bit apart from the sign of a zero result, which is +0 in both orders. Unlike the integer jumps of LeadLag the bias moves continuously with the correlations, and a value near 0 means no clear leader rather than a leader at lag 0.

Use returns rather than prices, as for CrossCorrelation. Katsanos’s leading against lagging comparisons ask the same question: whether the yen leads Japanese equities, from one-day leading and lagging correlations of one- and two-week changes on synchronous US-listed proxies (Stocks & Commodities 35:8), and whether bitcoin leads gold and the S&P 500, from weekly percent changes computed daily and shifted by -7 to +7 days over two years (38:9); the bias sums that evidence over every lag, weighting the nearest lags most, and changes sign when leadership switches, as Katsanos reads it doing around the US open for the Euro Stoxx against the S&P e-mini in his section 8.4, from differences of 0.011 or less.

Verification: two independent references, a Python reference and a Julia reference, both working from exact rational sums with high-precision square roots, agree with it; it is 0 for s2 = s and antisymmetric, so on inputs of equal length s.LeadLagBias(s2, n, maxLag) equals -s2.LeadLagBias(s, n, maxLag), a zero result aside; Katsanos’s weights reproduce the printed averages of Tables 7.3 to 7.5; and since no identity reaches an existing method, the alternative oracle is those references and the printed tables.

LeadLagCorrelation

func (s Series) LeadLagCorrelation(s2 Series, n, maxLag int) Series

LeadLagCorrelation returns the signed rolling n-bar cross-correlation of s with s2 at the lag chosen by LeadLag.

At bar j it is r(best), the value that s.CrossCorrelation(s2, n, best) gives at that bar, where best is the lag LeadLag returns there: the same scan of r(k) for k from -maxLag to maxLag, the same paired windows through the same kernel with the window of s2 as the regressor, and the same incumbent rule, lag 0 first, then +k and -k for k = 1, 2, …, maxLag, replaced only when math.Abs(r(k)) exceeds math.Abs of the incumbent’s correlation by more than 1e-9, keeping the correlation of the final incumbent rather than its lag. It is signed, so an inverse relationship found by the scan shows as a negative value, and its magnitude is the strongest absolute correlation over the scanned lags, up to the tie tolerance. With maxLag = 0 the scan holds lag 0 alone and the result is Correlation bit for bit. Katsanos prints the displaced correlations that it scans (Intermarket Trading Strategies, Wiley 2008, Tables 7.3 to 7.5, pages 101 to 107) and, in his section 8.4, pages 116 to 118, one leading and one lagging correlation beside the synchronous one, 0.788 synchronous against 0.652 lagging for the Euro Stoxx against the S&P e-mini; there the synchronous correlation is the strongest, and it is what this method would report.

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, counted from the start of the overlap, is n-1+maxLag, and the minimum overlap is n+maxLag; a warm-up zero before the first valid index is indistinguishable from a correlation of 0, so make sure the overlap reaches the minimum before reading the result. An empty overlap, n < 2, maxLag < 0, maxLag > 64 or n+maxLag > min(len(s), len(s2)) gives a series of len(s) zeros, in O(len(s)) time whatever the size of n or maxLag; the span test subtracts each span from the room rather than adding the spans, so every int value of n and maxLag, including math.MinInt and math.MaxInt, is accepted without overflow. The cap of 64 on maxLag bounds the work at 129 correlations per bar; the cost is O((2*maxLag+1)*n) per bar with one kernel of capacity n (none at n = 2) per call.

Every output depends only on the 2*maxLag+1 pairs of n-bar windows ending at its bar or up to maxLag bars earlier, which together span the n+maxLag most recent bars of each input, so the value at a bar is the same whatever precedes them (start-invariant): dropping leading bars of both inputs leaves the later outputs unchanged bit for bit, and lengthening only the longer input adds leading zeros and changes nothing else. An output whose lag windows are all finite is in [-1, 1].

If the correlation at any scanned lag is NaN, because a window at that lag holds a NaN or an infinity, the output is NaN, the window-local contract of the whole scan, since the lag windows overlap; a non-finite value at overlap index p of either input therefore gives NaN on [p, p+n+maxLag-1], clipped to the valid bars, and the later outputs recover. An input flat over the n+maxLag bars gives exactly 0 at every lag, and the result is the +0 of lag 0; two ramps of small integers correlate exactly 1 at every lag (-1 in opposite directions), and the result is that value, where ramps with inexact steps give a value within a few ulps of it; at n = 2 every r(k) is the product of the signs of two changes, so the result is -1, 0 or 1. A shifted copy s2 = s.Shift(D) with 0 < D <= maxLag gives exactly 1, at lag -D, wherever the window of s at lag -D is not flat and every other lag correlates below 1-1e-9 in absolute value; where |r(0)| is within 1e-9 of 1, as on nearly linear windows such as s = t + 1e-6*t^2, lag 0 is kept and the result is r(0). Ties between equally strong lags go to the incumbent, so the value reported is that of the shorter lag, or of the positive lag at equal length.

Read it with LeadLag: the lag says which input leads and by how many bars, this method says how strongly, and its sign says whether the two move together or against each other. Katsanos sets one-shift leading and lagging correlations beside the synchronous one in his section 8.4, and his scan in Stocks & Commodities 38:9, weekly percent changes computed daily and shifted by -7 to +7 days over two years, reports the strength at which bitcoin leads gold by one day and the S&P 500 by three; 35:8 sets one-day leading and lagging correlations of one- and two-week changes of the yen and Japanese equities beside the synchronous one, on synchronous US-listed proxies. Use returns rather than prices, as for CrossCorrelation.

Verification: two independent references, a Python reference and a Julia reference, both working from exact rational sums with high-precision square roots and deciding every comparison of the scan far above binary64 precision with the same tie rule, agree with it; at maxLag = 0 it is Correlation bit for bit and LeadLag is 0 wherever the window is finite; it equals the scan applied to explicitly computed CrossCorrelation values; and since no identity with an existing method exercises the scan itself, the alternative oracle is those references, which are partial entries on the alternative-oracle list, for the scan.

Relative: Cross-Section

CrossSectionalRank

func (s Series) CrossSectionalRank(ss ...Series) Series

CrossSectionalRank returns the percentile of s among itself and the other series at each right-aligned bar, from 0 for the weakest to 100 for the strongest.

s is the base and ss the other members; the inputs are right-aligned at their most recent bar, only the trailing bars common to all of them are ranked, the result has len(s), and bars before the overlap are zero. The first valid index is len(s) minus the shortest length, and the minimum overlap is 1.

Let K = 1 + len(ss) be the number of members. At each bar of the overlap, with less the number of other members strictly below s and equal the number equal to s (negative zero equals positive zero), the result is 100*(less + 0.5*equal)/(K-1), the percentage of the other members that s is above. Ties count half, so a member equal to s adds one half to the count; this is the normalised mid-rank, (r - 1)/(K - 1) for a mid-rank r. The numerator is exact, so the division is the only rounding. Every result lies from 0 to 100, or is NaN, with 100 meaning that s is above every other member and 0 that it is below every one. Tied leaders score below 100 (two leaders among three members score 75 each), and with an even K and no ties no member scores 50, so select the largest rank rather than testing for 100.

With no other series (K = 1) the result is all zeros and no index is valid. An empty member likewise leaves no overlap, and the result is all zeros.

The NaN rule is pointwise: if s or any other member is NaN or infinite at a bar of the overlap, the result at that bar is NaN, and no other bar is affected. The method is start-invariant, bit for bit: each bar is ranked from the values at that bar alone, so a result does not depend on how far back the inputs start. Only the order of the values matters: applying to every member one strictly increasing map that keeps values finite and distinct values distinct changes nothing, and neither does reordering ss, bit for bit.

Rank each member’s own scale-free momentum measure, never raw price or ratio levels, for example a.RateOfChange(n).Window(-n).CrossSectionalRank(b.RateOfChange(n).Window(-n), c.RateOfChange(n).Window(-n)), where Window(-n) drops the first n bars, which have no n-bar change; the result then starts n bars after a’s first bar. A rate of change or log return of the ratio against one common benchmark, over the same window, ranks members as their own returns do, up to rounding in near ties, so such a benchmark adds nothing to a rank; other relative measures, such as the RSI of the ratio, can rank differently. Rotate into the top rank, or into the middle ranks, Yang and Pinsky’s “mid-momentum” sectors. A rank always selects something, even when every member is falling, so an absolute-trend or correlation filter belongs beside it.

Katsanos’s sector rotation (Stocks & Commodities 43:9) uses such ranks. His weekly system ranks the eleven SPDR sector ETFs by their n-week rate of change, with n = 13 (n = 26 is his alternative), holds the top-ranked ETF for 52 weeks (26 is his alternative; the two choices are independent), optionally makes an ETF ineligible while its own 20-week moving average is falling, and can skip the top N. His daily Nasdaq-100 version ranks the 63-day rate of change, holds for 20 days, optionally trades only while the 25-day moving average of QQQ is rising, and skips the top three. Such eligibility filters, skipping the top N and holding periods are consumer options, not parameters of this method.

Kaeppel, Singer, Wright, Parzen and Vomund publish top-N rank-and-hold rotations, the Winklers a bottom-N one, and Schmidt and D’Errico ranking screens; Masonson’s review of ETFReplay describes an overall rank that fits a weighted sum of per-factor ranks, with mid-rank ties as the 0.5 weight gives, though the review does not state its tie rule. Rank buffers, cash members (the Hucks rank a money-market fund as a member) and carry rankings (Kaufman) are consumer logic.

Verification: no published source gives this formula, so the alternative oracle stands in for a published reference: a Python reference and a Julia reference of the definition agree with it on every row, together with the exact K = 2 identity: the result is exactly 100, 0 or 50 as s is above, below or equal to the other series.

CrossSectionalZScore

func (s Series) CrossSectionalZScore(ss ...Series) Series

CrossSectionalZScore returns how many population standard deviations s lies above or below the mean of itself and the other series at each right-aligned bar.

s is the base and ss the other members; the inputs are right-aligned at their most recent bar, only the trailing bars common to all of them are scored, the result has len(s), and bars before the overlap are zero. Series are paired by position only; see “Time alignment” in the manual.

With K = 1 + len(ss) the number of series, the mean is the sum s + ss[0] + … + ss[K-2] in that order divided by K, as SAverage computes it; the standard deviation is the population one, the square root of the mean squared deviation from that mean; and the result is the deviation of s divided by it, or 0 when it is 0 or when all the series are equal at that bar. Writing z for the result, pandas’ default ddof = 1 gives z*sqrt((K-1)/K) instead.

For K >= 2 the first valid index is len(s) minus the shortest length, and the minimum overlap is 1. K = 1 (no other series) or an empty member give zeros at every bar, even where s is not finite. The NaN rule is pointwise: a NaN or an infinity in any input at a bar of the overlap gives NaN at that bar only. Each bar of the overlap depends only on the inputs at that bar, so the result is start-invariant.

In exact arithmetic |z| <= sqrt(K-1). Rounding can exceed that bound slightly, and by far more when the members nearly coincide, because the rounding error of the mean is then large relative to their spread: no ulp bound holds for any K. For K = 2, s = 100 against 100*(1 + 3e-13) gives -1.0004735968892793, and two members one ulp apart can score 0 and about sqrt(2); for K = 3 a member at the minimum can score above 0. The last bits of the mean depend on the order of the arguments, so the members’ z-scores, each computed with that member as s, sum to 0 only approximately, and not at all near coincidence.

Scaling every input by the same power of two leaves the result unchanged unless the squares overflow or underflow. When the sum of the squared deviations exceeds math.MaxFloat64, which takes a deviation of at least about 1.3e154/sqrt(K), the result is 0, or NaN when the sum of the inputs or the deviation of s itself overflows, unless all the series are equal. Deviations all below about 1.6e-162 give 0, and below about 1.5e-154 the squares are subnormal and the result is inaccurate.

Uses: Parzen, testing Martin and McCann’s relative strength oscillator (RSO), z-scores the RSO values of a 15-ETF universe cross-sectionally each week and holds the top three by z-scored RSO, excluding ETFs with fewer than 104 weeks of history from the ranking (Stocks & Commodities 44:7); he does not say which standard deviation he uses. Cline’s second method weights each leg of a basket by its RSI’s deviation from the group’s mean RSI (Stocks & Commodities 33:5); this method divides that deviation by the group’s standard deviation, which rescales each bar’s weights without changing their proportions. Unlike CrossSectionalRank, the z-score keeps the size of a lead; within a bar it orders the members as their values do, except in near ties, so a top-N pick by it, as in Parzen, matches one by the raw values or by CrossSectionalRank. A per-leg measure, such as each member’s own momentum, makes the most recent bar comparable across members with different calendars; earlier bars are still paired by position.

Warm-up hazard: before the overlap the output is 0, which reads as a member at the mean; trim it with Window(m), m the shortest length, or mask it with SetN(len(s)-m, math.NaN()). Inside the overlap 0 also marks equal values, s exactly at the mean, and squares that overflow or underflow. A member that starts later shortens the overlap for every member, so a universe that grows needs one call per set of members.

Verification: two references in exact rational arithmetic, one in Python and one in Julia, agree with it within a per-row tolerance; the mean it uses equals SAverage bit for bit, and on small integers two series give exactly -1 and 1.

Compositions

Hayes’s weekly score

Hayes, of Ned Davis Research, ranks currencies by the average of their 4-, 12- and 52-week rates of change on Friday closes (Stocks & Commodities 10:5), from the highest score down. His rate of change is the change over the earlier close; RateOfChange’s percent is 100 times that, which changes no rank. His text and the opening of his figure 5 caption give the long leg as 56 weeks, while the caption’s step-by-step procedure uses 52; the recipe uses 52.

score := x.RateOfChange(4).Add(x.RateOfChange(12)).Add(x.RateOfChange(52)).Div(3).Window(-52)
  • First valid index: 52, counted from the start of the series; element e is bar 52 + e. Minimum history: 53 weekly bars.
  • Domain: positive prices. RateOfChange gives 0, not NaN, at a zero base.
  • Published ranks: Hayes prints a ranking for 17 January 1992, from the yen first to the Australian dollar tenth; its closes are not transcribed, so the recipe is not checked against it.
  • Pinned by TestRecipeT4ATASCScores.

Hayes’s daily score

Hayes’s short-term score on daily bars: twice the 10-day rate of change plus the 25-day one, divided by three (Stocks & Commodities 10:5).

score := x.RateOfChange(10).Mul(2).Add(x.RateOfChange(25)).Div(3).Window(-25)
  • First valid index: 25, counted from the start of the series; element e is bar 25 + e. Minimum history: 26 daily bars.
  • Domain: as the weekly score’s.
  • Pinned by TestRecipeT4ATASCScores.

Cafritz’s and Rugg’s momentum sums

Two monthly mutual-fund scores: Cafritz’s Composite Performance Measure, the sum of the 3-, 6- and 9-month percentage changes in net asset value, as tested by Dworkin (Stocks & Commodities 5:7), and Donald Rugg’s sum of the 1-, 3-, 6- and 9-month changes, which Zin ranks by (7:12). Both are sums rather than averages, which changes no rank.

roc3, roc6, roc9 := x.RateOfChange(3), x.RateOfChange(6), x.RateOfChange(9)
dworkin := roc3.Add(roc6).Add(roc9).Window(-9)
zin := x.RateOfChange(1).Add(roc3).Add(roc6).Add(roc9).Window(-9)
  • First valid index: 9 for both, counted from the start of the series; element e of each is bar 9 + e. Minimum history: 10 monthly bars.
  • Inputs: monthly net asset values adjusted for distributions (Zin adjusts for capital-gain distributions but not for income dividends).
  • Domain: positive values. RateOfChange gives 0, not NaN, at a zero base.
  • Pinned by TestRecipeT4ATASCScores.

Quarter weighting (Vomund)

Vomund’s ranking splits the recent past into four consecutive quarters and averages their changes with the latest counted twice, (2*q1 + q2 + q3 + q4)/5 with q1 the latest: in Stocks & Commodities 21:10 the quarters of the past year, and in 23:1 AIQ’s short-term relative strength report, which breaks the last 120 days into quarters. The quarters are disjoint sub-periods, not nested horizons.

q := x.RateOfChange(h)
score := q.Mul(2).Add(q.Shift(h)).Add(q.Shift(2 * h)).Add(q.Shift(3 * h)).Div(5).Window(-4 * h)
  • First valid index: 4h, counted from the start of the series; element e is bar 4h + e. Minimum history: 4h + 1 bars.
  • Parameters: h >= 1: 13 for a year of weekly bars, 30 for the 120-day window on daily bars.
  • Caution: an average of percentage changes is biased upward: a fall of 50% followed by a rise of 100% averages +25%, yet the price is back where it started. Vomund’s example (21:10) is a stock that halved and then doubled: “Averaging the percentages shows a positive number, but the stock is breakeven”, and he finds such low-priced rebounders dominating the relative-strength rankings after the bubble. The log-change score below has no such bias.
  • Domain: positive prices. RateOfChange gives 0, not NaN, at a zero base.
  • Anchors: on a series flat for its first 3h + 1 bars, the first element is RateOfChange(h) at bar 4h times 2, divided by 5 (E).
  • Pinned by TestRecipeT4ATASCScores.

Log-change score

The log change over h bars, ln(x_j) - ln(x_{j-h}). It is the sum of the log changes of any sub-periods that make up the h bars and equals the log of the total change, so it has no upward bias: a fall of 50% and a rise of 100% are log changes of -ln 2 and +ln 2, which sum to 0. On one horizon it is ln(1 + R/100) for the percent change R, an increasing function, so it ranks members as RateOfChange(h) does, in exact arithmetic.

score := x.Log().Momentum(h).Window(-h)
  • First valid index: h, counted from the start of the series; element e is bar h + e. Minimum history: h + 1 bars.
  • Units: log points; Mul(100) puts them on a percent-like scale.
  • Domain: positive prices: Log gives -Inf at a zero price and NaN at a negative one.
  • Pinned by TestRecipeT4ATASCScores.

Ranking a universe by a score

Each member (here a, b and c) is scored on its own history, then compared with the others at each bar: CrossSectionalRank gives the receiver’s percentile among the members, 100 for the strongest, and CrossSectionalZScore its distance from their mean in standard deviations, which keeps the size of the lead. Both right-align the members at their most recent bar, so members of different lengths are scored over their common trailing bars. Any score above, or a weighted sum of rates of change, takes the place of hayesWeekly; add the other members as further arguments.

ha, hb, hc := hayesWeekly(a), hayesWeekly(b), hayesWeekly(c)
rank := ha.CrossSectionalRank(hb, hc)
z := ha.CrossSectionalZScore(hb, hc)
  • First valid index: 52, counted from the start of the members’ common overlap: on members of equal length element e of rank and of z is bar 52 + e; with unequal lengths both have len(ha) elements, and those before the scores’ common trailing bars are 0. Minimum overlap: 53 bars.
  • Aligned zeros: a 0 before the common bars, or at every bar when a member has 52 bars or fewer (its score is empty, which leaves no overlap), is not a rank but reads as the weakest one, and as the mean in z. Check every member’s length, or read only the last min(len(ha), len(hb), len(hc)) elements. Warm-up zeros of a score that is not trimmed rank as real values in the same way.
  • Rank buffers: holding rules are consumer logic over the rank, and the published ones damp churn. Vomund buys the top two and sells a holding only when it is no longer in the top three, buying the highest-rated in its place, with the report run again two weeks later (23:1). Cafritz’s rules, as Dworkin tests them, buy the funds ranked 1 to 3, hold them while they stay in the top 25 of 85, sell a fund when its rank drops below 25 and buy any fund that rises to rank 1, 2 or 3 (5:7). Zin buys funds that have been in the top 20% for three consecutive months and sells a fund when it falls below the top 20% (7:12).
  • Ranks of several factors: the ETFReplay screen reviewed by Masonson (Stocks & Commodities 29:13) weights two return horizons and one volatility horizon, each with a user-set period and weight (50% six-month return, 40% three-month return and 10% 20-day volatility in one of his examples), and the overall ranks in his figure 5 agree with a weighted sum of per-factor ranks with low volatility ranking best. That is CrossSectionalRank of each factor, with the volatilities negated, combined with Mul and Add: each percentile is the same decreasing linear function of the member’s mid-rank, so the weighted sum of percentiles orders the members as the weighted sum of ranks does.
  • Benchmark columns: Pendergast’s RSI screen (Stocks & Commodities 32:4) lists each stock’s 4-, 13-, 26- and 52-week and year-to-date relative strength against the S&P 500. Against a common benchmark, a relative strength that increases with the member’s own change (a return difference or a ratio) ranks the members as their own changes do, so the fixed columns are the members’ RateOfChange(4), RateOfChange(13) and so on on weekly bars, ranked; the year-to-date column needs timestamps.
  • Pinned by TestRecipeT4ATASCRanking; runnable example ExampleSeries_CrossSectionalRank_hayes.

Relative: Timeframes

PeriodToDateHighest

func (s Series) PeriodToDateHighest(newPeriod Series) Series

PeriodToDateHighest returns at each bar the highest value of the receiver from the first bar of the current period to that bar, where newPeriod is non-zero and not NaN on the first bar of each period; it equals HighestSince(newPeriod) on every bar.

A period runs from one flagged bar to the bar before the next flagged bar. At bar j let b1 be the latest flagged bar up to j, and b0 the latest flagged bar before b1. The current period is b1 to j and the completed one b0 to b1-1. The result is the highest value of the receiver from b1 to j, so the flagged bar b1 opens the period and is part of it. It is the developing period, built from the base bars to date: a completed value is not written back inside its own period. The method is s.HighestSince(newPeriod) bit for bit, under this family’s name: the same flag rule, alignment and recursion, and NaN before the first flag.

A value of newPeriod is a flag when it is non-zero and not NaN, so +Inf and -Inf are flags while 0, -0 and NaN are not. The flags come from the consumer. In algolang, kit.TradeDateStarts(bars, cal, tf) returns them for a day, an ISO week, a month or a year, and kit.SessionStarts(bars, cal) for sessions, oldest first: 0 on the oldest bar, so the first, possibly truncated, period is not latched; 1 on the first bar of each new period; and 0 on a bar without a trade date and on the bar after it, so the periods either side of an unknown day merge. A bar’s date is that of its last instant.

newPeriod is right-aligned with the receiver at the most recent bar: the last values are paired, then the second last, and so on. A newPeriod longer than the receiver contributes only its last len(s) values, and a shorter one pairs only the receiver’s last len(newPeriod) bars: the bars before the overlap see no flag, are not read and give NaN. An empty or all-zero newPeriod gives all NaN. The result has length len(s).

The warm-up is NaN rather than 0 because the start depends on the data, since 0 would be a readable level of the receiver. The first valid index is the first flagged bar of the overlap, so the minimum overlap is one flagged bar. With none, every output is NaN.

The maximum is Go’s builtin max, as in HighestSince, so a NaN gives NaN from its bar to the end of its period, where the next flag restarts the maximum. Also max prefers +0 over -0, infinities are ordinary values, and every output that is not NaN is a value of the receiver. A NaN before the first flag changes nothing.

On the last bar of a period the result is the value that PriorPeriodHighest holds through the next period. With every bar flagged the result is the receiver itself on the overlap.

Uses: the session-to-date high on intraday bars, with the flags from kit.SessionStarts(bars, cal), and the level of the breakout of Unger (Stocks & Commodities 44:7) on 15-minute bars: the session’s high to date.

Verification: a Python reference and a Julia reference that translates Bobrowski’s VT Trader listing, and the anchor that with a flag every k bars the result on the last bar of each block equals s.Highest(k).

PeriodToDateLowest

func (s Series) PeriodToDateLowest(newPeriod Series) Series

PeriodToDateLowest returns at each bar the lowest value of the receiver from the first bar of the current period to that bar, where newPeriod is non-zero and not NaN on the first bar of each period; it equals LowestSince(newPeriod) on every bar.

A period runs from one flagged bar to the bar before the next flagged bar. At bar j let b1 be the latest flagged bar up to j, and b0 the latest flagged bar before b1. The current period is b1 to j and the completed one b0 to b1-1. The result is the lowest value of the receiver from b1 to j, so the flagged bar b1 opens the period and is part of it. It is the developing period, built from the base bars to date: a completed value is not written back inside its own period. The method is s.LowestSince(newPeriod) bit for bit, under this family’s name: the same flag rule, alignment and recursion, and NaN before the first flag.

A value of newPeriod is a flag when it is non-zero and not NaN, so +Inf and -Inf are flags while 0, -0 and NaN are not. The flags come from the consumer. In algolang, kit.TradeDateStarts(bars, cal, tf) returns them for a day, an ISO week, a month or a year, and kit.SessionStarts(bars, cal) for sessions, oldest first: 0 on the oldest bar, so the first, possibly truncated, period is not latched; 1 on the first bar of each new period; and 0 on a bar without a trade date and on the bar after it, so the periods either side of an unknown day merge. A bar’s date is that of its last instant.

newPeriod is right-aligned with the receiver at the most recent bar: the last values are paired, then the second last, and so on. A newPeriod longer than the receiver contributes only its last len(s) values, and a shorter one pairs only the receiver’s last len(newPeriod) bars: the bars before the overlap see no flag, are not read and give NaN. An empty or all-zero newPeriod gives all NaN. The result has length len(s).

The warm-up is NaN rather than 0 because the start depends on the data, since 0 would be a readable level of the receiver. The first valid index is the first flagged bar of the overlap, so the minimum overlap is one flagged bar. With none, every output is NaN.

The minimum is Go’s builtin min, as in LowestSince, so a NaN gives NaN from its bar to the end of its period, where the next flag restarts the minimum. Also min prefers -0 over +0, infinities are ordinary values, and every output that is not NaN is a value of the receiver. A NaN before the first flag changes nothing.

On the last bar of a period the result is the value that PriorPeriodLowest holds through the next period. With every bar flagged the result is the receiver itself on the overlap.

Uses: the session-to-date low on intraday bars, with the flags from kit.SessionStarts(bars, cal), and the level of the breakout of Unger (Stocks & Commodities 44:7) on 15-minute bars: the session’s low to date.

Verification: a Python reference and a Julia reference that translates Bobrowski’s VT Trader listing, and the anchor that with a flag every k bars the result on the last bar of each block equals s.Lowest(k).

PriorPeriodHighest

func (s Series) PriorPeriodHighest(newPeriod Series) Series

PriorPeriodHighest returns at each bar the highest value of the receiver over the most recent completed period, where newPeriod is non-zero and not NaN on the first bar of each period.

A period runs from one flagged bar to the bar before the next flagged bar. At bar j let b1 be the latest flagged bar up to j, and b0 the latest flagged bar before b1. The completed period is b0 to b1-1, and the result is the highest value of the receiver over it, held from b1 up to the bar before the next flag. The flagged bar b1 opens the new period and is not part of the completed one. This is the first-bar rule of Steckler (Stocks & Commodities 18:8), whose daily stochastic signal is gated by the weekly %K of the “preceding Friday’s” close, a completed week used from the next week’s first bar. A first-bar flag is known at its bar’s close from the previous bar’s date, whereas a last-bar flag reads the next bar’s date, which is look-ahead.

A value of newPeriod is a flag when it is non-zero and not NaN, so +Inf and -Inf are flags while 0, -0 and NaN are not. The flags come from the consumer. In algolang, kit.TradeDateStarts(bars, cal, tf) returns them for a day, an ISO week, a month or a year, and kit.SessionStarts(bars, cal) for sessions, oldest first: 0 on the oldest bar, so the first, possibly truncated, period is not latched; 1 on the first bar of each new period; and 0 on a bar without a trade date and on the bar after it, so the periods either side of an unknown day merge. A bar’s date is that of its last instant.

newPeriod is right-aligned with the receiver at the most recent bar: the last values are paired, then the second last, and so on. A newPeriod longer than the receiver contributes only its last len(s) values, and a shorter one pairs only the receiver’s last len(newPeriod) bars: the bars before the overlap see no flag, are not read and give NaN. An empty or all-zero newPeriod gives all NaN. The result has length len(s).

The warm-up is NaN rather than 0 because the start depends on the data, since 0 would be a readable level of the receiver. The first valid index is the second flagged bar of the overlap, so the minimum overlap is two flagged bars. With fewer, every output is NaN.

The extremes use Go’s builtin max, as HighestSince does, so a NaN in a period gives NaN for the whole next period; max prefers +0 over -0, infinities are ordinary values, and every output that is not NaN is a value of the receiver. A NaN before the first flag changes nothing, and a NaN is not carried past the period after its own, because each flag restarts the running extreme.

Dropping the first d bars of both inputs leaves every output from the cut’s own first valid index unchanged, bit for bit, and a period cut by the start is not latched. For any lengths the result equals s.HighestSince(newPeriod).Shift(1).SetN(1, math.NaN()).ValueWhen(newPeriod) on every bar. The method does this in one pass and one allocation instead of four series, with the mask SetN(1, math.NaN()) built in: without it the composition latches the 0 that Shift puts on bar 0 when the first period starts there.

Uses: c.FloorTraderPivot(ph, pl, pc) is the pivot of the prior period, with ph from PriorPeriodHighest, pl from PriorPeriodLowest and pc from PriorPeriodLast. Krausz (Stocks & Commodities 14:11) takes the balance point of the prior higher-timeframe bar, which is that pivot, with bands at 0.5 and 0.618 of the prior range either side of it. Bobrowski (Stocks & Commodities 31:3) builds Camarilla levels from the prior high, low and close, with R4 as pc.Add(ph.Sub(pl).Mul(1.1).Div(2)) and R5 as ph.Div(pl).Mul(pc). Drummond (Stocks & Commodities 33:12) draws weekly and monthly levels on daily bars, and D’Errico and Trombetta (Stocks & Commodities 42:8) gate a breakout on hourly bars by the prior day’s compression. The cross-timeframe relative line is c.Sub(pl).Div(ph.Sub(pl)).

It is the hand latch of the section “Prior completed period’s open, high, low and close” in the “Floor Trader Pivots” compositions. The prior period’s first value is s.ValueWhen(newPeriod, 1), which has no method of its own. An indicator computed on weekly bars needs the weekly bars, built with kit.Resample, not the base bars.

Verification: a Python reference and a Julia reference that translates Bobrowski’s VT Trader listing, the anchor that with every bar flagged the result equals s.Shift(1) from the second bar of the overlap, and the anchor that with a flag every k bars the result on every bar of a block after the first equals s.Highest(k) on the last bar before the block.

PriorPeriodLast

func (s Series) PriorPeriodLast(newPeriod Series) Series

PriorPeriodLast returns at each bar the value of the receiver on the last bar of the most recent completed period, where newPeriod is non-zero and not NaN on the first bar of each period.

A period runs from one flagged bar to the bar before the next flagged bar. At bar j let b1 be the latest flagged bar up to j, and b0 the latest flagged bar before b1, if any. The completed period is b0 to b1-1, or the cut period up to b1-1 when there is no b0, and the result is the value of the receiver on its last bar, bar b1-1, sampled verbatim, so NaN, +Inf, -Inf and -0 are copied as they are, and a NaN elsewhere in the period does not matter. The value is held from b1 up to the bar before the next flag. The flagged bar b1 opens the new period and is not part of the completed one. This is the first-bar rule of Steckler (Stocks & Commodities 18:8), whose daily stochastic signal is gated by the weekly %K of the “preceding Friday’s” close, a completed week used from the next week’s first bar. A first-bar flag is known at its bar’s close from the previous bar’s date, whereas a last-bar flag reads the next bar’s date, which is look-ahead.

A value of newPeriod is a flag when it is non-zero and not NaN, so +Inf and -Inf are flags while 0, -0 and NaN are not. The flags come from the consumer. In algolang, kit.TradeDateStarts(bars, cal, tf) returns them for a day, an ISO week, a month or a year, and kit.SessionStarts(bars, cal) for sessions, oldest first: 0 on the oldest bar, so the first, possibly truncated, period is not latched; 1 on the first bar of each new period; and 0 on a bar without a trade date and on the bar after it, so the periods either side of an unknown day merge. A bar’s date is that of its last instant.

newPeriod is right-aligned with the receiver at the most recent bar: the last values are paired, then the second last, and so on. A newPeriod longer than the receiver contributes only its last len(s) values, and a shorter one pairs only the receiver’s last len(newPeriod) bars: the bars before the overlap see no flag, are not read and give NaN. A flag on the overlap’s first bar therefore gives NaN until the next flag, also when a shorter newPeriod leaves a bar of the receiver before it, which is the one difference from newPeriod with zeros prepended. The result has length len(s). An empty or all-zero newPeriod gives all NaN.

The warm-up is NaN rather than 0 because the start depends on the data, since 0 would be a readable level of the receiver. The first valid index is the first flagged bar of the overlap whose previous bar is also in the overlap, so the minimum overlap is two bars, the second of them flagged. The result is valid one period before PriorPeriodHighest and PriorPeriodLowest, whose extremes need the whole period: when the data start inside a period, the last value of that cut period is genuine. A NaN in the receiver changes the result only when it is the bar before a flag, which gives NaN from that flag to the bar before the next flag.

For a newPeriod at least as long as the receiver, the result equals s.Shift(1).SetN(1, math.NaN()).ValueWhen(newPeriod) on every bar. The SetN(1, math.NaN()) mask is built in: without it the composition latches the 0 that Shift puts on bar 0 when the first period starts there. When a shorter newPeriod flags the first bar of the overlap, the composition reads the bar before the overlap, where PriorPeriodLast gives NaN; they agree elsewhere. The method is one pass and one allocation.

The result is the pc of the floor pivot and of the Camarilla levels of Bobrowski (Stocks & Commodities 31:3), with ph and pl from PriorPeriodHighest and PriorPeriodLowest. It is the hand latch of the section “Prior completed period’s open, high, low and close” in the “Floor Trader Pivots” compositions. The prior period’s first value is s.ValueWhen(newPeriod, 1), which has no method of its own. An indicator computed on weekly bars needs the weekly bars, built with kit.Resample, not the base bars.

Verification: a Python reference and a Julia reference that translates Bobrowski’s VT Trader listing, and the anchor that with every bar flagged the result equals s.Shift(1) from the second bar of the overlap.

PriorPeriodLowest

func (s Series) PriorPeriodLowest(newPeriod Series) Series

PriorPeriodLowest returns at each bar the lowest value of the receiver over the most recent completed period, where newPeriod is non-zero and not NaN on the first bar of each period.

A period runs from one flagged bar to the bar before the next flagged bar. At bar j let b1 be the latest flagged bar up to j, and b0 the latest flagged bar before b1. The completed period is b0 to b1-1, and the result is the lowest value of the receiver over it, held from b1 up to the bar before the next flag. The flagged bar b1 opens the new period and is not part of the completed one. This is the first-bar rule of Steckler (Stocks & Commodities 18:8), whose daily stochastic signal is gated by the weekly %K of the “preceding Friday’s” close, a completed week used from the next week’s first bar. A first-bar flag is known at its bar’s close from the previous bar’s date, whereas a last-bar flag reads the next bar’s date, which is look-ahead.

A value of newPeriod is a flag when it is non-zero and not NaN, so +Inf and -Inf are flags while 0, -0 and NaN are not. The flags come from the consumer. In algolang, kit.TradeDateStarts(bars, cal, tf) returns them for a day, an ISO week, a month or a year, and kit.SessionStarts(bars, cal) for sessions, oldest first: 0 on the oldest bar, so the first, possibly truncated, period is not latched; 1 on the first bar of each new period; and 0 on a bar without a trade date and on the bar after it, so the periods either side of an unknown day merge. A bar’s date is that of its last instant.

newPeriod is right-aligned with the receiver at the most recent bar: the last values are paired, then the second last, and so on. A newPeriod longer than the receiver contributes only its last len(s) values, and a shorter one pairs only the receiver’s last len(newPeriod) bars: the bars before the overlap see no flag, are not read and give NaN. An empty or all-zero newPeriod gives all NaN. The result has length len(s).

The warm-up is NaN rather than 0 because the start depends on the data, since 0 would be a readable level of the receiver. The first valid index is the second flagged bar of the overlap, so the minimum overlap is two flagged bars. With fewer, every output is NaN.

The extremes use Go’s builtin min, as LowestSince does, so a NaN in a period gives NaN for the whole next period; min prefers -0 over +0, infinities are ordinary values, and every output that is not NaN is a value of the receiver. A NaN before the first flag changes nothing, and a NaN is not carried past the period after its own, because each flag restarts the running extreme.

Dropping the first d bars of both inputs leaves every output from the cut’s own first valid index unchanged, bit for bit, and a period cut by the start is not latched. For any lengths the result equals s.LowestSince(newPeriod).Shift(1).SetN(1, math.NaN()).ValueWhen(newPeriod) on every bar. The method does this in one pass and one allocation instead of four series, with the mask SetN(1, math.NaN()) built in: without it the composition latches the 0 that Shift puts on bar 0 when the first period starts there. The result equals, apart from NaN payloads, s.Mul(-1).PriorPeriodHighest(newPeriod).Mul(-1).

Uses: pl in c.FloorTraderPivot(ph, pl, pc), in the balance point and bands of Krausz (Stocks & Commodities 14:11) and in the Camarilla levels of Bobrowski (Stocks & Commodities 31:3). See PriorPeriodHighest for these uses with ph and pc.

It is the hand latch of the section “Prior completed period’s open, high, low and close” in the “Floor Trader Pivots” compositions. An indicator computed on weekly bars needs the weekly bars, built with kit.Resample, not the base bars.

Verification: a Python reference and a Julia reference that translates Bobrowski’s VT Trader listing, the anchor that with every bar flagged the result equals s.Shift(1) from the second bar of the overlap, and the anchor that with a flag every k bars the result on every bar of a block after the first equals s.Lowest(k) on the last bar before the block.

Compositions

Timeframe emulation by length

nseries has no timestamps. A higher-timeframe indicator is emulated on the base bars by multiplying every length by the number of base bars per higher bar: MACD(5*f, 5*sl), Stochastic(h, l, 5*n), r.XAverage(5*n) and so on, including every relative entry that takes lengths. No method is needed. The published conversion factors:

FromToFactorSource
weeklydaily×5 (12 and 26 weeks are 60 and 130 days; a 14-week %K is 70 days, with the 3-bar slowing kept, not multiplied)Vitali Apirine, Stocks & Commodities 35:13, 36:2, 36:10; Martin Pring, 26:12 (his intermediate group only)
weeklydaily20 days ≈ 4 weeks ≈ a 40% weekly exponential; 40 days ≈ 8 weeks ≈ 22%Arthur Merrill, 8:9
monthlyweekly×4.33 (9, 12, 18 and 24 months are 39, 52, 78 and 104 weeks)Pring, 10:10, 18:4; Narcouzi, 19:8
monthlyweekly14 months ≈ 59 weeks: the author’s own figure, 294 days divided by 5, not 14 × 4.33 = 60.6Meibuhr, 10:12
monthlydaily×21 (1 and 12 months are 21 and 252 days; 14 months ≈ 294 days)Schmidt, 40:2; Meibuhr, 10:12
dailyhourly7 bars for an equity session; 23 for 60-minute gold futures (a 23-bar high as “roughly a daily rolling high”)Carey, 13:5; D’Errico and Trombetta, 42:8
daily30-minute×13 (a daily SuperSmoother(4) is about a 30-minute SuperSmoother(52); 26 for less lag)John Ehlers, 34:1

The emulations are partial in places: Apirine’s weekly slow stochastic keeps its 3-bar slowing rather than making it 15; Pring’s daily Special K is exactly ×5 only in its intermediate group (its long group uses spans 195, 265, 390 and 530 where ×5 gives 195, 260, 390 and 520, and its short group is the daily short-term KST); Meibuhr’s 59 weeks is his own figure. Guppy’s two groups of daily averages stand for two timeframes, because his software “made it easier to measure weeks in days than it would be to shift to a weekly view” (16:2), and long horizons serve as regimes: Russell’s barometer, the daily close against a 30-week average, is a 150-bar average of daily closes, and a 200-day average is a popular overlay for Dow Theory’s primary trend (Bowman and Hartle, 8:9).

Fidelity. Over 2003 to 2007 on the Russell 2000, all eight centreline crossovers of Apirine’s ×5 daily MACD fell within six business days of the true weekly MACD’s (35:13, figure 8). The differences come from holidays (Pring: “not an exact simulation because some weeks only contain four trading days”), from phase (a rolling five-bar window is not a calendar week) and from aliasing. Emulation on base bars does not alias, and Ehlers notes that it allows a shorter-than-proportional length for less lag. Averages emulate best; range-based indicators include partial weeks in their windows.

History budget. Emulation multiplies the minimum overlaps: a 26-week average emulated as XAverage(130) needs 130 bars plus four to five times 130 of burn-in.

Weekly and daily MACD on daily bars (Apirine)

Apirine’s weekly MACD on daily bars is the ×5 MACD, and his relative daily MACD adds the daily MACD to it (Stocks & Commodities 35:13). In this and the next two recipes c, h and l are one instrument’s daily closes, highs and lows, of equal length.

weekly := c.MACD(60, 130).Window(-129)
relative := c.MACD(60, 130).Add(c.MACD(12, 26)).Window(-129)
  • First valid index: 129 for both, counted from the start of c. Minimum length: 130 bars, plus four to five times 130 bars of burn-in before the 130-bar average forgets its seed.
  • Pinned by TestRecipeT3TASCWeeklyDailyMACD; runnable example ExampleSeries_MACD_weeklyOnDaily.

Weekly and daily PPO (Apirine)

Apirine’s percentage form of the same pair (Stocks & Commodities 36:2): the weekly PPO is the ×5 MACD as a percentage of the 130-day average, and the daily term is the 12- and 26-day MACD as a percentage of the same 130-day average, so the two terms share a denominator; the relative daily PPO is their sum. The PPO’s advantage over the MACD is that its readings can be compared across securities and over long periods in which the price has doubled or tripled.

e := c.XAverage(ws)
w := c.XAverage(wf).Sub(e).Div(e).Mul(100).Window(-(ws - 1))
d := c.XAverage(df).Sub(c.XAverage(ds)).Div(e).Mul(100).Window(-(ws - 1))
rel := w.Add(d)
  • First valid index: ws - 1 for both, counted from the start of c (129 at Apirine’s 60, 130, 12 and 26). Minimum length: ws bars.
  • Parameters: 1 <= wf < ws, 1 <= df < ds <= ws, so one trim of ws - 1 bars covers every average.
  • Printed values: Apirine’s spreadsheet (36:2, figure 1; the fixture tasc-apirine-36-2.csv) is reproduced from its printed closes and its printed first-row averages: its first weekly PPO is 100 × 21.26/1215.39 = 1.749 from the printed weekly MACD (1.748 from the printed averages, whose difference is 21.25), printed 1.75, and its last relative daily PPO, 2.592 from the printed averages and printed 2.59, is reproduced only with the 130-day denominator (dividing the daily term by the 26-day average gives 2.570).
  • Pinned by TestRecipeT3TASCWeeklyDailyPPO and TestRecipeT3TASCApirinePrinted.

Weekly and daily stochastics (Apirine)

Apirine’s weekly and daily slow stochastics (Stocks & Commodities 36:10): a 3-bar simple average of the 70-bar and of the 14-bar stochastic, an average of ratios. The weekly one is a partial emulation: its %K length is ×5, but its 3-bar slowing is kept rather than made 15.

w := c.Stochastic(h, l, nw).Window(-(nw - 1)).Average(slowing).Window(-(slowing - 1))
d := c.Stochastic(h, l, nd).Window(-(nd - 1)).Average(slowing).Window(-(slowing - 1))
  • First valid index: nw + slowing - 2 and nd + slowing - 2, counted from the start of the instrument (71 and 15 at Apirine’s 70, 14 and 3). Minimum length: nw + slowing - 1 and nd + slowing - 1 bars. With a slowing of 1, leave out the last trim, which Window(0) would empty.
  • Domain: the existing Stochastic, which gives 0 on a flat window, where the channel-position convention of this library gives 50; and an average of ratios, which the slowed stochastic that divides smoothed sums matches only at a slowing of 1.
  • Printed values: his spreadsheet (36:10, figure 1; the fixture tasc-apirine-36-10.csv) is reproduced from its printed closes and printed 70- and 14-bar highs and lows, and its printed 3-bar averages from the printed stochastics.
  • Pinned by TestRecipeT3TASCWeeklyDailyStochastic and TestRecipeT3TASCApirinePrinted.

Relationship lines on emulated bars

An emulated weekly relationship line on daily bars is the composition with multiplied lengths, on a common clock: for example the log ratio’s MACD with lengths 60 and 130 (the MACD of the log ratio, under “Relative: Ratio and Relative Strength”), or a 13-week RSMK with 3-week smoothing as RSMK(b, 65, 15).

a, b := s.Overlap(s2)
rsmk := a.RSMK(b, n, smooth).Window(-(n + smooth - 1))
  • First valid index: n + smooth - 1, counted from the start of the overlap (79 for 65 and 15). Minimum overlap: n + smooth bars (80 for 65 and 15).
  • Pinned by TestRecipeT3TASCEmulatedRSMK.

Per-leg readings across bar intervals

Legs on different bar intervals (a daily s against a weekly s2) can be compared only through per-leg readings, each leg composed on its own history with lengths that span the same calendar time and read at “now” with Value. The two-series methods themselves pair by position and cut both legs to the overlap, so even those that take a length for each leg do not see a leg’s history beyond the shorter leg’s length. RSMK and Stress take one length for both legs, so they cross bar intervals only through their per-leg forms. For RSMK, the log relative momentum of the two legs:

mom := 100 * (s.Log().Momentum(n1).Value() - s2.Log().Momentum(n2).Value())

and, because the average is linear, its smoothed form 100*(s.Log().Momentum(n1).Window(-n1).XAverage(k1).Value() - s2.Log().Momentum(n2).Window(-n2).XAverage(k2).Value()) with calendar-matched k1 and k2. For Stress’s first stage, the per-leg channel-position divergence s2.ChannelPosition(n2, 1).Value() - s.ChannelPosition(n1, 1).Value() (of opposite sign to Stress’s difference). Anything that annualises states its bar interval: the same volatility annualised by the square root of bars per year differs materially between daily, weekly and monthly bars (Katsanos, Stocks & Commodities 44:3).

  • Pinned by TestRecipeT3TASCPerLegLogMomentum.

Which relative indicators are meaningful across timeframes

Pairing classAcross timeframesRule
Per leg, with a length for each leg: the per-leg compositions (relative momentum, the per-leg PPO difference, the PMO spread, the channel-position divergence) and the divergences’ legs composed per leg (each leg’s band position, disparity or percentage slope)Yes, with each leg on its own intervalChoose per-leg lookbacks that span the same calendar time and combine at “now” with Value. A smoothing tail pairs raw values bar by bar, so it needs a common clock. The two-series methods themselves pair by position; across intervals compose each leg on its own history and subtract at “now”.
Per leg, but one length for both legs: RSMK, Stress (its first stage)Only as per-leg compositions, or on a common clockdaily.RSMK(weekly, 90, 1) compares 90 days with 90 weeks; use the per-leg forms above.
Paired window, and recursive entries on a relationship line: correlations, betas, regressions, lead-lag, channels on relationship lines, the oscillator and regimes on a ratio, Stress’s second stageOnly on a common clockNever pair weekly with daily bars. Resample both legs to the higher interval, or emulate it on both legs’ base bars. Correlations differ by interval (Sharp found copper against the T-bond yield at 0.06 over 50 monthly averages and 0.37 over 100 daily closes, 8:4, so the span differs as well as the interval), so the interval is part of the parameters.
Pointwise: the ratio, the log ratio, the percent premiumOn a common clock, or as a level against a mapped higher-timeframe levelA mapped weekly level against a daily one is a same-bar comparison with a stale value: meaningful for levels (a daily close against the prior week’s range), not for returns.
The same symbol on two timeframesYes: treat the mapped or emulated higher-timeframe line as s2Apirine’s relative daily MACD and PPO above; a daily close against a 30-week average.
Votes across timeframesYes, once each timeframe’s state is on the base barsA count of agreeing states.

Mapping a higher timeframe onto base bars without look-ahead

When a strategy computes on true higher-timeframe bars and maps them back onto its base bars, five rules keep the mapping causal:

  1. Completed bars only. A base bar may use a higher-timeframe bar only if that bar closed at or before the base bar’s close; with bars stamped at their close, that is stamp <= epoch. Steckler’s rule is the published statement: the weekly %K that gates a daily entry within the week is “the preceding Friday’s” (Stocks & Commodities 18:8).
  2. Developing bars only when rebuilt from base bars to date. A week-to-date value built from the base bars so far is legitimate. A completed higher bar’s value must never be written back onto the base bars inside its own period: that is the “repainting” error.
  3. Stamps. A weekly bar stamped at the week’s start would leak the whole week onto its first base bar. A week whose stamp falls after its last session (a Friday holiday) appears one base bar late, which is conservative.
  4. Boundary flags. First-bar-of-period flags are causal; last-bar flags read the next bar’s timestamp. algolang’s kit functions give first-bar flags that need no correction (below; amended 19 October 2026, when they replaced the per-bar flags and the three corrections those needed).
  5. Forward fill. A mapped series repeats each higher value for its period, so its base-bar changes are mostly zero, and paired-window statistics on it are biased (the Epps effect): compute them on the common higher clock instead.

Sampling-interval effects. Sharp correlates monthly averages rather than month-end closes (8:3, 8:4), which no algolang accessor offers. Ehlers recommends a two-bar average or a four-bar SuperSmoother before applying any other indicator (34:1); the same filters protect any later decimation. Overlapping k-bar returns on base bars inflate any significance computed on them (Schmidt’s daily-rolled monthly returns, 40:2): their effective sample is about 3n/(2k). A true monthly beta or correlation needs non-overlapping two-series returns, which only true monthly bars give.

The algolang accessors for higher timeframes

nseries stays timestamp-free and adds no timestamp API; higher-timeframe bars come from algolang. At the time of writing:

  • A daily strategy can load true weekly bars with rs.LoadSeries(engine.SeriesOptions{BarInterval: "weekly"}) and read them with rs.Close(n). The engine advances the weekly series to its latest bar stamped at or before the primary’s bar, so it holds completed weeks only, provided weeks are stamped at their close. It holds at most 2*maxBarsBack weeks, and that is an upper bound: the warm-up skip applies to the primary only, so with a daily primary and a weekly secondary that start together, the weekly series holds about 10 bars on the first evaluated bar at the default maxBarsBack of 50. Check its length before relying on a per-leg lookback; a lookback longer than the history returns a warm-up zero without warning.
  • CloseW, CloseM and CloseCompressed (and their siblings) are stubs that return 0.0 or an empty series; do not use them.
  • CloseD gives completed days (with lookback = 1) or the developing day, and the split accessors (CloseSplit and siblings) completed or developing sub-day splits at clock times, but their cache is keyed without the data series, so they are safe for one data series only: CloseD(1, loc) after CloseD(0, loc) returns data 0’s closes. Do not use them for a second data series until algolang keys the cache by dataN.
  • (Amended 19 October 2026: this bullet described the per-bar flags IsFirstBarOfDay and IsFirstBarOfWeek and three corrections they needed; the kit functions replace them.) First-bar flags come from algolang’s kit: kit.TradeDateStarts(bars, cal, tf), for a day, an ISO week, a month or a year, and kit.SessionStarts(bars, cal), its day form, return one flag per bar, oldest first: 0 on the oldest bar, so the first, possibly truncated, period is not latched; 1 on the first bar of each new period; and 0 on a bar without a trade date and on the bar after it. A bar’s date is that of its last instant. kit.PeriodStarts flags calendar periods in a location. PriorPeriodLast, PriorPeriodHighest, PriorPeriodLowest, PeriodToDateHighest and PeriodToDateLowest take these flags (t4b-prior-period-levels.md), and kit.Resample builds the true higher-timeframe bars that an indicator computed on them needs.

Prior-period and period-to-date levels from first-bar flags (Steckler; algolang’s kit)

Five methods map a higher timeframe’s levels onto base bars from a flag series that is non-zero and not NaN on the first bar of each period. PriorPeriodLast, PriorPeriodHighest and PriorPeriodLowest hold the last, highest and lowest value of the most recent completed period through the next period; PeriodToDateHighest and PeriodToDateLowest are the highest and lowest value of the current period from its first bar to date, and equal HighestSince and LowestSince on every bar. A period runs from one flagged bar to the bar before the next, so the flagged bar opens the new period and is not part of the completed one. That is Steckler’s rule: his daily signal is gated by the weekly %K of “the preceding Friday’s” close, a completed week used from the next week’s first bar (Stocks & Commodities 18:8). A first-bar flag is known when its bar closes, from the previous bar’s date; a last-bar flag reads the next bar’s date, which is look-ahead.

nseries cannot import algolang, so the block below is algolang code, not compiled here: in a strategy the flags come from the kit, and the base-bar columns from the indicators bridge.

// In an algolang strategy: bars is the strategy's series and cal its calendar.
week := kit.TradeDateStarts(bars, cal, kit.TimeframeWeek)
h, l, c := indicators.Highs(bars), indicators.Lows(bars), indicators.Closes(bars)
ph, pl, pc := h.PriorPeriodHighest(week), l.PriorPeriodLowest(week), c.PriorPeriodLast(week)
  • Flags: kit.TradeDateStarts(bars, cal, tf), for a day, an ISO week, a month or a year, and kit.SessionStarts(bars, cal), its day form, return one flag per bar, oldest first: 0 on the oldest bar, so the first, possibly truncated, period is not latched; 1 on the first bar of each new period; and 0 on a bar without a trade date and on the bar after it, so the periods either side of an unknown day merge. A bar’s date is that of its last instant. kit.PeriodStarts flags calendar periods in a location rather than trade dates, with the oldest bar also unflagged. The kit’s flags replace the per-bar accessors and the three corrections that the timeframe page listed for them. indicators.Highs, Lows and Closes are oldest first, as nseries expects; they are views valid during the current bar’s call, and the methods return new series.
  • First valid index: data-dependent, NaN before it because the start depends on the data. PriorPeriodLast: the first flagged bar of the overlap whose previous bar is also in the overlap. PriorPeriodHighest and PriorPeriodLowest: the second flagged bar. The to-date pair: the first flagged bar. Minimum overlap: two bars, the second of them flagged; two flagged bars; one flagged bar. On the 200 daily AAPL bars of 2023 (16 March to 29 December) the ISO-week flags, 0 on bar 0, number 41, the first on bar 2, so the first valid indices are 2, 7, 7, 2 and 2.
  • Reading: pc is valid one period before ph and pl when the data start inside a period: that period’s last value is genuine, but its extremes are not. Good Friday, 7 April 2023, was a holiday, so the week of 10 April holds the high of 3 to 6 April, 166.84 (4 April), and Thursday 6 April’s close, 164.66.
  • Anchors: with every bar flagged the three prior methods equal s.Shift(1) from the overlap’s second bar, and the to-date pair equal the receiver on the overlap (E). With a flag every k bars from the overlap’s start, PriorPeriodHighest and PriorPeriodLowest hold s.Highest(k) and s.Lowest(k) of the bar before each block from the second block on, and the to-date pair equal them on each block’s last bar (E). On a period’s last bar PeriodToDateHighest is the value PriorPeriodHighest holds through the next period, and likewise the lows (E).
  • The floor-pivot hand latch: the compositions of “Prior completed period’s open, high, low and close” in floor-trader-pivots/t6-pivot-levels.md are these methods: t6PriorPeriodLevels’s ph, pl and pc equal PriorPeriodHighest, PriorPeriodLowest and PriorPeriodLast bit for bit on every bar of its 2001 calendar groups with the month, week and all-bar flags, and so does c.FloorTraderPivot(ph, pl, pc) on them. Its po := o.ValueWhen(ev, 1), the prior period’s first value, has no method.
  • Traps: an indicator computed on weekly bars, such as a weekly MACD or %K, needs the weekly bars from kit.Resample, not a prior-period level of the base-bar indicator. A NaN in the receiver gives NaN for the whole next period in ph and pl, from its bar to the end of its own period in the to-date pair, and in pc only when it is the period’s last value.
  • Pinned by TestRecipePriorPeriodLevels and TestPriorPeriodHandLatch; runnable examples ExampleSeries_PriorPeriodHighest, ExampleSeries_PriorPeriodLast and ExampleSeries_PeriodToDateHighest.

Camarilla levels of the prior period (Bobrowski)

Bobrowski builds the Camarilla levels from the prior period’s high H, low L and close C: R1 to R4 are C + (H - L)*1.1/k and S1 to S4 are C - (H - L)*1.1/k for k = 12, 6, 4 and 2, R5 is (H/L)*C and S5 is C - (R5 - C) (Stocks & Commodities 31:3). His VT Trader listing latches the prior period with valuewhen, HHV and LLV over the bars since the previous calculation time, a window that includes the new period’s first bar; the methods take the period that ends on the bar before the flag. On base bars with the flag f the levels are the floor-pivot page’s t6Camarilla on the latched period in place of the prior bar, in the same order of operations:

ph := h.PriorPeriodHighest(f)
pl := l.PriorPeriodLowest(f)
pc := c.PriorPeriodLast(f)
rg := ph.Sub(pl)
h1 := pc.Add(rg.Mul(1.1).Div(12))
h2 := pc.Add(rg.Mul(1.1).Div(6))
h3 := pc.Add(rg.Mul(1.1).Div(4))
h4 := pc.Add(rg.Mul(1.1).Div(2))
h5 := ph.Div(pl).Mul(pc)
l1 := pc.Sub(rg.Mul(1.1).Div(12))
l2 := pc.Sub(rg.Mul(1.1).Div(6))
l3 := pc.Sub(rg.Mul(1.1).Div(4))
l4 := pc.Sub(rg.Mul(1.1).Div(2))
l5 := pc.Sub(h5.Sub(pc))
  • Inputs: base-bar highs, lows and closes of one length, positive prices (a zero prior low gives h5 0 through Div’s zero rule), and the first-bar flag f.
  • First valid index: the second flagged bar for every level, NaN before: pc is valid earlier, but every level also reads ph and pl. Minimum overlap: two flagged bars.
  • Anchors: with every bar flagged the ten levels equal t6Camarilla’s on the same bars bit for bit, bar 0 included (both NaN) (E).
  • Printed values: the fixture tasc-bobrowski-31-3.csv holds synthetic base bars for his six DJIA examples, one block of 25, 15, 19, 27, 3 and 19 bars, his day counts, each with his printed high and low attained once and his printed close last, a flag on each block’s first bar and one terminal flagged bar. On the first bar of each later block, and through it, ph, pl and pc are his printed values bit for bit and the levels equal t6Camarilla’s on the floor-pivot fixture’s two-bar inputs: 31 of his 36 printed levels hold at two decimals, example 2’s S5 within 0.01 (11876.45 against the printed 11876.46), and four are misprints (example 1’s R4 13239.77 for 13249.77, example 4’s R3 12960.53 for 12960.33, and example 5’s S3 13234.89 and S4 13234.39 for 13234.39 and 13197.13). Every level is NaN on the first block, which has no completed period.
  • Evidence: he printed each period’s high, low and close and the levels, not his bars, so the fixture pins the latch’s selection and the order of operations, not his data. Evidence, not a reference.
  • Pinned by TestRecipePriorPeriodCamarilla.

Balance point and bands of the prior period (Krausz)

Krausz’s fixed balance point is the prior higher-timeframe bar’s high, low and close summed and divided by three, held through the next period, with a resistance band from 0.5 to 0.618 of the prior range above it and a support band from 0.5 to 0.618 of the range below (Stocks & Commodities 14:11); on daily bars with a week flag, the weekly balance point and bands.

ph := h.PriorPeriodHighest(f)
pl := l.PriorPeriodLowest(f)
pc := c.PriorPeriodLast(f)
bp := c.FloorTraderPivot(ph, pl, pc)
rg := ph.Sub(pl)
r50 := bp.Add(rg.Mul(0.5))
r618 := bp.Add(rg.Mul(0.618))
s50 := bp.Sub(rg.Mul(0.5))
s618 := bp.Sub(rg.Mul(0.618))
  • First valid index: the second flagged bar, NaN before. Minimum overlap: two flagged bars.
  • Anchors: bp is ((ph + pl) + pc)/3 in that order, the pivot’s fixed order (E); with every bar flagged it equals the classic pivot from the prior bar, c.FloorTraderPivot on h, l and c shifted by one bar, from bar 1 (E).
  • Traps: his balance point steps, the average of the last five weekly balance points, are bp.ValueWhen(f).Add(bp.ValueWhen(f, 1)).Add(bp.ValueWhen(f, 2)).Add(bp.ValueWhen(f, 3)).Add(bp.ValueWhen(f, 4)).Div(5), valid from the sixth flagged bar, not bp.Average(5) on base bars, which averages five days of mostly one held value. His dynamic balance point is a rolling synthetic week of the last five days, a window of base bars, not a prior-period level.
  • Evidence: his figures are charts with no numbers. Evidence, not a reference.
  • Pinned by TestRecipePriorPeriodKrausz.

The close against the prior period’s range

The position of the base close in the prior period’s range, 0 at the prior low and 1 at the prior high: a same-bar comparison of a level with a stale level, which the timeframe page’s table allows for levels.

ph := h.PriorPeriodHighest(f)
pl := l.PriorPeriodLowest(f)
rel := c.Sub(pl).Div(ph.Sub(pl))
  • First valid index: the second flagged bar; NaN before, where ph and pl are NaN. Minimum overlap: two flagged bars.
  • Traps: a flat prior period gives 0 through Div’s zero rule, not NaN or an infinity, so mask it where a flat period can occur.
  • Anchors: with every bar flagged it equals the same line on the prior bar’s high and low, h.Shift(1) and l.Shift(1), from bar 1 (E).
  • Pinned by TestRecipePriorPeriodRelativeLine.

Session-to-date high and low and the daily factor (Unger)

On 15-minute bars Unger enters with a stop at the session’s high to date for a long and at its low to date for a short (HighS(0) and LowS(0)), and filters the entries by his daily factor, the previous session’s body over its range, below a level such as 0.90 (Stocks & Commodities 44:7). With the session flag sess from kit.SessionStarts(bars, cal):

sh := h.PeriodToDateHighest(sess)
sl := l.PeriodToDateLowest(sess)
po := o.ValueWhen(sess, 1)
pc := c.PriorPeriodLast(sess)
df := po.Sub(pc).Abs().Div(h.PriorPeriodHighest(sess).Sub(l.PriorPeriodLowest(sess)))
  • Reading: sh and sl on a bar are the stop levels for the next bar; po is the previous session’s first open and pc its last close.
  • First valid index: sh, sl and pc from the first flagged bar (pc reads the bar before it, which the kit’s unflagged oldest bar provides); po and df from the second. Minimum overlap: one flagged bar for the levels and two for the factor.
  • Departures: his code sets the factor only when the previous range is not zero, so a flat session keeps the factor of the session before; Div gives 0 there, which passes any level. His weekly factor compares the body of the last five sessions, absvalue(opens(5)-closes(1)), with their range, maxlist(highs(1),highs(2),highs(3),highs(4),highs(5)) less the matching minlist of the lows: a rolling window of session values, not a calendar week’s prior-period level.
  • Anchors: sh and sl equal h.HighestSince(sess) and l.LowestSince(sess) bit for bit (E).
  • Pinned by TestRecipePriorPeriodUnger.