Volatility and bands

All functions · nseries package

Sections: True Range · Volatility Estimators · Bid-Ask Spread Estimators · Ulcer Index · Bollinger Bands · Keltner Channels · Donchian Channels

True Range

ATR

func (s Series) ATR(h, l, c Series, n int) Series

ATR is an alias for AvgTrueRange.

It is a simple moving average of TrueRange, not Wilder’s smoothing, and it passes TrueRange through on bars 0 to n-2; the first full average is at bar n-1. For Wilder’s smoothing use ATRWilder. RandomWalkIndexHigh and RandomWalkIndexLow divide a k-bar range by this average over the k bars before the current one, ATR(k).Shift(1), times sqrt(k), lag by lag.

ATRWilder

func (s Series) ATRWilder(h, l, c Series, n int) Series

ATRWilder returns Wilder’s average true range over n bars from the highs h, lows l and closes c, in TA-Lib’s convention (TA_ATR; also R TTR’s ATR): the true range smoothed as in RMA, with alpha = 1/n. Bar 0 has no previous close, so its true range is left out: the seed, at bar n, is the mean of the true ranges of bars 1 to n (summed in order from 0, then divided by n), and later bars follow Wilder’s recursion. So c.ATRWilder(h, l, c, n)[1:] is c.TrueRange(h, l, c)[1:].RMA(n) and bar 0 is 0. The receiver is not used.

The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. Trim or mask the warm-up before use: a zero average true range reads as no volatility. n is in bars; n < 1 is invalid and gives aligned zeros (len(h) zeros), and n of len(h) or more gives len(h) zeros, as there is no valid index. If h, l and c are not all the same length, the result is an empty series. The result is in the units of the prices.

TradingView’s ta.atr, Tulip’s atr and pandas-ta’s atr include bar 0’s true range, h[0] - l[0], and give their first value at bar n-1; that convention is c.TrueRange(h, l, c).RMA(n). ATR and AvgTrueRange are the simple moving average of the true range over n bars (TradeStation’s convention). On the split between platforms in ATR smoothing see Katsanos, “Is The Price REIT?”, Technical Analysis of Stocks & Commodities, June 2024.

A NaN or infinite true range makes the output NaN from its bar on (from bar n if that bar is in the seed), and the recursion does not recover. A NaN high or low makes its own bar’s true range NaN, and a NaN close the next bar’s. With n = 1 the output from bar 1 is the true range with each non-finite value replaced by NaN. The method is recursive, so under a sliding slice of history its value depends on where the slice starts; the seed’s weight decays by the factor 1 - 1/n per bar. Sources for the smoothing: Wilder, New Concepts in Technical Trading Systems (1978), through Achelis, Technical Analysis from A to Z (whose ATR example includes bar 0, the TradingView convention); for the convention here, TA-Lib TA_ATR. BreakoutModeIndex takes ATRWilder(10) as its volatility. TrueHigh and TrueLow are the two ends of each bar’s true range.

AvgTrueRange

func (s Series) AvgTrueRange(h, l, c Series, n int) Series

AvgTrueRange computes the average true range over n bars: the simple moving average of TrueRange over n bars. During the warm-up (indices below n-1) it passes the true range through unchanged, as Average does, and for n < 1 or n > len(h) it returns the true range unchanged. It is not Wilder’s exponentially smoothed ATR, which TA-Lib, Tulip, pandas-ta, TradingView, MetaStock and AmiBroker use: for that, use ATRWilder (TA-Lib’s convention) or c.TrueRange(h, l, c).RMA(n) (TradingView’s).

The first full average is at bar n-1; bars 0 to n-2 pass TrueRange through, as the simple average does.

MassIndex

func (s Series) MassIndex(h, l Series, e, n int) Series

MassIndex returns Dorsey’s Mass Index in StockCharts’ convention: the n-bar sum of the e-bar exponential average of the range over that average’s own e-bar exponential average.

The receiver is not used. With e1 := h.Sub(l).XAverage(e), the e-bar XAverage of the range h less l, seeded at bar e-1 with the simple average of the first e ranges, and e2 := e1.Right(len(e1) - (e - 1)).XAverage(e), the same average of e1 from bar e-1 on, seeded at bar 2(e-1), the result for valid parameters and equal lengths is e1.Div(e2).Sum(n).SetN(2*(e-1)+n-1, 0), bit for bit. The ratio pairs e1 and e2 by right alignment (bar t reads e1[t] over e2[t-(e-1)]), and a zero e2 gives a zero ratio (Div’s zero rule).

This is the convention of StockCharts ChartSchool’s “Mass Index” and its spreadsheet cs-massindex.xls, whose averages start from simple averages; Dorsey’s “The Mass Index” (Technical Analysis of Stocks & Commodities, June 1992) seeds with the previous value (his sidebar states it for the average of the close, and the range’s averages follow by analogy), and the two converge as the seeds decay. Dorsey’s settings are e = 9 and n = 25, with the first valid index at bar 40. The ratio stays near 1, so the index hovers near n.

Dorsey’s reversal bulge, the index rising above 27 and then falling below 26.5, is the latch mi.Hysteresis(27, 26.5), with mi the index and its warm-up set to NaN, and the bulge completes on the bars of mi.Hysteresis(27, 26.5).Delta(1).LT(-1); these and the related recipes are compositions under “True Range” in the manual. On StockCharts’ 504-row sheet, MassIndex(h, l, 9, 25) matches the printed index to within 1e-12 and the bulges complete at bars 119 and 265.

A constant nonzero range gives n from the first valid index, exactly for a power-of-two range and to rounding otherwise, and with e = 1 every ratio is 1 where the range is not 0 and 0 where it is, so the index counts the nonzero ranges among the last n bars. The index is scale invariant: scaling h and l by a power of two changes no bit, and by another factor only the rounding, while the ranges and the averages’ products stay in float64’s normal range. Near the smallest subnormal the products underflow: five ranges of 5e-324 give MassIndex(h, l, 3, 1) = 0 at bar 4, where twice those ranges give 1.

The warm-up reads 0, not NaN: bars 0 to 2(e-1)+n-2 are 0 and the first valid index is 2(e-1)+n-1. Validation: e < 1 or n < 1 gives len(h) zeros, as does e or n above len(h), where every bar would be warm-up; len(h) != len(l) gives Series{}, checked first, since h and l are columns of one bar table, not right-aligned; and an empty series gives an empty result.

A NaN or infinite high or low at bar p, or a finite high and low whose difference overflows there, makes every bar from max(p, 2(e-1)+n-1) on NaN, since the averages do not recover; earlier bars are unchanged. MassIndex is causal and recursive, so start-dependent: the index on StockCharts’ 50-row sheet differs by up to 0.0222 from the index on its 504-row sheet over the same bars, the last 50. XAverage’s multiply-add is fused by the compiler on arm64, so the last bits can differ between arm64 and amd64. It costs O(1) a bar and returns a new Series without modifying its inputs.

NATR

func (s Series) NATR(h, l, c Series, n int) Series

NATR returns the normalised average true range, ATRWilder(h, l, c, n) as a percentage of the close: ATRWilder(h, l, c, n)[t] / c[t] * 100. It is TA-Lib’s TA_NATR; the division comes first, in TA-Lib’s order. The unit is percent: 1.5 means the average true range is 1.5% of the close. The smoothing constant is 1/n and the seed is ATRWilder’s, at bar n: the mean of the true ranges of bars 1 to n. The receiver is not used.

TradingView’s ta.atr-based form and Tulip’s natr include bar 0’s true range and start at n-1: c.TrueRange(h, l, c).RMA(n).Div(c).Mul(100) (Tulip multiplies by 100 first, which agrees to rounding). They differ from NATR in the seed, not only in the order of the operations; that form and the simple average of TradeStation are compositions under “True Range” in the manual.

The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. 0% reads as no volatility, so trim or mask the warm-up before use (for example SetN(n, math.NaN())). n < 1 is invalid and gives aligned zeros (len(h) zeros), and n of len(h) or more gives len(h) zeros; if h, l and c are not all the same length, the result is an empty series.

A close that is 0, NaN or infinite gives NaN on that bar only (TA-Lib gives 0 for a zero close), and a NaN average true range (see ATRWilder) gives NaN. A result that overflows is left infinite. The method is recursive through ATRWilder, so under a sliding slice of history its value depends on where the slice starts; the seed’s weight decays by the factor 1 - 1/n per bar.

TrueHigh

func (s Series) TrueHigh(c Series) Series

TrueHigh returns each bar’s true high, the greater of the receiver’s high and the previous close, with bar 0’s own high.

The receiver is the highs h and the argument c the closes. Bar 0 has no previous close, so it holds h[0] verbatim (NaN, +Inf, -Inf and -0 included). From bar 1 the true high is max(h[t], c[t-1]) with Go’s builtin max: a NaN high or a NaN previous close gives NaN, even beside +Inf; +0 is taken over -0; and the infinities are otherwise ordinary values. Highs below lows are not validated. These are Wilder’s true high and true low, and DeMark’s TDST levels (DeMarkSetupResistance and DeMarkSetupSupport) are built on them, as are the TD risk levels and the countdown cancellations of the manual’s “DeMark Indicators” section.

h.TrueHigh(c).Sub(l.TrueLow(c)) equals c.TrueRange(h, l, c) bit for bit on every bar, bar 0 included, where both are h[0] - l[0]. If c does not have the receiver’s length, the result is Series{}; an empty receiver with an empty c gives an empty, non-nil Series. There is no warm-up: the first valid index is 0. The result is a new Series that shares no storage with the inputs, no input is modified (even when c is the receiver), and the method allocates only the result.

TrueLow

func (s Series) TrueLow(c Series) Series

TrueLow returns each bar’s true low, the lesser of the receiver’s low and the previous close, with bar 0’s own low.

The receiver is the lows l and the argument c the closes. Bar 0 has no previous close, so it holds l[0] verbatim (NaN, +Inf, -Inf and -0 included). From bar 1 the true low is min(l[t], c[t-1]) with Go’s builtin min: a NaN low or a NaN previous close gives NaN, even beside -Inf; -0 is taken over +0; and the infinities are otherwise ordinary values. Lows above highs are not validated. These are Wilder’s true high and true low, and DeMark’s TDST levels (DeMarkSetupResistance and DeMarkSetupSupport) are built on them, as are the TD risk levels and the countdown cancellations of the manual’s “DeMark Indicators” section.

h.TrueHigh(c).Sub(l.TrueLow(c)) equals c.TrueRange(h, l, c) bit for bit on every bar, bar 0 included, where both are h[0] - l[0]. If c does not have the receiver’s length, the result is Series{}; an empty receiver with an empty c gives an empty, non-nil Series. There is no warm-up: the first valid index is 0. The result is a new Series that shares no storage with the inputs, no input is modified (even when c is the receiver), and the method allocates only the result.

TrueRange

func (s Series) TrueRange(h, l, c Series) Series

TrueRange computes the true range of each bar from the highs h, lows l and closes c. Bar 0 has no previous close, so its true range is the bar’s own high minus its low, h[0] - l[0], as in Wilder, TradingView and pandas-ta. From bar 1 it is max(previous close, high) minus min(previous close, low). If h, l and c are not all the same length, the result is an empty series.

Compositions

The recipe below reads daily highs, lows and closes and does not read the open o: its entries and exits act on the bar after their signal, and a caller who fills them at the open anchors the trade views on o. Prices are positive and finite, lengths are in bars and series are paired by position, so the caller aligns them by timestamp first. Recipe values are NaN before their first valid index (SetN(F, math.NaN())), and flags and latches read 0 there: before its first full window XAverage passes the close through, so the unmasked MACD reads 0 and then the 40-bar average less the close, and Highest runs over expanding windows, so an unmasked comparison would read a warm-up as a decision, and every shifted flag reads its +0 pad as no signal. An exponential average depends on the history it starts from, so flags built on one agree with a longer history only after a burn-in (the Donchian Channels page t6-breakout-systems.md gives the lengths), and Go may fuse the average’s multiply-add on arm64: a ratio built on it then agrees across architectures to rounding, and the flags agree wherever they are decided away from ties. 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.

Volatility-normalised pullback in an EMA trend (Clenow)

Andreas F. Clenow (Following the Trend, 2nd ed., Wiley, 2022, ch. 7, pp. 235-241) trades long when “the 40-day exponential moving average is above the 80-day exponential moving average” and the price “falls back three times its average true range from the highest closing price in the past 20 days”; if the trend “turns bearish, we exit on the following day”, and otherwise “we hold the position for 20 trading days”. His average true range is an exponential average of the true range (“I use an exponential moving average”, ch. 3, pp. 51-52), and Figure 7.1 divides “the difference between the close price and the higher close in the past 20 days” by it: “If this dips below 3, we go long”, the sign lost, since the ratio is at most 0. t6ATRPullback returns that ratio pb, the entry flag and the book filled on the next bar; his trend model is on the Donchian Channels page t6-breakout-systems.md (“Clenow’s core trend model”).

m := c.MACD(fast, slow).SetN(slow-1, math.NaN())
atr := c.TrueRange(h, l, c).XAverage(n).SetN(n-1, math.NaN())
pb := c.Sub(c.Highest(n).SetN(n-1, math.NaN())).Div(atr)
entry := pb.LT(-k).Mul(m.GT(0))
pos := c.Position(entry.Shift(1), m.LT(0).Shift(1), hold)
  • Inputs: daily highs, lows and closes (the true range); n 20 (the window of the highest close and the length of the true-range average), fast 40, slow 80, k 3 and hold 20. pb is in units of the average true range and at most 0, since today’s close is in its window. His sizing (“Position size is volatility parity, based on average true range”) is outside the recipe.
  • First valid index: n - 1 (19) for pb, NaN before; slow - 1 (79) for entry, 0 before; pos reads 0 until the bar after the first entry, bar 80 at the earliest. On the test’s 700-bar path pb[19] is -0.86021505376344087 (the seed bar, where the exponential average is still the simple mean of the first 20 true ranges), the entry first fires on bars 132 to 136 and 179, and the trades start on bars 133, 180, 234, 281, 344, 410 and 439, five of the seven ending on the hold.
  • Anchors: the unmasked c.MACD(40, 80).GT(0) equals c.XAverage(40).GT(c.XAverage(80)) bit for bit on every bar, so m.GT(0) equals the pair of averages from bar 79; unmasked, MACD is exactly 0 on bars 0 to 38 and exactly XAverage(40) - c on bars 39 to 78, where the 80-bar average passes the close through and the pair reads “the 40-bar average above the close”, not a trend. pos equals bit for bit a loop of the rules as his 2019 code runs them (enter the day after a signal; leave the day after a bearish close, or on the 20th day after the fill), with 39 trades on a 3,000-bar synthetic path. The average’s multiply-add may fuse on arm64, so pb agrees across architectures only to rounding (by at most 4.4e-15 on the test path), while entry and pos agree bit for bit: on the test data |m| is at least 0.0203565 and pb at least 0.0100334 from -3, far from a tie.
  • Traps: the dossier’s c.ATR(h, l, c, 20) is the simple average of the true range and ATRWilder Wilder’s smoothing; neither is his exponential average, though on the 700-bar fixture group the three give 119 to 121 pullback bars (pinned: within 2 of each other), so the choice among them barely matters. The dossier’s pb.LTE(-3) is inclusive where his “dips below” and the 2019 code’s < are strict, and its trend flag, unmasked, compares the 40-bar average with the close on bars 39 to 78. His chapter also says “we will use a 40-day standard deviation of price changes both for position-sizing purposes and for pullback”, while the rule list and Figure 7.1 use the average true range. This edition prints no Python: the code the dossier cites is Trading Evolved (2019), ch. 17, which divides by a standard deviation (the variant) and takes its averages with pandas ewm(span) over a 250-bar history, which XAverage with its simple-mean seed does not reproduce. The rule list omits the 20-day time exit, which is Position’s hold, not since-entry state; the hold counts from the fill bar, the bar after the signal, so the book is long for 20 bars and exits on the 20th bar after the fill, 21 after the signal.
  • Variant: the 2019 code’s unit, the sample standard deviation of the last 40 daily changes, sd := c.Delta().StdDevSample(40).SetN(40, math.NaN()) with pb := c.Sub(c.Highest(20)).Div(sd), F 40 (the first window of 40 changes is bars 1 to 40): a different rule, with 142 pullback bars on the fixture group against 119 to 121 (pinned: at least 15 more).
  • Evidence: Table 7.1 (futures, about 2002 to 2021, “we have loaded trading costs on the strategy”) gives 19.7% a year against 8.3% for the MSCI World total return index, a maximum drawdown of -43.0% against -56.2%, a Sharpe ratio of 0.83 against 0.58 and 60.8% of months profitable; Table 9.2’s counter-trend row (14.7% a year, Sharpe ratio 0.89) is a differently scaled run, and the text quotes its Sharpe ratio beside Table 7.1’s figures. One author, one period, no hold-out. Evidence, not a reference.
  • Pinned by TestRecipeT6ATRPullback; runnable example ExampleSeries_TrueRange_atrPullback.

The range-contraction recipes below compare a range, h - l, an n-bar high-low range, a true range or its average, with a reference range. The printed decisions are products, as Range_<ConsolidationFactor*Range_[Length], so a zero reference cannot pass; the plotted ratios are quotients, and Div reads 0 on a zero reference, which a quotient test would take for a deep contraction: a plot, not a decision. TrueRange[0] is h[0] - l[0], so ATR’s first full window is at n - 1, the convention of t6NATRForms on t6-range-indicators.md (TradeStation’s ATR starts a bar later). Values and levels are NaN before their first valid index and the flags and books built on them read 0 there; prices are positive and finite, lengths are in bars and the factors are unitless. The books fill on the close, as the sources’ code does; resting stop entries (Conway and Behle’s, Unger’s) need the intrabar order and are outside the package. Unlike D’Errico’s VoltyRatio (trade-state/t2-latched-entries.md), whose short range is nested in the long one, these compare disjoint windows (a ratio can exceed 1) or a bar with a band, decide by products and have their own levels and exits; Kaufman’s annualised-volatility gates are on volatility-estimators/t6-volatility-gates.md and Crabel’s narrow-range bars on bar-classification/t6-narrow-range-bars.md.

Consolidation factor (D’Errico and Zaffino)

Domenico D’Errico and Antonio Zaffino (“Portfolio Strategy Based On Accumulation/Distribution”, Stocks & Commodities V36:9, August 2018, pp. 8-15 and 29) test “Last four-week range / Previous four-week range < ConsolidationFactor” and “ATR of last four weeks / ATR of previous four weeks < ConsolidationFactor” on weekly bars, where “The consolidation factor is a number between zero and 1”. Their EasyLanguage decides by a product, Range_<ConsolidationFactor*Range_[Length], and plots the quotient only if Range_[Length]>0. t6ConsolidationFactor gives both decisions and both quotients.

r := h.Highest(n).Sub(l.Lowest(n))
ref := r.Shift(n).SetN(n+n-1, math.NaN())
consol := r.LT(ref.Mul(f))
ratio := r.Div(ref)
a := c.ATR(h, l, c, n)
aref := a.Shift(n).SetN(n+n-1, math.NaN())
consolATR := a.LT(aref.Mul(f))
atrRatio := a.Div(aref)
  • Inputs: highs, lows, closes (weekly bars in the source); n 4; f 0.75. The reference is the adjacent, earlier n bars.
  • First valid index: n + n - 1 for all four (7 at n = 4; NaN before for the quotients, 0 for the flags); with TradeStation’s ATR start the ATR forms take SetN(n+n, math.NaN()). Unmasked, bars n to 2n - 2 compare with a partial (expanding) window and can fire.
  • Anchors: consol equals the EasyLanguage decision as a loop bit for bit, and consolATR the same on the ATR. On a zero reference (a flat stretch) consol is 0 while ratio reads 0 and ratio.LT(f) reads 1.
  • Traps: the quotient is a plot, not the recipe: the dossier’s r.Div(r.Shift(n)).SetN(2*n-1, math.NaN()).LT(0.75) passes on a flat reference. Adding ref.GT(0).Sub(1).Sqrt() to a quotient gives NaN where the reference is 0 (the square root of -1) and the quotient elsewhere, so it draws nothing where TradeStation draws nothing. The Tips’ AIQ and Excel versions smooth the true range Wilder’s way, and Wealth-Lab’s ATR_[Length] reads a fixed bar. D’Errico’s ADX variant (“If ADX is below 30 for the last four weeks”) is on the Average Directional Index page.
  • Evidence: Figure 9 (p. 12; S&P 100 stocks, weekly bars, January 2003 to March 2018, 10 dollars a trade and 10 cents a share): the range-compression breakout made 507,593 dollars on 1,244 trades with a 92,387-dollar drawdown, the ATR-compression breakout 851,530 dollars on 1,316 trades; there is no unfiltered baseline. Evidence, not a reference.
  • Pinned by TestRecipeT6ConsolidationFactor; runnable example ExampleSeries_Highest_consolidationFactor.

Consolidation breakout (D’Errico and Zaffino)

Their strategy (p. 11) buys on “Price consolidation / A higher 12-week low / Price breaks above the top of the consolidation channel” and sells “if price goes below the bottom of consolidation channel”. The EasyLanguage sets Top=highest(high,Length) and Bot=Lowest(Low,Length) on each consolidation bar and then tests close > Top and Bot>Bot[12] and average(volume,VolAvg)[VolDelay]>VolRatio*average(volume,VolAvg)[VolAvg+VolDelay], buying and selling “this bar close”. t6ConsolidationBreakout gives the held levels and the long book.

r := h.Highest(n).Sub(l.Lowest(n))
consol := r.LT(r.Shift(n).SetN(n+n-1, math.NaN()).Mul(f))
top := h.Highest(n).ValueWhen(consol)
bot := l.Lowest(n).ValueWhen(consol)
vAvg := v.Average(va)
volOK := vAvg.Shift(delay).GT(vAvg.Shift(va+delay).SetN(va+va+delay-1, math.NaN()).Mul(vr))
buy := c.GT(top).Mul(bot.GT(bot.Shift(12).SetN(12, math.NaN()))).Mul(volOK)
pos := c.Position(buy, c.LT(bot))
  • Inputs: highs, lows, closes, volumes (weekly bars); n 4, f 0.75; va 4, delay 4 and vr 1, the code’s VolAvg, VolDelay and VolRatio.
  • First valid index: the first consolidation bar for top and bot (data-dependent, at least 2n - 1; NaN before); pos from bar 0, with no buy before 2va + delay - 1 (11), where the volume rule is first valid.
  • Anchors: pos equals a MarketPosition loop of the strategy with NaN-initialised levels bit for bit (levels updated before the test, fills on the close); a consolidation bar cannot fire, since its close lies inside its own top.
  • Traps: a departure: EasyLanguage starts Bot at 0, so Bot>Bot[12] passes for the 12 bars after the first consolidation, whereas here the levels are NaN until the first consolidation and the higher-low test is 0 until a bottom 12 bars back exists (171 of 300 random 120-bar walks differ there, and nowhere else; a probe figure, not pinned). “A higher 12-week low” is the held bottom above its value 12 bars ago, not a rising 12-week lowest low. The volume rule compares bars t-7 to t-4 with bars t-11 to t-8, not the current four weeks. The position size, (10000+netprofit)/close, and the start date are outside a series recipe.
  • Evidence: Figure 9’s range-compression breakout (above); AIQ’s own run in the Traders’ Tips (Nasdaq 100, 2006 to 2018, its own exits) made 430 trades, 312 of them winners. Evidence, not a reference.
  • Pinned by TestRecipeT6ConsolidationBreakout.

Low- and high-volatility filters (Unger; Beatson)

Andrea Unger (“Pivot Points Reimagined: A Systematic Approach Across Futures Markets”, Stocks & Commodities V44:1, January 2026, pp. 26-31 and 36) codes Low_Vol=Truerange data2<ATR*coeff_ATR and High_Vol=Truerange data2>ATR*coeff_ATR with ATR = averagetruerange(len)data2 on daily bars. His “three consecutive bars of decreasing ranges” (“The Power Of Volatility Contraction Patterns In Breakout Trading Strategies”, V42:6, May 2024, pp. 16-19 and 51) is (highS(1)-lowS(1))< (highS(2)-lowS(2)) and (highS(2)-lowS(2)) < (highS(3)-lowS(3)), his trend compression (“The Trend Compression Pattern”, V42:2, February 2024, pp. 26-29 and 35) “the range of the last session is less than one-third of the previous two ranges combined”, and Stephen Beatson (“Identifying Short-Term Bottoms In Bull Markets”, V34:5, April 2016, pp. 16-20) compares the day’s range with yesterday’s. t6UngerFilters gives the five flags.

tr := c.TrueRange(h, l, c)
atr := c.ATR(h, l, c, k).SetN(k-1, math.NaN())
lowVol := tr.LT(atr.Mul(coeff))
highVol := tr.GT(atr.Mul(coeff))
rg := h.Sub(l)
shrink3 := rg.ConsecDown().GTE(2)
dc3 := rg.LT(rg.Shift(1).Add(rg.Shift(2)).Div(3).SetN(2, math.NaN()))
smaller := rg.LT(rg.Shift(1))
  • Inputs: highs, lows, closes (daily bars); k and coeff, (8, 0.9) for Low_Vol and (5, 1) for High_Vol as published.
  • First valid index: k - 1 for lowVol and highVol (k with MultiCharts’ ATR start), 2 for shrink3 and dc3, 1 for smaller; 0 before (only dc3 needs its mask).
  • Anchors: ConsecDown counts strictly smaller ranges and resets on a tie or a NaN: the ranges [5 4 3 NaN 2 1 0.5] give shrink3 [0 0 1 0 0 0 1], dc3 [0 0 0 0 0 0 1] and smaller [0 1 1 0 0 1 1]; Unger’s pattern is two decreases in a row, hence GTE(2). With coeff 1, highVol equals tr.GT(tr.Average(k-1).Shift(1)) in exact arithmetic (to rounding in floats; bit for bit on the test path’s 1/64 grid).
  • Traps: the window includes the bar’s own true range, and the products are strict, so a tie is neither. The dossier’s quotient tr.Div(tr.Average(8)).SetN(8, math.NaN()).LT(0.9) passes on a zero ATR, where the printed product reads 0 < 0. Unmasked, dc3 would compare bar 1 with (rg[0] + 0)/3. On daily bars the flags gate the next bar (Shift(1)); holding a daily value on intraday bars is the missing piece recorded on the Average Directional Index page.
  • Evidence: Unger’s Figures 3 and 4 are optimisation grids, so the published settings are fitted, and Figure 5’s trigger-1 net profit is a misprint (Figure 3’s row gives the printed average trade); Beatson’s table: 22 entries after a smaller range, 20 of them winners, against 30 and 26 after a greater one. Evidence, not a reference.
  • Pinned by TestRecipeT6UngerFilters.

Relative-volatility band (Kaufman)

Kaufman (“Revisiting The 3-Day Trade”, Stocks & Commodities V43:3, March 2025, pp. 18-23) uses “a ratio defined as today’s true range divided by the N-day average true range; therefore, it is relative volatility”, coded if ATR[1] <> 0 then volratio = truerange/ATR[1] and tested strictly between a minimum and a maximum; “The best value for the 3x2 test was a 15-day period, a minimum of 0.25 and a maximum of 3.25”. t6TRBand gives the ratio, held where the prior ATR is 0 as the listing holds it, and the entry filter.

tr := c.TrueRange(h, l, c)
atrP := c.ATR(h, l, c, n).Shift(1)
vr := tr.Div(atrP).ValueWhen(atrP.GT(0)).SetN(n, math.NaN())
ok := vr.GT(lo).Mul(vr.LT(hi))
  • Inputs: highs, lows, closes; n 15, lo 0.25, hi 3.25.
  • First valid index: n when the prior ATR at n is positive, otherwise the first bar after it with a positive prior ATR (NaN before); ok is 0 before.
  • Anchors: ValueWhen(atrP.GT(0)) can only hold a 0 or a NaN: a zero prior ATR forces a zero prior true range, so the last computed ratio was 0. So vr equals the plain quotient tr.Div(atrP).SetN(n, math.NaN()) except on a series whose first n bars are flat, where it reads NaN, not 0, until the first positive prior ATR.
  • Traps: the text’s “the N-day average true range” omits the lag of the code’s ATR[1]. The band is an entry filter (the listing computes it in the long-entry block), not an exit. The dossier’s tr.Div(tr.Average(15).Shift(1)) is unmasked: bar 0 reads 0 and bars 1 to 14 read TR[t]/TR[t-1].
  • Variant: Kaufman’s exit in “Exiting The Trade: How To Avoid Being The Greater Fool” (Stocks & Commodities V44:7, July 2026, p. 20), the 20-bar ATR above 6 times its value 60 bars back: with a := c.ATR(h, l, c, 20), a.GT(a.Shift(60).Mul(6)).SetN(79, 0) (the figures and “I’ve used 60-day”; the text prints “ATR/ATR[20]” once).
  • Evidence: Figures 4 and 5 (p. 20): adding the band to the 3x2 trade with an 80-day trend cut the trades from 390 to 379 and the maximum drawdown from 1,144 to 572 dollars. Evidence, not a reference.
  • Pinned by TestRecipeT6TRBand.

Tight trading range (Saitta)

Alex Saitta (“Range Breakout Trading In Treasury Bonds”, Stocks & Commodities V13:4, April 1995, pp. 149-152): “a tight and narrow trading range occurs when the number of price ticks where the market did not trade is less than 200% of the height of the range”, over five days; “The parameters of this trading range remain fixed until a breakout occurs” (Figure 1); the entry needs a day whose range “expands to more than 125% of the average daily price range of the last eight days”; a long is held “until the market closed below the eight-day moving average of the lows”, a short “until the market closed above the 10-day moving average of the highs”. The untraded ticks are five heights less the sum of the ranges (TechniFilter’s rewrite in the Traders’ Tips of the issue), so the test is “the five ranges sum to more than three heights”, exact on a tick grid. “Remain fixed” has two readings, and the article does not say which: the latest detection’s levels, as Behold! and TechniFilter code them, and the first detection’s, held until a breakout. t6SaittaHeld gives the detection, the latest detection’s levels and the two books on them, and the second member, the first detection’s pair held on PositionSignedRules: a long trade entered on a detection, with the five-bar low as its stop and the five-bar high as its target, both sampled on the detection bar, frozen and read strictly on the close, and EntryValue reading the pair back.

rg := h.Sub(l)
hh := h.Highest(5)
ll := l.Lowest(5)
tight := rg.Sum(5).GT(hh.Sub(ll).SetN(4, math.NaN()).Mul(3))
expand := rg.GT(rg.Average(8).Shift(1).SetN(8, math.NaN()).Mul(1.25))
hi := hh.ValueWhen(tight)
lo := ll.ValueWhen(tight)
up := c.GT(hi)
dn := c.LT(lo)
armed := tight.Mul(2).Sub(up.Add(dn).Mul(expand).Shift(1)).Hysteresis(0, 0).GT(0)
long := c.Position(armed.Mul(up).Mul(expand), c.LT(l.Average(8)))
short := c.Position(armed.Mul(dn).Mul(expand), c.GT(h.Average(10)))
held := c.PositionSignedRules(tight, nil, nil, nil, PositionRules{
    LongStop:   PositionLevel{Level: ll},
    LongTarget: PositionLevel{Level: hh},
})
hiHeld, loHeld := hh.EntryValue(held), ll.EntryValue(held)
upHeld := held.Exits().Mul(c.GT(hiHeld.Shift(1)))
dnHeld := held.Exits().Mul(c.LT(loHeld.Shift(1)))
  • Inputs: highs, lows, closes (daily Treasury bond futures in 32nds in the source).
  • First valid index: 4 for tight (0 before; unmasked, four bars spanning the whole band fire at bar 3), the first detection for hi and lo (NaN before), 8 for the expansion; long and short from bar 0; held reads 0 until the first detection, upHeld and dnHeld until the first release, and hiHeld and loHeld are NaN while the pair is not held.
  • Anchors: Figure 1 read by its X marks, h = 103 + {38, 36, 40, 36, 37}/32 and l = 103 + {3, 0, 2, 2, 2}/32 (exact in binary): the ranges sum to 178 ticks and the height is 40, so 22 ticks are untraded, fewer than 80, and the range is tight at bar 4 with hi 104.25 and lo 103; the bar ends as drawn ({38, 35, 40, 35, 36} and {4, 0, 3, 3, 3}) give 29, also tight. The first five outputs equal a loop of the latest-detection reading with its armed latch, and the held pair a loop of the first-detection reading (held from the first detection with that bar’s five-bar high and low, released by a close strictly below the low, read first, or strictly above the high, detections ignored while held and on the release bar, the next detection on a later bar sampling anew), bit for bit on the six fixture groups, where the pair is released 84 times.
  • Traps: the reading: the levels are the latest detection’s (as Behold! and TechniFilter code them), armed by a detection and spent by the first qualifying breakout; the latch’s input reads 2 on a detection, 1 on a detection the bar after a qualifying breakout (it wins either way), -1 on the bar after a qualifying breakout and 0 otherwise (it holds). The article says “more than 125%” four times and “equal to or greater than 125%” once: strict here, so a range of exactly 1.25 times the mean does not qualify. The exits use the current bar’s averages (Behold!; TechniFilter compares with yesterday’s). Without gaps, c.Chop(h, l, c, 5).GT(68.26061944859854) (100 log 3 / log 5, written as a number) is the same test from bar 5; with gaps it is a necessary condition only.
  • Variant: the Tips’ levels without the latch, c.Position(c.GT(hi).Mul(expand), c.LT(l.Average(8))), enter again on later closes outside a held range (54 long entries against 28 on the fixture’s path).
  • Evidence: Figure 4 (one bond contract, 1984 to 1994, no costs): 34 trades, 53% winners, an average of 636 dollars a trade. Evidence, not a reference.
  • Pinned by TestRecipeT6SaittaRange; runnable example ExampleSeries_PositionSignedRules_saittaHeld.

