Breadth

All functions · nseries package

Breadth inputs

Breadth indicators are built from counts and volumes of a market’s issues, which the caller supplies as ordinary Series, one value per bar: advances a, declines d, unchanged u (so total issues ti is a + d + u), up volume uv, down volume dv, new highs nh and new lows nl. nseries holds no breadth data and has no timestamps, so the series are paired by position only, right-aligned at the most recent bar, as every two-series method pairs them (see “Time alignment” in the manual): line them up in time before they reach nseries. Weekly breadth must be supplied as weekly data, not summed from daily counts.

The cumulative lines are CumSum of a daily net figure:

adl := a.Sub(d).CumSum()                 // advance-decline line
ratio := a.Sub(d).Div(a.Add(d)).CumSum() // ratio-adjusted: Div gives 0 where a + d is 0
tiAdj := a.Sub(d).Div(ti).CumSum()       // adjusted by total issues
udv := uv.Sub(dv).CumSum()               // up/down volume line
hl := nh.Sub(nl).CumSum()                // new high - new low line

A cumulative line depends on where its history starts: starting later subtracts a constant from every bar. Read it through forms in which that constant cancels. adl.Sub(adl.Shift(k)) equals a.Sub(d).Sum(k) for bars t >= k (before bar k both are start-dependent, so mask those bars), and on integer counts whose partial sums stay below 2^53 the two are equal bit for bit, as is adl itself to a plain running sum. The oscillator adl.Sub(adl.Average(n)) depends only on the last n-1 net advances, to rounding, and so do crossovers of two simple averages (Average) of adl and a stochastic of adl; an exponential average remembers the start. Morris’s 21-day advance-decline oscillator is the difference adl.Sub(adl.Shift(21)), which is start-invariant. The percentage distance of adl from its average and its percentage rate of change (RateOfChange) divide by the level, which depends on the start, so they are not start-invariant.

CrossSectionalEntropy

func (s Series) CrossSectionalEntropy(ss ...Series) Series

CrossSectionalEntropy returns, at each bar, the Shannon entropy in bits of the non-negative weights of the receiver and each member of ss, normalised to shares. With K = 1 + len(ss) weights w_k, their sum W and the shares q_k = w_k / W, it is H = -sum_k q_k log2 q_k (multiply by ln 2 for nats), which lies in the range [0, log2 K] up to rounding: it is 0 when all the weight is in one member and log2 K when the weights are equal (exactly so when K is a power of two and the weights’ sum is exact). The receiver and each member of ss supply one weight per bar. Like CrossSectionalRank, it works across the members at each bar, not along one series’ history.

The inputs are right-aligned at their most recent bar, as for CrossSectionalRank. The warm-up reads 0, not NaN: the result has len(s), bars before the overlap are zero and the first valid index is len(s) minus the shortest length. A bar is NaN if any weight is NaN, infinite or negative (-0 counts as zero), or if every weight is zero; a single positive weight gives 0. If the weights’ sum overflows, every weight is first multiplied by 2^-64, which leaves the shares unchanged (a weight that becomes subnormal or zero by the scaling has a share that underflows to zero either way). Zero shares contribute nothing. Each bar depends only on the inputs’ values at that bar, so the result is start-invariant, bit for bit. No input is modified.

Source: C. E. Shannon, “A Mathematical Theory of Communication”, Bell System Technical Journal 27 (1948).

CumSum

func (s Series) CumSum() Series

CumSum returns the cumulative sum of s, from bar 0 with no warm-up.

out[i] = s[0] + s[1] + ... + s[i]

It is exactly s.Sum(len(s)), so the running total is compensated, and an empty or nil s gives an empty result. A NaN at bar p makes every value from p on NaN; an infinity makes the total that infinity from its bar on, and infinities of both signs make it NaN.

It answers to ta.cum in TradingView and to cumsum in numpy and pandas (pandas skips NaN by default; CumSum does not). TA-Lib’s CUMSUM is a plain running sum from its start index, which agrees with CumSum to rounding when it starts at bar 0. Its main use is cumulative breadth and volume lines; see the Breadth section of the manual.

The total depends on where the history starts, but two forms built on it do not. With cum := s.CumSum(), cum.Sub(cum.Shift(k)) equals s.Sum(k) for t >= k (before bar k both are start-dependent, so mask t < k), and from bar n-1 cum.Sub(cum.Average(n)) depends, to rounding, only on the last n-1 increments. When every partial sum is an integer below 2^53 in magnitude, as with breadth counts, the total equals a plain running sum and the first form equals s.Sum(k), both bit for bit; otherwise they agree to within rounding of the total, which can swamp a small window sum.

To restart the total at events, use SumSince; with its only event at bar 0 it equals CumSum bit for bit.

SectorEntropy

func (s Series) SectorEntropy(sizes []Series, ss ...Series) Series

