Relative: Cross-Section
All functions · nseries package
CrossSectionalRank
func (s Series) CrossSectionalRank(ss ...Series) Series
CrossSectionalRank returns the percentile of s among itself and the other series at each right-aligned bar, from 0 for the weakest to 100 for the strongest.
s is the base and ss the other members; the inputs are right-aligned at their most recent bar, only the trailing bars common to all of them are ranked, the result has len(s), and bars before the overlap are zero. The first valid index is len(s) minus the shortest length, and the minimum overlap is 1.
Let K = 1 + len(ss) be the number of members. At each bar of the overlap, with less the number of other members strictly below s and equal the number equal to s (negative zero equals positive zero), the result is 100*(less + 0.5*equal)/(K-1), the percentage of the other members that s is above. Ties count half, so a member equal to s adds one half to the count; this is the normalised mid-rank, (r - 1)/(K - 1) for a mid-rank r. The numerator is exact, so the division is the only rounding. Every result lies from 0 to 100, or is NaN, with 100 meaning that s is above every other member and 0 that it is below every one. Tied leaders score below 100 (two leaders among three members score 75 each), and with an even K and no ties no member scores 50, so select the largest rank rather than testing for 100.
With no other series (K = 1) the result is all zeros and no index is valid. An empty member likewise leaves no overlap, and the result is all zeros.
The NaN rule is pointwise: if s or any other member is NaN or infinite at a bar of the overlap, the result at that bar is NaN, and no other bar is affected. The method is start-invariant, bit for bit: each bar is ranked from the values at that bar alone, so a result does not depend on how far back the inputs start. Only the order of the values matters: applying to every member one strictly increasing map that keeps values finite and distinct values distinct changes nothing, and neither does reordering ss, bit for bit.
Rank each member’s own scale-free momentum measure, never raw price or ratio levels, for example a.RateOfChange(n).Window(-n).CrossSectionalRank(b.RateOfChange(n).Window(-n), c.RateOfChange(n).Window(-n)), where Window(-n) drops the first n bars, which have no n-bar change; the result then starts n bars after a’s first bar. A rate of change or log return of the ratio against one common benchmark, over the same window, ranks members as their own returns do, up to rounding in near ties, so such a benchmark adds nothing to a rank; other relative measures, such as the RSI of the ratio, can rank differently. Rotate into the top rank, or into the middle ranks, Yang and Pinsky’s “mid-momentum” sectors. A rank always selects something, even when every member is falling, so an absolute-trend or correlation filter belongs beside it.
Katsanos’s sector rotation (Stocks & Commodities 43:9) uses such ranks. His weekly system ranks the eleven SPDR sector ETFs by their n-week rate of change, with n = 13 (n = 26 is his alternative), holds the top-ranked ETF for 52 weeks (26 is his alternative; the two choices are independent), optionally makes an ETF ineligible while its own 20-week moving average is falling, and can skip the top N. His daily Nasdaq-100 version ranks the 63-day rate of change, holds for 20 days, optionally trades only while the 25-day moving average of QQQ is rising, and skips the top three. Such eligibility filters, skipping the top N and holding periods are consumer options, not parameters of this method.
Kaeppel, Singer, Wright, Parzen and Vomund publish top-N rank-and-hold rotations, the Winklers a bottom-N one, and Schmidt and D’Errico ranking screens; Masonson’s review of ETFReplay describes an overall rank that fits a weighted sum of per-factor ranks, with mid-rank ties as the 0.5 weight gives, though the review does not state its tie rule. Rank buffers, cash members (the Hucks rank a money-market fund as a member) and carry rankings (Kaufman) are consumer logic.
Verification: no published source gives this formula, so the alternative oracle stands in for a published reference: a Python reference and a Julia reference of the definition agree with it on every row, together with the exact K = 2 identity: the result is exactly 100, 0 or 50 as s is above, below or equal to the other series.
CrossSectionalZScore
func (s Series) CrossSectionalZScore(ss ...Series) Series
CrossSectionalZScore returns how many population standard deviations s lies above or below the mean of itself and the other series at each right-aligned bar.
s is the base and ss the other members; the inputs are right-aligned at their most recent bar, only the trailing bars common to all of them are scored, the result has len(s), and bars before the overlap are zero. Series are paired by position only; see “Time alignment” in the manual.
With K = 1 + len(ss) the number of series, the mean is the sum s + ss[0] + … + ss[K-2] in that order divided by K, as SAverage computes it; the standard deviation is the population one, the square root of the mean squared deviation from that mean; and the result is the deviation of s divided by it, or 0 when it is 0 or when all the series are equal at that bar. Writing z for the result, pandas’ default ddof = 1 gives z*sqrt((K-1)/K) instead.
For K >= 2 the first valid index is len(s) minus the shortest length, and the minimum overlap is 1. K = 1 (no other series) or an empty member give zeros at every bar, even where s is not finite. The NaN rule is pointwise: a NaN or an infinity in any input at a bar of the overlap gives NaN at that bar only. Each bar of the overlap depends only on the inputs at that bar, so the result is start-invariant.
In exact arithmetic |z| <= sqrt(K-1). Rounding can exceed that bound slightly, and by far more when the members nearly coincide, because the rounding error of the mean is then large relative to their spread: no ulp bound holds for any K. For K = 2, s = 100 against 100*(1 + 3e-13) gives -1.0004735968892793, and two members one ulp apart can score 0 and about sqrt(2); for K = 3 a member at the minimum can score above 0. The last bits of the mean depend on the order of the arguments, so the members’ z-scores, each computed with that member as s, sum to 0 only approximately, and not at all near coincidence.
Scaling every input by the same power of two leaves the result unchanged unless the squares overflow or underflow. When the sum of the squared deviations exceeds math.MaxFloat64, which takes a deviation of at least about 1.3e154/sqrt(K), the result is 0, or NaN when the sum of the inputs or the deviation of s itself overflows, unless all the series are equal. Deviations all below about 1.6e-162 give 0, and below about 1.5e-154 the squares are subnormal and the result is inaccurate.
Uses: Parzen, testing Martin and McCann’s relative strength oscillator (RSO), z-scores the RSO values of a 15-ETF universe cross-sectionally each week and holds the top three by z-scored RSO, excluding ETFs with fewer than 104 weeks of history from the ranking (Stocks & Commodities 44:7); he does not say which standard deviation he uses. Cline’s second method weights each leg of a basket by its RSI’s deviation from the group’s mean RSI (Stocks & Commodities 33:5); this method divides that deviation by the group’s standard deviation, which rescales each bar’s weights without changing their proportions. Unlike CrossSectionalRank, the z-score keeps the size of a lead; within a bar it orders the members as their values do, except in near ties, so a top-N pick by it, as in Parzen, matches one by the raw values or by CrossSectionalRank. A per-leg measure, such as each member’s own momentum, makes the most recent bar comparable across members with different calendars; earlier bars are still paired by position.
Warm-up hazard: before the overlap the output is 0, which reads as a member at the mean; trim it with Window(m), m the shortest length, or mask it with SetN(len(s)-m, math.NaN()). Inside the overlap 0 also marks equal values, s exactly at the mean, and squares that overflow or underflow. A member that starts later shortens the overlap for every member, so a universe that grows needs one call per set of members.
Verification: two references in exact rational arithmetic, one in Python and one in Julia, agree with it within a per-row tolerance; the mean it uses equals SAverage bit for bit, and on small integers two series give exactly -1 and 1.
Compositions
Hayes’s weekly score
Hayes, of Ned Davis Research, ranks currencies by the average of their 4-, 12- and 52-week rates of change on Friday closes (Stocks & Commodities 10:5), from the highest score down. His rate of change is the change over the earlier close; RateOfChange’s percent is 100 times that, which changes no rank. His text and the opening of his figure 5 caption give the long leg as 56 weeks, while the caption’s step-by-step procedure uses 52; the recipe uses 52.
score := x.RateOfChange(4).Add(x.RateOfChange(12)).Add(x.RateOfChange(52)).Div(3).Window(-52)- First valid index:
52, counted from the start of the series; elementeis bar52 + e. Minimum history: 53 weekly bars. - Domain: positive prices.
RateOfChangegives 0, not NaN, at a zero base. - Published ranks: Hayes prints a ranking for 17 January 1992, from the yen first to the Australian dollar tenth; its closes are not transcribed, so the recipe is not checked against it.
- Pinned by
TestRecipeT4ATASCScores.
Hayes’s daily score
Hayes’s short-term score on daily bars: twice the 10-day rate of change plus the 25-day one, divided by three (Stocks & Commodities 10:5).
score := x.RateOfChange(10).Mul(2).Add(x.RateOfChange(25)).Div(3).Window(-25)- First valid index:
25, counted from the start of the series; elementeis bar25 + e. Minimum history: 26 daily bars. - Domain: as the weekly score’s.
- Pinned by
TestRecipeT4ATASCScores.
Cafritz’s and Rugg’s momentum sums
Two monthly mutual-fund scores: Cafritz’s Composite Performance Measure, the sum of the 3-, 6- and 9-month percentage changes in net asset value, as tested by Dworkin (Stocks & Commodities 5:7), and Donald Rugg’s sum of the 1-, 3-, 6- and 9-month changes, which Zin ranks by (7:12). Both are sums rather than averages, which changes no rank.
roc3, roc6, roc9 := x.RateOfChange(3), x.RateOfChange(6), x.RateOfChange(9)
dworkin := roc3.Add(roc6).Add(roc9).Window(-9)
zin := x.RateOfChange(1).Add(roc3).Add(roc6).Add(roc9).Window(-9)- First valid index:
9for both, counted from the start of the series; elementeof each is bar9 + e. Minimum history: 10 monthly bars. - Inputs: monthly net asset values adjusted for distributions (Zin adjusts for capital-gain distributions but not for income dividends).
- Domain: positive values.
RateOfChangegives 0, not NaN, at a zero base. - Pinned by
TestRecipeT4ATASCScores.
Quarter weighting (Vomund)
Vomund’s ranking splits the recent past into four consecutive quarters and averages their changes with the latest counted twice, (2*q1 + q2 + q3 + q4)/5 with q1 the latest: in Stocks & Commodities 21:10 the quarters of the past year, and in 23:1 AIQ’s short-term relative strength report, which breaks the last 120 days into quarters. The quarters are disjoint sub-periods, not nested horizons.
q := x.RateOfChange(h)
score := q.Mul(2).Add(q.Shift(h)).Add(q.Shift(2 * h)).Add(q.Shift(3 * h)).Div(5).Window(-4 * h)- First valid index:
4h, counted from the start of the series; elementeis bar4h + e. Minimum history:4h + 1bars. - Parameters:
h >= 1: 13 for a year of weekly bars, 30 for the 120-day window on daily bars. - Caution: an average of percentage changes is biased upward: a fall of 50% followed by a rise of 100% averages +25%, yet the price is back where it started. Vomund’s example (21:10) is a stock that halved and then doubled: “Averaging the percentages shows a positive number, but the stock is breakeven”, and he finds such low-priced rebounders dominating the relative-strength rankings after the bubble. The log-change score below has no such bias.
- Domain: positive prices.
RateOfChangegives 0, not NaN, at a zero base. - Anchors: on a series flat for its first
3h + 1bars, the first element isRateOfChange(h)at bar4htimes 2, divided by 5 (E). - Pinned by
TestRecipeT4ATASCScores.
Log-change score
The log change over h bars, ln(x_j) - ln(x_{j-h}). It is the sum of the log changes of any sub-periods that make up the h bars and equals the log of the total change, so it has no upward bias: a fall of 50% and a rise of 100% are log changes of -ln 2 and +ln 2, which sum to 0. On one horizon it is ln(1 + R/100) for the percent change R, an increasing function, so it ranks members as RateOfChange(h) does, in exact arithmetic.
score := x.Log().Momentum(h).Window(-h)- First valid index:
h, counted from the start of the series; elementeis barh + e. Minimum history:h + 1bars. - Units: log points;
Mul(100)puts them on a percent-like scale. - Domain: positive prices:
Loggives -Inf at a zero price and NaN at a negative one. - Pinned by
TestRecipeT4ATASCScores.
Ranking a universe by a score
Each member (here a, b and c) is scored on its own history, then compared with the others at each bar: CrossSectionalRank gives the receiver’s percentile among the members, 100 for the strongest, and CrossSectionalZScore its distance from their mean in standard deviations, which keeps the size of the lead. Both right-align the members at their most recent bar, so members of different lengths are scored over their common trailing bars. Any score above, or a weighted sum of rates of change, takes the place of hayesWeekly; add the other members as further arguments.
ha, hb, hc := hayesWeekly(a), hayesWeekly(b), hayesWeekly(c)
rank := ha.CrossSectionalRank(hb, hc)
z := ha.CrossSectionalZScore(hb, hc)- First valid index:
52, counted from the start of the members’ common overlap: on members of equal length elementeofrankand ofzis bar52 + e; with unequal lengths both havelen(ha)elements, and those before the scores’ common trailing bars are 0. Minimum overlap: 53 bars. - Aligned zeros: a 0 before the common bars, or at every bar when a member has 52 bars or fewer (its score is empty, which leaves no overlap), is not a rank but reads as the weakest one, and as the mean in
z. Check every member’s length, or read only the lastmin(len(ha), len(hb), len(hc))elements. Warm-up zeros of a score that is not trimmed rank as real values in the same way. - Rank buffers: holding rules are consumer logic over the rank, and the published ones damp churn. Vomund buys the top two and sells a holding only when it is no longer in the top three, buying the highest-rated in its place, with the report run again two weeks later (23:1). Cafritz’s rules, as Dworkin tests them, buy the funds ranked 1 to 3, hold them while they stay in the top 25 of 85, sell a fund when its rank drops below 25 and buy any fund that rises to rank 1, 2 or 3 (5:7). Zin buys funds that have been in the top 20% for three consecutive months and sells a fund when it falls below the top 20% (7:12).
- Ranks of several factors: the ETFReplay screen reviewed by Masonson (Stocks & Commodities 29:13) weights two return horizons and one volatility horizon, each with a user-set period and weight (50% six-month return, 40% three-month return and 10% 20-day volatility in one of his examples), and the overall ranks in his figure 5 agree with a weighted sum of per-factor ranks with low volatility ranking best. That is
CrossSectionalRankof each factor, with the volatilities negated, combined withMulandAdd: each percentile is the same decreasing linear function of the member’s mid-rank, so the weighted sum of percentiles orders the members as the weighted sum of ranks does. - Benchmark columns: Pendergast’s RSI screen (Stocks & Commodities 32:4) lists each stock’s 4-, 13-, 26- and 52-week and year-to-date relative strength against the S&P 500. Against a common benchmark, a relative strength that increases with the member’s own change (a return difference or a ratio) ranks the members as their own changes do, so the fixed columns are the members’
RateOfChange(4),RateOfChange(13)and so on on weekly bars, ranked; the year-to-date column needs timestamps. - Pinned by
TestRecipeT4ATASCRanking; runnable exampleExampleSeries_CrossSectionalRank_hayes.