Rectangle (Conway and Behle)

Conway and Behle (Professional Stock Trading, ch. 5, section 5.1, pp. 106-108, and the Acme R system, pp. 109-111): “In order to qualify as a rectangle, the aspect ratio must be less than or equal to a maximum aspect ratio. Further, the height of the rectangle must be less than a specified multiple of the ATR, or RangeFactor”. Their function AcmeRectangular (Example 5.1) codes it as If RectangleHeight > 0 and RangeHeight > 0 Then, and then tests the ratio RectangleHeight / RangeHeight against the system’s maximum-ratio input and RectangleHeight <= RangeFactor * ATR, with ATR = Average(Range, VLength) and VLength(30). t6Rectangle is the code.

ht := h.Highest(rl).Sub(l.Lowest(rl))
ref := h.Highest(rr).Shift(rl).Sub(l.Lowest(rr).Shift(rl)).SetN(rl+rr-1, math.NaN())
atr := h.Sub(l).Average(v).SetN(v-1, math.NaN())
rect := ht.Div(ref).LTE(ratio).Mul(ht.LTE(atr.Mul(factor))).Mul(ht.GT(0)).Mul(ref.GT(0))
  • Inputs: highs and lows; rl 4, rr 12, v 30, ratio 0.3, factor 1.0. The reference is the rr bars before the rectangle.
  • First valid index: max(rl + rr - 1, v - 1), 29 with the defaults (0 before).
  • Anchors: their worked numbers (pp. 106-107: height 2, reference 8, aspect 0.25, an ATR of 1.9 fails and 2.1 passes) on constructed 30-bar series: a mean range of 1.8999999999999999 (57/30) gives 0, one of 2.1000000000000001 (63/30) gives 1 at bar 29, the first; both ATR readings agree on these bars, which have no gaps.
  • Traps: the code’s “ATR” is Average(Range, 30), the mean of h - l, while the Calculations box says “Average True Range” over 30 bars; the prose’s “(< 0.3)” and “less than” against the code’s <=. The masks matter: a path whose partial windows qualify fires at bar 7 unmasked. The dossier’s rectangle takes c.ATR(h, l, c, 14), with no guards and no masks. The entry levels and Example 5.3’s Volatility(20) offsets are the system’s. Jaekle and Tomasini’s triangle (Trading Systems, 2nd ed., 2019, appendix 2) is related: a setup when “The volatility indicator of the last 300 bars has dropped to its lowest point” and “The moving average of the last 200 closing prices is moving nearly horizontal”, with the code withheld (“we do not disclose this code but just describe its logic”) and the volatility measure, the slope tolerance and the entry offset unstated.
  • Variant: the Calculations box’s ATR30, c.ATR(h, l, c, v).SetN(v-1, math.NaN()) in place of the mean range.
  • Evidence: chart examples only (Figures 5.1 to 5.5); no statistics for the rectangle alone. Evidence, not a reference.
  • Pinned by TestRecipeT6Rectangle.

Dow line (Rhea; Russell)

Robert Rhea (The Dow Theory, 1932, ch. II): “A"line” is a price movement extending two to three weeks or longer, during which period the price variation of both averages move within a range of approximately five per cent. … Simultaneous advances above the limits of the “line” indicate accumulation and predict higher prices; conversely, simultaneous declines below the “line” imply distribution". Richard Russell (The Dow Theory Today, 1961, ch. IV) adds that a line “must last at least 10 trading days to qualify as a line” and that “it is not necessary for both averages to penetrate a line on the same day”. t6DowLine gives the bars on which both averages are in a line and the two confirmations.

lineA := ca.Highest(n).LTE(ca.Lowest(n).SetN(n-1, math.NaN()).Mul(1 + b))
lineB := cb.Highest(n).LTE(cb.Lowest(n).SetN(n-1, math.NaN()).Mul(1 + b))
both := lineA.Mul(lineB)
age := both.BarsSince()
upA := ca.GT(ca.Highest(n).ValueWhen(both)).BarsSince().LT(age)
upB := cb.GT(cb.Highest(n).ValueWhen(both)).BarsSince().LT(age)
dnA := ca.LT(ca.Lowest(n).ValueWhen(both)).BarsSince().LT(age)
dnB := cb.LT(cb.Lowest(n).ValueWhen(both)).BarsSince().LT(age)
accum := upA.Mul(upB)
distr := dnA.Mul(dnB)
  • Inputs: two averages of closes, ca and cb, of equal length on the same dates (right-aligned; a shorter one reads 0 before its overlap); n 10, b 0.05.
  • First valid index: n - 1 for both (0 before; unmasked, bar 0 is a one-bar line); accum and distr are 0 before the first line, where BarsSince is NaN.
  • Anchors: both, accum and distr equal a loop bit for bit on two test paths, and accum and distr are 0 on every line bar. The band is in product form, HH <= (1 + b) LL: closes 100 and 105 are a line (105 <= 105), while the quotient form c.Highest(n).Div(c.Lowest(n)).Sub(1).LTE(0.05) is not (105/100 - 1 is 0.050000000000000003).
  • Traps: Rhea: “There are people who insist on trying to place an exact mathematical interpretation on the duration or height of a line, but this cannot be done successfully.” The divisor (the low, the mean or the high), the band and the length are readings; Hamilton’s own example (“the industrials do not sell below seventy or above seventy-four”) is a band of 5.7%. Both averages must be in a line on the same bar; the breakouts need not fall on one day.
  • Variant: the dossier’s breakout ca.Breakout(n, 1) is the first close above a line only if the line held through the bar before, ca.Breakout(n, 1).GT(0).Mul(lineA.Shift(1)), and for a longer line it reads only the line’s last n days.
  • Evidence: Rhea found that “the terminating valley of seven of these nine bear markets contained a period of from 60 to 90 days when each average moved in a line within a range of about three per cent of a mean price”. Evidence, not a reference.
  • Pinned by TestRecipeT6DowLine.

The range compositions below mask their values with NaN before their first valid index (SetN(F, math.NaN())), where the methods they call read +0 or pass their input through: each value feeds a comparison or a latch, and a +0 would read as a decision. Flags and latches built on masked values read 0 there. Prices are positive and finite, lengths are in bars, and thresholds are in the units of the series they test. The simple ATR is c.TrueRange(h, l, c).Average(n), first full at n - 1. The range-contraction tests (D’Errico and Zaffino’s consolidation factor, Unger’s filters and Kaufman’s relative-volatility band, a true range over the ATR) are on t6-range-contraction.md, Clenow’s pullback in units of c.TrueRange(h, l, c).XAverage(n), the exponential average of pandas-ta’s variant below, on t6-atr-pullback.md, and the breakout levels off the open on t6-volatility-breakout-levels.md; Crabel’s narrow-range bars, which compare ranges without an average, are under “Bar Classification”.

Normalised ATR conventions (Forman; TA-Lib, TradingView and Tulip, TradeStation)

John Forman (“Cross-Market Evaluations With Normalized Average True Range”, Stocks & Commodities V24:5, May 2006, pp. 60-63) normalises the average true range “by dividing the ATR by some current price measure such as the most recent close or a moving average”. His prose says the average “is similar to that of the standard simple moving average”, while his only formula, MetaStock’s (ATR(14)/CLOSE)*100, is Wilder’s average. Platforms differ in the seed as well as the smoothing, so t6NATRForms gives three conventions in percent of the close: TA-Lib’s NATR, TradingView’s and Tulip’s Wilder average from bar 0, and the simple average (TradeStation’s AvgTrueRange).

natr := c.NATR(h, l, c, n).SetN(n, math.NaN())
tv := c.TrueRange(h, l, c).RMA(n).Div(c).Mul(100).SetN(n-1, math.NaN())
sma := c.ATR(h, l, c, n).Div(c).Mul(100).SetN(n-1, math.NaN())
  • Inputs: highs, lows and closes of one length; the close must be positive. A zero close gives NaN in NATR and 0 through Div’s zero rule in tv and sma.
  • First valid index: n for natr, since TA-Lib leaves bar 0’s true range out and seeds at bar n with the mean of bars 1 to n; n - 1 for tv and sma, whose first window holds bar 0’s true range, h[0] - l[0]. The masked bars read NaN; unmasked, NATR reads +0 there and ATR passes the true range through, so sma would read TR/c times 100.
  • Anchors: c.NATR(h, l, c, n) equals c.ATRWilder(h, l, c, n).Div(c).Mul(100) bit for bit on positive closes (the division first, in TA-Lib’s order). tv[n-1] equals sma[n-1], because RMA’s seed is the simple average of the first n true ranges; it is bit for bit on the test’s data and on every price set tried, but the two sums are not the same code, so the identity is pinned, not proved.
  • Witness: Vitali Apirine’s spreadsheet (“Average Percentage True Range”, Stocks & Commodities V33:12, November 2015, p. 35) prints an ATR column for 32 S&P 500 bars of 2010. TrueRange matches its 32 true ranges and TrueRange.RMA(14) its 19 ATR values at two decimals, while ATRWilder(14) and ATR(14) both read 18.36 at row 15 against the printed 17.50 and miss every printed row from 15 on. On its last row the three forms read 2.52138, 2.49896 and 2.48424.
  • Traps: Tulip’s natr includes bar 0’s true range and starts at n - 1, so it is tv, not TA-Lib’s form (0.07 points apart at Apirine’s row 15); Tulip and AmiBroker multiply by 100 first, which agrees to rounding only. The dossier’s c.TrueRange(h, l, c).XAverage(27) has Wilder’s constant but seeds over 27 bars and is neither form. Since TrueRange gives h[0] - l[0] at bar 0, the simple form needs n - 1 masked bars, not n.
  • Variant: pandas-ta’s natr without TA-Lib takes an exponential average with a simple seed, c.TrueRange(h, l, c).XAverage(n).Div(c).Mul(100), first valid n - 1, to rounding (pandas-ta multiplies 100 over the close by the average).
  • Variant: Forman’s alternative divisor, a moving average of the close, as Pezzutti uses it: c.ATR(h, l, c, n).Div(c.Average(n)).Mul(100).
  • Not documented here: Apirine’s percentage true range divides each bar’s true range by the midpoint of its own interval, between the true high and the true low; TrueHigh and TrueLow give them, so it composes over the shipped API and is left to a later recipe.
  • Evidence: Forman reads monthly S&P 500 N-ATR “just below 5” in January 2006 and “nearly 13%” in 1987, and his Figure 3 legend’s 4.33832 is the printed ATR, 54.9014, over the close, 1,265.50, times 100. Evidence, not a reference.
  • Pinned by TestRecipeT6NATRForms; runnable example ExampleSeries_NATR_conventions.

Normalized Volatility Indicator (Kayakkal)

Rajesh Kayakkal (“Normalized Volatility Indicator”, Stocks & Commodities V28:9, August 2010, pp. 28-31) divides “the 64-day average true range” by “the end-of-day value” of the S&P 500, times 100, and trades its level: “When the NVI goes above 1.343, close all long positions and open short positions”, and “As soon as NVI goes below 1.343, close all your short positions and open long positions.” His ProRealTime code tests nvi < x and nvi > x, so a value equal to the threshold runs neither branch. ProRealTime’s average true range is Wilder’s smoothing (its seed is undocumented), so t6NVI builds on NATR and holds the side through ties; with n = 64 the seed’s weight decays by 63/64 a bar and is immaterial over his twenty years. Fosback’s negative volume index, also abbreviated NVI, is another line, on on-balance-volume/t6-volume-indexes.md.

nvi := c.NATR(h, l, c, n).SetN(n, math.NaN())
side := nvi.Mul(-1).Hysteresis(-thr, -thr)
  • Inputs: highs, lows and positive closes; n (his 64) and thr in percent. His 1.343 was picked for the S&P 500 on a grid of 0.001 from 1 to 1.5: “The pivot value of the NVI may vary for different indexes.”
  • First valid index: n for nvi (NaN before); side reads 0 through bar n - 1 and until the first decision.
  • Anchors: negation is exact, so -nvi > -thr holds exactly when nvi < thr: side is 1 from a bar with nvi below thr, -1 from a bar above it, and holds on a tie and on NaN. On NaN, 1.2, 1.343, 1.5, 1.343, 1.3, NaN, 1.4 with thr 1.343 it reads 0, 1, 1, -1, -1, 1, 1, -1.
  • Traps: nvi.LT(thr) reads a tie and every NaN as not long: 0, 1, 0, 0, 0, 1, 0, 0 on the same values. After a NaN close ATRWilder’s recursion does not recover, so nvi is NaN from that bar on and side keeps its last state. Adding a unit a day while the side holds is position sizing, outside the recipe.
  • Variant: the simple average of the TradeStation and TradersStudio tips in place of nvi: c.ATR(h, l, c, n).Div(c).Mul(100).SetN(n-1, math.NaN()).
  • Evidence: his ProRealTime report (S&P 500, April 1990 to May 2010, no commissions) shows 24 trades, 4 of them winners, at a threshold chosen on that data. Evidence, not a reference.
  • Pinned by TestRecipeT6NVI.

The Range Indicator (Weinberg)

Jack Weinberg (“The Range Indicator”, Stocks & Commodities V13:6, June 1995, pp. 238-242) builds “an oscillator of the ratio of the daily true range with the interday range” and smooths it “with an exponential moving average”. His SuperCharts code reads if Close-Close[1]>0 then Value1=TrueRange/(Close-Close[1]) else Value1=TrueRange;, gives StochRange=100*(Value1-Value2) on a flat window, and calls TRI=XAverage(StochRange(m),q): with his q = 3 and m = 10 that is a 10-bar stochastic smoothed over 3 bars, while his equation reads a 3-bar stochastic and a 10-bar average. t6RangeIndicator names the lengths by role, k for the stochastic and e for the average, with the code’s 10 and 3.

tr := c.TrueRange(h, l, c)
d := c.Delta(1)
v := tr.Div(d.Max(0)).Add(tr.Mul(d.LTE(0)))
sr := v.Stochastic(v, v, k)
a := sr.Window(-(k - 1)).Mul(0).Add(2 / float64(e+1))
tri := sr.XAverageAlphaS(a).SetN(k-1, math.NaN())
srM := sr.SetN(k-1, math.NaN())
  • Inputs: highs, lows and closes of one length; k >= 2 (k = 1 makes Window(0) empty and TRI all zeros) and e, both in bars.
  • First valid index: 0 for v (bar 0 has no close change, so it reads the true range, h[0] - l[0]); k - 1 for srM and tri, NaN before, where Stochastic reads exactly 50 and XAverageAlphaS +0. tri[k-1] is srM[k-1], and the recursion runs with the constant 2/(e + 1) from bar k.
  • Anchors: v equals d.If(func(x float64) bool { return x > 0 }, tr.Div(d), tr) bit for bit (they differ only where d is NaN): where the close rose, d.Max(0) is d and d.LTE(0) is 0; elsewhere Div’s zero rule gives 0 and the true range is added. Stochastic gives 0 on a flat window, Weinberg’s else branch, where ChannelPosition gives 50. TRI lies in [0, 100] and follows his T + a(SR - T) to rounding.
  • Traps: the equation’s “unless Close-Closet-1 < 0” leaves an unchanged close undefined; with Div’s zero rule it would read 0 there, the window’s minimum, so the recipe follows the code’s “> 0”. TRI depends on the price level (v is a ratio on up closes and a range in price units on the others): doubling the prices changes it on every bar of the test’s walk, so it does not compare across instruments. sr.XAverage(e) seeds over Stochastic’s warm-up 50s (it passes them through to bar e - 2 and seeds at e - 1).
  • Variant: the equation’s reading, a 3-bar stochastic and a 10-bar average, is t6RangeIndicator(h, l, c, 3, 10).
  • Evidence: Weinberg’s test on four currency futures (1984 to 1994, 100 dollars a trade for commission and slippage) raised net profit from 153,180 to 156,780 with TRI as an entry filter and to 179,918 with it as an entry and exit filter, while the maximum drawdown rose in two of the four markets with the entry filter. His Figure 8’s total of 39% profitable does not follow from its rows (its rounded percentages give about 101 of 265, 38%). Evidence, not a reference.
  • Pinned by TestRecipeT6RangeIndicator; runnable example ExampleSeries_Stochastic_rangeIndicator.

Mass Index and reversal bulge (Dorsey)

Donald Dorsey (“The Mass Index”, Stocks & Commodities V10:6, June 1992, pp. 265-267) takes “a 25-day moving sum of a ratio of two moving averages”, the exponential average of the daily range (alpha 0.2, a 9-period average) over that average smoothed a second time: “If the moving sum reaches 27, then falls below 26.5, a reversal signal is indicated.” t6MassIndex follows StockCharts’ spreadsheet (ChartSchool, “Mass Index”, cs-massindex.xls), whose averages are seeded with simple averages, the only printed table of inputs and outputs. The latch is set above setup and reset below trigger, and the bulge is its fall from 1 to -1.

r := h.Sub(l)
e1 := r.XAverage(e)
e2 := e1.Window(-(e - 1)).XAverage(e)
mi := e1.Div(e2).Sum(n).SetN(e+e+n-3, math.NaN())
latch := mi.Hysteresis(setup, trigger)
bulge := latch.Delta(1).LT(-1)
  • Inputs: highs and lows (the close is not read); e and n in bars (9 and 25); setup and trigger (27 and 26.5).
  • First valid index: 2(e - 1) + n - 1 for mi, 40 for the defaults (NaN before). e1 passes the range through on bars 0 to e - 2 and e2 starts at bar 2e - 2, so the unmasked ratio is 0 on bars 0 to e - 2 and exactly 1 on bars e - 1 to 2e - 3. latch and bulge read 0 through the masked bars.
  • Anchors: on StockCharts’ “50 periods” sheet (S&P 500, 11 June to 20 August 2012) the nine printed values at bars 41 to 49 agree within 1e-12, as do its EMA at bar 8 (e1[8]), its EMA of the EMA at bar 16 (e2[8], as e2 starts at bar 8) and its first ratio at bar 17 (e1[17] over e2[9]); the sheet starts its ratio column a bar late, so it prints from bar 41. StockCharts says some sheet values “are off by a penny” from its charts, whose calculations “extend back two years”, so a chart reading will not match the sheet exactly. A constant range gives exactly 25.
  • Ties: Hysteresis is strict at both thresholds. The trigger is strict in every source (“falls below 26.5”); the setup is inclusive in Dorsey’s sidebar (“reaches 27”) and strict in his captions and StockCharts (“moved above 27”). His inclusive reading is mi.Hysteresis(math.Nextafter(27, math.Inf(-1)), 26.5), which sets the latch on a value of exactly 27.
  • Traps: latch.CrossesBelowS(0) also fires on a first decision from 0 to -1, which is not a bulge; the fall of 2 is. The dossier’s e.Div(e.XAverage(9)).Sum(25) seeds the double average over the pass-through ranges and misses the sheet’s first printed value by 0.06. XAverage’s step fuses on arm64, so mi differs between architectures in the last places; on the test’s walk every value lies at least 0.0024 from a threshold.
  • Variant: Dorsey’s own recursion, seeded with the first range, is h.Sub(l).XAverageAlpha(0.2) over its own XAverageAlpha(0.2), summed over 25 bars, first valid 24.
  • Variant: his direction rule trades against the recent trend at the bulge: short when c.XAverageAlpha(0.2) is above its own XAverageAlpha(0.2), long when below.
  • Evidence: Dorsey reports that “80% of all mass index signals would have been profitable” on the 15th day after entry, with no market, period, sample or costs; StockCharts notes that “the Mass Index rarely exceeds 27”. Evidence, not a reference.
  • Method: Series{}.MassIndex(h, l, e, n) is mi from bar 2(e - 1) + n - 1, bit for bit, for e >= 2, and reads 0 before; at e = 1 the block’s e1.Window(-(e - 1)) is empty, so mi reads 0 where the method counts the nonzero ranges. On StockCharts’ 504-row sheet (23 August 2010 to 20 August 2012) the method matches the printed index within 1e-12, and the block’s bulge marks two bulges, at bars 119 and 265.
  • Pinned by TestRecipeT6MassIndex; runnable example ExampleSeries_Hysteresis_massIndexBulge.

The three recipes below place levels at today’s open plus or minus a multiple of a range measured on earlier bars, so a level is known at the bar’s open and uses that open and earlier bars only; it is NaN before its first valid index (SetN(F, math.NaN())), where the Shift pad and the warm-up would give a wrong level (the open itself on bar 0). A fill flag tests the bar’s own high or low against the level and is known at its close: a buy stop is touched when the high reaches the level (GTE), a sell stop when the low does, as a stop order fills, and the fill price, the level or the open when the bar opens beyond it, is the caller’s (the resting-stop fill series of the trade-state pages). Williams’s exits, a stop a distance beyond the entry level and the bailout on the first profitable open, run on PositionSignedRules with the open as receiver, and PositionSignedExitValue posts the value each exit was read at. On a daily bar that touches both levels the order of the two touches is unknowable. TrueRange is h[0] - l[0] on bar 0 and the true range from bar 1. Inputs are daily bars o, h, l and c of one length, paired by position, positive and finite with h >= l (with unequal lengths TrueRange returns an empty series and every level built on it reads 0 on the unmasked bars, not a price). “TD Range Expansion Breakout”, “TD REBO” and the other TD names are trademarks of DeMark Analytics LLC (formerly Market Studies LLC), used here only to name the indicators.

Volatility breakout off the open (Williams)

Larry Williams (Long-Term Secrets to Short-Term Trading, 2nd ed., 2011, ch. 4, pp. 71-98) tests a percentage of yesterday’s range added to the close, to the high and to tomorrow’s open, and concludes: “the best point to add or subtract a volatility expansion value to is tomorrow’s open” (p. 76). His Tables 4.1 to 4.3 trade the levels always in the market (“no stop was used and you were always long short”, p. 74). His bond system (Figure 4.5) adds “A protective stop of” 1,500 dollars “or 50 percent of the previous day’s range subtracted from our entry” and exits on “the bailout or the first profitable opening after entry technique”, and his exit rules (ch. 11, p. 195) are “Always use a dollar stop on all trades”, the bailout, “exit on the first profitable opening. If the profit is only one tick, take it”, which “for slower-moving markets I will delay […] a day or two”, and “Exit and reverse if you get an opposite signal”. t6WilliamsBailout returns the two levels, their touch fills and the always-in book of the fills, and on PositionSignedRules the book with his exits, the value each exit was read at and each trade’s result.

rng := h.Sub(l).Shift(1)
buy := o.Add(rng.Mul(kb)).SetN(1, math.NaN())
sell := o.Sub(rng.Mul(ks)).SetN(1, math.NaN())
buyFill := h.GTE(buy)
sellFill := l.LTE(sell)
pos := c.PositionSigned(buyFill, nil, sellFill, nil)
r := PositionRules{
    MinHold:     delay,
    LongStop:    PositionLevel{Level: buy.Sub(d), Touch: l},
    ShortStop:   PositionLevel{Level: sell.Add(d), Touch: h},
    LongTarget:  PositionLevel{Level: buy},
    ShortTarget: PositionLevel{Level: sell},
    Open:        o,
}
book := o.PositionSignedRules(buyFill, nil, sellFill, nil, r)
ex := o.PositionSignedExitValue(buyFill, nil, sellFill, nil, r)
px := buy.Mul(book.GT(0)).Add(sell.Mul(book.LT(0)))
res := ex.Sub(px.EntryValue(book).Shift(1)).Mul(book.Shift(1))
  • Inputs: daily opens, highs, lows and closes; kb, ks >= 0, the fractions of yesterday’s range (1 and 1 for his bond system, 0.5 for the S&P, 0.4 and 2 in Figure 4.23); d > 0, the stop’s distance in price units (his dollar stop over the point value, which is the caller’s; Figure 4.5’s other stop, half of yesterday’s range, is the level buy.Sub(rng.Mul(0.5)), and the text does not say which of the two it takes); delay, the bars from the fill bar before the bailout is read, MinHold (0 and 1 read it from the next open, as Figure 4.5 does; his “a day or two” later is 2 or 3).
  • First valid index: 1 for the levels (NaN on bar 0) and the fills (0 there); pos and book read 0 until the first single fill; ex and res are NaN until the first exit and on every bar that is not an exit bar.
  • Anchors: on the constructed hand case the levels, fills and always-in book are those of ExampleSeries_Shift_volatilityBreakout, string for string; pos equals a loop of the always-in rule on the test tapes: a flat book takes a single fill and stays flat on a bar touching both, a long book reverses on a sell fill and a short one on a buy fill. Its Hysteresis twin, buyFill.Sub(sellFill).Hysteresis(0, 0), holds on a bar that touches both and differs from the book on 212, 183 and 196 of 720 bars of the three tapes (from bar 5 of the hand case). For kb, ks >= 0, buy >= o >= sell, the fill price o.Maximum(buy) is buy, and as a cross-check buy equals o.SuperTrend(h.Shift(1), l.Shift(1), l, kb, 1) from bar 1. Crabel’s opening-range test, t6CrabelORB on bar-classification/t6-narrow-range-bars.md, rests the same buy stop a fixed x above the open on the day after a narrow-range setup: buy is that level with x the series rng.Mul(kb), yesterday’s range times kb, and no setup. book, ex and res equal a loop of his rules bit for bit (the always-in loop with the stop, read from the bar after the fill, and the bailout, the stop first on a bar with both) on the test tapes for three pairs of fractions, four distances and four delays; delay 1 is delay 0; with no stop (d +Inf) and the bailout held back (delay math.MaxInt) book is pos. On the q tape (kb and ks 0.5, d 30, delay 0) book takes 312 trades: 33 close on the stop (14 at an open through it), 103 on the bailout and 176 on an opposite fill (73 of them on a stop bar); with delay 3 it takes 345, with 42 stops, 23 bailouts and 279 reversals. The runnable example’s constructed bars give a stop at its level, a bailout at the open and a gap through the stop at the open, and with delay 2 a later bailout.
  • Traps: a strict fill, h.GT(buy), misses the hand case’s bar 5, whose high equals the level. Unmasked, bar 0’s levels are the open and both fill. His definition’s “the difference between the day’s high and close” (p. 74) is a misprint for the range, high minus low, which every table uses. Weekday gates read the entry bar’s weekday (bonds: buys on Tuesdays and Thursdays, sells on Wednesdays and Thursdays). The bailout is strict (“If the profit is only one tick, take it”: an open at the entry level is no profit), so the target has no Touch and is read on the receiver o; the stop rests and is touched by the low, so it has Touch: l, touch-inclusive. res is ex less the entry level times the side, so a short trade that exits at its entry level reads -0.
  • Variant: his 2011 unit, the average true range of the past three days, o.Add(c.AvgTrueRange(h, l, c, 3).Shift(1).Mul(k)).SetN(3, math.NaN()), first valid at bar 3 (n in general), the stop of Ruggiero’s runaway gap under “QStick” behind a gap test.
  • Variant: the other anchors of Tables 4.1 and 4.2, c.Add(h.Sub(l).Mul(kb)).Shift(1).SetN(1, math.NaN()) and the high and low forms, whose fill price is o.Maximum(level) (a gap can open beyond them).
  • 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, so a stop touched there after the fill is honoured on the next bar’s test, if still touched, or not at all: 65 of the q tape’s 312 fill bars reach the stop, before or after the fill, which a daily bar does not say. On a reversal bar ex is the open, or the stop on a stop bar, where the trade closes at the opposite fill, or at the stop if the price reaches it first. A bar that opens beyond the entry level and later touches the stop exits at the stop, where his bailout at the open comes first (none on the q tape at d 30). A fill on the bar of a stop or a bailout opens a trade only on a later fill (44 bars on the q tape). Point values and weekday keys are the caller’s.
  • Evidence: Tables 4.1 to 4.3 give best-fit percentages for 11 futures with no dates or costs, three of their rows are inconsistent in themselves, and his text’s averages per trade (389 dollars off the open, 327 off the close, 313 off the high and low) do not follow from them; the bond system (Figure 4.5, 1990 to August 1998, 100% either way, a stop and the bailout) shows 651 trades and 80% winners, and the S&P system (Figure 4.22) 1,333 trades and 74% (his prose rounds it to 75), with “Comm $ 0” in every summary although his ch. 3 says 50 dollars are deducted; his 2011 re-tests, on an unnamed market, show the edge fading after 2008. Evidence, not a reference.
  • Pinned by TestRecipeT6WilliamsBreakout; runnable example ExampleSeries_PositionSignedRules_williamsBailout.

TD Range Expansion Breakout (DeMark, in Perl)

Jason Perl (DeMark Indicators, 2008, ch. 9, pp. 158-161) applies TD REBO to daily bars with the current bar’s open as the base: “Multiply the previous day’s true range separately by 0.382 and by 0.618”, then add the two values to the current day’s opening price for the upside levels and subtract them for the downside. With DeMark’s qualifiers, “if yesterday’s close was a down close compared with the previous day’s close, and then price takes out yesterday’s true range multiplied by 0.382 and added to today’s open, there’s a good chance the up move will extend higher toward” the 0.618 level, the mirror after an up close, until “a signal occurs in the opposite direction”. t6TDREBO returns the four levels, the qualified extensions and their latch.

tr := c.TrueRange(h, l, c).Shift(1)
up1 := o.Add(tr.Mul(0.382)).SetN(2, math.NaN())
up2 := o.Add(tr.Mul(0.618)).SetN(2, math.NaN())
dn1 := o.Sub(tr.Mul(0.382)).SetN(2, math.NaN())
dn2 := o.Sub(tr.Mul(0.618)).SetN(2, math.NaN())
dc := c.LT(c.Shift(1)).Shift(1).SetN(2, 0)
uc := c.GT(c.Shift(1)).Shift(1).SetN(2, 0)
extUp := dc.Mul(h.GT(up1))
extDn := uc.Mul(l.LT(dn1))
state := extUp.Sub(extDn).Hysteresis(0, 0)
  • Inputs: daily opens, highs, lows and closes; the true range is Wilder’s, Perl’s true high less true low.
  • First valid index: 2 for the levels (NaN before) and the flags (0 before): yesterday’s true range needs the close before it, and the qualifier compares yesterday’s close with the day before’s. state reads 0 until the first extension.
  • Anchors: on the hand case the four levels are those of the runnable example; every output equals a loop of Perl’s rule bit for bit on the test tapes, a walk with a NaN high and the tie witnesses; up2 >= up1 >= o >= dn1 >= dn2 from bar 2. A down close and an up close cannot both qualify a bar, so the latch equals its PositionSigned twin.
  • Traps: “takes out” is strict and “reaches” is a touch: on the tie witnesses a high equal to up1 after a down close and a low equal to dn1 after an up close do not extend, where h.GTE(up1) and l.LTE(dn1) would. The qualifiers’ SetN(2, 0) matters only for the variant with levels from bar 1: there the unmasked up-close test compares bar 0’s close with the Shift’s zero, reads 1 on bar 1 and fires a spurious extension; in the helper the levels are NaN on bar 1 and the flags 0 anyway. The figure captions say “range” where the rule says “true range”. On a daily bar the break of the 0.382 level and the reach of the 0.618 one come in an unknown order.
  • Variant: the fades, after an up close a short where the high takes out up1 and reaches up2, uc.Mul(h.GT(up1)).Mul(h.GTE(up2)), and the mirror long.
  • Variant: levels from bar 1 accepting TrueRange[0], o.Add(tr.Mul(0.382)).SetN(1, math.NaN()), with the qualifiers still masked by SetN(2, 0).
  • Evidence: Perl’s Figures 9.5 to 9.8 (Nikkei 225) are charts with no prices printed, and he reports no statistics. Evidence, not a reference.
  • Pinned by TestRecipeT6TDREBO; runnable example ExampleSeries_TrueRange_rangeExpansion.

Swing volatility levels (Chalek, in Williams)

Williams (ch. 4, pp. 95-97) credits Mike Chalek’s “Talon” system: “measure the amount of price movement from the high three days ago to today’s low. That is Step 1. Step 2 is to take the swing distance from the high one day ago minus the low three days ago. Finally, we will use the largest of these values”, and “buy at 80 percent of the swing value above the opening and sell at 120 percent of the swing value below the opening”, with “a dollar stop of” 1,750 dollars “and my bailout exit”. t6ChalekBailout returns the swing, the next bar’s two levels and their touch fills, and on PositionSignedRules the book with the stop and the bailout of the open breakout, the value each exit was read at and each trade’s result, with the fills priced at bpx and spx.