SectorEntropy returns, at each bar, Yuqi Fan’s ratio entropy of large movers across sectors, in bits. With m_k the number of movers in sector k, N_k the number of companies in it and r_k = m_k / N_k, it is H = -sum_k r_k log2 r_k (multiply by ln 2 for nats). The receiver holds the movers of sector 0 and ss[k-1] those of sector k; sizes[k] holds the size of sector k at each bar, so sizes[0] goes with s and sizes[k] with ss[k-1]. The ratios are not normalised to sum to 1 (Fan’s construction), so with K sectors the result lies in [0, K / (e ln 2)], about 0.531 K, rather than in [0, log2 K]; it is 0 where every ratio is 0 or 1.

The caller counts the movers and supplies each sector’s size at every bar, since constituents and classifications change (for example the GICS changes of 2016 and 2018). Fan counts the companies whose monthly price change is greater than 5% (strictly); how the change is measured, and whether she meant signed or absolute moves, is not printed, though her reading of low entropy, “fewer rising companies”, implies upward moves. CrossSectionalEntropy of the counts is the normalised (share) alternative, whose standardised values differ when the sector sizes differ. SectorEntropyZ standardises this entropy.

The inputs are right-aligned at their most recent bar, as for CrossSectionalRank. The warm-up reads 0, not NaN: the result has len(s), bars before the overlap are zero and the first valid index is len(s) minus the shortest length. If sizes does not have 1 + len(ss) entries, the result is len(s) zeros. A bar is NaN if any count or size is NaN, infinite, negative or not an integer (-0 is the integer zero), or if a count exceeds its sector’s size. Sectors of size 0 are dropped and contribute nothing; if every sector is dropped, the bar is NaN. Each bar depends only on the inputs’ values at that bar, so the result is start-invariant, bit for bit. No input is modified.

Sources: Y. Fan, “Predicting S&P 500 Tops And Bottoms With Shannon Entropy”, Technical Analysis of Stocks & Commodities 42:13 (December 2024); C. E. Shannon, “A Mathematical Theory of Communication”, Bell System Technical Journal 27 (1948).

SectorEntropyZ

func (s Series) SectorEntropyZ(sizes []Series, ss ...Series) Series

SectorEntropyZ returns, at each bar, SectorEntropy standardised against its exact mean and variance under the permutation reading of Fan’s null, Z = (H - E[H]) / sqrt(Var[H]), where H is the bar’s SectorEntropy value in bits (Z does not depend on the base). The receiver, ss and sizes are as for SectorEntropy: the sectors’ counts of movers and their sizes. Fan says only that the movers are randomly reassigned to sectors with their total kept fixed; here the M movers (the sum of the counts) are a uniformly random M-subset of the N companies (the sum of the sizes), which respects the sector sizes, so no ratio exceeds 1 and the counts are jointly multivariate hypergeometric. The exact moments replace Fan’s 100 000-draw Monte Carlo: they are deterministic, and agree with a Monte Carlo of this null to within its sampling error. E[H] sums the sectors’ mean terms and Var[H] adds their variances and twice the covariance of each pair; the hypergeometric pmfs are evaluated by their ratio recurrence.

Fan’s reading: upward crossings of +5, +6 and +7 were followed by above-average S&P 500 returns; downward crossings of -5, -6 and -7 by below-average returns, and by large declines when the index was below its 12-month simple moving average. The evidence is in-sample, on one market with few events (January 2000 to June 2023, with 3 to 34 events per rule).

The inputs are right-aligned at their most recent bar, as for CrossSectionalRank. The warm-up reads 0, not NaN: the result has len(s), bars before the overlap are zero and the first valid index is len(s) minus the shortest length. The sizes rule (aligned zeros), the per-bar NaN rules and the dropping of empty sectors are those of SectorEntropy. A bar is also NaN if any size, or the total N, exceeds 2^31 - 1; if the supports of the sectors’ null counts hold more than 16384 values in total; if M = 0 or M = N, where every assignment gives the same entropy; or if the computed variance is below 1e-12 or not finite (for example M = 1 with every sector the same size). The threshold is absolute, so for very large universes with few movers, where every ratio and so the variance is tiny, a bar can be NaN although the variance is positive. Each bar depends only on the inputs’ values at that bar, so the result is start-invariant, bit for bit. No input is modified.

The result agrees with Z formed from exactly computed moments to within an absolute 1e-9 max(1, |Z|) wherever the variance is at least 1e-10 and is not formed by severe cancellation. The cost per bar is at most about S^2/2 inner-loop steps (each a few multiplications, a division and two compensated additions), with S the total size of the supports.

Sources: Y. Fan, “Predicting S&P 500 Tops And Bottoms With Shannon Entropy”, Technical Analysis of Stocks & Commodities 42:13 (December 2024); C. E. Shannon, “A Mathematical Theory of Communication”, Bell System Technical Journal 27 (1948).