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.
| Stage | First valid index f | Before f |
|---|---|---|
PriceRelative(b, k) | k - 1 (0 for k <= 1) | zeros |
LogRatio(b) | 0 | — |
ChannelPosition(n, smooth) | n + max(smooth, 1) - 2 | zeros |
Breakout(n, lag) | n + lag - 1 | zeros |
Shift(k) | k | zeros |
DonchianMiddle(n) | n - 1 | the middle of the bars so far |
BollingerX(n, k) | n - 1 | the input, passed through; its average then seeds on the first n inputs, so run it only on trimmed input |
Correlation(b, n) | n - 1 | zeros |
ReturnCorrelation(b, n, k) | n + k - 1 | zeros |
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 (asmoothof 1 or less is no smoothing). Minimum overlap:n + max(smooth, 1) - 1bars. - 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 isL.Sub(L.Lowest(n)).Div(L.Highest(n).Sub(L.Lowest(n))).Mul(100)withL := a.LogRatio(b)(E,ChannelPosition’sHighest/Lowestidentity); swapping the legs gives100less the reading (T(1e-12) forsmoothup to 5; the rounding of the smoothed sums grows withsmooth). - Pinned by
TestRecipeLogRatioIMO; runnable exampleExampleSeries_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) - 2withkthe average’s length (the average’s trim is left out whenk <= 1). - Seed:
PriceRelativeseeds 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, asPriceRelativedoes) differs from it by an amount that halves each bar: on this project’s test pairs it moves only the first dozen valid readings atn = 300by 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-finites2or a non-finites, and because the average never recovers from a NaN, every later reading is NaN. A negatives2flips 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 afterimo’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 whenimo.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 ofrs. - Anchors: with
s2all ones the oscillator equals the same chain ona.XAverage(3).Window(-2)(E; this repeatsPriceRelative’s all-ones anchor and does not testChannelPositionitself); the chain on theOverlapviews 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 (Stochastichas no slowing); exit whenrscrosses belowrs.XAverage(90)orrs.RateOfChange(3)is below -0.5. - Domain: as the IMO of the relative-strength line.
- Anchors: with
lag = 1and a finite channel range, +1 implies thatrs.ChannelPosition(n + 1, 1)is 100 (E, asBreakoutdocuments); the chain on theOverlapviews 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 (withn = 1the trim is left out, the upper and lower lines equalr, and the middle equalsrwhere|r|is at mostMaxFloat64/2:DonchianMiddleadds the extremes before halving, so a larger ratio gives an infinite middle). Minimum overlap:nbars. - Domain: the ratio line’s (signed ratio): NaN at a zero or non-finite
s2, a non-finitesor an overflowing quotient, and every channel whose window contains it NaN; later windows recover. A negatives2flips the ratio’s direction, so the upper channel is then the leg’s weakest point. - Anchors: with
s2all ones, on finites, the three equals.Highest(n),s.Lowest(n)ands.DonchianMiddle(n)from indexn - 1(E);s2 == sgives exactly 1 on non-zeros(E); scalings2by2^kscales them by exactly2^-kwhen no ratio or channel overflows or rounds in the subnormal range (E); on positive ratiosLogcommutes withHighestandLowest(E); a bitwise-flat ratio gives three equal channels (E) where its magnitude is at mostMaxFloat64/2. - Pinned by
TestRecipeRatioDonchian; runnable exampleExampleSeries_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 (withn = 1the trim is left out). Minimum overlap:nbars. - 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))andBollinger(-L, n, -k) == -Bollinger(L, n, k)(E); withs2all ones, on finite, normal, positives,s.Log().Highest(n)ands.Log().Bollinger(n, k)(E);s2 == sgives 0 (E); on prices within 1e±150 the log of the ratio’sHighestagrees withL.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 whyLogRatiotakesln 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:nbars.BollingerXseeds its average with the mean of the firstnratios 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
s2all ones,s.Bollinger(n, k)ands.BollingerX(n, k)(E);k = 0givesr.Average(n)on windows whose standard deviation is finite (E); scalings2by2^jscales the bands by exactly2^-jwhen 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)forBollingerandr.XAverage(n)forBollingerX(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, andStandardise(30)on the trimmeddfrom 29 more.zhas lengthm - 29for an overlap ofmbars; its first 29 elements areStandardise’s warm-up zeros, so readvalid. - 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.Standardiseis 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 readsz*sqrt((n-1)/n)against the same thresholds. - Anchors:
s2 == sgives exactly 0, because the ratio is exactly 1 anddis exactly 0 (tag E);s2all ones on finiteswhose 30-bar standard deviation does not overflow givesd == s.Sub(s.Average(30))from bar 29, becauseRelativeBollinger(ones, 30, 0)equalss.Bollinger(30, 0), which isAverageplus a zero term (tag E; where the standard deviation overflows, both are NaN); the chain on theOverlapresults equals the chain on inputs trimmed to the overlap (the composition property of RELATIVE-INDICATORS-SPEC.md §3.5, E). References A (Python) and B (Julia) compute 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)/rdround 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 exampleExampleSeries_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 - 1for both, counted from the start of the overlap; elementeis overlap barn - 1 + e. Minimum overlap:nbars. - Parameters:
n >= 2and a finitek > 0;k <= 0is 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 withDiv, gives 0 there (plain division gives NaN), and agrees with this one to about2e-16*|mean|/(k*sd)elsewhere, so within1e-12unless the window’s deviation is below about1e-4of its mean. - Bandwidth:
(upper - lower)/averageof the log ratio, which is2k*sd/meanin 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 byDiv); read it on legs of clearly different level, or read the band width2k*sditself. - 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 - 1for both; elementeis overlap barn - 1 + e. Minimum overlap:nbars. - Parameters:
n >= 2; atn = 1the trim isWindow(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
Divrather than the signed percent leg. - Anchors:
s2 == sgives exactly 0 (E); withs2all ones it iss.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 whereChannelPosition(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; elementeis overlap barn - 1 + e. Minimum overlap:nbars. - Parameters:
n >= 2; atn = 1the trim isWindow(0), which returns an empty series. - Domain: both legs must be finite and positive; a bad price gives NaN on the
nwindows 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; elementeis overlap barn - 1 + e. Minimum overlap:nbars. - Parameters:
n >= 2; atn = 1the trim isWindow(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; elementeis overlap barmax(n1, n2) + sm - 2 + e. Minimum overlap:max(n1, n2) + sm - 1bars. - Parameters:
n1, n2, sm >= 1withmax(n1, n2) + sm >= 3; a smaller sum makes the trimWindow(0), which returns an empty series. - Range: within ±100; a flat leg window gives that leg 50, and a leg with
n = 1is 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 atsm = 1on 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 twoHighest/Lowestcompositions (E);s2 == swithn1 == n2gives 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 - 1bars of its own. The channel position sums each window afresh, so where the overlap holdsmax(n1, n2) + sm - 1bars 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.