a := h.Shift(3).Sub(l)
b := h.Shift(1).Sub(l.Shift(3))
sv := a.Maximum(b).SetN(3, math.NaN())
buy := o.Add(sv.Shift(1).Mul(0.8)).SetN(4, math.NaN())
sell := o.Sub(sv.Shift(1).Mul(1.2)).SetN(4, math.NaN())
buyFill := h.GTE(buy)
sellFill := l.LTE(sell)
bpx := o.Maximum(buy)
spx := o.Minimum(sell)
r := PositionRules{
    LongStop:    PositionLevel{Level: bpx.Sub(d), Touch: l},
    ShortStop:   PositionLevel{Level: spx.Add(d), Touch: h},
    LongTarget:  PositionLevel{Level: bpx},
    ShortTarget: PositionLevel{Level: spx},
    Open:        o,
}
book := o.PositionSignedRules(buyFill, nil, sellFill, nil, r)
ex := o.PositionSignedExitValue(buyFill, nil, sellFill, nil, r)
px := bpx.Mul(book.GT(0)).Add(spx.Mul(book.LT(0)))
res := ex.Sub(px.EntryValue(book).Shift(1)).Mul(book.Shift(1))
  • Inputs: daily opens, highs and lows; d > 0, the stop’s distance in price units (a point value is the caller’s).
  • First valid index: 3 for sv and 4 for buy and sell, NaN before, and 4 for the fills, 0 before; the levels are masked after the Shift, since a mask before it is undone on bar 0, where the Shift’s pad is +0 again and the level reads the open. book reads 0 until the first single fill, and ex and res are NaN until the first exit.
  • Anchors: on the hand case the swing and both levels are those of the runnable example; sv, buy and sell equal a loop of the two swings, the larger taken, bit for bit on the test tapes. The dossier’s If form, a.Sub(b).If(func(v float64) bool { return v > 0 }, a, b), equals Maximum on finite inputs and returns b where a is NaN, hiding a NaN high on the walk that has one; the abs form a.Add(b).Add(a.Sub(b).Abs()).Div(2) is not exact (on the cent witness the swing 102.57000000000001 differs). book, ex and res equal the open breakout’s loop of his exits, with the fills at bpx and spx, bit for bit on every group for four distances; on the q tape (d 30) book takes 68 trades: 30 close on the stop (15 at an open through it), 25 on the bailout and 12 on an opposite fill (10 of them on a stop bar).
  • Traps: the swing is negative when both differences are, today’s low above the high three bars ago and yesterday’s high below the low three bars ago (the negative-swing witness reads -0.5): the buy level is then below the open and fills at it, the sell level above it fills too, so a flat book stays flat on such a bar and an open one reverses, and the source is silent. bpx and spx, the fills, are the levels wherever the swing is not negative. Unmasked, the swing is 0 on bar 0 and a price on bars 1 and 2 (yesterday’s high), and the level on bar 2 about 1.8 times the price.
  • Variant: his bond filter, buys only “if the closing price of bonds is greater today than five days ago”, bc.GT(bc.Shift(5)).Shift(1).SetN(6, 0) on the bonds’ closes bc (sells when bonds are “lower than 35 days ago”, as printed).
  • Departures: those of the open breakout: 49 of the q tape’s 68 fill bars reach the stop, before or after the fill, 1 stop bar opens beyond the fill, where the bailout at the open comes first, and 3 bars of a stop or a bailout carry a single fill; on a reversal bar ex is the open or the stop, not the opposite fill. Point values are the caller’s.
  • Evidence: Figure 4.39 (S&P 500, July 1982 to August 1998, a 1,750-dollar stop and the bailout) shows 538 trades, 84% winners and 122,837.50 dollars net with “Comm $ 0”, and Figure 4.40, with the bond filter, 295 trades and 87%. Evidence, not a reference.
  • Pinned by TestRecipeT6ChalekSwing; runnable example ExampleSeries_Maximum_swingVolatility.

Volatility Estimators

GARCHVariance

func (s Series) GARCHVariance(omega, alpha, beta float64) Series

GARCHVariance returns the GARCH(1,1) conditional variance of Bollerslev (1986) with fixed parameters and a zero mean. The receiver is a series of returns, normally log returns c.Log().Delta(1) (or c.LogRatio(c.Shift(1))), with index 0 the oldest bar. Bar 0 is not a return (Delta(1) writes 0 there), so s[0] is not read. The model has no mean term: pass demeaned returns for a constant-mean model. The parameters are the caller’s, for example from an offline fit; the library does no estimation.

With M = len(s), out the newly allocated result and h the conditional variance state, h is seeded with the unconditional variance omega / (1 - alpha - beta), evaluated as omega / ((1 - alpha) - beta); this is the forecast for bar 1 before any return is seen. Then, for each bar t = 1 .. M-1, with r = s[t]:

h = (omega + alpha r^2) + beta h
out[t] = h

Every product is rounded on its own and the additions run left to right, so no platform fuses a multiply and an add and every platform gives the same bits.

Storage convention: out[t] is the variance forecast for bar t+1, made at the close of bar t from the returns up to and including s[t]. Python’s arch (arch_model(r, mean=“Zero”, vol=“GARCH”, p=1, q=1)) and R’s rugarch store the variance for bar t at bar t, so out[t] corresponds to their value for the next bar, and out[M-1] to arch’s one-step forecast at the end of the sample. arch_model(…).fix(params) always computes its own backcast, so for a close match use arch’s GARCH().compute_variance on the returns from bar 1 (r[1:]) with backcast set to the seed (and variance bounds, which clamp only in extreme cases); arch’s sigma2[t] then matches out[t] to rounding (about 1e-15 relative), not bit for bit.

The default backcast of arch is data-driven (it reads the first 75 squared returns, a look-ahead), so values agree with arch only once the seed difference has decayed (by the factor beta per bar), unless arch is given the returns from bar 1 (r[1:]) and the seed as its backcast (through compute_variance; fix() accepts no backcast). The method is recursive, so a slice of the history that starts later differs from the full series by a seed difference that decays by the factor beta per bar.

The warm-up reads 0, not NaN: bar 0 is 0 and the first valid index is 1. Invalid parameters give aligned zeros, M values of 0. The parameters are valid when omega, alpha and beta are finite, omega is positive, alpha and beta are not negative, (1 - alpha) - beta is positive and the seed is finite. The positivity of (1 - alpha) - beta is the stationarity condition, which makes the unconditional variance exist; it is computed exactly so, not as alpha + beta < 1, which differs in the last bit for some values. The recursion does not recover from non-finite values: if s[t] is NaN or infinite at a bar t >= 1, or the state is not finite at bar t (an overflow), bars t to M-1 are NaN. A NaN at s[0] changes nothing.

The variance is in squared return units, and for t >= 1 a finite out[t] is at least omega. With percent returns (as arch recommends) omega scales by a factor of 1e4. Annualise a daily variance by multiplying it by 252 (or the caller’s count of bars per year), and the volatility by its square root.

The case alpha + beta = 1 with omega = 0 (IGARCH, the RiskMetrics exponentially weighted variance) is outside the domain, because the unconditional variance does not exist. For RiskMetrics in this same storage convention use the zero-mean recipe r.Mul(r).XAverageAlpha(0.06), and XVarianceAlpha for the demeaned form, with r from c.Log().Delta(1): both read bar 0 (a 0 seed whose weight decays by 0.94 per bar), so a NaN there, as from c.LogRatio(c.Shift(1)), makes every value NaN. With beta = 0 the model is the ARCH(1) model of Engle (1982).

Recipe for Chan’s signal: with v := r.GARCHVariance(omega, alpha, beta), the sign of v.Sub(r.Squared()) forecasts whether the next bar’s squared return is above today’s.

Sources: Bollerslev, “Generalized Autoregressive Conditional Heteroskedasticity”, Journal of Econometrics 31 (1986); Engle, “Autoregressive Conditional Heteroscedasticity with Estimates of the Variance of United Kingdom Inflation”, Econometrica 50 (1982); Engle and Mezrich, “GARCH for Groups”, Risk 9 (1996), for targeting; Kissell, Algorithmic Trading Methods (2nd ed., 2020), ch. 11; Chan, Machine Trading (2017), for the signal; Python arch.

GARCHVarianceTargeted

func (s Series) GARCHVarianceTargeted(alpha, beta, longVar float64) Series

GARCHVarianceTargeted returns the GARCH(1,1) conditional variance of Bollerslev (1986) with fixed alpha and beta, a zero mean and variance targeting (Engle and Mezrich 1996): omega = longVar (1 - alpha - beta), so the unconditional variance is longVar. The receiver is a series of returns, normally log returns c.Log().Delta(1) (or c.LogRatio(c.Shift(1))); bar 0 is not a return (Delta(1) writes 0 there), so s[0] is not read. The model has no mean term: pass demeaned returns for a constant-mean model.

longVar is the caller’s choice. To keep the result causal, compute it from bars before the first output you use, for example the sample variance of an earlier window; a variance of the whole sample, or of any window that ends later, looks ahead. The recursion is that of GARCHVariance, with omega evaluated as longVar times ((1 - alpha) - beta), rounded once, and the state h seeded with longVar, the forecast for bar 1 before any return is seen. With M = len(s), out the newly allocated result and r = s[t], for each bar t = 1 .. M-1:

h = (omega + alpha r^2) + beta h
out[t] = h

So out[t] is the variance forecast for bar t+1, made at the close of bar t from the returns up to and including s[t]. Python’s arch and R’s rugarch store the variance for bar t at bar t, so out[t] corresponds to their value for the next bar. The default backcast of arch is data-driven (it reads the first 75 squared returns, a look-ahead), so values agree with arch only once the seed difference has decayed (by the factor beta per bar), unless arch is given the returns from bar 1 (r[1:]) and longVar as its backcast (through compute_variance; fix() accepts no backcast). The method is recursive, so a slice of the history that starts later differs from the full series by a seed difference that decays by the factor beta per bar.

The warm-up reads 0, not NaN: bar 0 is 0 and the first valid index is 1. Invalid parameters give aligned zeros, M values of 0: alpha, beta and longVar must be finite, longVar positive, alpha and beta not negative, and (1 - alpha) - beta positive (stationarity). A subnormal longVar can make omega underflow to 0. The recursion does not recover from non-finite values: if s[t] is NaN or infinite at a bar t >= 1, or the state is not finite at bar t, bars t to M-1 are NaN. A NaN at s[0] changes nothing.

The variance is in squared return units. With percent returns (as arch recommends) longVar, and so omega, scales by a factor of 1e4. Annualise a daily variance by multiplying it by 252 (or the caller’s count of bars per year), and the volatility by its square root. See GARCHVariance for the RiskMetrics recipe, Chan’s signal and the sources.

GARCHVolatility

func (s Series) GARCHVolatility(omega, alpha, beta float64) Series

GARCHVolatility returns the forecast conditional standard deviation of the GARCH(1,1) model of Bollerslev (1986) with fixed parameters and a zero mean: math.Sqrt of GARCHVariance(omega, alpha, beta), bar by bar, in the units of the returns. The receiver is a series of returns, normally log returns c.Log().Delta(1) (or c.LogRatio(c.Shift(1))); bar 0 is not a return (Delta(1) writes 0 there), so s[0] is not read. The model has no mean term: pass demeaned returns for a constant-mean model.

As in GARCHVariance, the state is seeded with the unconditional variance omega / ((1 - alpha) - beta), and out[t] is the forecast for bar t+1, made at the close of bar t from the returns up to and including s[t]. Python’s arch and R’s rugarch store the variance for bar t at bar t, so out[t] corresponds to their value for the next bar. The default backcast of arch is data-driven (it reads the first 75 squared returns, a look-ahead), so values agree with arch only once the seed difference has decayed (by the factor beta per bar), unless arch is given the returns from bar 1 (r[1:]) and the seed as its backcast (through compute_variance; fix() accepts no backcast). The method is recursive, so a slice of the history that starts later differs from the full series by a seed difference that decays by the factor beta per bar.

The warm-up reads 0, not NaN: bar 0 is 0 and the first valid index is 1. Invalid parameters give aligned zeros, M values of 0: omega, alpha and beta must be finite, omega positive, alpha and beta not negative, and (1 - alpha) - beta positive (stationarity), and the seed must be finite. The recursion does not recover from non-finite values: if a return is NaN or infinite at a bar t >= 1, or the variance state is not finite at bar t, bars t to M-1 are NaN, and NaN stays NaN through the square root.

The result is in return units. Annualise a daily volatility by multiplying it by the square root of 252 (or of the caller’s count of bars per year). See GARCHVariance for the RiskMetrics recipe, Chan’s signal and the sources.

GarmanKlassVolatility

func (s Series) GarmanKlassVolatility(o, h, l, c Series, n int) Series

GarmanKlassVolatility returns the Garman-Klass range estimator of volatility over the trailing n bars (Garman and Klass 1980, Journal of Business 53(1)): the square root of the window mean of 0.5 (ln(h) - ln(l))^2 - (2 ln 2 - 1) (ln(c) - ln(o))^2, from the open o, high h, low l and close c of each bar. It assumes no drift and no opening gaps. Each value is the per-bar standard deviation of log returns, unitless and not annualised; annualise with .Mul(math.Sqrt(252)) for daily bars (256 or 260 are also used), or the square root of the bars per year. The receiver is not used.

o, h, l and c must have equal lengths; otherwise the result is an empty Series. For n < 1 or n > len(o) the result is len(o) zeros. Bars 0 .. n-2 are the warm-up and are +0, not NaN; the first valid value is at index n-1, and n = 1 gives the per-bar estimator. Trim or mask the warm-up (SetN(n-1, math.NaN())) before sizing positions on the result.

A bar with a zero, negative, NaN or infinite price has a NaN term, and exactly the windows that hold that bar are NaN; later windows recover. Bars are not validated otherwise: a bar with o or c outside [l, h], or with h < l, is computed as it is. On sound bars every term is >= 0, so a negative window mean needs such a bad bar; it gives NaN rather than 0, because a zero volatility would size an infinite position. Every value is exactly +0 when every bar of its window has h = l = o = c, positive and finite.

With o = c finite and positive on every bar the result equals ParkinsonVolatility(h, l, n) times sqrt(2 ln 2) to within 1e-14 relative, on every valid bar. It equals RogersSatchellVolatility / sqrt(2) on sound bars (l <= o, c <= h) only when every bar has o = c = l or o = c = h; otherwise it is larger. Each value costs O(n) and depends only on the bars of its own window, so the method is window-local and start-invariant.

HistoricalVolatility

func (s Series) HistoricalVolatility(n int) Series

HistoricalVolatility returns the sample standard deviation of the last n log returns ln(s[t]/s[t-1]), the per-bar close-to-close volatility, not annualised.

The result is s.LogRatio(s.Shift(1)).StdDevSample(n).SetN(n, 0), bit for bit. The first sentence’s ln(s[t]/s[t-1]) is computed as ln(s[t]) - ln(s[t-1]), a difference of logarithms that can differ in its last bits from the logarithm of the rounded quotient. The deviation is taken about the window’s own mean with the divisor n-1, Natenberg’s default for an estimate from a sample, and n counts returns, so a window spans n+1 closes.

The value is a unitless log return per bar: it is not annualised and not a percentage. To annualise, multiply by the square root of the number of bars in a year, .Mul(math.Sqrt(252)) for trading days (256, 260 and 365 are also in use), and by 100 for percent. ParkinsonVolatility and the other range estimators are per bar too and scale the same way.

Natenberg’s Option Volatility and Pricing (2nd edition, 2015) shows both scales: the eleven weekly closes of his Figure B-2 give HistoricalVolatility(10) at the last bar, which times the square root of 365/7 and times 100 is his 39.65 at two decimals. His chapter 6 closes, 45 then the changes +0.98, -0.65, -0.70, +0.25 and -0.85, give HistoricalVolatility(5) at the last bar, which times 16 and times 100 is his 27.8 to one decimal.

Other conventions are compositions on r := s.LogRatio(s.Shift(1)): the population form r.StdDev(n), the zero-mean form r.RMS(n), the zero-mean sample form r.RMS(n) times the square root of n/(n-1), and TTR’s volatility, whose window counts n prices rather than n returns. Tulip Indicators’ volatility takes the population form over the simple returns s[t]/s[t-1]-1 in place of r, times the square root of 252. They and the range estimators are under “Volatility Estimators” in the manual.

Connors and Raschke’s N-day historical volatility is HistoricalVolatility(N - 1), N counting closes: their 6/100 ratio is HistoricalVolatility(5) over HistoricalVolatility(99), the sample deviations of 5 and 99 log returns with which Hartle’s sidebar to their 1996 article reproduces their values. The ratio recipes are listed under “Compositions” in the “Volatility Estimators” category.

The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. Bar 0 has no return, so the first full window of n returns ends at bar n. A position sized by the inverse of the volatility would be infinite on a zero, so mask the warm-up, for instance with SetN(n, math.NaN()). A window of equal closes gives exactly 0. Validation: n < 2 or n >= len(s) gives len(s) zeros, and an empty series gives an empty result.

A close that is NaN, +Inf, -Inf, 0, -0 or negative at bar p has no logarithm, so the returns at p and p+1 are NaN and bars max(p, n) to p+n are NaN; every other bar is unchanged. The method is causal and window-local bit for bit: StdDevSample takes each window afresh, so a bar’s value depends only on the n+1 closes ending there, and a suffix of the closes gives the same values from its own bar n on (start-invariant). It costs O(n) a bar, for StdDevSample’s centred kernel, and returns a new Series without modifying the receiver.

ParkinsonVolatility

func (s Series) ParkinsonVolatility(h, l Series, n int) Series

ParkinsonVolatility returns the Parkinson range estimator of volatility over the trailing n bars (Parkinson 1980, Journal of Business 53(1)): the square root of the window mean of (ln(h) - ln(l))^2, times sqrt(1 / (4 ln 2)), from the high h and low l of each bar. It assumes no drift and no opening gaps. Each value is the per-bar standard deviation of log returns, unitless and not annualised; annualise with .Mul(math.Sqrt(252)) for daily bars (256 or 260 are also used), or the square root of the bars per year. The receiver is not used.

h and l must have equal lengths; otherwise the result is an empty Series. For n < 1 or n > len(h) the result is len(h) zeros. Bars 0 .. n-2 are the warm-up and are +0, not NaN; the first valid value is at index n-1, and n = 1 gives the per-bar estimator. Trim or mask the warm-up (SetN(n-1, math.NaN())) before sizing positions on the result: a zero volatility would size an infinite position.

A bar whose high or low is zero, negative, NaN or infinite has a NaN term, and exactly the windows that hold that bar are NaN; later windows recover. Bars are not validated otherwise: a bar with h < l is computed as it is and, as the squared log range is symmetric, gives the same value as the swapped bar. Every value is exactly +0 when every bar of its window has h = l, positive and finite. The log range is the difference of two logarithms, so on a nearly flat bar (a high within a few ulps of the low) it can be 0, or off by the logarithms’ rounding, rather than the tiny exact range.

ParkinsonVolatility(h, l, n) equals RogersSatchellVolatility(l, h, l, l, n) times sqrt(1 / (4 ln 2)) (the float64 0.60056120439322491) bit for bit, and, with o = c finite and positive on every bar, GarmanKlassVolatility equals this estimator times sqrt(2 ln 2) to within 1e-14 relative, on every valid bar. Each value costs O(n) and depends only on the bars of its own window, so the method is window-local and start-invariant.

RogersSatchellVolatility

func (s Series) RogersSatchellVolatility(o, h, l, c Series, n int) Series

RogersSatchellVolatility returns the Rogers-Satchell range estimator of volatility over the trailing n bars (Rogers and Satchell 1991, Annals of Applied Probability 1(4)): the square root of the window mean of (ln(h) - ln(c)) (ln(h) - ln(o)) + (ln(l) - ln(c)) (ln(l) - ln(o)), from the open o, high h, low l and close c of each bar. It allows a drift but assumes no opening gaps. Each value is the per-bar standard deviation of log returns, unitless and not annualised; annualise with .Mul(math.Sqrt(252)) for daily bars (256 or 260 are also used), or the square root of the bars per year. The receiver is not used.

o, h, l and c must have equal lengths; otherwise the result is an empty Series. For n < 1 or n > len(o) the result is len(o) zeros. Bars 0 .. n-2 are the warm-up and are +0, not NaN; the first valid value is at index n-1, and n = 1 gives the per-bar estimator. Trim or mask the warm-up (SetN(n-1, math.NaN())) before sizing positions on the result.

A bar with a zero, negative, NaN or infinite price has a NaN term, and exactly the windows that hold that bar are NaN; later windows recover. Bars are not validated otherwise: a bar with o or c outside [l, h], or with h < l, is computed as it is. On sound bars every term is >= 0, so a negative window mean needs such a bad bar; it gives NaN rather than 0, because a zero volatility would size an infinite position. Every value is exactly +0 when every bar of its window has h = l = o = c, positive and finite.

RogersSatchellVolatility(l, h, l, l, n) equals h.LogRatio(l).RMS(n) to within 1e-14 relative for n >= 2 (each term is then the squared log range), and ParkinsonVolatility(h, l, n) equals it times sqrt(1 / (4 ln 2)) bit for bit. Each value costs O(n) and depends only on the bars of its own window, so the method is window-local and start-invariant.

YangZhangVolatility

func (s Series) YangZhangVolatility(o, h, l, c Series, n int) Series

YangZhangVolatility returns the Yang-Zhang estimator of volatility over the trailing n bars (Yang and Zhang 2000, Journal of Business 73(3)), which allows both opening gaps and a drift. Over bars t-n+1 .. t it is the square root of Vo + k Vc + (1 - k) RS, where Vo is the sample variance (divisor n-1) of the n overnight gaps ln(o) - ln(previous c), Vc the sample variance of the n open-to-close log returns ln(c) - ln(o), RS the mean of the n Rogers-Satchell terms (see RogersSatchellVolatility) and k = 0.34 / (1.34 + (n+1)/(n-1)), which tends to 0.34/2.34 as n grows. This is TTR’s form: n bars plus the close before them, with demeaned sample variances. Each value is the per-bar standard deviation of log returns, unitless and not annualised; annualise with .Mul(math.Sqrt(252)) for daily bars (256 or 260 are also used), or the square root of the bars per year. The receiver is not used.

o, h, l and c must have equal lengths; otherwise the result is an empty Series. For n < 2 or n >= len(o) the result is len(o) zeros. Bars 0 .. n-1 are the warm-up and are +0, not NaN; the first valid value is at index n, as its window also needs the close before it. Trim or mask the warm-up (SetN(n, math.NaN())) before sizing positions on the result.

A bar with a zero, negative, NaN or infinite price has NaN terms, and exactly the windows that hold any of them are NaN; a bad close at bar p also spoils the gap of bar p+1, so it reaches the values up to bar p+n. Later windows recover. Bars are not validated otherwise: a bar with o or c outside [l, h], or with h < l, is computed as it is. Of the variance checks, only a negative total gives NaN, which needs such a bad bar; it is NaN rather than 0 because a zero volatility would size an infinite position. TTR also returns NaN when the Rogers-Satchell mean alone is negative; that extra NaN is not copied. Every value is exactly +0 when every bar of its window has h = l = o = c, positive and finite, and there are no gaps.

With o = c equal to one constant on every bar (no gaps and no open-to-close moves) the result equals sqrt(1 - k) times RogersSatchellVolatility to within 1e-14 relative, from bar n. Siroky’s modified Rogers-Satchell estimator (Stocks & Commodities 36(5), 2018) is Yang-Zhang with k = 0 and the gap variance replaced by the mean of the squared gaps; it is a recipe, not provided here. Close-to-close volatility over n prices, the sample standard deviation of the n-1 log returns r := c.LogRatio(c.Shift(1)) (Shift pads bar 0 with 0, so bar 0’s return is NaN), is c.HistoricalVolatility(n-1) for n >= 3, which reads 0 on bars 0 to n-2 where r.StdDevSample(n-1) is NaN at index n-2 (its window holds bar 0’s return); the values from n-1 match TTR’s close form, which uses n prices and n-1 returns. Each value costs O(n) and depends only on the bars of its own window, so the method is window-local and start-invariant.

Compositions

The volatility compositions below mask their values with NaN before their first valid index, where the kernels read +0 (a legitimate “no volatility”) or NaN. Prices are positive and finite: the logarithm of a zero price is not defined, and a NaN close makes the recursive estimates NaN for good. Lengths are in bars, and the annualisation factor comes from the caller as the square root of the bars per year: math.Sqrt(252), 16 for a 256-day year, or math.Sqrt(365.0 / 7) for weekly bars at t = 7/365. The gates on HistoricalVolatility alone (Kaufman’s annualised volatility and the short-over-long ratios of Connors and Raschke and of Landry) are on t6-volatility-gates.md, and its comparisons with an implied-volatility index (the IV/HV ratio, the variance risk premium and the expected move) on t6-volatility-implied.md. Carver’s forecasts under “MACD” divide by the risk of the blend’s first variant below, c.Delta(1).Window(-1).XStdDev(span), which that variant’s ann of 1 leaves bit for bit.

Close-to-close historical volatility (Natenberg)

Sheldon Natenberg (Option Volatility and Pricing, 2nd ed., McGraw-Hill, 2015, ch. 20, p. 383) writes that “historical volatility calculations are most often made using the sample standard deviation, that is, dividing by n - 1”, that “most historical volatility calculations use a zero-mean assumption”, and that the data points are, “more commonly, the logarithmic change”. His Appendix B (pp. 557-558) works one example four ways, and his chapter 6 (pp. 79-81) prints a five-day answer, “27.8 percent”. t6HistoricalVolatility gives the four forms over the last n log returns, the current one included, annualised by ann.

r := c.LogRatio(c.Shift(1))
hvS := r.StdDevSample(n).Mul(ann).SetN(n, math.NaN())
hvP := r.StdDev(n).Mul(ann).SetN(n, math.NaN())
hvP0 := r.RMS(n).Mul(ann).SetN(n, math.NaN())
hvS0 := r.Squared().Sum(n).Div(n-1).Sqrt().Mul(ann).SetN(n, math.NaN())
  • Inputs: positive closes; n counts returns (n + 1 prices); ann from the caller. hvS is the lead form, the sample deviation with the actual mean; hvP is the population one, hvP0 the population root mean square (a zero mean, the settlement form of realised-volatility contracts) and hvS0 the zero-mean sample form.
  • First valid index: n for all four (NaN before). Unmasked, StdDev, StdDevSample and RMS read +0 on bars 0 to n - 2 and NaN at n - 1, whose window holds bar 0’s NaN return (the Shift pad is 0, not a price), and Squared().Sum(n) is NaN through bar n - 1.
  • Anchors: on Figure B-2 (eleven weekly closes from 97.70 to 103.85, annualised with the square root of 365/7) hvP, hvP0 and hvS give the printed 37.62, 37.88 and 39.65 percent; hvS0 gives 39.92, and Natenberg’s 39.93 comes from his returns rounded to six decimals (39.925006), so the test asserts 39.925 +/- 0.005. On his chapter 6 path (45, 45.98, 45.33, 44.63, 44.88, 44.03, times 16) only hvS rounds to 27.8. hvP0 squared is hvP squared plus ann squared times the window’s squared mean, to rounding; c.LogRatio(c.Shift(1)) equals c.Log().Delta(1) bit for bit from bar 1 and is NaN at bar 0.
  • Traps: c.Log().Delta(1) reads 0 at bar 0, a return that enters the window ending at n - 1 as a finite, biased value. Natenberg prints the simple return as (p_n - p_n-1)/p_n, a misprint for the previous price, and Figure B-2 needs t = 7/365: with the square root of 52 it reads 37.57. Robert Kissell’s historical moving average (Algorithmic Trading Methods, 2nd ed., 2021, ch. 11) is a zero-mean variance over n - 1 that excludes the current return, r.Squared().Sum(n).Div(n - 1).Shift(1); he calls the divisor unbiased, which it is not for a known zero mean.
  • Variant: TTR’s volatility counts n prices and annualises with the square root of 260, r.StdDevSample(n - 1).Mul(math.Sqrt(260)).SetN(n-1, math.NaN()); Tulip’s takes simple returns, the population form and the square root of 252, c.Delta(1).Div(c.Shift(1)).StdDev(n).Mul(math.Sqrt(252)).SetN(n, math.NaN()), to rounding (Tulip takes c[t]/c[t-1] - 1), masked because its bar-0 return is 0, not NaN.
  • Variant: Natenberg’s multi-horizon forecast (ch. 20, pp. 392-393) weights his 6, 12, 26 and 52-week volatilities. Read as daily hvS over 30, 60, 130 and 260 returns, an assumption since his 52-week volatility is a weekly-return one (p. 86), the five-month weights give hv30.Mul(0.15).Add(hv60.Mul(0.25)).Add(hv130.Mul(0.35)).Add(hv260.Mul(0.25)), first valid 260.
  • Missing piece: Natenberg’s windowed exponential estimate (pp. 394-395) weights the last n squared returns by powers of lambda through FIR, which needs an ExponentialWeights(n, lambda) builder.
  • Evidence: his five printed blends of the volatilities 28, 22, 19 and 18 percent (21.75, 23.0, 23.4, 20.85 and 22.15) are arithmetic on assumed inputs, and his Figures 20-1 and 20-2 compare 252 against 365 days and daily against weekly returns on unprinted data. Evidence, not a reference.
  • Method: c.HistoricalVolatility(n).Mul(ann) is hvS from bar n, bit for bit; the method reads 0, not NaN, on bars 0 to n - 1.
  • Pinned by TestRecipeT6HistoricalVolatility; runnable example ExampleSeries_StdDevSample_natenberg.

Square-root rule (Fosback)

Norman Fosback (Stock Market Logic, 1976, ch. 61, “Volatility Analysis”, pp. 241-242) states that “given a certain market advance all stocks change in price by adding a constant amount to the square root of their beginning prices”: if the average-priced stock goes from 25 to 36, its square root moves from 5 to 6, “up 1 point”, and every other stock adds 1 to its square root. His Square Root Volatility rating (pp. 328-331) uses the rise in the square root for a 10% advance of the average-priced stock. t6SquareRootRule gives the projected price and the fractional move for a given rise lam.

end := c.Sqrt().Add(lam).Squared()
move := end.Div(c).Sub(1)
  • Inputs: positive prices, and lam, the square root of the average-priced stock’s new price less that of its old one, from the caller: 1 for Table 59, math.Sqrt(1.1*m0) - math.Sqrt(m0) for the 10% rating of an average price m0. Fosback does not say which average or universe: lam is a cross-sectional input.
  • First valid index: 0.
  • Anchors: Table 59 (p. 242): with lam 1 the prices 4, 25 and 100 end at 9, 36 and 121 bit for bit, moves of 125%, 44% and 21% at his integers. With lam for a 10% rise of a 25 stock, math.Sqrt(27.5) - 5, that stock moves 10% to rounding.
  • Traps: lam is a number: a one-element series in its place is right-aligned and adds to the last bar only, so Series{4, 25, 100}.Sqrt().Add(Series{1}).Squared() gives 0, 0, 121. The dossier’s per-series c.Sqrt().Momentum(n) is its own reading, not Fosback’s cross-sectional rule.
  • Evidence: Fosback writes that Square Root Volatility “is usually superior to Beta as an estimator of future expected return”, with no figures. Evidence, not a reference.
  • Pinned by TestRecipeT6SquareRootRule.

Blended volatility forecast (Carver)

Robert Carver (Advanced Futures Trading Strategies, Harriman House, 2023, Strategy three, “Forecasting future volatility”, and Appendix B) estimates risk with “an exponentially weighted standard deviation” of percentage returns over a span of 32 days, annualised by 16, and because volatility “also mean reverts in the long run” uses “a blend of long (weight 0.3) and short run (weight 0.7) estimates of volatility”, the long one a “Ten year average”. The book is silent on a history shorter than ten years. t6CarverBlend reads the long mean as expanding until lw valid bars exist and rolling over the last lw after.

