Relative: Channels

All functions · nseries package

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.

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.

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).

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.

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.

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.