Momentum and oscillators
All functions · nseries package
Sections: Momentum · Rate of Change · Delta / Change · Velocity / Acceleration · RSI · Connors RSI · Stochastic · Williams %R · Percent R · Ultimate Oscillator · Casey Percent C · Commodity Channel Index · MACD · TRIX · Trend Continuation Factor · QStick · Fisher Transform
Momentum
CMO
func (s Series) CMO(n int) Series
CMO returns Chande’s momentum oscillator over n bars: 100 times the net change over the sum of the absolute one-bar changes.
With Su and Sd the sums of the up and down one-bar changes over the window, it is 100(Su-Sd)/(Su+Sd), signed, from -100 to 100; the numerator telescopes to the net change s[t] - s[t-n]. The result is s.Delta(n).Div(s.Delta(1).Abs().Sum(n)).Mul(100).SetN(n, 0), bit for bit, with plain sums. It is not clamped: rounding can carry a value up to two ulps beyond 100 or -100. A flat window gives 0 (Div’s zero rule) where Marisch’s TradeStation listing of Chande’s average keeps the previous ratio.
Chande and Kroll introduced it, as the net momentum oscillator, in “Stochastic RSI And Dynamic Momentum Index” (Technical Analysis of Stocks & Commodities, May 1993), and in The New Technical Trader (1994); their Figures 1 to 4 use the closes 375.22, 381.40, 380.96, 380.07 and 377.39, on which CMO(4) at the last bar is 100 times 2.17/10.19, or 21.295388 at six decimals. The plain sums are those of pandas-ta (talib=False) and TradingView’s ta.cmo; TA-Lib’s CMO smooths the up and down changes with Wilder’s average instead, so its values differ.
It is the signed efficiency ratio times 100: s.CMO(n).Abs() equals s.EfficiencyRatio(n).Mul(100) bit for bit on prices whose one-bar changes and window sums are exact, such as prices on a quarter grid, and within 2 ulps in general. s.CMO(n).Abs().Div(100) is within 2 ulps of s.EfficiencyRatio(n) from bar n but not bit for bit, since multiplying by 100 and dividing by 100 is not an identity in binary64. VIDYACMO is the exponential average whose constant this ratio drives. CMO and its recipes are under “Momentum” in the manual, and VIDYACMO is under “Moving Averages”.
The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n, as for EfficiencyRatio(n). Validation: n < 1 or n >= len(s) gives len(s) zeros, and an empty series gives an empty result.
With a NaN close at bar p, bars max(p, n) to p+n are NaN. An infinite close at p gives NaN at bar p+n and, when p >= n, at bar p (an infinite net change over an infinite sum); the bars between read a zero signed as the net change (a finite change over an infinite sum), and bars 0 to n-1 stay +0. Every other bar is unchanged. Finite closes whose difference overflows count as an infinite change: on -math.MaxFloat64 and math.MaxFloat64, CMO(1) is NaN at bar 1. CMO is causal and window-local up to the rolling sum’s rounding: Sum keeps compensated partial sums whose grouping depends on where the series starts, so a later start could move the last bit, though not where the window sums are exact, as on a binary grid. It costs O(1) a bar and returns a new Series without modifying the receiver.
Momentum
func (s Series) Momentum(n int) Series
Momentum returns a series calculated as the difference between the current price and the price n bars ago,
The warm-up reads 0, not NaN: bars 0 to n-1 are 0, no change.
TrendContinuationPlus(n) is at most this momentum in exact arithmetic, the window sum of the up changes less the down-run losses, and TrendContinuationMinus(n) at most its negation.
Compositions
Two short-term setups read from the last few daily bars: Ait Hellal and Zhang’s eight-bar continuation, a pullback from a 60-day extreme bought or sold on a one-bar stop, and Taylor’s 2-period rate-of-change flip, a latched long or short state with the pivot the next close must beat. Recipe values, levels and results are NaN before their first valid index (SetN(F, math.NaN())), and flags, states and latches read 0 there: every shifted series a pad can satisfy is masked, or the flag is (the 60-day high is Breakout’s, which reads 0 before its own first valid index). Prices are positive and finite, on daily bars, and series are paired by position (the caller aligns them by timestamp); weekday keys are the caller’s (the Comparison intro’s “Calendar keys” table). The 8-day pattern reads the open o for its fill prices, and Williams’s variant for its entry; the flip reads closes only. A resting stop is touched when the high reaches it (GTE) unless the source says “above” (strict on a tick grid), and a stop fill price is max(o, level), written o.Maximum(level) (a sell stop’s is o.Minimum(level)). A trade that opens and closes on one bar is a bar-level result, not a Position latch, which does not test an exit on its entry bar; such results read 0 or -0 off their signal bars, so their totals are a plain CumSum, where a result with a NaN head would take res.SetN(F, 0).CumSum(). Exact decimal ties at a threshold (a pullback of exactly 3%, a close exactly on the pivot) are decided by binary rounding in every float form, and each section says which form it pins. Backtests over data we do not have are evidence, not a reference. Palmquist’s declining-volume pullback is on the Consecutive page t6-declining-volume-pullback.md, and Raschke and Connors’s Anti on the Stochastic page t6-stochastic-compositions.md. Counts on probe paths that the tests do not reproduce are illustrations, not pins.
8-day continuation pattern (Ait Hellal and Zhang)
Omar Ait Hellal and Chen Zhang (“An 8-Day Pattern For Short-Term Traders”, Stocks & Commodities V38:1, January 2020, pp. 22-25) number the bars “from 1 to 8, where bar 1 refers to the oldest” and go long when “Bar 1 makes a 60-day high (bar 1’s high is higher than the high of the previous 59 days); it should also be the highest of bars 2 through 7”, there is “at least a 3% pullback/selloff (from the high of bar 1 to the low of bar 6; bar 6 should be the lowest of bars 1 through 7)” and “Bar 7’s high is higher than bar 6’s high”, to “Buy at the high of bar 7 plus an increment the next day (bar 8)”; “For a short trade, reverse steps 1 through 4 above.” t6EightDayPattern takes bar 1 as t-6 and bar 7 as the setup bar t, and returns both setups, the stop fills on bar 8 and their prices.
ih7 := h.IndexOfHighestS(7)
il7 := l.IndexOfLowestS(7)
hi60 := h.Breakout(59, 1).EQ(1).Shift(6)
lo60 := l.Breakout(59, 1).EQ(-1).Shift(6)
pull := h.Shift(6).Sub(l.Shift(1)).GTE(h.Shift(6).Mul(pct))
rally := h.Shift(1).Sub(l.Shift(6)).GTE(l.Shift(6).Mul(pct))
long := ih7.EQ(0).Mul(il7.EQ(5)).Mul(hi60).Mul(pull).Mul(h.GT(h.Shift(1)))
short := il7.EQ(0).Mul(ih7.EQ(5)).Mul(lo60).Mul(rally).Mul(l.LT(l.Shift(1)))
buy := long.Shift(1).Mul(h.GT(h.Shift(1)))
sell := short.Shift(1).Mul(l.LT(l.Shift(1)))
buyPx := o.Maximum(h.Shift(1).Add(tick))
sellPx := o.Minimum(l.Shift(1).Sub(tick))- Inputs: opens, highs and lows of daily bars on a price grid of
tick, the “increment” (one cent in their example, 1/64 in the test);pct0.03, the fall from bar 1’s high to bar 6’s low as a share of bar 1’s high for a long, and the rise from bar 1’s low to bar 6’s high as a share of bar 1’s low for a short. The setups read no close; the same-day exit does. - First valid index: 65 for
longandshort(bar 1 at t-6 needs the 59 highs from t-65 to t-7) and 66 forbuyandsell; they read 0 before with no mask, becauseBreakout(59, 1)reads 0 before its first valid index, 59, and turns a window holding a non-finite value into NaN, whichEQreads as 0 (IndexOfHighestS(7)andIndexOfLowestS(7)read -1 on bars 0 to 5).buyPxandsellPxare fill prices, meaningful on fill bars only and not masked: on bar 0 they read theShiftpads (sellPx[0]is-tick). - Anchors: the latest tie wins in both index methods, so
ih7.EQ(0)holds where bar 1’s high is strictly above bars 2 to 7 (it equalsh.Shift(6).GT(h.Highest(6))from bar 6) andil7.EQ(5)where bar 6’s low is at or below bars 1 to 5 and strictly below bar 7 (it equalsl.Shift(1).LTE(l.Shift(2).Lowest(5)).Mul(l.Shift(1).LT(l))from bar 6);hi60equalsh.GT(h.Highest(59).Shift(1)).Shift(6)from bar 65,Breakout’s own identity. Both setups equal a direct loop of the rules on the tests’ planted blocks, and a probe loop with an exact rational 3% test on two 20,000-bar walks (not in the tests). On the test’s bars, bar 113 makes a 60-bar high of 128.125 and bars 117 and 118 the low 124.28125, exactly 3% below it:pullholds (GTEon dyadic prices), the later of the tied lows is bar 6,longfires on bar 119 andbuyon bar 120 at 127.015625, bar 7’s high plus 1/64;shortfires on bar 197 andsellon bar 198 at its gap-down open, 109.9375. - Traps: the all-strict reading of “lowest”,
l.Shift(1).LT(l.Shift(2).Lowest(5)).Mul(l.Shift(1).LT(l))in place ofil7.EQ(5), is the variant for symmetric ties: it drops a setup whose bar 6 ties an earlier low, as bar 119’s does. The dossier’sh.Shift(6).GT(h.Shift(7).Highest(59))equalshi60from bar 65 but fires on warm-up bars (theShiftpad reads against 0, then the window expands), so it needsSetN(65, 0). A pullback of exactly 3% in decimal is decided by binary rounding: the pinnedpullfailed the inclusive test for 2,392 of the 5,000 whole-dollar highs from 1 to 5,000 with the low exactly 3% below (about half; every float form fails some; the test pins 48 of the 101 from 100 to 200) and is right one cent either side. On a tick grid a high at least one tick above bar 7’s reaches the stop, so the fill test ish.GT(h.Shift(1)), exact, andtickenters only the prices; a fill-bar high equal to bar 7’s does not fill (pinned by a constructed case).h.GTE(h.Shift(1).Add(tick))is not exact on decimal prices: the float sum lands above the decimal level for 48,721 of the 300,000 cent levels from 0.01 to 3,000.00, where a high exactly at the level would miss. The same-day close exit (“exiting a trade the same day at the close”) is a bar-level result on the fill bar,c.Sub(buyPx).Mul(buy)(andsellPx.Sub(c).Mul(sell)), not aPositionlatch. Their 16 stop and target pairs are per-setup statistics over overlapping trades, a loop; for non-overlapping trades they are the bracket with resting orders of the trade-state paget2-entry-anchored-stops.md, anchored onbuyPx. - Variant: the 1-2-3-4 (Larry Connors, Connors On Advanced Trading Strategies, 1998, ch. 8, pp. 57-70), the pattern’s declared ancestor: “Identify a market whose 14-day ADX is greater than 30”, “The 14-day +DI reading must be greater than the 14-day -DI reading”, then “three consecutive intraday lower lows or any combination of two lower lows and an inside day”, and “On day four only, buy 1 tick (1/8 for equities) above the day-three high”. With
ll := l.LT(l.Shift(1)), the strict previous-bar inside dayin := h.LT(h.Shift(1)).Mul(l.GT(l.Shift(1)))(notInsideBars, which compares inclusively with the prior non-inside mother bar) andcorr := ll.Add(in).Sum(3).EQ(3).Mul(ll.Sum(3).GTE(2))(in any order; equal to a direct loop), the setup isc.ADX(h, l, c, 14).GT(30).Mul(c.DMIPlus(h, l, c, 14).GT(c.DMIMinus(h, l, c, 14))).Mul(corr)on day 3, filled bysetup.Shift(1).Mul(h.GT(h.Shift(1)))ato.Maximum(h.Shift(1).Add(tick))on day 4. The ADX reads 0 on bars 0 to 13, and a level of 30 read in the first 6n to 8n bars compares seed transients (the Average Directional Index paget6-dmi-systems.md). - Variant: Williams’s uptrend pullback, C > C[30] and C < C[9] (Larry Williams, Long-Term Secrets to Short-Term Trading, 2nd ed., 2011, ch. 7, pp. 125-127) wants “today’s price to be greater than the close 30 days ago” and “today’s close lower than the close 9 days ago”, then will “buy on the open tomorrow and exit on the next day’s close”:
setup := c.Momentum(30).GT(0).Mul(c.Momentum(9).LT(0)), 0 on bars 0 to 29 with no mask (wherec.GT(c.Shift(30))fires against the pad), tradedpos := c.Position(setup.Shift(1), nil, 1)with the resultc.Sub(o.Shift(1)).Mul(pos.Exits())on the exit bar (it readso; “the next day’s close” is read as the close after the entry day). His weekday split (it needs a caller’sweekdaykey) and his 3,250-dollar stop (live on the entry bar, whichPositiondoes not test) are left out. - Evidence: Ait Hellal and Zhang tested 171 NYSE and NASDAQ stocks and 67 ETFs, chosen from a most-active list of 12 February 2018, over 2006 to 2017: 4,656 setups (2,715 long, 1,941 short); the same-day close exit made 0.348% a trade on the stocks and 0.53% on the ETFs, with no costs, and their grid of 1%, 2%, 5% and 10% stops and targets is chosen in sample. Their AMZN example (a 60-day high of 1,498 dollars on 2 February 2018, a low of 1,265.93 on 9 February, “a 15% pullback” that is 15.49% of the high, and bar 7’s high of 1,393.81, bought above 1,393.82) prints no other bars, so it is not reproducible. Connors’s 1-2-3-4 examples are charts. Williams’s pattern made 354 trades with 57% winners and an average profit of 421 dollars (S&P 500; his book says 50 dollars of commission is deducted from every result), and his weekday Tables 7.1 and 7.2 hold 2,023 and 1,934 trades, so they cannot be the pattern’s. Evidence, not a reference.
- Pinned by
TestRecipeT6EightDayPattern; runnable exampleExampleSeries_IndexOfHighestS_eightDayPattern.
2-period rate-of-change flip (Taylor; Raschke and Connors)
Linda Bradford Raschke and Laurence A. Connors (Street Smarts, 1995, ch. 8 “2-Period ROC”, pp. 60-65) give Taylor’s rhythm (“the market tended to make a swing high or swing low every two to three days”) a pivot: “close (day one) - close (day three) equals the 2-period rate of change”, “Add this number to yesterday’s closing price (day two)”, and “We want to be long by the close if the price is trading above this pivot point and short by the close if the price is below this pivot point.” t6TwoPeriodFlip returns the pivot, stamped on the bar that computes it and compared with the next close, the latched go-home state (1 long, -1 short) and the fresh flips.
m2 := c.Momentum(2)
piv := c.Shift(1).Add(m2).SetN(2, math.NaN())
state := m2.Delta(1).SetN(3, math.NaN()).Hysteresis(0, 0)
flip := state.Mul(state.Shift(1)).LT(0)- Inputs: closes only (the worksheet’s are two-decimal prices; its market and year are not printed); no parameters.
- First valid index: 2 for
piv(NaN on bars 0 and 1), 3 forstate(0 before: the masked difference is NaN on bars 0 to 2, whereHysteresis(0, 0)holds its undetermined 0) and 4 forflip(0 before). A NaN close makes the pivot NaN on its bar and the two after, and the state holds through the NaN differences. - Anchors: their p. 61 worksheet, the closes of 10-30 to 11-8 (586.70, 583.85, 588.25, 592.35, 592.50, 591.30, 588.20, 594.10), prints the pivots 585.40, 596.75, 596.60, 591.45, 587.00 and 591.00 on 11-1 to 11-8, which
pivreproduces to 1e-9 (the closes are not dyadic; the worst error is about 1.1e-13), and the go-home states long, long, short, short, short, long with the fresh flips on 11-3 and 11-8, whichstateandflipreproduce exactly from 11-2 (the 11-1 “long” needs the unprinted close of 10-29, andstatereads 0 there).c.GT(piv.Shift(1))equalsm2.Delta(1).GT(0)bit for bit when the four closes lie in one binade (0 disagreements on 60,000 probe bars).flipis a product of values of 1 and -1, exact, and does not fire on the first decision from the undetermined 0, wherestate.CrossesAboveS(0)does (on 11-2). - Traps: a close exactly on the pivot holds the state (“closes above” and “close below” leave a tie unsaid). On the tie fixture, closes 100, 102, 103, 104, 105, the last close equals bar 3’s pivot, 105: the latch holds -1, while with
d := m2.Delta(1).SetN(3, math.NaN())the dossier’s unlatchedd.GT(0).Sub(d.LT(0))reads 0 there, and on walks it then misses the flip across such a tie (6 flip bars of a 20,000-bar cent-tick walk, 209 of a quarter-tick one). The worksheet has no tie, so it cannot tell the two apart; the test’s 320 synthetic bars put a close exactly on the prior pivot 13 times (bar 250’s close equals bar 249’s pivot, 108.5625), and the state holds on each. The prose’s “below the previous day’s pivot of 596.60” cites the 11-3 row’s own pivot (592.35 + 4.25), the level for 11-6; the 11-2 pivot is 596.75 (588.25 + 8.50), the worksheet is consistent, and the dossier’s “a typo or a different 10-31 close” is wrong. Step 1’s words, “Subtract today’s close from the close two days ago”, reverse the subtraction its own formula performs. On decimal prices a close equal to the pivot in decimal is decided by binary rounding in the pinned difference of the rates and in the pivot form alike (on a cent-tick walk 8 of 19,997 flags differ from exact decimal arithmetic); dyadic prices are exact. Unmasked,c.GT(piv.Shift(1))reads 1 on bar 0 against theShiftpad. - Variant: Raschke’s caveats (p. 65): the oscillator “is prone to whipsaw action at times in flat quiet markets (for example, when the ADX is less than 16)” and is “not an appropriate tool in a strong-trending market (for example, when the ADX is greater than 30 and still rising.)”. With
a := c.ADX(h, l, c, 14)(14 assumed: the chapter gives no length) the gate isa.GTE(16).Mul(a.GT(30).Mul(a.Delta().GT(0)).Mul(-1).Add(1)).SetN(15, 0), masked to n + 1 because the one-bar change reads the ADX’s warm-up 0 at bar n, as on the Average Directional Index paget6-directional-index.md. - Evidence: Exhibit 8.1 (S&P, December 1994): entering on the close of a fresh flip and exiting on the next close, “you would have been profitable on 8 out of 11 trades” (a chart; “we do not recommend trading this way on a mechanical basis”). The Appendix’s Moore Research reports (Exhibits A.9 and A.10, futures contracts from 1986 to 1995, entry on the close of a fresh flip, exit at the next open or the next close, no costs) give the S&P’s fresh buys 57% winners and an average net of 168 a trade at the next close, and their counts imply about one fresh buy every 4.2 days (derived), as “a buy signal is given, on average, one out of every four days”; “This is not meant to be a mechanical system.” Evidence, not a reference.
- Pinned by
TestRecipeT6TwoPeriodFlip; runnable exampleExampleSeries_Momentum_twoPeriodFlip.
Two momentum ratios, each a net move over a total move: Chande and Kroll’s net momentum oscillator sums the changes over a window, and Blau’s True Strength Index averages them exponentially, twice. With a one-bar change the first is CMO, so its section is also the method’s conformance note. Values are NaN before their first valid index; flags built on them read 0 there. The exponential averages follow their sources: where an author’s own table or listing starts each average at its first input, as Blau’s do, the helpers use XAverageAlpha from that input; where a source is TA-Lib-like or silent, they use the house XAverage on the valid slice. Prices are positive and finite and lengths are in bars. Altman’s relative momentum index and Chande and Kroll’s unsmoothed RSI are under “RSI”, Carver’s EWMAC forecasts, another momentum over its risk, under “MACD”, and the trend-strength measures below share this category.
Net momentum oscillator (Chande and Kroll)
Tushar Chande and Stanley Kroll (“Stochastic RSI And Dynamic Momentum Index”, Stocks & Commodities V11:5, May 1993, pp. 189-199) define the “Net Momentum Oscillator = 100*(Su - Sd)/(Su + Sd)”, the up-day less the down-day momentum over their sum, or"100(Momentum / |Momentum|)" (their Equation 18), and set “overbought and oversold levels at +50 and -50 where the ratio of Su to Sd is 3:1 or 1:3”. With the one-bar change it is c.CMO(n); their heating-oil example is a “20-day Unsmoothed NMO with 5-day lookback”, the 5-bar change in place of the one-bar change, and they smooth “with a simple or exponential moving average”, five periods in their figures (a simple average of the RMI in Figure 8, an exponential one of the NMO in Figure 11). t6NetMomentum takes the lag and returns the oscillator and a q-bar simple average of it.
d := c.Delta(x)
nmo := d.Sum(n).Div(d.Abs().Sum(n)).Mul(100).SetN(x+n-1, math.NaN())
sm := nmo.Average(q).SetN(x+n+q-2, math.NaN())- Inputs: the close
c, the windown, the lagxand the smoothing lengthq(20, 5 and 5);nmoruns from -100 to +100 andsmis its q-bar simple average. Withx = 1,nmoisc.CMO(n), the method under “Momentum”. - First valid index:
x + n - 1fornmo(24 for 20 and 5) andx + n + q - 2forsm(28). Unmasked,Delta’s pad andSum’s partial windows give ratios, not zeros;sm’s own mask binds when q > x + n, whereAveragewould pass the maskednmothrough. A window of n zero changes reads 0 (Div’s rule). - Anchors: with
x = 1,nmoequalsc.CMO(n)from bar n bit for bit on prices whose changes and window sums are exact, such as a quarter grid, and to rounding otherwise; the dossier’sc.Momentum(n).Div(c.Delta().Abs().Sum(n)).Mul(100)isCMO’s body, bit for bit, and the up and down sums give it to rounding, with partial windows before bar n (100, 86.71 and 64.58 on bars 1 to 3 of Figure 1’s closes). On the closes of their Figures 1 to 4 (375.22, 381.40, 380.96, 380.07, 377.39)CMO(4)at bar 4 is 21.295387634935935, 100 times the printed totals 2.17/10.19. On Figure 5’s 25 closes, the first 25 of Altman’s sidebar,nmowith a lag of 5 over 20 bars is valid at bar 24 only, 91.794724496146, 100 times the printed 191.75/208.89, and equals there2 rmiC - 100of the RMI section under “RSI” (to rounding elsewhere). Siligardos’s identity CMO = 2 RSI - 100 (“The Concept Of Equivalence”, Stocks & Commodities V21:2, February 2003) holds for the simple-sum RSI:c.CMO(n).Add(100).Div(2)equals the RSI page’s unsmoothed RSI within 3e-14 from bar n, flat windows included.c.CMO(n).Abs()isc.EfficiencyRatio(n).Mul(100)asCMO’s doc states. The +50 level is Su = 3 Sd:Series{100, 103, 102}.CMO(2)reads exactly 50 at bar 2. - Traps: Cutler’s form, twice the up sum over the total less 100, reads -100 on a flat window, where
CMOreads 0 and Marisch’s TradeStation listing keeps the previous ratio (a hold isValueWhen).CMOis not clamped: rounding can carry it two ulps past 100, soClamp(-100, 100)where a bound matters. Smooth exponentially on the valid slice,c.Mul(0).Add(nmo[x+n-1:].XAverage(q))masked tox + n + q - 2:c.CMO(20).XAverage(5)seeds on the warm-up zeros and is 5.5 points off at bar 24 of the test fixture. TradingView’sta.cmouses running sums, equal to rounding. - Variant: TA-Lib’s CMO smooths the gains and losses with Wilder’s average,
c.RSI(n).Mul(2).Sub(100).SetN(n, math.NaN()): a loop translation ofta_CMO.cagrees within 1e-12 from bar n, where it equals Chande’s form; after bar n the two part. - Variant: Weinberg’s AmiBroker “CMO” (“Walk-Forward Testing”, Stocks & Commodities V27:1, January 2009) counts days,
d.GT(0).Sum(n).Sub(d.LT(0).Sum(n)).Div(d.GT(0).Sum(n).Add(d.LT(0).Sum(n))).Mul(100)withd := c.Delta(1), whose numerator isc.UpDownSum(n)bit for bit from bar n: another oscillator. - Evidence: Chande and Kroll’s DJIA and heating-oil figures are charts; Chande’s yen sidebar (Stocks & Commodities V13:10) prints the only table of |CMO| values among these sources, reproduced through
EfficiencyRatio(9)by the VIDYA section under “Moving Averages”; Weinberg’s band system made 225 trades with 127 winners on a survivorship-biased universe, with his count form, so it says nothing of Chande’s. Evidence, not a reference. - Pinned by
TestRecipeT6NetMomentum; runnable exampleExampleSeries_CMO_netMomentum.
True Strength Index (Blau)
William Blau (“True Strength Index”, Stocks & Commodities V9:11, November 1991, pp. 438-446) writes “TSI = 100 Ez(Ey(Mtm)) / Ez(Ey(|Mtm|))”, the one-day momentum of the close averaged twice over its absolute value averaged twice, each “exponential constant” being 2/(y + 1). His sidebar “Calculating TSI”, reprinted with “Trading With The True Strength Index” (V10:5, May 1992, pp. 214-219), starts each average at its first input, and the 1992 article adds “a seven-day EMA of the true strength index line” as a signal and the lengths 25 and 13. The numerator alone is his divergence indicator: “I call it the divergence indicator (DI)”. t6TSI returns the index, its signal and the DI, each average seeded with its first value.
a1 := 2 / (float64(r) + 1)
a2 := 2 / (float64(s) + 1)
a3 := 2 / (float64(q) + 1)
d := c.Delta(1)[1:]
num := d.XAverageAlpha(a1).XAverageAlpha(a2)
den := d.Abs().XAverageAlpha(a1).XAverageAlpha(a2)
tsi := c.Mul(0).Add(num.Div(den).Mul(100)).SetN(1, math.NaN())
sig := c.Mul(0).Add(tsi[1:].XAverageAlpha(a3)).SetN(1, math.NaN())
di := c.Mul(0).Add(num).SetN(1, math.NaN())- Inputs: the close
c, the first and second smoothing lengthsrandsand the signal lengthq(25, 13 and 7; the 1991 table uses 14 and 3);tsiandsigrun from -100 to +100 anddiis in price units. The averages are arch-identical (XAverageAlpharounds each product on its own). - First valid index: 1 for all three, the first change;
tsiat bar 1 is exactly +100 or -100, or 0 on a zero first change. Minimum length 1: an empty series panics, and one bar gives NaN only. - Anchors: Blau’s 1991 table (the closes 336.65 to 346.20): the same statements with his printed constants 0.1333 and 0.5 give all 50 cells of its five computed columns at two decimals, and the helper at 14 and 3 gives its TSI column (printed without the 100). His 1993 table (Stocks & Commodities V11:1, January 1993, sidebar “True Strength Index”, 40 and 20) needs his 0.049 and 0.095: they give all 40 cells, where the helper’s 2/41 and 2/21 give 3 of its 8 TSI cells. TSI = 2 DRSI - 100, Blau’s double-smoothed RSI on the same seeds, to rounding; r = s = 1 gives 100 times the sign of the change; with
d.RMA(n)in place of the two averages, the TSI of 2n - 1 and 1 is2 RSI(n) - 100within 1e-12 from bar n; the zero-seeded DI,c.Delta(1).XAverageAlpha(a1).XAverageAlpha(a2), is the change of the double average of the close to rounding. The boxcar form of the ratio isc.CMO(n). - Traps: the first-value seed is a transient: the helper’s TSI of 27 and 1 is up to 59 points from
2 RSI(14) - 100on the test fixture, its seed being the first change, not Wilder’s mean. The dossier’sc.Delta().XAverage(r)readsDelta’s bar-0 pad and passes changes through into the seeds. A flat start reads 0 (Div’s rule) until the first move, and a NaN close makes every later value NaN.sigstarts fromtsiat bar 1, +100 or -100, so the first change dominates it for roughly 3q bars. A length below 1 makes an alpha above 1, whichXAverageAlpharejects with zeros, silently. - Variant: the house average with SMA seeds on the valid slices, valid from
r + s - 1:c.Mul(0).Add(d.XAverage(r)[r-1:].XAverage(s).Div(d.Abs().XAverage(r)[r-1:].XAverage(s)).Mul(100)).SetN(r+s-1, math.NaN()); on 50 random walks at 25 and 13 it differs by about 0.014 points 100 bars afterr + s - 1and by less than 1e-12 after 400. - Variant: Blau’s Figure 9 rule is the DI against its three-bar simple average,
di.CrossesAboveS(di.Average(3))anddi.CrossesBelowS(di.Average(3));di.Average(3)is NaN at bar 2, so the first cross can fire at bar 4. - Evidence: Blau’s S&P 500, DJIA and Amerada Hess figures are charts, and his fast and slow pair “is not a complete trading system”. pandas-ta seeds with a simple average of the first changes and signals over 13 bars, and TradingView’s
ta.tsireturns the ratio in [-1, 1]. Evidence, not a reference. - Pinned by
TestRecipeT6TSI; runnable exampleExampleSeries_XAverageAlpha_tsi.
Three trend-strength measures built on momentum, each a gate rather than a direction signal. Their values are NaN before their first valid index, where a sum, an average or a deviation would read Momentum’s or Average’s warm-up; flags and latches built on them read 0 there, a latch’s 0 being its undetermined state. Prices are positive and finite, lengths are in bars and thresholds are in the closes’ units unless a section says otherwise; series are paired by position, so the caller aligns them by timestamp first.
Trend Detection Index (Pee)
M.H. Pee (“Trend Detection Index”, Stocks & Commodities V19:10, October 2001, pp. 54-61) defines “20-day TDI = (AV20) - {(SumAM40) - (SumAM20)}”, AV20 being the “Absolute value of the sum of 20-day momenta of the last 20 days” and the others sums of absolute momenta over 40 and 20 days, and enters “long tomorrow at the open if both the TDI and direction indicator are positive after today’s close or enter short at the open if the TDI is positive and the direction indicator is negative”. His sheet’s momentum is the close less the close 19 rows earlier, as his theory says (“summing the changes in the close for the past 19 days”), so t6TrendDetectionIndex lags n - 1, adds in the sheet’s order, (|DI| + S_n) - S_2n, and latches his rule with Hysteresis.
mom := c.Momentum(n - 1)
am := mom.Abs()
d := mom.Sum(n)
tdi := d.Abs().Add(am.Sum(n)).Sub(am.Sum(n+n)).SetN(n+n+n-2, math.NaN())
di := d.SetN(n+n-2, math.NaN())
trend := tdi.GT(0.0)
up := trend.Mul(di.GT(0.0))
dn := trend.Sub(up)
pos := up.Sub(dn).Hysteresis(0, 0)- Inputs: closes and n >= 2 (20).
- First valid index: 2n - 2 for
diand 3n - 2 fortdi(58 at n = 20: his first TDI needs 59 closes, although his caption says 40 days), NaN before. Unmasked, the 2n-bar sum still holds the momentum’s warm-up zeros, and on his data the TDI reads 86.53, 60.41, 37.38 and 13.87 on bars 38 to 41, a false trend a month before his first TDI.posreads 0 until the first positive TDI; it is the decision for the next open, and his position column ispos.Shift(1). - Anchors: on his sidebar Figure 1 (yen, 23 September to 31 December 1998, 69 closes) all 165 printed cells hold at 2 decimals, with a TDI of -128.55 on 981216 and -9.49 and a direction indicator of 56.04 on 981231 (-128.54999999999998, -9.4899999999999949 and 56.040000000000035 in binary); the latch equals
c.PositionSigned(up, dn, dn, up)and a loop of his rule; the direction indicator is n(SMA_n(t) - SMA_n(t-n+1)) to rounding; and with New and Old the sums of absolute momentum over the latest n bars and the n before them, -Old <= TDI <= New - Old, so a positive TDI needs rising absolute momentum and a direction indicator other than 0. - Traps: his sheet’s TDI stays negative, so its latch does not decide and is pinned on synthetic data; a direction indicator of 0 with a positive TDI goes short, as his sheet’s else branch does (exact arithmetic excludes it); his own EasyLanguage tip uses
Price[Length], a lag of n, which gives a TDI of -17.09 and a direction indicator of 54.14 on 981231 instead of the printed -9.49 and 56.04. - Variant: the EasyLanguage lag,
mom := c.Momentum(n), with first valid indices 2n - 1 and 3n - 1. - Evidence: his Figures 2 and 3 (15 futures markets, 4 January 1982 to 31 December 1998, 75 dollars a trade): 676 trades, 41.93% winners and every market profitable at n = 20, in sample; on a Gaussian random walk the TDI is positive on about 37.7% of bars at n = 20, so the gate alone is loose. Evidence, not a reference.
- Pinned by
TestRecipeT6TrendDetectionIndex; runnable exampleExampleSeries_Momentum_trendDetectionIndex.
Trend Persistence Rate (Poster)
Richard Poster (“Trend Strength: Measuring The Duration Of A Trend”, Stocks & Commodities V39:2, February 2021, pp. 9-12 and 41) counts “the number of bars that an SMA slope exceeds a threshold (pips) in a specified period”: “TPR = 100 * | (N_Up - N_Down)/N_Total |”, with a signed form obtained “by removing the absolute value operation”. His MQL4 sidebar compares the one-bar change of a 5-bar simple average with the threshold strictly over the last 20 closed bars. t6TrendPersistenceRate counts with Sum, scales in his order, Mul(100).Div(n), and masks the windows that hold a missing change.
d := c.Average(p).Delta(1)
signed := d.GT(th).Sub(d.LT(-th)).Sum(n).Mul(100).Div(n).MaskNonFinite(d.Sum(n)).SetN(p+n-1, math.NaN())
tpr := signed.Abs()- Inputs: closes,
pandn(5 and 20) and the thresholdthin price units per bar: his ThrshFixed of 1 pip is 0.0001 on a five-digit EURUSD quote. - First valid index: p + n - 1 (24 at the defaults), NaN before:
Averagepasses the first p - 1 closes through, so the change at bar p - 1 is not a change of the average (-2 on a path rising 2 a bar), and the unmasked rate at bar 23 of that path reads 90. - Anchors: on a constructed integer path (1000, rising 2 a bar to bar 29, flat to bar 59, falling 3 a bar; p 5, n 20, th 1) the signed rate is 100 on bars 24 to 31, falls by 5 a bar to 0 at bar 51 and reaches -100 at bar 80;
Mul(100).Div(n)is exact against his100.*(ctrP-ctrM)/ctrTfor every count, whereMul(100.0/float64(n))misses some counts at n = 14, 15, 21 and 30;c.Average(p).UpDownSum(n, tol)is the count with the threshold seriessma.Shift(1).Abs().Mul(tol).Div(100), bit for bit from bar n; a missing close gives NaN on exactly the windows that hold its changes (bars 40 to 64 for a close at bar 40), which the unmasked count reads as flat. - Traps: a change equal to
this not a move, but on a tick grid the average’s change lands onth, within rounding, wherever the close moved exactly p timesth, and the decision then rests on how the average was summed; passth+ tick/(2p) instead (0.000101 for one pip on EURUSD at p = 5), which makes the strict rule exact: on a 300-bar tick path it equals the exact count in integer pipettes on every bar, where 0.0001 leaves 105 bars to rounding. His loop reads the bars before MetaTrader’s forming bar 0, so the value it shows on a bar issignedof the bar before (signed.Shift(1)). His Figure 4 plots a smoothed TPR on a 0 to 1 scale. - Variant: the scale-free count
c.Average(p).UpDownSum(n, tolPct).Mul(100).Div(n), with a tolerance in percent of the previous average: a different rule from Poster’s. - Variant: a smoothed line needs the trim before the average,
signed.Mul(0.0).Add(signed.Window(-(p + n - 1)).XAverage(5)).SetN(p+n+3, math.NaN()), sinceXAverageof a NaN-masked series is NaN throughout; Poster does not say which smoothing his chart uses. - Evidence: his Figure 5 (EURUSD hourly, 2010 to 2020, a moving-average crossover): with the volatility and TPR filters, 374 trades and a profit factor of 1.86 against 925 trades and 1.22 unfiltered, with the filter levels optimised on the same data and two of the six rows internally inconsistent. Evidence, not a reference.
- Pinned by
TestRecipeT6TrendPersistenceRate; runnable exampleExampleSeries_Sum_trendPersistenceRate.
Trend Analysis Index (White)
Adam White (“Filtering Trades With A Moving Average Slope”, Stocks & Commodities V10:8, August 1992, pp. 358-360) defines the TAI as “simply the standard deviation of the last x bars of a y-bar moving average”, MetaStock’s std(mov(c,21,s),8); in “A Weekly S&P Trading System” (Stocks & Commodities V13:6, June 1995, pp. 233-237) and “The Derivative Moving Average” (Stocks & Commodities V14:6, June 1996, pp. 253-257) it is the range of the last five values of the average, “dividing the raw value of the TAI by the last close, then multiplied by 100”. MetaStock’s std divides by n (the standard-deviation calculation in MetaStock’s online edition of Achelis’s Technical Analysis from A to Z), so the 1992 form is StdDev’s population deviation. t6TrendAnalysisIndex returns both forms.
sma := c.Average(y)
tai1 := sma.StdDev(x).SetN(y+x-2, math.NaN())
tai2 := sma.Highest(x).Sub(sma.Lowest(x)).Div(c).Mul(100).SetN(y+x-2, math.NaN())- Inputs: positive closes,
yandx>= 2 (21 and 8 in 1992; 26 and 5 on weekly bars in 1995; 28 and 5 in 1996). - First valid index: y + x - 2 for both (27 at 21 and 8), NaN before:
Averagepasses the first y - 1 closes through, and on the line 100 + t the unmasked 1992 form reads 2.2913 at bar 7 and 2.7272 at bar 20. - Anchors: on the straight line 100 + t (y 21, x 8) the 1992 form is sqrt(5.25) = 2.2912878474779199 from bar 27, bit for bit, and the 1995/96 form 7 over the close times 100 (5.46875 at bar 28); where the average is monotonic over the window the range equals
sma.Delta(x-1).Abs()bit for bit, and the 1995/96 form equalsc.Momentum(y).Sum(x-1).Abs().Div(y).Div(c).Mul(100)to 1e-11 relative: (x - 1) times the mean y-bar momentum over y, in percent of the close. - Traps:
StdDevSamplechanges the 1992 gate, since sqrt(4/3) and sqrt(10/9) rescale the fast and slow indices differently (4 bars of the synthetic path, and a few dozen of 4,000 on a longer walk); the 1995 summary’s “one half of 1%” contradicts his Formula #1, a fraction tested from 0.03 to 0.07; the thresholds (1.2 and 0.4) are optima on one market and scale with y and x. - Variant: the 1992 filter,
fast.GT(slow)withfast := c.Average(38).StdDev(4).SetN(40, math.NaN())andslow := c.Average(38).StdDev(10).SetN(46, math.NaN()), admitting his 26-bar crossover trades only where it is 1. - Variant: the exits: 1996’s
tai2.CrossesBelowS(0.4)(hiswhen(fml("TAI"),<,0.4) and when(ref(fml("TAI"),-1),>=,0.4)) and 1995’stai2.LT(1.2)arming the retracement exitl.Highest(13).Sub(l).Div(l).GT(0.05). - Evidence: in 1992 the filter left 6 of 98 trades on 1,440 S&P 500 bars, with a printed win-loss ratio its own dollars do not give, ignoring commission and interest; in 1996 the 0.38 exit gave 16 wins and 6 losses on daily S&P 500 data of unstated length, with no statement on costs; both in sample. Evidence, not a reference.
- Pinned by
TestRecipeT6TrendAnalysisIndex; runnable exampleExampleSeries_StdDev_trendAnalysisIndex.
Rate of Change
RateOfChange
func (s Series) RateOfChange(n int) Series
RateOfChange computes the percentage rate of change over n bars. The result is ((current / n-bars-ago) - 1) * 100.
The output is a percentage, (s[i]/s[i-n] - 1)*100, which can differ from DeltaPercent in the last bit. The warm-up reads 0, not NaN: bars 0 to n-1 are 0.
Compositions
The factor-style features below are returns as fractions in pandas’ pct_change form, c.Div(c.Shift(k)).Sub(1), which matches Jansen’s notebooks bit for bit on series without gaps (pandas’ pct_change forward-filled missing prices by default before pandas 2.1) and reads -1 unmasked on bars 0 to k - 1 (Div reads 0 over the pad); a percent form is one Mul(100) away, and RateOfChange(k) gives it bit for bit. Values are NaN before their first valid index; prices are positive and finite and lengths are in bars, 21 bars a month and 252 a year, while monthly and weekly bars and calendar months are the caller’s. The evidence for these features is cross-sectional (ranks across stocks) or model-level, not the timing of one instrument, and the sources’ pooled steps (winsorisation at quantiles of the whole panel, rank normalisation) are out of scope: they look ahead and need the universe (CrossSectionalRank). Related forms: the return-volatility form under Relative: Divergence, the relative drawdown under Relative: Channels, the multi-horizon scores under Relative: Cross-Section, the squash transforms of the Math introduction, the rolling 52-week form c.GT(h.Highest(252).Shift(1)).Shift(1) of the Darvas Box introduction and Amihud’s illiquidity under Bid-Ask Spread Estimators.
Skip-month momentum and short-term reversal (Jansen; Green, Hand and Zhang)
Stefan Jansen (Machine Learning for Algorithmic Trading, 2nd ed., Packt, 2020, ch. 20, notebook 05_conditional_autoencoder_for_asset_pricing_data.ipynb) computes Green, Hand and Zhang’s mom12m, “11-month cumulative returns ending month t-2” (“The Remarkable Multidimensionality in the Cross-Section of Expected U.S. Stock Returns”, working paper, 2014, Table 2; published as “The Characteristics that Provide Independent Information about Average U.S. Monthly Stock Returns”, Review of Financial Studies, 2017), as close.pct_change(periods=11 * MONTH).shift(MONTH) with MONTH = 21, and mom1m, “Return in month t-1”, as pct_change(periods=MONTH). Their month t-1 is the latest month, so mom1m includes it and mom12m skips it; t6SkipMonthMomentum writes both in pandas’ order.
mom := c.Shift(s).Div(c.Shift(l)).Sub(1).SetN(l, math.NaN())
rev := c.Div(c.Shift(s)).Sub(1).SetN(s, math.NaN())- Inputs: positive closes;
s<l(21 and 252 on daily bars; 1 and 12 on monthly bars, the academic form). - First valid index: l for
mom(252) and s forrev(21); NaN before. - Anchors:
momis Jansen’sc.Div(c.Shift(231)).Sub(1).Shift(21)bit for bit from bar 252, and times 100 it isc.RateOfChange(231).Shift(21)bit for bit; (1 + mom)(1 + rev) - 1 is the 252-bar return to rounding. - Traps: unmasked,
momreads -1 on every bar 0 to 251: the pads give 0 over 0, read as 0, less 1. Jansen samples the daily features at Friday closes; the library leaves the sampling to the caller. - Variant: chmom, months t-6 to t-1 less t-12 to t-7:
x := c.Div(c.Shift(126)).Sub(1)andx.Sub(x.Shift(126)).SetN(252, math.NaN()). - Variant: mom36m, 24 months ending month t-13:
c.Shift(252).Div(c.Shift(756)).Sub(1).SetN(756, math.NaN()). - Evidence: Green, Hand and Zhang test these characteristics in cross-sectional regressions on monthly stock data, and Jansen ranks them across stocks before his conditional autoencoder; his text says that GKX “identified the 20 most influential metrics” of 94 without listing them. Evidence, not a reference.
- Pinned by
TestRecipeT6SkipMonthMomentum.
Multi-horizon geometric returns and momentum spreads (Jansen)
Jansen (Hands-On Machine Learning for Algorithmic Trading, Packt, 2018, pp. 101-102, and the 2nd edition’s notebook 01_feature_engineering.ipynb, ch. 4) computes “returns over various monthly periods” with pct_change(lag), clips each lag’s column at the 1% and 99% quantiles of the whole stacked panel, “normalize[s] returns using the geometric average” (.add(1).pow(1/lag).sub(1)) and forms momentum spreads such as return_3m - return_1m. t6GeometricReturns is the per-bar transform without the pooled clip.
r1 := c.Div(c.Shift(1)).Sub(1).SetN(1, math.NaN())
rk := c.Div(c.Shift(k)).Pow(1/float64(k)).Sub(1).SetN(k, math.NaN())
spread := rk.Sub(r1)- Inputs: positive closes (monthly closes for Jansen’s features);
k>= 1. - First valid index: k for
rkandspread; NaN before. - Anchors: Jansen’s printed head(10) for ticker A, fourteen monthly returns from January 2001 as a price path from 100: 19 of the 20 printed 2- and 3-month values agree within 1.5e-6, the error budget of six-decimal inputs. At k = 1,
rkis the one-bar return bit for bit (Pow(x, 1)is x) andspreadis 0; the pandas orderq.Sub(1).Add(1).Pow(1/float64(k))equalsq.Pow(1/float64(k))bit for bit for q from 0.5 to 2. - Traps: the 20th printed value, return_3m of 30 April 2001 (-0.105747, against -0.101033 here), is off by 4.7e-3: the notebook replaced February 2001’s one-month return by the pooled 1% clip level, a quantile over every stock and date to 2018, a look-ahead the per-bar form does not have. Go’s
Powis within a few units in the last place (up to 3 on the test path) of the correctly rounded power but not equal to it, so the references bound it; at k = 2 it is the correctly rounded square root. The notebook’sdropna()starts all its features together at the twelfth month. - Variant: a causal rolling winsorisation of the k-bar return
xbefore the transform,x.Maximum(x.Quantile(n, lo)).Minimum(x.Quantile(n, hi)), masked n - 1 bars beyond x’s first valid index (Quantile’s warm-up reads 0). - Variant: lagged one-month returns
r1.Shift(j).SetN(1+j, math.NaN()), j from 1 to 6. - Evidence: the notebook’s pooled describe() over 360,752 stock-months puts the 1% and 99% clip levels of return_1m at -0.329564 and 0.430943; no single-instrument test is printed. Evidence, not a reference.
- Pinned by
TestRecipeT6GeometricReturns; runnable exampleExampleSeries_Pow_geometricReturns.
Volatility-normalised momentum (Jansen; Chan)
Jansen (Hands-On Machine Learning for Algorithmic Trading, Packt, 2018, ch. 4, p. 92; the 2nd edition’s table prints the label as “volume adjustment”) lists “12-month price momentum Vol Adj”, which “normalizes the total return over the previous 12 months by dividing it by the standard deviation of these returns”; voladj reads these returns as daily ones with pandas’ sample deviation. Ernest Chan (Machine Trading, Wiley, 2017, ch. 4) normalises returns “by their past daily return volatilities”, which his code rTree_SPX.m computes as the 252-day mean absolute daily return, vol1=smartMovingAvg(abs(ret1), 252). t6NormalisedMomentum gives both forms.
r1 := c.Div(c.Shift(1)).Sub(1)
voladj := c.Div(c.Shift(k)).Sub(1).Div(r1.StdDevSample(n)).SetN(max(k, n), math.NaN())
chanN := c.Delta(k).Div(c.Shift(k)).Div(c.Delta(1).Div(c.Shift(1)).Abs().Average(n)).SetN(max(k, n), math.NaN())- Inputs: positive closes;
kandn(252 and 252; Chan’s k is 1, 2, 5 or 20 with n 252). - First valid index: max(k, n) for both; NaN before.
- Anchors:
voladjequals the return-volatility form of Relative: Divergence rewritten with fraction returns andStdDevSample,x.Div(x.Shift(k)).Sub(1).Window(-1).Div(x.Div(x.Shift(1)).Sub(1).Window(-1).StdDevSample(n)).Window(-(max(k, n) - 1)), bit for bit, and is bitwise start-invariant;chanNequals a direct translation of Chan’s MATLAB to rounding from bar max(k, n). - Traps: the shipped return-volatility line, in percent with the population
StdDev, isvoladjtimes sqrt(n/(n - 1)) to rounding (0.2% at 252), and its running moments make it start-dependent in the last bits. Chan’s mean also has a value at bar n - 1, over n - 1 returns, which the mask drops. A flat window gives 0 (Div’s rule). - Variant: Jansen’s monthly reading, the deviation of the 12 monthly returns, on the caller’s monthly closes
mc:mc.Div(mc.Shift(12)).Sub(1).Div(mc.Div(mc.Shift(1)).Sub(1).StdDevSample(12)).SetN(12, math.NaN()). - Evidence: TSSB’s PRICE MOMENTUM (Aronson and Masters, Statistically Sound Machine Learning for Algorithmic Trading of Financial Instruments, 2014) “measures the price today relative to the price HistLength days ago […] normalized by the standard deviation of daily price changes” and compresses it to -50 to 50, with its form and constant unprinted (
voladjwith k = n, then the Math introduction’s squash, is the nearest shipped form); its stand-alone S&P 100 model with MOM_5_100 and MOM_10_100 reached out-of-sample profit factors of 1.07 to 1.09; costs are not discussed. Chan’s normalisation turned his regression tree’s out-of-sample Sharpe ratio from -0.4 to 0.9 (S&P 500 stocks, 2007 to 2013; his text, where the comments of rTree_SPX.m give -0.37 and 0.87). Evidence, not a reference. - Pinned by
TestRecipeT6NormalisedMomentum.
Distance from the 52-week high and price acceleration (Jansen)
Jansen (2nd ed., ch. 4; price acceleration also in the Appendix’s Alpha Factor Library) lists “Percent off 52-week high”, the percent difference “between the most recent and the highest price for the last 52 weeks” (his first-edition notebook’s PercentBelowHigh, close[-1] / np.max(close, axis=0) - 1 over 252 closes), and price acceleration, “the gradient of the price trend using the linear regression coefficient beta of a time trend on daily prices for a longer and a shorter period”, printed as beta_63 / beta_252. t6OffHigh writes both as Jansen’s ratios.
off := c.Div(c.Highest(n)).Sub(1).SetN(n-1, math.NaN())
accel := c.Slope(a).Div(c.Slope(b)).SetN(b-1, math.NaN())- Inputs: positive closes;
n(252);a<b(63, 252). - First valid index: n - 1 for
off(the window includes today) and b - 1 foraccel; NaN before. - Anchors:
offis at most 0 and exactly 0 on a new n-bar closing high (63 bars of the test path);off.Mul(100)is the single-series relative drawdownc.Div(c.Highest(n)).Sub(1).Mul(100)of Relative: Channels bit for bit; on a rampaccelis exactly 1. - Traps: the ratio explodes and changes sign where the b-bar slope is near 0 (from about -5,166 to 921 on the test path), and on prices a constant growth rate reads above 1. Unmasked,
offreads the drawdown from the expanding high on bars 0 to n - 2. The dossier’s volatility-scaled difference of log slopes is not Jansen’s printed formula. - Variant: percent above the 52-week low,
c.Div(c.Lowest(252)).Sub(1).SetN(251, math.NaN())(Jansen’sPercentAboveLow). - Variant: the acceleration on log prices,
lc := c.Log()andlc.Slope(63).Div(lc.Slope(252)), which reads 1 on constant growth (this page’s form, not Jansen’s). - Evidence: the sources read print no value of either feature, and Jansen uses them only in cross-sectional factor research. Evidence, not a reference.
- Pinned by
TestRecipeT6OffHigh.
Risk and liquidity characteristics (Green, Hand and Zhang; Jansen)
Green, Hand and Zhang define maxret, “Max daily return in calendar month t-1”, retvol, “Standard deviation of daily returns in month t-1”, and dolvol, “Natural log of trading volume times price per share from month t-2”; Jansen’s notebook computes retvol as close.pct_change().rolling(21).std() and dolvol as np.log1p(dv.rolling(21).mean().shift(21)), with dv the close times the volume. t6RiskLiquidity follows that code, except that its maxret keeps the daily definition.
r := c.Div(c.Shift(1)).Sub(1)
maxret := r.Highest(n).SetN(n, math.NaN())
retvol := r.StdDevSample(n).SetN(n, math.NaN())
dolvol := c.Mul(v).Average(n).Shift(n).Log1p().SetN(n+n-1, math.NaN())- Inputs: positive closes and non-negative volumes of one length;
n(21). - First valid index: n for
maxretandretvol(21) and 2n - 1 fordolvol(41); NaN before. - Anchors:
maxretisr.Quantile(n, 1)bit for bit;retvolis pandas’ sample deviation and equals a two-pass loop to rounding;dolvoldiffers from the log of the same mean by less than 1e-7 (dollar volumes are large), and from Green, Hand and Zhang’s log of the month’s total by log(21), which ranks ignore. - Traps: Jansen’s notebook takes the maximum of 21-day returns, against its own markdown and the daily definition; the dossier’s percent returns and population
StdDevare other numbers. A NaN close makesrNaN on its bar and the next, and an infinite close makesmaxretinfinite for n bars. - Variant: Jansen’s notebook maxret,
c.Div(c.Shift(21)).Sub(1).Highest(21).SetN(41, math.NaN()). - Variant: beta and idiovol on the caller’s weekly returns
rwand index returnsmw,rw.Beta(mw, 156)andrw.StdDevSample(156).Mul(rw.Correlation(mw, 156).Squared().Mul(-1).Add(1).Sqrt()); Amihud’s ill is the illiquidity recipe under Bid-Ask Spread Estimators (Jansen’s simple-return form, NaN on zero-volume days). - Evidence: Green, Hand and Zhang test these characteristics in cross-sectional regressions on monthly stock data, and Jansen rank-normalises them across stocks before his model. Evidence, not a reference.
- Pinned by
TestRecipeT6RiskLiquidity.
Where the recipes below take a rate of change, it is in percent: RateOfChange(k) is the fraction form of the factor-momentum page above, c.Div(c.Shift(k)).Sub(1), times 100, bit for bit from bar k on positive prices. The net momentum oscillator and Blau’s True Strength Index, momenta over their own total moves, are under “Momentum”. Each section gives its first valid index; prices are positive and finite and lengths are in bars.
Coppock curve (Coppock; Hayes; Metghalchi and Cloninger)
Tim Hayes (“The Coppock Guide”, Stocks & Commodities V11:3, March 1993, pp. 125-126) computes it “by first determining the 14-month rate of price change of the Dow Jones Industrial Average’s (DJIA) monthly close, then adding that rate to the closing’s 11-month rate of price change, and finally smoothing the result with a 10-month front-weighted moving average”. Coppock’s own steps (Barron’s, 1962, quoted by Middleton in Stocks & Commodities V12:11) multiply “the current period percentage change by 10, last period’s percentage change by 9” and so on, and post “the grand weighted total”, 55 times the average. Massoud Metghalchi and Peggy Cloninger (“Trading Natural Gas Using UNG”, Stocks & Commodities V40:10, September 2022, pp. 20-25) “replaced the 10-month weighted moving average (WMA10) in the original formula with a 10-week exponential moving average (EMA10)”. t6Coppock returns both forms on the caller’s bars.
r := c.RateOfChange(long).Add(c.RateOfChange(short))
cop := r.SetN(long, math.NaN()).WAverage(w)
copE := c.Mul(0).Add(r[long:].XAverage(w)).SetN(long+w-1, math.NaN())- Inputs: the close
con the caller’s monthly or weekly bars (the library does not resample),long >= shortandw(14, 11 and 10); both curves are in percent. - First valid index:
long + w - 1for both (23 for the defaults). - Anchors: masking
rbeforeWAveragechanges no valid bar, sinceWAveragerecomputes each window:copequals the unmaskedr.WAverage(w)bit for bit from bar 23.copEequals an exponential average ofr[long:]seeded with the mean of its firstwvalues, to rounding. Jay Kaeppel’s DJROC example (“A DJIA Long-Term Momentum Indicator”, Stocks & Commodities V13:5, May 1995) witnesses the weighted average: his ten 11-month rates of change,Series{6.08, 4.37, 6.89, 2.42, 3.10, 10.08, 4.42, 4.92, -0.40, -3.62}.WAverage(10), give 2.76 at bar 9 and a weighted total of 151.65, as printed, andRateOfChange(11)from 3978.36 to 3834.44 gives -3.62. Machine transcriptions of his 72-dpi table misread four cells (6.69, 3634.44, 3906.12, 151.85); the arithmetic (3 x 6.89 = 20.67; the column sums to 151.65), his text (3834.44) and the DJIA’s October 1994 close (3908.12) give 6.89, 3834.44, 3908.12 and 151.65. - Traps:
RateOfChangereads 0 in its warm-up, so the sum holds the 11-month change alone on bars 11 to 13: mask before smoothing. A NaN mask would poisonXAveragefor good, so the exponential form slicesr[long:]and re-pads withc.Mul(0).Add(...); unsliced,r.XAverage(w)seeds on the warm-up zeros and is 0.18 off at bar 23 on the test fixture.WAverageis a fused multiply-add on arm64, so its last bits can differ between platforms. - Variant: Kaeppel’s DJROC is
c.RateOfChange(11).SetN(11, math.NaN()).WAverage(10), first valid at 20. - Variant: Coppock’s posted grand weighted total is
cop.Mul(55)(on it the NDR thresholds would be 71.5 and 605). - Evidence: Hayes reports for the NDR rule on the DJIA from July 1957 to December 1992 90% profitable trades and 9.2% a year against 5.4% for buy-and-hold; Metghalchi and Cloninger’s weekly UNG gate (the curve above 0 and the close above its 20-week average) made 46 trades in 14.7 years. Evidence, not a reference.
- Pinned by
TestRecipeT6Coppock; runnable exampleExampleSeries_WAverage_coppock.
Coppock rules, classic and NDR (Coppock; Hayes)
Hayes gives Coppock’s buy, “when the 10-month smoothing dropped below zero and then reversed upward”, and NDR’s rules: “buying whenever the index dropped below 1.3 and then reversed upward”, and “sell signals flashed when the index rises above 1.3, tops out and then drops by 11 points”. t6CoppockRules writes both on a masked curve with CrossesAboveS, HighestSince and BarsSince.
d := cop.Delta(1)
buy := cop.Shift(1).LT(lo).Mul(d.GT(0)).Mul(d.Shift(1).LTE(0))
cross := cop.CrossesAboveS(lo)
sell := cross.BarsSince().LTE(buy.BarsSince()).Mul(cop.HighestSince(cross).Sub(cop).GTE(drop))
pos := cop.Position(buy, sell)- Inputs: the curve
cop, NaN before its first valid bar, the levello(0 for the classic buy, 1.3 for NDR’s) anddrop(11);posis the long-only book at the signal bar’s close (Hayes’s “November 1992’s close”); for next-open fills passbuy.Shift(1)andsell.Shift(1). - First valid index: that of
copplus 2 (25 for 14, 11 and 10): a turn reads the change of the bar before. - Anchors: on the hand-made path NaN, NaN, 5, 2, -1, -3, -2, -2.5, -1.5, 3, 15, 30, 25, 18, 12, 20, 9, 0.5, 1, 2, -4, -6, -5, the classic buy fires on bars 6, 8 and 22, the NDR buy on bars 6, 8, 18 and 22, the cross of 1.3 on bars 9 and 19 and the NDR sell on bars 13, 14, 16 and 17; the book is long on bars 6 to 12, flat on 13 to 17 and long from 18. On the test fixture the book exits only after a cross of
loat or after its entry. - Traps: the turn needs the bar before not to have risen (
d.Shift(1).LTE(0)), so a flat step counts as the trough and a second rise belowlodoes not buy again.Shift(1)pads +0, so for a positivelocop.Shift(1).LT(lo)reads 1 on bar 0; the NaN of the turn there keeps it out. The source leaves open points, read here as follows: “drops by 11 points” is inclusive (GTE); the peak is measured since the cross, not since the entry; a buy and a cross on one bar arm the sell (LTE); a second buy while long disarms the sell until the next cross; and a curve already above 1.3 at its first valid bar is not armed until it crosses again. - Variant: Kaeppel’s DJROC rule, a decline “to -1.0 or below” and a rise, held 12 months or to the first monthly decline, is
cop.Position(cop.Shift(1).LTE(-1).Mul(d.GT(0)).Mul(d.Shift(1).LTE(0)), d.LT(0), 12)on his curve. - Evidence: Hayes’s chart of the rule from 1957 to 1992 shows about 11 buys and 11 sells; in an interview (Stocks & Commodities V38:8, July 2020) Kaeppel lists 14 S&P 500 buys from 1970, held 12 months or until the indicator turned down; their gains average 16.3% (the mean of his table), the three 2002 whipsaws included. Evidence, not a reference.
- Pinned by
TestRecipeT6CoppockRules.
Anchored momentum (Stefanel)
Rudy Stefanel (“Anchored Momentum”, Stocks & Commodities V16:2, February 1998, pp. 89-98; byline “Stefenel”) anchors momentum where a centred simple average ends rather than at a past price: “Most anchored momentum = Latest price - Simple moving average”, with “Simple moving average period = (2*Momentum period) +1”, as a percentage 100( (Latest price/Simple moving average) -1 ). He trades the form “with exponential smoothing of price”, 100*((Ema/ Sma) -1), through “a hysteresis region defined by two lines with the zero line centered between them”, entering above Hyst/2 and exiting below -Hyst/2 “at the next bar at the market”, with MomPer 10, EmaPer 7 and Hyst 1.1 optimised, and “the fund must be held more than 29 days” before the exit ((DateToJulian(Date)-DateToJulian(EntryDate) ) >29). t6AnchoredMomentumHold returns both forms and the book with the minimum hold on PositionSignedRules: SuperCharts’ EntryDate is the fill bar e and the exit condition is read on the signal bar, so the exit acts on a fill bar x with (x-1) - e > 29, that is x - e >= 31, MinHold: 31 on the shifted flags (30 would exit a bar early), with daily bars counted as days.
sma := c.Average(m + m + 1)
am := c.Div(sma).Sub(1).Mul(100).SetN(m+m, math.NaN())
ams := c.XAverage(e).Div(sma).Sub(1).Mul(100).SetN(m+m, math.NaN())
pos := c.PositionSignedRules(ams.GT(hyst/2).Shift(1), ams.LT(-hyst/2).Shift(1), nil, nil, PositionRules{MinHold: 31})- Inputs: the close
c, the momentum periodm, the exponential lengthe <= 2m + 1andhyst(10, 7 and 1.1);amandamsare in percent,posis the long-only book from the bar after the signal. - First valid index:
2mforamandams(20 for m = 10);posreads 0 until the bar after the first entry signal. - Anchors:
amis the disparity recipe’s simple form,x.Div(x.Average(n)).Sub(1).Mul(100), atn = 2m + 1bit for bit. On the ramp100 + 0.25tthe average is the closembars back, soamequalsc.RateOfChange(m)bit for bit, the exact sense of “the anchored momentum plot does not lag the ordinary momentum plot”; there the exponential form is100 b (m - (e - 1)/2)/smawith slopeb, 0 ate = 2m + 1and negative beyond, where Stefanel’s form “switches into reverse”. Without the minimum hold the book isams.Hysteresis(hyst/2, -hyst/2).GT(0).Shift(1)bit for bit (t6AnchoredMomentum, which the clean-room references pin); with it,posequals a loop of SuperCharts’ rules bit for bit on both fixture groups and four parameter sets, and on thesgroup with Stefanel’s 10, 7 and 1.1MinHold: 30differs from those rules on 2 bars and the book without the hold on 53. - Traps: the published code tests levels (
> Hyst/2) where the text says the plot “crosses the upper hysteresis line moving up from below”; they differ only when the first evaluated value is already above the line. A zero average gives -100 throughDiv, where the SuperCharts code returns 0. Mask before a latch, since warm-up values latch too. The exponential seed is unprinted: an SMA seed (XAverage) and a first-value seed (XAverageAlpha) differ by up to 3e-6 from bar 40 on the test fixture’s synthetic walk; the gap is the seed difference shrinking by1 - 2/(e + 1)a bar, so it depends on the first bars. - Variant: the general form against an earlier point of the centred average is
c.Div(c.Average(q).Shift(j)).Sub(1).Mul(100).SetN(q-1+j, math.NaN()), with oddqandj = m - (q - 1)/2. - Departures: the hold is in calendar days and
MinHoldcounts bars, so the book is exact on a series with a bar for every calendar day and approximate on trading days (a trading-day bar usually spans one to three calendar days, and more across a holiday); an exact form needs a caller’s day key, a later facet. - Evidence: on Fidelity Select Biotechnology, 10 in-sample trades from 1993 to 1997 (70% profitable) and 16 out of sample from 1988 to 1993 (69%), with zero costs and a fixed share count: one fund, optimised in sample. Evidence, not a reference.
- Pinned by
TestRecipeT6AnchoredMomentum; runnable exampleExampleSeries_PositionSignedRules_anchoredMomentum.
Speed filter (Vetrivel)
Gomu Vetrivel (“Improve Your Trading System With Speed”, Stocks & Commodities V23:11, November 2005) defines “Speed per bar = Absolute value of close - Close [1]” and notes that “Momentum is nothing but speed with direction”: for closes of 100, 150 and 100, “momentum on day 1 is 50 and on day 2 is -50, whereas speed on day 1 is 50 and day 2 is also 50”. His hypothesis 1 takes a crossover of the 10-bar average “and the speed of the current bar is greater than the speed of the previous bar”, hypothesis 2 when it is less, and his listing tests c>average(c,10) and c[1]<average(c,10)[1] with level exits on the 5-bar average, at c. t6SpeedFilter returns both filters and the hypothesis 1 book.
sp := c.Delta(1).Abs().SetN(1, math.NaN())
faster := sp.GT(sp.Shift(1))
slower := sp.LT(sp.Shift(1))
ma := c.Average(n)
up := c.GT(ma).Mul(c.Shift(1).LT(ma.Shift(1)))
dn := c.LT(ma).Mul(c.Shift(1).GT(ma.Shift(1)))
mx := c.Average(x)
pos := c.PositionSigned(up.Mul(faster), c.LT(mx), dn.Mul(faster), c.GT(mx))- Inputs: the close
c, the cross lengthnand the exit lengthx(10 and 5);fasterandslowerare 0 or 1, andposis +1, -1 or 0 at the signal bar’s close (stop-and-reverse entries, level exits). - First valid index: 2 for
fasterandslower(the masked speed is NaN on bar 0); the strict cross can first fire at barn. - Anchors:
sp.Delta(1).GT(0)equalsfasterbit for bit. Withtie := sp.EQ(sp.Shift(1)), every base entry is a hypothesis 1, hypothesis 2 or tied entry:upequalsup.Mul(faster).Add(up.Mul(slower)).Add(up.Mul(tie)). On Vetrivel’s closes 100, 150 and 100 the speeds tie on day 2, so neither filter passes. - Traps:
c.CrossesAboveS(ma)is not the printed cross: it tests the bar before with<=, so it can fire at barn - 1, whereAveragepassed the close through on barn - 2, and after a bar whose close equals its average (on eleven closes of 10 and then 11 it fires at bar 11, where the strict cross does not).Delta(1)reads 0 on bar 0, so unmaskedfasterwould read 1 on bar 1. - Variant: the hypothesis 2 book is
c.PositionSigned(up.Mul(slower), c.LT(mx), dn.Mul(slower), c.GT(mx)); the TradeStation port’sSpeedLenisc.Delta(j).Abs(). - Evidence: on the RUT and SPX from 2001 to May 2005 (the captions’ period; the text says 2005 only), hypothesis 1 raised the profit factor from 1.16 to 1.33 and from 1.23 to 1.53, and hypothesis 2 lost money, without costs; Wealth-Lab’s run over the S&P 500 components found “near zero correlation”. Evidence, not a reference.
- Pinned by
TestRecipeT6SpeedFilter; runnable exampleExampleSeries_Delta_speed.
Delta / Change
Delta
func (s Series) Delta(n ...int) Series
Delta computes the difference between each value and the value n entries back. By default, n is one.
The warm-up reads 0, not NaN: bars 0 to n-1 are 0, no change, with n defaulting to 1.
DeltaPercent
func (s Series) DeltaPercent(n ...int) Series
DeltaPercent computes the percentage difference between series elements.
The output is a percentage, (s[i] - s[i-n])/s[i-n]*100. The warm-up reads 0, not NaN: bars 0 to n-1 are 0.
Velocity / Acceleration
AvgAcceleration
func (s Series) AvgAcceleration(length int, smoothLength int) Series
AvgAcceleration implements the acceleration component of Scott Cong’s Velocity And Acceleration indicator (as described in TASC September 2023).
The warm-up reads 0, not NaN: bars 0 to 2length-1 are 0 and the first valid index is 2length, the first bar whose lagged velocities are all past AvgVelocity’s warm-up. Releases before this change gave values from bar length, differenced against those warm-up zeros.
AvgVelocity
func (s Series) AvgVelocity(length int, smoothLength int) Series
AvgVelocity implements the velocity component of Scott Cong’s Velocity And Acceleration indicator (as described in TASC September 2023).
The warm-up reads 0, not NaN: bars 0 to length-1 are 0 and the first valid index is length. From bar length the velocity is smoothed with XAverage(smoothLength) as if the series started there: it passes the raw velocity through for smoothLength-1 bars and is then seeded with the simple average of its first smoothLength values. Releases before this change fed the warm-up zeros into the EMA’s seed, so the first values were too small (about half the velocity at smoothLength 3).
RSI
RelativeVolatilityIndex
func (s Series) RelativeVolatilityIndex(sdLen, smoothLen int) Series
RelativeVolatilityIndex returns Donald Dorsey’s Relative Volatility Index: the RSI’s construction with the sdLen-bar standard deviation in place of the price change, smoothed with Wilder’s average over smoothLen bars and put on a 0 to 100 scale.
On each bar t from sdLen-1 on, the up leg is the population standard deviation of the sdLen values ending at t (StdDev) when s[t] > s[t-1] and 0 otherwise, and the down leg is that deviation when s[t] < s[t-1] and 0 otherwise, so an unchanged value feeds neither leg, as in Dorsey and TA-Lib. Each leg is smoothed from bar sdLen-1 on exactly as RMA(smoothLen) smooths it: its average is seeded with the mean of its first smoothLen values and then moves 1/smoothLen of the way to each new value. With U and D the averaged up and down legs, the result is 100 U/(U+D), or 50 when U+D is 0. A constant series therefore gives 50 from the first valid index, and with smoothLen = 1 a rising bar gives 100, a falling bar 0 and an unchanged bar 50, unless the standard deviation underflows to 0 (deviations near 1e-162), when the bar reads 50. Dorsey’s code seeds with the sum of the first values rather than their mean; TA-Lib does not follow it there, and neither does this method.
Dorsey’s defaults are sdLen = 10 and smoothLen = 14, for which the first valid index is bar 22. sdLen must be at least 2 and smoothLen at least 1; invalid parameters give aligned zeros, as does a series too short to reach the first valid index.
The warm-up reads 0, not NaN: bars 0 to sdLen+smoothLen-3 are 0 and the first valid index is sdLen+smoothLen-2.
A NaN or infinite value makes its bar and every later bar NaN (every bar from the first valid index, if it comes before that index), so a non-finite value inside the seed window makes every bar from the first valid index NaN. So does the first bar whose standard deviation, smoothed leg or result is not finite: an overflow, once the standard deviation passes about 1.3e154/sqrt(sdLen). Earlier bars are unchanged.
RelativeVolatilityIndex is recursive, so a slice of the history that starts later gives different values while the start-up state decays.
It matches the TA_RVI goldens of TA-Lib’s regression tests to within 3e-14. On ordinary prices TA-Lib’s TA_RVI differs from it by up to about 5e-10, most at short sdLen, almost all of it because TA-Lib computes the standard deviation from shifted running sums, which lose digits (the two smoothing forms differ by about 4e-14). Where the price level is large against its moves, StdDev loses digits instead and the two differ by more: about 1e-8 for prices near 1e4 that move by 0.01. TradingView and pandas-ta smooth the legs with an exponential moving average instead of Wilder’s, and TradingView also puts an unchanged value in the down leg, so their values differ.
Dorsey’s 1995 refined RVI is the average of the RVI of the highs and of the lows (TA-Lib’s TA_RVIR):
nr := h.RelativeVolatilityIndex(10, 14).Add(l.RelativeVolatilityIndex(10, 14)).Mul(0.5)
His Inertia is the 20-bar simple average of the refined RVI, nr.Average(20), or, in the form he calls slightly better, the 20-bar regression line evaluated 10 bars back, nr.Average(20).Sub(nr.Slope(20).Mul(0.5)). Both read the RVI’s warm-up zeros until 20 valid RVI values exist (bar 41 at the defaults); Dorsey’s code shows 50 until then.
Sources: Donald Dorsey, “The Relative Volatility Index”, Technical Analysis of Stocks & Commodities, June 1993, and “Refining The Relative Volatility Index”, September 1995; TA-Lib (TA_RVI and TA_RVIR).
RSI
func (s Series) RSI(n int) Series
RSI implements Wilder’s Relative Strength Index using a slightly more efficient algorithm than Wilder used.
The output is on a 0 to 100 scale. The warm-up reads 0, not NaN: bars 0 to n-1 are exactly 0, the first valid index is n and the minimum length is n + 1. 0 is the oversold extreme, so a < 30 rule fires on every warm-up bar; trim (Window(-n)) or mask (SetN(n, math.NaN())) the warm-up first. A flat window after the warm-up gives 50. RelativeVolatilityIndex applies the same construction to the standard deviation of the price instead of its change.
RSIWilderOriginal
func (s Series) RSIWilderOriginal(n int) Series
RSIWilderOriginal calculates the n-period Relative Strength Index (using Wilder’s formula).
The output is on a 0 to 100 scale. The warm-up reads 0, not NaN: bars 0 to n-1 are exactly 0, the first valid index is n and the minimum length is n + 1. 0 is the oversold extreme, so a < 30 rule fires on every warm-up bar; trim (Window(-n)) or mask (SetN(n, math.NaN())) the warm-up first. Unlike RSI, a flat window after the warm-up also gives 0.
Compositions
The recipes below read daily closes (dividend-adjusted where a rule reads a total return); the ETF Avalanches also read the high and the open o, and so does the next-open variant of the RSI(2) pullback, and the stops test reads the low and the open. Prices are positive and finite and series are paired by position, so the caller aligns them by timestamp first. Recipe values, levels and results are NaN before their first valid index (SetN(F, math.NaN())), and flags, states and latches read 0 there: RSI(n) reads 0 on bars 0 to n - 1, which an LT threshold would take for an oversold close, so every RSI here is masked, and every shifted series a pad can satisfy is masked, or its flag is; totals of a per-bar result with a NaN head use res.SetN(F, 0).CumSum(). A resting stop or limit is touched when the high reaches it (GTE) unless the source says “above” (strict on a tick grid), a stop fill price is max(o, level), written o.Maximum(level), and a sell limit above the market fills like a buy stop; a sell stop below a long trade is touched when the low reaches it and fills at min(o, level), which PositionSignedExitValue posts. Position tests no exit on its entry bar, so a trade opened and closed on one bar is a bar-level result, not a latch. An exact decimal tie at a threshold, a high exactly 3% above the prior close, is decided by binary rounding: the page pins the prior close times 1 + x formed first, which for x = 0.03 is the double nearest 1.03, so Mul(1.03) gives the same bits. The books’ and the articles’ backtests ran over data we do not have: they are evidence, not a reference. Counts on probe paths that the tests do not reproduce are illustrations, not pins.
RSI(2) pullback above the 200-day average (Connors and Alvarez)
Larry Connors and Cesar Alvarez (Short Term Trading Strategies That Work, TradingMarkets, 2008, ch. 9, p. 62) buy when “The S&P 500 Index is above its 200-day moving average” and “The 2-period RSI of the S&P 500 Index closes below 5”, “on the close”, and “Exit when the S&P closes above its 5-period moving average”. Their RSI is Wilder’s smoothing with a simple-mean seed, which Chris Cain and Larry Connors print (The Alpha Formula, 2019, in its appendix on the RSI) and the package’s RSI computes. t6RSI2Pullback returns the entry flag, the exit flag and the long book, with the 200-day gate tested at entry only.
gate := c.GT(c.Average(gateN))
rsi := c.RSI(rsiN).SetN(rsiN, math.NaN())
entry := gate.Mul(rsi.LT(level))
exit := c.GT(c.Average(exitN))
pos := c.Position(entry, exit)- Inputs: daily closes only (no open, high or low); gateN 200, rsiN 2, level 5 and exitN 5. Their study of stocks above the 200-day average also reads levels of 10, 2 and 1.
- First valid index: gateN - 1 (199) for
entryand exitN - 1 (4) forexit, 0 before:Averagepasses the close through before its first full window, wherec > cfails, so the strict gate needs no mask.poshas no warm-up and enters on bar 199 at the earliest.rsiis NaN on bars 0 and 1; under the gate that mask is a no-op (the test asserts the masked and unmasked entries equal), while the ungatedc.RSI(2).LT(5)fires on bars 0 and 1. - Anchors:
posequals a direct loop of the four rules bit for bit, buying and selling on the close; the latch ignores an entry while long and re-enters at the earliest on the bar after an exit. On the test’s 700-bar path the entry fires on 19 bars (233, 234, 281, 408-410, 434-443, 614, 615 and 681) and the book takes 6 trades. RSI(2) reads 50 on a window flat since its seed, and across unchanged closes after a move it holds its previous value, asRSIWilderOriginaldoes (both of Wilder’s averages halve): bars 261 to 263 of that path close unchanged and read 91.07120178207883. The two methods differ only by rounding (by less than 1e-13 on every fixture group and walk tried; the test bounds it at 1e-13) and on a window flat since the seed, 50 against 0; no golden row tells them apart. - Traps: the dossier’s “RSI seeding dominates at period 2” is wrong:
RSIandRSIWilderOriginalshare the seed and the smoothing, and a later data start moves RSI(2) by less than 0.01 after at most 18 bars (400 starts on the 700-bar fixture group), so the start matters for many bars only at long periods such as 14. UseRSI:RSIWilderOriginalreads 0 on a window flat since the seed, which fires anLT(5)entry. One NaN close makes every later RSI value NaN, while the averages recover once it leaves their windows. The dossier’sSetN(199, 0)on the gate is a no-op on a strict gate; aGTEgate reads 1 on all of bars 0 to 198 and would need it. Its latch, aHysteresisofentry.Sub(exit), isPosition: with thresholds 1 and -1,entry.Sub(exit).Hysteresis(1, -1)is identically 0, since a difference of two 0/1 flags is not beyond strict thresholds, whileentry.Sub(exit).Hysteresis(0, 0).GT(0)equalsPositionwherever the two flags are not both 1. Repeating the gate as an exit is another rule: on public S&P 500 data it gives 395.71 points where the book prints 522.92 (an evidence check). - Variant: the cumulative RSI (ch. 9, p. 67: “Take the past X days of the 2-period RSI and add them up”, buy “below Y”, “Exit when the 2-period RSI closes above 65”):
cum := rsi.Sum(2)andc.Position(gate.Mul(cum.LT(35)), rsi.GT(65)), F 3, sinceSumof the masked RSI is NaN on bars 0 to 2 and recovers from bar 3; Y is 50 or 40 for indices and 10 for stocks, and the index form (ch. 12, strategy 4) sums two days ofc.RSI(3).SetN(3, math.NaN())below 45 with the same exit, F 4. Their SPY counts (50 and 105 signals) are not reproduced from public data under any reading tried. - Variant: next-open timing, as in the example of Connors and Ashton Dorkins, “an entry on the open of the next day” (“Does The RSI Give You An Edge?”, Stocks & Commodities V25:11, November 2007, pp. 48-52):
c.Position(entry.Shift(1), nil, 5), with the trade views read on the openo(o.TradeResult(pos)). - Members: (Davey’s numbers are those of his Algo Trading Cheat Codes, 2021, ch. 5)
- their ch. 13 exits for
exit:c.GT(c.Average(10)),rsi.GT(65)(or 70, 75), a first up close, a 7-day high - their ch. 13 time exits:
c.Position(entry, nil, k)with k 1, 3 or 5 (out on the close of bar e + k) - entry levels of 10, 2 or 1 in place of 5, the other thresholds of their ch. 9 study
- Double 7s, and Double 5 to Double 10 (the next section)
- ETF Avalanches, the bear-market short mirror with a limit fill (the last section)
- Clenow’s volatility-normalised pullback in an EMA trend (the True Range page
t6-atr-pullback.md) - the Holy Grail of Raschke and Connors (the Average Directional Index page
t6-holy-grail.md) - D’Errico’s SMA trend with a
CrossesAboveSof a maskedRSI, filled at the next open and held BarsIN + 1 bars - Raschke’s retracement model: an
XAverageover anAverage,RSIbelow 40, a buy stop above two bars’ highs - Davey’s MA stretch (#4):
a := c.Average(n).SetN(n-1, math.NaN())anda.Sub(c).GT(a.Mul(k)) - Davey’s %b stretch (#3): two closes below 0.1 of the
Bollingerband, %b held byValueWhenon a flat band - Davey’s down closes (#6):
c.ConsecDown().GTE(float64(m)), m from 2 to 5, out when the streak ends - Davey’s reverse breakout (#7): Double 7s without the gate,
c.EQ(c.Lowest(n)).SetN(n-1, 0), with a longer exit - Davey’s ch. 6 baseline: the close crossing up over
Momentumof 10 bars and down over 5, with anATRstop - left out: Davey’s #1 and #2 (RSI(2) and ConnorsRSI crosses, covered here), #5 (a printed bug), #8 to #10 (weak)
- left out: his Entry and Exit Confessions of a Champion Trader #4, #6, #27 and #28 (not pullbacks in an uptrend)
- their ch. 6 stops test, a stop below the entry with re-entry on the next signal (the section after Double 7s)
- left out: Gerald and Trent Gardner’s “Profit With ETFs” (Stocks & Commodities V26:6, June 2008), another system
- left out: the inverse-ETF rule for bear markets, which is this recipe run on the inverse ETF’s own series
- their ch. 13 exits for
- Evidence: ch. 9 prints 49 signals on the S&P 500 index from 1995 to 2007, 83.6% correct and 522.92 points, an average hold of three days, with no costs stated, in sample (the level comes from their study of the same years). On public data Wilder’s RSI(2) gives 49 trades, 83.67% and 524.60 points, and the three printed 2007 trades (bought on 7 June, 27 July and 19 October) reproduce to the dates, closes and points, while a simple-average or an exponential RSI(2) gives 240 or 133 trades. Evidence, not a reference.
- Pinned by
TestRecipeT6RSI2Pullback; runnable exampleExampleSeries_RSI_pullback.
Double 7s (Connors and Penn)
Larry Connors and David Penn (“Three Rules, One Easy Way To Trade ETFs”, Stocks & Commodities V27:1, January 2009, pp. 66-69) buy when “The ETF is above the 200-day moving average” and “The ETF closes at a seven-day low. Buy on the close”, and “Exit when the ETF closes at a seven-day high”; Connors and Alvarez give the same rules for the SPY (Short Term Trading Strategies That Work, 2008, ch. 10, pp. 75-80). t6DoubleSevens reads a seven-day low as the lowest of the last seven closes, today’s included and ties counted, and returns the entry, the exit and the long book.
entry := c.GT(c.Average(gateN)).Mul(c.EQ(c.Lowest(n)).SetN(n-1, 0))
exit := c.EQ(c.Highest(n)).SetN(n-1, 0)
pos := c.Position(entry, exit)- Inputs: daily closes only; gateN 200 and n 7 (Double 5 to Double 10 take n from 5 to 10: “They all hold up well in the testing”).
- First valid index: gateN - 1 (199) for
entryand n - 1 (6) forexit, 0 before;poshas no warm-up.LowestandHighestrun over expanding windows on bars 0 to 5, so bar 0 is both a 7-day low and a 7-day high, andSetN(n-1, 0)masks them; under the gate the low flag’s mask changes no bit, but it keeps the flag right without the gate (the second variant). - Anchors:
c.Stochastic(c, c, 7).EQ(0)equals the low flagc.EQ(c.Lowest(7)).SetN(6, 0)andc.PercentR(c, c, c, 7).EQ(100)the high flag bit for bit from bar 6, flat seven-close windows included (on bars 0 to 5 both oscillators read 50, which compares false like the masked flags);c.EQ(c.Lowest(7))equalsc.LTE(c.Lowest(7)), andc.EQ(c.Highest(7))equalsc.GTE(c.Highest(7)).posequals a direct loop of the three rules, andentry.Sub(exit).Hysteresis(0, 0).GT(0)wherever no bar carries both flags (which takes a flat window above the average). On the test’s 700-bar path the entry fires on 50 bars (the first 215, 216, 233, 234 and 258) and the book takes 19 trades. - Traps: the dossier’s “Stochastic or PercentR on closes over 7 bars equal 0 or 100 at the same bars” holds for two pairings only: a flat window reads 0 in
Stochasticand 100 inPercentR, soc.PercentR(c, c, c, 7).EQ(0)andc.Stochastic(c, c, 7).EQ(100)miss the flat windows, which are both lows and highs;c.WilliamsR(c, c, c, 7).EQ(0)is the high flag from bar 6 but reads 1 on bars 0 to 5. Unmasked,c.EQ(c.Lowest(7))fires on bar 0 and on every running low of the warm-up. The dossier’sSetN(199, 0)on the gate is a no-op on a strict gate, and its latch isPosition, not a strategy layer. The article’s “price channels”, a strict break of the seven prior closes (c.Breakout(7, 1)), are another rule: on public S&P 500 data they give 112 trades and 1,104.53 points against 132 and 1,133.83, and repeating the gate as an exit gives 722.45. - Variant: strict lows and highs, below (above) all six prior closes,
c.Breakout(6, 1).EQ(-1)andc.Breakout(6, 1).EQ(1), F 6; they differ from the inclusive flags only on ties (none on two-decimal S&P 500 closes from 1995 to 2008; on the SPY 137 trades against 140). - Variant: Rule 1 of Connors and Alvarez (ch. 2, pp. 9-16), “bought every 10-period low and exited when the market crossed above its 10-period moving average”, with no 200-day gate:
c.Position(c.EQ(c.Lowest(10)).SetN(9, 0), c.GT(c.Average(10))), F 9. - Evidence: the article reports +1,133 points on the S&P 500 from January 1995 to April 2008, “with nearly 80% of the trades closing profitably”, 2,822 points and 79.84% on the NASDAQ 100, 73% correct on FXI and more than 80% on EWZ, long only, no costs stated, with the seven-day lookback chosen in the same testing (in sample). On public S&P 500 closes the inclusive rule gives 132 trades, 79.55% and 1,133.83 points, the article’s trade of 27 November to 4 December 2006 among them; the NASDAQ 100 gives 2,608.82 points (data vendors differ). Evidence, not a reference.
- Pinned by
TestRecipeT6DoubleSevens; runnable exampleExampleSeries_Lowest_doubleSevens.
Stops on a 10-day low (Connors and Alvarez)
Larry Connors and Cesar Alvarez (Short Term Trading Strategies That Work, TradingMarkets, 2008, ch. 6, “Rule 5 - Stops Hurt”, pp. 31-34) test stops on a pullback system: “A stock is above its 200-day moving average and closes at a 10-day low. The exit is a close above its 10-day moving average, or when a stop is hit x% below the entry.” Their table (p. 32) runs stops from 1% to 50%, and the trades rise as the stop tightens (236,237 with no stop, 394,480 at 1%), so a stopped trade takes the next signal; they explain the harm by orders that rest in the market: “market makers, specialists and professional traders know that the stops are sitting there and many times they are gunning for them” (p. 33). t6StopStudy reads the 10-day low as Double 7s reads its 7-day low and returns the entry and exit flags, the book without a stop and, on PositionSignedRules, the stopped book with a sell stop x below the entry close, touched by the low, the value each exit was read at and each trade’s result.
gate := c.GT(c.Average(gateN))
entry := gate.Mul(c.EQ(c.Lowest(n)).SetN(n-1, 0))
exit := c.GT(c.Average(exitN))
pos := c.Position(entry, exit)
r := PositionRules{
LongStop: PositionLevel{Level: c.Mul(1 - x), Touch: l},
Open: o,
}
stopped := c.PositionSignedRules(entry, exit, nil, nil, r)
ex := c.PositionSignedExitValue(entry, exit, nil, nil, r)
res := ex.Sub(c.EntryValue(stopped).Shift(1))- Inputs: daily opens, lows and closes; gateN 200, n 10 and exitN 10; x, the stop as a fraction of the entry close (0.01, 0.03, 0.05, 0.07, 0.1, 0.2 and 0.5 in the table).
- First valid index: gateN - 1 (199) for
entryand exitN - 1 (9) forexit, 0 before;posandstoppedread 0 until the first entry, andexandresare NaN until the first exit ofstoppedand on every bar that is not an exit bar. - Anchors:
entryis Double 7s’ entry at n 10 andposits book, bit for bit.stopped,exandresequal a loop of the test’s rules (bought on the entry close, the stop at that close times 1 - x read from the next bar on, touched by the low and filled at the level or at an open below it, otherwise out at the exit close, flat after either until the next entry) bit for bit on the 700-bar group and two 3,000-bar walks, for every stop of the table; a stop of 100% (a level of 0) leavesstoppedequal topos. On a walkpostakes 57 trades, andstopped75 at 1%, 60 at 3% and 57 from 5% on: 3 of the 60 at 3% enter whereposwas long on the bar before, which the stop pattern of the Trade State introduction, cut until the base’s next entry, does not take. The runnable example’s constructed closes give an exit at the close and a stop touched exactly by a low. - Traps: the test’s sentence does not say whether “a stop is hit” by a close or by the price during the day; the authors explain the harm by stops “sitting there” for traders to gun, so the page reads a resting order: the stop is touched by the low (
Touch: l, inclusive) and fills at its level, or at the open when the bar opens below it. Bought on the close, the entry bar’s low came before the fill, so the latch leaves the entry bar untested, as the source does. The level is the entry close times1 - xformed first,c.Mul(1 - x), sampled on the entry bar. - Variant: the close test,
LongStop: PositionLevel{Level: c.Mul(1 - x)}, strict on the receiver and out at the close: on the walk at 3% it differs from the touch on 25 bars. - Evidence: the table (p. 32), stop, trades, average result, bars held and winners: none, 236,237, 0.58%, 7.74 and 69.81%; 1%, 394,480, 0.19%, 2.89 and 26.89%; 3%, 321,824, 0.20%, 4.11 and 47.31%; 5%, 286,991, 0.20%, 5.01 and 57.54%; 7%, 268,410, 0.22%, 5.65 and 62.72%; 10%, 253,863, 0.27%, 6.31 and 66.55%; 20%, 239,605, 0.39%, 7.26 and 69.35%; 50%, 236,337, 0.56%, 7.70 and 69.81%. The chapter states neither the stocks, the years nor the costs, and it says the result holds for stocks and indices and was not tested on options, futures or forex. Evidence, not a reference.
- Pinned by
TestRecipeT6StopStudy; runnable exampleExampleSeries_PositionSignedRules_stopStudy.
ETF Avalanches (Cain and Connors)
Chris Cain and Larry Connors (The Alpha Formula, The Connors Group, 2019, ch. 8) short an ETF that shows “a negative total return over the past year (252 trading days)” and “over the past month (21 trading days)” and whose “2-period RSI is above 70”, with “a sell limit order 3.0% above the current price” that “is canceled” if unfilled by the end of the day, and cover when “the 2-period RSI is below 15” or the one-month return “turns positive”, each “checked every business day at the close”. t6ETFAvalanches sets the limit at the prior close times 1 + x, fills it when the high reaches it, and returns the fill flag, the fill price px, the short book and day, which marks one-bar trades taken while flat (short at px, covered at the fill day’s close).
rsi := c.RSI(2).SetN(2, math.NaN())
setup := c.RateOfChange(252).LT(0).Mul(c.RateOfChange(21).LT(0)).Mul(rsi.GT(70))
lvl := c.Shift(1).Mul(1+x).SetN(1, math.NaN())
fill := setup.Shift(1).Mul(h.GTE(lvl))
px := o.Maximum(lvl)
exit := rsi.LT(15).Add(c.RateOfChange(21).GT(0)).GT(0)
oneBar := fill.Mul(exit)
pos := c.PositionSigned(nil, nil, fill.Sub(oneBar), exit)
day := oneBar.Mul(pos.Shift(1).EQ(0))- Inputs: daily opens, highs and dividend-adjusted closes (a “total return”; the book’s data were adjusted); x 0.03 (they find “positive test results using other limit order entry levels ranging from 1.5% - 3.5%”). The open enters only
px. Rules 7 to 9, at most five shorts ranked by their 100-day historical volatility and the rest of the capital in a Treasury ETF, are a cross-section and a portfolio, out of scope. - First valid index: 252 for the setup, which reads exactly 0 on bars 0 to 251 without a mask (
RateOfChangeis 0 there andLT(0)fails); 253 forfillandday, 0 before;lvlandpxare NaN on bar 0, andpxis meaningful on fill bars only;poshas no warm-up. - Anchors: the fill is the trade-state page
t2-latched-entries.md’s “Stop entry above the prior close (Connors)”, which cites this book: a sell limit above the market fills like a buy stop, when the high reaches the level, atmax(o, level), hereo.Maximum(lvl)in place of itsIf. For positive closesc.RateOfChange(n).LT(0)equalsc.LT(c.Shift(n))bit for bit, andc.RateOfChange(21).GT(0)equalsc.GT(c.Shift(21)).SetN(21, 0). On the test’s falling 600-bar path the limit fills on bars 274, 329, 359, 430, 489 and 577 at 106.21875, 103.77250000000001, 97.359375, 92.249375000000001, 83.6875 and 72.5625 (two at the open, two on a high exactly at the limit),daymarks 329 and 430, andposis -1 on bars 274-278, 359, 360, 489, 490, 577 and 578. A Python loop of the rules and the composition gave identical trade lists on 3,000 synthetic bars. The strictrsi.GT(70)is pinned by a constructed RSI(2) of exactly 70, thedaymask by a fill while short, and the 21-bar rate’s signs by a close equal to the close 21 bars before. - Traps: the authors are Chris Cain and Larry Connors (copyright 2019), not “Connors and Radtke”.
PositionandPositionSignedtest no exit on the entry bar, soc.PositionSigned(nil, nil, fill, exit)would hold a fill whose own close meets an exit to a later close; splittingfillintofill.Sub(oneBar)andoneBarbooks that trade on the fill day, as “every business day at the close” requires, and its result is the bar-levelpx.Sub(c).Mul(day), NaN on bar 0, totalled withres.SetN(1, 0).CumSum(). A fill while already short adds nothing:dayreads 0 there, and if that bar meets an exit the open short is covered at its close. A setup bar cannot be an exit bar (RSI(2) above 70 against below 15, a negative 21-day return against a positive one). “The current price” at “the beginning of the day” is ambiguous; the trade-state page tests the open-anchored readingo.Mul(1 + x)on the same bar. The rates of change read 0 on the first 252 and 21 bars of whatever history is passed, so the first valid index moves with the start of the slice. - Evidence: Table 8.2 (2007 to 2018, 40 country, sector and regional ETFs, data and backtests from Quantopian) gives 2.40% a year against -10.60% for SH, a volatility of 10.20% against 19.80%, a maximum drawdown of -20.00% against -86.50% and a Sharpe ratio of 0.24 against -0.54, with “Ability to borrow the ETFs and borrowing costs not included”; the text adds 19.49% in 2008. The book does not say whether the rules were fixed before that period. Evidence, not a reference.
- Pinned by
TestRecipeT6ETFAvalanches.
The LBR/RSI is Wilder’s RSI of the one-bar change. Its value is NaN before its first valid index, where the RSI of the changes would read its warm-up zeros, the oversold extreme; the setups read 0 there and are decided at the setup bar’s close. Wilder’s smoothing is the RSI’s own, so no other seed is chosen. The variants page below holds Altman’s relative momentum index, an RSI of the m-bar momentum built with the same slice and realignment; the LBR/RSI is not an RMI. The trade-state pages hold exits a close can decide. Prices are positive and finite and lengths are in bars.
LBR/RSI (Raschke and Connors)
Linda Bradford Raschke and Laurence Connors (Street Smarts, 1995, ch. 7, “Momentum Pinball”, pp. 51-57) use “a one-period rate of change or ‘momentum’ function. This is simply, the difference between today’s close and yesterday’s close”, and “A three-period RSI of this one period change is calculated.” Day one of a buy setup “is determined by an LBR/RSI value of less than 30”, of a sell setup by “an RSI value greater than 70”. t6LBRRSI computes the RSI on the changes from bar 1, so that Delta’s bar-0 pad is not read as a change, and realigns it with the closes.
d := c.Delta(1)
lbr := c.Mul(0).Add(d[1:].RSI(n)).SetN(n+1, math.NaN())
buy := lbr.LT(30)
sell := lbr.GT(70)- Inputs: the close
cand n (3);lbrruns from 0 to 100 andbuyandsellare 0 or 1. - First valid index: n + 1 (4), where TA-Lib’s
RSI(MOM(close, 1), 3)also starts. Minimum length 1: an empty series panics, and one bar gives NaN only. - Anchors: an RSI reads its input through its one-bar changes, so this is Wilder’s ratio on second differences: with
dd := c.Delta(1)[1:].Delta(1)[1:],c.Mul(0).Add(dd.RMA(n).Div(dd.Abs().RMA(n)).Add(1).Mul(50))masked to n + 1 equalslbrto rounding. On the constructed quarter-point closes 100, 101, 101.5, 101.25, 102.25, 102, 101, 101.75, 103, 102.5, 102.75 and 104 it reads 50, 200/7, 2000/97, 22900/383, 3875/58, 124000/3557, 466700/9301 and 36164/547 on bars 4 to 11, the exact rationals to rounding, with buys on bars 5 and 6 and no sell. Adding a linear trend to the closes or scaling them leaves it unchanged (bit for bit on a quarter grid), and a constant drift reads 50: it measures acceleration, not trend. - Traps:
c.Delta(1).RSI(3)feedsDelta’s bar-0 pad into Wilder’s seed as a change: on the example it reads 69.70 at bar 4 wherelbrreads 50, misses both buys and adds a sell on bar 8, and on the test fixture it is 17 points off at bar 4 and beyond 1e-9 until bar 64.c.Delta(1).SetN(1, math.NaN()).RSI(3)makes every value NaN, and a missing close makes every later value NaN. The levels are strict, as printed (“less than 30”). The RSI fuses multiply-adds on arm64, so its last bits depend on the platform. - Variant: the inclusive levels
lbr.LTE(30)andlbr.GTE(70), which differ only on readings of exactly 30 or 70, possible on grid prices. - Missing piece: the trade itself is intraday: a buy stop above the first hour’s high on day two, a protective stop at its low, a re-entry “on a buy stop at the original price”, carrying a winner overnight and the exit on the next morning’s follow-through need sessions, tier B (algolang).
- Evidence: Moore Research’s daily tests in the book’s appendix (Exhibits A.7 and A.8: 25 futures, 1986 to 1995, entry at the next open, no stops or costs, in sample): to the next open, buy setups won more often than not in 25 of 25 markets (median 58%) and sell setups in 22 of 25, but held to the close the sells did so in only 14. Evidence, not a reference.
- Pinned by
TestRecipeT6LBRRSI; runnable exampleExampleSeries_RSI_lbr.
The compositions below read Wilder’s RSI, which is exactly 0 on bars 0 to n - 1, so each masks or slices its warm-up before a channel or a smoothing. Chande and Kroll’s unsmoothed RSI, the first section’s variant, is c.CMO(n).Add(100).Div(2) within 3e-14 from bar n, the net momentum oscillator under “Momentum”; the LBR/RSI on t6-rsi-of-change.md above is Wilder’s RSI of the one-bar change, not a relative momentum index; and the RSI(2) pullback, Double 7s and ETF Avalanches are on t6-pullback-entries.md above. Each section gives its first valid index; prices are positive and finite and lengths are in bars.
Stochastic RSI (Chande and Kroll)
Tushar Chande and Stanley Kroll (“Stochastic RSI And Dynamic Momentum Index”, Stocks & Commodities V11:5, May 1993, pp. 189-199) define “the unsmoothed stochastic RSI” as (RSI_Today - RSI_Low) / (RSI_High - RSI_Low), where RSIHigh and RSILow “are the highest and lowest values of RSI over a given lookback period”, and the midpoint oscillator of their Equation 21, RSI%M = 100 (2(RSI_Today) - RSI_High - RSI_Low) / (RSI_High - RSI_Low), read against +100 and -100. “For symmetry” the lookback is the RSI’s own length, 14, and “You can multiply stochRSI by 100 if you wish”. t6StochRSI takes Wilder’s RSI, as their MetaStock listing and figures do, and its channel position on the 0 to 100 scale.
st := c.RSI(n).ChannelPosition(k, 1).SetN(n+k-1, math.NaN())
pm := st.Mul(2).Sub(100)- Inputs: the close
c, the RSI lengthnand the channel lengthk(14 and 14);struns from 0 to 100 andpmfrom -100 to +100. - First valid index:
n + k - 1(27 for 14 and 14).RSIreads exactly 0 on bars 0 ton - 1, and the channel reads those zeros as its low until barn + k - 2. - Anchors:
stequals the sliced formc.RSI(n)[n:].ChannelPosition(k, 1)bit for bit from barn + k - 1, andrsi.Stochastic(rsi, rsi, k)bit for bit off flat windows (ChannelPositionperforms the stochastic’s three operations in its order);pmequals Equation 21 to rounding, and the negated close gives100 - stto rounding. The RSI’s first value is the unsmoothed ratio: on the five S&P 500 closes of their Figures 1 to 4 (375.22, 381.40, 380.96, 380.07, 377.39),c.RSI(4)at bar 4 is 60.64769382, their “100*(6.18)/(6.18+4.01)”. - Traps: a flat RSI window, where the ratio is 0/0, reads 50 (
pm0), whilersi.Stochastic(rsi, rsi, k)reads 0, an oversold value. After a flat price stretch the RSI is constant in exact arithmetic but drifts by rounding, so a k-bar window inside the stretch has a range of about 1e-14 andstjumps between 0 and 100 on noise: the test fixture’s flat stretches of 17 and 19 bars givet6StochRSI(c, 14, 14)12 such bars. A test such asrsi.Highest(k).Sub(rsi.Lowest(k)).GT(1e-9)separates them. - Variant: Chande and Kroll’s text prefers the unsmoothed RSI,
d.Sum(n).Div(d.Abs().Sum(n)).Add(1).Mul(50)withd := c.Delta(1), which equalsRSI(n)at barnonly; their MetaStock stochRSI (formula 22a) is on 0 to 1,st.Div(100), and their MetaStock RSI%M (formula 23) on +-1,st.Div(50).Sub(1). - Variant: they define “the unsmoothed stochastic RSI” and, for their unsmoothed RSI and RMI, smooth with “a simple or exponential moving average” of unstated length; smooth the valid part,
st[n+k-1:].Average(q), since an average passes a NaN through its warm-up andXAveragekeeps one for good. - Evidence: their stochastic RSI figures (the DJIA in 1990 and 1991) are charts; Weinberg’s walk-forward of Peterson’s band system with it (Stocks & Commodities V27:1, January 2009) made 89 trades with 47 winners over 2004 to 2007 on the NASDAQ-100 members current when he wrote (late 2008), a survivorship-biased universe. Evidence, not a reference.
- Pinned by
TestRecipeT6StochRSI; runnable exampleExampleSeries_ChannelPosition_stochRSI.
SveStochRSI (Vervoort)
Sylvain Vervoort (“The V-Trade, Part 6: Technical Analysis - Divergence Indicators”, Stocks & Commodities V36:9, August 2018, pp. 16-24) describes the stochastic RSI as “an oscillator that fluctuates between zero and 100” and his SRSI as “a smoothed combination of the stochastics and RSI oscillators”. His MQL4 listing averages the RSI less its 5-bar low and the 5-bar range over 8 bars each, and divides with a guard, ema_Buffer1[i]/(0.1 + (ema_Buffer2[i]))*100, on a 21-bar RSI. t6SveStochRSI is that listing on Wilder’s RSI.
r := c.RSI(n).SetN(n, math.NaN())
ll := r.Lowest(k)
sve := r.Sub(ll).Average(p).Div(r.Highest(k).Sub(ll).Average(p).Add(0.1)).Mul(100)- Inputs: the close
c, the RSI lengthn, the channel lengthkand the averaging lengthp(21, 5 and 8); the result runs from 0 to just under 100. - First valid index:
n + k + p - 2(32 for 21, 5 and 8). The masked RSI makes every earlier bar NaN, throughLowest,HighestandAverage. - Anchors: the result equals the sliced form (the same statements on
c.RSI(n)[n:]) bit for bit from barn + k + p - 2. Without the guard it isc.RSI(n)[n:].ChannelPosition(k, p), a ratio of p-bar sums, to rounding off flat windows. - Traps: the guard of
0.1RSI points lowers every positive reading: on the test fixture with 21, 5 and 8 the unguarded ratio is higher wherever its numerator is positive, by up to 14.33 points (on windows whose p-bar average range is above 1e-3). A flat RSI range reads 0, whereChannelPosition(k, p)reads 50. MetaTrader’siRSIseed is unprinted, so platform values are not references; the listing’s input check restores 21, 5 and 8 only when an input is below 1. - Variant: the house form without the guard is
c.RSI(n)[n:].ChannelPosition(k, p), right-aligned withc. - Evidence: Vervoort reads SRSI divergences on charts only (“The main purpose of the SRSI is to confirm corrections and reversals based on the convergent or divergent moves”); no system statistic is printed, only hand-picked chart trades. Evidence, not a reference.
- Pinned by
TestRecipeT6SveStochRSI.
Relative Momentum Index (Altman; Chande and Kroll)
Roger Altman (“Relative Momentum Index: Modifying RSI”, Stocks & Commodities V11:2, February 1993, pp. 57-60, and the sidebar “The RSI and the RMI”) counts “up and down days starting from today’s close relative to the close Y days ago, where Y is not necessarily 1 as required by the RSI”, with Wilder’s smoothing (MU = Yesterday's MU - (MU/X) + ...) from a 20-day sum; his spreadsheet uses X = 20 and Y = 5. Chande and Kroll (Stocks & Commodities V11:5, May 1993, Figure 5) give the unsmoothed form RMI = 100*(Su)/(Su + Sd) over the whole lookback. t6RMI returns Altman’s RMI as rmi and Chande and Kroll’s as rmiC.
rmi := c.Mul(0).Add(c.Sum(m)[m-1:].RSI(n)).SetN(m+n-1, math.NaN())
d := c.Delta(m)
rmiC := d.Sum(n).Div(d.Abs().Sum(n)).Add(1).Mul(50).SetN(m+n-1, math.NaN())- Inputs: the close
c, the averaging lengthnand the momentum lagm(Altman’s X and Y, 20 and 5; Chande and Kroll call the lag the lookback). - First valid index:
m + n - 1for both (bar 24 for 20 and 5, the sidebar’s row 26). - Anchors: the one-bar changes of
c.Sum(m)are the m-bar momenta, sormiis Wilder’s RSI of them, and the slice drops the partial sums; withm = 1the slice is the close, andt6RMI(c, n, 1)equalsc.RSI(n)bit for bit from barn. The spreadsheet formd[m:].Max(0).RMA(n)over the sum of the up and down averages equalsrmito rounding and prints the sidebar’s columns E, F, G and H on rows 26 to 30 at two decimals (H: 95.90, 95.94, 95.96, 95.97, 93.68).rmiequalsrmiCto rounding at barm + n - 1, where Wilder’s seed is the unsmoothed ratio, and they part after it.rmiCprints Figure 5’s 95.89736225, with the momentum totals 191.75 and 208.89, Su 200.32 and Sd 8.57; their net momentum oscillatord.Sum(n).Div(d.Abs().Sum(n)).Mul(100)is2 rmiC - 100to rounding; and their Equation 14’s telescoping,c.Delta(m).Sum(n)equal toc.Delta(n).Sum(m), holds from barm + n - 1(bit for bit on the sidebar, 191.74999999999994). - Traps:
c.Sum(m).Window(1 - m)fails form = 1, sinceWindow(0)is empty; the slice[m-1:]works for every m. A NaN close makes the rolling sum NaN formbars and Wilder’s recursion NaN for good. Unmasked,c.Sum(m).RSI(n)seeds Wilder’s recursion on the partial sums and converges only geometrically: on the test fixture it is 51 points off at bar 27 and 0.07 at bar 200. Before barm + n - 1the sliced form reads exactly 0, an oversold value. A window whose momenta are all 0 reads 50 in both forms, where Altman’s spreadsheet divides by 0. - Variant: Chande and Kroll smooth the unsmoothed form with “a simple or exponential moving average”, for example the five periods of their Figure 8,
rmiC[m+n-1:].Average(5). - Evidence: Altman found that the original RSI (momentum 1) “had the worst average performance” of the momentum values 1 to 9 on about 10 years of S&P 500 futures, but “not large enough to be statistically significant”; his October 1993 forward test reports return on account against randomised data. Evidence, not a reference.
- Pinned by
TestRecipeT6RMI; runnable exampleExampleSeries_RSI_rmi.
Connors RSI
ConnorsRSI
func (s Series) ConnorsRSI(rsiPeriod, streakRSIPeriod, rocPeriod int) Series
ConnorsRSI computes the Connors RSI composite oscillator. Developed by Larry Connors, it combines three components: a standard RSI, an RSI of the up/down streak length, and a percentile rank of the rate of change. The result is the average of these three values.
The output is on a 0 to 100 scale. The warm-up reads 0, not NaN: bars 0 to F-1 are exactly 0, where F = max(rsiPeriod, streakRSIPeriod+1, rocPeriod+1) is the first valid index: the first bar at which the rate-of-change rank no longer compares with bar 0’s rate of change and the streak RSI’s first window no longer starts at bar 0’s streak, both of which are taken as 0. The component RSIs keep the fading memory of their start that Wilder’s smoothing gives RSI itself. 0 fires Connors’s entries below 5 to 10, so mask bars before F before a threshold rule. Releases before this change gave values from bar rocPeriod, including ranks against bar 0’s rate of change and averages of the component RSIs’ warm-up zeros.
A series shorter than rocPeriod gives zeros. Releases before this change also gave zeros for any series shorter than rsiPeriod+streakRSIPeriod-2 bars, so when that exceeded F a short history did not match the start of a longer one; it now does.
Stochastic
Stochastic
func (s Series) Stochastic(h, l Series, n int) Series
Stochastic calculates the percentage value of each input value across the range determined by looking back at the last n values. Mismatched input lengths return a zero-filled series of the receiver’s length.
The output is on a 0 to 100 scale, 100*(s - LL)/(HH - LL), with HH and LL the highest high and lowest low of the last n bars. Bars 0 to n-2 are exactly 50 (warm-up), not 0: the first valid index is n-1 and the minimum length is n, and n > len(s) gives all 50. 50 is mid-range, so neither an oversold (< 20) nor an overbought (> 80) rule fires on a warm-up bar, but a crossing of 50 or a comparison with a moving average of the output can. Trim the warm-up (Window(-(n-1))) before smoothing, because a NaN mask would poison a recursive average such as XAverage; before a threshold rule, trim or mask it (SetN(n-1, math.NaN())). A flat window (HH == LL) gives 0, not 50, so the warm-up and flat values differ. This warm-up was reviewed in GLE-254 (3 October 2026) and kept, because 0 would read as the oversold extreme and would change every later bar of an XAverage of the output. ChannelPosition(n, 1) gives the house form, exactly 0 before its first valid index n-1 and 50 on a flat channel, and equals s.Stochastic(s, s, n) on windows that are not flat, after that index. BreakoutModeIndex averages the 9-bar and 14-bar outputs over 3 bars for its mid-range condition. The single-series form x.Stochastic(x, x, n) on a caller’s series is the COT index of a group’s net position, and iv.Stochastic(iv, iv, n+1) the implied-volatility rank over the previous n sessions and today as the recipes pin it (Stochastic(n), n bars including today, is thinkorswim’s); those recipes are listed under “Compositions” in the “Stochastic” and “Volatility Estimators” categories.
The slow stochastic, Bernstein’s stochastic POP and Blau’s Stochastic Momentum Index are listed under “Compositions” in the “Stochastic” category.
Compositions
Three readings of the close in its high-low range: Blau’s Stochastic Momentum Index, Lane’s slow stochastic with Pruitt’s crossover, and Bernstein’s stochastic POP. Values are NaN before their first valid index; flags and books read 0 there and are decided at bar t’s close. The SMI’s exponential averages start at their first input on the valid slice, as Blau’s own sidebar does (the seed rule of the True Strength Index under “Momentum”); the slow lines are simple averages. The compositions page below holds the Anti, whose k and d are the slow lines here, and Lane’s ratio-of-sums slowing; the POP’s dwell idea returns as Pruitt’s smoothed-CCI dwell under “Commodity Channel Index”, and the brackets of Pruitt’s systems are the trade-state recipe “Entry-anchored bracket on the close (Pruitt)”. Prices are positive and finite and lengths are in bars.
Stochastic Momentum Index (Blau; Star)
William Blau (“Stochastic Momentum”, Stocks & Commodities V11:1, January 1993, pp. 11-18) defines the stochastic momentum “SM(q) = Close - 0.5 (HH:q + LL:q)” and the index as 100 times its double exponential average over half the double average of the range, “the bipolar form of the Ds-stochastic”, in “a q = 13-day lookback with EMA smoothings of 25 and two days” with a signal line “normally in the range of three to 12 bars”. Barbara Star (“Swing Trade With The Gann Hi-Lo Activator”, V38:11, October 2020, pp. 8-13) uses 8, 3 and 3 in thinkScript, “plot SMI = if avgdiff != 0 then avgrel / (avgdiff / 2) * 100 else 0”, with a three-period signal. t6SMI starts both averages at the first full window with their first values, as Blau’s sidebar does, and returns the index and its signal.
ar := 2 / (float64(r) + 1)
as := 2 / (float64(s) + 1)
hh := h.Highest(q)
ll := l.Lowest(q)
sm := c.Sub(hh.Add(ll).Mul(0.5))
rg := hh.Sub(ll)
num := sm[q-1:].XAverageAlpha(ar).XAverageAlpha(as)
den := rg[q-1:].XAverageAlpha(ar).XAverageAlpha(as)
smi := c.Mul(0).Add(num.Div(den.Mul(0.5)).Mul(100)).SetN(q-1, math.NaN())
sig := c.Mul(0).Add(smi[q-1:].XAverageAlpha(2/(float64(m)+1))).SetN(q-1, math.NaN())- Inputs:
h,landc, the lookbackq, the smoothing lengthsrandsand the signal lengthm(13, 25, 2 and 3; Star’s 8, 3, 3 and 3);smiandsigrun from -100 to +100 when every close is inside its bar’s range. Arch-identical. - First valid index: q - 1 for both, the first full window, where Blau’s sidebar prints its first SMI. Minimum length q - 1: a shorter series panics, and q - 1 bars give NaN only.
- Anchors: Blau’s sidebar “Stochastics Momentum Index” (SMI of 2, 20 and 20 on January 1992 bars), through the same statements with his rounded 0.095 at both stages, gives all 171 cells of its computed columns (the window’s high and low, the stochastic momentum, its two averages, the range, its two averages and the SMI) at two decimals; ten of the stochastic momentum cells are exact half-way decimals, which the statements as written put on the printed side by 2e-15 to 6e-14, so compute them as written (the test allows 0.005 + 1e-12). The helper’s 2/21 matches only 3 of the 19 SMI cells. SMI = 2 DS - 100, Blau’s DS-stochastic on the same seeds, to rounding; q = 1 is the double-smoothed close location (2c - h - l)/(h - l), bit for bit on a quarter grid; r = s = 1 is
2 Stochastic - 100off flat windows, where the SMI reads 0 and that form -100; mirrored bars give -SMI bit for bit. - Traps:
Div’s 0 matches the listing’s zero rule only while the range has been 0 on every bar since bar q - 1; after any range a flat stretch reads the ratio of two decaying memories (-100 after one bar of range in the test). The dossier’s unslicedXAverageform seeds onHighest’s expanding windows and the pass-through bars. A NaN high, low or close makes every latersmiandsigNaN: the recursion does not recover. - Variant: TradingView’s SMA seeds on the valid values, valid from
q + r + s - 3and panicking on short input:sm[q-1:].XAverage(r)[r-1:].XAverage(s)[s-1:]over the same chain ofrg, right-aligned; it differs from the first-value form by about 4.6 points at most on the test fixture at 13, 25 and 2, by tens of points on other series, and agrees within 1e-6 only after about 230 bars. - Variant: Blau’s trend
smi.GT(sig)and his crossessmi.CrossesAboveS(sig); Star’s zero linesmi.GT(0)(her colour test issmi.GTE(0)); the levels +-40 (+-20 at q = 2). - Evidence: Blau’s 19 figures and Star’s are charts; in the October 2020 Traders’ Tips, Zorro’s test of Star’s three-way agreement on NVDA reports “big losses in 2017 and 2018”. Evidence, not a reference.
- Pinned by
TestRecipeT6SMI; runnable exampleExampleSeries_Highest_smi.
Slow stochastic and Pruitt’s crossover (Lane; Pruitt)
George Pruitt (The Ultimate Algorithmic Trading System Toolbox, Wiley, 2016, ch. 2, pp. 41-47) states Lane’s slow stochastic: “In the slow framework the fast %D becomes the slow %K and the slow %D becomes a three-period moving average of the slow %K”, with “default parameters for the slow stochastic are (14, 3, 3)”. His Box 2.4 system buys “If mySlowK crosses above mySlowD and mySlowD < 20” and sells short on the cross below with the slow %D above 80, as the p-code and Appendix D have it (the box’s prose swaps the two). t6SlowStochastic returns the two slow lines and his two flags.
k := c.Stochastic(h, l, n).SetN(n-1, math.NaN())
slowK := k.Average(sk)
slowD := slowK.Average(sd)
buy := slowK.CrossesAboveS(slowD).Mul(slowD.LT(20))
short := slowK.CrossesBelowS(slowD).Mul(slowD.GT(80))- Inputs:
h,landc, the %K lengthnand the slowingsskandsd(14, 3 and 3); the lines run from 0 to 100 and the flags are 0 or 1. Arch-identical. - First valid index:
n + sk - 2forslowK(15) andn + sk + sd - 3forslowD(17); the flags can first fire atn + sk + sd - 2(18). The mask keepsStochastic’s warm-up 50s out: unmasked,slowDreads 45.61, 38.01, 26.52 and 21.02 on bars 13 to 16 of the test fixture. - Anchors: at 3 and 3 the slow lines are the Anti’s
kandd(t6Anti) bit for bit, whose test pins them against TA-Lib’s STOCH; the masked lines equal the sliced ones bit for bit. Steckler’s sidebar “Calculating stochastics” (“Trading Stochastic Pops”, Stocks & Commodities V18:8, August 2000):c.Stochastic(h, l, 10)prints its 14 %K cells, and its %D is a ratio of sums (below), which prints its 12 %D cells where the helper’s average of ratios misses every one by at least 0.0068 (and by up to 3.48). With a slowing of 1 the two slowings are the same. - Traps:
CrossesAboveStakes the prior bar with<=; Pruitt defines his cross with a strict<on the prior bar,slowK.Shift(1).LT(slowD.Shift(1)).Mul(slowK.GT(slowD)), which keeps 35 of the test fixture’s 37 buys, the others following an exact tie of the two lines. The levels are strict. nseries has noStochasticSlowmethod; a ratio-of-sums one is planned, not built. Pruitt’s PythonstochClass(Appendix C) has a 3-bar window and Wilder’s smoothing, and is not a reference.Stochasticreads 0, the oversold extreme, on a flat n-bar window, so a flat stretch can satisfyslowD.LT(20), and the POP’sLTE(25), without any decline. - Variant: Lane’s ratio of sums (Schirding 1984, Takano 1989, Blau 1991 and Steckler’s sidebar), with
hh := h.Highest(n)andll := l.Lowest(n):c.Sub(ll).Sum(sk).Div(hh.Sub(ll).Sum(sk)).Mul(100).SetN(n+sk-2, math.NaN())and its sd-bar average. - Missing piece: the system’s exits, a 3-ATR target and a 1-ATR stop on the close, are the trade-state recipe “Entry-anchored bracket on the close (Pruitt)” on the base
c.PositionSigned(buy, nil, short, nil); TradeStation re-enters on a later cross inside a base trade after a cut, which is tier B (algolang). - Evidence: Pruitt’s Table 2.4 (35 futures in TradeStation, no costs stated) sums to a loss of 684,391 with 12 of 35 markets positive. Evidence, not a reference.
- Pinned by
TestRecipeT6SlowStochastic; runnable exampleExampleSeries_Stochastic_slow.
Stochastic POP (Bernstein)
Jake Bernstein (Short-Term Futures Trading, revised edition, 1993, ch. 3, pp. 43-56) goes long “As soon as %K and %D rise above 75 percent on a closing basis” and stays long “until the two lines cross”, liquidating rather than reversing: “When this happens, don’t sell short; just liquidate your long”. His appendix (“Summary and Review of Indicators and Techniques”) triggers a buy POP when the lines “close at 75 percent or higher for a given period after having been below 75 percent”, and a sell POP mirrors it at 25. t6StochasticPOP runs it on the slow lines of the section above (14, 3 and 3), both lines at or beyond the level.
k := c.Stochastic(h, l, n).SetN(n-1, math.NaN()).Average(3)
d := k.Average(3)
up := k.GTE(75).Mul(d.GTE(75)).SetN(n+3, math.NaN())
dn := k.LTE(25).Mul(d.LTE(25)).SetN(n+3, math.NaN())
longIn := up.CrossesAboveS(0)
shortIn := dn.CrossesAboveS(0)
cross := k.CrossesAboveS(d).Add(k.CrossesBelowS(d))
pos := c.PositionSigned(longIn, cross, shortIn, cross)- Inputs:
h,landcand the %K lengthn(14);longIn,shortInandcrossare 0 or 1 andposis +1, -1 or 0 after bar t’s close, sopos.Shift(1)is the book from the next open, Bernstein’s “at the market” once the bar has completed. Arch-identical. - First valid index: n + 4 (18) for the events: the slow %K is valid from n + 1 and the slow %D from n + 3, and
upanddnare NaN before n + 3 so that an event needs a previous bar known to be outside the level. - Anchors:
up.CrossesAboveS(0)isup.Mul(up.Shift(1).EQ(0))on a 0 or 1 flag, “after having been below”; a steady ramp, overbought from its first valid bar, enters on no bar; the levels are inclusive (“75 percent and above”). The smoothed-CCI dwell under “Commodity Channel Index” is the same dwell idea on Lambert’s CCI. - Traps:
PositionSignedreverses on an opposite POP while in a position (bar 190 of the test fixture), where Bernstein liquidates only on the cross; a sell POP without a prior cross is rare. The exit is any cross, as printed, and %K can be below %D on the entry bar, so the next cross can be upward (1 of the test fixture’s 15 long entries). His appendix’s “one or both SI values” make a looser entry, and his charts use CQG’s exponential 5 and 3 where his text says 3-period averages. - Variant: the exit only against the position,
c.PositionSigned(longIn, k.CrossesBelowS(d), shortIn, k.CrossesAboveS(d)), changes 2 bars of the test fixture’s book. - Variant: Joe Luisi (“The Stochastic Oscillator”, Stocks & Commodities V15:12, December 1997) counts the days above 70, his buy being “a move above 70 holds for five days”: with
k5 := c.Stochastic(h, l, 5).SetN(4, math.NaN()),k5.Lowest(m).GT(70)at m = 5, which equalsk5.GT(70).Sum(m).EQ(m). - Evidence: Bernstein’s charts print CQG readings only (“These charts merely show the raw signals”), and Luisi gives no statistics. Evidence, not a reference.
- Pinned by
TestRecipeT6StochasticPOP.
The compositions below read the stochastic c.Stochastic(h, l, n) or, of a single series, s.Stochastic(s, s, n), whose house form is s.ChannelPosition(n, 1) (in the Relative: Channels category): the two agree bit for bit off flat windows, and on a flat window the stochastic reads 0 where the channel position reads 50. The slow stochastic, whose lines at the same lengths are the Anti’s k and d below bit for bit, Bernstein’s stochastic POP and Blau’s Stochastic Momentum Index are on t6-smi-slow-and-pop.md above; the single-series form x.Stochastic(x, x, n) on a caller’s series is the COT index of t6-stochastic-cot-index.md below and, as iv.Stochastic(iv, iv, n+1), the implied-volatility rank under “Volatility Estimators”.
Slope divergence (Kaufman)
Perry Kaufman (“Slope Divergence: Capitalizing On Uncertainty”, Stocks & Commodities V32:7, June 2014, pp. 20-25) finds divergence with regression slopes: “If the signs of the slopes are opposite, you have a divergence”, over three short periods at once, and in his method “the direction of the price usually wins”. He enters “When at least two of the three calculation periods show divergence at the same time” and exits “when all three price slopes move in the same direction as all three momentum slopes”; his pick for the QQQ is a momentum of 25 and slopes of 5, 12 and 14 bars. t6SlopeDivergence counts the votes of his EasyLanguage listing on the fast %K and books them long and short.
mom := c.Stochastic(h, l, p).SetN(p-1, math.NaN())
ps1, ms1 := c.Slope(a), mom.Slope(a)
ps2, ms2 := c.Slope(b), mom.Slope(b)
ps3, ms3 := c.Slope(d), mom.Slope(d)
nbuys := ps1.GT(0).Mul(ms1.LT(0)).Add(ps2.GT(0).Mul(ms2.LT(0))).Add(ps3.GT(0).Mul(ms3.LT(0))).SetN(p+d-2, 0)
nsells := ps1.LT(0).Mul(ms1.GT(0)).Add(ps2.LT(0).Mul(ms2.GT(0))).Add(ps3.LT(0).Mul(ms3.GT(0))).SetN(p+d-2, 0)
exit := ps1.GTE(0).Mul(ms1.GTE(0)).Mul(ps2.GTE(0)).Mul(ms2.GTE(0)).Mul(ps3.GTE(0)).Mul(ms3.GTE(0)).
Add(ps1.LT(0).Mul(ms1.LT(0)).Mul(ps2.LT(0)).Mul(ms2.LT(0)).Mul(ps3.LT(0)).Mul(ms3.LT(0))).SetN(p+d-2, 0)
pos := c.PositionSigned(nbuys.GTE(k), exit, nsells.GTE(k), exit)- Inputs: the high
h, lowland closec, the stochastic lengthp, the slope periodsa <= b <= dand the votek(25; 5, 12 and 14; two of three);nbuysandnsellscount from 0 to 3,exitis 0 or 1 andposis +1, -1 or 0, decided at bar t’s close (pos.Shift(1)is the book from the next open, where the listing fills). - First valid index:
p + d - 2(37 for 25 and 14).Slopereads 0, not NaN, in its warm-up, whichGTE(0)would count as up, so theSetN(p+d-2, 0)masks do the work; the NaN mask onmomkeeps each period’s flag honest, since the stochastic reads exactly 50 on bars 0 top - 2. - Anchors: the exit and the entries do not fire together (six slopes pointing one way leave no divergence), so
exit.Mul(nbuys.Add(nsells))is 0 on every bar; long only,c.PositionSigned(nbuys.GTE(k), exit, nil, nil)equalsc.Position(nbuys.GTE(k), exit); the flags depend only on the signs of the slopes, so doublingh,landcchanges no bit. - Traps: the printed listing adds
dvgsell2twice and does not countdvgsell3(“This code was used to produce the QQQ results”); its replica iss1.Add(s2).Add(s2)withs1 := ps1.LT(0).Mul(ms1.GT(0))and so on. A slope that is 0 in exact arithmetic can be a rounding residual: the centred fit subtracts a rounded mean, so the window 98, 98, 98, 97.5, 98.25 givesc.Slope(5)= -4.2e-18, not +0, and its sign test is decided by rounding (three votes of the test fixture are). Withk = 1a flat book can meet a buy vote and a sell vote on one bar; the listing then goes long, whilePositionSignedstays flat. - Variant: AmiBroker’s reading of
fastdis the 3-bar average of %K,mom := c.Stochastic(h, l, p).SetN(p-1, math.NaN()).Average(3), first valid atp + d. - Missing piece: Kaufman’s full version waits “until it first starts moving in your direction”, an exit armed by the open trade, and his listing closes a reversed trade at the signal close and opens the new one at the next open: tier B (algolang).
- Evidence: on the QQQ from 2000 to early 2014, 453 of 504 parameter tests (89.9%) were profitable net of a cost of 8 a trade; trades lasted 6.1 days on average, and the published long-short results include the
nsellsdefect. Evidence, not a reference. - Pinned by
TestRecipeT6SlopeDivergence; runnable exampleExampleSeries_Slope_divergence.
Raschke’s 3/10 oscillator, its slow-K and momentum first (Raschke; Kaufman; Ruggiero)
Linda Bradford Raschke (Trading Sardines, 2022, ch. 10) describes her oscillator as “the difference between a 3 and 10-period simple moving average”, with “A 16-period moving average of the 3/10” overlaid. Perry Kaufman (“The 1st And 2nd Cross”, Stocks & Commodities V38:3, March 2020, pp. 24-27) applies “a slow-K stochastic to the momentum value, osc”, and Murray Ruggiero (Cybernetic Trading Strategies, 1997, Table 4.13) buys “when the oscillator sets a new high” above zero and sells a new low below it, because “Momentum precedes price”. t6Raschke310 returns the oscillator, its signal line, the slow-K and Ruggiero’s flags.
fast := c.Average(f)
slow := c.Average(s)
osc := fast.Sub(slow).SetN(s-1, math.NaN())
sig := fast.Sub(slow).Average(m).SetN(s+m-2, math.NaN())
k := fast.Sub(slow).ChannelPosition(n, 3).SetN(s+n, math.NaN())
buy := osc.EQ(osc.Highest(lookback)).Mul(osc.GT(0))
sell := osc.EQ(osc.Lowest(lookback)).Mul(osc.LT(0))- Inputs: the close
c,fands(3 and 10), the signal lengthm(16), the stochastic lengthn(Kaufman’s “Mom = 3”, the only printed candidate) andlookback(32);kruns from 0 to 100,buyandsellare 0 or 1. - First valid index:
s - 1forosc,s + m - 2forsig,s + nforkands + lookback - 2for the flags (9, 24, 13 and 40).sigandkread the unmasked difference and mask their outputs:ChannelPositionreads +0 before its own first valid index, which a band test would take as oversold. - Anchors:
osc.GT(0)equalsfast.GT(slow)andosc.CrossesAboveS(0)equalsfast.CrossesAboveS(slow)bit for bit from bars(an IEEE difference of finite values is 0 only for equal values); of the sliced oscillatorx,x.ChannelPosition(n, 1)equalsx.Stochastic(x, x, n)bit for bit off flat windows. - Traps:
ChannelPosition(n, 3)is Lane’s ratio of 3-bar sums, not the 3-bar average of %K: on the test fixture they differ on 303 of 587 bars, by up to 33.3 points. Unmasked, the oscillator’s warm-up (the average passes the close through) gives false momentum-first flags: 9 buys and 5 sells on the test fixture’s first 40 bars.EQrepeats a flag on a repeated extreme, as the listing’sOsc = Highest(Osc,LookBack)does. - Variant: Kaufman’s simultaneous 1st cross is
osc.CrossesAboveS(osc.Average(q)).Mul(l.GT(l.Shift(1))); his band 1st cross is the 2nd cross’s first-occurrence gate,cand.Mul(cand.SumSince(osc.CrossesAboveS(0)).EQ(1))withcand := k.LT(20).Mul(osc.GT(0)). - Variant: Raschke’s divergence “8-12 bars apart” is
l.SwingLowDivergence(osc, q).Mul(gap.GTE(8)).Mul(gap.LTE(12))withgap := l.SwingLowAge(q, 1).Sub(l.SwingLowAge(q)), onSwingLowDivergenceandSwingLowAge; her pivot strengthqis unprinted. - Missing piece: Ruggiero’s stop exits (
ExitLong at Lowest(Low,StopLen) Stop) fill at the stop or at a gapped open and can exit on the entry bar: tier B (algolang). - Evidence: Ruggiero’s yen test made 192 trades with 43% winners from 1976 to 1996, with LookBack 32 and StopLen 12 chosen from 110 tests; Kaufman’s figures are re-optimised in sample on ES and SPY since 1998. Evidence, not a reference.
- Pinned by
TestRecipeT6Raschke310; runnable exampleExampleSeries_Average_raschke310.
The Anti (Raschke and Connors)
Raschke and Laurence Connors (Street Smarts, 1995, ch. 9, pp. 70-77) “Use a seven-period %K stochastic”, whose smoothing should “default to four”, and “a 10-period %D”. The buy setup is a %D that “has established a definite upward-trend” while a pullback pulls %K towards it, entered when %K turns “up once again in the direction of the slow line (forming a hook)”, after %K and %D “have formed opposing slopes for at least three days”. t6Anti reads the trend as a rising %D on the hook bar and the tension as the three bars before it.
k := c.Stochastic(h, l, n).SetN(n-1, math.NaN()).Average(sk)
d := k.Average(sd)
dk := k.Delta(1)
dd := d.Delta(1)
buy := dk.GT(0).Mul(dk.Shift(1).LT(0)).Mul(dd.GT(0)).Mul(dk.LT(0).Mul(dd.GT(0)).Shift(1).Sum(3).EQ(3))
sell := dk.LT(0).Mul(dk.Shift(1).GT(0)).Mul(dd.LT(0)).Mul(dk.GT(0).Mul(dd.LT(0)).Shift(1).Sum(3).EQ(3))- Inputs:
h,l,c, the %K lengthn, its smoothingskand the %D lengthsd(7, 4 and 10);kanddrun from 0 to 100,buyandsellare 0 or 1. - First valid index:
n + sk - 2forkandn + sk + sd - 3ford(9 and 18), NaN before them; a flag needsddon its own bar and the three before it, so the first can fire atn + sk + sd + 1. - Anchors:
kanddequal a loop replica of TA-Lib’s STOCH(7, 4 SMA, 10 SMA), a raw %K (0 on a flat window) slowed and smoothed by simple averages, to rounding and with the same first valid bars. - Traps: the stochastic reads exactly 50 on bars 0 to
n - 2, which the averages would mix intokandd, hence the NaN mask;Averageis not recursive, so the NaN leaves as the windows pass. A flat %K window reads 0, an oversold value. Two bars of tension are not enough: the hook needs three bars of tension. - Variant: Lane’s ratio-of-sums slowing is
c.Sub(l.Lowest(n)).Sum(sk).Div(h.Highest(n).Sub(l.Lowest(n)).Sum(sk)).Mul(100). - Missing piece: the anticipatory trigger, “a buy stop one tick above the previous day’s bar”, trailed down each day, is an order: tier B (algolang).
- Evidence: the chapter shows charts only (crude oil, cotton, Micron, five-minute S&P bars) and notes that “the average holding time is three to four days”. Evidence, not a reference.
- Pinned by
TestRecipeT6Anti.
The 2nd cross (Kaufman)
Kaufman (Stocks & Commodities V38:3, March 2020, pp. 26-27) builds on a fast and a slow average: “1. The trend turns up when the fast trend crosses above the slow trend. 2. The 1st cross begins when the stochastic value falls below 50. 3. The 1st cross ends when the stochastic value rises above 50.” The buy “occurs when the stochastic value falls below 20 … after a 1st cross has occurred” and “is exited when the stochastic value goes above 95 or the trend changes direction”; shorts mirror it at 60 and 45, on averages of 11 and 80 bars in his figure. t6SecondCross writes the sequence with SumSince, without a state machine.
osc := c.Average(f).Sub(c.Average(s)).SetN(s-1, math.NaN())
k := c.Average(f).Sub(c.Average(s)).ChannelPosition(n, 3).SetN(s+n, math.NaN())
upStart := osc.CrossesAboveS(0)
dnStart := osc.CrossesBelowS(0)
done := k.GT(50).Mul(k.LT(50).SumSince(upStart).GT(0)).SumSince(upStart).GT(0)
doneS := k.LT(50).Mul(k.GT(50).SumSince(dnStart).GT(0)).SumSince(dnStart).GT(0)
longIn := k.LT(20).Mul(osc.GT(0)).Mul(done)
shortIn := k.GT(60).Mul(osc.LT(0)).Mul(doneS)
pos := c.PositionSigned(longIn, k.GT(95).Add(osc.LTE(0)).GT(0), shortIn, k.LT(45).Add(osc.GTE(0)).GT(0))- Inputs: the close
c,fands(11 and 80) and the stochastic lengthn(his “Mom = 3”, the only printed candidate);longInandshortInare 0 or 1 andposis +1, -1 or 0 at bar t’s close. - First valid index:
s + n(83); at bars + n - 1the slow-K would read the average’s pass-through on bars - 2. - Anchors:
longIn,shortInandposequal a direct state machine of rules 1 to 5 and their mirror bit for bit (the test runs it on the fixture and on walks). - Traps:
SumSinceis NaN before the first up-start, so a trend already running at bar 0 is not traded (“following a new up-trend”). The bands are strict level tests: a slow-K of exactly 20 does not buy. The short side is a reading: Kaufman writes that the rules are “opposite”, but also that a short at 60 “will cause profits to be taken on longs at 60”. - Variant: one entry per trend instead of repeats is
longIn.Mul(longIn.SumSince(upStart).EQ(1)). - Evidence: his 2nd cross on ES made 86 trades, 65% profitable, with a profit factor of 2.79, in sample from 1998 with no costs stated. Evidence, not a reference.
- Pinned by
TestRecipeT6SecondCross.
Schaff Trend Cycle (Twomey; Schaff)
Brian Twomey (“The Schaff Trend Cycle”, Stocks & Commodities V28:5, April 2010, pp. 98, 96, 97 and 87) prints Doug Schaff’s EasyLanguage: a 10-bar stochastic of the MACD(23, 50) that holds its previous value on a range that is not positive, Frac1=IFF(Value2 > 0, ((XMAC-Value1)/Value2) * 100,Frac1(1)), smoothed by PF(1) + (Factor * (Frac1-PF(1))) with Factor(.5), then the same stochastic and smoothing of PF. “Buy and sell signals are generated when the signal line breaches 25 at the bottom and 75 at the top.” t6Schaff runs the code on the MACD from its first full bar.
m := c.MACD(fast, slow)[slow-1:]
ll1 := m.Lowest(cycle)
rng1 := m.Highest(cycle).Sub(ll1)
pf := m.Sub(ll1).Div(rng1).Mul(100).ValueWhen(rng1.GT(0).SetN(1, 1)).XAverageAlpha(factor)
ll2 := pf.Lowest(cycle)
rng2 := pf.Highest(cycle).Sub(ll2)
pff := pf.Sub(ll2).Div(rng2).Mul(100).ValueWhen(rng2.GT(0).SetN(1, 1)).XAverageAlpha(factor)
stc := c.Mul(0).Add(pff).MaskNonFinite(c.Mul(0).Add(m)).SetN(slow+cycle+cycle-3, math.NaN())- Inputs: the close
c,cycle,fast,slowandfactor(10, 23, 50 and 0.5); the result runs from 0 to 100. - First valid index:
slow + 2(cycle - 1) - 1(67 for the defaults); before it a window of either stochastic is partial. - Anchors: on the first element of the slice the range is 0 and
Divgives 0, Schaff’s initialFrac1;SetN(1, 1)makes that bar an event, andValueWhenthen holds the latest stochastic with a positive range, so the hold equalsk.If(math.IsNaN, 0.0, k)of the plain hold bit for bit, andm.ChannelPosition(cycle, 1)bit for bit off flat windows. On the exponential100 x 1.004^tthe MACD rises every bar and every second-stage window is flat: the STC reads exactly 100 from bar 103, where the ChannelPosition formc.MACD(23, 50).ChannelPosition(10, 1).XAverage(3).ChannelPosition(10, 1).XAverage(3)reads 50 and crosses below 75 once. - Traps:
XAverageinsideMACDdoes not recover from a NaN close, and the holds would keep the stochastics finite, soMaskNonFinitemarks every bar with a non-finite MACD as NaN. The result depends on where the history starts: a later start moves it by more than 1e-3 for about 300 bars (289 to 358 on five seeded walks).XAverageAlphaseeds with its first value, asPF=IFF(CurrentBar <=1,Frac1,...)does;XAverage(3)would seed with a 3-bar mean. nseriesMACDseeds its averages with simple means, where a first-value seed is common on trading platforms, so platform values differ early on even from the same first bar. - Variant: the signals are
stc.CrossesAboveS(25)(buy) andstc.CrossesBelowS(75)(short); Twomey’s own cycle was 20 (“My cycle period was set at 20”). - Evidence: Twomey’s hourly and daily currency charts and hand-picked trades in pips, with no statistics; the July 2010 erratum says “several errors appeared in the EasyLanguage code”, and the corrected code is not available here. Evidence, not a reference.
- Pinned by
TestRecipeT6Schaff; runnable exampleExampleSeries_MACD_schaffTrendCycle.
The compositions below read the Commitments of Traders report as an external series: each group’s long and short positions in contracts as of Tuesday’s close, released on the Friday after (weekly since 2000, every two weeks from 1992 and twice a month from 1990; in a week with a federal holiday the CFTC moves the release to the Monday after), one value per weekly bar. The report files carry the compilation date, so the caller pairs each report with the first bar on which it could be acted on; that release-date alignment is the caller’s, since nseries has no timestamps and cannot detect a misalignment, and the aligned inputs are then caller-supplied series paired by position and right-aligned at the most recent bar as the Breadth introduction (“Breadth inputs”) says. The groups are the legacy report’s: the commercials (cl, cs), the non-commercials, large speculators or funds (fl, fs), and the non-reportables or small traders (sl, ss), with the open interest oi (not nl, the Breadth introduction’s new lows); a change of contract size multiplies the positions by a factor from its date, so the caller rescales the history first. Positions are integer contracts, so nets and sums are exact; the indexes run from 0 to 100, ranks from 0 to 1, and lengths count weekly reports. Values are NaN before their first valid index F (SetN(F, math.NaN())) and flags read 0 there. A bar with no trading is not an input bar, and neither is a report with no open interest (Div reads 0 on a zero divisor). 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. 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: the Friday release comes after most US futures closes, so with weekly bars ending on Friday a setup on one week’s report is first tradable at the next week’s open, the book pos.Shift(1) (see the Trade State introduction).
COT index and movement index (Briese; Williams; Ruggiero)
Stephen Briese (The Commitments of Traders Bible, Wiley, 2008, ch. 6, pp. 63-73) writes that “The COT Index formula is identical to George Lane’s %K stochastics indicator, substituting net positions for price”: 100 times (current net - minimum net) over (maximum net - minimum net), with “the original three years” as the look-back, and his movement index (ch. 7, pp. 75-81) is “the difference between the COT Index and its reading of one or several weeks prior. I use six.” Larry Williams (Trade Stocks & Commodities with the Insiders: Secrets of the COT Report, Wiley, 2005, ch. 4, pp. 33-40) compares “the current week’s level of the net long/short position to where it had been over the prior three years”, and Murray Ruggiero (Cybernetic Trading Strategies, Wiley, 1997, ch. 5, pp. 86-91) prints the same formula with a look-back that “can vary between 1.5 and 4 years”. t6COTIndex gives a group’s net, its n-report stochastic and the k-report change of that index.
net := longs.Sub(shorts)
coti := net.Stochastic(net, net, n).SetN(n-1, math.NaN())
mi := coti.Momentum(k).SetN(n+k-1, math.NaN())- Inputs: one group’s longs and shorts, integer contracts on weekly reports;
nreports including the current one, 156 for three years (Briese’s Figure 6.3 is labelled “COT 156 Week Commercial”), 26 for Williams’s six-month index or 13 for Briese’s short look-back;k6 reports. The window includes the current report: the sources print readings of exactly 0 and 100, and Ruggiero’s TradeStation Highest and Lowest include the current bar. Briese reads his levels inclusively, a buy at “90 percent+” (coti.GTE(90)) and a sell “within 5 percent of the bottom” (coti.LTE(5)), though one passage has the index “below the 5 percent sell signal threshold”. Williams’s levels are strict,coti.GT(80)andcoti.LT(20)(“above 80 percent”, “below 20 percent”), and 75 and 25 in his 2011 book. Ruggiero’s classic rule is strict at 90 and 10,coti.GT(90)andcoti.LT(10). Briese’s surges, “a +40 point movement in the COT Index within a six-week period”, are read inclusively,mi.GTE(40)andmi.LTE(-40). - First valid index: n - 1 for
cotiand n + k - 1 formi, NaN before. The stochastic reads 50 on bars 0 to n - 2, a value no source has (three years of reports are needed), andMomentum(k)writes 0 on bars 0 to k - 1 whatever its input, somiis masked itself, not only throughcoti. The level and surge flags read 0 before their first valid index. - Anchors: Briese’s example (pp. 64-65): a 156-report window holding the minimum -112,469, the maximum 40,186 and the current net -44,892 (39,569 - 84,461) gives 44.267793390324591, bit for bit 100 times 67,577 over 152,655 (“COT Index = 44%”), and with the maximum printed on p. 64, +40,408, it gives 44.20351001131629, also 44%. Williams’s (p. 34): the current 350 between a lowest 150 and a highest 750 gives 33.333333333333329 in
Stochastic’s order, 200/600 times 100, one unit in the last place below 100 times 200 over 600 (33.333333333333336); the current 1,000 between 500 and 9,200 gives 5.7471264367816088 (“5.7 percent”). Multiplying the nets by a power of two changes no bit;cotilies in [0, 100] andmiin [-100, 100]. - Traps: Williams prints the lowest value as “-150”, but the minus is the operator of a column subtraction (350 - 150 = 200, 750 - 150 = 600): the lowest is +150, and with -150 the formula gives 55.6%, not his 33%. Briese prints the maximum net as +40,408 on p. 64 and computes with 40,186 on p. 65 (44.268% against 44.204%, both 44%), and his first substituted line drops the minus sign of the minimum. A flat window reads 0, the bearish extreme, so it fires every sell (
coti.LTE(5),coti.LT(20),coti.LT(10)). Unmasked, the warm-up 50 makes surges: in the dossier’scoti.Momentum(6)of the unmasked index, bars n - 1 to n + 4 measure the step from 50 to a real reading, up to 50 points, which can pass +/-40. Ruggiero’s lagged dual index (his rule 2’s “buy” is a misprint for sell) and Briese’s overlays on the net (%K/%D, MACD, Bollinger bands and a simple-sum RSI, which Wilder’snet.RSI(14)does not give) are notes here, not recipes. - Variant: for a net with flat stretches (a group with no reportable positions, a new contract),
net.ChannelPosition(n, 1).SetN(n-1, math.NaN())reads 50, Briese’s “neutral reading”, not 0, on a flat window, and equalscotibit for bit on every other bar from n - 1 (its warm-up is +0, also masked). - Variant: Williams’s WILLCO (ch. 10, p. 105), “a stochastic of the net of commercial longs minus shorts divided by the total open interest” over “a 26-week window”:
x := cl.Sub(cs).Div(oi)thenx.Stochastic(x, x, 26).SetN(25, math.NaN()), a buy setup above “the buy trigger zone of 75-80”, with no sell level printed. The next sentence says the charts use “the total commercial short position divided by OI”: withythe short sharecs.Div(oi)and withclandoiconstant over the window,y.Stochastic(y, y, 26).Mul(-1).Add(100)equals WILLCO to rounding off flat windows (on a flat window both stochastics read 0), so the net form keeps the charts’ sense, high at the yen lows; with varying open interest the two differ widely. - Evidence: Briese’s chapter 8 reports Wang (2001): a 36-month index on weekly reports for six markets, January 1993 to March 2000, buying when speculator sentiment was above 80 percent and selling when hedger sentiment was below 20 percent, with annualised returns of 18.9%, 43.8%, 31.9% and 24.6% over holding periods of 2, 4, 6 and 8 weeks, in-sample; de Roon, Nijman and Veld (2000) is a correlation study of hedging pressure, not a trading test; Kearns and Manners (2004) find large currency speculators profitable in 60 percent of weeks, gross in Briese’s Table 8.4 (the text says significant after costs). Williams’s gate (ch. 10, p. 121) bought the S&P 500 at each day’s open: with his six-month index below 50 it “lost money overall” over 846 days, and above 50 it made 76,605 dollars on 371 trades. Ruggiero found the classic rules “unimpressive”. Evidence, not a reference.
- Pinned by
TestRecipeT6COTIndex; runnable exampleExampleSeries_Stochastic_cotIndex.
Trend-gated COT setups (Williams)
Larry Williams (Trade Stocks & Commodities with the Insiders, Wiley, 2005, ch. 13, pp. 147-163) keeps the trend “really simple”: “as long as a 52-week moving average of price is higher than the prior week’s, we are in an uptrend”, and the opposite for a downtrend, because “COT selling in a downtrend puts us short, while COT buying in an uptrend tells us to become buyers”. His test (Table 13.1) bought when “this week’s 52-week moving average was greater than two weeks ago” and “the six-month COT index was greater than 80 percent”, and his 2011 book (Long-Term Secrets to Short-Term Trading, 2nd ed., Wiley, 2011, ch. 15, pp. 279-282) takes “sell signals when the index falls below 25 percent” in a downtrend. t6COTTrendGate gives the buy and sell setups of a COT index under that gate.
ma := c.Average(n).SetN(n-1, math.NaN())
up := ma.GT(ma.Shift(k))
down := ma.LT(ma.Shift(k))
buy := up.Mul(coti.GT(hi))
sell := down.Mul(coti.LT(lo))- Inputs: weekly closes
cand a COT indexcoti(t6COTIndex’s at n = 26, the six-month index), paired by position;n52 weekly closes, a simple average (the type is not printed);k1, the rule’s “prior week’s”, or 2, the test’s “two weeks ago”;hi80 andlo20, strict (“greater than 80 percent”), 75 and 25 in 2011. - First valid index: n - 1 + k (52 at k = 1, 53 at k = 2). Both setups read 0 before it: the masked average compares false, as does its shifted value on bars n - 1 to n - 2 + k, and so does a NaN index. Equal averages are neither up nor down.
- Anchors:
up.Mul(down)is 0 on every bar. On a constructed nine-report tape (n = 3, k = 1) with the index at exactly 80 on one report and exactly 20 on another, and the average flat on two others, neither tie and neither flat report is a setup: the only one is a sell. - Traps: the dossier’s unmasked gate
c.Average(52).GT(c.Average(52).Shift(2))fires on bars 0 to k - 1, whereShiftpads +0 and any positive average is “up”, and on bars 51 to 50 + k, where a full average meets pass-through closes (a false uptrend on falling prices); masked, it fires on none of them. Briese’s trend rules (“Downtrends are the time to pay attention to COT sell signals”) print no trend measure, so this gate is the only computable one. Table 13.1 uses k = 2 where the rule’s text uses 1. - Evidence: Williams’s Table 13.1, “Buying in an Uptrend” (k = 2, the index above 80), prints profit, trades and wins: British pound 51,311 dollars, 21, 71%; bonds 51,736 dollars, 26, 69%; Swiss franc 19,093 dollars, 16, 56%; S&P 500 82,683 dollars, 24, 54%; gold 13,285 dollars, 13, 69%. It is in-sample and gross, with no period, exit or costs printed and no ungated comparison. Evidence, not a reference.
- Pinned by
TestRecipeT6COTTrendGate.
Braswell’s weighted extreme index
Jason Braswell (“Commitment Of Traders Report: Demystified”, Stocks & Commodities V23:8, August 2005, pp. 22-23 and 28-31) ranks each group’s net position with Excel’s PERCENTRANK, subtracts 0.5 and takes “the absolute value so that oscillator extremes mirror position extremes”, then weights each group by its share of the open interest, “taking the longs plus the shorts of each group and dividing that sum by twice the total open interest”, over the non-spreaders’ total. The index is “nondirectional; that is, readings near 0.5 tell you that the trend, whatever it is, is probably ending”, and for the non-commercials’ rank he prefers “a simple moving average taken over seven periods and watching for crossovers in the extreme ranges”. t6WeightedExtreme gives the index over a rolling n-report rank, the non-commercials’ rank and its m-report average.
rc := cl.Sub(cs).RankPosition(n).SetN(n-1, math.NaN())
rf := fl.Sub(fs).RankPosition(n).SetN(n-1, math.NaN())
rs := sl.Sub(ss).RankPosition(n).SetN(n-1, math.NaN())
wc := cl.Add(cs).Div(oi.Mul(2))
wf := fl.Add(fs).Div(oi.Mul(2))
ws := sl.Add(ss).Div(oi.Mul(2))
wei := rc.Sub(0.5).Abs().Mul(wc).Add(rf.Sub(0.5).Abs().Mul(wf)).Add(rs.Sub(0.5).Abs().Mul(ws)).Div(wc.Add(wf).Add(ws))
sig := rf.Average(m).SetN(n+m-2, math.NaN())- Inputs: the commercial (
cl,cs), non-commercial (fl,fs) and non-reportable (sl,ss) longs and shorts and the open interestoi, integer contracts on weekly reports;n156 reports, a rolling window where Braswell ranks against “the entire history of the contract” (see the missing piece);m7. The spreaders hold the rest of the open interest and are left out of the weights. The crossoversrf.CrossesAboveS(sig)andrf.CrossesBelowS(sig)are his; his “extreme ranges” are not printed. - First valid index: n - 1 for
weiandrf, n + m - 2 forsig, NaN before.RankPositionreads +0 in its warm-up, an extremeness of 0.5, the maximum, so the ranks are masked;sigis masked itself becauseAverage(m)passes its input through on bars 0 to m - 2, which would leak finite ranks when n is below m. - Anchors: Braswell’s example, the non-commercials at the 10th percentile, the commercials at the 75th and the small traders at the 90th (j = 2, 15 and 18 of a 21-report window, exactly 0.1, 0.75 and 0.9), with 10%, 50% and 15% of the open interest, gives exactly 0.3 (
float64(0.3)).weilies in [0, 0.5]. The open interest cancels: with each group’s longs plus shorts,cl.Add(cs)and so on, in place of its share the index is the same to rounding. The expanding rank of the missing piece isfl.Sub(fs).RankPosition(i + 1)at bar i, row by row. - Traps: Braswell’s printed scores, 0.40, 0.35 and 0.40, give his 0.367, but |0.75 - 0.5| is 0.25, not 0.35: his ranks give 0.300, and 0.367 needs the commercials at the 85th percentile (j = 17 of 21 reproduces 0.3666… to rounding). Braswell ranks the net; the dossier’s
coti.RankPosition(n)ranks the COT index, which is not a strictly increasing map of the net across time, and the two ranks differ on most bars of a probe. The dossier’sRatioin place ofDivturns the masked warm-up NaN into 0, so the index reads 0 in its warm-up. His printed Excel formula has one argument and ranges over its own column (a circular reference), and Excel truncates PERCENTRANK to three digits (0.666 for 6/9) whereRankPositionkeeps every digit; ties take the lowest rank here, as in LibreOffice (Excel’s tie rule is undocumented). His “as of the preceding Wednesday” is an error: the CFTC tabulates Tuesday’s positions. - Missing piece: Braswell’s rank is expanding, each report against every report so far, which no fixed n gives;
fl.Sub(fs).RankPosition(i + 1)at bar i is one call per bar. It is the expanding self-rankRankPositionWhere(cond, k)with k = 0 recorded on the MACD page under “Volatility-regime attenuation (Carver)”; the rolling rank is Braswell’s own alternative, “the largest positions held over the last few years or so”. - Evidence: none: two charts, Figure 3’s expanding rank of the natural-gas non-commercial net (February 1991 to February 2005) and Figure 4’s index for corn (May 1993 to September 2004), peaking near 0.5, with no thresholds, statistics or costs. Evidence, not a reference.
- Pinned by
TestRecipeT6WeightedExtreme; runnable exampleExampleSeries_RankPosition_weightedExtreme.
Williams %R
WilliamsR
func (s Series) WilliamsR(h, l, c Series, n int) Series
WilliamsR calculates the Williams %R values over an n-period term. The output is on a -100 to 0 scale, -100*(HH - c)/(HH - LL) over the last n bars. The warm-up reads 0, not NaN: bars 0 to n-2 are exactly +0, the first valid index is n-1 and the minimum length is n, and n > len(s) gives all 0. On this scale 0 is the overbought extreme, the close at the highest high, so an overbought rule such as > -20 fires on every warm-up bar; trim (Window(-(n-1))) or mask (SetN(n-1, math.NaN())) the warm-up first. A flat window (HH == LL) gives -0, which compares equal to 0. This was reviewed in GLE-254 (3 October 2026): documentation only, no code change. Mismatched input lengths return a zero-filled series of the receiver’s length.
Percent R
PercentR
func (s Series) PercentR(h, l, c Series, n int) Series
PercentR returns series whereby each value represents the percentage in the range (of the last n bars) of the closing price. It is effectively the Williams %R calculation but with a return value range of 0 - 100. Mismatched input lengths return a zero-filled series of the receiver’s length.
The output is on a 0 to 100 scale, 100 - 100*(HH - c)/(HH - LL) over the last n bars; the receiver is used only for its length. Bars 0 to n-2 are exactly 50 (warm-up), not 0: the first valid index is n-1 and the minimum length is n, and n > len(s) gives all 50. 50 is mid-range; trim the bars before n-1 (Window(-(n-1))) before a moving average, because a NaN mask would poison a recursive average, and trim or mask them (SetN(n-1, math.NaN())) before a crossing rule. A flat window (HH == LL) gives 100, so the warm-up and flat values differ. This warm-up was reviewed in GLE-254 (3 October 2026) and kept: 0 would be the oversold extreme.
Ultimate Oscillator
WilliamsUltimateOscillator
func (s Series) WilliamsUltimateOscillator(h, l, c Series, short, mid, long int) Series
WilliamsUltimateOscillator is Larry Williams’s Ultimate Oscillator (1985): TA-Lib’s ULTOSC, Tulip’s ultosc, pandas-ta’s uo and TradingView’s Ultimate Oscillator. The name says whose oscillator it is: UltimateOscillator is Ehlers’s 2025 band-pass filter, a different indicator.
For each bar t >= 1, with the prior close cp = c[t-1], the buying pressure is bp = c[t] - min(l[t], cp) and the true range is tr = max(h[t], cp) - min(l[t], cp), the same bits as TrueRange(h, l, c) at that bar. For each of the three periods the ratio a = Sum(bp) / Sum(tr) is taken over the last short, mid and long bars, and the result is (4a_short + 2a_mid + a_long) * 100 / 7, on a 0 to 100 scale. The usual periods are 7, 14 and 28.
The receiver is unused, as in TrueRange: the data come from h, l and c, which must have equal lengths, otherwise the result is empty. The periods must satisfy 1 <= short <= mid <= long and long < len(c); the weights 4, 2 and 1 go with short, mid and long by position, and any other triple gives zeros. TA-Lib sorts its three periods, so its ULTOSC(28, 14, 7) equals its ULTOSC(7, 14, 28); here a descending triple returns zeros.
The warm-up reads 0, not NaN: bars 0 to long-1 are 0 whatever the data and the first valid index is long, since bar 0 has no prior close and the first window of long bars with one ends at bar long. Trim or mask the warm-up before use, since 0 is also a legitimate reading.
A window whose true ranges are all 0 contributes 0 to its ratio, so a flat market reads towards 0, not 50 (Tulip, TTR, TradingView and pandas-ta’s own uo give NaN there; with TA-Lib installed pandas-ta calls ULTOSC, which gives 0). The value lies in [0, 100] when every close lies within its bar’s range; closes outside the range, and highs below lows, are computed as they are.
NaN footprint: bar t >= 1 is bad when h[t], l[t], c[t] or c[t-1] is NaN or infinite, and a valid bar is NaN when any bar of its long window is bad or when one of the six window sums overflows. So a non-finite h[p] or l[p] with p >= 1 gives NaN on bars max(p, long) to min(p+long-1, len(c)-1), and a non-finite c[p] on bars max(p, long) to min(p+long, len(c)-1), as it is also bar p+1’s prior close; h[0] and l[0] are not read. The result recovers once the bad bar leaves the long window. A ratio that overflows from finite sums is passed through.
Recipe: on finite data, from bar long, with
tr := c.TrueRange(h, l, c).SetN(1, 0)
bp := c.Sub(l).Add(l.Sub(c.Shift(1)).Max(0.0)).SetN(1, 0)
the result equals
bp.Sum(short).Ratio(tr.Sum(short)).Mul(4.0).
Add(bp.Sum(mid).Ratio(tr.Sum(mid)).Mul(2.0)).
Add(bp.Sum(long).Ratio(tr.Sum(long))).
Mul(100.0 / 7)
bit for bit, apart from the sign of a zero and barring overflow of the window sums, when each low lies within a factor of two of its close and of the prior close, so that the two buying-pressure forms agree. The cost is O(1) amortised per bar.
Casey Percent C
CaseyPercentC
func (s Series) CaseyPercentC(lookback int, smoothLen int) Series
CaseyPercentC implements Ali Casey’s C% oscillator.
The output is on a 0 to 100 scale. Bar 0’s return is taken as 0, so bars 0 to lookback-1 are 0 and the first valid index is lookback, the first bar whose lookback window holds only real returns. From there the smoother runs as if the series started at bar lookback, so its own pass-through bars follow; the simple average’s first full window ends at bar lookback+smoothLen-1. 0 also comes from a flat window. Releases before this change kept bar 0’s zero return in the windows and used expanding windows before bar lookback, so early values were often 0 or 100.
CaseyPercentCSuperSmoother
func (s Series) CaseyPercentCSuperSmoother(lookback int, smoothLen int) Series
CaseyPercentCSuperSmoother implements Ali Casey’s C% oscillator but with Ehlers’ Super Smoother rather than SMA.
The warm-up is CaseyPercentC’s: bars 0 to lookback-1 are 0 and the smoother starts at bar lookback, passing its own seed bars through.
CaseyPercentCUltimateSmoother
func (s Series) CaseyPercentCUltimateSmoother(lookback int, smoothLen int) Series
CaseyPercentCUltimateSmoother implements Ali Casey’s C% oscillator but with Ehlers’ Ultimate Smoother rather than SMA.
The warm-up is CaseyPercentC’s: bars 0 to lookback-1 are 0 and the smoother starts at bar lookback, passing its own seed bars through.
CaseyPercentCXAverage
func (s Series) CaseyPercentCXAverage(lookback int, smoothLen int) Series
CaseyPercentCXAverage implements Ali Casey’s C% oscillator but with an EMA rather than SMA.
The warm-up is CaseyPercentC’s: bars 0 to lookback-1 are 0 and the smoother starts at bar lookback, passing its own seed bars through.
Commodity Channel Index
CCI
func (s Series) CCI(h, l, c Series, n int) Series
CCI computes the Commodity Channel Index over a period of n bars. Developed by Donald Lambert, CCI measures how far the typical price deviates from its average, scaled by the mean absolute deviation. Values above +100 suggest overbought conditions; below -100 suggest oversold. A window whose typical prices are all equal and finite gives exactly 0, rather than ±66.67 from rounding in the mean. Lambert’s denominator is 0.015 times the mean absolute deviation of the typical price tp = (h+l+c)/3, which is tp.MeanAbsDeviation(n). CCI runs its own loop on h+l+c, where the factor 3 cancels in the ratio, so CCI equals (tp - mean)/(0.015 * tp.MeanAbsDeviation(n)) to rounding, not bit for bit.
A window of finite prices gives a finite value. Where that loop’s sums would overflow on such a window (prices near 1e308, or beyond about 6e307/n in magnitude, where the window sum overflows), or where its denominator, 0.015 times the mean deviation, underflows to 0 on subnormal prices, CCI recomputes the window from prices scaled by powers of two, which leave the ratio unchanged: Series{1e308, 0} as h, l and c gives about -66.67 at n = 2, Series{0, 1e-323} as h, with l and c 0, gives about 66.67, and a flat window gives 0. Only those windows changed; releases before 7 October 2026 gave NaN, an infinity or a zero there. A subnormal window whose mean deviation itself rounds to 0 still gives 0. A window holding a NaN or an infinity gives NaN.
The warm-up reads 0, not NaN: bars 0 to n-2 are 0, the neutral centre of the oscillator; the first valid index is n-1 and the minimum length is n.
If h, l and c differ in length the result is an empty Series. Otherwise it has their length, and every bar is 0 when n < 1 or n > len(h): a history shorter than the window gives zeros rather than an empty Series, so CCI over a prefix of the bars is that prefix of CCI over all of them. Releases before this change returned an empty Series when n < 1 or n > len(h).
Pruitt’s smoothed-CCI dwell and its overbought/oversold control are listed under “Compositions” in the “Commodity Channel Index” category.
Compositions
Pruitt’s smoothed-CCI rules read Lambert’s CCI, the typical price’s distance from its average over 0.015 times its mean absolute deviation. The smoothed value is NaN before its first valid index, where CCI would read its warm-up zeros; the flags read 0 there and are decided at bar t’s close. The averages are simple, so no seed is chosen. The stochastic POP under “Stochastic” is the same dwell idea, and the brackets are the trade-state recipe “Entry-anchored bracket on the close (Pruitt)”. Prices are positive and finite and lengths are in bars.
Smoothed-CCI dwell and its overbought/oversold control (Pruitt)
George Pruitt (The Ultimate Algorithmic Trading System Toolbox, Wiley, 2016, Box 2.6, “Donald Lambert’s CCI Trend-Following System”, pp. 52-54) buys “If the lowest myCCIVal for the past 3 days > 100”, where myCCIVal is the “9-day average of the 20-day CCI”: “Trend detection occurs when the CCI stays in an extreme territory for several bars.” His Box 2.5 (pp. 48-50) reads the same value as an overbought/oversold control, buying when it “crosses above -100” and selling short when it “crosses below + 100”. t6CCIDwell returns the smoothed CCI, the two dwell flags and the two control crosses.
x := c.CCI(h, l, c, n).SetN(n-1, math.NaN()).Average(m)
buy := x.Lowest(d).GT(100)
short := x.Highest(d).LT(-100)
longOB := x.CrossesAboveS(-100)
shortOB := x.CrossesBelowS(100)- Inputs:
h,landc, the CCI lengthn, the smoothingmand the dwelld(20, 9 and 3);xis in CCI units and the flags are 0 or 1.CCI’s receiver is not read. - First valid index:
n + m - 2(27) forx,n + m + d - 3(29) for the dwell flags, sinceLowestandHighestcarry the NaN, andn + m - 1(28) for the crosses. A missing price on bar j makesxNaN on bars j toj + n + m - 2; the windows then recover. - Anchors: Barbara Star’s sidebar “Calculating an 11-period CCI” (“The Commodity Channel Index”, Stocks & Commodities V10:2, February 1992) prints 18 CCI cells, which
c.CCI(h, l, c, 11)gives at two decimals with Lambert’s mean deviation, where the standard deviation of Pruitt’s prose (“Calculate the one standard deviation”) misses all 18; on her barsxwith m = 9 is valid from bar 18 and its dwell holds on bars 24 to 27. The dwellx.Lowest(d).GT(100)isx.GT(100).Sum(d).EQ(d), the CCI that “stays above 100 for three days straight”, and the short dwell mirrors it. The stochastic POP under “Stochastic” reads the same dwell on the slow stochastic. - Traps: unmasked,
CCI’s warm-up zeros enter the average: a series flat at 100 that then rises 8% a bar fires the unmasked dwell at bar 26, three bars before the first valid bar 29. Pruitt defines his crosses with a strict prior (“CCI[1] < -100 and CCI[0] > -100”), whereCrossesAboveStakes<=and so fires from exactly -100. The levels are strict. - Variant: the dwell on the raw CCI,
c.CCI(h, l, c, n).SetN(n-1, math.NaN()).Lowest(d).GT(100), valid fromn + d - 2. - Missing piece: the systems’ exits, 5 and 3 ATR for the dwell and 3 and 1 for the control, are the trade-state recipe “Entry-anchored bracket on the close (Pruitt)” on the base
c.PositionSigned(buy, nil, short, nil); the dwell is a level, and TradeStation’s level entries re-enter after a cut inside a base trade, which is tier B (algolang). - Evidence: Pruitt’s Table 2.6 (the dwell, 35 futures, no costs stated) sums to 346,559.40 with 20 markets positive, 13 negative and 2 untraded, and his Table 2.5 (the control) to a loss of 532,387 with 7 positive: the same smoothed CCI loses as a reversal signal and gains as a dwell, with different brackets, in sample. Evidence, not a reference.
- Pinned by
TestRecipeT6CCIDwell; runnable exampleExampleSeries_CCI_dwell.
MACD
MACD
func (s Series) MACD(fastLen, slowLen int) Series
MACD returns the Moving Average Convergence/ Divergence of a fast and slow EMA.
Bars 0 to fastLen-2 are exactly 0, because both averages pass the input through, so their difference is 0; bars fastLen-1 to slowLen-2 are XAverage(fastLen) - s, not a true MACD; the first full value is at slowLen-1.
MACD(n, 4n), for n >= 1, is Robert Carver’s EWMAC crossover (Advanced Futures Trading Strategies, 2023), bit for bit XAverage(n).Sub(XAverage(4n)), since the method is that subtraction. His forecasts divide it by an exponentially weighted standard deviation of the daily price changes (blended with a slow estimate in Appendix B; the recipes use the unblended one), scale it and cap it at 20; those forecast recipes are listed under “Compositions” in the “MACD” category. The signal line and histogram recipes, Aspray’s MACD-Momentum, Pruitt’s histogram pivot and the Elder-ray’s Triple Screen are listed under “Compositions” in the “MACD” category.
Compositions
Carver’s forecasts below are risk-adjusted rule values in forecast units: a raw value times a forecast scalar, so that its mean absolute value is about 10, capped at +/-20. The scalars are Carver’s pooled, backward-looking estimates over his own portfolio and are parameters here (Tables 29, 91 and 100 of Advanced Futures Trading Strategies, Harriman House, 2023; 33.3, 37.2 and 39.2 for skew). Price-unit risk here is the unblended exponentially weighted standard deviation of daily price changes, c.Delta(1).Window(-1).XStdDev(span), which works on negative back-adjusted prices; the Window(-1) keeps Delta’s bar-0 zero out of the seed. It departs from Appendix B, which says “Use the exponentially weighted standard deviation formula with a span of 32 days. Blend this with a slower estimate of standard deviation. Do not annualise”, and Strategy seven prints a second definition, sigma_p = Price x sigma% / 16, beside the raw forecast. The span is 35 in the examples: p. 210, beside Table 29, says the calculations used a 35-day estimate blended with a very long average; 32 is Appendix B’s. pysystemtrade also floors its risk (a rolling 5% quantile and an absolute minimum), whereas the risk here has no floor. The blend (“Blended volatility forecast (Carver)”) and p x sigma% / 16 differ from the unblended risk by up to about 25% and 15% on random walks. The trend forecasts divide by the same bar’s risk (the book’s sigma_p,t), the mean reversion by the previous bar’s. Averages are the library’s XAverage, seeded with a simple average after the upstream warm-up is trimmed; pysystemtrade’s pandas adjust=True seeds differ for about three slow spans, and its bias=False deviation is larger by sqrt(span / (span - 1)) for good (1.46% at span 35). Every value is NaN before its first valid index; closes are back-adjusted daily prices and lengths are in bars. Grinold and Kahn state the same idea in units of return: a forecast is their IC x z up to scale, and alpha = IC x omega x z converts it to a return in omega’s units and horizon (Advances in Active Portfolio Management, McGraw-Hill, 2019, ch. 3, eq. (20); its Exhibits 7 and 8 follow from it exactly, evidence, not a reference).
EWMAC trend forecast and its acceleration (Carver)
Robert Carver (Advanced Futures Trading Strategies, Strategies seven to nine and twenty-three, and Appendix B) divides the crossover of two exponential averages by the risk “in units of daily price”: “Raw forecast = (Fast EWMA - Slow EWMA) / sigma_p”, scales it by a pooled scalar and caps it, since “the maximum absolute forecast value you should use is 20”; EWMACn is the pair (n, 4n). Strategy twenty-three’s acceleration is the change of the scaled forecast over n bars, “Raw acceleration_N,t = Forecast_N,t - Forecast_N,t-N”, times its own scalar, capped like the forecast. t6EWMAC takes both lengths and the span as parameters.
sig := c.Delta(1).Window(-1).XStdDev(span)
raw := c.MACD(fast, slow).Div(sig).SetN(max(slow-1, span), math.NaN())
scaled := raw.Mul(scalar)
f := scaled.Clamp(-20, 20)
acc := scaled.Delta(fast).Mul(accScalar).Clamp(-20, 20).SetN(max(slow-1, span)+fast, math.NaN())- Inputs: closes
c, back-adjusted (no step divides by the price, so negative prices are fine);fast<slow, Carver’s N and 4N (any ratio from 2 to 6 does as well, his footnote 94);span(35);scalarfrom Table 29 (12.1, 8.53, 5.95, 4.10, 2.79 and 1.91 for EWMAC2 to EWMAC64) andaccScalarfrom Table 100 (1.87, 1.90, 1.98 and 2.05 for Acceleration8 to Acceleration64). - First valid index: max(slow - 1, span) for
rawandf(63 for 16/64 at span 35; the span binds for EWMAC2), that plusfastforacc(79); NaN before. Unmasked, the quotient is 0 on bars 0 to span - 1 (the risk’s warm-up) andXAverage(fast)less the close, over the risk, on bars span to slow - 2, which is not a trend. - Anchors:
c.MACD(n, 4*n)isc.XAverage(n).Sub(c.XAverage(4*n))bit for bit; the forecast ofc.Mul(4)is the forecast ofcand that ofc.Mul(-1)its negation, bit for bit (a power of two and a sign commute with every rounding);accisscaled.Sub(scaled.Shift(fast))times the scalar, capped. - Traps: an exact ramp has equal price changes, so the risk is exactly 0 and every forecast reads 0 (
Div’s zero rule) however strong the trend, while one bar raised by 1/64 sends it to the cap: do not test on exact ramps. The dossier’sc.Delta(1).XStdDev(35)keeps the bar-0 zero in the seed (about 2% off at bar 35 on the test path, 0.1% by bar 100), and Strategy six’s filter asGT(0).Mul(2).Sub(1)reads -1 on ties and on the warm-up. The book’s p. 187 gives the scalar 1.9 to EWMAC(16,64) and prints EWMA(64) less EWMA(16): 1.9 is EWMAC(64,256)’s (Table 29: 1.91) and EWMAC16’s is 4.10. pysystemtrade’sacceldifferences the raw forecast and scales the difference once, the same rule with the two scalars multiplied to 1%; footnote 222’s “square root of two” pattern fits those scalars, not Table 100’s. - Variant: Strategy six’s direction
m.GT(0).Sub(m.LT(0))onm := c.MACD(64, 256).SetN(255, math.NaN()), 0 on a tie (“we’d hold no position”, footnote 104) and on the masked bars. - Variant: Leveraged Trading (Harriman House, 2019, ch. 10) divides a simple-average crossover by the annual risk in price units,
c.Average(16).Sub(c.Average(64)).Div(c.Mul(r.StdDevSample(25).Mul(16)))withr := c.Delta(1).Div(c.Shift(1)), times Table 65’s 57.12, capped and masked to bar 63: the continuous form of the Starter System’s base, whose stop is the frozen-gap trailing stop on the trade-state page. - Evidence: Tables 30 and 31 (Carver’s Jumbo portfolio of about 100 futures, after costs): Sharpe ratios from 0.15 for EWMAC2 to 1.06 for EWMAC16, turnover from 381 to 27.5 a year; Table 102: 0.20 to 0.59 for the four accelerations. Evidence, not a reference.
- Pinned by
TestRecipeT6EWMAC; runnable exampleExampleSeries_MACD_ewmac.
Normalised price and normalised trend (Carver)
Carver (Strategy seventeen) asks “What if we applied a volatility normalisation to the price itself?”: “P^N_t = (100 x [p_t - p_t-1] / sigma_p,t) + P^N_t-1, with the price P^N_1 set at the point when we can first estimate a standard deviation. I arbitrarily set P^N_0 = 0”. The trend is the EWMAC rule on P^N, divided by “an estimate of the daily standard deviation of price differences” computed from “the normalised price”, with Table 29’s scalars (“EWMAC scalars are a property of the trading rule, not the prices”) and the cap of 20. t6NormalisedTrend divides each change by the same bar’s risk, as the formula prints, and trims the warm-up before the averages and the second risk.
d := c.Delta(1)
sig := d.Window(-1).XStdDev(span)
pn0 := d.Div(sig).Mul(100).CumSum()
pn := pn0.SetN(span-1, math.NaN())
pnT := pn0.Window(-(span - 1))
sgN := pnT.Delta(1).Window(-1).XStdDev(span)
nt := c.Mul(0).Add(pnT.MACD(fast, slow).Div(sgN)).Mul(scalar).Clamp(-20, 20).SetN(span-1+max(slow-1, span), math.NaN())- Inputs: closes;
fast<slow;spanfor both risks;scalarfrom Table 29. - First valid index: span - 1 for
pn, exactly 0 there (P^N_0; P^N_1 is at bar span, the first bar with a risk), and span - 1 + max(slow - 1, span) fornt(97 for 16/64 at span 35); NaN before. Untrimmed, the warm-up zeros ofpn0would enter the averages’ seeds and the second risk’s seed window. - Anchors:
pn’s first increment is 100 d / sigma at bar span bit for bit; its one-bar change equals the increment within one unit in the last place of the larger |PN| (a running total rounds at its own magnitude);inc.SumSince(ev), withincthe increments and one event at bar span - 1, ispnbit for bit;pnandntofc.Mul(4)are those ofcbit for bit. The trend is invariant to the level of PN and to the multiplier 100 in exact arithmetic and to rounding in floating point (about 1e-14 here). - Traps: the second risk is about 100 but not constant (97 on average, 39 to 134 on the test path), so dividing by it is not dividing by 100.
pnis start-dependent, a running sum: use only its changes and the trend on it. The same-bar risk bounds a day’s increment (at most 1 / sqrt(alpha(1 - alpha)) risks from the running mean, 4.37 at span 35), so a shock partly normalises itself. The dossier’sd.Div(d.XStdDev(35))keeps the bar-0 zero in the seed, itsSum(len(c))isCumSum, and its trend smooths the untrimmed series. - Variant: pysystemtrade’s
get_cumulative_daily_vol_normalised_returnslags the risk,d.Div(sig.Shift(1)), so P^N_0 is at bar span and every first valid index moves one bar later; its code has no factor 100, and on a random walk its increments’ realised deviation is about 105 against 99 for the same-bar form. - Evidence: Tables 71 and 72 (Jumbo portfolio, costs included): Sharpe ratios from 0.33 for EWMAC2 to 1.04 for EWMAC32 on the normalised price, costs and turnover about a quarter higher than on prices; Table 74: 1.15 against 1.14 for the trend on prices, a return correlation of 0.97. Evidence, not a reference.
- Pinned by
TestRecipeT6NormalisedTrend.
Breakout forecast (Carver)
Carver (Strategy twenty-one) measures “where the current price is within a recent range”: “Raw forecast = EWMA_span=h/4[40.0 * (p_t - mean_h,t) / (max_h,t - min_h,t)]”, over h closes ending today, the mean the middle of the range. The factor 40 puts the price at the rolling minimum at -20 and at the maximum at +20, and “as the forecast before scaling cannot be larger than 20, and all the forecast scalars are less than one”, the cap “is actually redundant”. t6BreakoutForecast is pysystemtrade’s operation order with Donchian’s channel, smoothed by the library’s XAverage after the channel’s warm-up is trimmed.
raw := c.Sub(c.DonchianMiddle(h)).Div(c.DonchianUpper(h).Sub(c.DonchianLower(h))).Mul(40).SetN(h-1, math.NaN())
f := c.Mul(0).Add(raw.Window(-(h - 1)).XAverage(h/4)).Mul(scalar).SetN(h+h/4-2, math.NaN())- Inputs: closes; the horizon
h>= 4 (10, 20, 40, 80, 160, 320; Go’sh/4is pysystemtrade’s smoothing span for these);scalarfrom Table 91 (0.60, 0.67, 0.70, 0.73, 0.74, 0.74). - First valid index: h - 1 for
raw(HighestandLowestexpand before it) and h + h/4 - 2 forf(23 at h = 20, 98 at 80); NaN before. - Anchors:
rawisc.Sub(c.Highest(h).Add(c.Lowest(h)).Div(2)).Div(c.Highest(h).Sub(c.Lowest(h))).Mul(40)bit for bit, pysystemtrade’s raw value; |raw| is at most 20 to rounding; a flat window givesraw0, which pullsftowards 0. Leveraged Trading’s crude-oil example (ch. 8 and 10: a 320-day maximum of 107.78, minimum 98.76 and close 100.3) gives the scaled price -0.33 at two decimals (raw-13.17), and times Table 66’s 33.5 it is -11.03. - Traps: the dossier’s
c.ChannelPosition(h, 1).Mul(0.4).Sub(20)equalsrawto rounding only (1.1e-14 here) and reads -20, the most bearish value, on bars 0 to h - 2, which an untrimmedXAverageseed averages in (up to 4.5 forecast units early at h = 80). pysystemtrade gives NaN on a flat window, and pandas’ewmskips the bar (there is no forward fill), holding the smoothed value. The book’s -11.1 comes from its rounded chain, -0.33 x 33.5, and its middle line prints “(107.78 - 98.76) / 2 = 103.3” with a minus for the plus; Leveraged Trading’s appendix spreadsheet divides the average of the maximum and the minimum by 2 a second time, where Formula 28 is meant. - Variant: pysystemtrade’s
breakoutstarts on partial windows at bar ceil(h/2) - 1 and smooths with pandas’ adjusted average, which skips flat windows; it composes as the ratio of two first-valueXAverageAlphasums, of the raw values and of a validity flag each prefixed with a 0, and matches pandas to about 5e-14 (prose only). - Variant: Leveraged Trading’s unsmoothed forecast
spr.Mul(factor).Clamp(-20, 20)withspr := c.Sub(c.DonchianMiddle(h)).Div(c.DonchianUpper(h).Sub(c.DonchianLower(h)))and Table 66’s factor (28.6 at 10 to 33.5 from 80); the cap does not bind (0.5 x 33.5 is 16.75). - Evidence: Tables 93 and 94 (Jumbo portfolio, costs included): Sharpe ratios from 0.30 for Breakout10 to 1.10 for Breakout80; Table 98 and its text: a correlation of 0.97 with the multiple trend strategy. Evidence, not a reference.
- Pinned by
TestRecipeT6BreakoutForecast; runnable exampleExampleSeries_DonchianMiddle_breakoutForecast.
Skew forecast (Carver)
Carver (Strategy twenty-four) puts “a minus sign in front of the rolling skew estimate to get a raw forecast”: “Raw skew forecast_w,t = -Skew(r_t, r_t-1, r_t-2, … r_t-w+1)” over percentage returns of the back-adjusted price, with windows of 60, 120 and 240 days, smoothed by an EWMA of span w/4, scaled and capped at 20; “skew does not require any explicit risk adjustment”. “The formula for skew is available in all common spreadsheet packages”: Excel’s SKEW, the sample coefficient that SkewnessSample computes. The return divides by the previous close (Appendix B’s r_t = (p_t - p_t-1) / p*_t-1, footnote 228), and t6SkewForecast takes it as a fraction.
sk := c.Delta(1).Div(c.Shift(1)).SkewnessSample(w).SetN(w, math.NaN())
f := c.Mul(0).Add(sk.Window(-w).XAverage(w/4)).Mul(-scalar).Clamp(-20, 20).SetN(w+w/4-1, math.NaN())- Inputs: positive closes;
w>= 4 (60, 120, 240);scalar(33.3, 37.2, 39.2). - First valid index: w for
sk(the window that ends at bar w - 1 holds bar 0’s return, 0/0 read as 0) and w + w/4 - 1 forf(74, 149, 299); NaN before. - Anchors:
SkewnessSampleisSkewnesstimes sqrt(w(w - 1)) / (w - 2) to rounding (the population form would scale the forecast down by 2.6% at 60, 1.3% at 120 and 0.6% at 240); the skew of fraction returns equals that ofc.RateOfChange(1)to rounding (about 1e-14);skandfofc.Mul(4)are those ofcbit for bit, andXAveragecommutes with the sign bit for bit. - Traps: a flat window gives
sk+0, and a series flat for long enough givesf-0, the sign ofMul(-scalar)(Add(0)restores +0). Carver warns that “skew is certainly sensitive to the inclusion or exclusion of a few outlier returns”: an outlier enters and leaves the window w bars apart, a step in the forecast that the smoothing softens. The cap binds (90 bars of the test path at w = 60). The dossier’sc.RateOfChange(1).SkewnessSample(w)lets bar 0’s zero into the first window and its untrimmedXAverage(w/4)seeds on the warm-up zeros. pysystemtrade’s skew rules are another rule: calendar windows of 365 and 180 days, demeaned by cross-sectional averages, divided by their own volatility, on returns over the day’s price. - Variant: futures with the current contract’s price
cur:adj.Delta(1).Div(cur.Shift(1))as the returns (footnote 228: “divide by the current futures price at the time”). - Variant: pysystemtrade’s day-t denominator
c.Delta(1).Div(c)gives the skew of r / (1 + r), up to 0.4 skew units apart on a random walk: a departure, not an equivalent. - Evidence: Table 108 (Jumbo portfolio, costs included): Sharpe ratios 0.60, 0.75 and 0.71 for the three windows; Table 113: 1.27 for carry and trend, 1.38 with skew. Evidence, not a reference.
- Pinned by
TestRecipeT6SkewForecast; runnable exampleExampleSeries_SkewnessSample_skewForecast.
Scaling, capping and mapping forecasts (Carver)
Carver scales forecasts “so that they have an average absolute value of 10” (“Forecast scalar = 10 / (Average absolute value of raw forecast)”, Strategy seven) with pooled estimates: “there is no harm in using the fixed values”. Strategy twelve maps three speeds with two rules instead of capping: EWMAC2 with a “double V” (2F for |F| below 10, falling to 0 at 20 and 0 beyond), EWMAC4 and EWMAC64 with “scale and cap” at 15 and “The correct multiplier this time is 1.25”. pysystemtrade’s forecast_scalar estimates the scalar as 10 over an expanding mean of the non-zero |raw|, from 500 values, pooled across instruments by default. t6ScaleAndCap gives the fixed scalar’s cap, that self-scaling for one instrument without pooling or backfill, and both maps.
f := raw.Mul(scalar)
capped := f.Clamp(-20, 20)
af := raw.Abs()
nz := af.GT(0)
self := raw.Mul(10).Div(af.AverageWhere(nz, 0)).MaskNonFinite(af.AverageWhere(nz, mp)).Clamp(-20, 20)
dv := f.Clamp(-10, 10).Mul(4).Sub(f.Clamp(-20, 20).Mul(2))
sc := f.Clamp(-15, 15).Mul(1.25)- Inputs:
raw, a raw forecast NaN in its warm-up (t6EWMAC’sraw, or any rule’s);scalar(Tables 29, 91, 100);mp, the minimum count of non-zero values (500). - First valid index: the raw forecast’s for
capped,dvandsc(63 for EWMAC16), and the bar of the mp-th non-zero raw value forself(562 at mp = 500 on the test path); NaN before. An exact zero (a zero risk) is not counted. - Anchors:
dvequals the book’s piecewise rule bit for bit except that -0 maps to +0 (Mul(4)andMul(2)are exact and theSubis the book’s own operation) and gives 0, 0, -10, -20, -10, 0, 10, 20, 10, 0, 0 for F from -25 to 25 in steps of 5;scisf.Mul(1.25).Clamp(-18.75, 18.75)bit for bit; Leveraged Trading’s corn and Euro Stoxx (ch. 10): -20 / (379 x 11.9%) x 57.12 = -25.33, capped to -20, and +20 / (3391 x 9.6%) x 57.12 = +3.509, printed +3.5;selfequals a direct expanding mean to rounding. - Traps: scale and cap’s limit is 18.75, not 15: the dossier’s
f.Mul(1.25).Clamp(-15, 15)differs wherever |F| > 12. pysystemtrade’smap_forecast_valueis a dead zone for small accounts, not the double V.selfis an expanding mean and depends on where the history starts, unlike every other output on this page; pysystemtrade also pools across instruments (a cross-sectional median) and backfills the first estimate, a look-ahead. Carver himself “wouldn’t trade strategy twelve”: its gains are not “justified by the considerable increase in complexity”. - Variant: the rolling stand-in
raw.Mul(10).Div(af.Average(m)).SetN(F+m-1, math.NaN()), with F the raw forecast’s first valid index: start-invariant, and reading +/-10 in its warm-up without the mask. - Variant: Appendix B’s forecast turnover, 256 times the mean of
f.Delta(1).Abs().Div(10)over the backtest with the warm-up masked, a diagnostic. - Evidence: Tables 57 and 58 (Jumbo portfolio, costs included): the double V turns EWMAC2’s Sharpe ratio from 0.15 to -0.22 (turnover 381 to 704); scale and cap moves EWMAC4 from 0.64 to 0.65 and EWMAC64 from 0.96 to 0.95. Evidence, not a reference.
- Pinned by
TestRecipeT6ScaleAndCap; runnable exampleExampleSeries_Clamp_doubleV.
Combining forecasts (Carver)
Carver averages “capped rather than uncapped forecasts” with “forecast weights [that] will sum to 1 and none will be negative” (Strategy nine), multiplies by a forecast diversification multiplier (FDM), because the average’s mean absolute value falls below 10 (“just 7.5”), and caps again at 20. Appendix B: “FDM = 1 / sqrt(w.rho.wT)” over the correlations of forecast values, with “negative correlations […] floored at zero” and the FDM capped at 2. t6CombineForecasts takes the FDM as a constant.
comb := f1.Mul(w1).Add(f2.Mul(w2)).Add(f3.Mul(w3)).Mul(fdm).Clamp(-20, 20)
equal := f1.SAverage(f2, f3).Mul(fdm).Clamp(-20, 20)- Inputs: capped forecasts
f1,f2,f3of one length; weights >= 0 summing to 1;fdm, a constant (Table 36 for the EWMAC speeds: 1.26 for six, 1.19 for five, 1.13 for four, 1.08 for three and 1.03 for two; Table 52 by the number of rules; Tables 84, 96 and 105 for horizons, breakouts and accelerations). - First valid index: the latest of the forecasts’ (255 when EWMAC64 is one of them); NaN before.
- Anchors: Leveraged Trading’s weighted average (ch. 10), 0.4 x 6 + 0.1 x -12 + 0.5 x 10, is 6.2 bit for bit; with an FDM of 1 the final cap does not bind; footnote 141’s zero-correlation FDM for six rules is sqrt(6) = 2.45, against Table 36’s 1.26.
- Traps: Table 36 prints six equal weights as 0.167, which sum to 1.002: use
SAverage(argument order, one division) or 1/N, andSAverageequals threeMul(1.0 / 3)terms to rounding only. pysystemtrade caps the FDM at 2.5 and smooths it over 125 days, where the book says 2 and about 30 days; Leveraged Trading omits it (“simpler and safer”). Table 52’s step from 1.03 for three rules to 1.23 for four is as printed. - Variant: six equal speeds,
f1.SAverage(f2, f3, f4, f5, f6).Mul(1.26).Clamp(-20, 20)(Table 36). - Variant: footnote 137’s telescoping: four equally weighted raw crossovers EWMA x - EWMA 4x to EWMA 8x - EWMA 32x sum to
c.XAverage(x).Add(c.XAverage(2*x)).Sub(c.XAverage(16*x)).Sub(c.XAverage(32*x)), to rounding; per-speed scalars and caps break it. - Evidence: 1 / sqrt(w’rho w) with equal weights on Table 33’s return correlations gives 1.24 for six speeds against Table 36’s 1.26: the formula and the table agree in form, but the table rests on forecast correlations the book does not print. Evidence, not a reference.
- Pinned by
TestRecipeT6CombineForecasts; runnable exampleExampleSeries_SAverage_combineForecasts.
Volatility-regime attenuation (Carver)
Carver (Strategy thirteen) measures “the relative level of volatility”, “V_i,t = sigma%_i,t / mean(sigma%_i,t-2560, … sigma%_i,t)”, takes"the quantile point Q based on historical data for a given instrument" (“Q_i,t = Quantile of V_i,t in distribution(V_i,0 … V_i,t)”) and multiplies the raw forecast by “M_i,t = EWMA_span=10(2 - 1.5 x Q_i,t)”: “if volatility is especially low, with Q = 0.0, then we’d multiply our forecast by two”. The EWMAC scalars are unchanged. t6VolAttenuation ranks V over a rolling window, the causal stand-in for the expanding quantile; its seed line gives a constant of 1 on the bar after a NaN, which restarts the average there with the first value, and 2/11 (span 10) after it.
valid := sig.EQ(sig)
v := sig.Div(sig.AverageVariable(valid.CumSum(), 1, lw))
q := v.RankPosition(m).SetN(m-1, math.NaN())
seed := q.Shift(1).EQ(q.Shift(1)).Mul(-1).Add(1).Max(2.0 / 11)
mult := q.Mul(-1.5).Add(2).XAverageAlphaS(seed)
adj := raw.Mul(mult).MaskNonFinite(mult)- Inputs:
raw(t6EWMAC’s);sig, the percentage risk of “Blended volatility forecast (Carver)” on the realised-volatility page (_, _, sig := t6CarverBlend(c, 32, 2560, 16)), NaN before its first valid index and finite from it, one bar shorter thanc(any length up tolen(raw), right-aligned);lw2,560, ten years of 256 days (the printed window t - 2560 to t holds 2,561; pysystemtrade’s is 2,500);m2,560. - First valid index: sig’s for
v(exactly 1 there, the mean of one value), sig’s plus m - 1 formult(element 2,590 at span 32 and m 2,560, in sig’s frame), and the bar after that, in c’s frame, foradj(2,591); NaN before. Tests need more than about 2,600 bars. - Anchors:
multlies in [0.5, 2] exactly, and after the NaN prefix it isq.Window(-F).Mul(-1.5).Add(2).XAverageAlpha(2.0 / 11)bit for bit, F its first valid index; the constants 2, 0.5 and 1.25 pass through unchanged, and a constant risk gives V exactly 1, Q 0 and M 2; the expanding Q at the last bar isRankPositionover the whole valid V;adjkeeps raw’s sign and zeros, so the attenuation does not flip or zero a forecast. - Traps: the rolling rank compares V with its last m values, not with all history, and needs m - 1 more bars. A NaN inside
sigstays in the mean’s window (to the end while it is expanding), soqandmultstay NaN:sigmust be finite after its warm-up. The dossier’ssig.Average(2560)passes the risk through for ten years (V exactly 1, Q 0, M 2), itsXAverage(10)seeds with a simple average and does not recover from the NaN prefix, andRankPositionreads 0, not NaN, in its warm-up. pysystemtrade’s quantile counts the earlier values below over the bars so far: it does not reach 1, counts the warm-up bars and gives a NaN bar Q = 0; its default risk (mixed_vol_calc) blends with a ten-year exponential average, not the rolling mean of the realised-volatility page’s blend, so V fed with that blend is not pysystemtrade’s V. - Missing piece: an expanding self-rank,
RankPositionWhere(cond, k)(k = 0: every bar so far where cond fires), would give Carver’s Q as a series;RankPositionVariable(p, lo, hi), expanding then rolling likeAverageVariable, is the alternative. - Variant: pysystemtrade’s pandas
ewm(span=10)(adjusted) smoothing isXAverageAlphaSwith the per-bar constant a / (1 - (1 - a)^(t+1)), to about 2e-15. - Variant: per-instrument regime labels
q.GT(0.75)andq.LT(0.25); Carver’s boundaries are pooled over his 102 instruments (“Low vol: V between 0.28 and 0.77”). - Evidence: Table 60 (Jumbo portfolio, costs included): adjusting EWMAC raises Strategy eleven’s Sharpe ratio from 1.27 to 1.39, and adjusting carry lowers it to 1.25. Evidence, not a reference.
- Pinned by
TestRecipeT6VolAttenuation.
Fast mean reversion and the safer variant (Carver)
Carver (Strategy twenty-six) takes “Equilibrium_t = EWMA_span=5 (p_t-1, p_t-2, p_t-3, …)”, where “p_t-1 is the last available daily closing price”, makes the raw forecast “equal to the equilibrium minus the price”, divides it by the daily risk, scales it by 9.3 and caps it. Strategy twenty-seven adds “a forecast overlay: if the forecast from a trend trading rule has a different sign to the mean reversion forecast, then we set the risk adjusted mean reversion forecast to zero” (EWMAC16) and the volatility multiplier, with a scalar of “around 20”. t6FastMeanReversion is the daily-bar form: every daily quantity comes from the previous close, as in the book’s overnight calculation (footnotes 247 and 249-250), and only the price is current.
e := c.XAverage(5).Shift(1)
sigP := c.Delta(1).Window(-1).XStdDev(span).Shift(1)
raw := e.Sub(c).Div(sigP).SetN(span+1, math.NaN())
f := raw.Mul(scalar).Clamp(-20, 20)
tr := c.MACD(fast, slow).Shift(1).SetN(slow, math.NaN())
gate := raw.GT(0).Mul(tr.GT(0)).Add(raw.LT(0).Mul(tr.LT(0)))
safe := raw.Mul(gate).Mul(mult).Mul(safeScalar).Clamp(-20, 20).MaskNonFinite(mult, tr)- Inputs: closes;
multalready lagged one bar (mult.Shift(1)of “Volatility-regime attenuation (Carver)”, or all ones), right-aligned withc;span(35);fastandslow(16, 64); the scalars 9.3 and 20. - First valid index: span + 1 for
f(36), and the latest of span + 1, slow and mult’s first valid bar forsafe(64 without attenuation, 232 with the lagged m = 200 multiplier, about ten years with Carver’s); NaN before.MaskNonFinite(mult, tr)masks tr’s NaN too: without it the closed gate reads 0 on bars span + 1 to slow - 1. - Anchors: Carver’s US 10-year bond example (equilibrium 132.5, price 133): five closes of 132.5 and one of 133 give the raw forecast -0.5 exactly; over 133 x 6% / 16 = 0.49875 (printed 0.4987) it is -1.0025 and times 9.3 -9.32, and at 134.5 the scaled -37.29 caps at -20. The gate equals the dossier’s
raw.Mul(raw.Mul(tr).GT(0))except on underflow (1e-200 against 1e-200); Strategy twenty-seven’s printed equilibrium with p_t gives two thirds of this forecast, to rounding. - Traps: a test must form the risk from variables: Go folds the constant expression 133 * 0.06 / 16 to 0.49875 exactly, while binary64 at run time gives 0.49874999999999997. A closed gate on a negative raw gives -0 (
Add(0)restores +0). Strategy twenty-seven’s p_t, printed twice, is read as a misprint for p_t-1: on hourly bars it would move the equilibrium every hour. The text gives a correlation of -0.28 with trend where Table 135 prints -0.24. - Variant: the book’s derived risk,
c.Mul(blend).Div(16).Shift(1)assigPwith_, _, blend := t6CarverBlend(c, 32, 2560, 16), the previous close times the previous bar’s sigma% over 16. - Missing piece: the published rule compares the current hourly price with the previous day’s equilibrium and risk and trades through limit orders; an exponential variance that updates only at day boundaries on hourly bars (a per-bar-alpha
XVarianceAlphaS) is not built, and the hourly strategy belongs in algolang with the daily accessors of the timeframe page. - Evidence: Table 131 (Jumbo portfolio since 2013, hourly data, commissions only, with no spread cost on the market orders): a Sharpe ratio of 0.80; Table 134: 2.14 for the safer variant, of which Carver writes “I do not trust a backtested Sharpe ratio over two”, on just under ten years of data with footnote 252’s fill assumption (a limit filled when the next hourly price passes it). Evidence, not a reference.
- Pinned by
TestRecipeT6FastMeanReversion; runnable exampleExampleSeries_XAverage_fastMeanReversion.
The MACD histogram is the MACD less its signal line, and the signal here is the house XAverage of the MACD’s valid part, from its first full bar s - 1: TA-Lib’s signal seed, since the sources are TA-Lib-like or silent on it. Elder’s average of the close is the house XAverage too; Blau’s first-value seeds (under “Momentum”) apply where an author’s own table starts an average at its first input. Values are NaN before their first valid index; flags read 0 there and are decided at bar t’s close, so flag.Shift(1) gives a fill at the next open. Carver’s forecasts above divide MACD by a risk, Schaff’s trend cycle reads it under “Stochastic”, Apirine’s weekly MACD on daily bars is under “Relative: Timeframes”, and the bracket exits of Pruitt’s systems are the trade-state recipe “Entry-anchored bracket on the close (Pruitt)”. Prices are positive and finite and lengths are in bars.
MACD histogram and MACD-Momentum (Aspray; Star)
Thomas Aspray (“Individual stocks and MACD”, Stocks & Commodities V7:2, February 1989, pp. 56-61) calculates “MACD-H using 10-, 20- and 10-periods”, and “My new MACD-Mo is a 3-period smoothed (simple moving average) of a 10-period momentum of the MACD-H”. Barbara Star (“The MACD Momentum Oscillator”, Stocks & Commodities V12:2, February 1994, pp. 81-85) restates it with the histogram as the MACD less its trigger line and reads the pair so: “Actual buy and sell signals are given when the histogram crosses above or below its zero line in the direction suggested by the momentum oscillator.” t6MACDHistogram returns the signal, the histogram, MACD-Mo and Star’s crosses, with MACD-Mo’s sign as the direction.
m := c.MACD(f, s)
sig := c.Mul(0).Add(m[s-1:].XAverage(g)).SetN(s+g-2, math.NaN())
h := m.Sub(sig)
mo := h.Momentum(k).Average(q).SetN(s+g+k+q-3, math.NaN())
buy := h.CrossesAboveS(0).Mul(mo.GT(0))
sell := h.CrossesBelowS(0).Mul(mo.LT(0))- Inputs: the close
candf,s,g,kandq(Aspray’s 10, 20, 10, 10 and 3; Appel’s 12, 26 and 9);sig,handmoare in price units andbuyandsellare 0 or 1.XAveragefuses multiply-adds on arm64, so the last bits ofsig,handmodepend on the platform. - First valid index:
s + g - 2forsigandh(28; 33 for 12, 26 and 9) ands + g + k + q - 3formo(40), where the flags can first fire.Momentum(k)writes +0 overh’s NaN on bars 0 to k - 1, whichAveragewould carry: hencemo’s own mask. Minimum length s - 1: a shorter series panics, and every value is NaN below s + g - 1 bars. - Anchors:
h.CrossesAboveS(0)equalsm.CrossesAboveS(sig)bit for bit on finite values, since x - y rounds to 0 only when x = y; the sliced signal is TA-Lib’s, the mean of the first g full MACD values then the average, within 1e-12 of a loop translation; scaling the closes by a power of two scales every output bit for bit. Aspray’s Figure 1 (72 closes, July to October 1988): his column 1 isc.XAverageAlpha(0.182)on all 72 rows, a first-value average at his rounded alpha, and his printed histogram throughMomentum(10)andAverage(3)gives his columns 6 and 7 within 0.0105 on every printed row, first at bars 39 and 41. - Traps: the house MACD does not reproduce Aspray’s table: his 20-day column is two formulas (an average of column 1, then a 20-day simple average of the close), his signal column is reproduced by an alpha of 0.186, not the 0.182 its header implies, and his histogram after its first row cannot be recomputed;
c.MACD(10, 20)reads -1.5949 at bar 19 where the table prints -2.68. The whole-seriesm.XAverage(g)seeds on the MACD’s warm-up zeros and pass-through bars: on the test fixture it is 0.37 off at the first valid bar and beyond 1e-12 until bar 160. Aspray’s equation and table header print the signal less the MACD, the reverse of his text, Star, MetaStock and CompuTrac. Aspray and Star take daily signals in the weekly indicators’ direction, which needs weekly bars (see the Elder-ray below). The last bits ofsig,handmodepend on the platform (XAveragefuses multiply-adds on arm64), so a zero cross on a bar within an ulp of 0 can differ. - Variant: Star’s reading by MACD-Mo’s slope,
h.CrossesAboveS(0).Mul(mo.Delta(1).GT(0)), or with Aspray’s confirmation “in two to five days” (“MACD momentum Part 2”, Stocks & Commodities V6:9, September 1988),h.CrossesAboveS(0).Mul(mo.CrossesAboveS(0).Shift(2).Sum(4).GT(0)). - Variant: first-value averages throughout, as Aspray’s table and Pine’s
ta.emastart them:mf := c.XAverageAlpha(2/(float64(f)+1)).Sub(c.XAverageAlpha(2/(float64(s)+1)))and the signalmf.XAverageAlpha(2/(float64(g)+1)), a different number for hundreds of bars. - Evidence: Aspray’s and Star’s figures are charts, and Aspray’s silver test of 10, 20 and 9 (“profitable 54% of the time”, “MACD momentum Part 1”, V6:8, August 1988) is in sample. Luo’s test of the 12, 26 and 9 crossover (“MACD-Suitable Stocks”, Stocks & Commodities V33:3, March 2015: 1,816 stocks, 2005 to 2014, 1.45% a year, no costs stated) is a test of the histogram’s zero cross by the identity above. Evidence, not a reference.
- Pinned by
TestRecipeT6MACDHistogram; runnable exampleExampleSeries_MACD_histogram.
Histogram pivot (Pruitt)
George Pruitt (The Ultimate Algorithmic Trading System Toolbox, Wiley, 2016, Box 2.7, “Gerald Appel’s MACD Histogram System”, pp. 55-57) trades a two-bar pivot of the 12, 26 and 9 histogram: “waiting for the histogram to form a two-bar high pivot (two bars to the left and to the right are shorter than the center bar) and then entering a short position”, and the mirror low for a long, with the outer bars beyond zero: “If leftBar2 < 0 and rightBar2 < 0 and centerBar < leftBar1 and centerBar < leftBar2 and centerBar < rightBar1 and centerBar < rightBar2, then Buy MOC”. t6HistogramPivot reads the confirmed swing events of the histogram.
buy := hs.SwingLowEvent(2).Mul(hs.Shift(4).LT(0)).Mul(hs.LT(0))
short := hs.SwingHighEvent(2).Mul(hs.Shift(4).GT(0)).Mul(hs.GT(0))- Inputs: a histogram
hs, NaN before its first valid index: Pruitt’s ist6MACDHistogram(c, 12, 26, 9, 10, 3)’sh, valid from bar 33.buyandshortare 0 or 1, decided and filled at bar t’s close (MOC). - First valid index: that of
hsplus 4 (37).SwingLowEvent(2)sees a window holding a NaN as no event and theShiftpad’s +0 is not below 0, so no mask is needed. - Anchors:
buyequals Box 2.7’s six comparisons,ctr := hs.Shift(2)againsths.Shift(4),hs.Shift(3),hs.Shift(1)andhswithhs.Shift(4).LT(0)andhs.LT(0), bit for bit;SwingLowEvent(2)is strict, so a tie between the centre and any neighbour gives no event; negatinghsswaps the two flags. - Traps: the p-code’s comment “all bars below 0” is stronger than its rule: only the outer bars, and so the centre, must be below 0. The box prints
myMACDiffandXaverage(MACD,9)for its own names. Appendix A’s AmiBroker system is another rule (a simple 9-bar signal and a 100-day filter). - Missing piece: the system. Its 3-ATR target and 1-ATR stop on the close are the trade-state recipe “Entry-anchored bracket on the close (Pruitt)” on the base
c.PositionSigned(buy, nil, short, nil), but TradeStation re-enters on a later pivot inside a base trade after the bracket cuts it: on 50 random walks of 3,000 bars about seven cut trades in ten saw such a pivot, and on the test fixture 18 such pivots fire. Re-entries are tier B (algolang). - Evidence: Box 2.7 prints no results; Figure 2.12 is an E-mini S&P chart with arrows. Evidence, not a reference.
- Pinned by
TestRecipeT6HistogramPivot.
Elder-ray and the Triple Screen (Elder; Gimelfarb; Penfold)
Vadim Gimelfarb (“Bull And Bear Balance Indicator”, Stocks & Commodities V21:10, October 2003, pp. 68-72) states Alexander Elder’s Elder-ray: “Elder defines the bull power as the difference between the maximum price and the 13-day exponential moving average (H-EMA). The bear power is the difference between the minimum price and the 13-day exponential moving average (L-EMA).” Brent Penfold (The Universal Tactics of Successful Trend Trading, Wiley, 2021, ch. 6, pp. 223-227) tests Elder’s Triple Screen with it: the trend is “the slope of the weekly MACD (moving average convergence divergence) histogram”, and a buy’s retracement is “Elder-Ray Bear Power declines below zero & then ticks back up towards the centre line”. t6ElderRay returns the two powers, their sum and Penfold’s setups on a weekly histogram that the caller aligns to the daily bars.
e := c.XAverage(n).SetN(n-1, math.NaN())
bull := h.Sub(e)
bear := l.Sub(e)
line := bull.Add(bear)
setL := bear.LT(0).Mul(bear.Delta(1).GT(0)).Mul(wh.Delta(1).GT(0))
setS := bull.GT(0).Mul(bull.Delta(1).LT(0)).Mul(wh.Delta(1).LT(0))- Inputs:
h,landc, the weekly MACD histogramwhon the daily bars and n (13). The shippedwhis the x5 emulation of “Timeframe emulation by length” under “Relative: Timeframes”,t6MACDHistogram(c, 60, 130, 45, 10, 3)’sh, the weekly 12, 26 and 9 on daily bars, valid from bar 173 (its slope from 174).bull,bearandlineare in price units, their last bits depending on the platform (XAveragefuses multiply-adds on arm64);lineis TradingView’s single “Bull Bear Power”;setLandsetSare 0 or 1 at the setup bar’s close. - First valid index: n - 1 (12) for
bull,bearandline; the setups can first fire at the later of n and the first valid index ofwh’s slope (174 with the x5wh, where the test fixture’s first long setup fires). Each setup starts from a daily power, so it keeps the daily length whenwhis shorter. - Anchors:
bull - bearequalsh - lbit for bit when both subtractions are exact, by Sterbenz’s lemma when the average is within a factor of 2 of the high and the low, and not in general (a high of 8.830050900127464 and a low of 8.528003974348144 against an average of 43.93690755349225);lineis h + l - 2e to rounding; pandas-ta’seri, SMA-seeded, gives the powers to rounding. Penfold’s entry, “Buy break of the previous bar’s high”, is the flagsetL.Shift(1).Mul(h.GT(h.Shift(1)))one bar after a setup. - Traps: unmasked, bars 0 to 11 read h - c and l - c, since
XAveragepasses the close through. TradingView’s first-value average differs for about 170 bars at 1e-12. On synthetic five-bar weeks the x5 emulation’s one-day slope agreed with the true weekly slope on about 95% of week-ends and a five-day slope on about 87%; calendar weeks with holidays do worse. The powers’ last bits depend on the platform, so aDelta(1).GT(0)within an ulp of 0 can differ. A weekly signal ofm.XAverage(9)repeats the unsliced trap of the first section. - Variant: Elder’s own setups, from the dossier’s reading of Trading for a Living (1993), not checked here:
e.Delta(1).GT(0).Mul(bear.LT(0)).Mul(bear.Delta(1).GT(0))and its mirror on bull power. - Missing piece: a completed-week map onto daily bars, the value of the last completed week from the next week’s first bar, which “Mapping a higher timeframe onto base bars without look-ahead” describes and nseries does not ship; the caller supplies
whor uses the emulation. Penfold’s initial, breakeven and swing stops and his re-entries after a stop inside a base trade are tier B (algolang). - Evidence: Penfold’s test on his P24 portfolio of 24 US futures from 1980 made 336,473 over 11,633 trades, 29 a trade, net of his 51 a trade for brokerage and slippage (the book’s convention: his Random Trend Trader’s 1,567,646 less 31,953 trades at 51 is exactly its printed -61,957); positive, but with a 100% simulated risk of ruin, a UPI of 0.3 and a drawdown of 5,039 days, so he rejects it. The MetaStock 11 review’s run of “PS Bull Power Bear Power 1” on 500 days of QQQQ made 12.84% on 71 trades, 26 of them winners. Evidence, not a reference.
- Pinned by
TestRecipeT6ElderRay; runnable exampleExampleSeries_XAverage_elderRay.
TRIX
TRIX
func (s Series) TRIX(n int) Series
TRIX calculates the TRIX indicator but using the more recent approach of percentage gains of E3(t) compared with E3(t-1), rather than the older approach that used the log of the price.
The output is a percentage, via DeltaPercent. Bar 0 is 0; bars 1 to n-2 are the raw one-bar percentage change of the input, since the triple smoothing passes it through; the result is clean from bar 3n-2.
TRIXTS
func (s Series) TRIXTS(n int) Series
TRIXTS calculates the TRIX indicator using the same formula as TradeStation.
The output is 10000 times the one-bar change of the log of the triple-smoothed series, about 100 times TRIX. Bar 0 is 0, and the raw scaled log change follows while the averages pass the input through; the result is clean from bar 3n-2.
Trend Continuation Factor
TrendContinuationMinus
func (s Series) TrendContinuationMinus(n int) Series
TrendContinuationMinus returns the negative Trend Continuation Factor of s over the last n one-bar changes, the sum of the down changes minus the sum of the up-run gains, which reads as a downtrend when positive.
At bar t the window holds the changes of bars t-n+1 to t and so reads the n+1 closes s[t-n] to s[t]. With ch-[t] the change s[t-1] - s[t] on a down bar (s[t] < s[t-1], both closes finite, as in ConsecDownLoss) and 0 on every other bar, and with +CF the gain of ConsecUpGain, the value is the window sum of ch- minus the window sum of +CF. Both window sums are the package’s Sum(n) applied to the whole series and fed from bar 0, and the last step is one subtraction of the two sums, so on data without infinities (NaN allowed) the composition s.Delta(1).Mul(-1).Max(0).Sum(n).Sub(s.ConsecUpGain().Sum(n)) equals the result bit for bit from bar n.
The source is M.H. Pee, “Trend Continuation Factor”, Stocks & Commodities V20:3 (March 2002). His default is n = 35, tested over 25 to 45. A value above 0 reads as a downtrend and a TrendContinuationPlus above 0 as an uptrend; the two cannot both be positive, both can be 0 on a flat window, and both negative is Pee’s consolidation reading, which he did not test. His always-in latch, short while this is positive and long while the other is, is a recipe built on the two values rather than part of the method.
The leading term is the negation of Momentum(n): the window sum of ch- minus the window sum of the up changes is the negation of Momentum(n) in exact arithmetic, so the value is at most the negation of Momentum(n), with equality when every up run in the window has length 1. On strictly alternating closes, every bar from bar 1 up or down in turn so that every run has length 1, the result equals 0 - TrendContinuationPlus(n) bit for bit from bar n, the 0 written so that a zero is +0. The units are those of s and the value is unbounded on both sides.
The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. The minimum length is n+1, and n < 1 or n > len(s)-1 gives len(s) zeros, so n = len(s)-1 gives one valid bar, the last.
A close at bar p that is NaN or an infinity gives NaN on the bars [max(p, n), p+n] and on no other bar; the later bars recover, with new runs measured from the bar after the bad close. Finite closes whose sums overflow can still give +Inf, -Inf or NaN on bars with no bad close: {0, 1e308, 0, 1e308, …} gives an infinity at n = 3, and {-1e308, 1e308, -1e308, …} gives NaN at n = 2.
The value is not window-local: the gain of a window bar is measured from the anchor of its up run, which can lie before the window. Dropping bars from the front of s leaves the value at a bar unchanged to rounding, as long as the bar stays at slice index n or later and the slice keeps the anchor of every up run active at the window’s bars; the compensated Sum(n) is not guaranteed to be correctly rounded, so the bits can differ.
The mirror s.Mul(-1).TrendContinuationMinus(n) equals TrendContinuationPlus(n) bit for bit on every input. The cost is O(1) amortised per bar.
TrendContinuationPlus
func (s Series) TrendContinuationPlus(n int) Series
TrendContinuationPlus returns the positive Trend Continuation Factor of s over the last n one-bar changes, the sum of the up changes minus the sum of the down-run losses, which reads as an uptrend when positive.
At bar t the window holds the changes of bars t-n+1 to t and so reads the n+1 closes s[t-n] to s[t]. With ch+[t] the change s[t] - s[t-1] on an up bar (s[t] > s[t-1], both closes finite, as in ConsecUpGain) and 0 on every other bar, and with -CF the loss of ConsecDownLoss, the value is the window sum of ch+ minus the window sum of -CF. Both window sums are the package’s Sum(n) applied to the whole series and fed from bar 0, and the last step is one subtraction of the two sums, so on data without infinities (NaN allowed) the composition s.Delta(1).Max(0).Sum(n).Sub(s.ConsecDownLoss().Sum(n)) equals the result bit for bit from bar n.
The source is M.H. Pee, “Trend Continuation Factor”, Stocks & Commodities V20:3 (March 2002). His default is n = 35, tested over 25 to 45. A value above 0 reads as an uptrend and a TrendContinuationMinus above 0 as a downtrend; the two cannot both be positive, both can be 0 on a flat window, and both negative is Pee’s consolidation reading, which he did not test. His always-in latch, long while this is positive and short while the other is, is a recipe built on the two values rather than part of the method.
The leading term is Momentum(n): the window sum of ch+ minus the window sum of the down changes is Momentum(n) in exact arithmetic, so the value is at most Momentum(n), with equality when every down run in the window has length 1. The units are those of s and the value is unbounded on both sides.
The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. The minimum length is n+1, and n < 1 or n > len(s)-1 gives len(s) zeros, so n = len(s)-1 gives one valid bar, the last.
A close at bar p that is NaN or an infinity gives NaN on the bars [max(p, n), p+n] and on no other bar; the later bars recover, with new runs measured from the bar after the bad close. Finite closes whose sums overflow can still give +Inf, -Inf or NaN on bars with no bad close: {0, 1e308, 0, 1e308, …} gives an infinity at n = 3, and {-1e308, 1e308, -1e308, …} gives NaN at n = 2.
The value is not window-local: the loss of a window bar is measured from the anchor of its down run, which can lie before the window. Dropping bars from the front of s leaves the value at a bar unchanged to rounding, as long as the bar stays at slice index n or later and the slice keeps the anchor of every down run active at the window’s bars; the compensated Sum(n) is not guaranteed to be correctly rounded, so the bits can differ.
The mirror s.Mul(-1).TrendContinuationPlus(n) equals TrendContinuationMinus(n) bit for bit on every input. The cost is O(1) amortised per bar.
QStick
QStick
func (s Series) QStick(o, c Series, length int) Series
QStick implements Tushar Chande’s Qstick momentum indicator: the length-bar average of the close c less the length-bar average of the open o. The receiver is not used and the result has the length of c. Each average is taken over its own series, and the two are paired by Sub, so they are right-aligned at the most recent bar: when o is shorter than c, the leading len(c)-len(o) values are zero, and an empty o gives zeros.
For equal input lengths, bars 0 to length-2 equal c[i] - o[i] because both averages pass their inputs through. The first full averaged difference is at length-1, requiring length bars.
For equal lengths, QStick is the length-bar average of the signed candle body c.Sub(o), to rounding; RealBody gives the body’s size, and the Candlesticks category the other parts of a candle.
On the heikin-ashi candles, haC.QStick(haO, haC, p) with haO and haC from HeikinAshiOpen and HeikinAshiClose is Valcu’s Mov-haDiffCO, haC.Sub(haO).Average(p), in exact arithmetic, and agrees with it to rounding.
Compositions
Three oscillators of the bar’s body, the close less the open, over its range: the balance of power over the high less the low, and TSSB’s intraday intensity and the body pressure ratio over the true range. A zero divisor gives 0 (Div’s rule). Values are NaN before their first valid index; flags read 0 there and are decided at bar t’s close. The averages are simple, so no seed is chosen. QStick averages the body unnormalised, and Ehlers’s Relative Vigor Index on the gap-and-vigor page below filters a ratio of sums of the same kernel, where the balance of market power is the mean of the per-bar ratios. Prices are positive and finite and lengths are in bars.
Balance of power, BMP and Sell RSI (TA-Lib; Livshin; Wang)
Igor Livshin (“Balance Of Market Power”, Stocks & Commodities V19:8, August 2001) scores the bulls and the bears three ways, each “relative to the full price movement for that day (HighPrice-LowPrice)”, and plots “a 14-day moving average of the balance of power indicator”; the six scores reduce to TA-Lib’s BOP, “(Close-Open) / (High-Low)”, as the same issue’s TradeStation Traders’ Tip says. Howard Wang (“Sell Relative Strength Index”, V37:2, February 2019, pp. 36-38) averages the body over the range in absolute values and, in his letter of May 2019 (V37:6), revises it to the signed ratio: “Set the SRSI to 7 rather than 20”. t6BalanceOfPower returns the per-bar ratio and its n-bar mean.
bop := c.Sub(o).Div(h.Sub(l))
bmp := bop.Average(n).SetN(n-1, math.NaN())- Inputs:
o,h,landcand n (Livshin’s 14; Wang’s revised Sell RSI is the same line at 7);bopruns from -1 to +1, 0 on a bar with h = l, andbmpis the mean of the ratios. Arch-identical. - First valid index: 0 for
bopand n - 1 forbmp, sinceAveragepassesbopthrough before. - Anchors: Livshin’s sidebar (21 rows, 7 September to 5 October 1998):
bopprints his raw column andbmphis eight 14-day averages, -0.1002 to -0.1387, at four decimals, and his six spreadsheet scores givebopto rounding. Wang’s printed formc.RealBody(o).Div(h.Sub(l).Abs())isbop.Abs()bit for bit, so it cannot be negative. Barics’s percent buying with the open ("% B complements on-balance volume", Stocks & Commodities V8:5, 1990) isbop.Add(1).Mul(50)to rounding.QStickis the unnormalisedc.Sub(o).Average(n)to rounding. - Traps: a mean of ratios is not a ratio of sums:
c.Sub(o).Sum(n).Div(h.Sub(l).Sum(n))is QStick over the average range, another line. pandas-ta’s native path gives 1 on a flat bar (epsilon over epsilon). Wang’s charts show negative values that his printed code cannot give; his May 2019 code is the signed form. - Variant:
bop.Sum(n), n timesbmpto rounding, the scale of Livshin’s charts (-10 to 10 for 14 days); TA-Lib gives 0 on an inverted bar, h below l, which isc.Sub(o).Div(h.Sub(l).Max(0)). - Variant: Wang’s colours:
bmp.GT(0.05)green,bmp.LT(-0.05)red and yellow between, TradeStation plot colours rather than a signal; his lines at +-0.38 are drawn and not tested. - Evidence: Livshin’s and Wang’s charts (the Nasdaq, the DJIA of 1929 to 1935, NUGT, QQQ and SPY) carry no rule, trade or statistic. Evidence, not a reference.
- Pinned by
TestRecipeT6BalanceOfPower; runnable exampleExampleSeries_Div_balanceOfPower.
Intraday intensity (Aronson and Masters)
David Aronson and Timothy Masters (Statistically Sound Machine Learning for Algorithmic Trading of Financial Instruments, the TSSB manual) define TSSB’s INTRADAY INTENSITY from the true range and “today’s close minus today’s open”: “The intraday intensity is defined as the ratio of the latter to the former. This quantity is computed over the specified history length, and the moving average returned after being slightly transformed and rescaled to the range -50 to 50”. DELTA INTRADAY INTENSITY “is the difference between today’s INTRADAY INTENSITY and that of the specified number of days ago”. The concept is David Bostian’s and TSSB’s “is a modified version”. t6IntradayIntensity returns the ratio, its mean and the change of the mean, before the transform.
ii := c.Sub(o).Div(c.TrueRange(h, l, c))
intens := ii.Average(n).SetN(n-1, math.NaN())
dint := intens.Delta(d).SetN(n+d-1, math.NaN())- Inputs:
o,h,landc, the length n and the change d (TSSB’s INT_10 and DINT_10: 10 and 10);iiruns from -1 to +1. Arch-identical. - First valid index: 0 for
ii, the true range of bar 0 being the bar’s own range; n - 1 forintens; n + d - 1 fordint, sinceDelta’s pad reads +0 on bars 0 to d - 1, hence its own mask. - Anchors:
ii[0]equalsbopat bar 0, andiiisbopbit for bit wherever the true range is the bar’s range (the previous close inside it), and no larger in magnitude elsewhere. Masters’s downloadable code for his own intraday intensity is Bostian’s close location times volume,100(2c - h - l)/(h - l)times v: another quantity. - Traps: TSSB’s first value may be one bar later if it needs a previous close. The manual’s example of a change length of 10 speaks of “20 bars ago”.
- Variant: the squash with a caller’s k,
intens.Mul(k / math.Sqrt2).Erf().Mul(50), which is 100 Phi(k x) - 50, the Math introduction’s form, from -50 to 50;Erffuses multiply-adds on arm64, so its last bits depend on the platform. - Missing piece: TSSB’s constant k for this indicator is not published, so TSSB’s values cannot be reproduced.
- Evidence: TSSB’s stand-alone system (S&P 100 stocks, next open to next open, no costs stated): INT_10 was the most popular predictor in all five subsampled models (out-of-sample profit factors 1.07 to 1.09), and the printed intraday-intensity regression coefficients are negative, a short-term reversal reading. Evidence, not a reference.
- Pinned by
TestRecipeT6IntradayIntensity.
Body pressure ratio (TD Pressure, DeMark’s name; Perl)
Jason Perl (DeMark Indicators, Bloomberg Press, 2008, ch. 7, pp. 137-140) describes DeMark’s TD Pressure: “DeMark uses a five-price-bar period and compares the current close and open, divided by the current day’s range, and multiplied by the current day’s volume. The five-bar ratio of that buying pressure is totaled and then divided by the absolute value of those five price bars”, read against 75 and 25 with duration analysis: “An excursion of more than six price bars into overbought or oversold territory, however, suggests that a trend is strong.” The Bloomberg recommended settings use “(Close - Open)/True Range” with “Data for Calculations: Price”. “TD Pressure” is a registered trademark (the book credits Market Studies and Thomas DeMark), used here only to name the indicator; the helper’s name is descriptive. t6BodyPressure follows the settings and reads 50 on a window of zero pressures.
p := c.Sub(o).Div(c.TrueRange(h, l, c))
tot := p.Abs().Sum(n)
tdp := p.Max(0).Sum(n).Div(tot).Mul(100).Add(tot.EQ(0).Mul(50)).SetN(n-1, math.NaN())
over := tdp.GT(75)
under := tdp.LT(25)
strong := over.Sum(7).GTE(7)- Inputs:
o,h,landcand n (5);tdpruns from 0 to 100;over,underandstrongare 0 or 1,strongafter seven bars in a row above 75, more than six. Arch-identical. - First valid index: n - 1 (4), where
Sum’s partial windows end. - Anchors:
tdpis 50 + 50 sum(p)/sum|p| to rounding, andp.CumSum().CMO(n).Add(100).Div(2)to rounding from bar n: Chande’s oscillator of the cumulative pressure.strongequalsover.StateAge().Mul(over).GT(6)bit for bit. - Traps: a window of zero pressures is 0/0 in the text, and
Divalone reads 0, maximum selling pressure; the termtot.EQ(0).Mul(50)reads 50 there and changes no other bit (20 windows of the test fixture, inside its flat stretches). The text divides by the day’s range, the settings by the true range. The arrows, the rate-of-change tab and the gap adjustment of the settings are not defined. - Variant: the text’s volume form,
p.Mul(v)in place ofp(.Mul(v)on the pressure before the sums), and the range formc.Sub(o).Div(h.Sub(l)). - Evidence: Perl’s figure (S&P 500 futures, late 2007) is a chart, no statistic for TD Pressure is printed, and of TD Sequential’s signals he writes “I have not felt the need to backtest the signals”. Evidence, not a reference.
- Pinned by
TestRecipeT6BodyPressure.
Two readings of the bar against the one before. Kaufman’s gap momentum sums the opening gaps o.Sub(c.Shift(1)): from bar 1 the gap g is the overnight leg in points, onPts, of t6-overnight-legs.md, which masks bar 0 with NaN where this page sets it to 0 for CumSum. Ehlers’s Relative Vigor Index filters the body over the range, a ratio of filtered sums, where the balance of power on t6-body-over-range.md above is the mean of the per-bar ratios. Each section gives its first valid index; prices are positive and finite and lengths are in bars.
Gap momentum (Kaufman)
Perry Kaufman (“Gap Momentum”, Stocks & Commodities V42:1, January 2024, pp. 14-17) applies Granville’s on-balance volume to opening gaps, “today’s open minus yesterday’s close”: “Add all the positive gaps over the past N days. Separately, add all the negative gaps (as positive numbers) over the last N days”, divide them, and trade “the direction of the signal line”, executing “on the open following a new trading signal, which was generated without the current day’s gap”. His EasyLanguage keeps upgaps and dngaps from bar to bar, so the totals are cumulative, not reset, and sets the ratio to 1 when there is no down gap. t6GapMomentum is that listing, started at bar p + 1.
g := o.Sub(c.Shift(1)).SetN(1, 0)
up := g.Max(0).Shift(1).Sum(p).SetN(p+1, 0).CumSum()
dn := g.Mul(-1).Max(0).Shift(1).Sum(p).SetN(p+1, 0).CumSum()
ratio := up.Mul(100).Div(dn).Add(dn.EQ(0)).SetN(p+1, math.NaN())
sig := ratio.Average(s).SetN(p+s, math.NaN())
pos := o.Position(sig.GT(sig.Shift(1)), sig.LT(sig.Shift(1)))- Inputs: the open
o, the closec, the gap periodpand the signal lengths(1 and 20 as tested; the listing’s defaults are 40 and 20);ratiois 100 times the up total over the down total, andposthe long-only book at bar t’s close (pos.Shift(1)is the book from the next open, where Kaufman fills). - First valid index:
p + 1forratioandp + sforsig(2 and 21 for 1 and 20). - Anchors: at
p = 1the totals are the running sums of the up and down gaps through the bar before, andratioequals 100 times one over the other bit for bit. The signal rises exactly whenratiois above its valuesbars earlier, sincesigmoves by(ratio_t - ratio_{t-s})/s(to rounding near a tie). Doublingoandcchanges no bit. - Traps: the totals depend on where the history starts, like
OBV: starting the test fixture 50 bars later moves the last ratio from 96.96 to 96.14 atp = 1and from 88.51 to 88.27 atp = 40.gis 0 at bar 0 becauseShift(1)pads +0 and a NaN would poisonCumSum(the laterSetN(p+1, 0)makes the first mask redundant, but a baregis then right for other uses).Averagepasses the ratio through before bars - 1, hence the mask onsig; a NaN close makes both totals NaN from two bars later. Gaps are in points, so split-unadjusted data breaks them. Where the down total is 0 the ratio is 1, as in the listing, which on a scale centred on 100 reads as an extreme bearish balance rather than “no down gaps yet”. - Variant: the Traders’ Tips’ form (MetaStock, Pine, NeuroShell, Zorro) resets every bar and includes today’s gap:
g.Max(0).Sum(p).Mul(100).Div(dR).Add(dR.EQ(0)).SetN(p, math.NaN())withdR := g.Mul(-1).Max(0).Sum(p), its signal first valid atp + s - 1; atp = 1its ratio is exactlyg.GTE(0). - Evidence: on SPY, the QQQ and five stocks (TSLA, AMZN, BAC, BA and MCD) from 2010 to 2021 with
p = 1ands = 20, 55.8% profitable trades against 43.1% for a 100-day average, and profit over drawdown 4.10 against 2.29, with no costs stated. Evidence, not a reference. - Pinned by
TestRecipeT6GapMomentum; runnable exampleExampleSeries_CumSum_gapMomentum.
Relative Vigor Index (Ehlers)
John Ehlers (“Relative Vigor Index (RVI)”, Stocks & Commodities V20:1, January 2002, pp. 16-20) starts from “prices tend to close higher than they open in up markets”, so “RVI = (Close-Open) / (High-Low)”. He filters the numerator and the denominator with “C = [1 2 2 1] /6”, “to guarantee elimination of both the two-bar and three-bar cycle components”, sums each over “a nominal value of 10”, and plots a trigger, the same filter of the ratio, which “trails by only 1.5 bars”; his listing keeps the ratio when the sum of ranges is 0 (If Denom <> 0 then RVI = Num / Denom). t6RelativeVigor returns the Relative Vigor Index as vigor (the library’s RelativeVolatilityIndex is Dorsey’s, not this one) and its trigger.
num := c.Sub(o).TriangularAverage(4).Sum(n)
den := h.Sub(l).TriangularAverage(4).Sum(n)
vigor := num.Div(den).ValueWhen(den.Abs().GT(0)).SetN(n+2, math.NaN())
trig := vigor.TriangularAverage(4).SetN(n+5, math.NaN())- Inputs: the open
o, highh, lowland closecand the summing lengthn(10);vigorlies in [-1, 1] when every bar’s high and low bound its open and close. - First valid index:
n + 2forvigorandn + 5fortrig(12 and 15); a cross can first fire atn + 6. - Anchors: Ehlers’s filter facts hold exactly:
TriangularAverage(4)zeroes the alternating sequence and the period-3 sequence 1, -0.5, -0.5 from bar 3, passes a constant and lags a ramp by exactly 1.5 bars;Sum(10)of the alternating sequence is 0 andSum(9)leaves +-1, 20 log10(1/9) = -19.1 dB (“weaker only by about 20 dB”).TriangularAverage(4)equalsFIR([]float64{1, 2, 2, 1}), and from bar 3 on data on a binary gridAverage(2).Average(3), the TradeStation tip’s form, bit for bit.vigoris a filtered, range-normalisedQStick:c.QStick(o, c, n).TriangularAverage(4)overh.Sub(l).Average(n).TriangularAverage(4), to rounding. - Traps: where the summed range is 0 (n + 3 flat bars with high equal to low)
vigorholds its last value, as the listing does;Divalone would read 0, andRatiowould also turn a NaN quotient into 0. Unmasked, bars 0 to 2 read 0 and bars 3 ton + 1are ratios of partial sums. - Variant: Marge Sherald (“Turbocharge It!”, Stocks & Commodities V23:8, August 2005) takes “the RVI lagged by one bar” as the trigger and buys “when the RVI trigger crosses above the RVI”:
vigor.Shift(1).CrossesAboveS(vigor), which equalsvigor.CrossesBelowS(vigor.Shift(1)), a turn down. - Variant: Brys and Van Hof’s OHLC indicator (Stocks & Commodities V21:1, January 2003) smooths the body and the range with first-value exponential averages:
c.Sub(o).XAverageAlpha(a1).Div(h.Sub(l).XAverageAlpha(a2)).XAverageAlpha(a3). - Missing piece: the stochastic and adaptive forms come from Ehlers’s Cybernetic Analysis for Stocks and Futures (2004), which is not available, so they are left out; the adaptive form’s variable sum composes (
SumVariable), but its length needs Ehlers’s Cyber Cycle period estimator, which the library lacks. - Evidence: Sherald’s Home Depot test made 60 trades out of sample with a profit factor of 1.18 (46 trades and 1.89 after a genetic search that reused the validation year), costs not stated; Brys and Van Hof’s euro equity curves for their indicator carry Sharpe ratios of -0.25 and -0.10, with no slippage and no costs stated. Evidence, not a reference.
- Pinned by
TestRecipeT6RelativeVigor; runnable exampleExampleSeries_TriangularAverage_relativeVigor.
The setups below trade opening gaps on day-session daily bars (the sources trade pit sessions: “Night sessions are omitted”; on 24-hour bars the open is a different event), and all three read the open o: the Oops and the runaway gap for the gap and the stop, the unfilled gap only for its fill price. Recipe values, levels and results are NaN before their first valid index (SetN(F, math.NaN())), and flags read 0 there: every shifted series a pad can satisfy is masked, or the flag is, because a gap-up test such as o.GT(h.Shift(1)) reads 1 on bar 0 against the +0 pad. After F a result reads 0, or -0 where the close lies on the losing side, on bars without a trade, as the gated nights of the overnight-legs page t6-overnight-legs.md do, so totals of a result with a NaN head use res.SetN(F, 0).CumSum() (a plain CumSum is NaN on every bar). Prices are positive and finite, and series are paired by position (the caller aligns them by timestamp); weekday and trading-day keys are the caller’s (the Comparison intro’s “Calendar keys” table). A resting stop is touched when the high reaches it (GTE, the low for a sell) unless the source says “above”, which is strict on a tick grid; a stop fill price is max(o, level), written o.Maximum(level), and is the level itself when the bar opens below a buy stop (above a sell stop). A trade that opens and closes on one bar, as the Oops and runaway day trades do, is a bar-level result, c.Sub(px).Mul(fill), not a Position latch, which does not test an exit on its entry bar; Williams’s bailout on the Oops, a trade held across bars, is a latch on PositionSignedRules with the open as receiver, its stop touched by the low (the high for a sell) and its exit values from PositionSignedExitValue. An exact decimal tie at a threshold (the 10% Oops’s close location, the runaway’s 0.4 times the average true range) is decided by binary rounding; the goldens run on prices in sixty-fourths, and the runaway’s touch is pinned on that grid by a constructed case. Backtests over data we do not have are evidence, not a reference. Counts on probe paths that the tests do not reproduce are illustrations, not pins.
Oops gap fade (Williams; Ruggiero)
Larry Williams (Long-Term Secrets to Short-Term Trading, 2nd ed., Wiley, 2011, ch. 7, pp. 145-152) buys after “an opening that is below the previous day’s low” when “price then rallies back to the previous day’s low”, on a stop (“Buy here on a stop”, Figure 7.26), and sells the mirror after “an open greater than the prior day’s high”. Murray A. Ruggiero Jr. (Cybernetic Trading Strategies, Wiley, 1997, ch. 4, section “Gap Analysis”, pp. 72-77) tests it as a day trade: “We will buy when we gap lower than yesterday’s low on a stop at yesterday’s low. We will exit on the close.” Williams’s bond trades use “the bailout technique” (ch. 11, p. 195: “exit on the first profitable opening”) with a stop: buys “any day of the week except Wednesday, and a stop-loss of” 1,800 dollars “from the point of entry” (Figure 7.29), sells on Wednesday “using a closer” 1,000-dollar “stop and 4-day bailout exit” (Figure 7.30), and with the nine-day trend “The exit is the close on the first profitable opening after 3 days in the trade” (Figures 7.32 and 7.33). t6OopsBailout returns the buy and sell flags and their day results in points, from the fill at the prior extreme to the close, and on PositionSignedRules the book of the fills with his stop and bailout, the value each exit was read at and each trade’s result.
l1 := l.Shift(1).SetN(1, math.NaN())
h1 := h.Shift(1).SetN(1, math.NaN())
buy := o.LT(l1).Mul(h.GTE(l1))
sell := o.GT(h1).Mul(l.LTE(h1))
buyRes := c.Sub(l1).Mul(buy)
sellRes := h1.Sub(c).Mul(sell)
r := PositionRules{
MinHold: delay,
LongStop: PositionLevel{Level: l1.Sub(d), Touch: l},
ShortStop: PositionLevel{Level: h1.Add(d), Touch: h},
LongTarget: PositionLevel{Level: l1},
ShortTarget: PositionLevel{Level: h1},
Open: o,
}
book := o.PositionSignedRules(buy, nil, sell, nil, r)
ex := o.PositionSignedExitValue(buy, nil, sell, nil, r)
px := l1.Mul(book.GT(0)).Add(h1.Mul(book.LT(0)))
res := ex.Sub(px.EntryValue(book).Shift(1)).Mul(book.Shift(1))- Inputs: day-session daily bars
o,h,landcof one instrument (Williams’s 2011 note says the pattern “no longer works” because “We no longer have pit trading sessions”); results in price points. The fill is the prior extreme exactly: the open lies beyond it, soo.Maximum(l1)equalsl1on every buy bar (and the sell mirror’s fill ish1), and the open is known before the stop can trigger.d> 0 is the stop’s distance in price units (the point value is the caller’s);delayis the bars from the fill bar before the bailout is read,MinHold: 0 for Figure 7.29, 4 for Figure 7.30’s 4-day bailout and 3 for “after 3 days in the trade”. - First valid index: 1 for every output.
l1andh1are NaN on bar 0, sobuyandsellread 0 there andbuyResandsellResare NaN; from bar 1 the results read 0 on bars without a trade, -0 where the close is below the prior low (above the prior high forsellRes). Totals arebuyRes.SetN(1, 0).CumSum()andsellRes.SetN(1, 0).CumSum().bookreads 0 until the first fill, andexandresare NaN until the first exit and on every bar that is not an exit bar. - Anchors: on the 320-bar fixture group the buys fall on bars 164, 172, 189, 205, 209, 211, 213 and 233 and the sells on 12, 185, 206, 214, 230 and 250; bars 205 and 206 touch the level exactly (a high equal to the prior low after a gap-down open, a low equal to the prior high after a gap-up open), and
buyRes[205]andsellRes[206]are -0.125. The unmasked level,c.Sub(l.Shift(1)).Mul(buy), reads 0 on bar 0 and equalsbuyResbit for bit from bar 1. The 10% Oops and the ADX gapper below are products ofbuyandsell, so every trade of theirs is an Oops trade.book,exandresequal a loop of his exits (the fill at the prior extreme, a reversal on the opposite fill, the stop read from the bar after the fill and first on a bar with both, the bailout oncedelaybars have passed) bit for bit on the 320-bar group and two 3,000-bar walks for four distances and five delays, anddelay1 isdelay0; on the 320-bar group (d 1.5) the 14 fills give 12 trades, closed by 2 stops, 7 bailouts and 3 reversals at delay 0 and by 3, 5 and 4 at delays 3 and 4. The runnable example’s constructed bars give a bailout on the next open at delay 0, held to bar e + 3 at delay 3, and a stop at its level. - Traps: the dossier’s gap-up flag
o.GT(h.Shift(1))reads 1 on bar 0 for any positive bar (the +0 pad), and itsoopsSell := gapUplacks the fill testl.LTE(h.Shift(1)); the gap-down tests read 0 on bar 0 only because prices are positive (back-adjusted futures can be negative). Day-session bars: the authors trade pit sessions (“Night sessions are omitted”, “night data is omitted”), and an open after an electronic overnight session is not the open they tested. The touch fills (GTE, “rallies back to”), and the stricth.GT(l1)drops bar 205. The dossier’s “window-local” holds for the Oops, the 10% Oops and the gap-down reversal only. “After 3 days in the trade” isMinHold: 3, the bailout first read on the open of bar e + 3, the fill bar counted as the first day: his reports count a trade’s bars as its exit bar less its entry bar, so a trade out on the next open is 1 bar (Figure 7.28), and Figure 7.32 prints an average of 3 bars in winners, where a first reading on bar e + 4 would give every winner at least 4 (Figure 7.30’s 4-day bailout prints 4). “The exit is the close on the first profitable opening” is read as the bailout at that open, his rule in ch. 11. - Variant: the 10% Oops (Laurence A. Connors, Connors On Advanced Trading Strategies, M. Gordon Publishing, 1998, ch. 21, pp. 167-170, after Sklarew: day one must “close in the bottom 10 percent of its daily range”, then “buy at the day-one low”):
buy.Mul(c.Stochastic(h, l, 1).Shift(1).LTE(10)).Mul(h.Shift(1).GT(l.Shift(1))), sells withGTE(90), F 1. The last factor masks a flat prior bar, whichStochasticreads as 0, the bottom 10% but not the top; the thresholds are inclusive (unsaid), and on decimal prices an exact 10% is decided by binary rounding (a close of 72.1 in a bar from 72 to 73 reads 9.999999999999432, one of 0.8 from 0.7 to 1.7 reads 10.000000000000009). - Variant: the ADX gapper (Linda Bradford Raschke and Laurence A. Connors, Street Smarts, M. Gordon Publishing, 1995, ch. 11, pp. 87-95: “a 12-period ADX and a 28-period +DI/-DI”, “The ADX must be greater than 30”, and “the +DI must be greater than the -DI”):
buy.Mul(c.ADX(h, l, c, 12).GT(30).Mul(c.DMIPlus(h, l, c, 28).GT(c.DMIMinus(h, l, c, 28))).Shift(1)), sells with -DI above +DI, all read on the prior bar since the stop rests from the open. F 13 (ADX(12)is 0 on bars 0 to 11), and the zero-seeded DIs, live from bar 1, carry a seed transient through the burn-in of the Average Directional Index paget6-directional-index.md. - Members:
- Williams’s S&P exit, “out on the opening tomorrow”:
o.Sub(l.Shift(2)).Mul(buy.Shift(1)).SetN(2, math.NaN()); - the nine-day trend,
m := c.Average(9)thenbuy.Mul(m.Shift(1).LT(m.Shift(2)).SetN(10, 0))(sellsGT), F 10; - the S&P weekday filter (no Wednesday or Thursday buys; ISO
weekday, Monday 1, as in the Comparison intro):buy.Mul(weekday.LT(3).Add(weekday.GT(4))); - the trading-day filter, “after the 17th trading day of the month”:
buy.Mul(tdm.GT(17)); - Street Smarts’ gap-down reversal (ch. 12),
o.LT(l1).Mul(c.GT(o)).Mul(c.Stochastic(h, l, 1).GTE(50)), a gated night oft6-overnight-legs.md; - Ruggiero’s close-cross fade,
o.LT(l1).Mul(h.GTE(c.Shift(1))), filled at the prior close, above the open.
- Williams’s S&P exit, “out on the opening tomorrow”:
- Departures: the book is a bar-level state, and the order of fills inside a bar is algolang’s. The fill bar is not tested: the fill comes after the open, and a daily bar does not say whether its low (its high, for a sell) came before the fill or after it, so a stop the fill bar reaches is honoured on the next bar’s test, if still touched, or not at all. On a reversal bar
exis the open, or the stop on a stop bar, where the trade closes at the opposite fill. A bar that opens beyond the fill and later touches the stop exits at the stop, where his bailout at the open comes first (13 of the 124 trades of a 3,000-bar walk at d 0.5 and delay 0). A fill on the bar of a stop or a bailout opens a trade only on a later fill. The day resultsbuyResandsellResstay unstopped (Ruggiero’s 600-dollar money stop is left out) for the same order. Point values and weekday and trading-day keys are the caller’s. - Evidence: Williams’s TradeStation reports print Comm 0 and Slippage 0 (only his bond text deducts commissions of 50 dollars): S&P buys except Wednesday and Thursday, out on the next open with a 2,000-dollar stop, made 42,687.50 dollars in 98 trades from 1987 to 1998, 82% winners, 435.59 dollars a trade (Figure 7.28; his text says 438); in bonds from 1990 to 1998, with the bailout, buys made 27,875 dollars in 138 trades, 86% winners (Figure 7.29), sells on Wednesday 9,875 dollars in 55, 56% (Figure 7.30), and with the nine-day trend and the bailout after 3 days buys 24,625 dollars in 66, 81% (Figure 7.32), and sells 13,406.25 dollars in 34, 79% (Figure 7.33); with the nine-day trend (“if Friday’s nine-day moving average is less than Thursday’s”, after Joe Krutsinger), S&P buys on Tuesday, Wednesday and Friday made 22,362.50 dollars in 49 trades (Figure 7.34); and Figure 7.35, captioned for the trading-day filter, carries the Wednesday-sell numbers, so no such result is printed. Ruggiero’s Table 4.5 (S&P 500, 4/21/82 to 6/28/96, out on the close, costs not restated) made 73,075 dollars in 588 trades, 56% winners, and Table 4.6, with a 600-dollar money stop, 145,950 dollars. Connors’s 1996 S&P list for the 10% Oops sums to 43.75 points over 15 trades, 12 of them profitable, and its total of 19,880 dollars does not reconcile at 500 dollars a point; his Figures 21.1 and 21.2 are partly printed charts (the “Ave” of Figure 21.2 reads 73.763, which the chart’s two closes cannot average), so they serve only as the shape of constructed bars. Street Smarts prints no sample for its members, only claims: “an almost 60 percent head start” for the gap-down reversal (ch. 12) and “between two to four trades per week” for the ADX gapper. No out-of-sample test is printed. Evidence, not a reference.
- Pinned by
TestRecipeT6OopsGap; runnable exampleExampleSeries_PositionSignedRules_oopsBailout.
Runaway gap continuation (Ruggiero)
Ruggiero (Cybernetic Trading Strategies, ch. 4, Table 4.2 “Runaway Gaps”, p. 74) follows Williams’s “strategy of buying or selling in the direction of the gap if the market continues to move in that direction” with If NextOpen>High then buy at NextOpen+Brakeout*Average(TrueRange,3) stop; and ExitLong at close;, a system that “buys on a stop order and then exits on the close without holding any overnight positions”, tested with “a breakout equal to .40”. t6RunawayGap re-indexes the listing to the trade bar, whose open gaps beyond the prior extreme and whose stop lies k times the prior bar’s n-bar average true range beyond that open, and returns the flags and their results in points, from the stop to the close.
off := c.ATR(h, l, c, n).Shift(1).Mul(k)
up := o.Add(off).SetN(n, math.NaN())
dn := o.Sub(off).SetN(n, math.NaN())
buy := o.GT(h.Shift(1)).Mul(h.GTE(up))
sell := o.LT(l.Shift(1)).Mul(l.LTE(dn))
buyRes := c.Sub(up).Mul(buy)
sellRes := dn.Sub(c).Mul(sell)- Inputs: day-session daily bars
o,h,landc; n (3) and k (0.4, the tested “.40”). The listing’sNextOpenis the trade bar’s open and itsHighandAverage(TrueRange,3)the prior bar’s, so nothing is read before it is known. The fill price is the stop: k times an average true range is not negative, so the stop lies at or beyond the open ando.Maximum(up)equalsup. The stop is the open plus a fraction of a range, the volatility-breakout level of the True Range category, here behind a gap test. - First valid index: n (3) for every output.
ATRis a simple average ofTrueRangethat passes it through on bars 0 to n - 2, and bar n’s stop reads the first full average, over bars 0 to n - 1, which still holds bar 0’sh - l,TrueRange’s own convention (SetN(n+1, ...)if every true range should see a prior close). The NaN stop reads 0 in both flags, which need no mask of their own; the results are NaN before n and 0 or -0 on bars without a trade after it, and totals arebuyRes.SetN(n, 0).CumSum()andsellRes.SetN(n, 0).CumSum(). - Anchors:
c.ATR(h, l, c, n)isc.TrueRange(h, l, c).Average(n)by its source, the dossier’s form, bit for bit. On the 320-bar fixture group the buys fall on bars 91, 113, 185, 206, 214 and 217 and the sells on 189, 191, 205, 209, 211, 213, 220, 224 and 228; on the 60-bar group with a NaN close on bar 40,buyResis NaN on bars 0 to 2, 40 and 42 to 44 (the NaN enters bar 41’s true range and the averages of bars 41 to 43) and -0.31562499999999716 on bar 41. The touch is pinned by a constructed case: three true ranges of 15/64 giveATR15/64, and 15/64 x 0.4 rounds to 6/64 exactly, so a high at the open plus 6/64 reaches the stop and buys, whereh.GT(up)would not. - Traps: unmasked, the dossier’s
runaway := o.GT(h.Shift(1))reads 1 on bar 0 against the +0 pad, and its long fills there, at a stop equal to the open (the shifted average is the pad); the stops of bars 1 and 2 read a single true range passed through, not an average of three.ATRis the simple average, not Wilder’sATRWilder. The gap is measured against the prior high or low, not the prior close (Kaufman’s gap on the paget6-gap-and-vigor.md). - Variant: the filter “We will require the S&P500 to close lower than 5 days ago”, on both sides as the text says:
buy.Mul(c.Shift(1).LT(c.Shift(6)).SetN(6, 0)), F 6. Tables 4.3 and 4.4 print the same short profit, 62,500 dollars, as if the filter changed only the longs. The mask is a no-op on positive closes (the pad cannot satisfyLT) and is needed in theGTmirror; in exact arithmetic it is a falling 5-bar average on the prior bar,m.Shift(1).LT(m.Shift(2))withm := c.Average(5)(the average’s change is the 5-bar change over 5), the Oops section’s nine-day trend with 5 bars, and to rounding in binary64. - Departures: the printed short,
If NextOpen<Low then sell at NextOpen+Brakeout*Average(TrueRange,3) stop;, adds the offset: a sell stop above the market fills at once at the open in TradeStation, so the code as printed sells at the open on every gap below the low and covers at the close (o.Sub(c).Mul(o.LT(l.Shift(1)).SetN(1, 0))); the recipe corrects the ‘+’ to the mirror of the buy,o.Sub(off). The input readsBreakout(.3)against the tested .40, and both order lines use “Brakeout” as printed, a name the input does not declare, so the listing would not verify as printed. - Evidence: Table 4.3 (S&P 500, 4/21/82 to 5/31/96, 50 dollars a trade for slippage and commissions, k .40): 80,150 dollars over 384 day trades, 17,650 long and 62,500 short, 55% winners; Table 4.4, with the filter: 86,150 dollars over 237 trades, 59% winners. With the printed ‘+’, which short side was tested is unknown, and no out-of-sample test is printed. Evidence, not a reference.
- Pinned by
TestRecipeT6RunawayGap.
Three-day unfilled gap reversal (Raschke and Connors)
Raschke and Connors (Street Smarts, ch. 13, pp. 103-108) buy when “Today the market must gap lower and not fill the gap”: “Over the next three trading sessions, have in place a buy stop one tick above the high of the gap-down day”, “If filled, place a protective sell stop at the low of the gap-down day”, and “If not filled after three trading sessions, cancel the initial buy stop”. t6UnfilledGap reads the gap as a whole bar below the prior low, h < l[t-1] (which implies the open gap), and returns the first fill after the latest gap, its price max(o, level) and the protective stop one tick below the gap bar’s low, as their examples place it.
gap := h.LT(l.Shift(1)).SetN(1, 0)
lvl := h.ValueWhen(gap)
age := gap.BarsSince()
trig := age.GTE(1).Mul(age.LTE(float64(k))).Mul(h.GT(lvl))
buy := trig.Mul(trig.SumSince(gap).EQ(1))
px := o.Maximum(lvl.Add(tick))
stop := l.ValueWhen(gap).Sub(tick)- Inputs: day-session daily bars
o,handl(the close is not read); k sessions (3) and the price grid’stick(1/64 in the goldens). “One tick above” is the stricth.GT(lvl)on a tick grid, where distinct prices differ by at least a tick, so the flag needs no tick and only the fill price and the stop do. The open is read only inpx: an open above the stop fills at the open. - First valid index: 2 for
buy: bar 0 cannot gap, so a gap on bar 1 is the earliest and its first trigger is bar 2.pxandstopare NaN until the first gap (bar 115 on the fixture) and then hold the latest gap’s levels;lvlandageare NaN before it, which makestrig0 there. TheSetN(1, 0)ongapis a no-op on positive prices (a high below the +0 pad fails), asserted as such, and essential in the sell mirror. - Anchors: a new gap replaces the pending order:
buyequals a loop with one order, re-placed at each new gap, bit for bit. Against a loop that keeps every gap’s order (each live for three sessions), the entries from flat are identical and only add-on fills differ (a few per 20,000 probe bars, none on some walks), because a newer gap’s high lies below the older gap bar’s high (H[g'] < L[g'-1] <= H[g]), so its stop is lower and lives longer. On the 320-bar fixture group the buys fall on bars 192, 199, 225, 230 and 258 with (px, stop) = (110.234375, 109.265625), (110.015625, 109.453125), (108.671875, 107.953125), (107.84375, 107.171875) and (107.609375, 106.984375): bar 225 follows a second gap down that replaced the first, bar 230 fills at a gap-up open, and bar 258 on the third day (age.LTE(float64(k))is inclusive). The strictGTis pinned by a constructed case: a later high equal to the gap bar’s high does not fill. - Traps: the dossier’s three-
Shiftform,gap.Shift(1).Mul(h.GT(h.Shift(1)))with the same at lags 2 and 3, fires on every trigger bar inside the window and for either of two overlapping gaps (about twice as many bars as first triggers on a 20,000-bar probe walk), not once per gap.SumSinceincludes the gap bar, wheretrigis 0 (its age is 0), and before the first gap it is NaN, whichEQ(1)reads as 0.lvl,pxandstophold the latest gap’s levels on every bar after it, so read them onbuybars only. - Variant: the sell mirror (“SELLS ARE REVERSED”):
gapU := l.GT(h.Shift(1)).SetN(1, 0)withlvlU := l.ValueWhen(gapU), the triggerl.LT(lvlU)and the priceo.Minimum(lvlU.Sub(tick)); there the bar-0 mask is essential, since any positive low is a gap up against the pad. - Missing piece: Bruce Babcock’s opening-gap continuation (Charles Le Beau and David W. Lucas, Day Trading Systems & Methods, 1992, ch. 8, pp. 46-51) waits “for an opening price that gaps noticeably from the previous day’s close”, then would “Tack on a few points above or below the opening price range and enter the market on a buy stop or sell stop”. The opening range needs intraday bars (each session’s first minutes), so on daily bars only the gap line composes,
on.GTE(g)andon.LTE(-g)withonthe overnight-legs page’sonPts; his “gaps of 75 points” and “25 points” are hundredths of an index point (Exhibit 9’s axis reads 34800 to 35400), so 0.75 and 0.25 index point. - Evidence: the book prints chart examples and no backtest: wheat (Exhibit 13.1, the sell mirror: its sell stop at 394 1/4 and protective stop at 400 1/4 give the printed “six cents loss”), Motorola and orange juice, with the remark that “a gap that occurred 20 days ago is less significant than one that occurred yesterday”; Babcock’s chapter prints no results. Evidence, not a reference.
- Pinned by
TestRecipeT6UnfilledGap; runnable exampleExampleSeries_BarsSince_unfilledGap.
The overnight legs below split each bar’s close-to-close move at the open: bar t’s overnight leg runs from bar t - 1’s close to bar t’s open and its daytime leg from that open to bar t’s close. The opening gaps that Kaufman’s gap momentum accumulates are on t6-gap-and-vigor.md, and the gap setups of Williams, Ruggiero and Raschke and Connors on t6-gap-setups.md. Each section gives its first valid index; prices are positive and finite and lengths are in bars.
Overnight and daytime legs (Skyba; Connors and Alvarez)
Erik Skyba (“The Overnight Return Study”, Stocks & Commodities V30:2, February 2012, pp. 34-38) defines “the day’s return as the difference between the day’s open and the day’s close” and the overnight return as “the difference between the day’s close and the following morning’s open”. Larry Connors and Cesar Alvarez (Short Term Trading Strategies That Work, 2008, ch. 7) total SPY’s points “Overnight - Buy the SPY on the close and sell on the next day’s open” against “Day - Buy the SPY on the open and sell on the same day’s close”. t6OvernightLegs gives both legs, each indexed by the bar on which it is realised, in points, fractions and logarithms.
onPts := o.Sub(c.Shift(1)).SetN(1, math.NaN())
dayPts := c.Sub(o)
onFrac := o.Div(c.Shift(1)).Sub(1).SetN(1, math.NaN())
dayFrac := c.Div(o).Sub(1)
onLog := o.Log().Sub(c.Log().Shift(1)).SetN(1, math.NaN())
dayLog := c.Log().Sub(o.Log())- Units: points add over a window to the net change (Connors and Alvarez’s totals); fractions are trades at a fixed notional (Skyba invests 100,000 a trade); logarithms decompose the close-to-close return exactly. Fractions do not add:
onFrac + dayFracmisses the productonFrac*dayFrac, while(1 + onFrac)(1 + dayFrac)is the close ratio to an ulp or two. - Keys: none. Prices must be positive: a zero divisor gives 0 through
Div’s zero rule, not an error, and a NaN open makes both of its bar’s legs NaN. - First valid index: 1 for the overnight legs, 0 for the daytime legs.
SetN(1, math.NaN())replaces theShiftpad, which would make bar 0’s overnight legs the open, -1 and the open’s logarithm. - Anchors:
onPts.Add(dayPts)equalsc.Delta(1)bit for bit from bar 1 when the open and the two closes are positive and pairwise within a factor of 2, because each difference is then exact (Sterbenz’s lemma);onLog.Add(dayLog)equalsc.Log().Delta(1)bit for bit from bar 1 when the three logarithms have one sign and are pairwise within a factor of 2, which holds for prices well away from 1. The ratio formo.Div(c.Shift(1)).Log().Add(c.Div(o).Log())agrees only to rounding.onPtsis the opening gapgthat Kaufman’s gap momentum ont6-gap-and-vigor.mdaccumulates, the same expression with bar 0 set to NaN where that page sets it to 0, so the two agree bit for bit from bar 1. - Traps: ex-dividend drops fall in the overnight leg, so open and close must share one adjustment basis (back-adjusted futures keep points, ratio-adjusted stocks keep fractions and logarithms). A night after a weekend or a holiday spans several calendar nights, and a missing bar merges two nights.
- Evidence: in Skyba’s SPY test (1993 to 2010) the overnight Sharpe ratio was 0.88 against -0.25 for the day, and a commission of one cent a share more than halved the overnight profit (two cents left a loss); in Connors and Alvarez’s, SPY gained 171.40 points overnight and lost 70.88 in the day sessions from 1995 to 2007. Evidence, not references.
- Pinned by
TestRecipeT6OvernightLegs.
Overnight hold after an up, a down or a weak close (Skyba; Unger)
Skyba conditions the night on “the day’s close” against the previous close (his Figures 9 and 10), bought at that close. Andrea Unger (“Measuring And Exploiting The Overnight Edge In U.S. Stocks: From SPY To A Nasdaq-100 Portfolio”, Stocks & Commodities V44:10, October 2026, pp. 44-48) enters “at the close (4:00 pm exchange time)” and exits “the following morning at the open”, with filters such as “Enter a position only if today’s close is higher than yesterday’s close” and “Enter a position only if today’s close falls within the lowest fifth of the daily range”, coded c<range/position_close+l. t6OvernightHold gates each night’s fraction by its condition at the previous close.
on := o.Div(c.Shift(1)).Sub(1).SetN(1, math.NaN())
up := c.Delta(1).GT(0)
down := c.Delta(1).LT(0)
weak := c.LT(h.Sub(l).Div(5).Add(l))
afterUp := on.Mul(up.Shift(1))
afterDown := on.Mul(down.Shift(1))
afterWeak := on.Mul(weak.Shift(1))- Timing: each condition is read at the entry close, a market-on-close decision, and the night is realised on the next bar, so the gate is shifted by one. The outputs are the held nights’ fractions and 0 (or -0) on the other bars, so their sums, with bar 0’s NaN set to 0, are the strategies’ totals at a fixed notional.
- Keys: none. A NaN close reads 0 in every gate; a tie is neither up nor down; a flat bar (h equal to l) is not weak, since
c < lfails. - First valid index: 1 (bar 0 is NaN); the up and down gates can first hold the night of bar 2, because bar 0 has no previous close.
- Traps:
c.GT(c.Shift(1))fires on bar 0, where theShiftpad is 0, andc.Delta(1).GT(0)does not.c.Stochastic(h, l, 1).LT(20)fires on a flat bar, where Stochastic reads 0, and otherwise agrees withweakup to rounding at a close exactly a fifth of the range above the low. - Unresolved: the sign of the up and down split. Skyba finds the nights after down closes better since 2001, while Unger’s average trade after an up close is 16.02 against 9.21 after a down close (per 10,000 traded, 2010 to 2025). The page gives both gates and recommends neither.
- Variant: Unger’s skip-day filter leaves out Friday’s night,
on.Mul(weekday.LT(5).Add(weekday.GT(5)).Shift(1)), which also leaves out a NaNweekday. - Variant: his RSI filter, “the two-period RSI indicator is in oversold territory (RSI < 30)”, is
c.RSI(2).SetN(2, math.NaN()).LT(30), masked because RSI reads exactly 0 on bars 0 and 1. - Missing piece: his week-open filters compare this week’s open with last week’s. On daily bars the first session of the week is
weekday.LTE(weekday.Shift(1)), but intraday bars need a first-session-of-week key, which the keys table does not hold. - Evidence: Unger’s overnight baseline on a Nasdaq-100 subset made 34,392 trades from 2010 to 2025, averaging 12.65 per 10,000 traded (0.13%; the article does not state whether costs are deducted), and 7,232 trades averaging 20.91 with the weak-close filter. Evidence, not a reference.
- Pinned by
TestRecipeT6OvernightHold.
ProGo lines (Williams)
Larry Williams (Trade Stocks & Commodities with the Insiders: Secrets of the COT Report, Wiley, 2005, ch. 10, pp. 108-112) separates “public buying from professional buying”: the public line is “an index of the previous close to open (+/- values) for the public and then taking a 14-day average of these values”, and the professional line does the same with “the price change from today’s open to today’s close”. He reads divergences between price and the professional line by eye. t6ProGo gives both lines in points.
pro := c.Sub(o).Average(n).SetN(n-1, math.NaN())
pub := o.Sub(c.Shift(1)).SetN(1, math.NaN()).Average(n).SetN(n, math.NaN())- Keys: none.
- First valid index: n - 1 for
proand n forpub.Averagepasses its input through before bar n - 1, sopromasks bars 0 to n - 2. The gap is NaN at bar 0, which has no previous close, so its average is NaN at bar n - 1 and passes the raw gaps through on bars 1 to n - 2; the outer mask removes them all. - Anchors:
proequalsc.QStick(o, c, n)(whose receiver is not read) to rounding from bar n - 1, and QStick passesc - othrough on the bars before. The difference scales with the price level, because QStick subtracts two rounded averages of prices: within four ulps of the largest price on the test’s fixture.pro.Add(pub)equalsc.Delta(n).Div(float64(n))to rounding from bar n: the n nights and n days telescope to the n-bar change. - Variant: the cumulative lines are
c.Sub(o).CumSum()ando.Sub(c.Shift(1)).SetN(1, 0).CumSum(); there bar 0’s gap must be 0, because a NaN would make every later sum NaN. - Evidence: the indicator panel on his silver and gold charts is labelled “ProGo Professional (14)”, and its last values times 14 are whole multiples of the price increment, which fits a simple 14-bar average in points; no divergence rule or test is printed. Evidence, not a reference.
- Pinned by
TestRecipeT6ProGo; runnable exampleExampleSeries_QStick_proGo.
Fisher Transform
Fisher
func (s Series) Fisher() Series
Fisher is the pointwise Fisher transform 0.5 ln((1+v)/(1-v)) of each value v, the transform alone: no channel normalisation and no smoothing. It expects values between -1 and 1 and clamps its input at +/-0.999, so an input beyond 0.999 in magnitude counts as 0.999 with its sign and the output lies within +/-0.5 ln(1999).
FisherTransform is the indicator that TradingView, Tulip and pandas-ta call “Fisher Transform”; their values differ from it after a flat window, and pandas-ta’s also differ at the start, since it starts its recursion a bar later. Fisher of a smoothed series is not that indicator: the indicator smooths the transformed value as well, and its clamp acts inside its recursion and at 0.99, so the clamped value feeds the next bar.
FisherInverse
func (s Series) FisherInverse() Series
FisherInverse implements the Inverse Fisher Transform, resulting in a series of output values between -1 and 1.
FisherTransform
func (s Series) FisherTransform(n int) Series
FisherTransform returns John Ehlers’s Fisher Transform indicator of the price s over an n-bar channel. On each bar t from n-1 on, it takes the highest and the lowest price, hh and ll, of the n bars ending at t and places the price x in that channel as q = (x-ll)/(hh-ll) - 0.5, a position between -0.5 and 0.5. It smooths q as v = 0.66 q + 0.67 v (Ehlers’s .33 times 2, each product rounded on its own), clamps v to 0.999 when it exceeds 0.99 and to -0.999 when it falls below -0.99, and returns the smoothed Fisher transform fish = 0.5 ln((1+v)/(1-v)) + 0.5 fish, with v and fish starting at 0. The clamp acts inside the recursion: the clamped v is the one carried to the next bar, so |v| <= 0.999 on every bar and every finite output lies within +/-0.5 ln(1999)/(1 - 0.5), about +/-7.6004.
The receiver is the price: pass the median price (high+low)/2, which Ehlers, Tulip and TradingView use, as h.Add(l).Div(2). Ehlers’s default is n = 10; TradingView and pandas-ta use 9.
A flat window (hh == ll) gives q = 0, so a constant price gives 0 on every bar, as does n = 1 on finite input. Ehlers’s code divides by zero there; Tulip uses a range of 0.001 there, which gives -0.5 in place of 0, and pandas-ta floors every range below 0.001 at 0.001; TradingView gives na on that bar and restarts both recursions from 0 on the next. pandas-ta also starts its recursion a bar later: its bar n-1 is 0 and its first computed value is at bar n.
The warm-up reads 0, not NaN: bars 0 to n-2 are 0 and the first valid index is n-1. Invalid parameters (n < 1) give aligned zeros, as does a series shorter than n.
A NaN or infinite price makes its bar and every later bar NaN, or every bar from n-1 if it falls in the warm-up. So does the first bar from n-1 on whose channel position q is not finite, which only happens for prices beyond about +/-8.9e307; a range hh-ll that overflows while x-ll stays finite gives q = -0.5, which is not a stop. Earlier bars are unchanged, and the recursion does not recover.
FisherTransform is recursive, so a slice of the history that starts later gives different values while the start-up state decays.
The trigger line of the published indicator is the previous bar’s value: for a result f, the trigger is f.Shift(1).
FisherTransform is not Fisher applied to a smoothed series. Fisher is the pointwise transform alone, clamping its input at 0.999, while here the clamp acts inside the recursion and at 0.99, and the transformed value is smoothed again.
Sources: John F. Ehlers, “Using The Fisher Transform”, Technical Analysis of Stocks & Commodities, November 2002; Tulip Indicators (fisher); pandas-ta (fisher); TradingView’s Fisher Transform.