r := c.Delta(1).Div(c.Shift(1)).Window(-1)
sig := r.XStdDev(span).Mul(ann)
valid := sig.Mul(0).Add(1).SetN(span-1, 0)
long := sig.AverageVariable(valid.CumSum(), 1, lw)
blend := sig.Mul(0.7).Add(long.Mul(0.3)).SetN(span-1, math.NaN())
sigM := sig.SetN(span-1, math.NaN())
longM := long.SetN(span-1, math.NaN())
  • Inputs: positive, finite closes (a NaN close makes every later value NaN: XStdDev does not recover and the count of valid bars turns NaN); span 32 (35 in Strategy nine’s box); lw 2,560, ten years of Carver’s 256-day year, or 2,520 on a 252-day year; ann 16.
  • First valid index: span - 1 of the results, which are one bar shorter than c (element j is c’s bar j + 1: bar 0’s return is 0/0, which Div gives as 0, and Window(-1) drops it). NaN before; unmasked, sig and long read +0 there.
  • Anchors: long equals sig.AverageWhere(valid, 0) bit for bit before the lw-th valid bar and sig.Average(lw) from it. At the first valid bar long is sig and blend equals sig to rounding.
  • Traps: the dossier’s sig.Average(lw) passes sig through for lw - 1 bars, so its blend is sig to one ulp for the first ten years. A bar-0 return of 0 left in place enters XStdDev’s seed, and SetN(1, math.NaN()) instead would make it NaN for good. Carver’s annualised risk is of percentage returns, not of the dossier’s log returns c.Log().Delta(1).
  • Variant: the risk of price changes that Carver does not annualise (“Do not annualise”) takes c.Delta(1).Window(-1) as r and ann 1.
  • Variant: pysystemtrade’s mixed_vol_calc (span 35, pandas’ adjust=True and bias=False) blends the deviation of price differences, divides by the price afterwards and takes an exponential long mean (pandas ewm(span=...).mean(), adjust=True) whose span the system config sets to ten years, 2,560 (defaults.yaml, slow_vol_years: 10; the function’s own default is twenty years, 5,120). It is a different rule from the book’s: its long mean carries old price levels.
  • Missing piece: pandas’ adjusted, bias-corrected exponential deviation, an XStdDevAdjusted(n), would give pysystemtrade’s short estimate in one call (it composes from four exponential sums today). XStdDev seeds with the first window’s population moments instead, and the two short estimates converge only slowly: on a synthetic walk about 1% apart at bar 35 and still about 4e-5 apart at bar 6,000.
  • Evidence: the 0.3 and 0.7 “came from a regression of future standard deviation on the two estimates”, and “Retesting various trading strategies using a purely backward looking estimate of these weights did not damage performance” (footnote 61); no data are printed. Evidence, not a reference.
  • Pinned by TestRecipeT6CarverBlend.

The volatility gates below compare a per-bar volatility with a threshold and take a book out of the market or let it back in, or compare a short volatility with a long one. HistoricalVolatility is the per-bar sample standard deviation of log returns; its conventions (Natenberg’s four forms, TTR’s n-price window, Tulip’s simple returns) are on t6-realised-volatility.md and are not restated here. Kaufman’s estimator is the population standard deviation of simple returns, close/close[1] - 1, a different estimator: it differs from the sample log form by sqrt((n - 1)/n) and by the window’s skew. Values are NaN before their first valid index and the flags and latches built on them read 0 there; volatilities are per bar until the caller’s ann annualises them (math.Sqrt(252) in Kaufman’s articles, math.Sqrt(260) in Connors’s), and levels are fractions (0.30 for Kaufman’s 30%) or unitless ratios (the sources print 50% for 0.5). Every published threshold was chosen on the data it was tested on, in sample. A NaN close makes the windows that hold it NaN for n + 1 bars (it spoils two returns), during which no flag fires and the latches hold. The books decide at the close; a fill at the next open is the caller’s Shift(1). Kaufman’s relative-volatility band, a true range over the ATR, is on true-range/t6-range-contraction.md (“Relative-volatility band (Kaufman)”). These recipes differ from D’Errico’s VoltyRatio (trade-state/t2-latched-entries.md): they exit and re-enter on close-to-close volatility, or compare two standard deviations of returns, where VoltyRatio divides two high-low ranges and latches breakout levels. The comparison of HistoricalVolatility with an implied-volatility index, Lindgren’s IV/HV ratio and Natenberg’s variance risk premium, is on t6-volatility-implied.md, whose hv is this page’s hv times 100, bit for bit.

Annualised volatility (Kaufman)

Perry Kaufman (“Price Shocks: Anticipation Is Everything”, Stocks & Commodities V38:7, June 2020, pp. 22-25) measures “the annualized volatility as the standard deviation of your last 20 daily returns times the square root of 252 (the standard measure)”. His listings (“Is It Too Volatile To Trade?”, Stocks & Commodities V40:8, July 2022, pp. 14-19, Listing 1) compute returns = close/close[1] - 1 and stddev(returns,volperiod)*squareroot(252), TradeStation’s population deviation; “Using The Elusive Volume Confirmation” (Stocks & Commodities V43:10, September 2025, pp. 14-18) reprints code with standarddev(returns,volperiod,2), the sample one. t6KaufmanVolatility gives the population form av, the sample form avS and, beside them, HistoricalVolatility’s sample log form hv, each times ann.

r := c.Div(c.Shift(1)).Sub(1)
av := r.StdDev(n).Mul(ann).SetN(n, math.NaN())
avS := r.StdDevSample(n).Mul(ann).SetN(n, math.NaN())
hv := c.HistoricalVolatility(n).Mul(ann).SetN(n, math.NaN())
  • Inputs: positive closes; n returns (20), a window of n + 1 closes; ann from the caller, math.Sqrt(252) in every Kaufman article. The results are fractions: 0.30 is Kaufman’s 30%.
  • First valid index: n for all three (NaN before). Bar 0’s return is -1 (Div reads 0 on the Shift pad, less 1); it enters the window ending at n - 1 and no later one, so unmasked that bar reads 3.46 annualised on a 1% walk, a false extreme, and from bar n the values carry no trace of it.
  • Anchors: on closes from 2^40 multiplied alternately by 1.5 and 0.5 the returns are exactly +0.5 and -0.5: avS is 0.5 x sqrt(20/19) x ann bit for bit, av is 0.5 x ann within one unit (bit for bit on arm64, one unit below on 12 of 40 bars as amd64 code, from the running moments; the test pins both counts) and hv is atanh(0.5) x sqrt(20/19) x ann to 4.1e-16. r equals a loop of c[t]/c[t-1] - 1 bit for bit from bar 1, and the realised-volatility page’s Tulip return c.Delta(1).Div(c.Shift(1)) equals it within a unit of 1. Kaufman’s Figure 10 (“Exiting The Trade: How To Avoid Being The Greater Fool”, Stocks & Commodities V44:7, July 2026, p. 20) prints crude-oil closes with their “Returns”: r rounds to the printed value at four decimals on all 29 rows with a previous close, while a log return does not (3/10: -0.1228 against the printed -0.1156; 3/23: -0.0998 against -0.0950), and TrueRange rounds to its TR column.
  • Traps: the population and sample forms differ by sqrt(n/(n - 1)), 2.6% at 20, and Kaufman’s 2022 equation prints the spreadsheet’s STDEV (sample) against its own population code. Simple and log forms differ window by window with the window’s skew (about +-1.5% at a 1% daily volatility and +-4% at 3% on simulated walks; a probe figure, not pinned), far more than on average. c.RateOfChange(1).Div(100) rounds twice and differs from r in the last bits (10 of 1,800 bars of the test path).
  • Variant: the sample form of the 2025 reprint is avS; Fraisse’s sidebar formula (“Measuring Volatility”, Stocks & Commodities V8:10, 1990), 1.026 x 15.811 x STD(Log(C/ref(C,-1)),20) with 1.026 the square root of 20/19, is the sample log form, hv with n 20 and ann the square root of 250 (his text says 252; he calls 20 “the number of price observations”, but the formula takes 20 log ratios, 21 prices).
  • Evidence: “My own calculation for extreme volatility in equity index markets is 32%” (“Determining Risk Before It Happens”, Stocks & Commodities V42:5, April 2024, p. 30); “Playing It Safe With Cryptos” (Stocks & Commodities V39:9, August 2021, pp. 18-22) resets after “a decline of at least 30% in volatility” without saying from what, and three readings (the exit value, the peak since the exit, the exit level) stand aside 1,851, 1,704 and 2,000 bars on one simulated walk (a probe figure, not pinned). Evidence, not a reference.
  • Pinned by TestRecipeT6KaufmanVolatility.

Price-shock exit and re-entry (Kaufman)

Kaufman’s rules (“Price Shocks: Anticipation Is Everything”, p. 25) “exit all positions when the volatility is over 30% and the net returns over the past three days are negative” and “Reenter all positions when the volatility falls below 30% and the net returns over the past three days are positive”, with “a buffer zone, that is, exit when the volatility is over 30% and reenter when it falls below 25%”. The rule reads “the returns of your trading”, a system’s daily returns: once out, the gated book’s own returns are 0, so it could not re-enter. t6PriceShock therefore reads the returns of the unfiltered base position sh, held from the bar before and fully invested, and gated is that position less the bars out.

r := sh.Shift(1).Mul(c.Div(c.Shift(1)).Sub(1))
av := r.StdDev(n).Mul(ann).SetN(n, math.NaN())
r3 := r.Sum(3)
exitEv := av.GT(x).Mul(r3.LT(0))
reEv := av.LT(xb).Mul(r3.GT(0))
out := c.Position(exitEv, reEv)
gated := sh.Mul(out.Shift(1).Mul(-1).Add(1))
  • Inputs: closes; sh the unfiltered base position, 0 or 1, of c’s length (it is the receiver of r, so sh of another length gives outputs of two lengths, av and gated of its length and out of c’s); n 20, ann, x 0.30, xb 0.30 or the 0.25 buffer. r3 is the net return of the last three days.
  • First valid index: n for av (NaN before); out and gated from bar 0. Bar 0’s r is -0 (the Shift pad times -1), not a return, and it still sits in the window ending at n - 1. out 1 is standing aside, decided at the close and in force from the next bar through out.Shift(1); the book is in the market before the first exit.
  • Anchors: on a constructed walk of 141 closes whose returns alternate +-0.5% for 40 bars, +-3% for 40 and +-0.5% for 60, with sh all ones, x 0.30 and xb 0.25, the book stands aside on bars 48 to 94 and re-enters at 95 (av 0.307409 at 48 and 0.247045 at 95); with xb 0.30 it re-enters at 93. out equals exitEv.Sub(reEv).Hysteresis(0, 0).GT(0) bit for bit, since Kaufman’s two events cannot fall on one bar (a three-day return below 0 and above 0).
  • Traps: the dossier’s latches exitEv.Sub(reEv).Hysteresis(1, -1) and reenter.Sub(exit).Hysteresis(1, -1) stand aside on no bar (a difference of two 0/1 flags is not above 1 nor below -1), and Position(reenter, exit) read as “allowed” starts out of the market. c.RateOfChange(3) on an equity line that starts at 0 divides by 0 or by negative values; the three-day net return is r.Sum(3). A fixed 30% whipsaws commodities and single stocks: “Each trading strategy will have its unique threshold for"extreme” volatility. You’ll want to look at that on a spreadsheet" (rule 4), in sample.
  • Variant: a caller’s return series in place of the first statement, its bar 0 a real return: av := r.StdDev(n).Mul(ann).SetN(n-1, math.NaN()), first valid index n - 1.
  • Variant: the price reading, sh all ones (c.Mul(0).Add(1)): buy and hold gated by its own volatility, the dossier’s reading rather than Kaufman’s rule.
  • Evidence: Figure 8 (p. 25), his ten-market trend program to March 2020: over 18.2 years the filtered program returned 18.80% a year at a risk of 18.44% against 15.43% at 19.73% unfiltered (the year-to-date ratios do not equal return over risk as printed). Evidence, not a reference.
  • Pinned by TestRecipeT6PriceShock; runnable example ExampleSeries_Position_priceShock.

Volatility exit on a trend system (Kaufman)

“Is It Too Volatile To Trade?” sets the levels from each stock’s own history: “I chose to use the"Mdn+2 Stdev” calculation as my volatility exit level and the “Mdn + 1 Stdev” calculation as my reentry point" (Figure 9, p. 19), where Listing 1 takes the median and the population deviation of every bar’s volatility at the last bar, a look-ahead. t6VolatilityReentry takes them causally, the expanding median and population deviation of av after a burn-in of m0 valid bars; out is the prose’s two-level gate, and pos the printed trend system of Listing 2 (long from a bar on which the p-bar average rises to one on which it falls) with its volatility exit and its re-entry path on the per-bar levels: a volatility above lvlX closes the trade and arms the latch, and from armed a volatility below lvlR, or the trend entry, takes the next trade.

r := c.Div(c.Shift(1)).Sub(1)
av := r.StdDev(n).Mul(ann).SetN(n, math.NaN())
valid := av.GTE(0)
med := av.QuantileWhere(valid, 0, 0.5)
sd := av.StdDevWhere(valid, 0)
lvlX := med.Add(sd.Mul(2)).SetN(n+m0-1, math.NaN())
lvlR := med.Add(sd).SetN(n+m0-1, math.NaN())
out := c.Position(av.GT(lvlX), av.LT(lvlR))
ma := c.Average(p)
up := ma.GT(ma.Shift(1)).SetN(p, 0)
dn := ma.LT(ma.Shift(1)).SetN(p, 0)
pos := c.PositionArmed(up.Mul(av.LT(lvlX)), dn, av.GT(lvlX), nil, av.LT(lvlR))
  • Inputs: positive, finite closes (the mask assumes no NaN av after n); p 70 (Figure 9’s tests) or 80 (Listing 2’s default); n 20; ann; m0 the caller’s burn-in, 252 for instance.
  • First valid index: n + m0 - 1 for lvlX and lvlR (271 at 20 and 252; NaN before); out and pos hold no position before it, since a comparison with a NaN level is 0.
  • Anchors: at bar n the expanding median is av[n] and the deviation 0; the last bar’s levels use the median of the whole history exactly and its population deviation to rounding, so Kaufman’s look-ahead values are lvlX and lvlR at the last bar (for reproducing his tables only). pos equals a loop of Listing 2 with its re-entry path bit for bit when the levels are read bar by bar and a NaN level fires neither volatility test nor the re-entry (against the listing with fixed levels it is bit for bit only when the level is constant), and out equals a loop of “stand aside above lvlX until below lvlR”. On the fixture’s path at p 70 the re-entry path changes no bar; at p 20, n 20 and m0 252 it changes one, bar 658, a one-bar trade that the path-off book does not take, and on the test’s 60 walks with volatility regimes it changes 19 bars on 16 walks.
  • Traps: Kaufman’s exitvol = 0 is the unfiltered system, which the recipe does not reproduce before the burn-in (52 of 3,000 bars differ on a probe walk; a probe figure, not pinned). The code departs from the prose twice: it re-enters at the exit level (Avol < exitvol) whenever the average rises, so the buffer acts only while the average is not rising, and its signal survives a volatility exit, so the re-entry path ignores the average’s direction; the dossier’s “re-enter below median + 1 SD while the trend is still up” is neither. An exit level of exactly 0 would switch Kaufman’s filter off (his exitvol = 0) and a re-entry level of 0 his re-entry path (Avol < 0 does not hold); no level is 0 on the fixture’s runs (pinned). SetN(p, 0) on the trend flags matters when the levels exist before the average (p 120, m0 30: unmasked, the flags trade on the average’s pass-through).
  • Variant: rolling levels over the last m bars of av, av.Quantile(m, 0.5).Add(av.StdDev(m).Mul(2)) and av.Quantile(m, 0.5).Add(av.StdDev(m)), masked with SetN(n+m-1, math.NaN()): first valid index n + m - 1.
  • Variant: the re-entry path off, a nil re-entry flag in place of av.LT(lvlR), which is c.Position(up.Mul(av.LT(lvlX)), dn.Add(av.GT(lvlX)).GT(0)) bit for bit (Listing 2 with reentervol = 0, the armed state without a re-entry flag a flat one) and t6VolatilityExit’s pos.
  • Evidence: Figures 8 and 9: HD’s, AMZN’s and MUB’s levels round from Figure 8’s median rows (HD’s 0.392 to 0.40), but TSLA’s 0.75 and 0.40 and BAC’s 0.60 and 0.45 do not (Figure 8 gives 0.876 and 0.661, 0.595 and 0.422); profit over drawdown rose for four of five symbols (TSLA 2.83 to 7.19) and fell for AMZN (5.15 to 4.34). Evidence, not a reference.
  • Pinned by TestRecipeT6VolatilityExit.

Bottom fishing (Kaufman)

“Using The Elusive Volume Confirmation” (pp. 17-18) reprints Kaufman’s 2016 “Bottom Fishing”: “The closing price must be below the 100-day moving average”, “The annualized volatility must be greater than the threshold of 20% to show extreme volatility” and “The raw stochastic must be below 15% to indicate oversold”, with exits when “The stochastic rises above the exit of 60%” or “The annualized volatility drops below 5%”. The listing also requires avol > avol[1] (“high volatility and rising”) and computes standarddev(returns,volperiod,2), the sample deviation of simple returns. t6BottomFishing gives the entry and exit flags and the long book.

r := c.Div(c.Shift(1)).Sub(1)
av := r.StdDevSample(n).Mul(ann).SetN(n, math.NaN())
pk := c.Stochastic(h, l, k).SetN(k-1, math.NaN())
ma := c.Average(p).SetN(p-1, math.NaN())
entry := c.LT(ma).Mul(av.GT(vIn)).Mul(av.GT(av.Shift(1))).Mul(pk.LT(kIn))
exit := av.LT(vOut).Add(pk.GT(kOut)).GT(0)
pos := c.Position(entry, exit)
  • Inputs: highs, lows, closes; n 20, k 14, p 100, ann; vIn 0.20 and vOut 0.05 as fractions; kIn 15 and kOut 60 on the stochastic’s 0 to 100 scale.
  • First valid index: max(n + 1, k - 1, p - 1) for entry (99 with the defaults; av.Shift(1) reads the masked bar n - 1 at bar n), k - 1 for exit (0 before); pos from bar 0.
  • Anchors: pos is Position’s latch, the listing’s order (exits tested only when long, entries only when flat; with both flags on one bar it enters from flat and exits from long); the listing’s next-open fills are the caller’s pos.Shift(1). As printed, volthreshold(20) and volexitthreshold(5) are percents against a fractional avol, so the entry fires on no bar and the exit on every bar from n.
  • Traps: no guard for a flat range: Stochastic reads 0 on a flat 14-bar range, which passes pk.LT(15), a false oversold reading, and TradeStation’s result for the division by zero is not stated. “Rising” is in the code only. The stochastic’s mask matters only for an exit level below its warm-up 50 (at kOut 40 the unmasked exit fires on bars 0 to 12).
  • Evidence: Figure 6 (p. 17) prints a different volatility threshold for each of ten ETFs, 0.13 to 0.59, against “the same parameters were used for all tests”, on 4 to 64 trades each. Evidence, not a reference.
  • Pinned by TestRecipeT6BottomFishing.

Historical-volatility ratio (Connors and Raschke)

Laurence Connors and Linda Bradford Raschke (“Historical Volatility And Pattern Recognition”, Stocks & Commodities V14:8, August 1996, pp. 338-341) “found that large price moves occur when the six-day historical volatility reading is less than 50% of the 100-day reading”, and the setup also needs that day to be “either an inside day or an NR4 day”. Thom Hartle’s sidebar reproduces their numbers in Excel only with “five and 99 periods” of =LN(E3/E2) under STDEV, and Connors’s own appendix (Connors On Advanced Trading Strategies, 1998, p. 237) defines “the unbiased standard deviation of the"N” minus one most recent logarithmic daily returns": an N-day volatility counts N closes and is HistoricalVolatility(N - 1). t6HVRatio gives the ratio, the compression flag and the setup, with the NR4 day and the inside day of bar-classification/t6-narrow-range-bars.md.

hvS := c.HistoricalVolatility(ns - 1)
hvL := c.HistoricalVolatility(nl - 1)
ratio := hvS.Div(hvL).SetN(nl-1, math.NaN())
comp := ratio.LT(k).Mul(hvL.GT(0))
rg := h.Sub(l)
nr4 := rg.Breakout(3, 1).EQ(-1)
inside := h.LT(h.Shift(1)).Mul(l.GT(l.Shift(1)))
setup := comp.Mul(nr4.Add(inside).GT(0))
  • Inputs: highs, lows, closes; ns and nl in closes as the sources name them (6/100; 10/100 the earlier pair), with ns >= 3 and nl > ns (HistoricalVolatility(1) is all zeros, so ns = 2 would flag every bar with a positive long volatility); k 0.5. Connors’s square root of 260 and the percent cancel in the ratio.
  • First valid index: nl - 1 (99) for all three (NaN before for ratio, 0 for the flags). Unmasked, the warm-up ratio is 0/0 or x/0, which Div reads as 0, so LT(0.5) fires: the unmasked setup fires on 28 of the 99 warm-up bars of the test path.
  • Anchors: on HistoricalVolatility, the recipe’s building block: Hartle’s sixteen closes (5/24/95 to 6/15/95) give HistoricalVolatility(5) times sqrt(260) times 100 that rounds to all ten printed values, 28.11 to 44.65, at two decimals; Connors’s four closes give .007227 at six decimals and 11.65%, his .116536 being that times sqrt(260) truncated, not rounded; Street Smarts (Raschke and Connors, 1995, Appendix, pp. 203-204, after Natenberg 1994) gives .025338, .000642, .005778 and 18.30% with sqrt(365/7), its printed 18.29% coming from the rounded factor 7.22. A flat 100-close window gives ratio 0, which Mul(hvL.GT(0)) keeps out of comp.
  • Traps: the dossier’s r := c.Log().Delta(1); r.StdDev(6).Div(r.StdDev(100)) is Landry’s population ratio over 6 and 100 returns (equal to the LogRatio population form bit for bit from bar 100), not this one. Across unequal windows the divisor does not cancel: the sample ratio is the population ratio times sqrt(a/(a - 1))/sqrt(b/(b - 1)) to rounding, 1.112 at 5/99, so their 0.5 is 0.4495 on population ratios.
  • Variant: Koh and Lee’s intraday crossing (“Forecasting Futures Movement”, Stocks & Commodities V25:13, Bonus Issue 2007, pp. 30-33), ratio.CrossesBelowS(0.8) with HistoricalVolatility(180) over HistoricalVolatility(360) on 30-second bars, per session; their volatility formula is not printed, and a window that restarts each session is a missing piece.
  • Missing piece: TSSB’s variance ratios (Aronson and Masters, Statistically Sound Machine Learning for Algorithmic Trading of Financial Instruments, 2013, pp. 142-145) compose only uncompressed and in this package’s reading (the divisor, the window count and the ATR’s scale are not printed): CHANGE VARIANCE RATIO as the square of c.HistoricalVolatility(n).Div(c.HistoricalVolatility(nl)), PRICE VARIANCE RATIO on c.Log() with StdDevSample, ABS PRICE CHANGE OSCILLATOR on the absolute log change over a log-price ATR; the DELTA forms need an explicit final mask. The -50 to 50 transform is not printed; an F CDF would need BetaIncomplete(a, b) or FCDF(d1, d2), with degrees of freedom that are not printed either.
  • Evidence: chart examples only (Figures 3 and 4; Street Smarts Exhibits 20.1 to 20.6); Koh and Lee chose their windows and the 0.8 on the data they tested. Evidence, not a reference.
  • Pinned by TestRecipeT6HVRatio; runnable example ExampleSeries_HistoricalVolatility_connorsRaschke.

Ratio extensions (Connors; Gipson)

Connors (Connors On Advanced Trading Strategies, ch. 7, pp. 37-53) adds that “it is even better to have both readings under 50 percent or for either reading to have multiple days under 50 percent”, and an HV thrust: “On the day of the readings under 50 percent look for a daily price range that is larger than the daily range of the previous nine days”. Derek Gipson (ch. 25, “Gipsons”, pp. 193-203) trades with the trend on “a 14 period ADX reading” above 25 and “+DI and -DI”, waits “for its 6/100 day historical volatility reading to drop under .40”, and would “Exit half your position and tighten your stops on the other half when the 6-day H.V. reading reaches the 100-day reading”. t6HVRatioExtensions gives the flags on the 6/100 and 10/100 ratios.

r6 := c.HistoricalVolatility(5).Div(c.HistoricalVolatility(99)).SetN(99, math.NaN())
r10 := c.HistoricalVolatility(9).Div(c.HistoricalVolatility(99)).SetN(99, math.NaN())
f6 := r6.LT(0.5)
f10 := r10.LT(0.5)
both := f6.Mul(f10)
persist := f6.Sum(kd).EQ(float64(kd))
thrust := f6.Add(f10).GT(0).Mul(h.Sub(l).Breakout(9, 1).EQ(1))
adx := c.ADX(h, l, c, 14)
pdi := c.DMIPlus(h, l, c, 14)
mdi := c.DMIMinus(h, l, c, 14)
low := r6.LT(0.4)
longSet := adx.GT(25).Mul(pdi.GT(mdi)).Mul(low)
shortSet := adx.GT(25).Mul(mdi.GT(pdi)).Mul(low)
halfOff := r6.GTE(1)
  • Inputs: highs, lows, closes; kd, the persistence count, the caller’s: Connors gives none (“two consecutive days”, “three consecutive days” and “two weeks” in his examples).
  • First valid index: 99 for every flag (0 before); persist is 0 before 98 + kd.
  • Anchors: persist equals the run-length form f6.Mul(f6.StateAge()).GTE(kd) bit for bit, both exact integers; the thrust’s Breakout(9, 1).EQ(1) equals a loop of “larger than each of the previous nine ranges” from bar 9, on the high-low range, not the true range.
  • Traps: the thrust’s “direction of the large-range day” is not defined (the close against the previous close or the open, or the side of the range broken): the caller’s. Gipson’s wait after the signal day is not stated; his entry level is h.ValueWhen(longSet) (“when the high of the signal day is exceeded”) and the fills are outside the package. The dossier’s Gipson form drops the DI direction (“we only trade in the direction of the trend”). Connors’s extraordinary move is not defined, and shock.BarsSince().GT(98) reads 0 before the first shock, blocking every bar of a series that has none.
  • Variant: the 100-bar invalidation after a caller’s shock flag, shock.Sum(99).EQ(0): Connors would “wait a full 100 days to remove the extraordinary event from our calculations”, and a shock return stays in HistoricalVolatility(99)’s window for 99 bars.
  • Evidence: Gipson’s IBM example (p. 197): a “six-day H.V. reading of 11.6 percent” over a 100-day reading of 30.9 is 37.5 percent, below .40. Evidence, not a reference.
  • Pinned by TestRecipeT6HVRatioExtensions.

Landry’s three ratios (Landry)

David Landry (“A Volatility Trade In Gold”, Stocks & Commodities V16:7, July 1998, pp. 344-348) prints the MetaStock formulas Std(Log(C/Ref(C,-1)),4) / Std(Log(C/Ref(C,-1)),100) and the same over 6 and 10 returns: “I ran an average of the three by adding each of the four-, six- and 10-day historical indicators together, dividing by 3, then calculating a 12-day exponential moving average”. MetaStock’s Std is the population deviation on forum evidence only (its formula primer does not say), over n returns, not Connors’s N - 1. t6LandryHV gives the three ratios, their mean and its exponential average, run on the trimmed series.

