Relative: Ratio and Relative Strength

Relative: Ratio and Relative Strength

All functions · nseries package

LogRatio

func (s Series) LogRatio(s2 Series) Series

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

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

s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.

The first valid index, counted from the start of the overlap, is 0 (result index len(s) - min(len(s), len(s2))), and the minimum overlap is 1: there is no warm-up, and every paired bar has a value.

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

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

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

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

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

PriceRelative

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

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

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

s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.

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

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

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

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

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

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

RSMK

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

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

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

s is the base and s2 the comparison; the inputs are right-aligned at their most recent bar, only the trailing min(len(s), len(s2)) bars are paired, the result has len(s), and bars before the overlap are zero.

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

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

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

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

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

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

Compositions

Percentage premium

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

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

Relative performance and relative return

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

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

Relative momentum

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

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

Katsanos’s relative rate of change

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

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

AccuTrack

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

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

Pring’s Money Flow Indicator

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

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

Excess over a universe

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

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

Two-asset switching and yield comparisons

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

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

Mansfield relative strength

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

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

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

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

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

Z-score of the log ratio

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

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

Z-score of the raw ratio

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

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

Relative Strength Oscillator (Martin and McCann)

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

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

EMA regime of a relationship line

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

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

SMA regime of a relationship line

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

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

MACD of the log ratio

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

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

Ratio MACD and its signal line (Raff)

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

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

PPO of the ratio

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

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

Per-leg PPO difference

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

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

Price Momentum Oscillator (DecisionPoint)

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

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

PMO spread (Apirine’s CPMO)

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

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