Volume and breadth
All functions · nseries package
Sections: Accumulation / Distribution · On-Balance Volume · Money Flow Index · Chaikin Oscillator · Klinger Volume Oscillator · Volume Flow · Volume Profile · Breadth
Accumulation / Distribution
AccumDist
func (s Series) AccumDist(h, l, c, v Series) Series
AccumDist implements Chaikin’s Accumulation/Distribution (A/D) line. Developed by Marc Chaikin in the 1970s, it measures the flow of money into and out of a security. This volume-based indicator helps to identify the underlying buying or selling pressure that may not be immediately apparent from price action alone.
The A/D line examines where the closing price falls within a trading range. When prices close in the upper portion of the range, this indicates buying pressure. When prices close in the lower portion of the range, this indicates selling pressure. When prices close around the midpoint, this indicates neutral pressure.
The indicator becomes more significant when accompanied by high volume. A strong close near the high with high volume results in a substantial A/D line increase, while low volume or mid-range closes produce smaller movements.
A rising A/D line confirms an uptrend and a falling A/D line confirms a downtrend.
When the A/D line moves in the opposite direction of price, it can signal potential reversals. A bullish divergence occurs when prices makes a new low and the A/D line shows strength (potential capitulation). A bearish divergence occurs when price make a new high but the A/D line fails to follow (potential exhaustion). - Bearish divergence: Price makes new highs while A/D line fails to follow - Bullish divergence: Price makes new lows while A/D line shows strength
Note that this is a lagging indicator, and it may generate false signals during periods of consolidation.
Note that this differs from the open-to-close form (C-O)/(H-L) * V that some platforms give under the same name, Williams’s accumulation/distribution with volume; both forms are in the “Accumulation / Distribution” category’s compositions.
The cumulative line starts at 0 on bar 0, so bar 0’s own money flow is not added. The running total is a plain sum whose update may fuse into a multiply-add on arm64, so ad.Delta(n) agrees with a window sum of the flows only to rounding and can differ between architectures; the Chaikin money flow composition under “Compositions” in the “Accumulation / Distribution” category sums the flows directly.
Compositions
Three accumulation/distribution definitions share a name and differ: Chaikin’s close location, the bar’s volume weighted by where the close falls in its range (AccumDist, and the money flow below), Williams’s true-range move (the price-only Williams A/D line) and Williams’s open-to-close move with volume (WAD). Recipe values are NaN before their first valid index (SetN(F, math.NaN())), and flags built on masked values read 0 there. The cumulative lines are valid from bar 0, where each starts at 0 or, for WAD with volume, at the bar’s own flow (AccumDist skips bar 0’s flow), and their level depends on where the history starts, so rules should read window sums, slopes, differences or ratios to their own averages. CumSum is a compensated running sum: it equals the vendors’ plain running sums to rounding, and bit for bit when every partial sum is an integer below 2^53. Prices are positive and finite and volumes finite and non-negative; Div reads 0 where the source’s ratio is undefined (a flat bar, a window without volume), as each section says. Lengths are in bars, and series are paired by position, so the caller aligns them by timestamp first.
Chaikin money flow and persistency of money flow (Chaikin; Brogan)
Thom Hartle’s interview (“Chatting With Marc Chaikin”, Stocks & Commodities V12:1, January 1994, pp. 30-37, with the sidebar “Chaikin Indicators”) gives the money flow as “the 21-day sum of the raw daily accumulation/distribution indicator divided [by] the 21-day sum of volume”, the daily value being the sidebar’s ((Close - Low) - (High - Close))/(High - Low) times the volume, and Bob Brogan’s persistency of money flow as “the percentage of days in a six-month window that the money flow indicator was positive with no regard to magnitude”, read over “the last 120 trading days”. Christopher Narcouzi (“Chaikin’s Money Flow”, Stocks & Commodities V18:8, August 2000, pp. 42-46) adds that “Marc Chaikin recommends using a 21-day period” and that “Readings above 0.10 are considered bullish”. t6ChaikinMoneyFlow sums the flows directly in the sidebar’s spelling, rather than differencing AccumDist, and counts the positive days with Sum, dividing once.
clv := c.Sub(l).Sub(h.Sub(c)).Div(h.Sub(l))
cmf := clv.Mul(v).Sum(n).Div(v.Sum(n)).SetN(n-1, math.NaN())
pmf := cmf.GT(0).Sum(m).Mul(100).Div(m).SetN(n+m-2, math.NaN())- Inputs: highs, lows, closes and volumes (any volume unit: the ratio is scale-free); n = 21 (Chaikin, Narcouzi; TA-Lib and pandas-ta default to 20) and m = 120.
cmfis a fraction in [-1, 1], Narcouzi’s scale; Chaikin’s sheet prints it times 100. A flat bar (H == L) has a close location of 0 with its volume still counted in the divisor, and a window without volume reads 0, TA-Lib’s rule (“A window whose volume is entirely zero has no money flow to distribute and reports 0.0”). - First valid index: n - 1 for
cmf(20 at n = 21, the bar of the sheet’s first value, on its 21st row) and n + m - 2 forpmf(139 at the defaults), NaN before. Unmasked, bars 0 to n - 2 hold ratios of partial sums (bar 0 is that bar’s own close location, 1 on a close at the high), and before bar n + m - 2 the count takes the masked bars as days that were not positive. - Anchors: on Chaikin’s sidebar sheet (Paramount, 22 rows of high, low, close and volume, 2 November to 2 December 1992)
cmftimes 100 on the 21st and 22nd rows rounds to the printed 37 and 24 (0.36528512395057033 and 0.23772734466815509 at n = 21); his cumulative column isc.AccumDist(h, l, c, v)plus bar 0’s own flow (850500) on all 22 rows, to its printed whole numbers, and his oscillator column isad.Average(3).Sub(ad.Average(10))of that line on its 13 printed rows (599133 to 169417): simple averages, whereChaikinOsctakes the vendors’ EMAs. The close location(c-l)-(h-c)equals2c-h-l, writtenc.Mul(2).Sub(h).Sub(l), bit for bit on decimal prices (Downing’s 28 bars), because each difference of nearby prices is exact.pmfis a correctly rounded percentage, the count times 100 over m rounded once: with n = 10 and m = 5 on Downing’s bars it is NaN on bars 0 to 12, 100 on 13 to 20, then 80, 80, 60, 40, 20, 20 and 0. A NaN volume makescmfNaN on the n bars whose window holds it, and the bars after them recover. - Traps: the
AccumDistDelta form,c.AccumDist(h, l, c, v).Delta(n).Div(v.Sum(n)), reads 0 at bar n - 1, so it cannot produce the sheet’s first value (37); from bar n it is within 1e-15 of the direct form but differs from it in bits on some bars of a 700-bar path, and sinceAccumDist’s running total may fuse into a multiply-add on arm64 and not on amd64 it also differs between architectures (on bar 20 of the StockCharts workbook it reads -0.09994728118297644 on arm64 and -0.09994728118297642 on amd64), where the direct form gives the same bits on both; after a NaN volume it stays NaN for good, because the running total keeps the NaN, except on a window without volume, whereDivreads 0 even over the NaN. pandas-ta’s2c-(h+l),c.Mul(2).Sub(h.Add(l)), differs in bits on some bars, since the sum h + l rounds. The testcmf > 0is strict, so a money flow of exactly 0 (a flat or volume-free window) is not a positive day, andcmf.GT(0).Average(m).Mul(100)rounds twice and passes the 0/1 flags through before bar m- A NaN volume inside the history makes cmf NaN on the n bars whose window holds it, and
GT(0)counts those as days that are not positive, so pmf reads low for the next m bars. Chaikin’s money flow is not the Money Flow IndexMFI(Quong and Soudack’s typical-price index), although Katsanos calls it “Marc Chaikin’s money flow index (CMF)” (“Detecting Breakouts”, Stocks & Commodities V21:4, 2003, pp. 26-38), and it is notChaikinOsc.
- A NaN volume inside the history makes cmf NaN on the n bars whose window holds it, and
- Variant: Jim Horn’s dollar-volume money flow (“Does Volume Still Confirm Price?”, Stocks & Commodities V44:10, October 2026, pp. 20-23; n = 20, “price multiplied by share volume”), with the close as the price,
clv.Mul(c.Mul(v)).Sum(n).Div(c.Mul(v).Sum(n)).SetN(n-1, math.NaN()); he prints no formula and no rule, and his own spreads (under Evidence) are against readingGT(0)of it as confirmation. - Variant: Massoud Metghalchi and Peggy Cloninger’s “RSI>40 and the 21-day Chaikin money flow (CMF) indicator is increasing” (“Trading SLV In The Age Of Zero Commission, Part 2”, Stocks & Commodities V41:6, May 2023, pp. 20-26),
c.RSI(14).GT(40).Mul(cmf.Delta(1).GT(0)), a state held “as long as these two conditions hold”, first possible 1 at bar n; “increasing” is read as a one-bar rise, which they do not define. - Evidence: Markos Katsanos (“Comparing Seven Money Flow Indicators”, Stocks & Commodities V29:8, July 2011, pp. 15-26) ran a divergence system on 30 stocks from 31 October 2000 to 31 October 2010 (10,000 dollars a trade, a 9-dollar commission; one period with no hold-out sample, and the CMF threshold not printed): CMF compounded 9.5% a year over 396 trades, 56.6% profitable, profit factor 1.58, against 6.4% for buy and hold. Metghalchi and Cloninger’s rule returned 13.12% a year on SLV from February 2007 to October 2022 in their first strategy, against 1.34% for buy and hold, without costs; Horn’s 60-day bullish-minus-bearish spreads for a 20-bar CMF on dollar volume, 2005 to 2025, are negative on all four funds (from -1.32% on QQQ to -2.01% on IWM). The StockCharts workbook (
cs-cmf.xls, 30 rows) reproduces its multiplier and flow bit for bit and its CMF within 9 ulp of the exact value; it is kept as an archive probe. Evidence, not a reference. - Pinned by
TestRecipeT6ChaikinMoneyFlow; runnable exampleExampleSeries_Sum_chaikinMoneyFlow.
Williams accumulation/distribution, price-only and with volume (Williams; Barics)
Larry Williams’s operating rule (Long-Term Secrets to Short-Term Trading, 2nd ed., Wiley, 2012, ch. 2, p. 51) is that “sellers in any time period are represented by the price swing from the high of the day to the close, while buying is represented by the close minus the low”; the price-only line that Tulip Indicators (wad) and TA-Lib (TA_WAD, new in v0.8.1: “It consumes no volume.”) ship adds the close less the true low on an up close, the close less the true high on a down close and 0 on an unchanged close. Joseph Barics (“WAD trades ABX”, Stocks & Commodities V8:2, February 1990, p. 80) calls the form with volume “Williams’ on-balance volume”: “W = [(Close - Yesterday’s Close) / (High - low)] Volume”, added “to a cumulative total”, with the note “when available, open replaces yesterday’s close”. t6WilliamsAD takes the true low and high from TrueLow and TrueHigh, and the volume form with the open, bar 0 included.
d := c.Delta(1)
acc := c.Sub(l.TrueLow(c)).Mul(d.GT(0))
dis := c.Sub(h.TrueHigh(c)).Mul(d.LT(0))
wad := acc.Add(dis).CumSum()
wadV := c.Sub(o).Div(h.Sub(l)).Mul(v).CumSum()- Inputs: opens, highs, lows, closes and volumes;
wadreads no open or volume and is in price units (on a high-low-close table pass the closes for both,t6WilliamsAD(c, h, l, c, c)), andwadVis in the volume’s units. A flat bar (H == L) adds 0 towadV(Div’s zero rule; Barics is silent). - First valid index: 0 for both, with nothing masked.
wadis 0 on bar 0, TA-Lib’s first output, becauseDelta’s +0 there fails both flags (Tulip starts its output at bar 1, with the same numbers);wadVholds bar 0’s own flow, as a plain sum of (C - O)/(H - L) times V over every bar does, whereAccumDiststarts from 0 and skips bar 0’s flow. - Anchors: on Tulip Indicators’
wadexample (15 rows of high, low and close, 1 to 21 November 2005)wadrounds to the 14 printed values at 2 decimals (-0.83, 0.98, 1.33, 1.94, 1.19, 0.70, 2.39, 2.95, 2.31, 3.81, 4.96, 6.09, 6.97, 6.39) and equals a plain loop of Tulip’s increment on that table bit for bit (-0.8299999999999983 on bar 1, 6.390000000000001 on bar 14). On a 700-bar path the incrementacc.Add(dis)equals the Tulip and TA-Lib per-bar increment bit for bit: the close less the lower of the low and the prior close on an up close, less the higher of the high and the prior close on a down close, 0 on an unchanged close (the flags are exactly 0 or 1, and the unused leg times 0 is a signed zero). TheMinimum/Maximumspelling,l.Minimum(c.Shift(1))andh.Maximum(c.Shift(1))with the increment masked bySetN(1, 0), equals theTrueLow/TrueHighform bit for bit. - Traps: a spelling on
pc := c.Shift(1), with the true lowpc.Sub(pc.Sub(l).Max(0))and the true highpc.Add(h.Sub(pc).Max(0)), adds c[0] on bar 0, becauseShiftpads 0 and the close is above that pad, so every value is c[0] too high (81.59 on Tulip’s example). A NaN high on an up bar poisonswadfrom that bar on, although an up close does not use the high, because the unused leg is multiplied by 0 and 0 times NaN is NaN (Tulip’s loop does not read it); mask the inputs first.AccumDistis neither line: it weights the volume by the close’s place in the range, not by the move from the open or the prior close. - Variant: Barics’s normalised line (“WAD trades ABX”, p. 80: “WADnl = Lmin + [ (Lmax - Lmin) / (WADmax - WADmin) ] (WAD - WADmin)”, Lmin and Lmax “the extreme lows in a trading range”; his “%B complements on-balance volume”, Stocks & Commodities V8:5, May 1990, pp. 195-196, normalises %B the same way, with Lmin the “Lowest of lows” and Lmax the “Highest of lows”), in a causal trailing form over n bars (60 spans his window):
lo.Add(hi.Sub(lo).Div(wmax.Sub(wmin)).Mul(wadV.Sub(wmin))).SetN(n-1, math.NaN())withlo := l.Lowest(n),hi := l.Highest(n),wmax := wadV.Highest(n)andwmin := wadV.Lowest(n). His own window (26 July to 18 October 1989) runs 23 bars past his entry, so its extremes, and his touch of Lmin, come from bars after the signal; on a trailing window a rising WADnl is not always a rising WAD (the window’s extremes move), so readwadV.Delta(1).GT(0). - Variant: the no-open form of Tom Johnson and Mark Boucher (“Developing and using pattern recognition”, Stocks & Commodities V7:10, October 1989, pp. 362-364), “[(close-yesterday’s close)/(high - low)] volume” added “to a cumulative total”,
c.Delta(1).Div(h.Sub(l)).Mul(v).CumSum(), 0 on bar 0; across a gap the flow per share (C - C[t-1])/(H - L) is unbounded, and a locked-limit bar (H == L with the close moved) adds 0, while Barics uses the open when he has it. - Evidence: Barics’s one ABX trade (October 22-1/2 calls bought at 11/16 on 15 September 1989, the 100% to 200% objective reached by 25 September), read on a window chosen with hindsight; Johnson and Boucher’s pattern “occurred 238 times. Of 238 occurrences, 72% were profitable”, in multiples of initial risk with costs not stated, with WAD one of three volume indicators in the pattern and its own contribution not separated; no source read prints WAD values with their inputs, and Tulip’s table checks only the arithmetic of the price-only line. Evidence, not a reference.
- Pinned by
TestRecipeT6WilliamsAD; runnable exampleExampleSeries_TrueHigh_williamsAD.
On-Balance Volume
OBV
func (s Series) OBV(c, v Series) Series
OBV implements Joe Granville’s On-Balance Volume indicator.
The cumulative line starts at 0 on bar 0, as is conventional.
The cumulative line depends on the bar the history starts on, so a rule reads its slope, its own average or a window sum: obv.Delta(n) is c.UpDownSum(1).Mul(v).Sum(n) from bar n, bit for bit on finite integer volumes while every partial sum stays below 2^53. The volume-price trend, the dead-band OBV and the negative and positive volume indexes are listed under “Compositions” in the “On-Balance Volume” category.
Compositions
Three recipes on the scaffold of OBV, a running total to which each bar adds a signed share of its volume: Markstein’s volume-price trend adds the share proportional to the close’s change, Kaufman’s dead-band line the whole volume of a move outside a band, and Katsanos trades a money-flow line’s divergence from price. Recipe values are NaN before their first valid index (SetN(F, math.NaN())), and flags, states and latches built on masked values read 0 there. The cumulative lines start from 0 on bar 0 and their level depends on where the history starts, so rules read window sums, slopes, differences or ratios to their own averages rather than the level. Prices are positive and finite and volumes finite and non-negative; Div reads 0 where a ratio is undefined (the volume-price trend’s bar 0 and a zero prior close), and Katsanos’s stochastic reads 100 on a zero range, as his code does. CumSum is a compensated running sum: it equals the vendors’ plain running sums to rounding, and bit for bit when every partial sum is an integer below 2^53. Lengths are in bars, and series are paired by position, so the caller aligns them by timestamp first.
Volume-price trend and the modified volume-price trend (Markstein; Hawkins)
David G. Hawkins (“Modified Volume-Price Trend Indicator”, Stocks & Commodities V28:5, April 2010, pp. 22-28) restates David L. Markstein’s 1972 volume-price trend, which adds “a fraction of the day’s volume proportional to the price change”: V[i]*(P[i]-P[i-1])/P[i-1] on day i, “Where: V = Volume, P = Close”. His modified line, the MVPT, uses “the typical price, (O + H + L + C)/4” in place of the close, is overlaid linearly on a log-scale price chart with “the level and scale of the MVPT plot” adjusted “so it coincides with the price plot as much as possible”, and reads “The direction of the MVPT during a divergence” as “the leading indicator of the price”. t6VolumePriceTrend computes each increment in the vendors’ order, the change over the prior close times the volume, sums it from 0 on bar 0 with CumSum, and leaves out his Level, Scale and division of the volume by 50,000, which are affine.
vpt := c.Delta(1).Div(c.Shift(1)).Mul(v).CumSum()
p4 := o.Add(h).Add(l).Add(c).Div(4)
mvpt := p4.Delta(1).Div(p4.Shift(1)).Mul(v).CumSum()- Inputs: opens, highs, lows and closes (
vptreads only the closes) and volumes in any unit; the lines scale with the volume unit, and Hawkins’s division by 50,000 is such a scale. No parameters. - First valid index: 0 for both lines: bar 0 is 0 (
Delta’s 0 overShift’s +0 pad, whichDivreads as 0), as TA-Lib’s TA_PVT gives, and a zero prior close adds 0 where TA-Lib skips the increment. A missing close or volume makes the line NaN from its bar on. No mask is needed, but the level depends on the first bar, so a rule readsvpt.Delta(k), a slope or a regression residual rather than the level. - Anchors: on Jez Liberty’s Excel sheet for the article (Traders’ Tips, Stocks & Commodities V28:5, April 2010, p. 75, Figure 14: 18 rows of OHLCV, 3 to 26 January 1995),
mvpt.Add(v[0]), his seed being the first day’s volume, rounds to the printed MVPT column at 2 decimals on all 18 rows (26691.00 to 26764.47), andp4equals the printed OHLC-average column to its printed digits but differs from those decimals in bits on 5 rows (607.4250000000001 against 607.425). The incrementc.Delta(1).Div(c.Shift(1)).Mul(v)equals a loop of the vendors’((c - c1)/c1)*v(TA-Lib’s TA_PVT, TradingView’s help formula, TradeStation’s PriceVolTrend) bit for bit, on Liberty’s 17 increments and on a 700-bar synthetic path.vpt.Delta(k)equalsinc.Sum(k)from bar k, withincthe increment, to rounding (not bit for bit): the start-invariant form. With v = 1 the line less ln(c/c[0]) equals the running sum of r - ln(1 + r), r the close’s fractional change, to rounding; that gap is non-negative and grows like n times the mean square of r over 2 (n sigma^2/2, sigma the standard deviation of r, when the mean change is small), so Hawkins’s “virtually perfect match” of the V = 1 line and the log price is first-order. The force index over the prior close,fi.Div(c.Shift(1))withfi := c.Delta(1).Mul(v), equals the increment to rounding, not bit for bit (Hawkins’s listing multiplies before it divides, in this order). His listing below equalsmvpt.Div(50000).Mul(scale).Add(level)to rounding, and the regression residual below is unchanged by Level and Scale to rounding. - Traps: the percentage form
c.RateOfChange(1).Div(100).Mul(v)differs from the vendors’ increment in the last bits (on all 17 of Liberty’s), sinceRateOfChangeis (c/c1 - 1)*100; pandas-ta’s PVT is 100 times the VPT and NaN on bar 0, which TA-Lib’s issue tracker rules a “Scale bug, not a convention”; Hawkins’s “typical price” is the OHLC average (O+H+L+C)/4, not the vendors’ (H+L+C)/3; “MVPT above price means accumulation” is not his wording, since his rule is the MVPT’s direction during a divergence; andCumSummatches a plain running sum to rounding only. - Variant: Hawkins’s exact MetaStock listing,
v.Div(50000).Mul(p4.Delta(1)).Div(p4.Shift(1)).CumSum().Mul(scale).Add(level), an affine map ofmvpt; his Level and Scale are fitted by eye over regions between discontinuities chosen with hindsight. - Variant: a causal fit in place of his overlay,
mvpt.RegressionZScore(p4.Log(), n).SetN(n-1, math.NaN()): the receiver is the response, so z > 0 means the MVPT sits above the line fitted on the log price; a causal analogue, not Hawkins’s by-eye fit (which regresses price on MVPT), with n the caller’s. - Evidence: Hawkins prints charts only, the V = 1 chart of AOL included, with no numbers. Katsanos’s direction test (“Comparing Seven Money Flow Indicators”, Stocks & Commodities V29:8, July 2011, Figure 4; 30 stocks, October 2000 to October 2010, a 9-dollar commission) gave the VPT 15.8% a year compounded over 855 trades with a profit factor of 1.75, against 14.1% and 1.69 for the OBV, and he wrote that the VPT “did not perform as well, barely managing to outperform the OBV”; its VPT code is not printed, its settings were optimised on the same data, and the bullish level of 50 printed for it was not applied (his text) and would have no meaning on a line whose level depends on the first bar. Evidence, not a reference.
- Pinned by
TestRecipeT6VolumePriceTrend; runnable exampleExampleSeries_CumSum_volumePriceTrend.
Money-flow divergence system (Katsanos)
Markos Katsanos (“Comparing Seven Money Flow Indicators”, Stocks & Commodities V29:8, July 2011, pp. 15-26) buys “if the linear regression slope of price was negative, while the 40-day slope of the indicator was positive” and a stochastic crosses “above its six-day moving average”, and liquidates “if the divergence turned negative (positive price slope with negative indicator slope)” with the indicator crossing “below its 50-day moving average”, “or after the specified two-month maximum holding period”. t6MoneyFlowDivergence follows his MFI DIVERGENCE TEST listing (p. 20) with any money-flow line x in place of MFI (the volume-price trend above, Chaikin’s money flow; he compared seven): the code’s 10-bar stochastic, a ratio of 3-bar averages with STOCH=100 on a zero range, is written as a ratio of 3-bar sums, and its barssinceentry>42 exit, decided at the close of entry + 43 and filled at the next open, is a hold of 44 bars.
ll := l.Lowest(k)
rng := h.Highest(k).Sub(ll).Sum(3)
st := c.Sub(ll).Sum(3).Mul(100).Div(rng).Add(rng.EQ(0).Mul(100))
trig := st.CrossesAboveS(st.Average(6))
buy := c.Slope(n).LT(0).Mul(x.Slope(n).GT(0)).Mul(trig)
sell := c.Slope(n).GT(0).Mul(x.Slope(n).LT(0)).Mul(x.CrossesBelowS(x.Average(m))).SetN(m, 0)
pos := c.Position(buy.Shift(1), sell.Shift(1), hold)- Inputs: highs, lows, closes and the line x, of one length; k, n, m and hold in bars, his STOCHBARS 10, LRSBARS 40, MASELL 50 and, for XTIME 42, a hold of 44. The flags are shifted one bar before
Position, soposis 1 on each bar held, from the open after the signal. - First valid index:
buyreads 0 before bar n - 1 and needs no mask, becauseSlope’s warm-up 0 is neither below nor above 0;stis valid from bar k + 1;sellis masked to 0 on bars 0 to m - 1: at bar m - 1 (49) of a 700-bar synthetic path all three sell conditions hold againstAverage(50)’s pass-through warm-up, and the mask stops it.posreads 0 before the first entry. With a masked 21-bar line (Chaikin’s money flow) the line’s 40-bar slope is valid from bar 59, and the flags read 0, not NaN, before it. - Anchors: the helper equals a loop of his EasyLanguage (the MFI listing with the line in place of MFI: the 3-bar-average stochastic,
STOCH=100on a zero range, the crosses, the regression-slope signs, next-bar market fills and thebarssinceentry>42exit) on every bar of the 700-bar path, with the volume-price trend as the line; its book enters on bars 122, 172, 482, 575 and 628 and leaves on 166, 216, 526 and 619 by the 44-bar limit and on 638 by the sell of bar 637. The ratio of 3-bar sums equals his ratio of 3-bar averages to rounding (3e-16 relative on the path) and decides the same crosses, and the zero-range branch reads 100 at the end of the path’s 12-bar flat run. His code gateIF BARNUMBER>MFPERIOD+LRSBARSisSetN(61, 0)on both flags and changes no flag on the path. - Traps: his text’s “15-day stochastic” is 10 bars in his code, and only the code smooths it over 3 bars; the 40-day slope is the regression slope (TradeStation’s LinearRegSlope, here
Slope), not a one-bar change; the bullish-level test of his text “wasn’t applied to the OBV and VPT or the oscillators”, and the level for Chaikin’s money flow in this test is not printed; only the MFI code is printed, so the other lines’ systems are this listing with the line substituted.Positiondoes not read a sell flag on the entry bar and re-enters no earlier than the bar after an exit, as his code’s single position does. - Variant: the text’s 15-bar stochastic, k = 15 (
l.Lowest(15),h.Highest(15)). - Variant: his code gate,
buy.SetN(61, 0)andsell.SetN(61, 0)(BARNUMBER>MFPERIOD+LRSBARS, 21 + 40); he says the system “requires a minimum of 81 bars to be loaded”. - Evidence: his Figure 2 (30 stocks, 31 October 2000 to 31 October 2010, a 9-dollar commission, 10,000 dollars a trade): the VPT system made 13.0% a year compounded with a profit factor of 1.82 over 393 trades, the system on Chaikin’s money flow 9.5% and 1.58 over 396, and buy and hold 6.4%; there is no control without the volume leg, and the test is in sample. Evidence, not a reference.
- Pinned by
TestRecipeT6MoneyFlowDivergence.
Dead-band on-balance volume (Kaufman)
Perry J. Kaufman (“Do Small Price Changes Matter Or Are They Just Noise?”, Stocks & Commodities V41:2, February 2023, pp. 24-28) adds “the total volume of today’s trading” to OBV on a higher close and subtracts it on a lower one, then creates “a new series that omits any price move under our threshold filter”, since “you need to use a percentage filter so that it changes as price levels change”; the best was “80 bp” on “the 80-day trend”. His OBV sidebar buys “next bar on open” when MAOBV > MAOBV[1] and sells when MAOBV < MAOBV[1], doing nothing on a tie, but its filter is in points, and his only percentage code, in the moving-average sidebar, is one-sided, so the two-sided percentage band here is a reading of his text. t6DeadBandOBV builds the line with UpDownSum(1, x), takes the one-bar change of its n-bar simple average and latches its sign with Hysteresis(0, 0).
obv := c.UpDownSum(1, x).Mul(v).CumSum()
d := obv.Average(n).Delta(1).SetN(n, math.NaN())
long := d.Hysteresis(0, 0).GT(0)- Inputs: closes and volumes of one length; x in percent of the prior close, so Kaufman’s 80 basis points is x = 0.8 (0.008 as a fraction); n = 80 (his code’s default is 60). Decisions are at the close and fills at the next open, so the position held during each bar is
long.Shift(1). - First valid index: 0 for
obv(bar 0 is 0, asOBV) and n ford, NaN before:Averagepasses the line through on bars 0 to n - 2, so unmaskeddreads the line’s own increments there and, at bar n - 1, a full average less a raw value.longreads 0 until the first rising average. - Anchors: with x = 0,
obvequalsc.OBV(c, v)bit for bit on integer volumes and to rounding on non-integer ones (a compensated sum againstOBV’s plain one).dhas the sign ofc.UpDownSum(1, x).Mul(v).Sum(n)from bar n, since the average’s change is (obv[t] - obv[t-n])/n, the last n increments over n: the window sum is the rule’s start-invariant form. The latch holds on a tie (dexactly 0, on a quiet stretch inside the band) whered.GT(0)reads flat, as his code holds onMAOBV = MAOBV[1].UpDownSum’s band equals the float replicac - c1 > |c1|*x/100on every bar. - Traps: a NaN volume, even on an unchanged bar, makes the line NaN from that bar on (
UpDownSum’s 0 times NaN), whereOBVignores it; the ratio formc/c1 - 1 > x/100agrees with the band only away from exact decimal ties, which rounding decides (100 to 100.8 at x = 0.8 is flat here, as 100.8 - 100 is 0.79999999999999716, and up in the ratio form, 0.0080000000000000071 against 0.008); a negative, NaN or infinite tolerance gives an all-zero line, silently; a regression slope of the average (Slope(k)) is a different rule from his one-bar comparison. - Variant: Kaufman’s printed point form,
dc.GT(k).Sub(dc.LT(-k))withdc := c.Delta(1)and k in price points, in place ofc.UpDownSum(1, x); k = 0, his code’s default, is Granville’s OBV. - Variant: the same rule on the window sum, with no start-dependent level,
c.UpDownSum(1, x).Mul(v).Sum(n).SetN(n, math.NaN()).Hysteresis(0, 0).GT(0). - Evidence: his Figure 2 (SPY, 2000 to October 2022, long only, the 80-day average of OBV): a profit factor of 1.31 without the filter and 2.01 with 80 basis points (151 and 114 trades, net 4,800.68 and 9,020 dollars); of the 20 non-zero filters from 0.05 to 1 percent on the 80-day average, about 13 beat no filter and those from 0.10 to 0.40 mostly do not, so the response is not monotone; in sample, one market, costs not stated, and his text swaps the numbers of Figures 3 and 4. He concludes that “Small price moves appear to be noise and do not help identify the direction of the trend.” Evidence, not a reference.
- Pinned by
TestRecipeT6DeadBandOBV; runnable exampleExampleSeries_UpDownSum_deadBandOBV.
Volume indexes that move only on bars of falling (or rising) volume: Fosback’s two indexes, Holt’s divergence of the close from one of them, and Downing’s light-volume line. Recipe values are NaN before their first valid index (SetN(F, math.NaN())), and flags and latches built on masked values read 0 there. The indexes and the light-volume line are cumulative: they start from the base or from 0 on bar 0, and their level depends on where the history starts, the multiplicative index by a constant factor and the additive one by a constant, so rules should read ratios or differences to their own averages, or band positions, as Fosback’s signal and Holt’s index do. Prices are positive and finite and volumes finite and non-negative; a volume counts as falling or rising only when it differs from the bar before, so an equal volume moves no index. CumSum is a compensated running sum: it equals the vendors’ plain running sums to rounding, bit for bit when every partial sum is an integer below 2^53. Lengths are in bars, and series are paired by position, so the caller aligns them by timestamp first.
Negative and positive volume indexes (Fosback)
Norman G. Fosback (Stock Market Logic, The Institute for Econometric Research, 1976, 1992 printing, ch. 34 “Negative and Positive Volume Indexes”, pp. 120-124) starts “with a base index level, say, 100.00” and changes the negative volume index by the percent change of a market index “only in those periods in which volume has fallen relative to the preceding period”; the positive index “measures market changes only on days, or weeks, that NYSE volume has increased from the preceding period”, and the signal compares “the current NVI figure” with “its one year average”. His text says the change “is added”, but his worked step, “101.03 (1.0103 x 100.00)”, and his Table 37 compound it. t6VolumeIndexes compounds the close’s fractional change on the bars whose volume fell (nvi) or rose (pvi) as a compensated sum of logarithms, returns the additive index nviA beside them, and gives bull, 1 where nvi is above its n-bar simple average. The heading spells the name out because the NVI of the True Range page is Kayakkal’s normalised volatility indicator (t6NVI), a different thing.
r := c.DeltaPercent(1).Div(100)
dn := v.Delta(1).LT(0)
up := v.Delta(1).GT(0)
nvi := r.Mul(dn).Log1p().CumSum().Exp().Mul(base)
pvi := r.Mul(up).Log1p().CumSum().Exp().Mul(base)
nviA := c.DeltaPercent(1).Mul(dn).CumSum().Add(base)
bull := nvi.GT(nvi.Average(n).SetN(n-1, math.NaN()))- Inputs: closes
cand volumesvof one length; only whether the volume fell or rose is read, so its unit is the caller’s.baseis 100 (Fosback; Holt’s editor) or 1000 (Tulip, StockCharts, pandas-ta’snvi); n is Fosback’s one-year average, 52 on weekly bars, or 255 on daily bars (Dennis D. Peterson, “Market Breadth: Volume”, Stocks & Commodities V19:2, February 2001, pp. 57-60, charts “its 255-day moving average”). The volume test is strict: an equal volume leaves both indexes unchanged. - First valid index: 0 for
nvi,pviandnviA: on bar 0DeltaPercentandDeltaread 0, so the sum of logarithms is 0 and the index isbase. n - 1 forbull, 0 before. Its mask,SetN(n-1, math.NaN()), changes no bar, since the warm-up average passesnvithrough andnvi.GT(nvi)is 0, and is kept to show the warm-up; with n above the lengthAveragereturns a copy andbullis 0 on every bar. A missing close makes the three indexes NaN from its bar on; a missing volume moves neither index on its bar or the next. - Anchors: on Fosback’s Table 37 (ch. 34, p. 121; 11 rows, 7 to 21 January 1976, NYSE volume and the Dow’s percentage change on the falling-volume days), with closes built from his printed percentages,
nviwith base 100 is 100.00, 101.03, 101.38, 101.38, 101.38, 103.24, 103.24, 103.81, 103.81, 103.81 and 103.41 at 2 decimals on all 11 rows; the closes are constructed, so this pins the multiplicative arithmetic, not index data. Bar 0 isbaseexactly, and a bar whose volume equals the prior bar’s leaves both indexes unchanged, bit for bit. Against an exact product recursion (basetimes the product of 1 + r, in high precision)nviis within 2^-49 relative (1.8e-15) on every bar of the 700-bar synthetic path. On a history cut at a later bar the index is the full history’s times a constant factor, sonvi.Div(nvi.Average(n))agrees with the full history’s from n - 1 bars after the cut to rounding, andbullbit for bit. - Traps: Fosback’s wording, a percent change “added” to the index, is the additive
nviA, which misses 6 of the 11 rows (103.21, 103.21, 103.76, 103.76, 103.76 and 103.38): the table compounds. The form throughc.Log().Delta(1),c.Log().Delta(1).Mul(dn).CumSum().Exp().Mul(base), drifts, since eachLogcarries an error proportional to the logarithm of the price that the difference keeps: its error grows with the length and the price level and passes 1e-14 relative on a 20,000-bar path. Mask the change beforeLog1p,r.Mul(dn).Log1p(): masked after (r.Log1p().Mul(dn)), a change of -1 (a close of 0) givesLog1p’s -Inf, and -Inf times 0 is NaN, whichCumSumcarries to every later bar. StockCharts’ calculation (“Add the Percentage Price Change to Cumulative NVI”) and pandas-ta 0.4.71b0’snviare additive with base 1000, Tulip’snvimultiplicative with base 1000; pandas-ta’spviis not cumulative (each bar is the base times one close ratio, or the base). When the index has not moved for n bars (no falling-volume bar with a price change, as in a run of zero volumes) it equals its average in exact arithmetic, andbullis decided by the rounding of the average of n equal values, which can land one unit away. - Variant: StockCharts’ signal, a 255-day exponential average,
nvi.GT(nvi.XAverage(n).SetN(n-1, math.NaN()))with n = 255, depends on where the history starts through its seed (the simple average of the first n values), where the simple average’s signal does not. - Variant: the breadth form that Fosback says the first indexes used (“the Advance/Decline Line being accumulated only for days in which volume is below that of the previous day”), Dysart’s negative issues traded index as Gregory Morris gives it (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 9 “Composite Indicators”):
a.Sub(d).Mul(dn).CumSum()with advancesaand declinesd, andupfor the positive one; on integer counts it equals a plain running sum bit for bit. - Notes: the TSSB manual (Aronson and Masters, Statistically Sound Machine Learning for Algorithmic Trading of Financial Instruments, 2013, pp. 158-159) gives a windowed NEGATIVE VOLUME INDICATOR, the average of the last HistLength relative changes with those on bars of unchanged or rising volume set to 0, divided by the standard deviation of price changes over twice HistLength or 250 days, whichever is longer, then “slightly compressed and transformed to the range -50 to 50” by a constant it does not publish. Arthur A. Merrill’s negative volume divergence index (Stocks & Commodities V8:10, October 1990, pp. 396-397) corrects the volume for “the unusually quiet Fridays and Mondays” with factors “obtained from a long historical record” that he does not print.
- Evidence: Fosback’s Table 38 (weekly NVI, 1941 to 1975, p. 122): with the index above its one-year average a bull market was in progress 96% of the time (bear 4%), with it below, 47% (bear 53%), against 70% and 30% over the 35 years; his Table 39 gives a bull market 79% of the time with the PVI up and 33% with it down, and the PVI “has no additional forecasting value” (p. 124). The bull and bear labels are dated after the fact, so the tables classify rather than forecast, in sample, with no trades and no costs. Evidence, not a reference.
- Pinned by
TestRecipeT6VolumeIndexes; runnable exampleExampleSeries_Log1p_negativeVolumeIndex.
Negative volume disparity indicator (Holt)
Phillip C. Holt (“Enhancing On-Balance And Negative Volume”, Stocks & Commodities V14:6, June 1996, pp. 265-269) built his negative volume disparity indicator “using the basic %B formula, but with a somewhat longer 33-period moving average” and “the customary two standard deviations”, “added 1 to each of the formulas for when %B values fell below zero”, and divides the close’s by the index’s (“a %B value of 1.5 for price and a %B value of 2 for on-balance volume would produce a ratio value of 0.75”); he buys “when the four-week simple moving average of NVDI rises through the 0.95 level” and sells when it “falls through the 1.1 level”. t6NVDI takes BollingerDivergence of the index against the close with smooth 1, which is the ratio (1 + %B of the close)/(1 + %B of the index) less 1, in percent, turns it back into the ratio with Div(100).Add(1) and books the two rules with Position. Holt’s own tests used MetaStock’s NVI(), which the article does not specify; the editor’s Excel sidebar uses the additive index of base 100, and is the anchor here.
nvdi := x.BollingerDivergence(c, n, n, 1).Div(100).Add(1).SetN(n-1, math.NaN())
m := nvdi.Average(4)
pos := c.Position(m.CrossesAboveS(0.95), m.CrossesBelowS(1.1))- Inputs: closes
cand a volume indexxof one length,xthe base of the divergence and the close its comparison (the editor’s sheet:nviAof the section above with base 100); n = 33 on weekly bars (“the selection of 33 weekly periods was judgmental”). The bands use the population standard deviation, asStdDevand the editor’s STDEVP do.CrossesAboveS(0.95)reads “rises through” as the prior average at or below 0.95 and the current one above it, andCrossesBelowS(1.1)the mirror. - First valid index: n - 1 for
nvdi(32 at n = 33) and n + 2 form(35), NaN before;posreads 0 before the first entry.BollingerDivergencereads 0 before its first valid index, whichAdd(1)would turn into a ratio of 1, hence the mask. - Anchors: on the editor’s Excel sidebar, Figure 2 (J.P. Morgan, 35 daily rows, 2 January to 20 February 1996),
nviAwith base 100 matches all 35 printed Negative Volume cells at 2 decimals (ending 104.97); on the last three rows the 27 printed band, %B and NVDI cells hold at 2 decimals with the population deviation (the last row: the close’s average 79.59, bands 83.66 and 75.53 and %B 0.47; the index’s 101.64, 105.66, 97.63 and 0.92; NVDI 0.77), and the helper’snvdithere is 0.9037308019493896, 0.867957921454266 and 0.7691223632932477, the printed 0.90, 0.87 and 0.77.mandposare NaN and 0 on all 35 rows: 35 rows are too few for the 4-bar average of a 33-bar index.nvdiequals the direct ratiolegC.Div(legX)to rounding, each leg written withAverageandStdDevass.Sub(s.Average(n)).Add(s.StdDev(n).Mul(2)).Div(s.StdDev(n).Mul(4)).Add(1); a flat leg is %B 0.5 (1.5 after the +1) byBollingerDivergence’s convention, where the direct formula divides 0 by 0 (Divreads a leg of 1 and Excel shows #DIV/0!). On the 700-bar synthetic path (n = 33) the book enters nine times. - Traps: unmasked,
maverages the warm-up 1s into bars n - 1 to n + 1: on the editor’s sheet it would read 0.9759, 0.9429 and 0.8852 on the last three rows instead of NaN. The sheet’s index is additive: the multiplicativenvimisses 24 of its 35 Negative Volume cells (from 17 January 1996 on), and the sample deviation misses 16 of the 27 band cells.Positionenters only on a cross of m from at or below 0.95, so if m is already above 0.95 on its first valid bar the book stays flat until the average dips and recovers, andBollingerDivergence’s rolling variance is not bitwise start-invariant, so nvdi and its crosses can move in the last bits when the history starts later. - Variant: Holt’s on-balance volume disparity indicator is the first statement on
x := c.OBV(c, v); his MetaStock formulas are written for it (“To create a negative volume disparity indicator, substitute NVI() where OBV() appears in the formulas”). - Evidence: Holt’s Figure 6 (weekly NYSE Composite, 1977 to 1996): the NVDI system made 37% a year in 15 trades (11 winners, 4 losers) and was out of the market 493 weeks, against 38% for the on-balance volume disparity system and 31% for a 39-week moving average; the 33 weeks were “judgmental” and the levels came from a sweep on the same data (“I followed a similar procedure for the NVDI system”), so in sample, with no costs stated. Evidence, not a reference.
- Pinned by
TestRecipeT6NVDI; runnable exampleExampleSeries_BollingerDivergence_nvdi.
Light-volume line (Downing)
Daniel E. Downing (“A new indicator for the S&P stock index futures”, Stocks & Commodities V8:4, April 1990, pp. 132-133) measures an up day “from the absolute low of the day being examined to the close”, the absolute low being “either the low for the day or the previous day’s close, whichever is lower”, records a down day from the close to the absolute high times -100, and on each day whose volume “is less than the previous day’s volume” adds the result, “making an on-balance line of the running total”. His spreadsheet under Figure 2, “Calculating the Negative Volume Index” (a different line from Fosback’s), takes the larger of the high and the previous day’s low on the down branch, max(H, L[t-1]) - C, and sends every close that is not up to that branch; his table follows the formula. t6LightVolumeLine writes that spreadsheet form with TrueLow for the absolute low, Maximum of the high and the shifted low for the down branch, 0/1 flags for the two branches and CumSum for the line.
upF := c.Delta(1).GT(0)
upR := c.Sub(l.TrueLow(c))
dnR := h.Maximum(l.Shift(1)).Sub(c)
g := upR.Mul(upF).Sub(dnR.Mul(upF.Mul(-1).Add(1))).Mul(100)
line := g.Mul(v.Delta(1).LT(0)).CumSum()- Inputs: highs, lows, closes and volumes of one length, prices in index points (the 100 turns his S&P futures moves into whole numbers); only whether the volume fell is read, so its unit is the caller’s.
- First valid index: 0, where the line is 0: bar 0’s volume flag is 0 (
Delta’s warm-up), so its down-branch value, -191 on Downing’s data, is not added. A missing close makes the line NaN from its bar on. - Anchors: on Downing’s Figure 2 (28 rows of OHLCV, 7 December 1989 to 17 January 1990, with 27 printed absolute ranges and index values) all 27 ranges and all 27 index values equal the printed integers within 1.02e-11 (120 on 8 December, 120.00000000000455 in binary; 43 on 17 January, 43.000000000006366). The last row’s volume equals the day before’s (52553 on both days), so its range is not added: the line moves on strictly falling volume only.
h.TrueHigh(l)equalsh.Maximum(l.Shift(1))from bar 1, a second spelling of the down branch. - Traps: his text takes the absolute high as “the high for the day or the previous day’s close, depending on which number is higher”, the true high
h.TrueHigh(c), and says “Unchanged days we ignore”, where his formula and table use the previous low and send an unchanged close down. The text form changes three increments (-314 for the printed -225 on 14 December 1989, -521 for -500 on 5 January 1990, -1090 for -992 on 12 January) and, as two of those days had falling volume, 23 of the index rows; his table has no unchanged close. - Variant: the text form,
dnR := h.TrueHigh(c).Sub(c)andg := upR.Mul(upF).Sub(dnR.Mul(c.Delta(1).LT(0))).Mul(100), in which an unchanged close adds 0. - Evidence: Downing prints no results: “We have used this indicator only six months or so”, and his Figure 1 plots the line’s weekly close against the S&P 500 cash index, with no rule and no test. Evidence, not a reference.
- Pinned by
TestRecipeT6LightVolumeLine; runnable exampleExampleSeries_TrueLow_lightVolumeLine.
Money Flow Index
MFI
func (s Series) MFI(h, l, c, v Series, period int) Series
MFI implements the Money Flow Index. A period of less than one returns a zero-filled series; mismatched input lengths return an empty series.
The output is on a 0 to 100 scale. The warm-up reads 0, not NaN: bars 0 to period-2 are exactly 0, the first valid index is period-1 and the minimum length is period. 0 is the oversold extreme, so a < 20 rule fires on every warm-up bar, and the first computed bar, period-1, sums only period-1 money flows, so the first full value is at bar period; trim (Window(-period)) or mask that many bars before a threshold rule. A flat window after the warm-up gives 50.
MoneyFlowIndex
func (s Series) MoneyFlowIndex(h, l, c, v Series, period int) Series
MoneyFlowIndex is an alias for MFI, with the same warm-up, scale and input rules.
Chaikin Oscillator
ChaikinOsc
func (s Series) ChaikinOsc(h, l, c, v Series, speeds ...int) Series
ChaikinOsc implements Marc Chaikin’s momentum indicator that measures the flow of money into and out of a security by analysing volume and price movements. It aims to identify buying and selling pressure.
Positive values indicate buying pressure and potential bullish conditions, while negative values suggest selling pressure and potential bearish conditions.
This indicator is three steps removed from price action: 1. price and volume form the ADL; 2. two EMAs are applied to the ADL; and 3. the difference between EMAs forms the oscillator.
This separation can sometimes lead to disconnects between the indicator and the underlying price movement.
It seems that values of 3 and 10 are the most commonly used for the two EMAs, so we default to these if no speeds are provided. Chaikin’s own 1994 spreadsheet (Hartle, Stocks & Commodities V12:1) uses simple 3- and 10-day averages of the line, ad.Average(3).Sub(ad.Average(10)) with ad the AccumDist line; the EMA form here is the vendors'.
Bars 0 to fast-2 are exactly 0, since both averages pass AccumDist through; the bars up to slow-2 are XAverage(fast) of AccumDist minus AccumDist; and the first full value is at slow-1 (3 and 10 by default).
Klinger Volume Oscillator
KlingerVolumeForce
func (s Series) KlingerVolumeForce(h, l, c, v Series) Series
KlingerVolumeForce returns the volume force of Stephen Klinger’s Volume Oscillator for the bars with high h, low l, close c and volume v. The receiver is not used (as in AccumDist, ChaikinOsc, MFI and TrueRange). The inputs h, l, c and v must have the same length M; otherwise the result is an empty, non-nil Series. Otherwise the result is a non-nil Series of length M (non-nil also when M is 0). The inputs are not modified and the result is freshly allocated.
The construction is Klinger’s (“Identifying Trends With Volume Analysis”, Technical Analysis of Stocks & Commodities, December 1997). The trend T is +1 when the sum high + low + close rises from the previous bar and -1 when it falls, and it is held when the sum is unchanged. The daily measurement is DM = high - low. The cumulative measurement CM adds up the DM of the current trend and restarts on a trend change, when it becomes the DM of the previous bar plus the DM of the current bar, so the first bar of the new trend is included. The volume force is
VF = v |2 DM/CM - 1| T 100
with Klinger’s factor 100. KlingerVolumeOscillator is the difference of two exponential moving averages of this series.
The warm-up reads 0, not NaN: the bars before the first valid index have no trend yet and read +0. The first valid index is the first bar whose sum high + low + close differs from the previous bar’s, normally bar 1.
Non-finite values: if h, l, c or v is NaN or infinite at a bar, or the sum high + low + close, DM, CM or the force overflows to an infinity there, that bar and every later bar are NaN. Earlier bars are unchanged by a later stop.
If CM is exactly 0 the ratio DM/CM is taken as 0. (The TradeStation code in the Traders’ Tips of December 1997 assigns the force only where CM <> 0, so it keeps the previous force there; Tulip gives NaN there and on every later bar.) The sign is applied by exact negation, so the force can be -0 (for example on a falling bar with DM/CM = 1/2 and a positive volume).
If the sum is unchanged the trend is held, as in Klinger’s text and in Tulip. (His spreadsheet, the TradeStation code of the December 1997 Traders’ Tips and the TradingView help page use -1 instead.)
Klinger’s defaults are 34 and 55 for the two averages and 13 for his trigger line; they are not encoded here. Both averages of KlingerVolumeOscillator are seeded with the first force, as Klinger’s spreadsheet and Tulip seed them (XAverage, TradingView’s ta.ema and pandas-ta seed with a simple average). So, with aF = 2 / (float64(fast) + 1) and aS = 2 / (float64(slow) + 1), the oscillator from the first valid index up to the bar before its first NaN is vf.XAverageAlpha(aF).Sub(vf.XAverageAlpha(aS)), where vf is the force over those bars, bit for bit.
KlingerVolumeOscillator gives the recipes for Klinger’s trigger line, the simplified signed-volume form of TradingView and pandas-ta, and the cumulative KVO.
The force is latched: the trend and CM carry over from bar to bar and a non-finite value is final, so on a later slice of the inputs the force can differ from the full inputs’ force on its first bars, until a trend change restarts CM in both; after that the two agree (unless a non-finite value before the slice stopped the full inputs).
Sources: Klinger, “Identifying Trends With Volume Analysis”, Technical Analysis of Stocks & Commodities, December 1997; Tulip Indicators, kvo.
KlingerVolumeOscillator
func (s Series) KlingerVolumeOscillator(h, l, c, v Series, fast, slow int) Series
KlingerVolumeOscillator returns Stephen Klinger’s Volume Oscillator: the difference of a fast and a slow exponential moving average of the volume force of KlingerVolumeForce, for the bars with high h, low l, close c and volume v. The receiver is not used (as in AccumDist, ChaikinOsc, MFI and TrueRange). The inputs h, l, c and v must have the same length M; otherwise the result is an empty, non-nil Series, whatever fast and slow are. The parameters are valid if 1 <= fast <= slow (there is no upper bound and no length requirement); invalid parameters give M values of +0. Otherwise the result is a non-nil Series of length M (non-nil also when M is 0). The inputs are not modified and the result is freshly allocated.
Klinger’s defaults are fast = 34 and slow = 55, with 13 for his trigger line; they are not encoded here.
The construction is Klinger’s (“Identifying Trends With Volume Analysis”, Technical Analysis of Stocks & Commodities, December 1997). The trend T is +1 when the sum high + low + close rises from the previous bar and -1 when it falls, and it is held when the sum is unchanged. The daily measurement is DM = high - low. The cumulative measurement CM adds up the DM of the current trend and restarts on a trend change, when it becomes the DM of the previous bar plus the DM of the current bar, so the first bar of the new trend is included. The volume force is
VF = v |2 DM/CM - 1| T 100
with Klinger’s factor 100. With aF = 2 / (float64(fast) + 1) and aS = 2 / (float64(slow) + 1), each average adds the force times its constant to the previous average times one minus that constant (a constant of 1 copies the force). Both averages are seeded with the first force, as Klinger’s spreadsheet and Tulip seed them (XAverage, TradingView’s ta.ema and pandas-ta seed with a simple average). So from the first valid index up to the bar before the first NaN the result is vf.XAverageAlpha(aF).Sub(vf.XAverageAlpha(aS)), where vf is the force over those bars, bit for bit; when fast equals slow every valid bar reads +0.
The warm-up reads 0, not NaN: the bars before the first valid index have no trend yet and read +0, and so does the first valid index itself, because both averages start from the first force. The first valid index is the first bar whose sum high + low + close differs from the previous bar’s, normally bar 1.
Non-finite values: if h, l, c or v is NaN or infinite at a bar, or the sum high + low + close, DM, CM, the force, an average or the oscillator overflows to an infinity there, that bar and every later bar are NaN. Earlier bars are unchanged by a later stop.
If CM is exactly 0 the ratio DM/CM is taken as 0. (The TradeStation code in the Traders’ Tips of December 1997 assigns the force only where CM <> 0, so it keeps the previous force there; Tulip gives NaN there and on every later bar.)
If the sum is unchanged the trend is held, as in Klinger’s text and in Tulip. (His spreadsheet, the TradeStation code of the December 1997 Traders’ Tips and the TradingView help page use -1 instead.)
Klinger’s trigger line is a 13-bar exponential average of the oscillator:
kvo := c.KlingerVolumeOscillator(h, l, c, v, 34, 55)
trigger := kvo.XAverageAlpha(2.0 / 14)
Because the oscillator reads 0 up to the first valid index, so does the trigger, and from that bar on it is the same average started from a seed of 0, which is how Klinger’s spreadsheet seeds its trigger. His spreadsheet steps with the rounded constant 0.143, M3 = M2 + 0.143 (L3 - M2), so its values differ from these.
TradingView’s built-in Klinger oscillator and the kvo of pandas-ta compute a simplified form with signed volume and no DM or CM. With
tp := h.Add(l).Add(c).Div(3)
d := tp.Momentum(1).SetN(1, math.NaN())
TradingView’s signed volume is sv := v.Mul(d.GTE(0).Mul(2).Sub(1)) (plus volume on an unchanged bar, minus volume on bar 0, where its change is na) and pandas-ta’s is sv := v.Mul(d.GT(0).Sub(d.LT(0)))[1:] (0 on an unchanged bar; pandas-ta leaves out bar 0, so its result starts at bar 1). The oscillator is then sv.XAverage(34).Sub(sv.XAverage(55)); their averages are seeded with a simple average, as XAverage is, and agree with it to rounding from the slow average’s seed bar. Before that bar they give no value while XAverage passes its input through, so mask those bars.
Klinger converts the oscillator to a cumulative KVO for longer horizons (his Figure 4) but does not print the formula. A running total is kvo.CumSum(), as in the SmarTrader code of the December 1997 Traders’ Tips; the TradeStation code there sums over a rolling window of Smooth bars, kvo.Sum(n), and sums its trigger the same way.
Where the inputs are finite, the sum changes between bars 0 and 1 and CM is not 0, the oscillator from bar 1 is the kvo of Tulip Indicators to rounding (Tulip’s output starts at bar 1): Tulip’s averaging step (vf-e)*a + e rounds differently from the step used here. Tulip gives NaN from the first bar where CM is 0 on, starts its trend as rising when the sums of bars 0 and 1 are equal, does not stop at a non-finite value, and its step can overflow near the largest floats where this one does not.
The method is recursive and latched: each bar depends on all the bars before it and a non-finite value is final, so the method on a later slice of the inputs gives different values from the same bars of the full inputs.
Sources: Klinger, “Identifying Trends With Volume Analysis”, Technical Analysis of Stocks & Commodities, December 1997; Traders’ Tips, Technical Analysis of Stocks & Commodities, December 1997; Tulip Indicators, kvo.
Volume Flow
FiniteVolumeElements
func (s Series) FiniteVolumeElements(h, l, c, v Series, n int, cutoff float64) Series
FiniteVolumeElements returns Markos Katsanos’s finite volume elements (FVE) with his fixed cut-off, from “Detecting Breakouts” (Technical Analysis of Stocks & Commodities, April 2003). A bar’s money flow is the close’s distance above the middle of the bar plus the change in its typical price (the mean of the high, the low and the close). A bar scores its volume when the money flow exceeds the cut-off, minus its volume when the money flow is below the negated cut-off, and 0 otherwise (a flow exactly on the band scores 0). The result is the sum of the scores over n bars as a percentage of the total volume of the same n bars, the bars that scored 0 included, so with non-negative volumes it lies between -100 and 100.
The cut-off is cutoff times the close: cutoff is a fraction of the close, not a percentage. Katsanos uses n = 22 and cutoff = 0.003 on daily bars, and in his September 2003 article scales the fixed cut-off by sqrt(T/390) for T-minute bars. The receiver is not used. h, l, c and v must have the same length; otherwise the result is empty. The call is valid when n is at least 2 and less than the length and cutoff is finite and not negative; an invalid call gives a series of zeros. The warm-up reads 0, not NaN: the first valid index is n (bar 0 has no previous typical price, so the first full window of scores ends at bar n), and bars 0 to n - 1 are 0.
The method is window-local: a NaN or infinite high, low or close gives NaN in every window that holds its bar or the next one (whose money flow reads the previous typical price), a NaN or infinite volume in every window that holds its bar, as does a window whose volumes sum to exactly 0, and later bars recover. With cutoff = 0 each bar’s volume is signed by the sign of its money flow.
FiniteVolumeElementsVolatility
func (s Series) FiniteVolumeElementsVolatility(h, l, c, v Series, n int, coef float64) Series
FiniteVolumeElementsVolatility returns Markos Katsanos’s finite volume elements (FVE) with his volatility cut-off, from “Detecting Breakouts In Intraday Charts” (Technical Analysis of Stocks & Commodities, September 2003). The money flow and the scoring are those of FiniteVolumeElements, but the cut-off is coef times the sum of the interday volatility (the n-bar standard deviation of the log return of the typical price) and the intraday volatility (the n-bar standard deviation of the log of the high over the low), times the close, so the band follows the market instead of being fixed for one bar size. The result is the sum of the scores over n bars as a percentage of the total volume of the same n bars, the bars that scored 0 included, so with non-negative volumes it lies between -100 and 100.
Katsanos uses coef = 0.1, with n = 22 on daily bars. The receiver is not used. h, l, c and v must have the same length; otherwise the result is empty. The call is valid when n is at least 2, coef is finite and not negative and 2n - 1 is less than the length; an invalid call gives a series of zeros. The warm-up reads 0, not NaN: the first valid index is 2n - 1 (the volatilities need n bars before the first scored bar, bar n, and the first full window of scores ends n - 1 bars later), and every earlier bar is 0.
The method is window-local: a NaN or infinite high, low or close at bar p, or a high, low or typical price without a finite logarithm, gives NaN from bar p up to bar p + 2n - 1 (the scores of bars p to p + n read it through their volatility windows or money flow, and each stays in the n-bar sum for n - 1 more bars); a NaN or infinite volume gives NaN while the n-bar window holds its bar, as does a window whose volumes sum to exactly 0; and later bars recover.
VolumeFlowIndicator
func (s Series) VolumeFlowIndicator(h, l, c, v Series, n int, coef, vcoef float64) Series
VolumeFlowIndicator returns Markos Katsanos’s volume flow indicator (VFI) from “Using Money Flow To Stay With The Trend” and “Volume Flow Indicator Performance” (Technical Analysis of Stocks & Commodities, June and July 2004). A bar scores its volume, capped at vcoef times the average volume of the n bars before it, with the sign of the change in its typical price (the mean of the high, the low and the close) when that change exceeds the cut-off, coef times the 30-bar standard deviation of the log return of the typical price times the close; a change inside the band, or exactly on it, scores 0. The scores are summed over n bars, the sum is divided by the average volume of the n bars before the current bar, and the ratio is smoothed with a 3-bar exponential average (a weight of one half on the new value). The 30-bar volatility window and the 3-bar smoothing are fixed, as in the articles. The smoothing is seeded with the mean of the first three raw ratios. MetaStock’s Mov(…, E) is understood to seed with the first value instead (a MetaStock forum formula; Katsanos does not state it), so MetaStock’s early bars differ, by half as much on each later bar. With coef at Katsanos’s 0.2 the cut-off has the form of the threshold Yang (2013) used, nine years later, to call a day nearly flat, 20% of a rolling standard deviation: here the deviation is of the typical price’s log changes over 30 bars including the current one, scaled by the close and compared with the typical price’s change in price units; the essential-bar recipes under “Event Operators” in the manual relate the two.
The receiver is not used. h, l, c and v must have the same length; otherwise the result is empty. Katsanos uses n = 130, coef = 0.2 and vcoef = 2.5 on daily bars, with n = 26 on weekly bars in his 2004 tests (26 to 30 in his 2023 code). The call is valid when n is at least 2 and at most the length, coef is finite and not negative and vcoef is positive (+Inf lifts the cap; NaN is invalid); an invalid call gives a series of zeros, as does a series too short to seed the smoothing. The warm-up reads 0, not NaN: the first valid index is 2n + 1 for n >= 30 (max(n, 30) + n + 1 in general, since the volatility needs 30 bars), and every earlier bar is 0.
The method is recursive, so it does not recover: a NaN or infinity that reaches a raw ratio (through the volatility, the scores or the lagged average) gives NaN from that bar on, because the smoothing feeds each value into the next. A lagged average volume of exactly 0 at bar max(n, 30) + n - 1 or later gives NaN from that bar, or from the first valid index if that is later, to the end; an earlier one, with a finite vcoef, caps that bar’s volume at 0. A +Inf volume is capped on its own bar and gives NaN from the next bar on. A NaN or +Inf volume that reaches only the lagged average of the cap, before bar max(n, 30) + n - 1, lifts the cap on the bars that read it instead of giving NaN. To re-seed, start a fresh slice of the inputs at a later bar.
VolumePriceConfirmationIndicator
func (s Series) VolumePriceConfirmationIndicator(v Series, short, long int) Series
VolumePriceConfirmationIndicator returns Buff Pelz Dormeier’s volume price confirmation indicator (VPCI), from “Between Price And Volume” (Technical Analysis of Stocks & Commodities, July 2007). The receiver s is the price and v the volume. The VPCI is the volume price confirmation (the long-window VWMA of the price minus its SMA, computed as the population covariance of the price and the volume over the long window divided by the mean volume, which equals it in exact arithmetic and gives exactly 0 on a constant volume), times the volume price ratio (the short-window VWMA over the short-window SMA), times the volume multiplier (the short-window mean volume over the long-window mean volume). Dormeier’s test setting is short = 7 and long = 28.
v is right-aligned with s: the trailing min(len(s), len(v)) bars of each overlap, the result has len(s) bars, and the bars of s before the overlap are 0. The call is valid when short is at least 1, long is at least 2, short is at most long and long is at most the overlap; an invalid call gives a series of zeros. The warm-up reads 0, not NaN: the first valid index is long - 1 within the overlap, and every earlier bar is 0.
The method is window-local: a NaN or infinite price or volume gives NaN in the windows that hold it, as does a window whose long-window mean volume is exactly 0, a short window of two or more bars whose volumes sum to exactly 0, a short window whose mean price is exactly 0, a product that overflows, or a short-window volume sum or VWMA sum that overflows, and later bars recover. A short-window price sum or a long-window volume sum that overflows while the other factors stay finite gives 0, not NaN, since a finite value over an infinite mean is 0. The long-window covariance is exact, so it overflows only when its own value is beyond the binary64 range. With a constant positive volume the VPCI is exactly 0 wherever it is not NaN, and scaling the price by a power of two scales the VPCI by the same factor, bit for bit, unless a scaled price, product or sum leaves the binary64 range (by other factors, to rounding).
The signal line, Dormeier’s VPCI smoothed, is a recipe rather than a method: vpci.VWMA(v, 20), the volume-weighted average of this result (Dormeier’s form; he gives no length, and 20 is the setting of the AmiBroker, TradeStation and Wealth-Lab ports in the July 2007 Traders’ Tips), with the bars before long + 18 of the overlap masked, since until then the average reads the warm-up zeros. The dollar-volume variant passes c.Mul(v), the close times the volume, as v.
The long-window covariance is computed exactly, from exact sums over the window, and rounded once (to the nearest float64, ties to even), so a call costs O(len(s)) time whatever long is, and O(len(s) + long) memory. The covariance is start-invariant: dropping leading bars of both inputs leaves its later values unchanged bit for bit. The VPCI as a whole is start-invariant only to rounding, since Average and VWMA sum each window in an order that depends on the bar’s index. On volumes that span many binades a later bar can move by a few ulps once leading bars are dropped (prices 2, 3, 5, 7, 11 and volumes 1, 1, 2^53, 1e-16, 1e-16 with short = long = 4 move bar 4 by 3 ulps), and a window sum that overflows in one order and not the other turns a value into NaN; on ordinary prices and volumes no such bar has been found. The covariance differs from the package’s Covariance, which centres each window and rounds as it goes, by Covariance’s rounding: within about 1e-12 times the product of the two standard deviations on ordinary data, and where cancellation leaves the covariance near 0, Covariance can read a nonzero value where the exact one is 0. A constant volume or a constant price gives a covariance of exactly 0. The exact sums grow with the spread of exponents in the overlap, and the cost with them: when 5e-324 and 1e308 both occur, the price and volume sums reach about 2100 bits, and the product sum about 4200 when one pair holds both.
Compositions
Price change times volume (the force index) and price change per unit of volume (the ease of movement and the Livermore money flow): every value carries the caller’s scale, the volume unit and, for the ease of movement, the divisor scale. Recipe values are NaN before their first valid index (SetN(F, math.NaN())), and flags built on them read 0 there. The cumulative lines (wobv, lmf, pct) start from 0 on bar 0 and their level depends on where the history starts, so rules should read window sums, slopes, differences or ratios to their own averages. Prices are positive and finite and volumes finite and non-negative; Div reads 0 where the source’s ratio is undefined, on a zero volume (the ease of movement and the Livermore flow) or a zero range (the ease of movement). CumSum is a compensated running sum: it equals the vendors’ plain running sums to rounding, bit for bit when every partial sum is an integer below 2^53, as on Sarkett’s and Goldstein and Kahn’s tables. Lengths are in bars, and series are paired by position, so the caller aligns them by timestamp first.
Force index and weighted on-balance volume (Elder; Goldstein and Kahn)
John A. Sarkett (“The Force Index”, Stocks & Commodities V13:4, April 1995, pp. 180-183) gives Elder’s “Force index = Volume today (Close today - Close yesterday)” and its two smoothings: a two-day EMA “helps pinpoint entry and exit points” and a 13-day EMA “shows major change in the force of bulls and bears”. Steven B. Goldstein and Michael N. Kahn (“Money flow analysis”, Stocks & Commodities V6:2, February 1988, pp. 79-81) build the same product into on-balance volume: “The volume for each day is weighted by (multiplied by) the amount the stock is up or down for the day.” t6ForceIndex smooths the unmasked product with XAverageAlpha, whose first-value seed on bar 0’s product of 0 is the zero seed of Sarkett’s table, masks bar 0 after smoothing, and sums the product with CumSum for the weighted line.
raw := c.Delta(1).Mul(v)
fi := raw.SetN(1, math.NaN())
fs := raw.XAverageAlpha(2/float64(short+1)).SetN(1, math.NaN())
fl := raw.XAverageAlpha(2/float64(long+1)).SetN(1, math.NaN())
wobv := raw.CumSum()- Inputs: closes
cand volumesv, the force index being in price units times volume units;shortandlong(2 and 13), smoothed with alpha 2/(short + 1) and 2/(long + 1), 0.66666666666666663 and 0.14285714285714285 in binary. - First valid index: 1 for
fi,fsandfl, NaN on bar 0, whererawisDelta’s 0 times the volume and nothing has moved; 0 forwobv, which is 0 there (bar 0’s volume is not read). The state before bar 1 israw[0], 0, sofs[1]is two-thirds of the first force index, -260710 on Sarkett’s table (not printed). Unmasked, bar 0’s 0 counts as a bar in the first windows: Maddox’s first full 10-bar average would hold nine products. - Anchors: on Sarkett’s Nikkei table (18 rows, 29 October to 22 November)
fiis the printed force index exactly on all 17 bars, andfsis within 1 of the 15 legible two-day EMA cells (at most 0.93 away; his rounding rule is unknown), the 2 November row’s -7161902 being a misprint for about -716101 (computed -716101.48); his “13-EMA” column is the 13-day EMA of the two-day EMA, the unmasked chainc.Delta(1).Mul(v).XAverageAlpha(2.0/3).XAverageAlpha(1.0/7), which rounds to his five printed values (-338231, -261590, -256796, -314660, -180921 on bars 13 to 17), whilefl, the 13-day EMA of the force index itself, reads -372157.58723294723 on the 14th row (bar 13) and is not that column. On Goldstein and Kahn’s Figures 1 and 2 (bar 0 the prior close, 15 3/8, then 4 to 8 May)wobvreads 0, 1250, 2500, -4250, -2375, -3625 andc.OBV(c, v)0, 10000, 20000, 11000, 26000, 16000, bit for bit; their Figure 2 prints two volumes as “15.000” and “10.000”, European separators for 15,000 and 10,000. Sarkett’s “programmer’s trick”, to “divide the force index by the price of the underlying security”, gives with yesterday’s closefi.Div(c.Shift(1)), the volume-price trend’s increment to rounding (5 of Sarkett’s 18 bars differ in bits). - Traps: a masked EMA cannot be re-smoothed:
fs.XAverageAlpha(a)is NaN on every bar, becauseXAverageAlphacarries bar 0’s NaN seed for ever, so smoothrawand mask after (the same holds for any NaN-masked line).raw.XAverage(n), the dossier’sfi.XAverage(n), seeds with a simple average that includes bar 0’s zero, which is no vendor’s seed (TradingView starts from the first force index, TA-Lib from the average of the first n): on Sarkett’s tableraw.XAverage(13)reads -454411 (rounded) on bar 12, where his averages start from a state of 0. A NaN close or volume inside the history makes fs and fl NaN for good (XAverageAlphadoes not recover), while fi is NaN on the bar of a missing volume only, and on two bars for a missing close. - Variant: Elder’s “Buy when a two-day EMA of the force index turns negative during uptrends”, with the uptrend a rising 13-day EMA of the close:
c.XAverage(13).Delta(1).GT(0).SetN(13, 0).Mul(fs.CrossesBelowS(0))(0 before bar 13:XAveragepasses the close through to bar 11 and seeds at bar 12); his order is a buy stop above that day’s high. - Variant: relatives of the force index: Katsanos’s “TSV should be near the top of its 40-day range” (“Fundamental, Technical, Or Both”, Stocks & Commodities V31:13, December 2013) with
fi.Sum(22)in place of Worden’s unpublished TSV,fi.Sum(22).ChannelPosition(40, 1).SetN(61, math.NaN())(first valid index 22 + 40 - 1 = 61;ChannelPositionreads 50 on a flat window, where his+ .001guard gives 0); Maddox’s 10-day momentum (Stocks & Commodities V9:4, April 1991),fi.Average(10).SetN(10, math.NaN())(first valid index 10); Macek’s market volume impact (Stocks & Commodities V11:4, April 1993), which drops the days below 170 million NYSE shares, a threshold of his period, withkeep := v.GT(thr).SetN(1, 0)andfi.AverageWhere(keep, 10), itsAverageWhere(keep, 5)and that line’sAverageWhere(keep, 10). - Evidence: Sarkett tells his trades on charts: on 20 September 1994 SLE “declined enough to turn the force index negative (-2051, to be exact)”, and the buy at 22.75 on 30 September made “a 10% gain in six weeks” by 15 November; a GPS short at 37.50 dollars on 14 September met a low of 30.125 on 27 September. Katsanos’s 2013 scan (several conditions together on four dates, 103 trades) made a mean 4.12% after one week and 0.50% after two months (1.90% and -0.57% over the SPXEW), so the TSV’s own part is not isolated; Maddox’s, Macek’s and Barics’s figures are charts or outputs without inputs. Evidence, not a reference.
- Pinned by
TestRecipeT6ForceIndex; runnable exampleExampleSeries_XAverageAlpha_forceIndex.
Ease of movement (Arms)
Richard W. Arms Jr. (“Ease of movement”, Stocks & Commodities V8:5, May 1990, pp. 187-190) divides the midpoint move, “the difference between one day’s median price and the next day’s median price”, by the box ratio, “the volume number divided by the range number”, so that “the easier the movement, the lower the box ratio”: “EMV = Midpoint Move / Box Ratio”. He smooths over 13 days, “a 13-day moving average” in his text and a 13-day sum in his Figure 3. t6EaseOfMovement divides in his MPM/BR order, the volume over scale over the range, and returns the one-bar value with its n-bar sum and simple average.
raw := h.Add(l).Div(2).Delta(1).Div(v.Div(scale).Div(h.Sub(l)))
emv := raw.SetN(1, math.NaN())
sum := raw.Sum(n).SetN(n, math.NaN())
avg := raw.Average(n).SetN(n, math.NaN())- Inputs: highs, lows and volumes;
scaledivides the volume: 10,000 at TA-Lib, Tulip and TradingView’s built-in, 100,000,000 at StockCharts and pandas-ta; Arms’s own units are the range in eighths and a “volume number” whose unit his text does not state,scale= 8u for a unit of u shares; n is 13 (Arms) or 14 (the vendors). The scale multiplies every value, so signs and crossings do not depend on it. - First valid index: 1 for
emv(Arms’s day 1 has no midpoint move; bar 0 ofrawisDelta’s 0) and n forsumandavg(bar 13, Arms’s day 14, at n = 13), NaN before. A zero range or a zero volume reads +0 byDiv’s rule, which is TA-Lib’s (“A bar with no range or no volume has no box ratio and contributes 0”), where Arms’s ratio is undefined. Unmasked,sumat bar n - 1 holds bar 0’s 0 and n - 1 values andavgpassesrawthrough on bars 0 to n - 2; maskingrawfirst costssumnothing (it recovers at bar n) but makes an EMA of it NaN for good, so the block smoothsrawand masks after. - Anchors: on Arms’s Figure 3 (days 2 to 45, rebuilt on bars of range 1 with his box ratio as the volume, scale 1) the BR order reproduces 42 of the 44 printed EMV cells at 2 decimals: day 12 prints 1.89 where 2.50/1.33 gives 1.88 (an unrounded box ratio) and day 38 prints -1.00 for +1.00 (a sign misprint). His “13-day” column, which the caption calls “smoothing these values using a 13-day moving average”, is a running 13-day sum of the printed EMVs plus 1.01 (days 14 to 30) or 1.13 (days 31 to 45), a sum, not an average (pinned by
TestRecipeT6Insync, the insync composite’s test on the Comparison page); the BR and Mul orders are bit-identical on his data. On Meibuhr’s worksheet (Stuart G. Meibuhr, “Spreadsheets - a universal technical analysis program?”, Stocks & Commodities V4:6, 1986, pp. 233-236, Figure 8: Avon Products, 21 November to 13 December 1985, 16 rows), with the box widths implied by his printed box ratios as the volume (1, 1, 1, 1, 1, 1, 1, 3, 1, 2, 2, 1, 1, 1, 1, 1) and scale 8, minusemvreproduces his 15 printed EMV cells at 3 decimals (0.125 on 22 November, 0.938 on 12 December) and minussumhis three printed 13-day sums exactly, -3.40625, -2.59375 and -2.84375 (printed -3.406, -2.594, -2.844): his EMV is positive when the midpoint fell, and his “13D SUM” heading is a second witness that the 13-day value was a sum. His 21 November close, 27.375, is below that day’s low of 27.5; the EMV reads only highs and lows.avgandsumhave the same sign on every bar (avgdivides the same compensated window sum by n), so Arms’s average and his printed sum cross the centre line together. - Traps: the dossier’s Mul order
h.Add(l).Div(2).Delta(1).Mul(h.Sub(l)).Div(v.Div(scale))differs from the BR order by up to 2 ulp and gives -0 on a flat bar whose midpoint fell, where the BR order (Arms’s, Tulip’s and TA-Lib’s) gives +0; the insync composite’s EMV on the Comparison page,h.Add(l).Mul(0.5).Delta(1).Mul(h.Sub(l)).Div(v), is that order unscaled, bit-identical on Arms’s own data and within 2 ulp elsewhere. Arms’s printed equation reads “MPM = (TH + TL)/2 - (YH - YL)/2”, a minus where his sentence (“the high plus the low, divided by 2”) and his worked example (“33 minus 31-1/2 or 2-1/2”) need a plus, and his EMV “higher if a stock moves a given amount on light volume than if it moves the same amount on low volume” means heavy volume. Ascaleof 0 is not rejected:v.Div(0)reads 0, so every value reads 0 without a warning. - Variant: David Hawkins’s “35-day EMA of the EOM crossing zero in the negative direction” (“Find Those Market Tops”, Stocks & Commodities V21:7, July 2003, pp. 48-52), with m = 35 on the Nasdaq Composite and 25 on the S&P 500 and
rawthe block’s unmasked line:h.SetN(len(h), 0).Add(raw.Window(-1).XAverage(m)).SetN(m, math.NaN())and its zero crossCrossesBelowS(0).Window(-1)keeps bar 0’s 0 out of the seed, the mean of bars 1 to m, so the first valid index is m;XAveragefuses into a multiply-add on arm64, so the line agrees across architectures to rounding. - Notes: Hawkins’s signal comes “after a TO divergence has been in effect for a while”, a judgement on Chande’s thrust oscillator over breadth data, and is not composed.
- Evidence: Arms prints no performance numbers: at the points he marks B and D the 13-day line “moved decisively across the center line, suggesting a buy”, and “Both trades would have led to good profits”; his charts plot a normalised line whose normalisation he does not publish. Hawkins’s Nasdaq and S&P 500 charts (2000 to 2003) carry legend readings with an unstated divisor and seed, and Morris’s T-bond chart and North’s insync tables rest on unpublished data. Evidence, not a reference.
- Pinned by
TestRecipeT6EaseOfMovement; runnable exampleExampleSeries_Delta_easeOfMovement.
Livermore money flow (Fosback)
Norman G. Fosback (Stock Market Logic, 1976, 1992 printing, ch. 57 “On-Balance Volume, Tick Volume, and Money Flows”, pp. 219-225) gives a money flow “originally popularized by Jesse Livermore”, which “holds that the less volume required to produce a given price change, the more significant is the reading”: “simply divide each day’s price change by share volume to determine the amount of price change per unit of volume. Then accumulate the daily quotients.” To compare stocks he would go further, “substituting percentage price changes for dollar price changes”. t6LivermoreMoneyFlow returns the running line, its n-bar sum and the percentage form, and Div reads a zero volume, where his quotient is undefined, as 0.
inc := c.Delta(1).Div(v)
lmf := inc.CumSum()
roll := inc.Sum(n).SetN(n, math.NaN())
pct := c.DeltaPercent(1).Div(v).CumSum()- Inputs: closes and volumes in the caller’s unit (Table 56 counts thousands of shares; in shares the line is 1/1000 of it) and n bars for
roll;pctis in percent per unit of volume. - First valid index: 0 for
lmfandpct(0 on bar 0, his Friday’s cumulative 0) and n forroll, NaN before (unmasked,Sum’s startup reads partial windows from bar 0’s 0). A NaN close or volume makeslmfandpctNaN from its bar on, whilerollrecovers n bars later. - Anchors: on Table 56 (p. 223; four rows, volume in thousands of shares)
lmfat 2 decimals is 0, 0.10, 0.30, -0.30, his daily +0.10, +0.20 and -0.60 accumulated (in binary 0.10000000000000001, 0.30000000000000004 and -0.29999999999999993: tenths are not exact in binary). His Tables 54 and 55 and his “accumulation of 15,000 shares (10,000 + 10,000 - 5,000)”, printed without a sign, areOBV’s 0, 10000, 6000; 0, 4000, -6000; and 0, 10, 20, 15 thousand on Table 56’s data. On Tables 54 and 55 the Livermore line ends at -0.30 per thousand shares, bearish as he says of Table 54 (“Livermore would maintain that the situation was bearish”), and at +0.30, “considered bullish”. - Traps:
rollequalslmf.Sub(lmf.Shift(n))only to rounding, and the rounding grows with the running total (1e-15 on the 700-bar test path, of the order of 1e-11 on a 5,000-bar history): a small window against a large total. The percentage form usesDeltaPercent(1), (c - c1)/c1*100, within an ulp of the exact percentage, whereRateOfChange(1)computes (c/c1 - 1)*100, which cancels (6e-14 relative on the 700-bar test path, 3.6e-11 on a 20,000-bar path of cent moves at a high price); the dossier’sc.RateOfChange(1).Div(v)is that percentage form, not Table 56’s dollar form. Fosback’s “money flow” is neither the Money Flow Index (MFI) nor Chaikin’s money flow (the Accumulation / Distribution compositions). - Variant: the percentage refinement over a window,
c.DeltaPercent(1).Div(v).Sum(n).SetN(n, math.NaN()), start-invariant likeroll; his other scaling, to “express each current reading in terms of the average readings in the days, weeks, or months which preceded it”, names no window or statistic and is not composed. - Variant: a zero volume read as missing rather than 0,
inc.MaskNonFinite(v.GT(0).Log())beforeSum(n): NaN on the n windows that hold the bar (beforeCumSumit would leave every later bar NaN). - Evidence: Fosback (p. 225): “Unfortunately none of these methods have been rigorously tested in a scientific manner, and it is impossible to state definitely how reliable they are”; his three tables are worked examples of two and three days. Evidence, not a reference.
- Pinned by
TestRecipeT6LivermoreMoneyFlow; runnable exampleExampleSeries_DeltaPercent_livermoreMoneyFlow.
Volume measured against its own recent level: the day’s volume over its moving average, as a ratio, a percentage or a z-score, a forecast from a rolling median, and oscillators of a short average against a long one. Volume is right-skewed (Kissell “found the ADV to be higher than the actual volume on the day 65% of the time”), so a spike lifts the mean and the deviation it is measured against, and the median and log forms are the robust ones. Recipe values are NaN before their first valid index (SetN(F, math.NaN())), and flags and books built on masked values read 0 there. Prices are positive and finite and volumes finite and non-negative; Div reads 0 where an average or a forecast is 0, as after a run of zero volumes, and each section says where that matters. All lengths are in bars, and series are paired by position, so the caller aligns closes and volumes by timestamp first.
Relative volume (Waxenberg; Star; Dickover; Kott)
Howard K. Waxenberg (“Technical analysis of volume”, Stocks & Commodities V4:2, 1986, pp. 65-68) finds normal volume “by making a 10-day moving average of the daily volume figures” and wants a breakout’s volume “to increase by at least 20 percent over its 10-day moving average”, a selling climax’s “in the order of 40 percent”. The MetaStock sidebar to Barbara Star’s “Volume Variations” (Stocks & Commodities V11:5, May 1993, pp. 215-219) codes his “Normal volume indicator” as MOV(V,10,S), a window that ends on the current bar, so the day is in its own average; Melvin E. Dickover (“Evidence-Based Support & Resistance”, Stocks & Commodities V32:5, April 2014, pp. 20-26) shows “how many standard deviations from the average volume today’s volume is” over 60 bars, and Vince Kott (“Volume Spike Analysis”, Stocks & Commodities V30:9, August 2012, pp. 36-40) marks a bar “greater than the past x volume bars”. t6RelativeVolume uses one n for the ratio, the percentage and the z-score and returns Kott’s flag beside them.
nv := v.Average(n)
rv := v.Div(nv).SetN(n-1, math.NaN())
pct := v.Sub(nv).Mul(100).Div(nv).SetN(n-1, math.NaN())
z := v.Standardise(n).SetN(n-1, math.NaN())
kott := v.GT(v.Shift(1).Highest(k)).SetN(k, 0)- Inputs: volumes in any unit (rv, pct and z are scale-free), n (10 for Waxenberg and Star in 1993, 20 for Star’s 2012 spike, 60 for Dickover) and k (20, 100 or 200 for Kott). Waxenberg’s breakout is
rv.GTE(1.2)(“at least”) and his climaxrv.GTE(1.4); Dickover’s spike isz.GT(2)at n = 60. A run of n zero volumes makes the average 0, where rv, pct and z read 0. - First valid index: n - 1 for rv, pct and z, NaN before; k for kott, 0 before. Unmasked,
Averagepasses the volume through on bars 0 to n - 2, so the ratio reads exactly 1 there (0 on a zero volume), a normal day where there is no average yet; before bar k Kott’s window still holdsShift(1)’s 0 pad, so bar 0 fires on any positive volume and later bars mark the largest volume so far. Dickover’s listing draws fromBar>period, bar 61 if its bars count from 0, two bars after the first full window. - Anchors: at exactly +20%, +40% and +50% (volumes of 120, 140 and 150 against an average of exactly 100) the ratio reads 1.2, 1.4 and 1.5, so thresholds on it are exact, and pct reads exactly 20, 40 and 50; with the day in the average rv is at most n, reached when the n - 1 earlier volumes are 0;
v.Average(1)is v, so the spike of Star’s TMV (“Trade Breakouts And Retracements With TMV”, Stocks & Commodities V30:2, February 2012, pp. 12-21),OscV(1,20,S,%)>50, ispct.GT(50)at n = 20, and pct equals the volume oscillator’s vop (last section) with fast 1 and slow n bit for bit, both in Tulip’s and TradingView’s order,Sub.Mul(100).Div; z equals the population z-score (v - mean)/(population deviation) written directly, to rounding, and times sqrt((n - 1)/n) it is the sample-deviation form, also to rounding (Dickover’s OmniLanguagestddoes not state its divisor); from bar k Kott’s flag differs fromv.EQ(v.Highest(k + 1))only on ties, a volume equal to the highest of the k bars before it. On finite volumeskottequalsv.Breakout(k, 1).GT(0)bit for bit on every bar (Breakoutreads +0 before bar k). - Traps: the percentage taken from the ratio,
rv.Sub(1).Mul(100), reads 19.999999999999996 at +20% and 39.99999999999999 at +40%, soGTE(20)andGTE(40)fail exactly where Waxenberg’s rule holds: threshold rv or pct; the orderv.Sub(nv).Div(nv).Mul(100)differs from pct in the last bits on about a third of the bars, so it is not vop; Star’s 2012 prose and the Wealth-Lab translation subtract the other way round, and the AIQ and TradersStudio translations test the absolute value, which also fires on a day with less than half the normal volume.zis not bitwise start-invariant (Standardise’s rolling variance depends on the history before the window), while rv and pct are. - Variant: the day out of the average, as Kott’s “past” bars and Kissell’s and Connors’s “previous” days read it,
v.Div(v.Average(n).Shift(1)).SetN(n, math.NaN()), valid from n. - Variant: Connors and Alvarez’s highest-volume day of the week,
v.EQ(v.Highest(5)).SetN(4, 0)(ties counted), split by the close withc.GT(c.Shift(1))andc.LT(c.Shift(1))(an unchanged close is in neither, as their day counts show). - Notes: Connors’s double-volume market top (Connors On Advanced Trading Strategies, 1998, ch. 13, pp. 103-108, after Tubbs) needs a stock “trading near or at a three-month calendar-day high” on volume “double its 15-day average volume”: an anecdotal rule whose “near” has no tolerance and whose months need calendar keys. Mike Slattery’s VPP (“An Early Warning System (Part 3)”, Stocks & Commodities V35:11, October 2017, pp. 30-35) measures volume against a Laguerre filter whose length and gamma are not published, so it is not written here.
- Evidence: Waxenberg, Star (1993 and 2012), Kott and Dickover print charts and thresholds, with no values and no test results. Connors and Alvarez’s Tables 6.1 and 6.2 (How Markets Really Work, 2nd ed., Wiley, 2012, ch. 6; the SPX from January 1989 and the NDX from September 1996, both to September 2011; costs not stated) buy on the week’s highest-volume day and sell the next day: the SPX gained 0.03% on 1,264 days against a benchmark of 0.03%, the NDX lost 0.01% on 838 days against 0.05%, and they conclude “Is volume important? Based upon 22+ years of data, it is not.” Evidence, not a reference.
- Pinned by
TestRecipeT6RelativeVolume; runnable exampleExampleSeries_Average_relativeVolume.
Volume-gated moving-average reversal (Saitta)
Alex Saitta (“A Price- And Volume-Based System”, Stocks & Commodities V14:3, March 1996, pp. 103-105) goes long “when the T-bond crossed and then closed above its 20-day moving average”, liquidates “when the market closed below the average” and goes short on the cross below, a system “always in the market”; his second system enters only when “that day’s trading volume was greater than the average daily volume of the last 10 days”. t6VolumeGatedReversal keeps the day in its 10-day average, as Waxenberg’s normal volume does and as a reading of the plotted average in his Figure 3 supports, and books both systems with PositionSigned: a cross against the open position reverses it, a close on the other side of the average exits it, and a gated-out cross leaves the position to the exit test.
gate := v.GT(v.Average(m)).SetN(m-1, 0)
ma := c.Average(n).SetN(n-1, math.NaN())
up := c.CrossesAboveS(ma)
dn := c.CrossesBelowS(ma)
base := c.PositionSigned(up, c.LT(ma), dn, c.GT(ma))
gated := c.PositionSigned(up.Mul(gate), c.LT(ma), dn.Mul(gate), c.GT(ma))- Inputs: closes and volumes of one length, n = 20 and m = 10. The books are the position after each bar’s close, his fill by the wording; pass the flags with
Shift(1)for a fill at the next open. - First valid index: m - 1 for the gate, n - 1 for
ma(NaN before), n for the crosses and both books (0 before, as a cross needs the prior bar’s average). The gate’sSetN(m-1, 0)is a no-op kept to show the mask: whereAveragepasses the volume through,v.GT(v)is 0, so the masked and unmasked gates are equal on every bar. - Anchors: both books equal a state machine of his rules on a 700-bar path, its twin with non-integer volumes and a 60-bar path with a run of zero volumes, and on the 700-bar path the gated book reverses on three bars only; the tests are strict, so a close exactly equal to its n-bar average neither crosses nor exits, and the cross then fires on the next bar that closes beyond the average, since the prior bar’s test is “at or below” (for a cross below, “at or above”); the gate with the day in equals
v.GT(v.Average(m - 1).Shift(1)), the day out over m - 1 bars, from bar m on integer volumes, since v > (S + v)/m is v > S/(m - 1) with S the sum of the m - 1 bars before. - Traps: unmasked,
c.Average(n)passes the close through on bars 0 to n - 2, where the close equals its “average”, so a cross can fire at bar n - 1, inside the warm-up; the gate is strict (“greater than”), so on a cross whose volume equals its average (both 0 inside a run of zero volumes) the base book reverses while the gated book goes flat: the cross is gated out and the close beyond the average exits the old position. - Variant: his short-entry sentence omits “crossed” (“A short position was initiated when the market closed below the 20-day moving average and that day’s volume was greater than …”); read literally, the gated book is
c.PositionSigned(up.Mul(gate), c.LT(ma), c.LT(ma).Mul(gate), c.GT(ma)), which goes short on any heavy-volume close below the average, while his summary (“closes through the moving average”) supports the cross. - Evidence: his Figures 1 and 4 (a continuous T-bond contract from the start of 1988 to the end of 1995, one contract, “no commission or slippage”): the base system made 12,504 dollars on 206 trades with 26% winners, the gated one 35,472 dollars on 125 trades with 33% winners and a largest drawdown of 8,625 against 12,812 dollars. The trade counts are the printed winners plus losers, 35,472 dollars over 125 trades is 283.78 a trade against his printed 282 and 281, and the test covers one market and one period with no out-of-sample check. Evidence, not a reference.
- Pinned by
TestRecipeT6VolumeGatedReversal.
Median volume forecast (Kissell)
Robert Kissell (Algorithmic Trading Methods, 2nd ed., Academic Press, 2021, ch. 12, “Forecasting Daily Volumes”, pp. 309-322) forecasts a day’s volume as the moving median daily volume, “MDV computed using previous n -trading days”, times a day-of-week factor, plus beta times e(t - 1), “the previous day’s volume forecast error (actual minus estimate)”, the estimate there using the day-of-week factor only; his final models take MDV(10) with beta 0.409 for large caps and 0.237 for small caps. t6MedianVolumeForecast sets the factor to 1, shifts Quantile(n, 0.5) one bar so that its window ends on the previous day, and returns the median, the forecast and the volume over the forecast.
mdv := v.Quantile(n, 0.5).Shift(1)
vhat := mdv.Add(v.Sub(mdv).Shift(1).Mul(beta)).SetN(n+1, math.NaN())
rv := v.Div(vhat).SetN(n+1, math.NaN())
med := mdv.SetN(n, math.NaN())- Inputs: volumes (med and vhat are in their unit, rv is scale-free), n = 10 and beta 0.409 (large caps) or 0.237 (small caps). The outputs are med, vhat and rv, in that order.
- First valid index: n for med, n + 1 for vhat and rv, NaN before.
Quantilereads 0 on bars 0 to n - 2, so the shifted median is 0 on bars 0 to n - 1, and unmasked the forecast at bar n would add beta times the whole of the previous day’s volume as its error. - Anchors:
Quantile(n, 0.5)equals the median of the sorted window, the mean of the two middle values for even n, bit for bit on integer volumes (on other values an even n can differ by an ulp, asQuantile’s documentation says). - Traps: the forecast can be 0 or negative: after a run of low volumes the previous error is negative, and where beta times it outweighs the median, rv reads 0 (a zero forecast, by
Div’s rule), a negative ratio, or -0 on a zero-volume day. A NaN volume makes med NaN on the n bars after it, vhat on n + 1 and rv on n + 2 (its own bar too). - Variant: the log ratio
v.LogRatio(vhat), ln(V/vhat), the form of his step-1 error: it is NaN wherever the volume or the forecast is not positive (and on the masked bars), so the bars on which rv misleads drop out. - Variant: the causal form of Mike Siroky’s median ratio (“Volume-Volatility Relationship”, Stocks & Commodities V37:2, February 2019, pp. 16-20 and 41),
v.Div(v.Quantile(n, 0.5)).SetN(n-1, math.NaN()), valid from n - 1, the day in its own median: his volume is “a multiple of the median volume for the entire series”, which looks ahead. - Notes: his DayOfWeek is the “percentage of weekly volume that typically trades on the given weekday”, a ratio to the week’s mean in his Figure 12.3 (five values near 100% summing to about 500%), estimated over a year without FOMC, triple-witching, index-reconstitution, earnings and month-end days: those calendar keys are the caller’s, and a factor series dow replaces the median by
mdv.Mul(dow)in the forecast and in the error. His regression of the error on its lag has an intercept (“a constant term is needed”) that the printed final models omit, and the footnote of his Table 12.3 lags the volume, not the error. - Evidence: his Tables 12.2 and 12.3 (S&P 500 and Russell 2000 stocks, January 2017 to December 2018): the lag term cut the correlation of successive errors from 0.412 to -0.012 for large caps and from 0.241 to -0.006 for small caps, and the forecast error, the average per-stock standard deviation of ln(V/vhat), from 43.0% with a 30-day average daily volume to 33.0% for large caps and from 53.6% to 41.0% for small caps. Beta and the 10-day look-back were fitted on the same data, and these are forecast errors, not trading returns, so costs do not enter. Evidence, not a reference.
- Pinned by
TestRecipeT6MedianVolumeForecast; runnable exampleExampleSeries_Quantile_volumeForecast.
Volume oscillators (Pring; Stokes)
Martin Pring (“The Volume Oscillator”, Stocks & Commodities V11:1, January 1993, pp. 27-29) builds “two moving averages, the shorter being divided by the longer”, using “a 10 day divided by a 25-day moving average” on daily charts. Martha Stokes (“Volume, The Forgotten Oscillator”, Stocks & Commodities V28:8, July 2010, pp. 40-45) “calculates the percentage of deviation from an exponential moving average (EMA)”, with “the short-term EMA … set at 12 periods and the long term … set at a 28-period EMA”, and smooths it with “The 25-day EMA”. t6VolumeOscillator returns Pring’s ratio and its percentage on simple averages, the percentage volume oscillator on MACD’s exponential averages in pandas-ta’s order, and its signal, smoothed on the valid part of the oscillator and aligned back to the bars.
fa := v.Average(fast)
sl := v.Average(slow)
vo := fa.Div(sl).SetN(slow-1, math.NaN())
vop := fa.Sub(sl).Mul(100).Div(sl).SetN(slow-1, math.NaN())
raw := v.MACD(fast, slow).Mul(100).Div(v.XAverage(slow))
pvo := raw.SetN(slow-1, math.NaN())
sg := raw.Window(-(slow - 1)).XAverage(sig)
sgl := raw.Mul(0).Add(sg).SetN(len(v)-len(sg)+sig-1, math.NaN())- Inputs: volumes (vo is a ratio with equilibrium 1; vop, pvo and sgl are percentages, scale-free); fast, slow and sig: Pring 10 and 25 (sig is not his), Stokes 12, 28 and 25, and pandas-ta’s defaults 12, 26 and 9, which are not Stokes’s. At Stokes’s lengths vo and vop are simple-average forms at 12 and 28, not Pring’s 10/25.
Divreads 0 where the slow simple average is 0, after slow zero volumes in a row (Tulip and pandas-ta give NaN or an infinity there). - First valid index: slow - 1 for vo, vop and pvo, slow + sig - 2 for sgl (27 and 51 at 12, 28 and 25), NaN before; an input shorter than slow is NaN on every bar. Unmasked, bars 0 to fast - 2 read 1 (vo) and 0 (vop and the oscillator), since both averages pass the volume through, and bars fast - 1 to slow - 2 compare the fast average with the volume itself, not an oscillator.
- Anchors: vop equals a loop of Tulip’s
100.0 * (savg - lavg) / lavgbit for bit; the oscillator is pandas-ta’sscalar * (fastma - slowma) / slowma, the orderMul(100).Div, and TA-Lib’s order,Div(...).Mul(100), differs from it in the last bits on some bars;v.MACD(fast, slow)equalsv.XAverage(fast).Sub(v.XAverage(slow))bit for bit;raw.Window(-(slow - 1))equals the sliceraw[slow-1:]bit for bit where the slice exists, and on an input shorter than slow - 1 bars the slice panics while the helper returns NaN on every bar.XAverage’s recursion fuses into a multiply-add on arm64, so pvo and sgl differ between arm64 and amd64 in the last bits; vo and vop do not. - Traps: the masked
pvo.XAverage(sig)is NaN on every bar, since the NaN in its seed is carried, so the signal smooths the valid part,raw.Window(-(slow - 1)), and is aligned back withraw.Mul(0).Add(sg); the oscillator of two exponential averages of non-negative volumes cannot go below -100, but Stokes’s Figure 1 plots it at about -130: that chart is not this formula and fits her phrase “with a time series method” (a least-squares average can be negative). pvo and sgl are recursive, so their values depend on where the history starts (the seeds’ weight decays bar by bar), while vo and vop are window-local. - Variant: Bruce R. Faber’s difference in shares (“The Volume Indicators”, Stocks & Commodities V14:5, May 1996, pp. 204-206),
fa.Sub(v.Average(50)).SetN(49, math.NaN())at fast 10; Pring’s smoothed volume rate of change on monthly volumes, “a nine-month rate of change … smoothed by a six-month moving average”,v.RateOfChange(9).Average(6).SetN(14, math.NaN()); and Markos Katsanos’s 5/21 gate above 25 (“Comparing Seven Money Flow Indicators”, Stocks & Commodities V29:8, July 2011, pp. 15-26), read as a percentage,vop.GT(25)at fast 5 and slow 21, which unmasked fires on warm-up bars. - Variant: pandas-ta’s signal, seeded with the first valid value,
raw.Window(-(slow - 1)).XAverageAlpha(2.0 / 26)at sig 25, valid from its first element (bar slow - 1) and aligned back as sgl is; and TSSB’s VOLUME MOMENTUM, a reconstruction from the pybroker and quant-feature-toolkit translations, not from Masters’s text (the TSSB manual, Aronson and Masters’s Statistically Sound Machine Learning for Algorithmic Trading of Financial Instruments, PDF p. 155, prints no formula):x := v.Average(n).Div(v.Average(n * k)).Log().SetN(n*k-1, math.NaN())squashed asx.Mul(g / math.Sqrt2).Erf().Mul(50)with g = 3/cbrt(k), valid from bar nk - 1, and -50 where a mean is 0, where the translations write 0. - Notes: Star and Waxenberg’s up-minus-down volume momentum,
MOV(ROC(C,12,$),10,S)on the daily spread of up and down volume, needs breadth inputs and is a breadth composition, not a volume oscillator: the Arms index page under “Breadth” writes it asuv.Sub(dv).Momentum(12).Average(10).SetN(21, math.NaN()). Joseph Barics’s %VLCH (“Bi signal indicator”, Stocks & Commodities V7:10, October 1989, pp. 375-377) has three readings, the one-bar change of volume, the volume over its start-to-present mean, and that ratio less 1, which his article does not decide. - Evidence: the chart scales: Pring plots his 10/25 oscillator on a zero-centred axis from -50 to 50, a percentage where his text describes a ratio; Stokes’s Figure 1 runs from about -135 to +220; Faber’s Figure 6 is scaled -50 to 100, a percentage where his text describes a difference in shares. Katsanos’s volume oscillator in his divergence test (30 stocks, 31 October 2000 to 31 October 2010, a 9-dollar commission) made 15.0% a year compounded with a profit factor of 1.65 (buy and hold 6.4%), and his direction-test parameters were optimised on the same data (“I decided to optimize all parameters except the indicator period and holding time”); Perry J. Kaufman (“Can Volume Predict Price?”, Stocks & Commodities V41:3, March 2023, pp. 16-20) finds that “a moving average of volume does not improve results”. Evidence, not a reference.
- Pinned by
TestRecipeT6VolumeOscillator; runnable exampleExampleSeries_MACD_percentageVolumeOscillator.
Volume Profile
VolumeProfileHistogram
func (s Series) VolumeProfileHistogram(h, l, c, v Series, n int, bins ...int) (mins Series, widths Series, hist [][]float64)
VolumeProfileHistogram exposes the per-bin volume histogram for each rolling window of length n. It returns three aligned outputs of length len(c): - mins[i]: the minimum price (window low) for window [i-n+1, i], or 0 for i < n-1 - widths[i]: the bin width for that window (may be 0 for degenerate ranges), or 0 for i < n-1 - hist[i]: the per-bin volumes slice of length k for that window; nil for i < n-1 or invalid inputs
Parameters are identical to the other Volume Profile functions. The optional bins parameter selects the number of bins explicitly (must be >0). When omitted or non-positive, the bin count is instead inferred from each window’s price range and tick granularity, capped at 40; it is not simply a fixed default of 40. Each bar’s volume is allocated to bins using exact interval overlap between the bar’s high-low range and each bin’s boundaries, with no epsilon tolerance at bin edges; a bar with zero range (a point bar) allocates its full volume to the single bin containing its close.
VolumeProfileHVNCount
func (s Series) VolumeProfileHVNCount(h, l, c, v Series, n int, bins ...int) Series
VolumeProfileHVNCount computes, for each rolling window of length n, the number of high-volume bins (HVNs) in the volume profile histogram.
Definition: a bin is considered a high-volume node if its volume is a strict local maximum relative to its immediate neighbours; edge bins are compared to their single neighbour. Tied highs are not counted.
Parameters mirror the other Volume Profile functions. If bins is omitted or <= 0, the number of bins is auto-inferred per window and clamped to 40.
Returns a Series aligned to the input length; entries before the first full window (i < n-1) are zero.
VolumeProfileIsAtHVN
func (s Series) VolumeProfileIsAtHVN(h, l, c, v Series, n int, price Series, bins ...int) Series
VolumeProfileIsAtHVN returns a binary Series indicating whether a provided price Series lies at a High-Volume Node (HVN) for each rolling window of length n. For index i, it builds the volume profile over [i-n+1, i], finds the bin containing price[i], and sets result[i]=1 if that bin is a strict local maximum in the histogram (edge bins compare to their single neighbour). Ties are not counted. Indices i < n-1 yield 0.
If bins is omitted or <= 0, the number of bins is auto-inferred per window (default/clamped to 40). If inputs are invalid or the window is degenerate, the result is 0 at that index. A price[i] strictly outside the window’s actual support (below its low or above its high) also yields 0, rather than being clamped into the nearest bin; a price exactly at the window’s high remains in-support.
VolumeProfileIsAtLVN
func (s Series) VolumeProfileIsAtLVN(h, l, c, v Series, n int, price Series, bins ...int) Series
VolumeProfileIsAtLVN returns a binary Series indicating whether a provided price Series lies at a Low-Volume Node (LVN) for each rolling window of length n. For index i, it builds the volume profile over [i-n+1, i], finds the bin containing price[i], and sets result[i]=1 if that bin is a strict local minimum in the histogram (edge bins compare to their single neighbour). Ties are not counted. Indices i < n-1 yield 0.
If bins is omitted or <= 0, the number of bins is auto-inferred per window (default/clamped to 40). If inputs are invalid or the window is degenerate, the result is 0 at that index. A price[i] strictly outside the window’s actual support (below its low or above its high) also yields 0, rather than being clamped into the nearest bin; a price exactly at the window’s high remains in-support.
VolumeProfilePOC
func (s Series) VolumeProfilePOC(h, l, c, v Series, n int, bins ...int) Series
VolumeProfilePOC computes the rolling Point of Control (price at the highest volume bin) from a Volume Profile built over the last n bars, using H/L/C and distributing each bar’s volume evenly across its price range.
Parameters: - h, l, c, v: High, Low, Close, Volume series (must be the same length) - n: lookback window (number of bars) - bins: optional number of price bins. If omitted or <=0, the bin count is inferred from the window using the smallest non-zero price delta (tick), clamped to a maximum of 40 bins; if the tick cannot be determined, it defaults to 40.
Returns a Series of the same length. For indices < n-1 (insufficient history), the value is 0.
VolumeProfileVAH
func (s Series) VolumeProfileVAH(h, l, c, v Series, n int, valueAreaFraction float64, bins ...int) Series
VolumeProfileVAH computes the rolling Value Area High (upper bound) price that, together with VAL, encloses approximately valueAreaFraction (conventionally 0.70) of the total volume in the profile, expanding outwards from the POC.
valueAreaFraction is a fraction, so 0.70 means 70%. It must be finite and in (0, 1], where 1 gives the whole range: 0, a negative value, NaN, an infinity or a value above 1 (including a percentage such as 70) is invalid and gives all zeros. The warm-up reads 0, not NaN: bars 0 to n-2 are 0, a price level far from any price.
VolumeProfileVAL
func (s Series) VolumeProfileVAL(h, l, c, v Series, n int, valueAreaFraction float64, bins ...int) Series
VolumeProfileVAL computes the rolling Value Area Low (lower bound) price that, together with VAH, encloses approximately valueAreaFraction (conventionally 0.70) of the total volume in the profile, expanding outwards from the POC.
valueAreaFraction is a fraction, so 0.70 means 70%. It must be finite and in (0, 1], where 1 gives the whole range: 0, a negative value, NaN, an infinity or a value above 1 (including a percentage such as 70) is invalid and gives all zeros. The warm-up reads 0, not NaN: bars 0 to n-2 are 0, a price level far from any price.
Breadth
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).
Compositions
The measures below turn a market’s daily advance, decline and unchanged counts into shares, ratios, oscillators and activity measures. Their inputs are the caller’s series, named, paired by position and right-aligned at the most recent bar as the Breadth introduction (“Breadth inputs”) says: advances a, declines d, unchanged u and total issues ti = a + d + u, non-negative integer counts, and the traded index x where a rule gates on the market; lengths are in bars. Values are NaN before their first valid index F (SetN(F, math.NaN())), and the flags built on them read 0 there. A share is a fraction from 0 to 1, or a percent where its section multiplies by 100, and two denominators are in use: the share of advances plus declines (STIX, Zweig) and the share of total issues (Gartley, Carlin, Connors’s PADI), which differ by the unchanged issues; the generic share and normalised-difference forms are in “Normalised difference and share” (Relative: Ratio and Relative Strength), and a breadth line read against an index is the partner case of “Relative-strength non-confirmation” (Relative: Divergence). A bar with no trading (all counts 0) is not an input bar. A threshold on one day’s ratio is written multiplied out, a.GT(d.Mul(4)) rather than a.Div(d).GT(4): exact on integer counts and true on a day with no declines, where Div and Ratio read 0 and miss the most extreme day; where a 0 would be a false extreme, an average of daily ratios masks its zero divisors with MaskNonFinite(d.Log()) (the advance/decline ratio’s guard, needed in small universes such as industry groups more than on NYSE counts). Signals act at the next bar’s open unless a section says otherwise (Connors trades on the close), and the fills are the caller’s.
STIX and the issues-traded shares (Dworkin, Morris; Gartley, Schultz, Carlin)
Fay Dworkin (“Defining Advance/Decline Indicators”, Stocks & Commodities V8:7, July 1990, pp. 274-278) writes that STIX, “one of Gerald Appel’s favorite market breadth indicators”, “uses a 21-day exponential moving average” of the Zweig format, both using “the sum of advances and declines in the denominator”; Gregory Morris (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 5) credits The Polymetric Report and prints “(A / (A + D)) 100”. Richard Carlin (“Technical analysis of industry groups”, Stocks & Commodities* V6:11, November 1988, pp. 408-410) averages"the percentage of stocks advancing over the past 10 days or the past 10 weeks", which Morris (ch. 6) writes as “(A / TI) * 100 for 10 days”, beside Gartley’s and Schultz’s shares of issues traded and the Velocity Index, “(U/2 + A) / (A + D + U)”. t6AdvanceShares gives the STIX and Carlin’s diffusion index in percent and the Velocity Index as a fraction.
stix := a.Div(a.Add(d)).Mul(100).XAverage(n).SetN(n-1, math.NaN())
carlin := a.Div(ti).Mul(100).Average(m).SetN(m-1, math.NaN())
vel := u.Mul(0.5).Add(a).Div(ti)- Inputs:
a,d,uandti; n = 21 (Dworkin, Morris) and m = 10 (Carlin; his 10 weeks are the same code on weekly counts, and his denominator is unstated: Morris reads it as total issues). The STIX zones are bands: normal 42 to 58, “somewhat overbought” 56 to 58,stix.GTE(56).Mul(stix.LTE(58)), and “somewhat oversold” 42 to 44,stix.GTE(42).Mul(stix.LTE(44))(Dworkin, Morris); neither says whether 44 and 56 are inside. - First valid index: n - 1 for
stix(20) and m - 1 forcarlin(9), NaN before; 0 forvel. Unmasked,XAverageandAveragepass the raw daily share through on the warm-up bars. - Anchors:
stix[n-1]is the plain mean of the first n percentage shares bit for bit (the simple-average seed ofXAverage); from there the recursion fuses on arm64, so the golden holdsstixwithin 2^-40 there and bit for bit at amd64 v1. On non-negative counts with a positive totalstixlies in [0, 100] andvelin [0, 1]. Zweig’s breadth thrust line ont6-breadth-thrusts-and-climaxes.mdis the same average of the share at n = 10 with the percent taken after it, sostixat n = 10 is that line times 100 to rounding, and its 0.40 and 0.615 are 40 and 61.5 on this scale. - Traps: the order is pinned: the percent taken after the average,
a.Div(a.Add(d)).XAverage(n).Mul(100), differs in the last bit on about two-thirds of the bars of random NYSE-sized counts. The dossier’sa.Div(a.Add(d)).Mul(100).XAverage(21)has no mask, so its bars 0 to 19 are raw daily shares, which can sit far outside 42 to 58, and its Gartley flaga.Div(ti).Average(7).GT(0.60)fires on one raw day above 60% among bars 0 to 5. The 60% rule is George Schade’s paraphrase (quoted in Morris, ch. 6) of Gartley’s Profits in the Stock Market (1935), which was not read. The Velocity Index’s “+10 and -12” do not fit its scale of 0 to 1, and Morris names no source for it (“I cannot find any additional information on it or who created it”): formula only. - Variant: Gartley’s and Schultz’s shares of issues traded as Schade reports them: consider selling on
a.Div(ti).Average(7).SetN(6, math.NaN()).GT(0.60), buy ond.Div(ti).Average(7).SetN(6, math.NaN()).GT(0.60)(“especially if a decline has been in progress for some time”), and Schultz’s “signal of weakness ahead”,a.Div(ti).Average(10).SetN(9, math.NaN()).LT(0.40). - Variant: Tushar Chande’s printed recursion (“Breadth STIX And Other Tricks”, Stocks & Commodities V12:5, May 1994, pp. 211-214), “STIXtoday = (0.09)(ADI index) + (0.91)(STIXyesterday)”, is
a.Div(a.Add(d)).Mul(100).XAverageAlpha(0.09): alpha 0.09 rather than 2/22, seeded with the first value, and within 0.11 STIX points ofstixafter bar 100 on the test tape. - Evidence: no source tests a rule: Dworkin and Morris give zones and charts, Chande charts 1993 (“The market was overbought in September with a STIX value above 54”), Schultz says the extremes “become obvious cues”, and Carlin writes “I do not feel the predictive power of this indicator is very high”. Evidence, not a reference.
- Pinned by
TestRecipeT6AdvanceShares.
Advance/decline ratio: the overbought/oversold index and Krow’s reversal (Fosback, Pring, Nicoski)
Norman Fosback (Stock Market Logic, Dearborn Financial Publishing, 1976, twenty-first printing 1992, ch. 37, pp. 129-131) divides “the number of advancing issues on a given day by the number of stocks which declined”, averages the ratio “over a period of about ten days”, and calls the market overbought “when the index is above 1.25” and oversold “when the ratio is below 0.75”; Harvey Krow’s system (Institutional Investor, February 1968, as Fosback reports it) “first required a ten day OB/OS Index above 1.25” and then “an Index decline of at least 0.05 in a single one day period (for example, from 1.40 to 1.35 or below)”, with buys “in precisely the opposite manner”. Martin Pring (“Internal Market Momentum”, Stocks & Commodities V11:7, July 1993, pp. 298-305) multiplies each day’s ratio by 100 before the 10-day average (Figure 1) and also divides the 10-day totals of advances and declines (Figure 3), the form of Nicoski’s ratio of 30-day averages that Dworkin reports. t6ADRatio gives Fosback’s index, Pring’s Figure 1 oscillator, the ratio of the n-bar sums, and Krow’s reversals read on the index in hundredths.
r := a.Div(d)
obos := r.Average(n).SetN(n-1, math.NaN())
pring := r.Mul(100).Average(n).SetN(n-1, math.NaN())
sumr := a.Sum(n).Div(d.Sum(n)).SetN(n-1, math.NaN())
h := obos.Mul(100).Round()
sell := h.Shift(1).GT(hi).Mul(h.Delta(1).LTE(-step))
buy := h.Shift(1).LT(lo).Mul(h.Delta(1).GTE(step))- Inputs:
aandd; n = 10;hi,loandstepin hundredths of the index, 125, 75 and 5, so that every comparison is between integers. The levels are strict as printed (“above 1.25”, “below 0.75”, though Fosback’s parenthesis “at least 25% more” would admit 1.25) and the step inclusive (“at least 0.05”). Krow’s oversold level is “below 0.75 in bull markets and below 0.50 in bear markets”:lois 75 or 50 as the caller chooses, since knowing the regime “requires us to know most of the answer before we start to ask the question” (Fosback). Nicoski’s form issumrat n = 30, bought on a cross above 1 after a reading below 0.84 and sold on a cross below 1 after one above 1.2 (Dworkin). - First valid index: n - 1 for
obos,pringandsumr(9), NaN before; unmasked,Averagepasses the daily ratio through andSumgives partial sums there. The Krow flags start at n, becauseh.Delta(1)is NaN at n - 1, and read 0 before. - Anchors: Pring’s Figure 1 (13 days, 2 to 18 January):
pringon bars 9 to 12 is 110.27565124933547, 119.20422267790688, 127.95422267790688 and 159.2042226779069 bit for bit, which round to his oscillator 110, 119, 128 and 159 and, times 10, to his totals 1103, 1192, 1280 and 1592. His Figure 3:sumr.Mul(100).Sub(100)on the same bars is -3.94736842105263, 9.859154929577471, 18.840579710144922 and 35.9375, printed -4, 10, 19 and 36. Fosback’s example as an index of 1.40 then 1.35 (n = 1) givesh140 and 135 and a sell on bar 1.Roundrounds halves away from zero: with n = 1, one advance against eight declines is 12.5 hundredths and reads 13. - Traps: the other order,
r.Average(n).Mul(100), gives 110.27565124933545 and 127.9542226779069 on bars 9 and 11. Pring’s totals 1103 and 1192 are sums of the unrounded ratios, while his printed ratio column (rounded, 63 for 62.5) sums to 1104 and 1193, so a test starts from his counts; his Figure 3 caption says “subtract the ratio from 100” where the column heading, “Subtract 100 From Ratio”, and its numbers subtract 100 from the ratio, and his “50 is often deducted” fits a share, not this ratio, which centres on 100. A day with no declines reads 0 inDiv, as in the dossier’s unmaskeda.Ratio(d).Average(10), so the strongest day enters the average as the weakest. The literal stepobos.Delta(1).LTE(-0.05)fails Fosback’s own example, 1.35 less 1.40 being -0.04999999999999982 in binary64: hence the hundredths. Krow’s rule is in Fosback only, not in Morris, and Chande’s “OB/OS” is another indicator, a 10-day exponential average of A - D. - Variant: the zero-decline guard
a.Div(d).MaskNonFinite(d.Log()).Average(n).SetN(n-1, math.NaN())keeps such a day missing (Log(0)is -Inf), at the cost of n NaN bars;sumrreads 0 only when the n-day decline total is 0. - Variant: Krow’s “first required” read as an armed state (above
hiat any time since the last drop), theHysteresisevent latchh.Delta(1).LTE(-step).Mul(h.GT(hi).Sub(h.Delta(1).LTE(-step)).Hysteresis(0, 0).GT(0).Shift(1)), which also sells on a drop that starts at or below 1.25 after an earlier reading above it. - Evidence: Fosback: “Extensive computer analysis of the index and the market reveals virtually zero predictive power”, the index staying in its zones “as short as one day and as long as several months”; Krow found “a high degree of accuracy” on 1953 to 1966, but his system “has failed to work well in ensuing years”, with “so many false signals that its accurate signals were rendered effectively useless”. Morris’s “has not tested well and is probably overrated” paraphrases Fosback. No trade counts or costs. Evidence, not a reference.
- Pinned by
TestRecipeT6ADRatio; runnable exampleExampleSeries_Div_advanceDeclineRatio.
Percent advance/decline indicator (Connors)
Laurence Connors (Connors On Advanced Trading Strategies, M. Gordon Publishing, 1998, ch. 4, pp. 23-26) divides advances and declines by “the total issues traded”, takes “the six-day average” of each, and buys when “yesterday’s declining average was 40 percent or higher and today’s declining average is less than yesterday’s declining average”, “on the close (only if the market is trading above its 100-day moving average)”, selling on the mirror rule for advances below the average. He exits “in five to seven days or if there is a signal in the opposite direction” and tests a five-day exit. t6PADI gives the two averages, the gated turns and the book pos, which holds a trade for hold bars and reverses on an opposite signal.
pa := a.Div(ti).Average(n).SetN(n-1, math.NaN())
pd := d.Div(ti).Average(n).SetN(n-1, math.NaN())
up := x.GT(x.Average(m))
dn := x.LT(x.Average(m))
buy := pd.Shift(1).GTE(lvl).Mul(pd.LT(pd.Shift(1))).Mul(up)
sell := pa.Shift(1).GTE(lvl).Mul(pa.LT(pa.Shift(1))).Mul(dn)
pos := x.PositionSigned(buy, nil, sell, nil, hold)- Inputs:
a,d,tiand the traded indexx; n = 6, m = 100, hold = 5 (the tested five days) and lvl = 0.40, inclusive (“40 percent or higher”), with a strict turn (“less than”). Connors states neither the average type (simple here) nor which series the gate reads (his test trades the S&P 500 futures), soxis the caller’s. The signals and the book’s exits fill at the close of their bar, as he trades them. - First valid index: n - 1 for
paandpd(5), NaN before; the turns can fire from n. The gate needs no mask:Average(m)passesxthrough on bars 0 to m - 2, sox.GT(x.Average(m))and its mirror read 0 there, andbuy,sellandposcannot fire before bar max(n, m - 1), 99 at m = 100, where the gate first opens;posis flat until the first signal. - Anchors: on a constructed tape with n = 5 and m = 10, where
pdis exactly 0.40 on bars 4 to 13, the inclusive level and the strict turn give one buy, on bar 14, andposis long on bars 14 to 18 (hold 5); an opposite signal reverses the book on its own bar (PositionSigned). A 40% tie depends on the summation: five shares of 0.40 average to 0.40 exactly, six to 0.40000000000000008, which passesGTE(0.40)through the compensated sum where a plain left-to-right sum gives 0.39999999999999997; exact ties stay out of the golden. - Traps: the dossier’s unmasked
d.Div(ti).Average(6)turn can fire at bar n - 1: a raw share of 0.5 on bar 4 followed by a first average of 0.333 is a false turn on bar 5. Connors prints the denominators as “(advances + total)” and “(declines + total)”, a misprint on the page, not a reading error: his step 1 adds “the advances, declines, and unchanged”, sotiis A + D + U. His 1.5% protective stop is not part of the recipe: it is the stop pattern of the Trade State introduction (“Positions, stops and shadow trades”) applied topos. - Evidence: Connors’s table for the S&P 500 futures from January 1990 to October 1997, with the 1.5% stop and the five-day exit: 109 trades, 61% profitable and a profit factor of 2.00 (longs 83 trades and 2.12, shorts 26 and 1.56), in sample, on one market, costs not stated. Evidence, not a reference.
- Pinned by
TestRecipeT6PADI.
Normalised advance-decline line (Chande; Morris)
Chande (“Breadth STIX And Other Tricks”, Stocks & Commodities V12:5, May 1994, pp. 211-214) cumulates his DDI index, (A - D)/(A + D), which includes “both the ADI and DCI to give equal impact for up and down markets”, converts “the cumulative DDI index into a stochastic oscillator”, “a five-day stochastic oscillator”, and reads it with “a 80/20 stochastic crossover rule”. Morris (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 4) applies the formula to the A-D line itself: “I used a 21-day period for the normalization and then smoothed the results with a 7-day exponential average”. t6NormalisedADLine gives Chande’s stochastic of his ratio line and Morris’s smoothed stochastic of the A-D line, the Breadth introduction’s cumulative lines ratio and adl, whose levels depend on where the history starts.
line := a.Sub(d).Div(a.Add(d)).CumSum()
st := line.Stochastic(line, line, n).SetN(n-1, math.NaN())
adl := a.Sub(d).CumSum()
sm := a.Mul(0).Add(adl.Stochastic(adl, adl, m).Window(-(m - 1)).XAverage(k)).SetN(m+k-2, math.NaN())- Inputs:
aandd; n = 5 (Chande, on the ratio line), m = 21 and k = 7 (Morris, on the A-D line). - First valid index: n - 1 for
st(4) and m + k - 2 forsm(26), NaN before. An n-barStochasticreads exactly 50 on bars 0 to n - 2, whichstmasks;smtrims the m - 1 such bars (Window(-(m - 1)), asStochastic’s doc says) before the k-bar average, whose simple-average seed falls on bar m + k - 2, anda.Mul(0).Add(...)pads it back to full length. - Anchors: on integer counts the m-bar stochastic of the A-D line built after a cut of the history equals the uncut one bit for bit from the cut’s bar m - 1 (the two lines differ by an integer constant, and the differences inside the stochastic are exact); on Chande’s ratio line it agrees to rounding only, within 5e-12 on the 0 to 100 scale.
smis not start-invariant (an exponential average remembers its start).sm[m+k-2]is the plain mean of the stochastic’s bars m - 1 to m + k - 2 (20 to 26) bit for bit; from thereXAveragefuses on arm64, so the golden holdssmwithin 2^-40 there and bit for bit at amd64 v1. - Traps: the dossier’s
adl := a.Sub(d).Sum(len(a))isa.Sub(d).CumSum(), and itsadl.Stochastic(adl, adl, 21).XAverage(7)seeds the average with six warm-up 50s and one real value. Unmasked, the warm-up 50 fires a false crossing of 50: on a rising line the first valid value is 100, soline.Stochastic(line, line, n).CrossesAboveS(50)fires at bar n - 1 (crossings of 20 and 80 cannot fire off a 50). A flat window reads 0, not 50: four days in a row with A = D make Chande’s five-day window flat, the oversold extreme, and the next rise crosses 20. Chande stochasticises his ratio line, not A - D, and his printed equation puts the five-day high less today’s value in the numerator (100 less %K) where his Figure 8 plots the usual %K. - Variant: the rules: Chande’s 80/20 rule read as the buy
st.CrossesAboveS(20)and the sellst.CrossesBelowS(80)(he does not say which crossing buys, and gives “values of 100 and zero as early warnings”), and Morris’s entrysm.CrossesAboveS(50)(“rising and goes above 50”), partial exitsm.CrossesBelowS(80)(“leg out of your long position”) and exitsm.CrossesBelowS(50)(“Sell all of it when it goes below 50”). - Evidence: charts only: Chande’s Figure 8 (1993), where “The arrows often provide good entry and exit signals”, and Morris’s charts; neither tests the rules. Evidence, not a reference.
- Pinned by
TestRecipeT6NormalisedADLine.
Absolute breadth index and plurality index (Fosback; Dysart, Rotnem and Shaw; Merrill)
Fosback (Stock Market Logic, Dearborn Financial Publishing, 1976, twenty-first printing 1992, ch. 35, pp. 124-126) divides “the absolute value of advances minus declines (ignoring the sign, plus or minus, of the differential), by the total number of issues traded”, “calculated on a weekly basis” and “averaged over ten weeks”, which “normally reads about 25%”: “Readings above 40% are highly bullish and values below 15% usually precede periods of far below average, and frequently negative, market returns”. Arthur Merrill (“Plurality”, Stocks & Commodities V8:4, April 1990, pp. 137-138) reports James Alphier’s claim that Paul Dysart wrote on the plurality by 1937, and that Ralph Rotnem used it and Alan Shaw named it: “a moving total, not an average of the plurality of the last 25 days”, where “If the index drops below 6,000, a sell warning is signaled”, the signal “becomes a definite sell” when the index “turns back up”, and the buy mirrors it above 12,000. t6AbsoluteBreadth gives the percentage and its n-bar average, the p-bar plurality and Shaw’s definite signals.
abi := a.Sub(d).Abs()
abiPct := abi.Div(ti).Mul(100)
abiAvg := abiPct.Average(n).SetN(n-1, math.NaN())
pi := abi.Sum(p).SetN(p-1, math.NaN())
up := pi.Delta(1).GT(0)
dn := pi.Delta(1).LT(0)
sell := pi.CrossesBelowS(lo).Sub(up).Hysteresis(0, 0).GT(0).Shift(1).Mul(up)
buy := pi.CrossesAboveS(hi).Sub(dn).Hysteresis(0, 0).GT(0).Shift(1).Mul(dn)- Inputs:
a,dandti. The average is Fosback’s, on weekly counts (the issues up and down on the week, not sums of daily counts), n = 10, of a type he does not state (simple here), read with his strict levelsabiAvg.GT(40)andabiAvg.LT(15); the plurality is daily, p = 25.loandhiare Shaw’s 6,000 and 12,000 (1990), which suit about 2,000 issues, or Morris’s 14,000 and 23,000 (2015, ch. 4): the zones drift with the number of issues, and the golden run on the NYSE-sized test tape uses Morris’s. - First valid index: 0 for
abiPct, n - 1 forabiAvg(9) and p - 1 forpi(24), NaN before;sellandbuyfrom p + 1 (26), because a cross needs a valid prior bar and the signal comes on a later bar. They read 0 before. - Anchors: Fosback’s 1,200 advances and 600 declines give the same |A - D| as the reverse, and Merrill’s 4,000 and 3,000 a plurality of 1,000 either way, bit for bit. Shaw’s latch, the
Hysteresisevent latch, equals a loop that arms on the strict cross belowlo, fires on the first strict rise on a later bar and holds through a flat bar (the buy mirrors it): on a constructed tape (n = p = 5, lo 2,000) whose plurality falls through 2,000 to 1,500, stays flat for a bar and rises to 1,700, the sell is on the rise, bar 11. Five constructed days, A 1,500, 600, 1,400, 700 and 1,300 against D 600, 1,500, 700, 1,400 and 800, give a five-day plurality of 3,700, while one bar of their summed counts gives 500. - Traps: a week of summed daily counts is not weekly data (“during a week some of the differences cancel out”, Merrill), as the five days show, so neither Fosback’s levels nor Shaw’s zones transfer between frequencies. Unmasked,
abi.Sum(25)gives partial sums on bars 0 to 23, which read below 6,000 as a level and rise through the zones as the window fills. The average form Morris has seen,abi.Average(25), times 25 differs frompiin the last bit on some bars, though its zones divided by 25 agree:abi.Average(25).LT(240)ispi.LT(6000)on integer counts from bar 24. Morris’s “Art Merrill claims that this must be a daily indicator” paraphrases a weekly study whose conclusion is “The daily index is the one to watch, not the weekly”. - Variant: Morris’s daily index
abi.Average(21).SetN(20, math.NaN()), watched “as it goes over 1100 for bottoms and below 300 for market tops” (250 and 600 in the 1970s and 1980s); he states no average type, and StockCharts’ legend values fit simple averages. - Variant: Morris’s adjusted index
abiPct.Average(21).SetN(20, math.NaN()), “over 30 for bottoms and below 10 for market tops”. - Evidence: Fosback’s Table 40 (1956 to 1975, in sample, no costs, bucket edges unstated): the S&P 500 returned 0.0% twelve months after readings under 15% (-1.3% at six months), 5.0% between 15% and 40% and 13.2% over 40%, against a 20-year average of 4.8%. Shaw reports, through Merrill, that “since 1981 all the signals have been successful, with two exceptions, in 1985 and 1986”, and Merrill’s weekly version “didn’t prove useful for the short term but gave significant signals for a one-year forecast”. StockCharts’ plurality values are not integers, so they are not Merrill’s 25-day total. Evidence, not a reference.
- Pinned by
TestRecipeT6AbsoluteBreadth; runnable exampleExampleSeries_Abs_absoluteBreadth.
The Arms index compositions below read the inputs of the Breadth introduction (“Breadth inputs”): advances a, declines d, up volume uv and down volume dv, caller-supplied series paired by position and right-aligned at the most recent bar, the counts non-negative integers and the volumes finite and non-negative in one unit, which cancels in every ratio here. Values are NaN before their first valid index F (SetN(F, math.NaN())) and flags read 0 there; lengths are in bars, TRIN and its averages are pure numbers, Wilbur’s normalised values run on a 0 to 100 scale and the thrust is in percent. A bar with no trading (all counts 0) is not an input bar. Threshold tests on one day’s ratio are written multiplied out (a.GT(d.Mul(4))), exact on integer counts and true on a zero-divisor day, where Div and Ratio read 0; averaged ratios mask zero divisors with MaskNonFinite(x.Log()) where a 0 would be a false extreme, and TRIN, whose 0 is its most bullish reading, is NaN on a bar with any zero count. A flag is a decision at a bar’s close and acts at the next bar’s open, a fill that is the caller’s (Connors and Alvarez, Connors and Wilbur trade at the signal day’s close; see the Trade State introduction). TRIN, (A/D)/(UV/DV), is the advance-decline ratio over the up/down volume ratio: bounded between 0 and 1 on strong days and unbounded above 1 on weak ones, so its logarithm, with Log(1/x) = -Log(x), is the symmetric scale, and the average of daily TRINs and the open TRIN, a ratio of averaged counts, are different indicators. The up-minus-down volume momentum of Star and Waxenberg reads the Breadth introduction’s udv line through its start-invariant window form, uv.Sub(dv).Sum(k).SetN(k-1, math.NaN()), the line’s k-bar change, whereas Waxenberg’s own momentum (“Technical analysis of volume”, Stocks & Commodities V4:2, 1986, pp. 65-68, as Barbara Star prints it in “Volume Variations”, V11:5, May 1993, pp. 215-219) is the 10-day average of the daily spread’s 12-day change, uv.Sub(dv).Momentum(12).Average(10).SetN(21, math.NaN()).
The Arms index, its log form and the open TRIN (Arms; Wilbur)
Richard W. Arms Jr. (“Using The Arms Index In Intraday Applications”, Stocks & Commodities V9:4, April 1991, pp. 145-147) calls his index “a ratio of two ratios”, advances over declines divided by advancing over declining volume, and regrets not inverting it “so that low numbers would reflect weakness”. Harley D. Wilbur (“A Twist On The Arms Index”, Stocks & Commodities V10:11, November 1992, pp. 449-453, with the sidebar “Weighted average TRIN10”) would “first calculate moving averages of the four TRIN components” and then the index “from the four component moving averages”, the open TRIN, normalises it in “10-day, two-standard deviation Bollinger bands” so that at the lower band “the normalized value is set at 100”, and weights five days of the result into WTRIN10. t6ArmsIndex gives the daily TRIN, its natural log, the n-bar average of daily TRINs, the open TRIN, Wilbur’s normalised value nt and his WTRIN10.
trin := a.Div(d).Div(uv.Div(dv)).MaskNonFinite(a.Log(), d.Log(), uv.Log(), dv.Log())
ltrin := trin.Log()
avg := trin.Average(n).SetN(n-1, math.NaN())
ot := a.Average(n).Div(d.Average(n)).Div(uv.Average(n).Div(dv.Average(n))).SetN(n-1, math.NaN())
up := ot.Bollinger(n, 2)
lo := ot.Bollinger(n, -2)
nt := up.Sub(ot).Div(up.Sub(lo)).Mul(100)
wtrin := nt.WAverage(m)- Inputs:
a,d,uvanddvas above;n(10: Wilbur’s component averages and bands, and the 10-day average of daily TRIN that Chande and StockCharts read as oversold above 1.20 and overbought below 0.80, Favors as oversold above 1.00) andm(5, Wilbur’s weighted average). The bands are the n-bar average ofotplus and minus two population deviations, the sheet’s STDEVP (“Use the formula for the standard deviation of the population”), andntis inverted: 100 at the lower band, 0 at the upper, below 0 or above 100 outside them. - First valid index: 0 for
trinandltrin, NaN on a bar with a zero count; n - 1 (9) foravgandot, NaN before, whereAveragewould pass the inputs through and make the open TRIN a daily TRIN; 2n - 2 (18) fornt, with no mask of its own, becauseStdDevis NaN while a masked bar ofotis in its window; 2n + m - 3 (22) forwtrin. A zero-count bar makesavgNaN on the n bars whose windows hold it. - Anchors: Wilbur’s sidebar (23 days from 6 August to 6 September 1990; row r of the sheet is bar r - 2):
oton bars 9 to 22 gives his 14 open-TRIN values, column G, rows 11 to 24 (0.9518 to 1.0173), at four decimals; the 10-day mean, population deviation, bands and normalised value of columns H to L, rows 20 to 24, agree at four decimals, column L beingnt(37.9922 on bar 18 to 87.1954 on bar 22); andwtrinon bar 22 gives row 24 of column M, 72.2690, at four decimals (WAverage’s weighted sum is an unfenced multiply-add, so its last bits differ between arm64 and amd64). Jack Rusin’s two printed days (“An Issue/Volume Weighted Long-Term Arms Index”, Stocks & Commodities V9:10, October 1991, pp. 419-421), 10 advances and 10 declines with 100 shares up and 200 down, then 200 up and 100 down, run through the helper with n = 2: daily TRINs 2 and 0.5,avg1.25 (his “2DAI”),ot1 (his “[20/20]/[300/300]=1/1=1”) and the geometric meanltrin.Average(2).Exp()1, sinceltrinreads ln 2 and -ln 2. The witness needs his counts: constructed counts with the same daily TRINs give an open TRIN of 0.889. StockCharts’ three days (ChartSchool, “Arms Index (TRIN)”, fetched 16 October 2026) give 0.99, 0.42 and 3.00 at two decimals. Arms’s April 1991 day, 445 up and 837 down against 35,374,000 shares of down volume, gives 1.05 with both printed up volumes, 17,982,400 in his text (1.0459) and 17,892,400 in his equation (1.0511); his printed volume ratio, 0.508, is the text’s. Fosback’s example (1,000 advancing issues with 10 million shares and 500 declining with 5 million, then 20 million shares up, then 10 million down) givesuv.Div(a).Div(dv.Div(d))= 1, 2 and 0.5 exactly. On positive counts theRatioforma.Ratio(d).Ratio(uv.Ratio(dv))equalstrinbit for bit. - Traps: on a bar with a zero count
Divand theRatioform both read 0, the most bullish TRIN there is, andtrin.Log()turns that 0 into -Inf; the mask makes the bar NaN, which comparisons read as 0 (no source defines TRIN at zero counts). The unmasked open TRINa.Average(10).Ratio(d.Average(10)).Ratio(uv.Average(10).Ratio(dv.Average(10)))reads daily TRINs on bars 0 to 8. The inverted form 1/TRIN does not fix the skew Chande describes but mirrors it; only the log is symmetric, and so is Chande’s TROS, (1 - TRIN)/(1 + TRIN), bounded by -1 and 1 (“Market Thrust”, Stocks & Commodities V10:8, August 1992, pp. 347-350), the normalised difference of the volume per advancing and per declining issue (the Relative: Ratio and Relative Strength page, “Normalised difference and share”).trin.Log().XAverage(21)is no source’s form. Wilbur’s TRIN10 is the open TRIN itself and his scale is inverted: Morris’s paraphrase, a furtherot.Average(10)placed in bands with 0 at the lower band, does not reproduce the sheet and turns the 20 and 80 levels round. The sheet’s fixed five-term formula reads the blank cells of column L above row 20 as zeros, so rows 20 to 23 of column M come from the zero-paddednt.SetN(18, 0).WAverage(5), not fromwtrin, which is NaN there. A flat band givesnt0, the upper-band reading, not 50 (Wilbur does not say). His MetaStock Formula 1 prints/4*std(C,10), dividing by 4 before multiplying by the deviation; the spreadsheet is the correct form. Chande repeats Rusin’s table and dates the article “October 1990” (it is October 1991, V9:10), and his Figure 3 prints day 1’s declining volume as “100,00”, where 100,000 is the only value that gives his Arms index of 1. - Variant: Fosback’s Short Term Trading Index (Stock Market Logic, 1976, ch. 38, pp. 131-134), “(AV/A)/(DV/D)”,
uv.Div(a).Div(dv.Div(d)), is 1/TRIN to rounding and reads bullish when high; it is not symmetric (its unbounded side is the strong one), and it needs the same zero-count mask astrin. - Variant: Arms’s “Cross Your Arms” (Stocks & Commodities V9:5, May 1991, pp. 177-179) uses “a simple arithmetic moving average in both cases” on TRIN itself, long “if the 21-day line is below the 55-day line”,
trin.Average(21).LT(trin.Average(55).SetN(54, math.NaN())), first valid at 54. The rule needs no inversion, and exponential averages are Morris’s report (“Richard Arms preferred exponential smoothing instead of arithmetic”) and Merrill’s (“Arms also has used exponential averages”, “Volume indices”, Stocks & Commodities V7:9, September 1989, pp. 301-303), not its rule. - Evidence: Wilbur’s rule, “Buy when WTRIN10 has an uptick after falling below 20, hold until it goes above 80, and then sell on its first downtick”, filled on the signal day, made 11.19% from 18 May to 18 December 1990 against -9.27% for buy-and-hold, 20.07% against 22.41% from November 1990 to June 1991 and 2.57% against 3.28% from November 1991 to June 1992; his trades need data from before 6 August 1990 and cannot be reproduced. James Alphier and Bill Kuhn (“A helping hand from the Arms Index”, Stocks & Commodities V5:4, April 1987, pp. 142-143) tabulate the days with TRIN at 2.65 or more since 1965 and the S&P 500 3 to 12 months later, up 15 times and down 3 at 12 months. Evidence, not a reference.
- Pinned by
TestRecipeT6ArmsIndex; runnable exampleExampleSeries_Bollinger_openTRIN.
TRIN events: TRIN-5, the three-day rule and the thrust (Bretz, Favors; Connors and Alvarez; Connors)
Gregory L. Morris (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 9, “Bretz TRIN-5”) gives Bretz’s TRIN-5, “a 5-day moving sum (not a moving average)” of TRIN, with a buy “When the indicator reaches an extreme level above 6.00, then turns down” and a sell when it reaches “an extreme level below 4.00, then turns up”; Jerry Favors (“The TRIN-5”, Stocks & Commodities V10:3, March 1992, pp. 125-126) prints the levels (“Do not divide this number by 5”). Larry Connors and Cesar Alvarez (Short Term Trading Strategies That Work, 2008, ch. 12, strategy 3, pp. 99-103) key their TRIN strategy on “The TRIN closes above 1.00 for three consecutive days”, and Larry Connors (Connors On Advanced Trading Strategies, 1998, ch. 3, “TRIN Thrusts”, pp. 17-21) buys “If today’s closing TRIN is 30 percent or more below yesterday’s closing TRIN” and sells on a rise of 30 percent or more. t6TRINRules gives the TRIN-5 and its turns, the three-day flag and the two thrusts from the daily trin of t6ArmsIndex.
t5 := trin.Sum(5).SetN(4, math.NaN())
buy5 := t5.Shift(1).GT(6).Mul(t5.LT(t5.Shift(1)))
sell5 := t5.Shift(1).LT(4).Mul(t5.GT(t5.Shift(1)))
three := trin.GT(1).Sum(3).EQ(3)
roc := trin.RateOfChange(1)
tBuy := roc.LTE(-30)
tSell := roc.GTE(30)- Inputs:
trin, the masked daily TRIN of the section above (NaN on a bar with a zero count); the levels 6 and 4, 1 and 30 percent are the sources’ and are written into the block, all strict but the thrust’s “30 percent or more”. The one-day level rules are parameters, not outputs: Newton Zinder’s two consecutive readings “of 2.00 or better” (reported by Alphier and Kuhn and by Morris),trin.GTE(2).Sum(2).EQ(2); Alphier and Kuhn’s “arbitrary threshold” of 2.65,trin.GTE(2.65); and Arthur Merrill’s next-day levels (“Closing Arms”, Stocks & Commodities V10:4, April 1992, pp. 148-150), “2.3 or higher” for a bearish signal,trin.GTE(2.3), and 0.4 or lower for a bullish one,trin.LTE(0.4). - First valid index: 4 for
t5, NaN before, becauseSumgives partial sums on bars 0 to 3, below 4 early on, which would fire the sell turn; 5 forbuy5andsell5; 2 forthree, which needs no mask (a partial sum of 0/1 flags cannot reach 3); 1 fortBuyandtSell(RateOfChangereads 0 on bar 0). The flags read 0 before them. A NaN TRIN makest5NaN on the five bars whose windows hold it, breaks a three-day run and gives no thrust on its bar or the next. - Anchors: Connors’s worked thresholds: 1.00 to 0.70 gives
RateOfChange(1)-30.000000000000004 and firestBuy, and 0.70 to 0.91 gives 30.000000000000004 and firestSell. On Wilbur’s 23 daily TRINs (the section above) the thrust buys on bars 5, 9, 10, 14, 17 and 19 and sells on 4, 6, 8, 11, 13, 16, 18, 20 and 22;threefires on bars 9 and 13; the TRIN-5 peaks at 7.2583 on bar 13, with buy turns on bars 14, 15 and 16 and a sell turn on bar 6. The ties are strict: three days of TRIN exactly 1 give nothree, and a TRIN-5 of exactly 6 or exactly 4 arms no turn.threeequals the run formtrin.GT(1).StateAge().GTE(3).Mul(trin.GT(1))bit for bit. - Traps: the multiplied-out thrust
trin.GTE(trin.Shift(1).Mul(1.3))fires on bar 0, whereShiftpads +0, and needs a bar-0 mask; it agrees with the rate of change on Connors’s examples but not on every two-decimal pair (1.3 to 0.91 is one), and an exact decimal 30% move such as 0.70 to 0.49 fires in neither form.buy5is a level-then-turn flag that fires on each falling bar while the previous total is above 6 (bars 14 to 16 above); a one-shot turn multiplies it byt5.Shift(1).GTE(t5.Shift(2)).threeis 1 on every bar of a run from its third day. Bretz’s buy rule is Morris’s reading: Favors prints the levels and warns that when the TRIN-5 “reaches an extreme below 4.00 and then turns up” a top is “within one to four days”.c.RSI(2)reads 0 on bars 0 and 1, which passesLT(50), so it is masked below. Connors’s “1 1/2 to 2 percent protective stop” is the caller’s (Trade State introduction); his example prints “982 to 987” for a market at 900, a misprint for 882 to 887. - Variant: Connors and Alvarez’s strategy on an index close
c(SPY),c.Position(c.GT(c.Average(200)).Mul(c.RSI(2).SetN(2, math.NaN()).LT(50)).Mul(three), c.RSI(2).GT(65)), buys and exits on the close, first possible at bar 199 through the 200-day gate; Connors’s thrust is held two days,c.Position(tBuy, nil, 2), his “Sell on the close two trading days later”. - Variant: Fosback’s shift of his Short Term Trading Index averaged over “a three to five day period”, “from below 0.85 to above 1.20”, is the latch
stti.Average(3).SetN(2, math.NaN()).Hysteresis(1.20, 0.85), wheresttiisuv.Div(a).Div(dv.Div(d))(the first variant of the section above), and the buy is its flip from -1 to +1,h.EQ(1).Mul(h.Shift(1).EQ(-1))for the latchh; on Wilbur’s days it fires on bar 15. - Evidence: Connors and Alvarez report 90 trades from 1995 to 2007, 75.56% correct, 558.30 S&P points, an average hold under four days and a loss only in 2002 (5.11 points); Connors’s thrusts on S&P 500 futures from January 1990 to October 1997 made 764 trades, 51% winners, profit factor 1.28, the longs 1.82 and the shorts 0.97, so the short side failed. Merrill found four days of 0.4 or lower in 435 (the DJIA rose 0.46% on average the next day) and three above 2.4 (it fell 0.21%). Evidence, not a reference.
- Pinned by
TestRecipeT6TRINRules; runnable exampleExampleSeries_RateOfChange_trinThrust.
The recipes below read one-day and few-day extremes of participation: thrusts out of a washed-out market, climax days, low-volatility streaks and the first correction day. Their inputs are caller-supplied series paired by position and right-aligned at the most recent bar, as the Breadth introduction (“Breadth inputs”) says: advances a, declines d, unchanged issues u, total issues ti (a + d + u), up volume uv and down volume dv, and for Northington a second exchange’s aq, dq and uq; counts are non-negative integers and volumes finite and non-negative. Values are NaN before their first valid index (SetN(F, math.NaN())), and flags and latches built on masked values read 0 there. Shares and ratios are fractions (0.615, not 61.5%) and lengths are in bars (trading days). A bar with no trading (all counts 0) is not an input bar. Threshold tests on one day’s ratio are written multiplied out, a.GT(d.Mul(4)): exact on integer counts and true on a zero-divisor day, where Div and Ratio read 0 and miss the most extreme day; averaged ratios mask zero divisors, a.Div(d).MaskNonFinite(d.Log()), where a 0 would be a false extreme. The advance share of a + d is the share form of “Normalised difference and share” (Relative: Ratio and Relative Strength). Signals act at the next bar’s open, which is the caller’s (positions, stops and fills: the Trade State introduction).
Zweig breadth thrust and the Appel/McClellan continuation
Gregory Morris (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 5, “Breadth Thrust” and “Breadth Thrust Continuation”; the e-book has no page numbers) gives Martin Zweig’s line as “a 10-day exponential average of the advances divided by the sum of the advances plus the declines” and its signal “when this indicator goes from under 0.40 to above 0.615 within a 10-day time period or less”. Gerald Appel’s continuation “is given whenever it goes above 0.615 without the requirement for it to come from below 0.40 in ten days” and “is good until it then drops below 0.50”, and Tom McClellan’s rule “expires after 50 days, but can restart that timing (50 days) with any subsequent move above 0.615”. t6BreadthThrust gives the line bt, the thrust as the bar on which the line crosses above hi with a bar below lo on it or among the w - 1 before, Appel’s latch cont, and contM, which ends life bars after the latest crossing and restarts on each one (on at ages 0 to 49).
x := a.Div(a.Add(d)).XAverage(n)
bt := x.SetN(n-1, math.NaN())
setup := x.SetN(n-1, 1).Lowest(w).LT(lo)
rise := bt.CrossesAboveS(hi)
thrust := rise.Mul(setup)
cont := bt.Hysteresis(hi, off).GT(0)
contM := cont.Mul(rise.BarsSince().LT(float64(life)))- Inputs: advances
aand declinesd(NYSE in every source; unchanged issues are not used);n10,w11 (a lag of at most 10 bars from the dip to the thrust),life50,lo0.40,hi0.615 andoff0.50, every test strict as the sources word it (“under”, “above”, “below”). Tushar Chande (“Breadth STIX And Other Tricks”, Stocks & Commodities V12:5, May 1994, pp. 211-214) states the same 10-day exponential average in percent, which changes only the levels (40 and 61.5). A bar witha + dof 0 would enter the average as a 0% day, carried for about 69 bars, that can make a false setup: such a bar is not an input bar. The STIX oft6-advance-decline-measures.mdis the percent form of this average at n = 21, with its own zones. - First valid index: n - 1 (9) for
bt, NaN before, and for the setup; n (10) forthrustandcontM(CrossesAboveSneeds a valid prior bar);contdecides from n - 1 and reads 0 before. The low side is masked with 1, not NaN: a share is at most 1, so a masked bar is not belowlo, while NaN would voidLowestforwbars (it is NaN while a NaN is in its window, here to bar 18), andmath.Inf(1)is outside the helpers’ allowlist (math.NaNonly). - Anchors: on lines given as counts with
a + dof 1,000 and run with n = 1 (XAverage(1)copies the share): a dip to 0.39 on bar 1 and 0.62 on bar 11 (lag 10) fire on bar 11, the same rise one bar later (lag 11) does not fire, and a line that touches exactly 0.40 and then exactly 0.615 does not fire (contstill turns on at 0.62 on bar 5); the line 0.5, 0.39, 0.45, 0.55, 0.615, 0.62 fires on bar 5, not on bar 4; a double crossing after one dip (0.5, 0.38, 0.5, 0.62, 0.61, 0.63, …) gives two events, bars 3 and 5.thrustis exactly the first bar of each run ofbt.GT(hi).Mul(setup)(1 on bars 3, 5 and 6 there). Ten days at 0.25 and five at 0.95 with n = 10 seed the line below 0.40 on bar 9 and cross 0.615 on bar 13, where the thrust fires; a NaN mask on the low side loses it. On a 12-bar warm-up tape whose raw shares swing between 0.1875 and 0.8125 the pinned form fires on no bar.contMequals a state machine (on at a crossing with age 0, off belowoffor at agelife); on the tests’ synthetic tapecontandcontMfirst differ on bar 1236, 50 bars after the rise of bar 1186.btagrees with the clean-room references bit for bit on amd64 (GOAMD64=v1) and to rounding on arm64. - Traps: the dossier’s
bt.GTE(0.615).Mul(bt.Lowest(11).LTE(0.40))is inclusive where every source is strict, is 1 on every bar of a run rather than once, and fires in the warm-up (bars 1, 4, 5 and 6 of the warm-up tape);bt.CrossesAboveS(0.615).Mul(bt.Lowest(10).LT(0.40))allows a lag of 9 only and misses the lag-10 thrust; andbt.GT(0.615).Highest(50)restarts on every bar above 0.615 and has no exit below 0.50.bt.Position(rise, bt.LT(off), life)cannot restart its time limit, since an entry while long is ignored (it differs fromcontMon about a hundred bars of a 4,000-bar path). A double crossing after one dip gives two dates, as pinned, though Morris lists single dates. McClellan’s “any subsequent move above 0.615” is read as a crossing and “expires after 50 days” as ages 0 to 49; restarting on every bar above (the factorbt.GT(hi).BarsSince().LT(float64(life))) or keeping age 50 (rise.BarsSince().LTE(float64(life)); about ten bars of 5,000 differ) are the other readings (his report of 19 November 2004 was not read).btreadsXAverage, whose step fuses on arm64, so a line within rounding of 0.40, 0.50 or 0.615 can test differently there. - Variant: Fay Dworkin (“Defining Advance/Decline Indicators”, Stocks & Commodities V8:7, July 1990, pp. 274-278) credits Zweig with “a 10-day simple moving average”, the line
a.Div(a.Add(d)).Average(n).SetN(n-1, math.NaN())(first valid n - 1; a thrust on it masks the low side withSetN(n-1, 1)as above), and Robert Colby’s latch on it (through Morris: “0.659 for a buy signal and dropping below 0.366 for a sell signal”, with no 10-day window) isa.Div(a.Add(d)).Average(n).SetN(n-1, math.NaN()).Hysteresis(0.659, 0.366). - Variant: Zweig’s levels as Dworkin reports them, “High readings (0.66 or more)” very bullish and “readings at 0.367 or lower are very bearish”, inclusive on the same line:
a.Div(a.Add(d)).Average(n).SetN(n-1, math.NaN()).GTE(0.66)anda.Div(a.Add(d)).Average(n).SetN(n-1, math.NaN()).LTE(0.367). - Evidence: Morris lists nine thrusts since 1965 (3 December 1971, 10 October 1974, 3 January 1975, 20 August 1982, 3 August 1984, 18 March 2009, 14 October 2011, 18 October 2013 and 7 October 2015) and counts 31 continuation signals; his “Between 1944 and 2014 there were 13 thrusts” stands beside that list, whose last date lies outside the span, and the four thrusts before 1965 are listed in no source read. Arthur Hill’s StockCharts articles of 21 March 2025 and 3 April 2026 put the cut-off 10 trading days after the dip (13 to 27 March 2025, on his S&P 500 and S&P 1500 lines; 20 March 2026 to “Monday”, the tenth day, 6 April, since 3 April was Good Friday), so the setup date depends on the universe, and his 2023 near misses “took 12 days” and “11 days”: they confirm the window, not a value. No source prints daily counts beside the line, so there is no worked example. Zweig’s Winning on Wall Street (1986) was not read: Dworkin attributes an SMA to him, while the EMA is Morris’s, Chande’s and Hill’s; a count of 14 thrusts with a 24.6% average gain is in no source read, and Morris dates the 2013 thrust 18 October, not January. Evidence, not a reference.
- Pinned by
TestRecipeT6BreadthThrust; runnable exampleExampleSeries_Hysteresis_breadthThrust.
Climax and 9-to-1 days (Morris; Hughes, Appel; Zweig)
Morris (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 5, “Panic Thrust”, ch. 6, “Breadth Climax”, and ch. 8, “Zweig Up Volume Indicator”) gives a panic thrust “Whenever the ratio of advances to declines is greater than 4” and re-sets Gerald Appel’s climaxes: a selling climax when “At least 75% of the total issues are declines” and “No more than 15% of the issues are advances”, a buying climax on “two consecutive days where the advances are greater than 65% of the total issues”. Martin Zweig’s 9-to-1 day comes “Whenever the ratio reaches 9 to 1” of up to down volume, and his “double nine” when “there are two signals in any 3-month period”; Morris adds the days “in which the down volume was 9 times greater than the up volume”. t6BreadthClimax gives the six flags, the double nine firing on the second 9-to-1 up day of a pair inside w bars.
panicDay := a.GT(d.Mul(4))
dsh := d.Div(ti)
ash := a.Div(ti)
sellClimax := dsh.GTE(0.75).Mul(ash.LTE(0.15))
buyClimax := ash.GT(0.65).Sum(2).EQ(2)
nineUp := uv.GTE(dv.Mul(9)).Mul(uv.GT(0))
nineDown := dv.GTE(uv.Mul(9)).Mul(dv.GT(0))
doubleNine := nineUp.Mul(nineUp.Sum(w).GTE(2))- Inputs: advances
a, declinesd, total issuesti(a + d + u, or the exchange’s issues traded where it reports both), up and down volumeuvanddv;w63, “any 3-month period” taken as 63 trading bars (an assumption: a calendar three months needs dates, which are the caller’s). The levels 4, 0.75, 0.15, 0.65 and 9 are the sources’ literals in the statements. The shares divide bytiwithDiv;tiis 0 only on a bar with no trading, which is not an input bar. - First valid index: 0 for every flag but
buyClimax, and 1 there (bar 0’sSum(2)holds one flag, soEQ(2)reads 0);doubleNinecounts the pairs inside the available history from bar 0, as the rule asks. - Anchors: the shares tested with
Divagree with the exact rational comparisons on every integer pair withtifrom 1 to 3,000, ties at 0.70, 0.75, 0.15 and 0.65 included; on such ties a day with exactly 75% declines and 15% advances is a selling climax, advances at exactly 65% and then 70% are not a buying climax (one day above 65%, not two), and down volume of exactly 9 times up volume is a 9-to-1 down day. On a fixture with 9-to-1 up days on bars 5, 6, 10, 75, 137 and 200, up volume without down volume on bar 210 and no volume on bar 215,nineUpfires on 5, 6, 10, 75, 137, 200 and 210 anddoubleNineon 6, 10, 137 and 210: a next-bar pair fires, a third day inside the window fires again, a pair 62 bars apart fires (75 and 137) and one 63 bars apart does not (137 and 200).doubleNineequals the dossier’snineUp.Mul(nineUp.Shift(1).Sum(w-1).GTE(1))bit for bit (sums of 0/1 flags are exact), andbuyClimaxequals the run formash.GT(0.65).StateAge().GTE(2).Mul(ash.GT(0.65))bit for bit, warm-up included. Every output is 0 on an all-zero bar. - Traps: the dossier’s
uv.Ratio(dv).GTE(9)reads 0 wheredvis 0 and so misses the day with no down volume, the most extreme 9-to-1 day there is (bar 210 above), anduv.GTE(dv.Mul(9))without the factoruv.GT(0)fires on a bar with no volume (0 >= 0); the factor removes that bar and nothing else.a.Div(d).GT(4)reads 0 on a day without declines, wherepanicDayfires. The multiplied-out inclusive shares,d.GTE(ti.Mul(0.75)).Mul(a.LTE(ti.Mul(0.15))), fire on an all-zero bar, which is why the shares useDiv. Departures: Appel’s and Morris’s third selling-climax condition, “Less than 150 issues are unchanged”, and Appel’s own buying climax, “no more than two consecutive days having 1000 or more advancing issues” (which reads as a garble), are absolute counts set before listing growth and decimalisation and are left out. Dennis Meyers (“Making The Titanic Fly”, Stocks & Commodities V13:5, May 1995, pp. 189-195) writes Zweig’s volume ratio as “greater than 9”, the strictuv.GT(dv.Mul(9)), where Morris’s “reaches” and StockCharts’ “at least 9 times greater” are inclusive. For Connors and Alvarez’s days (the second variant) the dossier’sd.Div(a).GTE(2)misses a day without advances and admits the exact 2:1 day their tables exclude, and its 2:1 day under the 200-day average is a combination the tables do not report. - Variant: Hughes’s selling climax (through Morris: “declines were over 70% of total issues while advances were less than 15% of total issues”), strict,
dsh.GT(0.70).Mul(ash.LT(0.15)), and Appel’s own, inclusive (“At least 70%”, “No more than 15%”),dsh.GTE(0.70).Mul(ash.LTE(0.15)). - Variant: Larry Connors and Cesar Alvarez’s washout conditioners (How Markets Really Work, Wiley, 2nd ed., 2012, ch. 5, Tables 5.1 and 5.2), strict and multiplied out as their Rule 1 column prints them (“Adv*2 < Dec for one day”, “Adv < Dec for 2 days”):
d.GT(a.Mul(2))andd.GT(a.Mul(3)), andd.GT(a).Sum(k).EQ(float64(k)), which fires on every bar of a run from its k-th (the mirrora.GT(d).Sum(k).EQ(float64(k))for advances). - Evidence: dated anecdotes only: Morris’s largest up-volume ratio on 17 August 1982 and down volume “over 500 times” up volume on 19 October 1987; no list of Zweig’s 9-to-1 dates was found besides that 1982 day (Winning on Wall Street, 1986, was not read), and no source prints breadth inputs beside its climax days. Connors and Alvarez’s tables (1 August 1996 to 30 September 2011, average returns over a day, two days and a week, no costs) are descriptive; their counts balance, 2,092 - 1,240 = 852 runs of advance days against 1,724 - 872 = 852 runs of decline days on the S&P 500, as the every-bar reading of “for 2 days” predicts. Evidence, not a reference.
- Pinned by
TestRecipeT6BreadthClimax; runnable exampleExampleSeries_Sum_doubleNine.
Eliades’s sign of the bear
Morris (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 5, “Eliades Sign of the Bear”) gives Peter Eliades’s three rules: “a streak of 21-27 consecutive days (trading) where the daily advance/decline ratio remains above 0.65 and below 1.95”, ending “with a downside break which means the advance/decline ratio is less than 0.65”, and a break “confirmed by either a two-day average advance/decline ratio or a three-day average advance/decline ratio following the end of the streak being below 0.75”. Rick Ackerman (“The Sign of the Bear”, a San Francisco Examiner column of 2000) asks for “a two- or three-day average immediately thereafter of less than 0.75”; Eliades’s own paper was not found. t6SignOfTheBear gives the break brk and sob, the first bar on which the two-day or the three-day average of the ratio from the break day is below conf.
inband := a.GT(d.Mul(lo)).Mul(a.LT(d.Mul(hi)))
prior := inband.StateAge().Shift(1)
seen := inband.EQ(0).CumSum().Shift(1).GT(0)
brk := inband.Shift(1).Mul(prior.GTE(float64(minLen))).Mul(prior.LTE(float64(maxLen))).Mul(a.LT(d.Mul(lo))).Mul(seen)
r := a.Div(d).MaskNonFinite(d.Log())
c2 := brk.Shift(1).Mul(r.Average(2).LT(conf))
c3 := brk.Shift(2).Mul(r.Average(3).LT(conf))
sob := c2.Add(c3.Mul(c2.Shift(1).EQ(0)))- Inputs: advances
aand declinesd(NYSE, as every source has it);lo0.65 andhi1.95, a strict band (“above”, “below”);conf0.75, strict (“below”, “less than”);minLen21 andmaxLen27 days (Morris: “Originally it was just 21 days”). The band and the break are multiplied out; the confirmation averages the ratior, NaN on a day without declines. The confirmation reading is pinned: the streak ends on its last in-band day, so the averages start on the break day. - First valid index:
brk22 at the earliest (bar 0 out of band, bars 1 to 21 in band),sob23; both are flags and read 0 before.seenkeeps only a streak with an out-of-band day before it inside the data: a streak that runs back to bar 0 is left-censored (StateAge’s first spell is a lower bound) and does not count. - Anchors: with
dof 100 anda100 times the ratio: three days at 2.5, 23 at 1.0, then 0.5, 0.9 and 0.8 break on bar 26 (whereprioris 23) and signal on bar 27 (two-day average 0.70); a 23-day streak from bar 0 and then 0.5 gives no break (left-censored); a 28-day streak gives none; one day at 2.5, 21 at 1.0, then 0.6, 1.0 and 0.6 break on bar 22 and signal on bar 24 (the two-day 0.80 fails, the three-day average of about 0.733 confirms); a streak ended by a day at exactly 0.65 gives no break (that day is neither in band nor below 0.65); and after the break of the first tape, ratios of 0.5, 1.0 and 0.75 make both averages exactly 0.75 and give no signal. - Traps: the dossier’s break,
prior.GTE(21).Mul(prior.LTE(27)).Mul(a.Ratio(d).LT(0.65)), lacksinband.Shift(1), sopriorcan be the length of an out-of-band run: after the break of a 23-day streak followed by 25 days at 0.5 it fires four more times inside that run, the pinned form not at all. A zero-decline day is not a break: its ratio is infinite, out of band, and the multiplied-out tests read it so, whilea.Ratio(d)anda.Div(d)read 0 there, below 0.65. A zero-decline day inside the confirmation window voids the confirmation: the masked average is NaN and compares false (a break on bar 26 with no declines on bar 27 gives no signal), as an infinite ratio would, where the unmasked 0 would pull the average below 0.75. - Variant: the other reading, averages of the days after the break,
brk.Shift(2).Mul(r.Average(2).LT(conf))andbrk.Shift(3).Mul(r.Average(3).LT(conf)), combined assobcombinesc2andc3; on the first tape the two days after the break average 0.85 and give no signal, where the pinned reading confirms on bar 27. - Evidence: dated anecdotes on NYSE breadth that is not supplied, and the counts disagree: Morris writes that there have “only been 7” signals since the late 1920s, the last two in April 1998 and September 2000; Ackerman counts “six instances” since 1929, after which the Dow Jones Industrial Average fell 41.6% on average; BWFA (“Sign of the Bear?”, 1 June 1998) says “only 6 times” in over 70 years and dates them 19 July 1929, 8 December 1961, 25 January 1966, 17 October 1968, 6 December 1972 and 6 April 1998; JustSignals (11 November 2017) writes “8 occurrences in the past 88 years” while listing nine dates, among them 15 September 2000, 10 July 2001 and 25 October 2017; its comments record that Eliades acknowledged two failed signals, in 1993 and 1994, after which the rules changed, so the counts rest on different rule sets. Evidence, not a reference.
- Pinned by
TestRecipeT6SignOfTheBear; runnable exampleExampleSeries_StateAge_signOfTheBear.
Northington’s correction and surge signals
Kirk Northington (Volatility-Based Technical Analysis, Wiley, 2009, ch. 13, “Correction and Surge: An Early Warning System”, pp. 333-355, and Appendix A, pp. 420-423) divides “the NYSE declining issues traded” by “a five-period simple moving average of the total issues traded” and calls a Correction above 0.805 (0.76 on the NASDAQ) and a Surge when the advances over the same average are above 0.77 (0.73), all strict in his MetaStock and EasyLanguage code. His MSW_Correction_Surge function adds 1 for a NASDAQ correction and 2 for an NYSE one and subtracts 1 and 2 for the surges, and “The signal is most significant and potent when no other Correction signals have occurred for at least 15 days” (p. 342). t6CorrectionSurge gives that score and first, a correction on either exchange with none on the quiet bars before.
avgN := a.Add(d).Add(u).Average(5).SetN(4, math.NaN())
avgQ := aq.Add(dq).Add(uq).Average(5).SetN(4, math.NaN())
corrN := d.Div(avgN).GT(0.805)
corrQ := dq.Div(avgQ).GT(0.76)
surgeN := a.Div(avgN).GT(0.77)
surgeQ := aq.Div(avgQ).GT(0.73)
score := corrQ.Add(corrN.Mul(2)).Sub(surgeQ).Sub(surgeN.Mul(2))
corr := corrQ.Add(corrN).GT(0)
first := corr.Mul(corr.Shift(1).Sum(quiet).EQ(0))- Inputs: the NYSE’s advances, declines and unchanged issues
a,danduand the NASDAQ’saq,dqanduq, paired by position across the two exchanges;quiet15 (“at least 15 days”; Appendix A’s “the first after three weeks without one”). The four levels are the code’s literals, and the current bar’s total is inside its five-day average, as in both listings. Northington warns that “different sources can produce different data” and that “you may need to adjust your thresholds accordingly”. - First valid index: 4: the five-day averages are masked to NaN on bars 0 to 3, so the four flags and the score read 0 there (comparisons with NaN), not NaN.
firstis left-censored until 4 +quiet(19 at 15): before that its quiet window reaches bars that read 0 for want of data. - Anchors: the score equals a loop transcription of the EasyLanguage
MSW_Correction_Surgeon every bar (integers from -3 to 3, exact), and is 3 only when both corrections fire and neither surge does. On a constructed tape with NYSE corrections on bars 5 and 20 the score is 2 on those bars and 0 elsewhere, andfirstfires on bar 5 only (bar 5 lies inside bar 20’s quiet window); ratios of exactly 0.805, 0.76, 0.77 and 0.73 do not fire. - Traps: the dossier’s
d.Div(a.Add(d).Add(u).Average(5)).GT(0.805)has no mask:Average(5)passes the total through on bars 0 to 3, which then compare the same-day share of declines with the level. A quiet test onBarsSince,corr.Mul(corr.Shift(1).BarsSince().GTE(float64(quiet))), reads NaN before the first correction and so drops the first correction in the data, whichfirstkeeps. Nothing in the book supports the dossier’s “(or 21)” quiet days: it has 15 days and three weeks. The score is ambiguous, as the source’s is: 1 is a NASDAQ correction alone or an NYSE correction with a NASDAQ surge, and 0 can hide offsetting events. His Surge rule (“wait for there to be two Surge signals that are more recent than the most recent Correction signal”) also asks that they be “relatively close together”, which he does not quantify, so it is not pinned. - Variant: the Total events, which join the two exchanges with
Andin his code,corrQ.Mul(corrN)andsurgeQ.Mul(surgeN): “The Total Correction event has much greater predictive value than the individual NYSE or NASDAQ Correction events”. - Evidence: Table 13.1, “New Correction Event Performance” (p. 344): 15 first corrections in 15 days from 13 April 2004 to 26 June 2008 (of “a total of 39 individual Correction signals”), with the average close-to-close returns of the S&P 500 members 2 to 10 days later, split by the close against his S/R 5 level: -0.25% over every event and horizon, -0.81% for the Total events, and event averages from -2.6% to +2.1%, a tiny and mixed sample. The book’s companion workbook (Wayback Machine, 28 April 2017) matches the table to its rounding but holds no breadth inputs, so nothing reproduces a ratio. Evidence, not a reference.
- Pinned by
TestRecipeT6CorrectionSurge.
The breadth input of the compositions below is a universe of member series: each member’s trend flag, then the cross-section count of the flags on each bar. The members’ closes are caller-supplied series of one length, positive and finite, paired by position and right-aligned at the most recent bar as the Breadth introduction (“Breadth inputs”) says; lengths are in bars, counts in members and percents run from 0 to 100. The arithmetic needs no timestamps, but the member list, its history and its point-in-time membership are the caller’s, and a current list applied to past bars is survivorship-biased. Values are NaN before their first valid index F (SetN(F, math.NaN())) and flags read 0 there, as pct.GT(50) does. A bar with no trading is not an input bar. Threshold tests on one day’s ratio are written multiplied out (a.GT(d.Mul(4))), exact on integer counts and true on a zero-divisor day, where Div and Ratio read 0; averaged ratios mask zero divisors with MaskNonFinite(x.Log()) where a 0 would be a false extreme. Here the percent divides by the number of members (in the last variant by the valid members, masked where there are none). A flag is a decision at a bar’s close and acts at the next bar’s open, a fill that is the caller’s (see the Trade State introduction).
Members above and below their average (Pring; Morris)
Martin Pring (“Internal Market Momentum”, Stocks & Commodities V11:7, July 1993, pp. 298-305) calls “the percentage of a basket of stocks in a positive trend” a diffusion index, where the trend “can be any measurable technical tool, such as the percentage of stocks above a specific moving average or the number of stocks with a positive rate of change (ROC)”. Gregory Morris (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 11, “Percent Above Moving Average”, taken from StockCharts’ ChartSchool; the electronic edition has no page numbers) counts the stocks “above their XX-day moving average”, the 50-day average for “the short-medium term timeframe” and the 150-day and 200-day averages for “the medium-long term timeframe”. t6MemberTrend gives one member’s two flags: up is 1 where the close is above its n-bar simple average and dn where it is below, so a tie is neither up nor down.
avg := c.Average(n)
up := c.GT(avg).SetN(n-1, math.NaN())
dn := c.LT(avg).SetN(n-1, math.NaN())- Inputs: one member’s closes
c;nbars, 25 for Jani’s funds (next section), 50, 150 or 200 for Morris’s stocks. The average is simple: Jani’s “25-day moving average” names no type, and StockCharts’ catalogue lists$SPXA200Ras the “S&P 500 Percent Above 200-day SMA”. - First valid index: n - 1 for both flags, NaN before; a member shorter than n is NaN on every bar, since
Average(n)then returns a copy of the closes. - Anchors: no source prints member closes, so the anchors are identities. From bar n - 1, where the average is finite,
c.GTE(avg).Add(dn)is 1 on every bar, andup.Add(dn)is 1 except on a tie, where both flags read 0: a flat member is neither up nor down (thirty closes of 0.1 have a 25-bar average of exactly 0.1). The simple average reads only the last n closes, so the flags do not depend on where the history starts, to the rounding of the window sum. - Traps: the dossier’s unmasked
c.GT(c.Average(n))reads 0 on bars 0 to n - 2, whereAveragepasses the close through, so an unmasked member counts as neither above nor below and pulls the percent above (and Jani’s count) towards 0 during its warm-up. ItsGTEfor “above” counts a tie as above: that is the complement of Jani’s “below”, not Morris’s or ChartSchool’s “above”. A NaN close reads 0 in both flags, not NaN, on its own bar and while it is in the average’s window, so a missing price counts as neither up nor down. - Variant: Pring’s rate-of-change test,
c.Momentum(k).GT(0).SetN(k, math.NaN()), first valid at k: on positive finite closes it equalsc.RateOfChange(k).GT(0)andc.GT(c.Shift(k))bit for bit from bar k, while before bar k the first two read 0 and the third compares with the +0 pad and reads 1, hence the mask. A zero change is not “a rate of change above zero”. - Variant: the exponential form of Morris’s Charts 11-2 and 11-6,
c.GT(c.XAverage(n)).SetN(n-1, math.NaN()), which “will not show the high and low readings that you get when using the SIMPLE moving average”. It is start-dependent (the recursion remembers the first bars), and its products can fuse on arm64, so a flag near a tie can differ between architectures. - Evidence: Arthur Merrill (“Stocks above moving averages”, Stocks & Commodities V8:5, May 1990, pp. 204-205) tested Investors Intelligence’s weekly shares of NYSE issues above their 10-week and 30-week averages over about ten years to 1989: the 10-week share was “right 61% of the time” 26 weeks ahead, and the 30-week share, read contrarily, earned “a score of 66% correct” a year ahead; in-sample, no costs stated. StockCharts’ published
$SPXA200Rvalues cannot be reproduced without the member closes and the membership history. Evidence, not a reference. - Pinned by
TestRecipeT6MemberTrend.
The diffusion count (Jani) and the percent above (Pring; Morris)
Ajay Jani (“It’s In The Sentiment”, Stocks & Commodities V24:8, August 2006, pp. 28-31) counts 15 Rydex sector funds: “Just add up the number of funds that are below their respective 25-day moving average”, so that “your sentiment index will be between zero and 15”. Pring’s diffusion index is the same count as a percentage, and Morris divides “the number of stocks above their XX-day moving average by the total number of stocks in the underlying index” (“Nasdaq 100 example: 60/100 = .60 or 60%”), reading “crosses above/below 50%”. t6DiffusionCount gives the number of members whose flag is set, recovered exactly from the flags’ average, and that count in percent of the members.
m := float64(len(rest) + 1)
count := first.SAverage(rest...).Mul(m).Round()
pct := count.Mul(100).Div(m)- Inputs: the members’ 0/1 flags from
t6MemberTrend,firstandrest, one per member and all of one length; with the flags in a slice built by a loop over the members in user code,t6DiffusionCount(flags[0], flags[1:]...). Jani’s index is the count of 15 funds’dnflags at n = 25, from 0 to 15; Morris’s percent above uses theupflags at n = 50, 150 or 200, and also reads “bullish/bearish divergences”: withpctas the partner of the index, that is the test of “Relative-strength non-confirmation” on the Relative: Divergence page. - First valid index: that of the flags, n - 1 for
t6MemberTrend’s; NaN before it and on any bar where a flag is NaN.countis an integer from 0 to the number of members andpctruns from 0 to 100. - Anchors:
countis exact for every number of set flags up to the number of members (tested up to 500 members) and is unchanged, bit for bit, by any reordering of the members: on 22 members with 15 set it is 15 where the average times 22 is 14.999999999999998.pctis the correctly rounded 100 times the count over the members, one division of exact integers: 7 of 25 members give exactly 28, and Morris’s divisions, 60 of 100, 80 of 500 and 7 of 30, give 60, 16 and 23.333333333333332. With no ties the count ofupflags plus the count ofdnflags is the number of members exactly; the two percents add to 100 only to rounding. - Traps: without
Round,first.SAverage(rest...).Mul(m)is not an integer for some member counts: 22 members with 15 set give 14.999999999999998, and 25, 50 and 100 members fail too. It is exact for every count up to 21 members, so the dossier’s average times 15, withoutRound, is right for Jani’s 15 funds by luck of the number. The pandas formfirst.SAverage(rest...).Mul(100)gives 28.000000000000004 at 7 of 25 members wherecount.Mul(100).Div(m)gives 28, so a strict level test such aspct.GT(28)would fire on it. A shorter member truncates the overlap and zero-fills:SAverageright-aligns its members, keeps only the trailing bars common to all and writes 0, not NaN, before them (Series{1, 1, 1, 1}.SAverage(Series{1, 1})is 0, 0, 1, 1), so one late listing shortens the whole index and its zero prefix reads as a count of 0.Overlappairs two series only; no method aligns many members beyondSAverage’s own right alignment. - Variant: Morris’s smoothing and 50% line,
pct.Average(20).SetN(n+18, math.NaN())with n the flags’ average length, and the crossingspct.CrossesAboveS(50)andpct.CrossesBelowS(50), which read 0 on NaN bars and so give no false crossing at F; the mask covers n < 20, where bars n - 1 to 18 would pass the raw percent through. The smoothing is Morris’s (“The pink line shows the 20-day SMA of the indicator”), not Pring’s: the 1993 article says nothing about smoothing a diffusion index. - Variant: for unequal histories or changing membership (documentation only), pad each member’s closes with NaN at the front to the common length and, with
avgthe masked averagec.Average(n).SetN(n-1, math.NaN()), take the flagc.GT(avg).Mul(w)and the validity flagavg.EQ(avg).Mul(w),wan optional point-in-time membership flag (1 while a member); count both lists witht6DiffusionCount(countandvalid) and divide,count.Mul(100).Div(valid).MaskNonFinite(valid.Log()), NaN where no member is valid. - Evidence: Jani’s Figure 3 gives the S&P 500’s average return over the 30 trading days after each reading, 0.73% after 0 and 3.49% after 15 against -1.69% after 5, a relationship his Figure 4 shows as “not linear but U-shaped”, entering at the close of the next business day without dividends or commissions, on data from 18 December 1998 to 11 April 2006; the 0.22% benchmark’s period is printed “December 1998 to April 2005”, against April 2006 everywhere else. No fund closes, counts per reading or significance tests are printed, and the readings are in-sample. Evidence, not a reference.
- Pinned by
TestRecipeT6DiffusionCount; runnable exampleExampleSeries_SAverage_diffusionIndex.
The recipes below read the counts of new 52-week highs and new 52-week lows, with the index whose highs they qualify. Their inputs are caller-supplied series paired by position and right-aligned at the most recent bar, as the Breadth introduction (“Breadth inputs”) says, under its names a, d, u, ti, uv, dv, nh and nl: counts are non-negative integers, volumes finite and non-negative, the index in points, the logic ratios in percent of issues traded and lengths in bars (252 for 52 weeks of daily bars). The counts must be rolling 52-week counts, which the NYSE has reported consistently since early 1978; before then a new high or low was measured from the start of the year (base periods of 2-1/2 to 14-1/2 months), and such calendar-year counts distort the levels. A bar with no trading (all counts 0) is not an input bar. Values are NaN before their first valid index F (SetN(F, math.NaN())), and flags, latches and positions built on them read 0 there. Threshold tests on one day’s ratio are written multiplied out, as Meyers’s code writes them (d.GTE(a.Mul(2)), not d.Div(a).GTE(2)): exact on integer counts, and true on a day without advances, where Div and Ratio read 0 and would miss the most extreme day. An averaged ratio masks a zero divisor with MaskNonFinite(x.Log()) where a 0 would be a false extreme; Kaeppel’s share of new highs keeps Div’s 0 by convention, as its section states. Signals are known at the close of their bar and act at the next bar’s open, a fill that is the caller’s.
High low logic index (Fosback) and the JK HiLo index (Kaeppel)
Norman Fosback (Stock Market Logic, Dearborn Financial Publishing, 1992 printing, ch. 20, “High Low Logic Index”, pp. 76-80) takes “simply the lesser” of new highs and new lows as percents of issues traded and smooths the weekly values (“A 10-week exponential moving average is useful”); his chapter “All You Ever Wanted to Know About Moving Averages” (ch. 41, from p. 139; the exponential average on pp. 142-143) seeds that average “equal to the first day’s price” and finds its constant by the rule “divide two by eleven”, printed as 0.18. Jay Kaeppel (“The JK HiLo Index”, Stocks & Commodities V29:11, October 2011, pp. 12-18) averages the same daily ratio (“F = 10-day simple moving average of E”), keeps it where “F >=2.15 or if F <= 0.40” and enters 1.00 otherwise, and multiplies by the 10-day simple average of NH/(NH + NL) and by 100 (“J = (G * I) *100”). t6HighLowLogic gives the ratio e, Fosback’s index hlli with the smoothing constant alpha, Kaeppel’s F as f and his index J as j.
e := nh.Minimum(nl).Div(ti).Mul(100)
hlli := e.XAverageAlpha(alpha)
f := e.Average(n).SetN(n-1, math.NaN())
ext := f.GTE(hi).Add(f.LTE(lo))
g := f.Mul(ext).Add(ext.EQ(0))
j := g.Mul(nh.Div(nh.Add(nl)).Average(n)).Mul(100)- Inputs: new highs
nh, new lowsnland issues tradedti(the Breadth introduction’sa.Add(d).Add(u); one MetaStock listing substitutes advances plus declines), integer counts of one exchange paired by position: weekly counts for Fosback’s index (the week’s new highs over the week’s issues, not sums of daily counts), daily counts for Kaeppel’s (NASDAQ).n= 10;alpha= 2/11 by default, Fosback’s rule, or 0.18, his printed constant;hi= 2.15 andlo= 0.40, Kaeppel’s. The same code on daily counts is Fosback’s daily form (Table 27A, a ten-day exponential average). Fosback reads the smoothed weekly index as bullish below 1% and bearish above 5% (hlli.LT(1),hlli.GT(5)), and a double-digit weeklye(e.GTE(10)) as having “presaged every major bear move in the last quarter century”. - First valid index: 0 for
eandhlli(Fosback’s seed is the first value: no warm-up), n - 1 forfandj, NaN before. The gate passes the NaN through (NaN*0 + 1is NaN), sojis NaN wherefis and level flags onjread 0 on bars 0 to n - 2.fandjdepend only on the last n bars;hllikeeps the first value with weight (1 - alpha)^t, so it depends on where the history starts. - Anchors: Fosback’s week, 227 new highs and 122 new lows of 2,128 issues, gives
e=hlli= 5.7330827067669166 on the single bar, his printed 5.7%, against 10.667293233082708 for the new highs, his 10.7%;fandjare NaN on a one-bar series. His Table 46 (p. 143) witnesses the average itself and is pinned on the method, not through the helper, whoseewould not carry the prices bit for bit: the prices 10, 12, 13, 15, 18, 22, 28, 32, 37 and 40 throughXAverageAlpha(0.18)round to all ten printed values, 10.00, 10.36, 10.84, 11.58, 12.74, 14.41, 16.85, 19.58, 22.72 and 25.83, whileXAverageAlpha(2.0 / 11)gives 11.60 on day 4 and rounding at each step, as the table’s method column suggests, gives 11.59. The gate equals a loop (orIf) bit for bit on a finitefand keeps a NaNfNaN. With 43 new highs and 43 new lows of 2,000 issues on every bar,fis exactly 2.15 andj107.5 from bar 9; with 1 and 1 of 250,fis 0.40 andj20: the gate is inclusive.jlies between 0 and 100 where the gate reads 1, is at most 40 wheref<= 0.40 and exceeds 100 only wheref>= 2.15;eis at most 50 when nh + nl <= ti.nh.Minimum(nl).Div(ti)equals Fosback’s order,nh.Div(ti).Minimum(nl.Div(ti)), bit for bit (division by a positive number is monotone). - Traps: the dossier’s minimum
nh.Add(nl).Sub(nh.Sub(nl).Abs()).Div(2)agrees withMinimumonly on integer counts: on percent ratios it differs in the last bit on about three in five random pairs (the formhr.Sub(hr.Sub(lr).Max(0))on about one in seven), and an infinite operand makes it NaN.XAverage(10)is not Fosback’s average: it passes the input through on bars 0 to 8 and seeds with the simple mean at bar 9 (on Table 46 it gives the prices to day 9, then 22.70).f.If(...)for the gate reads 1.00 on a NaNfand so unmasks the warm-up. Kaeppel’s plain-English bullet (“if today’s value for variable F is not greater than 2.15 or less than 0.40, we simply enter a value of 1.00”) and AIQ’s listing (avgHLidx > 2.15 or avgHLidx < 0.40) are strict, where his equation and six of the seven listings are inclusive: the helper follows the equation, and ties are reachable on integer counts (43 of 2,000 is exactly 2.15). His bullet for E also drops the times 100 that the equation prints (“E = (D / A) * 100”). H = NH/(NH + NL) reads 0 on a bar with no new highs and no new lows (Div), the convention of the share on the Relative: Ratio and Relative Strength page (“Normalised difference and share”, a recipe, not a method): Kaeppel and every listing are silent, and such a bar counts as all lows and pullsjtowards the buy level. The guardnh.Div(nh.Add(nl)).MaskNonFinite(nh.Add(nl).Log())makes the bar NaN instead, andjNaN on the n bars whose average holds it. Fosback’s Tables 26 and 27A are headed “10-WEEK MOVING AVERAGE” and “10-DAY MOVING AVERAGE” while his text says the smoothing is exponential; Kaeppel dates the index to Fosback’s “1975 classic work”, where Fosback writes that it “was invented by the author in 1979”. The MetaStock listings average NNH/ti, not H: not followed. - Variant: Kaeppel’s levels, read on every bar as his Figures 4 and 5 count days:
j.LTE(20)(a “buying opportunity”),j.GTE(90)(a “selling opportunity”),j.LT(10)(“an important bottom is in the process of forming”) andj.GT(100)(spikes “above, say, 100”), each split by the index closecagainst its 200-day simple average (the type is not stated), asj.LTE(20).Mul(c.GT(c.Average(200).SetN(199, math.NaN()))), where buys “have almost invariably been timely”; the crossingsj.CrossesBelowS(20)andj.CrossesAboveS(90)are the TradeStation listing’s alerts. - Variant: Gregory Morris (The Complete Guide to Market Breadth Indicators, 2nd ed., 2015, ch. 7) smooths the daily ratio “exponentially … by 50 days”,
e.XAverage(50).SetN(49, math.NaN()), and builds the Low High Logic Index on the higher ratio,nh.Maximum(nl).Div(ti).Mul(100).XAverage(50).SetN(49, math.NaN()), with “a decision zone at 10”; Gerald Appel (“Gerald Appel, With Systems and Forecasts”, Stocks & Commodities V12:3, March 1994) sells when the unsmoothed weekly ratio is “7.5% or greater”,e.GTE(7.5), and Morris’s 2.4 for Appel’s form,e.GT(2.4), is a level he set on daily data, not a weekly rescaling. - Evidence: Kaeppel’s Figures 4 and 5 (NASDAQ since 1990, 5,703 days): the 63-day index change averaged +8.4% after the 781 days with J at or below 20 and -5.0% after the 221 days at or above 90, against +2.8% on all days; average changes, not trades, on one market, with the levels and the gate chosen on the same history. Fosback’s Tables 26, 27 and 27A (NYSE, weekly 1944 to 1980, daily 1965 to 1980): the smoothed weekly index under 1% preceded a three-month total return of +7.6% and over 5% one of -1.4%, against +3.6% on average, and Table 27 lists 19 double-digit weekly readings from 1941 to 1980; in-sample, partly on calendar-year counts. Evidence, not a reference.
- Pinned by
TestRecipeT6HighLowLogic; runnable exampleExampleSeries_Minimum_highLowLogic.
The Titanic warning (Ohama)
Bill Ohama and Melanie Bowman (“Patterns that detect stock market reversals”, Stocks & Commodities V6:11, November 1988, pp. 416-421) see a major top when the “DJIA makes new high for the year and, no more than 7 trading days before or after the DJIA high, the number of yearly new lows on the NYSE Composite Index exceeds new highs” (their Figure 3; “The NYSE defines yearly as the past 52 weeks”). Dennis Meyers’s code (“Making The Titanic Fly”, Stocks & Commodities V13:5, May 1995, pp. 189-195, sidebar “TradeStation code”) reads the new high as the day’s high strictly above the 252 highs before it, if h > hhv[1] with hhv=highest(h,lookbk), and the coincidence over the bar and the seven before it, for value0 = 0 to 7. t6TitanicWarning gives the new-high flag hi and the warning warn, 1 on the later of a new high and a day with more new lows than new highs when the two are at most w - 1 bars apart, either first.
hi := h.Breakout(n, 1).GT(0)
bad := nl.GT(nh)
warn := hi.Highest(w).Mul(bad).Add(bad.Highest(w).Mul(hi)).GT(0)- Inputs: the index highs
h(the Dow) and the exchange’s new highsnhand new lowsnl(the NYSE), daily and paired by position;n= 252, the bars of 52 weeks standing for Ohama’s “for the year” (a calendar-year high is not built), andw= 8, a lag of at most seven bars. Meyers notes that the second half “would have to be modified for dates before January 1, 1978”. The warning is breadth failing to confirm the index’s high; the Relative: Divergence page’s “Relative-strength non-confirmation” is a different test, a high that a stock’s own ratio line does not confirm. - First valid index: n for
hiandwarn, which read 0 before it (Breakoutgives +0 there, and NaN where its window holds a NaN, whichGT(0)reads as 0). A series of n bars or fewer gives zeros throughout, so a test of a dated top needs at least n + 1 bars before it.badcounts from bar 0, so a bad day up to w - 1 bars before bar n takes part. - Anchors: after a constructed prefix of 253 bars (highs below 2746.65, more new highs than new lows), Ohama’s days from 25 August to 8 September 1987 (Labor Day, 7 September, is not a bar) give
hion 25 August only, his printed 2746.65, andwarnon 2 September only, six bars later, with 24 new highs against 31 new lows; the ten days alone give no warning, asBreakout(n, 1)needs n + 1 bars. On fixtures with n = 3, a bad day seven bars after a new high fires on its bar and one eight bars after does not; a bad day five bars before a high fires on the high’s bar (Ohama’s 1966 order, lows first); bad days two, four and nine bars after a high fire on the first two.warnequals a brute-force loop over every pair of a new high and a bad day at most w - 1 bars apart, firing on the later of the two;h.Breakout(n, 1).GT(0)equalsh.GT(h.Highest(n).Shift(1).SetN(n, math.NaN()))bit for bit. - Traps: the run
hi.Highest(w).Mul(bad.Highest(w))is not the warning: it is 1 from the later event until the earlier one leaves the window (on 2 and 3 September 1987), and it differs from the event form in most of 400 random flag pairs. It is Meyers’s per-bar test, which his code latches:hi.Highest(w).Mul(bad.Highest(w)).SumSince(hi).GT(0)equals the next section’ss2bit for bit.Highest(7)allows a lag of six bars only. The dossier’sc.GTE(c.Highest(252))is unmasked and inclusive: it fires on bar 0 and on every running maximum of bars 0 to 250, and on a tie with the year’s high, which Meyers’s strict>does not count. A centred 15-bar window delayed seven bars reports the warning seven bars late; the later-event form is causal as it stands. Ohama’s Figure 6 prints 2746.65 for the Dow on 25 August 1987 and his Figure 1 prints 2746, Morris (ch. 9, who spells the author “Omaha”) gives 2746.45, and UPI reported a peak of 2722.42 that day; the article does not say what kind of value 2746.65 is, and the 1987 tape uses it as printed. Ohama’s “Four business days later, on May 21” (1965) is a five-bar lag; his six tops (lags 1 to 6) fit a window of seven or eight alike, so the window rests on “no more than 7” and Meyers’s loop. The 400-point rally that also starts Ohama’s warning is in nominal points and is not built. - Variant: with closes only,
c.Breakout(n, 1).GT(0)forhi, a close above the n closes before it. - Evidence: Ohama’s Figure 1 lists six tops (1965, 1966, 1973, 1976, 1983 and 1987) whose two halves came 1 to 6 trading days apart, each followed by a Dow decline of 10% to 45%; it counts no warning that was not followed by a decline and names no exit. Evidence, not a reference.
- Pinned by
TestRecipeT6TitanicWarning; runnable exampleExampleSeries_Breakout_titanicSyndrome.
Meyers’s flying Titanic signals
Dennis Meyers (“Making The Titanic Fly”, Stocks & Commodities V13:5, May 1995, pp. 189-195, Figure 7 and the sidebar “TradeStation code”) restates Ohama’s confirmations as ratios, since “it’s relatively easy today for advances and declines to be greater than 1000”: after the warning, sell on two consecutive days with declines “greater than or equal to two times advancing issues” and one at five times, on “any three out of a four-day period” of declines above 1.75 times advances, or on down volume “greater than nine times” up volume; after a “9% drop in the DJIA from the 52-week high”, buy on the mirrored breadth conditions or on up volume at least nine times down volume with no down-volume spike on the two days before. His code holds each sell condition from the latest new high, which restarts the cycle, sells once per cycle, and arms the buys from a close below the 252-bar highest high less 9% (c < hhv-(tenpct*hhv)) until a buy condition fires. t6FlyingTitanicStop gives sell, buy and book, the stopless position of the two (1 long, -1 short, 0 before the first signal, a sell winning on a bar with both), and pos, the book with the code’s 2% stop and reverse (“A sell signal reverses to a buy signal if the yearly new high (the new high that generated the sell signal) is penetrated on the upside by 2%”) on PositionSignedRules with StopReverses: true: the short stop is 1.02 times the high of the latest new high, sampled on the sell bar and frozen while short (a second sell while short keeps it), and a high at or above it reverses the short to long, as a buy does.
hi := h.Breakout(n, 1).GT(0)
bad := nl.GT(nh)
warn := hi.Highest(w).Mul(bad).Add(bad.Highest(w).Mul(hi)).GT(0)
s2 := warn.SumSince(hi).GT(0)
two := d.GTE(a.Mul(2))
five := d.GTE(a.Mul(5))
s3 := two.Mul(two.Shift(1)).Mul(five.Add(five.Shift(1)).GT(0))
s4 := d.GT(a.Mul(1.75)).Sum(4).SetN(3, math.NaN()).GTE(3)
s5 := dv.GT(uv.Mul(9))
cand := s2.Mul(s3.Add(s4).Add(s5).SumSince(hi).GT(0))
sell := cand.Mul(cand.Shift(1).EQ(0).Add(hi).GT(0))
hhv := h.Highest(n).SetN(n-1, math.NaN())
drop := c.LT(hhv.Sub(hhv.Mul(0.09)))
twoB := a.GTE(d.Mul(2))
fiveB := a.GTE(d.Mul(5))
b2 := twoB.Mul(twoB.Shift(1)).Mul(fiveB.Add(fiveB.Shift(1)).GT(0))
b4 := a.GT(d.Mul(1.75)).Sum(4).SetN(3, math.NaN()).GTE(3)
ok := dv.LT(uv.Mul(9))
b5 := uv.GTE(dv.Mul(9)).Mul(ok.Shift(1)).Mul(ok.Shift(2))
anyB := b2.Add(b4).Add(b5).GT(0)
armed := drop.Sub(anyB.Mul(2)).Hysteresis(0, 0).GT(0)
buy := armed.Shift(1).Add(drop).GT(0).Mul(anyB)
book := buy.Sub(sell.Mul(2)).Hysteresis(0, 0)
pos := h.PositionSignedRules(buy, nil, sell, nil, PositionRules{
StopReverses: true,
ShortStop: PositionLevel{Level: h.ValueWhen(hi).Mul(1.02), Touch: h},
})- Inputs: the Dow’s highs
hand closesc, and the NYSE’s advancesa, declinesd, up and down volumeuvanddv, new highsnhand new lowsnl, daily and paired by position;n= 252 andw= 8 as in the warning. The 9% drop, the 2 and 5 times, the 1.75 times on three of four days and the 9 times volume are the code’s literals (tenpct,ad2x,ad4x,adfx,adv9x), and the comparisons are the code’s:>=for the two-day tests andb5’s own test,>for the three-of-four tests ands5,<for the drop and the prior-day spike testok. Precondition: no bar of zero breadth. A bar with a = d = 0 meets bothd.GTE(a.Mul(2))anda.GTE(d.Mul(2)), and one with uv = dv = 0 meetsuv.GTE(dv.Mul(9)), so such a bar fires spuriously: drop it before the call or mask each flag, astwo := d.GTE(a.Mul(2)).Mul(a.Add(d).GT(0))(withuv.Add(dv).GT(0)for the volume tests). - First valid index: n for
sell(the new high needs n + 1 bars) and n - 1 forbuy(the n-bar highest high is masked to bar n - 2); both read 0 before, andbookandposread 0 until the first signal.s4andb4mask the partial sums of bars 0 to 2, whereSum(4)would count three of three as three of four;s2andcandread 0 before the first new high (SumSinceis NaN there). The latches depend on the latest new high or drop in the data andbookandposon every bar before, so none of the four outputs is start-invariant. - Anchors: on the 1987 tape of the warning,
sellfires on 2 September and on no other bar, through the four-day conditions4: declines above 1.75 times advances on 27 and 28 August and 1 September (1.88, 3.12 and 2.80 times) make it hold from 1 September, and the warning arrives on 2 September. The two-day conditions3first holds on 8 September (1,532 declines against 242, after 1,060 against 513 on 4 September, the bars either side of Labor Day), after the sell, and the volume conditions5does not hold (the tape’s volumes are constructed; none are printed);buydoes not fire, the closes staying above the 9% drop. The flags equal a loop transcription of the sidebar code, its position and stop lines removed, on the synthetic tape. At ties the code decides, and a constructed 46-bar tape with n = 20 pins each tie: a count exactly 1.75 times the other does not count, down volume exactly nine times up volume is no sell condition but is a spike that blocksb5, up volume exactly nine times down volume and advances exactly twice declines count, and a second new high restarts the cycle (sells on bars 28 and 30, buys on 41 and 44). A close of 91 under a flat 252-bar high of 100 is not a drop (the test is a strict<), and a bar with both a drop and a buy condition buys and disarms, as the code sets the latch, tests the buys, then resets it.posequals a loop of the sidebar code with its stop on every fixture tape, its two departures (below) taken as the kernel takes them: on the synthetic tape the stop reverses three shorts, and one on each of its zero-bar and NaN copies. - Traps: the dossier’s
drop := djia.Ratio(djia.Highest(252)).LTE(0.91)is inclusive, unmasked and unlatched, and rounds otherwise than the code’shhv-(tenpct*hhv); itsadv.GTE(dec.Mul(1.75)).Sum(4).GTE(3)is inclusive where the code is strict and fires on bar 2 fromSum’s partial window; a ratio form such asd.Div(a).GTE(2)reads 0 on a day without advances, which the code counts. The “after the 52-week high” and “after the 9% drop” states are compositions,SumSince(hi)and the reset-winsHysteresis(0, 0)ofdrop.Sub(anyB.Mul(2));PositionandPositionSigneddo not fit the arming (both ignore an entry while in position, andPositiondoes not test an exit on its entry bar). The code buys and sells “on close” of the signal bar; here the fill is the caller’s, at the next open. With n = 20, a copy of the synthetic tape whose bar 47 is all zero sells on bar 47, one bar before the sell it gives without that bar: the precondition. The 2% stop departs from the code in two places, whichpostakes as the kernel does: the code also reads the stop while flat, against its initial value (djnhstop(h), the first bar’s high), and a sell from flat keeps that value, so the code buys back at once, wherepossamples the level on the sell bar (each fixture tape’s first signal is such a sell); and on a bar with a buy, or a stop, and a sell the code’s sell wins, processed last, whereposstays flat when flat and reverses a short to long (no fixture tape has such a bar). - Variant: Figure 7’s wording at ties, “greater than or equal to 1.75” in the three-of-four counts,
d.GTE(a.Mul(1.75)).Sum(4).SetN(3, math.NaN()).GTE(3)and its mirror, and “greater than nine times” for the volume buy,uv.GT(dv.Mul(9)).Mul(ok.Shift(1)).Mul(ok.Shift(2)); the published trades came from the code. - Variant: Ohama’s own confirmations (his Figure 3, and the 1991 criteria in Meyers’s Figure 1) count declines over 1,000: four days of seven,
d.GT(1000).Sum(7).SetN(6, math.NaN()).GTE(4), or two consecutive days with one below 1:4,big.Mul(big.Shift(1)).Mul(low.Add(low.Shift(1)).GT(0))withbig := d.GT(1000)andlow := a.LT(d.Mul(0.25)); on his 1987 counts the first holds from 2 September and the second first on 8 September, as his Figure 6 annotates. Meyers dropped them because issues traded had grown from “around 2000” to “around 3000”. - Evidence: Meyers’s Figure 9 (the Dow, daily, 3 January 1978 to 20 January 1995, TradeStation, fills on the close): 28 trades, 64% profitable, a net 3,989.72 points, among them the sell of 2 September 1987 at 2,602.00. The text says the first trade made “45.29 points”, then subtracts “the 49.29 points” for “a total net profit of 3949 points”, where the table gives 3,989.72 - 45.29 = 3,944.43; the rules were changed by hand on the same history. In “The Advance-Decline, New-High, New-Low Market System” (Stocks & Commodities V14:2, February 1996, pp. 69-75) the signals traded on the S&P 500 (1978 to September 1995) give 28 trades, 68% profitable and a profit factor of 4.33, and Meyers says his weekly system “ranks quite well” beside them. Evidence, not a reference.
- Pinned by
TestRecipeT6FlyingTitanic; runnable exampleExampleSeries_PositionSignedRules_titanicStop.