r := c.LogRatio(c.Shift(1))
sdL := r.StdDev(100)
c4 := r.StdDev(4).Div(sdL).SetN(100, math.NaN())
c6 := r.StdDev(6).Div(sdL).SetN(100, math.NaN())
c10 := r.StdDev(10).Div(sdL).SetN(100, math.NaN())
avg3 := c4.Add(c6).Add(c10).Div(3)
ema := avg3.Mul(0).Add(avg3.Window(-100).XAverage(12)).SetN(111, math.NaN())
  • Inputs: positive closes; the windows of 4, 6, 10 and 100 returns and the 12-bar average are Landry’s.
  • First valid index: 100 for c4, c6, c10 and avg3 (bar 99’s window holds bar 0’s NaN return), 111 for ema (NaN before).
  • Anchors: ema equals avg3.Window(-100).XAverage(12) re-aligned, bit for bit: c.Mul(0).Add(shorter) is the realignment idiom, Add reading 0 before the shorter operand’s overlap, and XAverage seeds with the mean of the first 12 values. The dossier’s c.Log().Delta(1) ratios equal these bit for bit from bar 100.
  • Traps: XAverage of the masked avg3 is NaN on every bar, so the average must run on the trimmed series. A flat 100-bar window reads the ratios as 0 (Div’s zero), so a LT(0.5) test fires there; a NaN close after bar 100 makes ema NaN for good while the ratios recover 100 bars later. Both printed Formula: lines are malformed (an unbalanced parenthesis; Mov( missing). Go’s logarithm is not correctly rounded and XAverage fuses on arm64, so the ratios and ema agree with independent references to rounding only. Landry’s own caution: “Be careful when developing a derivative (in this case, averaging the three historical volatility readings) and then taking the moving average of that derivative.”
  • Variant: all three below 0.5, c4.LT(0.5).Mul(c6.LT(0.5)).Mul(c10.LT(0.5)).
  • Evidence: his COMEX gold readings of April 10 (1997), 24.05%, 23.66% and 49.22%, and their mean 32.31%, “the lowest level of the year”. Evidence, not a reference.
  • Pinned by TestRecipeT6LandryHV.

The compositions below read an implied-volatility index (VIX-style) as an external series iv beside the closes c, one value per session: caller-supplied series paired by position and right-aligned at the most recent bar, as the Breadth introduction (“Breadth inputs”) says for any caller series, so the caller lines them up in time first and a session with no trading is not an input bar. iv is in points, as the index is quoted (a VIX of 18.21 is 18.21% a year): iv.Div(100) gives the fraction, a thinkScript ImpVolatility() series, already a fraction, is multiplied by 100 first, and a rule written for fractions is 100 times off on points (the dossier’s spike c.Delta(1).Div(c.Shift(1).Mul(iv.Shift(1)).Div(math.Sqrt(252))) fed a VIX in points is 100 times too small). The tenor trap: the index prices the next 30 calendar days (Cboe’s methodology works in calendar time), which match about 21 trading days of realised volatility, not a year’s window, and the horizon tau and the annualisation ann come from the caller. iv is NaN on a session with no print: a rank or range whose window holds it is NaN, while a comparison with it reads 0; to carry the last print forward, as thinkScript’s V[1] does, use iv.ValueWhen(iv.EQ(iv)) (NaN before the first print). Values are NaN before their first valid index F (SetN(F, math.NaN())), and flags and counts built on masked values read 0 there. A ratio masks a zero divisor with MaskNonFinite(hv.Log()) where Div and Ratio read 0, a false extreme, and the exceedance test is written multiplied out, the move against the deviation (GT), not the move divided by it. Readings used as signals act at the next bar’s open, a fill that is the caller’s (the Trade State introduction). The gates on HistoricalVolatility alone, Kaufman’s annualised volatility and the short-over-long ratios of Connors and Raschke and of Landry, are on t6-volatility-gates.md; Natenberg’s four close-to-close forms are on t6-realised-volatility.md.

IV percentile and IV rank (Spina, Kulikov and Sosnoff; Lindgren)

Julia Spina, Anton Kulikov and Tom Sosnoff (The Unlucky Investor’s Guide to Options Trading, Wiley, 2022, ch. 3) define the IV percentile as “the percentage of days in the past year where the IV was below the current IV level”, the count over 252 (“Note that 252 is the number of trading days in a year”, eq. 3.1), and the IV rank as the current IV less the year’s minimum over the year’s range (eq. 3.2). Chris Lindgren (“Judging Volatility”, Stocks & Commodities V33:9, August 2015, pp. 26-31) contrasts the percentile, which “counts all the data points in a given range falling below your current observation”, with thinkorswim’s rank, which “only looks at the high and low of the range and then interpolates” and so “can be misleading if the data in a range is not evenly distributed or if there are extreme data points”. t6IVRank gives ivp, the share of the previous n sessions strictly below today’s value, exactly j/n, ivr, the rank over those n sessions and today, and two other counts: ivpIncl, the share at or below today’s value, and ivpLind, Lindgren’s printed fold.

ivp := iv.RankPosition(n+1).Mul(n).Round().Div(n).SetN(n, math.NaN())
ivr := iv.Stochastic(iv, iv, n+1).SetN(n, math.NaN())
ivpIncl := iv.Mul(-1).RankPosition(n+1).Mul(n).Round().Mul(-1).Add(n).Div(n).SetN(n, math.NaN())
ivpLind := iv.RankPosition(n).Mul(n-1).Round().Add(1).Div(n).SetN(n-1, math.NaN())
  • Inputs: iv in points; n = 252 previous sessions, Spina’s 252 trading days (Lindgren’s input period = 252 is a window that includes today). RankPosition(n+1) counts the values of its window strictly below the current one, which does not count itself, so j runs over the previous n; ivpIncl ranks iv.Mul(-1), whose values below today’s are those above it in iv, and takes that count from n.
  • First valid index: n (252) for ivp, ivr and ivpIncl, n - 1 (251) for ivpLind; NaN before. Unmasked, ivp reads +0 there (RankPosition’s warm-up), ivr 50 (Stochastic’s), ivpIncl 1 and ivpLind 1/n. A missing print makes every output NaN on the bars whose window holds it, from its bar to n bars later (n - 1 for ivpLind).
  • Anchors: Lindgren’s fictional year, 253 sessions (100 at 17, one at 12, 150 at 14, one at 40, today 16), gives ivp = 151/252 bit for bit (59.92%, his “60th percentile”), ivr = 14.285714285714285 (his ((16-12)/(40-12))*100 = 4/28*100 = 14), ivpIncl = 151/252 (no ties) and ivpLind = 152/252 (60.32%, also 60). For every j = 0..252 the repaired ivp is j/252 exactly. ivpIncl - ivp is the number of the previous n values equal to today’s, over n (0 without ties). One outlier print, such as Lindgren’s 177% on GOOG, moves ivp by at most 1/n but holds ivr down for the n bars after it. A flat window reads 0 in ivp and ivr. ivr equals Spina’s order, the difference over the range times 100, bit for bit, and thinkorswim’s 100*(V - low)/range to rounding.
  • Traps: the raw iv.RankPosition(253) is j/252 only to one ulp: its product of the rounded 1/252 and j misses j/252 in the last bit for 80 of the 253 values of j, so reading the IV percentile as exactly iv.RankPosition(253) fails a bit-for-bit test, and .Mul(n).Round().Div(n) repairs it. The dossier’s claims that no rolling self percent-rank exists (PercentRank(val, n) ranks a constant) and that RankPosition(253) “interpolates and includes the current value” are stale: RankPosition is that kernel and counts strictly below. Its iv.Stochastic(iv, iv, 252) beside iv.RankPosition(253) pairs windows one bar apart, and is “identical to thinkorswim IV rank” only to rounding and not on a flat window, where thinkScript divides by zero (NaN, a gap in the plot) and Stochastic reads 0. Lindgren’s text puts today “higher than 151 of the prior 252 trading days”, Spina’s count; his listing’s fold index = 1 to period with perRank = 1 stops before period (thinkScript’s to is exclusive), so it counts the previous 251 values from a seed of 1, which is ivpLind, 152/252 on the year: both display 60, and his TosIVRank over Highest(V, period) is the n-bar rank of the variant below.
  • Variant: thinkorswim’s and Lindgren’s rank over n bars including today, iv.Stochastic(iv, iv, n).SetN(n-1, math.NaN()), one bar shorter than ivr’s window; a platform’s “IV percentile” may be this rank (some platform descriptions say thinkorswim’s statistic is), so match a platform’s number against both.
  • Evidence: Spina’s Figure 3.4 labels SPY’s IVP at the end of 2015 as 74% with the VIX “roughly 18”: the Cboe VIX close of 31 December 2015, 18.21, is above 186 of the previous 252 closes with no ties, 186/252 = 73.81%, reproduced from Cboe’s daily history, which is not committed; her AMZN 67% (VXAZN “roughly 36”) is not reproduced, since the 31 December close, 36.44, gives 173/252 = 68.65% (69.05% with its one tie counted). Lindgren’s chart labels (percentile 75 against rank 8 on GOOG after the 177% print) are thinkorswim outputs, and Spina finds IVP “more robust than IVR because IVR is more sensitive to outlier moves” while her studies use raw IV rather than either. Evidence, not a reference.
  • Pinned by TestRecipeT6IVRank; runnable example ExampleSeries_RankPosition_ivPercentile.

IV/HV ratio and the variance risk premium (Lindgren; Natenberg)

Lindgren (“Judging Volatility”, Stocks & Commodities V33:9, August 2015, p. 28) prefers “the ratio of IV to HV” to their difference and plots “the ratio of IV to its 20-day historic volatility” with “a 252-day moving average of that figure”. Sheldon Natenberg (Option Volatility and Pricing, 2nd ed., McGraw-Hill, 2015, ch. 20, pp. 395-398) sets implied volatility against the realised volatility that followed and finds that it “seemed to predict a future volatility that was too high by up to 10 percentage points” (“Under normal conditions, implied volatility seems to be too high”), the gap the literature calls the variance risk premium. t6IVHV gives hv, the annualised sample deviation of the last n log returns in points, ratio, iv over hv, the premium vrp in volatility points and vrpVar in variance points, and fcErr, the realised volatility less the implied volatility quoted n bars before, Natenberg’s forecast error on the bar where it is known.

r := c.LogRatio(c.Shift(1))
hv := r.StdDevSample(n).Mul(ann).SetN(n, math.NaN()).Mul(100)
ratio := iv.Div(hv).MaskNonFinite(hv.Log())
vrp := iv.Sub(hv)
vrpVar := iv.Squared().Sub(hv.Squared())
fcErr := hv.Sub(iv.Shift(n))
  • Inputs: positive closes c and iv in points, paired by position; n returns, 20 for Lindgren’s “20-day historical volatility” or 21 to match the VIX’s 30 calendar days (Natenberg’s three-month implied volatility against 63 trading days, his 12-month against 252); ann, the square root of the bars per year, math.Sqrt(252) from the caller.
  • First valid index: n (20) for every output; NaN before. hv is masked to n, the first bar whose window holds n returns after bar 0’s NaN, and the others inherit it; iv.Shift(n) pads +0 on bars 0 to n - 1, which hv’s NaN covers. ratio is also NaN where hv is 0 or iv is NaN.
  • Anchors: hv is 100 times the hvS of t6HistoricalVolatility bit for bit, the same operations with the Mul(100) last (“Close-to-close historical volatility (Natenberg)” on the realised-volatility page gives that form and its anchors), and HistoricalVolatility shortens it to c.HistoricalVolatility(n).Mul(ann).SetN(n, math.NaN()).Mul(100), equal bit for bit. thinkorswim’s built-in HV, r.StdDev(n).Mul(math.Sqrt(252*20.0/19)).Mul(100) at n = 20 (the population deviation rescaled to the sample one), equals hv to rounding. Twenty unchanged closes (n zero returns) give hv 0 and ratio NaN, where iv.Div(hv) alone reads 0. ratio is unit-free with both in points, vrp is in volatility points and vrpVar in points squared, annual variance times 10,000.
  • Traps: the dossier’s c.Log().Delta(1).StdDev(21).Mul(math.Sqrt(252)*100) is the population deviation, reads bar 0’s zero return into the window ending at bar 20 as a finite, biased value, takes 21 returns where Lindgren takes 20, and duplicates t6HistoricalVolatility. hvS itself is a fraction: iv.Div(hvS) with iv in points is 100 times too large. Natenberg’s Figures 20-9 to 20-11 plot the realised volatility of the next three or twelve months against the implied volatility quoted on the same date, which reads that many bars ahead (research only); fcErr puts the same difference, his “future realized volatility minus implied volatility”, on the bar where it is known, and Lindgren’s shift of IV “20 bars to the right” is iv.Shift(20), causal too.
  • Variant: Lindgren’s 252-day average of the ratio (his study’s “(20, 252)”), ratio.Average(252).SetN(n+251, math.NaN()), first valid at n + 251.
  • Variant: Robert Kissell’s HMA-VIX forecast (Algorithmic Trading Methods, 2nd ed., Academic Press, 2021, ch. 11) scales a stock’s zero-mean trailing volatility by the index’s implied volatility over the index’s own trailing volatility, hc.Mul(v.Div(100).Div(hx)).Div(1.31).Shift(1), with v the VIX in points and hc and hx the hvS0 form of t6HistoricalVolatility on the stock and the index. It is prose only, with no helper, and departs from the book: the printed HMA is a variance, with no square root and no annualisation, the adjustment 1/1.31 is inferred from “correct for this premium”, n is not printed, and the single-lag reading (every input known at the close of the bar before) is chosen over the literal subscripts, which lag the volatilities a bar more than the VIX.
  • Evidence: the variance risk premium’s literature (Bollerslev, Tauchen and Zhou, 2009) is cited, not read here. Natenberg’s Figures 20-9 to 20-11 (S&P 500, 2002 to 2010) are descriptive plots on unprinted data, with 8 September 2008 at a three-month future volatility of 72% against an implied 22%; Lindgren’s SPX chart of 13 June 2014 labels the ratio 1.84391 against its 252-day average 1.38431 (“IV, at 12%”, HV “a mere 6%”), a thinkorswim output; Kissell writes that “the VIX usually trades at a premium of 1.31 to the SP500 trailing volatility”, with no sample or test. Evidence, not a reference.
  • Pinned by TestRecipeT6IVHV.

Expected-move bands and the exceedance share (Natenberg; Spina, Kulikov and Sosnoff; Balsara)

Natenberg (Option Volatility and Pricing, 2nd ed., McGraw-Hill, 2015, ch. 6, “Scaling Volatility for Time”, pp. 78-81) scales an annual volatility by the square root of time and, since “many traders assume that there are 256 trading days in a year”, writes “To approximate a daily standard deviation, we can divide the annual volatility by 16” (by 7.2, “the square root of 52”, for a week), expecting a price change of “more than” one standard deviation “about one day in three”. Spina, Kulikov and Sosnoff (The Unlucky Investor’s Guide to Options Trading, Wiley, 2022, ch. 2, eq. 2.2, and App. II, eqs. A.5 and A.6) multiply the price by IV and the square root of the calendar days over 365, and Nauzer Balsara (Money Management Strategies for Futures Traders, Wiley, 1992, ch. 6, “Volatility Stops”, pp. 93-94) would “divide the annualized volatility estimate by the squareroot of 250/n”. t6ExpectedMove gives the linear bands upL and dnL, k standard deviations of the horizon’s move either side of the close, the lognormal bands upG and dnG (Natenberg’s App. B, p. 555, centred on the close rather than his forward price), and share, the m-bar share of bars whose absolute change is more than the previous bar’s one-day standard deviation.

sig := iv.Div(100)
sd := c.Mul(sig).Mul(tau)
upL := c.Add(sd.Mul(k))
dnL := c.Sub(sd.Mul(k))
upG := c.Mul(sig.Mul(k * tau).Exp())
dnG := c.Mul(sig.Mul(-k * tau).Exp())
sd1 := c.Mul(sig).Div(ann)
share := c.Delta(1).Abs().GT(sd1.Shift(1)).Average(m).SetN(m, math.NaN())
  • Inputs: positive closes c and iv in points, paired by position; tau, the square root of the horizon in years, from the caller: 1/16 for Natenberg’s day, 1/7.2 for his week, math.Sqrt(30.0/365) for Spina’s 30 calendar days, 1/math.Sqrt(250.0/5) for Balsara’s five days of 250, 1 for a year; ann, the square root of the bars per year (16 for Natenberg, or math.Sqrt(252)); k standard deviations (1, 2 or 3); m bars, where Natenberg’s traders “prefer to see larger price samplings, perhaps 20 days, or 50 days, or 100 days”.
  • First valid index: 0 for the four bands (pointwise, NaN where iv is NaN) and m for share; NaN before. Unmasked, Average passes the raw 0/1 flags through on bars 0 to m - 2, and bar 0’s flag compares Delta’s 0 with the Shift pad’s +0.
  • Anchors: Natenberg’s 45-dollar stock at 37% with tau 1/16 gives a deviation of 1.040625, his 1.04 dollars, and bands (to rounding) 43.959375 to 46.040625 at k 1 and 42.91875 to 47.08125 at k 2, his 43.96 to 46.04 and 42.92 to 47.08; with tau 1/7.2 the week’s 2.3125 and bands 42.6875 to 47.3125 (his 2.31 and 42.69 to 47.31), and 40.375 to 49.625 at k 2. On his 100 at 20%, upL less 100 is 1.25 with tau 1/16, 2.7777777777777715 with 1/7.2 and 5.5555555555555571 with 1/7.2 at k 2 (his 1.25, 2.78 and 5.56). His five changes from 45 (ann 16, m 5), which are +0.98, -0.65, -0.70, +0.25 and -0.85, leave share NaN on bars 0-4 and 0 at bar 5: no move is above the deviation (he holds it at 1.04 dollars, the helper rescales it by each previous close, with the same verdict). On Spina’s 100 at 20% over 30 calendar days upL less 100 is 5.7338217908099551 (her 5.73), and on her 275 at 19% over 45 days upL less 275 is 18.346199822993469 (her 6.7% and 18 dollars). Balsara’s cocoa at 1,000 and 25% over five days moves 3.536%, 35.355339 (his 3.536% and 35.36), with bands 1035.3553390593274 and 964.64466094067257 (his 1035.36 and 964.64) and, at k 3, 1106.0660171779821 and 893.93398282201792; over a year (tau 1) 750 to 1,250 and, at k 3, 250 to 1,750. A change of exactly one previous one-day deviation does not count: 1.25 on a close of 100 at iv 20 and ann 16 gives a 0 flag (strict GT). upG.Mul(dnG) is c squared to rounding, and the lognormal band lies above the linear one, 43.9713 to 46.0528 against 43.9594 to 46.0406.
  • Traps: the dossier’s c.Mul(iv.Div(100)).Div(math.Sqrt(252)) gives 1.048851, 1.05 dollars, where Natenberg prints 1.04: his rule is the 256-day year, tau 1/16 and ann 16. Spina’s 30 calendar days are not 21 of 252 trading days, which give 5.77 for her 5.73. The dossier’s exceedance c.Delta(1).Abs().GT(sd.Shift(1)).Average(n) lacks the mask, so its first n - 1 bars are raw flags and bar 0’s a fake 0, and its “(A.6) lognormal” without a rate is Spina’s note 4, where (A.6) itself is the price times e^(rt +/- sigma sqrt t). A missing print makes the next bar’s flag 0, a comparison with NaN, so share counts that bar as a quiet one: carry the last print forward first.
  • Variant: Lindgren’s price spike in standard deviations against the previous bar’s deviation, c.Delta(1).Div(sd1.Shift(1)).SetN(1, math.NaN()), and his 252-bar count of spikes of 3 or more, inclusive (his labels read “Count Deviations >= 3”), c.Delta(1).Div(sd1.Shift(1)).SetN(1, math.NaN()).Abs().GTE(3).Sum(252).SetN(252, math.NaN()); his IV label shows the current bar’s IV, which reacts to the move itself.
  • Variant: Spina’s platform expected move (App. II, eq. A.7) needs option prices, not an IV: with caller series of the at-the-money straddle st0 and the strangles one and two strikes out, st1 and st2, st0.Mul(0.6).Add(st1.Mul(0.3)).Add(st2.Mul(0.1)).
  • Evidence: Spina’s Table 2.2 finds that IV overstated the realised move for SPY 87% of the time from 2016 to 2021, where “theory predicts” about 68%, and 70% to 89% for six assets; the horizon of the move is not stated and the table is descriptive. Natenberg’s weekly two-deviation band, 40.38 to 49.62, doubles his rounded 2.31, and Balsara’s three-deviation band, 1106.08 and 893.92, triples his rounded 35.36: the printed bands are a cent off the helper’s 40.375 to 49.625 and 1106.066 and 893.934, which the test asserts. Evidence, not a reference.
  • Pinned by TestRecipeT6ExpectedMove; runnable example ExampleSeries_Exp_expectedMove.

Bid-Ask Spread Estimators

CorwinSchultzSpread

func (s Series) CorwinSchultzSpread(h, l, c Series, n int) Series

CorwinSchultzSpread estimates the bid-ask spread from the high, low and close of each bar by the estimator of Corwin and Schultz 2012, Journal of Finance 67(2), which the R package bidask calls CS2: each pair of consecutive bars gives an estimate that is clipped at 0, the n clipped estimates are averaged, and the paper’s overnight-gap adjustment is applied. The paper is “A Simple Way to Estimate Bid-Ask Spreads from Daily High and Low Prices”. The receiver is not used.

Pair t, for t >= 1, is the pair of bars t-1 and t with the prior close c[t-1]. With hl = ln(h) - ln(l) the log range of a bar, beta = hl[t]^2 + hl[t-1]^2 is the sum of the two bars’ squared log ranges (the per-bar Parkinson terms of ParkinsonVolatility), and gamma = (max(ln h[t] + g, ln h[t-1]) - min(ln l[t] + g, ln l[t-1]))^2 is the squared two-bar log range after the gap adjustment g. The paper’s alpha is (sqrt(2 beta) - sqrt(beta)) / (3 - 2 sqrt(2)) - sqrt(gamma / (3 - 2 sqrt(2))), evaluated here as the equal (1 + sqrt(2)) (sqrt(beta) - sqrt(gamma)), with 1 + sqrt(2) the float64 2.4142135623730949; a negative alpha is set to 0, the paper’s rule for negative two-day spreads; and the paper’s S = 2 (exp(alpha) - 1) / (1 + exp(alpha)) is evaluated as the equal 2 tanh(alpha/2), which keeps its accuracy for a small alpha.

The gap adjustment (the paper’s section II.A) is g = max(0, ln c[t-1] - ln h[t]) + min(0, ln c[t-1] - ln l[t]). It acts only when c[t-1] lies outside [l[t], h[t]], a low above the prior close giving g < 0 and a high below it giving g > 0; it shifts bar t’s logarithms by g, so that the bar touches the prior close, and leaves bar t’s own log range unchanged. It corrects overnight gaps and so rarely acts on intraday bars.

The window n counts two-bar pairs: the value at bar t is the mean of the estimates of pairs t-n+1 .. t, which read bars t-n .. t and closes t-n .. t-1, and n = 1 gives the per-pair estimate. For comparison, the width of the R package bidask is n + 1, as it counts bars; the sl of Lopez de Prado’s Snippet 19.1 averages beta only and equals n only at 1; and a month of daily bars is about n = 20.

The value is a proportion of price in [0, 2) in exact arithmetic (float64 reaches 2 only for a bar range of a factor of about 3.6e16), where 0.01 is a spread of 1%; it is not a per-bar rate and is not annualised. Two identical bars, with the prior close inside them, give beta = 2 hl^2 and gamma = hl^2, hence alpha = hl and a value of 2 (h - l) / (h + l), all of the range read as spread; a high of 101 and a low of 99 give 0.02 to within 1e-14. The estimate is an input for trading costs and liquidity regimes; the sources give it no evidence as a source of returns.

Inputs h, l and c of unequal lengths give an empty Series, whatever n is. The three inputs are one bar set, so they are not right-aligned as the base and comparison series of the relative methods are. Any n < 1 or n >= len(h) gives len(h) zeros, as a value needs n + 1 bars.

Bars 0 .. n-1 are the warm-up and are +0, not NaN, and the first valid index is n. A value of 0 is also a legitimate reading, given by clipped pairs and flat bars, so mask the warm-up with SetN(n, math.NaN()) before reading 0 as no spread.

A high or low that is zero, negative, NaN or +Inf at bar p makes pairs p and p+1 NaN, and with them the values at bars max(p, n) .. p+n, as far as the series runs; such a close at bar p spoils pair p+1 only, and with it the values at bars max(p+1, n) .. p+n. Later values recover, every other value keeps its bits, the last close is not read, and a positive subnormal price is valid. Bars are not otherwise validated: a bar with h < l is computed as it is and gives a finite value that is not negative, and a close outside its own bar is accepted, as it is read only as the prior close of the next pair. A flat bar (h = l) gives exactly +0 for its pair when the other bar has h >= l, so a window of flat bars is +0; the paper’s substitutions for infrequent trading (its section II) are not applied.

Other variants are not provided. Lopez de Prado’s Snippet 19.1 (Advances in Financial Machine Learning, 2018), and mlfinlab after it, average beta over the window before forming alpha, clip after and make no gap adjustment, a different estimator for n > 1 that equals this one at n = 1 when every close lies within the next bar’s range. The CS of bidask averages unclipped estimates and by default returns the absolute value of the mean. The unadjusted form, without the gap adjustment, is not provided either, nor is the Corwin-Schultz volatility of Snippet 19.2 and the paper’s spread-adjusted variance estimator, where the book’s sqrt(8/pi) and the paper’s 1/(4 ln 2) give values that differ by a factor of about 1.0435.

Each value costs O(n) and is computed afresh from its own window, with no running sum, so the method is window-local and start-invariant: dropping leading bars leaves every later value’s bits unchanged. The arithmetic after the logarithms rounds every product on its own, but math.Log and math.Tanh can differ in the last bit between amd64 and arm64, and so can the values: by a few units in the last place in absolute terms, which the cancellation in alpha can make a large relative difference in a small estimate.

RollSpread

func (s Series) RollSpread(n int) Series

RollSpread returns Roll’s estimate of the bid-ask spread from the serial covariance of the receiver’s one-bar changes over the trailing n pairs of changes. It is the measure of Roll 1984 (“A Simple Implicit Measure of the Effective Bid-Ask Spread in an Efficient Market”, Journal of Finance 39(4)): bid-ask bounce makes successive changes negatively correlated, with cov(d[t], d[t-1]) = -S^2/4 for a spread S, so that S = 2 sqrt(-cov).

With d[t] = s[t] - s[t-1], the value at bar t is 2 sqrt(max(0, -cov)), where cov is the population covariance, divisor n, of the n pairs (d[i], d[i-1]) for i = t-n+1 .. t. It is computed through the composition d := s.Delta(1); cv := d.Covariance(d.Shift(1), n), and from bar n+1 it equals s.Delta(1).Covariance(s.Delta(1).Shift(1), n).Mul(-1).Max(0).Sqrt().Mul(2) bit for bit.

The value is the full spread, as in Roll, in Corwin and Schultz, in bidask and in mlfinlab; the c of Lopez de Prado’s Advances in Financial Machine Learning (2018), section 19.3.2, is the half-spread, half of this value. A window whose covariance is positive gives 0, following the rule that positive correlations imply zero spreads, rather than the 2 sqrt(|cov|) of mlfinlab and of bidask’s default or a signed root.

The bidask and mlfinlab packages use the sample covariance, divisor n-1: where cov < 0 their value is sqrt(n/(n-1)) times this one, and where cov > 0 they give 2 sqrt(|cov|) against 0 here. No sample sibling is provided. Roll’s efficient-price variance is not exported either; with cv as above it is d.StdDev(n).Squared().Add(cv.Mul(2)), valid from bar n+1.

The receiver is used as given and the value is in the units of its changes: raw prices give a spread in price units, and s.Log() gives a proportional spread, bidask’s form and the percentage form of Roll’s own estimates, in which 0.01 is 1%. The estimate is an input for trading costs and liquidity regimes; the sources give it no evidence as a source of returns.

A bounce s[t] = m + c(-1)^t, with c > 0 and a full spread of 2c, gives, in exact arithmetic, 4c for even n, twice that spread, because an alternation is not Roll’s independent trade direction, and 4c sqrt(1 - 1/n^2) for odd n; with c = 0.5 the value is 2 for even n and 1.8856180831641267 for n = 3.

Any n < 2 or n > len(s)-2 gives len(s) zeros, as n pairs of changes need n + 1 changes and so n + 2 bars. Bars 0 .. n are the warm-up and are +0, not NaN, and the first valid index is n+1. The method writes those zeros itself, because the composition reads Delta’s 0 at bar 0 and Shift’s 0 pad there. A value of 0 is also a frequent legitimate reading, a quarter to a half of the values of a random walk, wherever the window’s covariance is positive, so mask the warm-up with SetN(n+1, math.NaN()) before reading 0 as no spread.

A NaN or an infinity at bar p makes the changes d[p] and d[p+1] non-finite, and with them the values at bars max(p, n+1) .. p+n+1 NaN; later values recover, and every other value keeps its bits. A change that overflows to an infinity does the same for every window that holds it. Any finite value is valid: zero and negative raw prices are computed as they are, while s.Log() turns them into an infinity or NaN, which gives that footprint. A window whose sum of products exceeds the float64 range gives a covariance of +Inf or -Inf, as Covariance documents, and so a value of 0 or +Inf.

Each value costs O(n) and depends only on its own window, so the method is window-local and start-invariant. Covariance rounds every product on its own and math.Sqrt is correctly rounded, so the bits are the same on every platform for the same input; s.Log() calls math.Log, which can differ in the last bit between amd64 and arm64.

Compositions

The price-impact measures below mask their values with NaN before their first valid index, where bar 0 has no return and the windows are partial, and a zero-volume or unchanged window that would divide by zero is NaN, not the 0 that Div gives. Prices are positive and finite, volumes and dollar volumes non-negative and finite, and lengths are in bars; the measures are in the units of the dollar volume passed in.

Amihud’s illiquidity and the Amivest liquidity ratio (Amihud; Fosback)

Yakov Amihud (“Illiquidity and stock returns: cross-section and time-series effects”, Journal of Financial Markets 5, 2002, pp. 31-56, eq. (1)) defines illiquidity as “the average ratio of the daily absolute return to the (dollar) trading volume on that day”, “the daily price response associated with one dollar of trading volume”, and relates it to the Amivest measure, “the ratio of the sum of the daily volume to the sum of the absolute return”. Norman Fosback’s liquidity beta (Stock Market Logic, 1976, ch. 62, pp. 244-245), “the number of dollars of volume that has historically been required to move a stock up or down 1%”, is that ratio with the change in percent. t6Illiquidity returns the mean over n bars of |r|/DV, r the log return from LogRatio, and the ratio of the n-bar sums of DV and of the absolute percent changes, in dollars per 1%.

r := c.LogRatio(c.Shift(1))
illiq := r.Abs().Div(dv).MaskNonFinite(dv.Log()).Average(n).SetN(n, math.NaN())
mv := c.RateOfChange(1).Abs().Sum(n)
liq := dv.Sum(n).Div(mv).MaskNonFinite(mv.Log()).SetN(n, math.NaN())
  • Inputs: closes; the bar’s dollar volume dv (c.Mul(v) is the proxy; a vendor’s dollar volume where one exists); n bars (Amihud averages over a year; mlfinlab’s code uses 20 and Jansen’s 21).
  • First valid index: n for both, the value of the masks; LogRatio is NaN at bar 0, where Log().Delta(1) gives a fake 0 return. MaskNonFinite(dv.Log()) makes the ratio NaN on a zero-volume bar (its log is -Inf), and on an infinite dollar volume or a NaN close too, so the n windows holding it are NaN in illiq, where Div alone would count a 0, and MaskNonFinite(mv.Log()) makes a window of unchanged closes NaN in liq, where Div would read no liquidity.
  • Anchors: on openquant’s hand vector (closes 100, 110 and 99, dollar volume 1e6, n = 2) illiq is (ln 1.1 + |ln 0.9|)/2e6 to rounding of the logarithm and liq is 99,999.99999999997; LogRatio equals Log().Delta(1) bit for bit from bar 1. With the simple return R, the percent change over 100, 1/(100 liq) is the dollar-volume-weighted mean of the daily ratios |R|/DV, rr.Div(dv).VWMA(dv, n) for rr = |R|, to rounding on windows with volume on every bar (a zero-volume bar drops out of the VWMA form and stays in the ratio of sums), where illiq is their equal-weighted mean, so the two measures have no exact identity.
  • Traps: Amihud’s return is CRSP’s simple daily return; c.RateOfChange(1).Div(100) in place of r changes the measure by the second-order term R^2/2 and reads 0, not NaN, at bar 0.
  • Variant: Amihud’s scale is illiq.Mul(1e6), after the average (his “multiplied by 10^6”; 1e6 is not a power of two, so scaling before the average differs in the last bit).
  • Variant: over the last n bars with volume, Amihud’s “days for which data are available” read as a count of trading days rather than a calendar window: r.Abs().Div(dv).AverageWhere(dv.GT(0), n), held on a zero-volume bar.
  • Evidence: Amihud’s cross-section (NYSE stocks, 1963 to 1996) supports the measure’s premium, not a trading rule, and Fosback states his view without data (“Experience shows that, in fact, they do just that”). Evidence, not a reference.
  • Pinned by TestRecipeT6Illiquidity.

Kyle’s and Hasbrouck’s lambdas on bars (Lopez de Prado)

Marcos Lopez de Prado (Advances in Financial Machine Learning, Wiley, 2018, ch. 19, pp. 286-290) estimates Kyle’s lambda by regressing the price change on the signed volume b_t V_t, and Hasbrouck’s by regressing the log return on the signed square root of dollar volume, both without an intercept, and finds “that the t-values associated with these microstructural estimates are more informative than the (mean) estimates themselves”. t6PriceImpact fits both by BetaOrigin over n bars and composes Kyle’s t-value with n - 1 degrees of freedom from window sums, the book’s coefficient over the square root of its variance (Snippet 17.4, p. 259). The trade sign b is an input: the book’s tick rule on closes is b := c.Delta(1).Hysteresis(0, 0) (0 until the first change; its “b_0 is arbitrarily set to 1” is c.Delta(1).SetN(1, 1).Hysteresis(0, 0)), and a vendor’s net volume sv gives sv.Div(sv.Abs()).

r := c.LogRatio(c.Shift(1))
dp := c.Delta(1)
x := b.Mul(v)
kyle := dp.BetaOrigin(x, n).SetN(n, math.NaN())
sxy := dp.Mul(x).Sum(n)
t := sxy.Mul(math.Sqrt(float64(n-1))).Div(x.Squared().Sum(n).Mul(dp.Squared().Sum(n)).Sub(sxy.Squared()).Sqrt()).
    SetN(n, math.NaN())
hasb := r.BetaOrigin(b.Mul(dv.Sqrt()), n).SetN(n, math.NaN())
  • Inputs: closes; the trade sign b (+1, -1 or 0); volumes and dollar volumes; n >= 2 bars (mlfinlab’s 20). A zero-volume bar is a zero regressor row, which drops out of a fit through the origin, so the lambdas need no mask there.
  • First valid index: n for all three, the value of the masks, so that each window holds n real changes; hasb is NaN while a window holds bar 0’s NaN return.
  • Anchors: kyle equals the ratio of window sums dp.Mul(x).Sum(n).Div(x.Squared().Sum(n)), here bit for bit, and on openquant’s hand vector of price changes 0.2, -0.1 and 0.4 on signed volumes 10, -5 and 20 the slope is 0.02; the t-value is within 1e-14 relative of exact rational sums away from a perfect fit.
  • Traps: with b from the bar’s own close change both lambdas are sign-free: kyle is dp.Abs().BetaOrigin(v, n) and hasb is r.Abs().BetaOrigin(dv.Sqrt(), n) bit for bit once b is set, so they measure the size of a move per unit of volume; on bars the book’s b is “the volume-weighted average of the trade signs” (exercise 19.5). The t-value loses accuracy as the fit nears perfection: an exact fit on integers gives 0 (Div by an exact 0) where the t-value is infinite, and a near-perfect one in binary64 a huge finite value, such as 88,080,384 on the hand vector, or NaN; realistic windows sit far from that (an uncentred r-squared of at most 0.83 on mlfinlab’s E-mini dollar bars). Hasbrouck’s regressor b sqrt(DV) stands in for the book’s within-bar sum of b sqrt(pV).
  • Variant: mlfinlab’s bar forms are means of ratios, dp.Abs().Div(v).Average(n) for Kyle and r.Abs().Div(dv.Sqrt()).Average(n) for Hasbrouck, and its “t-value” is the coefficient over its variance, not over its standard error.
  • Variant: the book’s Amihud regression, r.Abs().BetaOrigin(dv, n).SetN(n, math.NaN()).
  • Evidence: the book’s histograms of the three lambdas on E-mini S&P 500 futures (Figures 19.1 to 19.3) state no window or bar, and its preference for t-values is the author’s observation (“Although the literature does not mention this observation”). Evidence, not a reference.
  • Pinned by TestRecipeT6PriceImpact; runnable example ExampleSeries_BetaOrigin_kyleLambda.

Ulcer Index

Ulcer

func (s Series) Ulcer(n int) Series

Ulcer calculates the Ulcer Index on the provided series over an n-period term.

The output is in percent, the drawdowns times 100. The warm-up reads 0, not NaN: bars 0 to n-2 are 0, which reads as no drawdown.

Each drawdown is measured from the running high since the window’s first bar, as Martin and McCann define the index; a running high of 0 contributes no drawdown and is replaced by that bar’s value. StockCharts and pandas-ta’s ui measure each bar’s drawdown from its own n-bar high instead, c.Div(c.Highest(n)).Sub(1).Mul(100).Squared().Average(n).Sqrt() from bar 2n-2, so the two generally differ.

Bollinger Bands

Bollinger

func (s Series) Bollinger(length int, nbrStdDevs float64) Series

Bollinger returns a smoothed series adjusted by a specified number of standard deviations (which could be negative) determined from the specified previous number of bars.

Bars 0 to length-2 pass the input through, since Average passes it through and StdDev is 0 there, so the band equals the price on those bars.

Poster’s indicator deviation bands put UpsideSemiDeviation above the average and DownsideSemiDeviation below it in place of this one deviation; the recipe is under UpsideSemiDeviation.

BollingerX

func (s Series) BollingerX(length int, nbrStdDevs float64) Series

BollingerX returns a smooth series (where the smoothing function is an exponential moving average rather than the standard simple moving average) adjusted by a specified number of standard deviations (which could be negative) determined from the specified previous number of bars.

Bars 0 to length-2 pass the input through, since XAverage passes it through and StdDev is 0 there, so the band equals the price on those bars.

Compositions

The band compositions below mask their values with NaN before their first valid index, where Bollinger and Average pass the input through and the windows are not yet full; flags and positions built on masked values read 0 there. Prices are positive and finite, lengths are in bars, band multipliers are in standard deviations and envelope widths are fractions (0.02 for 2%). The squeezes on these bands, BandWidth at its six-month low, Katsanos’s normalised bandwidth and the bands inside Keltner Channels, are on t6-squeeze.md, which reads width from the %b section below; Pruitt’s initial-risk gate on the band’s half-width is on t6-initial-risk-gate.md.

BBTrend (Bollinger; TradingView’s definition)

John Bollinger, interviewed by Jayanthi Gopalakrishnan and Bruce Faber (“System Trading Made Easy With John Bollinger”, Stocks & Commodities V30:3, March 2012, pp. 36-42 and 71), built BBTrend on how “Bollinger Bands of 20 periods and 50 periods” align in a trend, and says it “also recognizes the direction of the trend”. The formula is not printed in the magazine, and bbands.com does not print it; TradingView’s help page (“BBTrend”, fetched 15 October 2026) gives (math.abs(shortLower - longLower) - math.abs(shortUpper - longUpper)) / shortMiddle * 100, with one deviation input for both pairs and, in its screenshot, lengths 20 and 50 and deviation 2. t6BBTrend is that definition.

up1, lo1 := c.Bollinger(n1, k), c.Bollinger(n1, -k)
up2, lo2 := c.Bollinger(n2, k), c.Bollinger(n2, -k)
bbt := lo1.Sub(lo2).Abs().Sub(up1.Sub(up2).Abs()).Div(c.Average(n1)).Mul(100).SetN(n2-1, math.NaN())
  • Inputs: closes; n1 < n2 (20 and 50) and one k (2) for both pairs; population standard deviations, as Bollinger and TradingView use.
  • First valid index: n2 - 1 (NaN before). Unmasked, bars 0 to n1 - 2 read exactly 0, because every band is the close, and bars n1 - 1 to n2 - 2 are nonzero, because the long bands still pass the close through.
  • Anchors: with d the gap between the two averages and e = k(sd1 - sd2) the gap between the half-widths, BBTrend is -2 sgn(d) sgn(e) min(|d|, |e|) / m times 100, m the short average, to rounding. Its sign is therefore the trend’s, d’s, only while the short band is the narrower, and it reverses when short-term volatility exceeds long-term; it is 0 when the averages or the deviations are equal. c.Bollinger(n, -k) is the lower band bit for bit.
  • Traps: the interview’s “It runs from +1 to -1” describes neither the plain ratio nor the scale times 100 (Bollinger’s own chart reads 2.47 on an axis marked to 12).
  • Variant: the plain ratio, without TradingView’s factor of 100, is bbt.Div(100).
  • Variant: Bollinger’s rules widen longer bands (“from 2 at 20 periods, to 2.1 at 50 periods”); the pair c.Bollinger(n2, 2.1) and c.Bollinger(n2, -2.1) as up2, lo2 changes the values.
  • Evidence: the interview’s GLD chart reads “BBTrend 2.47” and TradingView’s screenshot -3.64 on AAPL, platform outputs on unprinted data. Evidence, not a reference.
  • Pinned by TestRecipeT6BBTrend.

%b, BandWidth, BB Impulse and BB Momentum (Bollinger)

Bollinger (“Using Bollinger Bands”, Stocks & Commodities V10:2, February 1992, pp. 47-51) defines “%b = (close - lower band) / (upper band - lower band)” and “Band width = (upper band - lower band) / middle band”. In the 2012 interview BB Impulse is “%b this period - %b last period”, and BB Momentum is “the price change over a period, say of 10 periods, as a function of the bands”, printed in BBScript as BBmtm = mtm / (upperBB - lowerBB). t6BollingerSiblings gives the four, with %b and BandWidth as fractions.

up, lo := c.Bollinger(n, k), c.Bollinger(n, -k)
pctB := c.Sub(lo).Div(up.Sub(lo)).SetN(n-1, math.NaN())
width := up.Sub(lo).Div(c.Average(n)).SetN(n-1, math.NaN())
impulse := pctB.Delta(1).SetN(n, math.NaN())
mom := c.Momentum(m).Div(up.Sub(lo)).SetN(max(n-1, m), math.NaN())
  • Inputs: closes; n and k (20 and 2); m for BB Momentum, the caller’s (“It could be a five-day rate of change, or 10 days, or 20”; his BBScript uses 10).
  • First valid index: n - 1 for pctB and width, n for impulse, max(n - 1, m) for mom; NaN before. Unmasked, pctB is 0/0, which Div gives as 0, on bars 0 to n - 2 (the bands are the close there); the unmasked impulse reads +0 at bar 0 (Delta’s pad) and NaN on the other warm-up bars, so SetN(n-1, math.NaN()) gives the same series; with m above n - 1 the unmasked mom reads 0 on bars n - 1 to m - 1, a false “no momentum”.
  • Anchors: on Bollinger’s 1992 DJIA sidebar (24 September to 1 November 1991), Average(20), StdDev(20) and Bollinger(20, 2) and (20, -2) of the typical price match all 45 printed cells within 0.005 + 1e-9 (row 26’s average is exactly 3011.785 in decimal, so rounding equality is not defined there), and the sample deviation misses every printed deviation. pctB equals c.Standardise(n).Div(k + k).Add(0.5) to rounding except on a flat window, where pctB, width and mom are 0 and the z-score form 0.5. width is 2k times the coefficient of variation (“Using the default parameters BandWidth is four times the coefficient of variation”, his rule 18).
  • Traps: the 1992 caption scales %b by 100 (“At 100 we are at the upper band”); the interview’s words and his rules use 1 and 0, while its GLD chart (and the “Percent b(20,2) 61.87” below) prints the 100 scale.
  • Variant: the percent scales are pctB.Mul(100) and width.Mul(100).
  • Evidence: his GLD sidebar reads “Percent b(20,2) 61.87” and “BandWidth(20,2) 3.67” on unprinted data. Evidence, not a reference.
  • Pinned by TestRecipeT6BollingerSiblings.

Percentage envelope and the JM internal band (van der Merwe)

Koos van der Merwe (“Less Stress With The JM Internal Band”, Stocks & Commodities V28:13, December 2010, pp. 10-16 and 69) draws “a 15-period simple moving average (SMA) of a stock and offset it upward by 2% and downward by -2%”, takes “a long position when the close breaks above the upper band and a short position when it breaks below the lower band”, and suggests “the break above (below) the 15-period SMA as a sell (buy) stop”. His MetaStock buy is Close > UpBd and Ref(C,-1)<Ref(UpBd,-1), strict on both bars. t6Envelope gives the envelope, TradingView’s Envelope with a simple basis, and his position with the exits he suggests.

m := c.Average(n).SetN(n-1, math.NaN())
up, lo := m.Mul(1+p), m.Mul(1-p)
buy := c.GT(up).Mul(c.Shift(1).LT(up.Shift(1)))
short := c.LT(lo).Mul(c.Shift(1).GT(lo.Shift(1)))
pos := c.PositionSigned(buy, c.LT(m), short, c.GT(m))
  • Inputs: positive closes; n in bars (15); p as a fraction (0.02). The percentage is of the average, not of the price.
  • First valid index: n - 1 for m, up and lo (NaN before, where MetaStock’s average is undefined); buy, short and pos read 0 through bar n - 1, so the first possible signal is bar n. Shift(1) pads bar 0 with 0 on both sides, and 0 < 0 does not hold.
  • Anchors: c.CrossesAboveS(up) equals buy except after a close exactly on the band, since its previous bar may equal the band: on the test’s walk the closes placed on the bands at bars 239, 289, 369 and 419 are followed by closes beyond them where buy and short read 0 and the house crosses fire. Level entries, c.PositionSigned(c.GT(up), c.LT(m), c.LT(lo), c.GT(m)), give the same position as CrossesAboveS and CrossesBelowS entries from the first exit on: after a long’s exit the close is below the average and so below the upper band, so its next close above the band is a cross, and the same holds for shorts.
  • Traps: his tested exits are unpublished; the average’s exits are his suggestion, which George Arrington’s percentage-envelope filter also uses (“The Basics Of Moving Averages”, Stocks & Commodities V10:6, June 1992).
  • Variant: the exponential basis of TradingView and Durenard, c.XAverageAlpha(2.0 / float64(n+1)), valid from bar 0.
  • Variant: a width calibrated on the past year, after Chaikin’s Bomar bands (in Bollinger’s 1992 article) and Leigh Stevens’s “percent figure that contains within it 90% of the price swings” (Stocks & Commodities V22:8, August 2004): h.Div(m).Sub(1).Quantile(252, 0.9).SetN(n+250, math.NaN()) with m masked first, first valid n + 250 (Quantile’s own warm-up reads +0). Stevens himself fits 3-4% by eye.
  • Evidence: his MetaStock tests over the year to August 2010 (unpublished exits and costs) list gains of up to 46.99% long and 75.10% short per stock, and 5 of the 19 visible short rows show a losing trade. Evidence, not a reference.
  • Pinned by TestRecipeT6Envelope; runnable example ExampleSeries_PositionSigned_jmBand.

AMATR1 trend state (Durenard)

Eugene Durenard (Professional Automated Trading: Theory and Practice, Wiley, 2013, ch. 7, pp. 109-110) builds “a symmetric channel around an exponential moving average”, H(t) = (1 + beta)EMA(alpha, t) and L(t) = (1 - beta)EMA(alpha, t), with the average seeded by the first price, EMA(alpha, 0) = P(0): “One defines the market to be in an uptrend at time t if P(t) > H(t) and in a downtrend if P(t) < L(t).” t6AMATR1 holds the state between the bands.

e := c.XAverageAlpha(alpha)
state := c.GT(e.Mul(1+beta)).Sub(c.LT(e.Mul(1-beta))).Hysteresis(0, 0)
  • Inputs: positive closes; alpha in (0, 1], his grid 2/(N + 1) for N = 4, 6, …, 20; beta 0.02 to 0.10.
  • First valid index: 0; the state reads 0, his Init, until the first close outside the channel.
  • Anchors: the state equals a loop of the prose rule bit for bit; the two flags cannot both be 1 while e > 0 and beta > 0. The ratio form c.Div(e).Sub(1).Hysteresis(beta, -beta) agrees on the test’s data but compares a rounded quotient, so a tie can differ.
  • Traps: Figure 7.4’s brackets are inclusive, unlike the prose; that reading is c.GTE(e.Mul(1+beta)).Sub(c.LTE(e.Mul(1-beta))).Hysteresis(0, 0). On the closes 8, 24, 100, 38, 16, 4 with alpha and beta 0.5, which tie the upper band at bar 1 and the lower at bar 4, the strict state is 0, 0, 1, 1, 1, -1 and the inclusive one 0, 1, 1, 1, -1, -1. c.XAverage(n) in place of XAverageAlpha passes the input through and seeds with a simple average; Figure 7.3’s “EMA(1/5)” is a five-bar average, alpha 1/3.
  • Evidence: Figure 7.4 promises “representative statistics” that are not printed; AMATR otherwise appears only as a family in his fitness-control study (500 strategies over 200 paths) and a swarm example, neither of which reports its performance. Evidence, not a reference.
  • Pinned by TestRecipeT6AMATR1.

The gate below takes Pruitt’s dollar limit in price units: 2,000 dollars on a contract of 1,000 dollars a point (crude oil, his example) is a limit of 2 points, the point value being the caller’s conversion, as on the trade-state pages. The band system it gates is Faith’s Bollinger Breakout of donchian-channels/t6-breakout-systems.md (“Volatility channel breakouts (Faith; Penfold)”) at 60 bars and two standard deviations, with Pruitt’s cross entries and his fills at the close instead of the next open. Values are NaN before their first valid index and the flags and the book read 0 there; prices are positive and finite and lengths are in bars.

Initial-risk entry gate (Pruitt)

George Pruitt (The Ultimate Algorithmic Trading System Toolbox, Wiley, 2016, ch. 2, “Bollinger Bands”, pp. 62-68) trades 60-day, two-deviation bands as a trend follower and, in his version 2, filters the entries by their initial risk: “our initial trade risk can be defined as the distance between the upper band and the moving average. If we equate this distance to dollars, then we can filter out any trades that exceed our acceptable risk amount”; “If filterTrade is on or true, the trade entry logic is bypassed, thereby skipping the trade”. His AFL (ch. 4, pp. 141-142) enters on crosses, Buy = Cross(C,BBandTop(C,60,2)) AND COND1 with COND1 = riskAmt <= 2000, and exits on crosses of the average. t6BandRiskGate takes the risk as the band’s half-width, k population standard deviations, and gates the cross entries.

avg := c.Average(n).SetN(n-1, math.NaN())
risk := c.StdDev(n).Mul(k).SetN(n-1, math.NaN())
gate := risk.LTE(limit)
le := c.CrossesAboveS(avg.Add(risk)).Mul(gate)
se := c.CrossesBelowS(avg.Sub(risk)).Mul(gate)
pos := c.PositionSigned(le, c.LT(avg), se, c.GT(avg))
  • Inputs: closes; n (60), k (2) and limit in price units (2,000 dollars over the point value; math.Inf(1) gives the ungated system and a negative limit a flat book).
  • First valid index: n - 1 for risk (NaN before) and the gate (0 before); pos reads 0 until the first entry, bar n at the earliest (a cross needs the band of the bar before).
  • Anchors: the bands avg.Add(risk) and avg.Sub(risk) are c.Bollinger(n, k) and c.Bollinger(n, -k) bit for bit from bar n - 1, while c.Bollinger(n, k).Sub(c.Average(n)) is the risk only to half an ulp of the band (equal on 47 and 30 of the 1,141 valid bars of the test’s two walks). The book equals a loop of his AFL, with its cross exits or the pseudocode’s level exits, bit for bit, and the level entries of the variant below a loop of his version 2 pseudocode; limit = +Inf gives the ungated book; without the gate the level-entry book shifted one bar is t6VolatilityBreakout(c, c.StdDev(60), 60, 2, 2) bit for bit.
  • Traps: a gated reversal goes flat: a close below the lower band while long fires the short entry and the long exit together, and with the gate closed only the exit acts (the test’s fixture R: long from bar 70, flat on bar 80 at a limit of 1, short at 2). With cross entries a gated breakout is skipped and the book waits for the next cross (fixture L, bar 62), where level entries take it on the first later bar with the gate open (bar 63). The gate measures the band, not the entry: a close above the band risks its own distance to the average, and the bands move during the trade, as Pruitt says. The exits are strict: a close equal to the average holds the position. StdDev’s running moments differ in the last bits between architectures and with the history’s start, so a risk within rounding of the limit can flip the gate. A missing close makes the risk NaN for the n bars whose window holds it, and the gate reads 0, closed, on those bars.
  • Variant: Pruitt’s own VBA and Python Bollinger functions divide by n - 1: c.StdDevSample(n).Mul(k), larger by sqrt(60/59), about 0.84%.
  • Variant: his pseudocode’s level entries, c.GT(avg.Add(risk)).Mul(gate) and c.LT(avg.Sub(risk)).Mul(gate), which delay a gated breakout instead of skipping it.
  • Evidence: his Tables 2.8 and 2.9 (35 futures markets, one contract, period and costs not stated; most likely AmiBroker runs, p. 141): the gate cut the net profit from 1,319,369 to 513,207 dollars and the largest drawdown from 380,897 to 103,462 over 3,580 and 2,224 trades; the limit is set a priori (0.2% of a notional million), not fitted. Evidence, not a reference.
  • Pinned by TestRecipeT6BandRiskGate; runnable example ExampleSeries_StdDev_bandRiskGate.

The squeezes below compare a close-dispersion measure with its own history (Katsanos, Bollinger) or with the average bar range (Duddella and the TTM squeeze, pandas-ta’s squeeze_pro). The deviation is the population one, as in Bollinger; width, the BandWidth as a fraction, is t6BollingerSiblings’s on t6-bands-and-envelopes.md and is not redefined here. Values are NaN before their first valid index and the flags built on them read 0 there; the bands are masked one per side, so that the flags stay exactly 0 or 1 (a NaN flag would poison Shift and Mul). Prices are positive and finite and lengths are in bars. The squeezes are states and release events, not trades: the breakout entries that follow them are the caller’s. The evidence is thin: no published test isolates a squeeze filter. Unlike D’Errico’s VoltyRatio (trade-state/t2-latched-entries.md), which divides two high-low ranges and latches breakout levels, these compare the dispersion of closes and latch nothing; the volatility gates of Kaufman, Connors and Raschke and Landry are on volatility-estimators/t6-volatility-gates.md.

Normalised bandwidth, its stochastic and SD*ADX (Katsanos)

Markos Katsanos (“Las Vegas Or Los Nasdaq?”, Stocks & Commodities V23:1, January 2005, pp. 18-29) divides the 30-day standard deviation by the 30-day average (“The majority of breakouts (76%) occurred when this was less than 0.07”), places it in its 150-day range with Sum(SDC-LLV(SDC,150),3)/Sum(HHV(SDC,150)-LLV(SDC,150),3))*100<20 (the unbalanced parenthesis is the magazine’s misprint), and multiplies it by “the 25-day ADX”, a composite that his system code keeps below 1.3, SDC*ADX(25)<1.3. t6KatsanosSqueeze gives the normalised deviation, its position (ChannelPosition), the product and the squeeze.

raw := c.StdDev(n).Div(c.Average(n))
sdc := raw.SetN(n-1, math.NaN())
pos := raw.ChannelPosition(m, sm).SetN(n+m+sm-3, math.NaN())
sdAdx := sdc.Mul(c.ADX(h, l, c, a)).SetN(max(n-1, a), math.NaN())
squeeze := pos.LT(lim).Mul(sdAdx.LT(adxCap))
  • Inputs: highs, lows, closes; n 30, m 150, sm 3, a 25, lim 20, adxCap 1.3. The deviation is the population one (StdDev), as in the TradeStation and eSignal ports of the Traders’ Tips.
  • First valid index: n - 1 for sdc (29), n + m + sm - 3 for pos (180), max(n - 1, a) for sdAdx (29; ADX reads 0 before a, a false 0 that would pass the cap when a > n - 1), 180 for squeeze; NaN before for the values, 0 for the flag. Katsanos’s “170 days of data” covers his 170-day regression slope only.
  • Anchors: pos equals the article’s equation (sums of three, divide, times 100) bit for bit and the sidebar’s order (three-bar averages, the numerator times 100 before the division) to rounding. The mask on the output equals the dossier’s mask on the input from 180, but at bar 150 pos is NaN where sdc.SetN(29, math.NaN()).ChannelPosition(150, 3) reads 0 (and NaN on 151 to 179), so the dossier’s squeeze fires during the warm-up (146 of the first 180 bars of the test path). A flat channel gives 50, where MetaStock divides by zero. Bollinger’s 1992 DJIA sheet (the bands page’s fixture) checks sdc at n = 20, on the sheet’s typical price, as a normalised deviation: its printed SD over its printed average.
  • Traps: width is 2k times sdc to rounding only: the band subtraction loses the low bits as the bands narrow (bit-equal on a few percent of bars). The house ADX starts at a from zero-started averages; Wilder’s seed (t6WilderDMI on the Average Directional Index page) differs from it at bar 180 by tenths of a point on simulated walks (0.06 to 0.37 on the walks probed; probe figures, not pinned), well inside what the 1.3 cap on the product tolerates.
  • Variant: the descriptive level, SDC below 0.07, sdc.LT(0.07); the level depends on the universe (penny stocks).
  • Evidence: Figure 7: the 100-stock test made 158% (37.2% a year) on 293 trades with parameters optimised on that list, and the squeeze’s own contribution is not isolated; AmiBroker’s Traders’ Tip shows a cap at 10% of the daily volume turning 2,728% into 227% on one stock. Evidence, not a reference.
  • Pinned by TestRecipeT6KatsanosSqueeze; runnable example ExampleSeries_ChannelPosition_katsanosSqueeze.

The Squeeze: BandWidth at a six-month low (Bollinger)

John Bollinger’s BandWidth is “the width of the bands expressed as a percent of the moving average” (“Using Bollinger Bands”, Stocks & Commodities V10:2, February 1992, pp. 47-51). His Squeeze is known here at second hand: “The Squeeze is triggered when the BandWidth indicator drops to its lowest level in six months” (Barbara Star, “Very High And Very Low Bandwidth Volatility Extremes”, Stocks & Commodities V24:13, Bonus Issue 2006) and “The Squeeze occurs at six-month low volatility when his bands are at a minimum distance apart for a 125-trading day period” (Matt Blackman, “Volatility, Bollinger Bands, And The Yen”, Stocks & Commodities V24:4, April 2006); his book is not in the library. t6BandWidthSqueeze gives width, the six-month low with ties and the strict new low.

width := c.Bollinger(n, k).Sub(c.Bollinger(n, -k)).Div(c.Average(n)).SetN(n-1, math.NaN())
atLow := width.LTE(width.Lowest(q))
newLow := width.Breakout(q, 1).LT(0)
  • Inputs: closes; n 20 and k 2 (Bollinger’s defaults); q 125.
  • First valid index: n - 1 for width (NaN before), n + q - 2 for atLow (143) and n + q - 1 for newLow (144); 0 before.
  • Anchors: atLow is width at its q-bar minimum, width.EQ(width.Lowest(q)) bit for bit, and newLow is width.LT(width.Lowest(q).Shift(1)) from n + q - 1. On a flat stretch width is 0, atLow 1 throughout and newLow 1 on the first zero only.
  • Traps: unmasked, width is 0/0 = 0 on bars 0 to n - 2, where atLow would fire. The dossier’s “sdc equal to sdc.Lowest(126)” runs on Katsanos’s 30-bar deviation; Bollinger’s defaults are 20 and 2, and the sources say 125 bars.
  • Variant: the book’s “complicated” form, at third hand (a TradingView script’s quotation): with sd := c.StdDev(n), sd.LTE(sd.Bollinger(q, -1.5).SetN(n+q-2, math.NaN())).
  • Variant: the 1992 level, “a drop in band width below 2% for the Standard & Poor’s 500”, width.LT(0.02).
  • Evidence: none tested: Bollinger’s “squeezes are where trends are born” (interview, Stocks & Commodities V37:7, June 2019) is a statement, not a test. Evidence, not a reference.
  • Pinned by TestRecipeT6BandWidthSqueeze.

Bollinger Bands inside Keltner Channels (Duddella; the TTM squeeze)

Suri Duddella (“Trading Range Bars”, Stocks & Commodities V25:6, June 2007, pp. 28-34): “During quiet periods, Bollinger Bands produce narrow bands and trade inside the Keltner channels, suggesting lower volatility. When Bollinger Bands expand outside the Keltner channels, they suggest volatility expansion to the breakout side”, with “Bollinger Bands (21-period, two standard deviations) and Keltner channels (21-period, 1.5 standard deviations)”, the channels being “a moving average offset by average true range (ATR)”. John Carter’s TTM squeeze, as LazyBear’s TradingView script and pandas-ta code it, takes 20, 2 and 1.5 and a momentum histogram, linreg(source - avg(avg(highest(high, lengthKC), lowest(low, lengthKC)),sma(close,lengthKC)), lengthKC,0). t6KeltnerSqueeze gives the squeeze, its release and the side by the momentum.

bbHi := c.Bollinger(n, kBB).SetN(n-1, math.NaN())
bbLo := c.Bollinger(n, -kBB).SetN(n-1, math.NaN())
sq := bbHi.LT(c.Keltner(h, l, c, n, n, kKC)).Mul(bbLo.GT(c.Keltner(h, l, c, n, n, -kKC)))
fire := sq.Shift(1).Mul(sq.EQ(0))
mid := h.Highest(n).Add(l.Lowest(n)).Mul(0.5).Add(c.Average(n)).Mul(0.5)
val := c.Sub(mid).LinRegValue(n).SetN(n+n-2, math.NaN())
long := fire.Mul(val.GT(0))
short := fire.Mul(val.LT(0))
  • Inputs: highs, lows, closes; n 20 (Duddella’s 21 on range bars), kBB 2, kKC 1.5. Keltner is KeltnerSS, a simple-average midline plus a simple average of TrueRange, on the midline of Bollinger.
  • First valid index: n - 1 for sq (before it the bands pass the close through, and the unmasked test reads 1 on every bar with a positive true range), n for fire, 2n - 2 for val, long and short (a release before 2n - 2 is a fire with no side).
  • Anchors: the band form implies the direct form c.StdDev(n).Mul(kBB).LT(c.ATR(h, l, c, n).Mul(kKC)) and equals it except on near-ties: on 40 constructed closes at 1000 (h and l 0.5 either side, an ATR of exactly 1) at bar 39 the band form reads 0 and the direct form 1, 2 SD being 1.4999999999999638 on this series, since both upper bands round to one value. fire equals sq.Delta(1).LT(0) bit for bit, and val matches LazyBear’s Pine to rounding.
  • Traps: on a common midline the two band tests are one comparison, so the bands carry no direction: the side comes from the momentum. The ratio form c.StdDev(n).Div(c.ATR(h, l, c, n)).LT(0.75) (thinkorswim’s alert line) reads 1 on flat bars, where both other forms read 0. The dossier’s “TR[0] = 0, so mask 21 bars” is stale: TrueRange[0] is h[0] - l[0] and the first valid index is n - 1, and its sq.SetN(21, math.NaN()) makes fire NaN. pandas-ta 0.4.71b0 ignores bb_std and use_tr (pass lower_std, upper_std and tr=False instead), takes the sample deviation without TA-Lib and sets SQZ_NO in the warm-up; Pine and TA-Lib start the channels a bar later (no true range at bar 0).
  • Variant: Unger’s BandWidth gate (“Enhancing Trading Strategies With Volatility Compressions And Explosion”, Stocks & Commodities V41:11, October 2023), c.Bollinger(20, 2).Sub(c.Bollinger(20, -2)).LT(c.ATR(h, l, c, 5).Mul(5.5)), the same deviation-against-ATR test with other lengths (ratio 1.375).
  • Variant: a minimum squeeze length, fire.Mul(sq.StateAge().Shift(1).GTE(6)) (the dossier’s Carter form, unverified).
  • Evidence: Duddella prints two hand-picked range-bar charts; the dossier’s check of 30 futures markets found no expansion and no directional edge after releases; on the fixture’s 1,800-bar path the squeeze is on for 599 bars, a third; on simulated walks the share depends on how the bars are built. Evidence, not a reference.
  • Pinned by TestRecipeT6KeltnerSqueeze; runnable example ExampleSeries_Keltner_squeeze.

squeeze_pro (pandas-ta)

pandas-ta’s squeeze_pro (0.4.71b0) compares the Bollinger Bands with three Keltner widths, kc_scalar_narrow 1, kc_scalar_normal 1.5 and kc_scalar_wide 2, after “John Carter’s Squeeze Pro” on usethinkscript and Makit0’s TradingView script, neither read here. t6SqueezePro codes its states as one number.

bu := c.Bollinger(n, kBB).SetN(n-1, math.NaN())
bl := c.Bollinger(n, -kBB).SetN(n-1, math.NaN())
on1 := bu.LT(c.Keltner(h, l, c, n, n, k1)).Mul(bl.GT(c.Keltner(h, l, c, n, n, -k1)))
on2 := bu.LT(c.Keltner(h, l, c, n, n, k2)).Mul(bl.GT(c.Keltner(h, l, c, n, n, -k2)))
on3 := bu.LT(c.Keltner(h, l, c, n, n, k3)).Mul(bl.GT(c.Keltner(h, l, c, n, n, -k3)))
offW := bu.GT(c.Keltner(h, l, c, n, n, k3)).Mul(bl.LT(c.Keltner(h, l, c, n, n, -k3)))
state := on1.Add(on2).Add(on3).Sub(offW)
  • Inputs: highs, lows, closes; n 20, kBB 2, k1 < k2 < k3 (1, 1.5 and 2).
  • First valid index: n - 1 (0 before).
  • Anchors: the state is 3 inside the narrow channels, 2 inside the normal ones only, 1 inside the wide ones only, 0 neither and -1 off (both bands strictly outside the wide channels); with k1 < k2 < k3 the three squeeze flags nest bar by bar, and state.GTE(2) is t6KeltnerSqueeze’s sq at the normal width bit for bit.
  • Traps: the state 0 (pandas-ta’s SQZPRO_NO) is a tie or a warm-up state on a common midline: on the test path it occurs only on flat bars, where every band equals the midline. pandas-ta returns nothing unless wide > normal > narrow. Its momentum is pandas-ta’s default sma(mom(close, 12), 6), c.Momentum(12).Average(6).SetN(17, math.NaN()), not LazyBear’s.
  • Evidence: none published for squeeze_pro itself. Evidence, not a reference.
  • Pinned by TestRecipeT6SqueezePro.

Keltner Channels

Keltner

func (s Series) Keltner(h, l, c Series, priceLen int, trLen int, atrFactor float64) Series

Keltner is an alias for KeltnerSS. This calculation is the one used by TradeStation.

Before the averages fill, each bar is s[i] + atrFactor*TrueRange[i], because both averages pass their inputs through, so the band starts as the price plus a multiple of one bar’s true range.

KeltnerSS

func (s Series) KeltnerSS(h, l, c Series, priceLen int, trLen int, atrFactor float64) Series

KeltnerSS returns a simple smoothed series adjusted by the simple moving average of a specified number of true ranges (which could be negative) determined from the specified previous number of true range bars. This calculation is the one used by TradeStation. Mismatched input lengths return a zero-filled series of the receiver’s length.

The centre line is a simple moving average of the price s and the width a simple moving average of the true range. Other libraries differ. For pandas-ta’s kc with its defaults (an EMA basis, the true range, 20 bars, 2 widths), kc’s upper band is c.KeltnerXX(h, l, c, 20, 20, 2) (pandas-ta is NaN before bar 19, where XAverage passes the input through; with TA-Lib installed its true range starts at bar 1, so its band average is seeded a bar later and the early bars differ). TradingView’s default Keltner Channels (EMA 20, Wilder’s average true range over 10 bars, 2 widths) has the upper band c.XAverage(20).Add(c.TrueRange(h, l, c).RMA(10).Mul(2)) once the seeds have decayed (TradingView’s ta.ema starts from the first close, XAverage from the 20-bar simple average).

Before the averages fill, each bar is s[i] + atrFactor*TrueRange[i], because both averages pass their inputs through, so the band starts as the price plus a multiple of one bar’s true range.

The squeeze of Bollinger Bands inside Keltner Channels (Duddella’s, and the TTM squeeze), Bollinger(n, k) against KeltnerSS(h, l, c, n, n, atrFactor) on their common midline, is a recipe listed under “Compositions” in the “Bollinger Bands” category.

KeltnerSX

func (s Series) KeltnerSX(h, l, c Series, priceLen int, trLen int, atrFactor float64) Series

KeltnerSX returns a simple smoothed series adjusted by the exponential moving average of a specified number of true ranges (which could be negative) determined from the specified previous number of true range bars. Mismatched input lengths return a zero-filled series of the receiver’s length.

Before the averages fill, each bar is s[i] + atrFactor*TrueRange[i], because both averages pass their inputs through, so the band starts as the price plus a multiple of one bar’s true range.

KeltnerXS

func (s Series) KeltnerXS(h, l, c Series, priceLen int, trLen int, atrFactor float64) Series

KeltnerXS returns an exponentially smoothed series adjusted by the simple moving average of a specified number of true ranges (which could be negative) determined from the specified previous number of true range bars. Mismatched input lengths return a zero-filled series of the receiver’s length.

Before the averages fill, each bar is s[i] + atrFactor*TrueRange[i], because both averages pass their inputs through, so the band starts as the price plus a multiple of one bar’s true range.

KeltnerXX

func (s Series) KeltnerXX(h, l, c Series, priceLen int, trLen int, atrFactor float64) Series

KeltnerXX returns an exponentially smoothed series adjusted by the exponential moving average of a specified number of true ranges (which could be negative) determined from the specified previous number of true range bars. Mismatched input lengths return a zero-filled series of the receiver’s length.

Before the averages fill, each bar is s[i] + atrFactor*TrueRange[i], because both averages pass their inputs through, so the band starts as the price plus a multiple of one bar’s true range.

Compositions

The value indicator reads the close against the midline of a Keltner channel in units of the channel’s average true range: with m := h.Add(l).Mul(0.5), the band m.KeltnerSS(h, l, c, x, x, k) (Keltner is the same method) is f.Add(v.Mul(k)) in the block’s names, bit for bit. The squeeze of Bollinger Bands inside Keltner Channels is on bollinger-bands/t6-squeeze.md. Each section gives its first valid index; prices are positive and finite and lengths are in bars.

Chartmill value indicator (Vandycke)

Dirk Vandycke (“The Chartmill Value Indicator”, Stocks & Commodities V31:1, January 2013, pp. 44-47) establishes “an x-day consensus as the x-period moving average of the median of each period” and divides the close’s distance from it “by an x-day moving average of the true range”. His levels (part 2, V31:2) are those of Mark Helweg’s Value Chart: “Any value between -4 and +4 indicates a strong short-term consensus” and “plain undervaluation below -8”; they fit a unit of 0.2 average true ranges, as the Value Chart’s unit is “20% of the 5-day average” of its volatility measure in Helweg’s patent (below), not the bare ratio of his sidebar. t6ValueIndicator returns cvi on that unit and printed, the sidebar’s ratio.

f := h.Add(l).Mul(0.5).Average(x)
v := c.ATR(h, l, c, x)
cvi := c.Sub(f).Div(v.Mul(0.2)).SetN(x-1, math.NaN())
printed := c.Sub(f).Div(v).SetN(x-1, math.NaN())
  • Inputs: the high h, low l and close c and the length x (“a few periods”; 5, which his Figure 1 and the Value Chart’s default fit); cvi reads in units of 0.2 average true ranges.
  • First valid index: x - 1 (4). ATR takes bar 0’s true range as its high less its low, so the sidebar, whose first true range needs a previous close, agrees from bar x.
  • Anchors: the consensus is the middle of the Keltner channel, mid.KeltnerSS(h, l, c, x, x, 0) with mid := h.Add(l).Mul(0.5), bit for bit, so off ties and flat windows cvi.GT(j) equals c.GT(mid.KeltnerSS(h, l, c, x, x, 0.2*j)): a value above 8 is a close above the band 1.6 average true ranges over the consensus. printed is 0.2 cvi to rounding.
  • Traps: c.Sub(f).Div(v.Mul(0.2)) and c.Sub(f).Div(v).Mul(5) differ in the last bit on 274 of 596 bars of the test fixture; the unit is formed first, as the Value Chart code forms its range. A window of zero true ranges reads 0 (fair value) through Div. Average passes its input through before bar x - 1, hence the mask.
  • Variant: the normalised candles are o.Sub(f).Div(v.Mul(0.2)) and the same with h and l.
  • Variant: the Value Chart’s own unit averages the larger of the range and the absolute close-to-close change, an element-wise maximum of two series: h.Sub(l).Maximum(c.Delta(1).Abs()).Average(5).Mul(0.2), equal bit for bit on the test fixture to the masked form rg.Mul(rg.GTE(dc)).Add(dc.Mul(rg.LT(dc))) with rg := h.Sub(l) and dc := c.Delta(1).Abs(). FIG. 7 of Helweg’s US patent 7,461,023 tabulates that unit for AT&T in 1999; it is cited only, not a reference for this indicator.
  • Evidence: Vandycke’s SPY chart to 2 September 2011 shows the indicator beyond +-8 about a dozen times in a year, and the Value Chart’s +-4 band is by design “one standard deviation” (Barbara Star, Stocks & Commodities V31:9, August 2013), which fits his “two standard deviations” at 8 (on Gaussian walks the 0.2 unit at x = 5 gives a standard deviation of 3.8 to 3.9); his part 2 simulations (“a few months in a strong bull market”) do not take costs into account. Evidence, not a reference.
  • Pinned by TestRecipeT6ValueIndicator; runnable example ExampleSeries_ATR_valueIndicator.

Postponed entry (Vandycke)

Vandycke (part 2, Stocks & Commodities V31:2, February 2013, pp. 36-39): “After an entry signal occurs, you should postpone opening a position until the CVI shows short-term undervaluation”, and “the setup gets canceled if it does not go below -8 in the following five-day week”. t6PostponedEntry is the causal reading: the first of the w bars after the latest setup with cvi below level.

age := setup.BarsSince()
u := cvi.LT(level).Mul(age.GTE(1))
entry := u.Mul(u.SumSince(setup).EQ(1)).Mul(age.LTE(w))
  • Inputs: the 0/1 setup flags, the value indicator cvi, level (-8) and the window w (5); entry is 0 or 1.
  • First valid index: the first setup plus 1; before the first setup BarsSince is NaN and nothing fires.
  • Anchors: a setup gives at most one entry, only on bars 1 to w after it, and a new setup restarts the count; the setup bar itself does not enter, even with cvi below the level there. On the test fixture the two planted breakouts over the five prior highs (bars 60 and 500), each followed by three falls of 3 points, enter on bars 63 and 503.
  • Traps: his part 3 filters a five-bar Donchian breakout (“prices break above the highest high of the previous five bars”, under a 25-bar average above a 75-bar one) by a value below -8 on the same bar. Read as a close breakout, c.GT(h.Highest(5).Shift(1)).SetN(5, 0), that cannot happen for x <= 6: on a breakout bar the close is above the x - 1 previous medians and at least the bar’s low, so c - f > -(h - l)/(2x) >= -tr/(2x) with tr the bar’s true range, while 0.2 atr >= 0.2 tr/x, hence cvi > -2.5. Read as an intrabar breakout, h.GT(h.Highest(5).Shift(1)), it can fire, on a bar that closes far below its high. Under the close reading the filter is empty, and the postponed entry with the breakout as its setup is the tradable reading. His part 2 comparison arm, setups whose CVI “went below -8 within the next week”, selects them by the future (“That is looking forward!”).
  • Variant: his key reversal as the setup, o.LT(c.Shift(1)).Mul(c.GT(h.Shift(1))).SetN(1, 0).
  • Evidence: part 3 reports that the breakout system’s average trade “returns up to five times more” with the filter 50 days after entry, a filter that is empty under the close reading; no costs. Evidence, not a reference.
  • Pinned by TestRecipeT6PostponedEntry.

Donchian Channels

DonchianLower

func (s Series) DonchianLower(n int) Series

DonchianLower returns a series representing the lower Donchian channel measured over n bars.

Bars 0 to n-2 use an expanding window, the extremes of the bars so far, not n bars; the first full n-bar window is at bar n-1.

DonchianMiddle

func (s Series) DonchianMiddle(n int) Series

DonchianMiddle returns a series representing the middle Donchian channel measured over n bars.

Bars 0 to n-2 use an expanding window, the extremes of the bars so far, not n bars; the first full n-bar window is at bar n-1.

DonchianUpper

func (s Series) DonchianUpper(n int) Series

DonchianUpper returns a series representing the upper Donchian channel measured over n bars.

Bars 0 to n-2 use an expanding window, the extremes of the bars so far, not n bars; the first full n-bar window is at bar n-1.

Compositions

The adaptive channel breakouts below mask their lookbacks and channel levels with NaN before their first valid index, where a window would read the Shift pad, an unmasked warm-up or too short a history; the flags and the positions read 0 there. Prices are positive and finite, volumes non-negative and finite, and lengths are in bars; a channel at bar t ends at bar t - 1.

VIX- and volume-adaptive channel breakout (Castleman)

Scott Castleman (“Using Implied Volatility And Volume”, Stocks & Commodities V21:6, June 2003, pp. 32-37) bases a stop-and-reverse system “on the Donchian breakout system” and lets its lookback follow the implied-volatility index’s place in its 252-day range and the 10-day average volume’s place in its 50-day range: “The longest period of days referenced by the system is 29, and the shortest is four.” t6CastlemanBreakout is exact to his printed TradeStation code (Figure 4): the lengths “intportion(refLength1 - 0.5refLength2(…))” with 29 and 25, the short side from the complements “(highestIV - currentIV)” and “(highestVolume - avgVolume)”, both kept where either range is 0, the breaks “high>highest(high[1],dayLengthLong)” and “low<lowest(low[1],dayLengthShort)”, and “buy next bar on open” and “sell Short next bar on open”.

loIV := iv.Lowest(ivLen)
hiIV := iv.Highest(ivLen)
avgV := v.Average(10)
loV := v.Lowest(volLen)
hiV := v.Highest(volLen)
rIV := hiIV.Sub(loIV)
rV := hiV.Sub(loV)
ok := rIV.GT(0).Mul(rV.GT(0)).SetN(max(ivLen, volLen)-1, 0)
longLen := iv.Sub(loIV).Div(rIV).Add(avgV.Sub(loV).Div(rV)).Mul(-12.5).Add(29).Trunc().ValueWhen(ok)
shortLen := hiIV.Sub(iv).Div(rIV).Add(hiV.Sub(avgV).Div(rV)).Mul(-12.5).Add(29).Trunc().ValueWhen(ok)
buy := h.GT(h.Shift(1).SetN(1, math.NaN()).HighestVariable(longLen, 4, 29))
sell := l.LT(l.Shift(1).SetN(1, math.NaN()).LowestVariable(shortLen, 4, 29))
pos := h.PositionSigned(buy.Shift(1), sell.Shift(1), sell.Shift(1), buy.Shift(1))
  • Inputs: highs, lows and volumes of the traded market and the implied-volatility index aligned bar for bar (Castleman discusses the S&P 100 VIX and tests on Russell 2000 futures without naming the index he used); ivLen and volLen (252 and 50), volLen >= 10, so that the 10-bar average lies between the volume extremes and the lengths stay in [4, 29], where the clamp of HighestVariable is inert.
  • First valid index: max(ivLen, volLen) - 1 for the lengths, 251 at the defaults, or the first later bar where both ranges are positive (the mask of ok keeps out the expanding windows and Average’s pass-through), NaN before the first decision; the flags then need q bars before the bar, and they and pos read 0 until the channels exist. Shift(1) pads the flags’ bar 0 with a 0, which does not fire. The match with his code holds from that bar; TradeStation starts at MaxBarsBack with its own partial windows, so its first decisions can differ.
  • Anchors: the five outputs equal a direct loop of Figure 4 (the lengths, both flags and the next-open stop and reverse) bit for bit, on a walk with a flat stretch of the index and one of the volume, where the lengths hold; with a constant length n the flags are h.Breakout(n, 1).EQ(1) and l.Breakout(n, 1).EQ(-1).
  • Traps: the printed arithmetic is not exact: with the index at the low of a range of 10 to 20 and an average volume of 1032 in a range of 1000 to 1100 the long length is 25 and the short 7, because 1 + 0.68 is a binary64 tie that rounds up and 29 - 12.5(1 + 0.68) comes out 7.9999999999999964, where exact arithmetic gives 8. A form over Stochastic (iv.Stochastic(iv, iv, 252) and the average volume’s) also gives 8, reads 0 on a flat window and recomputes where Castleman keeps the old length: the zero-range rule is ValueWhen, not a mask.
  • Variant: where both flags fire on one bar, PositionSigned stays flat when flat and reverses in a position; Castleman does not say, and buy.Shift(1).Sub(sell.Shift(1)).Hysteresis(0, 0) would hold instead.
  • Evidence: Figure 5 (Russell 2000 futures, 10 January 1996 to 31 December 2002, one contract, costs not stated): 64 trades, 46.88% profitable, 199,750 dollars net, with no fixed-length baseline on one market. Evidence, not a reference.
  • Pinned by TestRecipeT6CastlemanBreakout; runnable example ExampleSeries_HighestVariable_castleman.

Cycle-length channel breakout (Ruggiero; Darley)

Murray Ruggiero (Cybernetic Trading Strategies, Wiley, 1997, ch. 7, p. 114, and ch. 15, pp. 218-219) “simply buys and sells at the highest high and lowest low of the past dominant cycle days”: “Buy at Highest (High, DCycle) stop; Sell at Lowest (Low, DCycle) stop”, “simply a stop and reverse” (Tables 7.2 and 15.4). Placed at bar t - 1’s close, the stops are the extremes of the bars ending there over bar t - 1’s period, so t6CycleChannel shifts HighestVariable’s result, the difference its doc names, and a stop is touched when the high or the low reaches it (GTE, LTE). Ruggiero’s period is the proprietary maximum-entropy cycle of TradeCycles (6 to 50 bars): the caller supplies p, and p := c.HilbertPeriod().SetN(32, math.NaN()).Round(), TA-Lib’s HT_DCPERIOD clamped to the same [6, 50], is the substitute, to which his results do not transfer.

up := h.HighestVariable(p, 6, 50).Shift(1).SetN(1, math.NaN())
dn := l.LowestVariable(p, 6, 50).Shift(1).SetN(1, math.NaN())
buy := h.GTE(up)
sell := l.LTE(dn)
pos := h.PositionSigned(buy, sell, sell, buy)
  • Inputs: highs and lows; the period p, right-aligned, NaN in its warm-up; HighestVariable clamps it to [6, 50] and truncates a fraction, hence Round in the caller’s line (Ruggiero rounds his cycle, “Round(…,0)”).
  • First valid index: data-dependent: 33 at the earliest with the Hilbert period (bar 32’s period sizes bar 33’s stops), and later where the period exceeds the history; pos reads 0 until the first touch. Unmasked, the period’s +0 warm-up is clamped to 6 and gives plausible-looking levels from bar 6, and without the SetN(1, math.NaN()) the shifted level of bar 0 would be the pad 0, which the high reaches.
  • Anchors: the three outputs equal a loop of the stop-and-reverse rule (the prior bar’s period, truncated; touch tests; a reversal on the opposite touch) bit for bit; a constant period n gives h.Highest(n).Shift(1) from bar n.
  • Traps: Castleman’s form, h.Shift(1).SetN(1, math.NaN()).HighestVariable(p, 6, 50), sizes the window with bar t’s period and gives a different level on about 5% of bars, so the two must not be interchanged; on ties the touch tests decide, and strict tests would change the positions.
  • Variant: Roger Darley’s Hilbert channel (“Optimizing With Hilbert Indicators”, Stocks & Commodities V18:11, November 2000, pp. 19-26, with its code in that issue’s Traders’ Tips) reads the period on the current bar with the window ending at the prior bar, long only, entering on h.GT(h.Shift(1).SetN(1, math.NaN()).HighestVariable(p.Mul(0.1), 1, 200)) and exiting on l.LT(l.Shift(1).SetN(1, math.NaN()).LowestVariable(p.Mul(3.4), 1, 200)) with Position (his Buy fills at the next open, so shift the flags by one for his timing); his period is Ehlers’s homodyne function of 22 July 2000, which, by one translation of his listing, reads some 4 to 7 bars longer than HT_DCPERIOD on average and is not built.
  • Missing piece: Ruggiero’s modified system (Table 15.8) switches its exit from the half-cycle channel to the full one after ten bars in the trade, an exit that depends on the position it produces, which no shipped latch composes.
  • Evidence: Table 7.2 (D-mark, yen and Swiss franc futures, 1 January 1980 to 22 June 1996, 50 dollars a trade deducted: 94, 95 and 105 trades) and Table 15.4 rest on the maximum-entropy cycle and TradeStation’s stops, and Darley’s test on GE (30 trades, in-sample constants) on his period. Evidence, not a reference.
  • Pinned by TestRecipeT6CycleChannel.

Three ways to confirm a break: two weekly closes beyond the previous week’s extreme (Panagakis), a close above a moving average filtered against noise (D’Errico) and a thresholded crossover of two averages of log prices (Masters). Recipe values, levels and results are NaN before their first valid index (SetN(F, math.NaN())), and flags, states and books read 0 there: every shifted series a pad can satisfy is masked, or the flag is, since a close compared with the +0 pad of h.Shift(1) reads 1 on bar 0. Prices are positive and finite (above 1 for Masters’s ratio); Panagakis’s bars are the caller’s weekly bars (nseries builds no sessions, and the rule on daily bars is a different rule), the others daily; series are paired by position, so the caller aligns them by timestamp first. No helper here reads the open o: Panagakis enters and stops on the weekly close, and only D’Errico’s book, which fills at the next open, takes o as its receiver. An exact decimal tie at a threshold (Edwards and Magee’s 3%, Masters’s thresholds) is decided by binary rounding: the page pins the products as written, level.Mul(1 + p) with 1 + 0.03 == 1.03 in binary64, and the goldens run on prices in sixty-fourths. Backtests over data we do not have are evidence, not a reference. Faith’s Donchian Trend, with the same PositionSigned time exit, and Clenow’s trend model are on this category’s t6-breakout-systems.md; the first section’s stop overlay is that of the trade-state page t2-entry-anchored-stops.md. Counts on probe paths that the tests do not reproduce are illustrations, not pins.

Two-close weekly breakout (Panagakis)

George Panagakis (“Trading T-Bonds And Currencies”, Stocks & Commodities V13:11, November 1995, pp. 478-485) buys “When two weekly closes occur in a row that are higher than the highest price of the previous week” and sells on the mirror; the position “will normally be closed after eight weeks”, a long goes flat on “a weekly close below the lowest price of the two higher weekly closes that gave us our entry signal”, and an opposite signal reverses it; “We get a buy signal on the eighth week […] in which case we would keep the long position open and begin the process again with the eighth week as our new starting point”. t6TwoCloseRenew returns the signal sig, the book pos on PositionSignedRules with Hold: hold, Renew: true and the two-week stops as levels read on the close, sampled on the entry week and frozen for the trade (a close strictly beyond the stop flattens the book, which then takes the next signal with a fresh count), and the stop levels stopL and stopS read back from pos by EntryValue.

up := c.GT(h.Shift(1)).SetN(1, 0)
dn := c.LT(l.Shift(1)).SetN(1, 0)
buy := up.Mul(up.Shift(1))
sell := dn.Mul(dn.Shift(1))
sig := buy.Sub(sell)
pos := c.PositionSignedRules(buy, nil, sell, nil, PositionRules{
    Hold: hold, Renew: true,
    LongStop:  PositionLevel{Level: l.Lowest(2)},
    ShortStop: PositionLevel{Level: h.Highest(2)},
})
stopL := l.Lowest(2).EntryValue(pos)
stopS := h.Highest(2).EntryValue(pos)
  • Inputs: the caller’s weekly highs, lows and closes (the nearest contract’s Friday settlement, a holiday week ending on its last session; rolls are the caller’s data); hold in weeks, 8 (“it works best when you use eight weeks as your normal exit point”, and “five or 10 weeks” also work). His markets are T-bond and yen futures.
  • First valid index: 2 for sig, the third week (the masks make up and dn 0 on bar 0, so buy and sell read 0 on bars 0 and 1); pos has no warm-up and reads 0 until the first signal (bar 2 at the earliest); stopL and stopS are NaN while pos is flat and defined on every trade, long or short, each the entry bar’s Lowest(2) of the lows or Highest(2) of the highs: a long trade’s stop is stopL and a short’s stopS. The Eurodollar member’s F is 2.
  • Anchors: the printed three weeks (98-00/90-00/95-00, 101-00/88-00/99-16 and 105-00/97-00/104-20, in 32nds and exact in binary64) give sig [0 0 1], pos [0 0 1], stopL [NaN NaN 88] and stopS [NaN NaN 105]: the entry “from the price of 104-20” and the stop 88-00, “the lowest price of the two higher weekly closes” (week 2’s low, not week 1’s 90-00). buy equals the dossier’s up.Sum(2).EQ(2) bit for bit, and the book equals a loop of his rules as the source runs them (flat after a stop, the next signal taken with a fresh count, the renewal on the limit week) bit for bit on the 320-bar group and four 3,000-bar walks, with holds of 2, 5, 8 and 10. The hold counts the entry week as week 0 (“February 12, 1982, was eight weeks from December 18”); the stop is strict, on the close and tested from the week after the entry (on the entry week the close is at or above l.Lowest(2)), and a close exactly on it holds (bar 201 of the fixture closes on its stop, 109.71875). On one bar a reversal wins over the stop, as on his 28 May 1982: the kernel reads the opposite entry before the stop. The Eurodollar member (Panagakis, “Trading The Eurodollar”, Stocks & Commodities V13:3, March 1995, pp. 102-106: “Whenever the settlement price of the week is higher than the highest price of the previous two weeks, we will initiate a long position”) takes up2 := c.GT(h.Highest(2).Shift(1)).SetN(2, 0) and its mirror dn2 on l.Lowest(2), and its book equals up2.Sub(dn2).Hysteresis(0, 0) bit for bit.
  • Traps: unmasked, c.GT(h.Shift(1)) reads 1 on bar 0 against the +0 pad, and the unmasked buy is [0 1 1] on the three weeks, a signal a week early. The stop is held from the entry, where the dossier’s l.Lowest(2) is a moving two-week low, and its “latch the value at entry (strategy layer)” is stale: PositionSignedRules holds it as a level frozen at entry. The renewal is a same-direction signal on the eighth week (the closes of weeks 7 and 8), not any signal completed in weeks 7 and 8, and a stop on that week wins over it. A NaN close inside a trade fails the stop test on that bar (a comparison with NaN is false), so it does not stop the trade.
  • Variant: (a) Edwards and Magee’s percentage penetration (Edwards, Magee and Bassetti, Technical Analysis of Stock Trends, 11th ed., CRC Press, 2019, ch. 14, pp. 220-222: prices must “close beyond it by a margin equal to about 3% of the stock’s price”), c.GT(level.Mul(1 + p)) or c.LT(level.Mul(1 - p)), strict, on the close and a state on every bar, p 0.03 for stocks and 0.02 for averages, with the level from a shipped method (l.Lowest(n).Shift(1).SetN(n, math.NaN()), l.SwingLowValue(n)); their double tops, “A first Top at 50, for example, and a second at 51 1/2 would come within this limit” (ch. 9), make 50 x 1.03 = 51.5 exactly: 51.5 reads 0, within, and 51.6 reads 1, a break. The close-relative reading of “of the stock’s price”, c.Mul(1 + p).LT(level), is another rule, differing only on closes near the 3% margin.
  • Variant: (b) the restart-on-every-signal hold, last := sig.ValueWhen(sig) and last.GT(0).Sub(last.LT(0)).Mul(sig.BarsSince().LT(hold)).Add(0), Tsinaslanidis and Zapranis’s ch. 8 holding rule (exact for it) and further from Panagakis: without the stop it differs from his rule on about three times as many bars of a probe walk as the hold without renewal does (not pinned), and his yen list, as read from Figure 8’s tables, has no trade of nine or ten weeks, which a restart on every signal would often give.
  • Members:
    • the Eurodollar one-close two-week channel, stop and reverse: c.PositionSigned(up2, dn2, dn2, up2)
    • k closes in a row (Panagakis’s “two” as a parameter): up.Sum(k).EQ(float64(k)), F k
  • Departures: the stop at a renewal is the entry’s, and the short trade’s stop, the highest high of the two sell-signal weeks, is the mirror: the source states neither.
  • Evidence: his trade lists and composite (Figures 8 to 10: one T-bond and one yen futures contract, fiscal years 1980 to 1994, 75 dollars a round turn, a 20,000-dollar account, 4 to 10 trades a market-year, 8 of the 76 yen trades lasting longer than eight weeks as read from its tables, the renewals) average 9,570 dollars net a year on the two contracts, 48% of the account, the eight weeks having worked best of five, eight and ten on the same years (in sample); the lists cannot be reproduced without the weekly bars. Marcello Cattaneo Adorno’s rerun (Stocks & Commodities V16:11, November 1998, pp. 508-512; T-bond weekly bars from January 1989 to January 1998, 115 dollars a trade, with an unspecified money-management stop) prints 42 trades, 26,732.50 dollars net and a profit factor of 1.59. Evidence, not a reference.
  • Pinned by TestRecipeT6TwoCloseBreakout; runnable example ExampleSeries_PositionSignedRules_twoCloseRenew.

Moving-average noise filters (D’Errico)

Domenico D’Errico (TradeStation EasyLanguage for Algorithmic Trading, Packt, 2024, ch. 3, “Handling market noise”; the ebook prints no page numbers) tests three entry conditions for a 200-day average system with a 20-bar time exit: the benchmark Close >average(Close,Len)[1], against yesterday’s average; “a close above the moving average for two consecutive days”; and Close >average(close,200) + 0.5*StdDev(close,200), where TradeStation’s StdDev “calculates a standard deviation of a population of values”. t6NoiseFilters returns the three conditions as the flags f0, f1 and f2.

a := c.Average(n).SetN(n-1, math.NaN())
f0 := c.GT(c.Average(n).Shift(1).SetN(n, math.NaN()))
f1 := c.GT(a).Mul(c.Shift(1).GT(a.Shift(1)))
f2 := c.GT(c.Bollinger(n, k).SetN(n-1, math.NaN()))
  • Inputs: daily closes; n 200 (his Len) and k 0.5 standard deviations, the population deviation of StdDev inside Bollinger (TradeStation’s StdDevS is the sample form).
  • First valid index: n for f0 and f1, n - 1 for f2, 0 before. The mask of f0 follows the Shift: unmasked, c.GT(c.Average(n).Shift(1)) fires on bar 0 against the pad and on about half of bars 1 to n - 1, where the average passes the close through and the test is a close above the previous close, and masking the average before the Shift still leaves bar 0. The masks of f1 and f2 are no-ops on positive prices (the average and the band pass the close through on bars 0 to n - 2, and c > c fails): the test asserts the masked and unmasked forms equal.
  • Anchors: f1 is his “both the current close above the current moving average value and the previous close above the previous moving average”; c.Bollinger(n, k) is c.Average(n).Add(c.StdDev(n).Mul(k)) bit for bit. The population and sample deviations split f2 on bar 199 of the 700-bar fixture group and on none of 2,800 probe walk bars. His book is o.Position(f.Shift(1), nil, 21): bought at the next open and sold at the open after BarsSinceEntry reaches BarsIN 20, which TradeStation reports as 22 bars a trade (“Avg. Bars in Total Trades 22.00”).
  • Traps: the dossier’s baseline (192 trades, net profit over drawdown 3.74) is Table 3.2’s “Close above SMA”, against today’s average, c.GT(a), not the listing’s benchmark against yesterday’s, so its 3.74, 4.18 and 4.91 mix two baselines and two drawdown measures; and the NoiseFilterType listing is D’Errico’s, not Davey’s or Jaekle’s. His filter 2 keeps the literal 200, so his ch. 4 optimisation of Len from 170 to 230 leaves it unchanged; the recipe takes n for both.
  • Evidence: SPY daily bars from 17 February 2004 to 14 February 2024, 10,000 dollars a trade, no costs stated, the three filters compared on the same data (in sample): Figure 3.17, a small blurred screenshot whose digits the ratio arithmetic fixes, gives 182, 179 and 168 trades and net profit over drawdown 3.65, 4.25 and 4.94 for filters 0, 1 and 2, and the performance summaries of Figures 3.14 and 3.16 print 179 trades and 12,765.27 dollars net for filter 1 and 168 trades and 12,953.13 dollars for filter 2. Evidence, not a reference.
  • Pinned by TestRecipeT6NoiseFilters; runnable example ExampleSeries_Bollinger_noiseFilters.

Thresholded log-average crossover (Masters)

Timothy Masters (Testing and Tuning Market Trading Systems: Algorithms in C++, Apress, 2018, ch. 4, pp. 112-118) computes “a short-term and a long-term moving average of log prices. If the short-term MA exceeds the long-term MA by at least a specified long threshold, a long position is taken for the next day”, and his DEV_MA code thresholds their ratio, change = short_mean / long_mean - 1.0, strictly, with change > long_thresh and change < -short_thresh. t6LogMACrossover returns chg and state, 1 long, -1 short and 0 neutral: three states recomputed each bar, with no latch.

x := c.Log()
chg := x.Average(short).Div(x.Average(long)).Sub(1).SetN(long-1, math.NaN())
state := chg.GT(longThresh).Sub(chg.LT(-shortThresh))
  • Inputs: closes above 1 in a fixed price unit, the precondition that every log price in the windows be positive; short and long in bars, the short length converted by the caller from his percentage as his code does, int(0.01 * pct * float64(long)) clamped to [1, long-1]; the thresholds as fractions, his parameters divided by 10,000 (“specified as 10,000 times the actual threshold”). His Figure 4-1 run: long 79, percentage 88.4591 (short 69), long threshold 0.00005328 and short threshold 0.00170877.
  • First valid index: long - 1 for chg, NaN before; state reads 0 there, both comparisons with NaN failing. His loop starts every parameter set at the same bar (“regardless of long_term, we always start trading at the same bar for conformity”), which is the caller’s choice here.
  • Anchors: the short-length conversions, in run-time arithmetic: 28 for (pct 29, long 100), whose product is 28.999999999999996; 69 for (88.4591, 79), Figure 4-1’s; 14 for (29, 50); and 0 for (7, 10), which the caller clamps to 1. chg equals his loop to rounding only, by about 2e-15 at log prices near 4.6 (Average keeps a compensated rolling sum, while he sums newest first and continues the long sum from the short one), and the states agreed on every bar in four parameter sets. The tests bound chg at 1e-13 relative, since Go’s math.Log is not correctly rounded, and assert state bit for bit, every fixture bar lying more than 1e-12 from a threshold; the strict comparisons are pinned by a constructed case.
  • Traps: at a price of exactly 1 (every log in the long window 0) Div’s zero rule makes chg -1, a short reading, not NaN, and below 1 the sign inverts, so that a rising market reads short; the ratio also changes with the price unit (logs of cents against dollars). The dossier’s d := c.Log().Average(s).Sub(c.Log().Average(l)) with GTE and LTE is CD_MA’s difference, not DEV_MA’s strict ratio, and its s = floor(pct/100 * l) drops the clamp and the binary artefact of C truncation (29% of 100 gives 28). A latch over the two flags, such as PositionSigned, would hold a state the rule drops.
  • Variant: CD_MA’s difference (ch. 3, pp. 63-64), x.Average(short).Sub(x.Average(long)), the unit-free form: the same states at zero thresholds when prices exceed 1, on another threshold scale.
  • Variant: the trading-range breakout of Tsinaslanidis and Zapranis (Technical Analysis for Algorithmic Pattern Recognition, Springer, 2016, eq. (4.6), pp. 60-61), the strict state c.Breakout(w, 1) with w 50; their ch. 8 rule (eq. (7.27), p. 159, w 50 in Table 8.2) is the crossing c.CrossesAboveS(hh).Sub(c.CrossesBelowS(ll)), with hh := c.Highest(w).Shift(1).SetN(w, math.NaN()) and its mirror ll, held HP bars (5 in their Table 8.3) by variant (b) of the two-close section.
  • Evidence: Figure 4-1 (OEX closes from inception to mid-2017, the optimised parameters, no costs) prints “Best performance = 2.6710” in sample, with “Bias = 0.3221” and “Expected = 2.3489”; Tsinaslanidis and Zapranis’s Table 8.3 (the NIKKEI, HP 5) gives their crossing rule 192 buy and 138 sell signals, with mean daily log returns of 6.38 x 10^-5 after buys (p 0.3427) and -2.37 x 10^-4 after sells (p 0.4328). Evidence, not a reference.
  • Pinned by TestRecipeT6LogMACrossover; runnable example ExampleSeries_Log_thresholdedCrossover.

The classic breakout systems below fill at the next open, through Shift(1) on their flags, unless a rule rests a stop at a level: then the flags act on their own bar and the fill is the caller’s (the resting-stop fill series of the trade-state pages). PositionSigned reverses on a bar where an opposite entry fires in a position, as the authors’ rules allow; a third-party transcription of Clenow’s Trading Evolved (2019) trend model, a different parameterisation, exits and waits, and pos.Mul(pos.Shift(1)).LT(0) marks those bars. Exponential averages depend on the history they start from: an EMA of 100 bars needs about 450 to 700 bars to agree with a longer history to 1e-6 (the seed’s weight decays as (1 - alpha)^t), one of 350 about 1,900 to 2,400, so flags built on them agree only after that. Levels and bands are NaN before their first valid index, and the flags and books read 0 there; prices are positive and finite and lengths are in bars. Carver’s breakout forecast, the close’s position in its Donchian channel, is on the MACD page (“Breakout forecast (Carver)”), and the adaptive channels of Castleman and Ruggiero sit beside this page. Not repeated here: the 2N stop and the bracket (“Bracket with resting orders” in trade-state/t2-entry-anchored-stops.md), the last-trade-loser filter and the Turtle failsafe (trade-state/t2-shadow-filters.md), Penfold’s two-week stop (the Darvas Box introduction), and Aronson’s channel-breakout trend state, which is OXXO’s expression x.Breakout(n, 1).Hysteresis(0, 0) (point-and-figure/t6-oxxo.md), with Mul(-1).Add(0) for its inverse rules.

Clenow’s core trend model

Andreas Clenow (Following the Trend, 2nd ed., Wiley, 2022, ch. 4, pp. 59-84, the core model on pp. 71-75) trades “the breakout strategy, using the moving average strategy as a trend filter”: “Trend is considered bullish if EMA 50 is above EMA 100”, “Enter long on new 100-day high, if trend is bullish”, “Exit long if price falls to 50-day low, or trend turns bearish”, and the mirror, with “trades […] always taken one day after the signal”. The breakout reads closes, “if today’s close is higher or equal to the highest close in the past 100 days” (the basic breakout strategy, p. 61), with today in the window, as the third-party transcription’s close[-1] == close[-50:].max() does in its 50-day model. t6ClenowFlat fills on the next bar and books the flat form of his code on PositionSignedRules with OppositeFlattens: true: an opposite entry takes the book flat and is not taken on its bar, so a persisting flag opens the opposite trade on the next bar. His ch. 7 counter-trend rule, a long taken when the close falls three average true ranges below its 20-day high while the 40-day exponential average is above the 80-day, held 20 days, is on true-range/t6-atr-pullback.md.

m := c.MACD(fast, slow).SetN(slow-1, math.NaN())
bull := m.GT(0)
bear := m.LT(0)
le := c.GTE(c.Highest(entryN).SetN(entryN-1, math.NaN())).Mul(bull)
se := c.LTE(c.Lowest(entryN).SetN(entryN-1, math.NaN())).Mul(bear)
lx := c.LTE(c.Lowest(exitN).SetN(exitN-1, math.NaN())).Add(bear).GT(0)
sx := c.GTE(c.Highest(exitN).SetN(exitN-1, math.NaN())).Add(bull).GT(0)
pos := c.PositionSignedRules(le.Shift(1), lx.Shift(1), se.Shift(1), sx.Shift(1), PositionRules{OppositeFlattens: true})
  • Inputs: closes; fast and slow (50, 100); entryN (100) and exitN (50). Clenow sizes by a 20-day ATR, outside the recipe.
  • First valid index: max(slow, entryN) - 1 for le and se (99), the first bar they can fire, and the next bar for pos (next-bar fills); 0 before. Unmasked, MACD’s bars fast - 1 to slow - 2 are XAverage(fast) less the close and fire, and so does Highest’s expanding warm-up (10 of bars 0 to 98 on the test path); the levels’ masks keep le at 0 before entryN - 1 even under a shorter trend filter.
  • Anchors: m.GT(0) is c.XAverage(50).GT(c.XAverage(100)) bit for bit; c.GTE(c.Highest(n)) is c.EQ(c.Highest(n)); pos equals a direct loop of the rules (flat tests the entries, a position the opposite entry, which takes it flat, and then its exit) bit for bit on the five fixture groups with the book’s lengths and with short ones (3, 7, 5 and 3). On the test path the close of bar 591 is a new 100-day low as the trend turns bearish while long: pos goes flat at 592 and short at 596, after the short entry fires again on bar 595.
  • Traps: the dossier’s latch longOn.Sub(longOff).Hysteresis(1, -1) does not decide: the difference of two 0/1 flags is not above 1 or below -1, so it stays 0. The dossier also exits on “not bull”, which fires on an exact tie of the averages and on masked bars, where the rule says “trend turns bearish”. After a trend, a flat tail alternates in and out on alternate bars on whichever side the filter allows (a flat close is both the 100-day high and the 50-day low; flat tests the entries, a position the exits). Clenow’s own warning (ch. 7, p. 237): “you will get a slightly different value for a 40-day exponential moving average if you apply it on a half-year time series history for a market, than if you apply it on ten years’ worth of history”.
  • Variant: the reversing book, c.PositionSigned(le.Shift(1), lx.Shift(1), se.Shift(1), sx.Shift(1)), the form this page printed before PositionSignedRules (t6ClenowTrend, which the clean-room references pin), with rev := pos.Mul(pos.Shift(1)).LT(0) marking its reversals: on the test path rev is 1 on one bar and the book goes short at 592, so the two books differ on bars 592 to 595 only; on the first 590 bars, with no reversal, they agree.
  • Variant: the exclusive window, a close at least as high as the previous 100, c.GTE(c.Highest(100).Shift(1)), or the basic breakout of pp. 61-62 without the filter: drop .Mul(bull), .Mul(bear), .Add(bear) and .Add(bull).
  • Evidence: Table 4.4 (p. 73; futures, whose columns are Table 4.2’s 2002-2021 figures although the text says 1992): a Sharpe ratio of 0.70 for the core model against 0.62 for the breakout alone and 0.54 for the moving-average cross. Evidence, not a reference.
  • Pinned by TestRecipeT6ClenowTrend; runnable example ExampleSeries_PositionSignedRules_clenowFlat.

Volatility channel breakouts (Faith; Penfold)

Curtis Faith (Way of the Turtle, McGraw-Hill, 2007, ch. 10, pp. 136-138) forms the ATR Channel Breakout “by adding 7 ATR to a 350-day moving average of the closing prices for the channel top and subtracting 3 ATR from the moving average for the channel bottom”, and the Bollinger Breakout with 2.5 standard deviations either side: “A long trade is entered on the open if the previous day’s close exceeded the top of the channel”, and “Trades are exited when the close crosses back through the moving average price”. Brent Penfold (The Universal Tactics of Successful Trend Trading, Wiley, 2021, ch. 6, pp. 217-222) trades Bollinger bands of 80 days and one standard deviation and ATR bands of 80 days and 2 ATR on the same rules (“previous close above the upper Bollinger band”, “Buy next day market on open”). t6VolatilityBreakout takes the unit as a series, so one block serves all four systems. Pruitt’s initial-risk gate on bollinger-bands/t6-initial-risk-gate.md trades the Bollinger form at 60 bars and two deviations with cross entries, fills at the close and a cap on the band’s half-width; its ungated level-entry book, shifted one bar, is this recipe with c.StdDev(60) as the unit, bit for bit.

ma := c.Average(n).SetN(n-1, math.NaN())
top := ma.Add(unit.Mul(up))
bot := ma.Sub(unit.Mul(dn))
le := c.GT(top)
se := c.LT(bot)
lx := c.LT(ma)
sx := c.GT(ma)
pos := c.PositionSigned(le.Shift(1), lx.Shift(1), se.Shift(1), sx.Shift(1))
  • Inputs: closes; unit, a series of c’s length: Faith’s ATR is his book’s N, “the 20-day exponential moving average of the true range” started from “a 20-day simple average”, c.TrueRange(h, l, c).RMA(20) (the channel’s length is not stated; 20 is N’s); c.StdDev(n) for Bollinger bands (population; neither book says which); c.ATR(h, l, c, 80) for Penfold’s ATR bands (a simple average; he gives no form); n, up and dn: 350, 7 and 3; 350, 2.5 and 2.5; 80, 1 and 1; 80, 2 and 2.
  • First valid index: n - 1 for top and bot (349, 79), NaN before; pos can first trade at bar n (434, 565, 88 and 88 on the test path). RMA(20)’s +0 warm-up, bars 0 to 18, lies inside the mask.
  • Anchors: with the StdDev unit the bands are c.Bollinger(n, up) and c.Bollinger(n, -dn) bit for bit, and with the ATR unit c.KeltnerSS(h, l, c, n, n, up) and c.KeltnerSS(h, l, c, n, n, -dn); for all four units pos equals a direct loop of the rules (strict entries, strict exits back across the average, next-bar fills) bit for bit.
  • Traps: the dossier’s c.KeltnerSS(h, l, c, 350, 20, 7) averages the true range simply, TradeStation’s ATR, not N, and moves the top band on every bar (the two 20-bar true-range averages differ by up to about 15% on a walk, the band by about 4% of its level). Entries are strict (“exceeded”, “fell below”) and the exits are states: a close on the average holds. A close below the bottom band while long fires the short entry and the exit together, and PositionSigned reverses.
  • Variant: Faith’s ch. 11 exit threshold x (0 is the original rule, -0.8 his optimum): lx := c.LT(c.Bollinger(350, x).SetN(349, math.NaN())) and sx := c.GT(c.Bollinger(350, -x).SetN(349, math.NaN())).
  • Variant: TradeStation’s reading, unit := c.ATR(h, l, c, 20), whose bands are c.KeltnerSS(h, l, c, n, 20, k).
  • Evidence: Faith’s Table 10-1 (27 futures, January 1996 to June 2006, Trading Blox runs, costs not stated): 49.5% a year (MAR 1.24) for the ATR Channel Breakout and 51.8% (MAR 1.52) for the Bollinger Breakout; Penfold’s Table 6.2 (24 markets from 1980, 51 dollars a trade deducted) for his two band systems, whose settings he says are in sample; Faith’s chapter states no costs or slippage. Evidence, not a reference.
  • Pinned by TestRecipeT6VolatilityBreakout.

Donchian Trend and the time exit (Faith)

Faith’s Donchian Trend (ch. 10, p. 139) “uses a 20-day breakout for entry and a 10-day breakout for exits and includes a 350-day/25-day exponential moving average trend filter. Trades are taken only in the direction indicated by the faster moving average”; the Donchian Trend with Time Exit (p. 140) “exits the trade after 80 days and does not use any stops whatsoever”. A breakout is the price “exceeding the high or low of the preceding 20 days”, entered “when the price exceeded by a single tick” that level (the original Turtle rules), so Breakout(n, 1) on highs and lows: strict, the current bar excluded. t6DonchianTrend returns the book with breakout exits and the time-exit book.

m := c.MACD(fast, slow).SetN(slow-1, math.NaN())
le := h.Breakout(entryN, 1).GT(0).Mul(m.GT(0))
se := l.Breakout(entryN, 1).LT(0).Mul(m.LT(0))
lx := l.Breakout(exitN, 1).LT(0)
sx := h.Breakout(exitN, 1).GT(0)
pos := c.PositionSigned(le, lx, se, sx)
timed := c.PositionSigned(le, nil, se, nil, hold)
  • Inputs: highs, lows and closes; entryN (20) and exitN (10); fast and slow (25, 350); hold (80).
  • First valid index: slow - 1 (349) for the entries, where the filter starts, and exitN for the exits; the books read 0 before (the first trade is at bar 383 on the test path).
  • Anchors: le is h.GT(h.Highest(20).Shift(1)).Mul(m.GT(0)) bit for bit from bar 20 (Breakout’s identity); pos equals a direct loop of the rules bit for bit; every closed trade of c.Position(le, nil, 80) lasts exactly 80 bars (17 trades on the test path), and timed holds a trade at most 80 bars.
  • Traps: the stops rest at levels set at the previous close, so the flags are not shifted, and the fill (the level, or the open when a bar gaps through it) is the caller’s. An outside bar that breaks the 20-day high and the 10-day low enters when flat (the exit is not tested on the entry bar; bars 450, 980, 1555 and 2890 of the test path) and exits when long; bar data cannot say which came first. In timed an opposite filtered breakout before the 80th bar reverses the trade, which the book does not discuss. Breakout reads NaN on a non-finite bar and GT turns it into no break. The 2N stop is not part of pos.
  • Variant: a stop touched at the level itself, as in George Pruitt’s TradeStation code (The Ultimate Algorithmic Trading System Toolbox, Wiley, 2016, Appendix D): h.GTE(h.Highest(20).Shift(1).SetN(20, math.NaN())).
  • Variant: Faith’s ch. 5 filter, c.Average(50).GT(c.Average(300)) (the kind of average is not stated; c.MACD(50, 300).GT(0) if exponential).
  • Evidence: Faith’s Table 10-1: 29.4% a year (MAR 0.80, 1,832 trades) for the Donchian Trend and 57.2% (MAR 1.31) with the time exit; Table 10-2, to November 2006: 27.4% and 57.1%. Evidence, not a reference.
  • Pinned by TestRecipeT6DonchianTrend.

Weekly channel stop and reverse (Penfold; Donchian; Dreyfus)

Penfold (ch. 6, pp. 210-214) gives Donchian’s Four-Week Rule as “a stop and reverse strategy that is always in the market”: “Buy a break of the highest weekly high of the preceding 4 weeks” and “Sell a break of the lowest weekly low of the preceding 4 weeks”, on daily data; Dreyfus’s 52-Week Rule is the same with 52 weeks (“No one knows for sure how Jack Dreyfus implemented his 52-Week Rule”). t6WeeklyChannel sets the levels on each week’s first session from the k completed calendar weeks before it and tests every daily high and low against them.

idx := nw.Mul(0).Add(1).CumSum()
span := idx.ValueWhen(nw).Sub(idx.ValueWhen(nw, k))
hiK := h.Shift(1).HighestVariable(span, 1, len(h)).ValueWhen(nw)
loK := l.Shift(1).LowestVariable(span, 1, len(h)).ValueWhen(nw)
up := h.GT(hiK)
dn := l.LT(loK)
pos := c.PositionSigned(up, dn, dn, up)
  • Inputs: highs, lows and closes; nw, a finite 0/1 flag, 1 on each week’s first session (from a change of ISO week between consecutive sessions, the boundary rule of the timeframe page) and 0 on the data’s first bar; a calendar week with no sessions is not counted; k (4 or 52).
  • First valid index: the first session of the (k+1)-th marked week (bar 24 for k = 4 and 259 for k = 52 on the test calendar), NaN before for the levels; pos reads 0 until the first break.
  • Anchors: the levels equal a direct loop over the marked weeks bit for bit (k = 2, 4 and 52, with holidays and a closed week); for k = 4, hiK is also w.ValueWhen(nw).Maximum(w.ValueWhen(nw, 1)).Maximum(w.ValueWhen(nw, 2)) .Maximum(w.ValueWhen(nw, 3)) with w := h.HighestSince(nw).Shift(1), bit for bit; pos equals h.LevelBreakDirection(l, hiK.Shift(-1), loK.Shift(-1)) on every bar (LevelBreakDirection tests the previous bar’s level, and these levels change on a week’s first session).
  • Traps: a daily bar that breaks both levels reverses a position and leaves a flat book flat (the order inside the bar is unknown); a closure longer than a week merges two weeks in the count of nw.
  • Variant: the caller’s weekly bars, wh.Breakout(k, 1) and wl.Breakout(k, 1), which act on the week of the break, not its day.
  • Variant: the x5 daily approximation h.Breakout(20, 1) (h.Breakout(260, 1) for 52 weeks), which differs from the weekly flags (on 137 bars of the test path at k = 4; about 5% of bars on walks).
  • Evidence: Penfold’s Table 6.2 (24 markets from 1980, 51 dollars a trade deducted, his unprinted code): 6,120 trades averaging 262 dollars for the four-week rule and 475 averaging 3,038 dollars for the 52-week rule. Evidence, not a reference.
  • Pinned by TestRecipeT6WeeklyChannel.

Walter Downs’s fear levels are one-series Donchian midpoints: bull fear the midpoint of the n-bar highest and lowest highs, h.DonchianMiddle(n), and bear fear that of the lows, over windows that include the current bar, as every printed port computes them (MetaStock’s HHV(HIGH,n), Wave Wi$e’s @MAX(HIGH,NUMB), Downs’s own MidPoint and Highest), so a close is compared with levels that contain its own bar’s high and low. The levels are NaN before bar n - 1, where the windows expand; flags and crossings read 0 there and the latches 0 until their first decision. Inputs are daily bars of one length, paired by position, positive and finite with h >= l on every bar: a bar with h < l can put bull fear below bear fear, paint a bar both ways and fire both crossings at once. A decision is made at a bar’s close and filled at the next open, so the position held during a bar is pos.Shift(1). A NaN close fires no comparison, so every latch holds its state across it. The Ichimoku lines on t6-midpoint-trend.md in this category are two-series midpoints, of the highest high and the lowest low, not these one-series ones.

Bull fear, bear fear, point of balance and Testing Fear (Downs)

Downs (“From Technical Terms To Technical Tools”, Stocks & Commodities V16:8, August 1998, pp. 359-366) finds “the point at which bull bias turns to bull fear” from “the relationship between the highest highs and lowest highs”: BLF = ((HH[n]+LH[n]) /2), BRF = ((HL[n]+LL[n]) /2) and POB = ((BLF-BRF) /2) + BRF, with n = 12. Bars that close above bull fear are painted blue and below bear fear red, and his Testing Fear system buys “If the market crosses above the bull fear level from below and closes above the bull fear level” and sells the mirror, “on the next day’s open”, as a “stop-and-reverse” system. t6FearLevels returns the levels, the paint flags and the book.

blf := h.DonchianMiddle(n).SetN(n-1, math.NaN())
brf := l.DonchianMiddle(n).SetN(n-1, math.NaN())
pob := blf.Sub(brf).Div(2).Add(brf)
bull := c.GT(blf)
bear := c.LT(brf)
buy := c.CrossesAboveS(blf)
sell := c.CrossesBelowS(brf)
pos := c.PositionSigned(buy, sell, sell, buy)
  • Inputs: daily highs, lows and closes; n (Downs’s 12).
  • First valid index: n - 1 for blf, brf and pob (NaN before) and for bull and bear (0 before); n for the crossings and pos (0 before).
  • Anchors: blf equals h.Highest(n).Add(h.Lowest(n)).Div(2) bit for bit, and brf likewise on the lows. The printed pob equals blf.Add(brf).Div(2) on positive levels within a factor 2 of each other and not beyond: with bull fear 0.05 and bear fear 0.01 it is 0.03, the binary64 nearest 0.03, where the mean is one ulp higher, 0.030000000000000002. pos equals a loop of Downs’s rule, buy.Sub(sell).Hysteresis(0, 0) and, from its first signal, the paint latch bull.Sub(bear).Hysteresis(0, 0), bit for bit on the test tapes.
  • Traps: on the bad-bar witness (high 8, low 30) bull fear falls below bear fear, the close is painted both ways, both crossings fire and the book reverses where the Hysteresis twin holds. The ports’ level order (HHV - LLV)/2 + LLV equals DonchianMiddle only where the highest high is at most twice the lowest (it gives 0.03 against 0.030000000000000002 on the counterexample’s highs).
  • Variant: the position held during each bar, filled at its open, c.PositionSigned(buy.Shift(1), sell.Shift(1), sell.Shift(1), buy.Shift(1)), which is pos.Shift(1) bit for bit.
  • Evidence: Figure 3A, on 19 years of back-adjusted T-bond futures (1977 to 1996) with 20 dollars a trade, shows 191 trades, 36% winners and a profit factor of 1.74, against 272 trades and 1.07 for his 9/18-day average crossover (Figure 4); the data are not printed. Evidence, not a reference.
  • Pinned by TestRecipeT6FearLevels; runnable example ExampleSeries_DonchianMiddle_fearLevels.

Four-Bar Fear (Downs)

Downs (p. 365) noticed that “Whenever the market had four consecutive market closes less than the bull fear level and greater than the bear fear level, the first close outside this envelope … tended to be a strong indication of the direction”. His Four-Bar Fear system: “If the market closes above the bull fear level, then buy the next new high”, the mirror below bear fear, and “Exit any long positions if the market trades below any three-day low”. Read as stop orders, t6FourBarFear arms on the close outside the envelope, enters on the next bar only if it trades strictly beyond the arming bar’s high or low, exits on a low strictly below the lowest low of the m bars before the bar (a high above their highest high), and reverses on an opposite entry.

blf := h.DonchianMiddle(n).SetN(n-1, math.NaN())
brf := l.DonchianMiddle(n).SetN(n-1, math.NaN())
inside := c.LT(blf).Mul(c.GT(brf))
setup := inside.Shift(1).Sum(k).EQ(float64(k))
armL := setup.Mul(c.GT(blf))
armS := setup.Mul(c.LT(brf))
entL := armL.Shift(1).Mul(h.GT(h.Shift(1)))
entS := armS.Shift(1).Mul(l.LT(l.Shift(1)))
stopL := l.Lowest(m).Shift(1).SetN(m, math.NaN())
stopS := h.Highest(m).Shift(1).SetN(m, math.NaN())
pos := c.PositionSigned(entL, l.LT(stopL), entS, h.GT(stopS))
  • Inputs: daily highs, lows and closes; n (12), k (4 inside closes) and m (a 3-bar exit window).
  • First valid index: n - 1 + k for the arms and n + k for the entries (0 before); the stops are NaN before bar m; pos reads 0 until the first entry.
  • Anchors: every output equals a loop of the pinned reading (entry on the next bar only, strict; exits over the m bars before the bar, strict, not tested on the entry bar; opposite entries reverse) on the test tapes and the witnesses; armL equals MetaStock’s Cross form setup.Mul(c.CrossesAboveS(blf)) bit for bit, since a close above bull fear after inside closes is a crossing. On the quarter-point tape at (12, 4, 3) it arms 7 longs and 11 shorts, enters 4 and 7, and the position changes 21 times.
  • Traps: the stops’ SetN(m, math.NaN()) is needed on bar 0, where the Shift’s zero would make h.GT(stopS) read as an exit, though the book is flat until bar n + k. The MetaStock port enters at the next open (armL.Shift(1)), not on a new high, and prints its short exit as HIGH < Ref(HHV(HIGH,3),-1), < for >. A high equal to the arming high does not enter; an inclusive entry differs on the tapes.
  • Variant: an entry pending until filled, the first later bar trading above the arming high, h.GT(h.ValueWhen(armL)).Mul(h.HighestSince(armL).Shift(1).LTE(h.ValueWhen(armL))).
  • Missing piece: a pending stop order that the position or an opposite arm cancels is a loop outside this package or algolang’s.
  • Evidence: Figures 9 to 11 sample his grid test, a few years each of 25 futures and 200 stocks: 27 trades and 15 winners on the eight markets shown, unoptimised. Evidence, not a reference.
  • Pinned by TestRecipeT6FourBarFear.

Point-of-balance close oscillator (Downs)

Downs builds “an average of an average” on the closes: POBC 1 = (HC[n] + LC[n]) /2, each following level the midpoint of the one before, ten levels, their average AV, and “POBC oscillator = 100*((C - AV) / (HC[v] - LC[v]))”. His TradeStation function in the issue’s Traders’ Tips (pp. 395-397) takes Level = 10 both for the number of levels and for the close range, and assigns the value only"IF HiPrice - LoPrice <> 0". t6POBC composes the ten midpoints and the oscillator, divided first.

p1 := c.DonchianMiddle(n)
p2 := p1.DonchianMiddle(n)
p3 := p2.DonchianMiddle(n)
p4 := p3.DonchianMiddle(n)
p5 := p4.DonchianMiddle(n)
p6 := p5.DonchianMiddle(n)
p7 := p6.DonchianMiddle(n)
p8 := p7.DonchianMiddle(n)
p9 := p8.DonchianMiddle(n)
p10 := p9.DonchianMiddle(n)
av := p1.SAverage(p2, p3, p4, p5, p6, p7, p8, p9, p10).SetN(10*(n-1), math.NaN())
rng := c.Highest(v).Sub(c.Lowest(v)).SetN(v-1, math.NaN())
pobc := c.Sub(av).Div(rng).Mul(100).SetN(10*(n-1), math.NaN())
  • Inputs: closes; n (12) and v (his code’s 10).
  • First valid index: 10(n - 1) for av; 10(n - 1) or v - 1, whichever is later, for pobc: 110 for n = 12 and v = 10. Where v - 1 is later pobc is NaN to bar v - 2 through the range.
  • Anchors: every output equals a loop of the ten midpoints, their sum in order over 10 and the oscillator, bit for bit; SAverage adds the receiver first and then its arguments in order, as Downs’s loop does. Dividing first and then multiplying by 100 is the printed order. The values from the first valid index do not depend on the warm-up (a history 37 bars shorter gives the same bits). The quarter-point tape gives av 4607.6697509765627 and pobc 394.17516516359012 on bar 110.
  • Traps: the dossier’s c.Sub(av).Mul(100).Div(rng) differs in the last bit on 148, 171 and 193 of 610 bars of the three tapes. The oscillator is unbounded: the midpoints reach 110 bars back while the range spans 10 closes, and near a flat run of the quarter-point tape it falls below -11,000 (bar 321). A zero close range gives 0 through Div, where Downs’s function leaves the value unassigned. The MetaStock port halves n and computes its tenth level from the first over 9 bars; TechniFilter and SmarTrader normalise by the n-close range.
  • Variant: hold the last value over a zero range, raw.ValueWhen(rng.GT(0)) with raw := c.Sub(av).Div(rng).Mul(100).
  • Variant: the ports’ range over n closes, v = n (12).
  • Evidence: Figure 2 plots the oscillator under T-bond prices on an axis from 250 to -200, with no data printed, and Downs gives no trading rule for it. Evidence, not a reference.
  • Pinned by TestRecipeT6POBC.

The two recipes below read the midpoints and ranges of a two-series channel, the highest high of h and the lowest low of l over n bars, which DonchianMiddle (one series) does not give. Their values are NaN before their first valid index, where a window would read Shift’s zero pad or the expanding windows of Highest and Lowest; flags, states and latches built on them read 0 there, a latch’s 0 being its undetermined state. Prices are positive and finite and lengths are in bars; series are paired by position, so the caller aligns highs, lows and closes by timestamp first.

Trend Trigger Factor (Pee)

M.H. Pee (“Trend Trigger Factor”, Stocks & Commodities V22:12, December 2004, pp. 28-36) defines the 15-day buy power as the “Highest high of (day 1 to day 15 inclusive) - Lowest low of (day 16 to day 30 inclusive)”, the sell power the other way round, and “15-day TTF = ((Buy power - sell power)/(0.5*(Buy power + sell power)))* 100”; his system is long above 100 and short below -100, and “if it is between -100 and 100, you should remain with the current position”. His sidebar’s Excel sheet computes the TTF in that order and its position column tests “> 100” and “< -100” strictly and otherwise copies the row above, starting from 0. t6TrendTriggerFactor keeps both, with Hysteresis as the latch.

hh := h.Highest(n)
ll := l.Lowest(n)
bp := hh.Sub(ll.Shift(n)).SetN(n+n-1, math.NaN())
sp := hh.Shift(n).Sub(ll).SetN(n+n-1, math.NaN())
ttf := bp.Sub(sp).Div(bp.Add(sp).Mul(0.5)).Mul(100)
pos := ttf.Hysteresis(100, -100)
  • Inputs: highs h and lows l of one length; n (15; Pee also tested 10, 12, 18 and 22). The open is only his fill price, the next bar’s open.
  • First valid index: 2n - 1 for bp, sp and ttf (29 at n = 15, his sheet’s first formula row), NaN before: unmasked, Shift’s pad leaves the buy power a price on bars 0 to n - 1, with the TTF near 200(HH + LL)/(HH - LL) there, and bars n to 2n - 2 read the expanding windows; either would latch, so the mask comes before Hysteresis. pos reads 0 (no position, his seed) until the first TTF beyond 100 or -100, then 1 or -1 for good: always in, as he tested it. It is the decision after the close; the position held during a bar is pos.Shift(1).
  • Anchors: on his sidebar Figure 1 (euro, 1 October 2002 to 15 January 2003, 72 rows) the buy and sell powers match his printed columns at 2 decimals, the TTF his 7 significant digits and the latch every printed position, on all 43 rows, with the first value 85.446009389671474 (buy power 4.5600000000000023, sell power 1.8299999999999983) bit for bit. With m and r the midpoint and range of the latest box and m’ and r’ those of the box before, TTF = 200(m - m’)/((r + r’)/2) to 1e-11 relative, not bit for bit; the denominator is r + r’ bit for bit on positive prices that do not halve or double within 2n bars; TTF is above 200 exactly where the sell power is negative (the boxes do not overlap after a gap up) and below -200 where the buy power is; the latch equals up.Sub(dn).Hysteresis(0, 0) and h.PositionSigned(up, dn, dn, up) with up := ttf.GT(100.0) and dn := ttf.LT(-100.0), bit for bit, and a TTF of exactly 100 or -100 holds it.
  • Traps: his 20021224 row prints 100 but is 100.00000000000023 in binary, so the strict test fires there (the sheet is already long). The dossier’s bp.Sub(sp).Mul(200).Ratio(bp.Add(sp)) is within 2 ulp but differs on 6 of the sheet’s 43 values, and Ratio reads 0, “consolidation”, for 2n bars after a missing high or low where Div keeps NaN; both give 0 where both boxes are single flat levels, which holds the latch, while the exact value is infinite or 0/0 and Excel shows #DIV/0!. A transcription’s <= -100 is not on the printed page. AmiBroker’s and NeoTicker’s tips go short on a cross below +100, and the Financial Data Calculator tip computes -TTF.
  • Variant: the platforms’ crossing form, ttf.CrossesAboveS(100.0).Sub(ttf.CrossesBelowS(-100.0)).Hysteresis(0, 0) (TradeStation’s and MetaStock’s tips), agrees with pos from its own first decision on data without missing bars; after a missing bar it waits for a new cross.
  • Variant: for NaN where both boxes are flat, ttf.Add(bp.Add(sp).Equal(0.0).Mul(-1).Sqrt()): Sqrt(-1) is NaN, and adding Sqrt(-0), which is -0, changes no other bar.
  • Evidence: his Figures 4 to 6 (35 futures markets, 2 January 1980 to 30 April 2003, one contract, 75 dollars a trade): at n = 15, 3,126 trades, 39.06% winners and 1,412,493 dollars net, with 30 of 35 markets and 21 of 23 years profitable, in sample and by the author alone; on a random walk the TTF is above 100 on about 18% of bars. Evidence, not a reference.
  • Pinned by TestRecipeT6TrendTriggerFactor; runnable example ExampleSeries_Hysteresis_trendTriggerFactor.

Ichimoku Kinko Hyo (Hosoda; Muranaka)

Ken Muranaka (“Ichimoku Charts”, Stocks & Commodities V18:10, October 2000, pp. 22-30) gives Goichi Hosoda’s lines: “Standard line = (Highest high + lowest low) / 2 for the past 26 days, including today”, the turning line the same over nine days, the first preceding span their mean and the second the 52-day midpoint, each “recorded in the table 26 days ahead of today, counting today”, and a buy “when the turning line shoots above the standard line”. His sheet writes the spans 25 rows on and its delayed line puts the close 25 rows later on each row, so the displacement d is 25 bars. t6Ichimoku returns the spans in force at each bar, Shift(d) of the spans computed d bars earlier, the close’s state against that cloud, the TK cross and the causal reading of the delayed line.

mid9 := h.Highest(t).Add(l.Lowest(t)).Div(2)
mid26 := h.Highest(k).Add(l.Lowest(k)).Div(2)
mid52 := h.Highest(b).Add(l.Lowest(b)).Div(2)
tenkan := mid9.SetN(t-1, math.NaN())
kijun := mid26.SetN(k-1, math.NaN())
spanB := mid52.SetN(b-1, math.NaN())
aNow := tenkan.Add(kijun).Div(2).Shift(d).SetN(d, math.NaN())
bNow := spanB.Shift(d).SetN(d, math.NaN())
top := aNow.Maximum(bNow)
bot := aNow.Minimum(bNow)
state := c.GT(top).Sub(c.LT(bot)).MaskNonFinite(c, top, bot)
tk := tenkan.CrossesAboveS(kijun)
chikou := c.Delta(d).GT(0)
  • Inputs: highs, lows and closes (the open is not used); t <= k <= b (9, 26 and 52) and the displacement d in bars: 25 for “26 days ahead of today, counting today”, as in his sheet, TradingView’s built-in (offset = displacement - 1) and pandas-ta’s releases from 0.4.26b (on PyPI and conda-forge); StockCharts and pandas-ta 0.3.14b shift 26 bars.
  • First valid index: t - 1 for tenkan, k - 1 for kijun, d + k - 1 for aNow and d + b - 1 (76 at the defaults) for bNow and state, NaN before; tk reads 0 until bar k and chikou until bar d. Each line is masked before it is combined: unmasked, the two lines start as the same expanding midpoint and the TK cross can fire in the warm-up. A missing bar makes state NaN on the bars whose cloud holds it, where without MaskNonFinite it would read 0, inside the cloud.
  • Anchors: his 20 printed turning-line and 3 standard-line values hold bit for bit (every midpoint is exact); h.DonchianUpper(n).Add(l.DonchianLower(n)).Div(2) is the midpoint bit for bit, while h.DonchianMiddle(26) is a one-series midpoint and not the standard line (it is Downs’s bull fear level at n = 26, on t6-fear-levels.md in this category); chikou equals c.GT(c.Shift(d)) from bar d.
  • Traps: look-ahead: the delayed line as he charts it is c.Shift(-d), which puts the close of bar i + d on bar i (“it looks ahead in the data”, his column ending in zeros), so a rule that reads it at its plotted bar uses a future close; every printed chikou rule compares today’s close with values d bars back, which is causal (chikou). The cloud’s edges are Maximum and Minimum: the mean plus or minus half the difference is not exact, and with aNow 79.01, bNow 79 and a close of 79.01 its top reads 79.009999999999991, which moves the close from the edge (0) to above the cloud (+1). CrossesAboveS fires again after a bar with the two lines equal. Elliott (“Ichimoku Kinko Hyo Charts”, Stocks & Commodities V25:9, September 2007) calls the two lines moving averages of the bar midpoint, which they are not (on Muranaka’s row 10, 1000 against 1001.72).
  • Variant: the plot forms: the delayed line with a NaN tail instead of zeros, c.Shift(-d).Reverse().SetN(d, math.NaN()).Reverse() (look-ahead), and the projected cloud, the d spans beyond the last bar, tenkan.Add(kijun).Div(2).Right(d) and spanB.Right(d).
  • Variant: the strict TK state change, tenkan.Sub(kijun).Hysteresis(0, 0).CrossesAboveS(0.0), which does not fire again after a tie (it can decide on the standard line’s first bar instead), and the TK cross above the cloud, tk.Mul(state.GT(0)).
  • Evidence: Lim, Yanyali and Savidge (“Do Ichimoku Cloud Charts Work and Do They Work Better in Japan?”, IFTA Journal 2016 Edition, pp. 18-24) traded chikou-cloud crossings on 446 US and 202 Japanese stocks from 2005 to 2014, without costs and on current constituents, and found both markets profitable with “little evidence for the tool working better in one market”. Evidence, not a reference.
  • Pinned by TestRecipeT6Ichimoku; runnable example ExampleSeries_Shift_ichimoku.