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.
pbis 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) forentry, 0 before;posreads 0 until the bar after the first entry, bar 80 at the earliest. On the test’s 700-bar pathpb[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)equalsc.XAverage(40).GT(c.XAverage(80))bit for bit on every bar, som.GT(0)equals the pair of averages from bar 79; unmasked, MACD is exactly 0 on bars 0 to 38 and exactlyXAverage(40) - con 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.posequals 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, sopbagrees across architectures only to rounding (by at most 4.4e-15 on the test path), whileentryandposagree bit for bit: on the test data|m|is at least 0.0203565 andpbat 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 andATRWilderWilder’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’spb.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 pandasewm(span)over a 250-bar history, whichXAveragewith its simple-mean seed does not reproduce. The rule list omits the 20-day time exit, which isPosition’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())withpb := 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 exampleExampleSeries_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:
consolequals the EasyLanguage decision as a loop bit for bit, andconsolATRthe same on the ATR. On a zero reference (a flat stretch)consolis 0 whileratioreads 0 andratio.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. Addingref.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’sATR_[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 exampleExampleSeries_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;
va4,delay4 andvr1, the code’s VolAvg, VolDelay and VolRatio. - First valid index: the first consolidation bar for
topandbot(data-dependent, at least 2n - 1; NaN before);posfrom bar 0, with no buy before 2va + delay - 1 (11), where the volume rule is first valid. - Anchors:
posequals 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
Botat 0, soBot>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
lowVolandhighVol(k with MultiCharts’ ATR start), 2 forshrink3anddc3, 1 forsmaller; 0 before (onlydc3needs its mask). - Anchors:
ConsecDowncounts strictly smaller ranges and resets on a tie or a NaN: the ranges [5 4 3 NaN 2 1 0.5] giveshrink3[0 0 1 0 0 0 1],dc3[0 0 0 0 0 0 1] andsmaller[0 1 1 0 0 1 1]; Unger’s pattern is two decreases in a row, henceGTE(2). Withcoeff1,highVolequalstr.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,dc3would 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,
lo0.25,hi3.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);
okis 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. Sovrequals the plain quotienttr.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’str.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 forhiandlo(NaN before), 8 for the expansion;longandshortfrom bar 0;heldreads 0 until the first detection,upHeldanddnHelduntil the first release, andhiHeldandloHeldare 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
hi104.25 andlo103; 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 exampleExampleSeries_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;
rl4,rr12,v30,ratio0.3,factor1.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 takesc.ATR(h, l, c, 14), with no guards and no masks. The entry levels and Example 5.3’sVolatility(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,
caandcb, of equal length on the same dates (right-aligned; a shorter one reads 0 before its overlap); n 10,b0.05. - First valid index: n - 1 for
both(0 before; unmasked, bar 0 is a one-bar line);accumanddistrare 0 before the first line, whereBarsSinceis NaN. - Anchors:
both,accumanddistrequal a loop bit for bit on two test paths, andaccumanddistrare 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 formc.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
NATRand 0 throughDiv’s zero rule intvandsma. - 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 fortvandsma, whose first window holds bar 0’s true range, h[0] - l[0]. The masked bars read NaN; unmasked,NATRreads +0 there andATRpasses the true range through, sosmawould read TR/c times 100. - Anchors:
c.NATR(h, l, c, n)equalsc.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]equalssma[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.
TrueRangematches its 32 true ranges andTrueRange.RMA(14)its 19 ATR values at two decimals, whileATRWilder(14)andATR(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
natrincludes bar 0’s true range and starts at n - 1, so it istv, 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’sc.TrueRange(h, l, c).XAverage(27)has Wilder’s constant but seeds over 27 bars and is neither form. SinceTrueRangegives h[0] - l[0] at bar 0, the simple form needs n - 1 masked bars, not n. - Variant: pandas-ta’s
natrwithout 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;
TrueHighandTrueLowgive 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 exampleExampleSeries_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
thrin 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);sidereads 0 through bar n - 1 and until the first decision. - Anchors: negation is exact, so
-nvi > -thrholds exactly whennvi < thr:sideis 1 from a bar withnvibelowthr, -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 withthr1.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 closeATRWilder’s recursion does not recover, sonviis NaN from that bar on andsidekeeps 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 forsrMandtri, NaN before, whereStochasticreads exactly 50 andXAverageAlphaS+0.tri[k-1]issrM[k-1], and the recursion runs with the constant 2/(e + 1) from bar k. - Anchors:
vequalsd.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 andd.LTE(0)is 0; elsewhereDiv’s zero rule gives 0 and the true range is added.Stochasticgives 0 on a flat window, Weinberg’s else branch, whereChannelPositiongives 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 exampleExampleSeries_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);
setupandtrigger(27 and 26.5). - First valid index: 2(e - 1) + n - 1 for
mi, 40 for the defaults (NaN before).e1passes the range through on bars 0 to e - 2 ande2starts 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.latchandbulgeread 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], ase2starts at bar 8) and its first ratio at bar 17 (e1[17]overe2[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:
Hysteresisis 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 ismi.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’se.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, somidiffers 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 ownXAverageAlpha(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 ownXAverageAlpha(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)ismifrom bar 2(e - 1) + n - 1, bit for bit, for e >= 2, and reads 0 before; at e = 1 the block’se1.Window(-(e - 1))is empty, somireads 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’sbulgemarks two bulges, at bars 119 and 265. - Pinned by
TestRecipeT6MassIndex; runnable exampleExampleSeries_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 levelbuy.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);
posandbookread 0 until the first single fill;exandresare 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;posequals 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). Forkb, ks >= 0,buy >= o >= sell, the fill priceo.Maximum(buy)isbuy, and as a cross-checkbuyequalso.SuperTrend(h.Shift(1), l.Shift(1), l, kb, 1)from bar 1. Crabel’s opening-range test,t6CrabelORBonbar-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:buyis that level with x the seriesrng.Mul(kb), yesterday’s range times kb, and no setup.book,exandresequal 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;delay1 isdelay0; with no stop (d+Inf) and the bailout held back (delaymath.MaxInt)bookispos. On the q tape (kb and ks 0.5, d 30, delay 0)booktakes 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 withdelay2 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’sweekday(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 noTouchand is read on the receivero; the stop rests and is touched by the low, so it hasTouch: l, touch-inclusive.resisexless 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 iso.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
exis 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 exampleExampleSeries_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.
statereads 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 >= dn2from bar 2. A down close and an up close cannot both qualify a bar, so the latch equals itsPositionSignedtwin. - Traps: “takes out” is strict and “reaches” is a touch: on the tie witnesses a high equal to
up1after a down close and a low equal todn1after an up close do not extend, whereh.GTE(up1)andl.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
up1and reachesup2,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 bySetN(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 exampleExampleSeries_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
svand 4 forbuyandsell, NaN before, and 4 for the fills, 0 before; the levels are masked after theShift, since a mask before it is undone on bar 0, where the Shift’s pad is +0 again and the level reads the open.bookreads 0 until the first single fill, andexandresare NaN until the first exit. - Anchors: on the hand case the swing and both levels are those of the runnable example;
sv,buyandsellequal a loop of the two swings, the larger taken, bit for bit on the test tapes. The dossier’sIfform,a.Sub(b).If(func(v float64) bool { return v > 0 }, a, b), equalsMaximumon finite inputs and returnsbwhereais NaN, hiding a NaN high on the walk that has one; the abs forma.Add(b).Add(a.Sub(b).Abs()).Div(2)is not exact (on the cent witness the swing 102.57000000000001 differs).book,exandresequal the open breakout’s loop of his exits, with the fills atbpxandspx, bit for bit on every group for four distances; on the q tape (d 30)booktakes 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.
bpxandspx, 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’ closesbc(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
exis 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 exampleExampleSeries_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);
annfrom the caller.hvSis the lead form, the sample deviation with the actual mean;hvPis the population one,hvP0the population root mean square (a zero mean, the settlement form of realised-volatility contracts) andhvS0the zero-mean sample form. - First valid index: n for all four (NaN before). Unmasked,
StdDev,StdDevSampleandRMSread +0 on bars 0 to n - 2 and NaN at n - 1, whose window holds bar 0’s NaN return (theShiftpad is 0, not a price), andSquared().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,hvP0andhvSgive the printed 37.62, 37.88 and 39.65 percent;hvS0gives 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) onlyhvSrounds to 27.8.hvP0squared ishvPsquared plusannsquared times the window’s squared mean, to rounding;c.LogRatio(c.Shift(1))equalsc.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
volatilitycounts 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
hvSover 30, 60, 130 and 260 returns, an assumption since his 52-week volatility is a weekly-return one (p. 86), the five-month weights givehv30.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 anExponentialWeights(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)ishvSfrom bar n, bit for bit; the method reads 0, not NaN, on bars 0 to n - 1. - Pinned by
TestRecipeT6HistoricalVolatility; runnable exampleExampleSeries_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:lamis a cross-sectional input. - First valid index: 0.
- Anchors: Table 59 (p. 242): with
lam1 the prices 4, 25 and 100 end at 9, 36 and 121 bit for bit, moves of 125%, 44% and 21% at his integers. Withlamfor a 10% rise of a 25 stock,math.Sqrt(27.5) - 5, that stock moves 10% to rounding. - Traps:
lamis a number: a one-element series in its place is right-aligned and adds to the last bar only, soSeries{4, 25, 100}.Sqrt().Add(Series{1}).Squared()gives 0, 0, 121. The dossier’s per-seriesc.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:
XStdDevdoes not recover and the count of valid bars turns NaN);span32 (35 in Strategy nine’s box);lw2,560, ten years of Carver’s 256-day year, or 2,520 on a 252-day year;ann16. - 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, whichDivgives as 0, andWindow(-1)drops it). NaN before; unmasked,sigandlongread +0 there. - Anchors:
longequalssig.AverageWhere(valid, 0)bit for bit before thelw-th valid bar andsig.Average(lw)from it. At the first valid barlongissigandblendequalssigto rounding. - Traps: the dossier’s
sig.Average(lw)passessigthrough forlw- 1 bars, so its blend issigto one ulp for the first ten years. A bar-0 return of 0 left in place entersXStdDev’s seed, andSetN(1, math.NaN())instead would make it NaN for good. Carver’s annualised risk is of percentage returns, not of the dossier’s log returnsc.Log().Delta(1). - Variant: the risk of price changes that Carver does not annualise (“Do not annualise”) takes
c.Delta(1).Window(-1)asrandann1. - Variant: pysystemtrade’s
mixed_vol_calc(span 35, pandas’adjust=Trueandbias=False) blends the deviation of price differences, divides by the price afterwards and takes an exponential long mean (pandasewm(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).XStdDevseeds 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;
annfrom 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 (
Divreads 0 on theShiftpad, 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:
avSis 0.5 x sqrt(20/19) xannbit for bit,avis 0.5 xannwithin 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) andhvis atanh(0.5) x sqrt(20/19) xannto 4.1e-16.requals a loop of c[t]/c[t-1] - 1 bit for bit from bar 1, and the realised-volatility page’s Tulip returnc.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”:rrounds 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), andTrueRangerounds 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 fromrin 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,hvwith n 20 andannthe 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;
shthe unfiltered base position, 0 or 1, of c’s length (it is the receiver ofr, soshof another length gives outputs of two lengths,avandgatedof its length andoutof c’s); n 20,ann,x0.30,xb0.30 or the 0.25 buffer.r3is the net return of the last three days. - First valid index: n for
av(NaN before);outandgatedfrom bar 0. Bar 0’sris -0 (theShiftpad times -1), not a return, and it still sits in the window ending at n - 1.out1 is standing aside, decided at the close and in force from the next bar throughout.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
shall ones, x 0.30 and xb 0.25, the book stands aside on bars 48 to 94 and re-enters at 95 (av0.307409 at 48 and 0.247045 at 95); with xb 0.30 it re-enters at 93.outequalsexitEv.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)andreenter.Sub(exit).Hysteresis(1, -1)stand aside on no bar (a difference of two 0/1 flags is not above 1 nor below -1), andPosition(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 isr.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,
shall 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 exampleExampleSeries_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
avafter 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
lvlXandlvlR(271 at 20 and 252; NaN before);outandposhold 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 arelvlXandlvlRat the last bar (for reproducing his tables only).posequals 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), andoutequals a loop of “stand aside abovelvlXuntil belowlvlR”. 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 = 0is 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 itssignalsurvives 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 (hisexitvol = 0) and a re-entry level of 0 his re-entry path (Avol < 0does 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))andav.Quantile(m, 0.5).Add(av.StdDev(m)), masked withSetN(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 isc.Position(up.Mul(av.LT(lvlX)), dn.Add(av.GT(lvlX)).GT(0))bit for bit (Listing 2 withreentervol = 0, the armed state without a re-entry flag a flat one) andt6VolatilityExit’spos. - 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;vIn0.20 andvOut0.05 as fractions;kIn15 andkOut60 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 forexit(0 before);posfrom bar 0. - Anchors:
posisPosition’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’spos.Shift(1). As printed,volthreshold(20)andvolexitthreshold(5)are percents against a fractionalavol, so the entry fires on no bar and the exit on every bar from n. - Traps: no guard for a flat range:
Stochasticreads 0 on a flat 14-bar range, which passespk.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 (atkOut40 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;
nsandnlin 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, whichDivreads as 0, soLT(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) giveHistoricalVolatility(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 givesratio0, whichMul(hvL.GT(0))keeps out ofcomp. - 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 theLogRatiopopulation 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)withHistoricalVolatility(180)overHistoricalVolatility(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 onc.Log()withStdDevSample, 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 needBetaIncomplete(a, b)orFCDF(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 exampleExampleSeries_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);
persistis 0 before 98 + kd. - Anchors:
persistequals the run-length formf6.Mul(f6.StateAge()).GTE(kd)bit for bit, both exact integers; the thrust’sBreakout(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, andshock.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 inHistoricalVolatility(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,c10andavg3(bar 99’s window holds bar 0’s NaN return), 111 forema(NaN before). - Anchors:
emaequalsavg3.Window(-100).XAverage(12)re-aligned, bit for bit:c.Mul(0).Add(shorter)is the realignment idiom,Addreading 0 before the shorter operand’s overlap, andXAverageseeds with the mean of the first 12 values. The dossier’sc.Log().Delta(1)ratios equal these bit for bit from bar 100. - Traps:
XAverageof the maskedavg3is 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 aLT(0.5)test fires there; a NaN close after bar 100 makesemaNaN for good while the ratios recover 100 bars later. Both printedFormula:lines are malformed (an unbalanced parenthesis;Mov(missing). Go’s logarithm is not correctly rounded andXAveragefuses on arm64, so the ratios andemaagree 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:
ivin points; n = 252 previous sessions, Spina’s 252 trading days (Lindgren’sinput period = 252is 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;ivpInclranksiv.Mul(-1), whose values below today’s are those above it iniv, and takes that count from n. - First valid index: n (252) for
ivp,ivrandivpIncl, n - 1 (251) forivpLind; NaN before. Unmasked,ivpreads +0 there (RankPosition’s warm-up),ivr50 (Stochastic’s),ivpIncl1 andivpLind1/n. A missing print makes every output NaN on the bars whose window holds it, from its bar to n bars later (n - 1 forivpLind). - 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) andivpLind= 152/252 (60.32%, also 60). For every j = 0..252 the repairedivpis j/252 exactly.ivpIncl - ivpis 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, movesivpby at most 1/n but holdsivrdown for the n bars after it. A flat window reads 0 inivpandivr.ivrequals Spina’s order, the difference over the range times 100, bit for bit, and thinkorswim’s100*(V - low)/rangeto 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 exactlyiv.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 thatRankPosition(253)“interpolates and includes the current value” are stale:RankPositionis that kernel and counts strictly below. Itsiv.Stochastic(iv, iv, 252)besideiv.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) andStochasticreads 0. Lindgren’s text puts today “higher than 151 of the prior 252 trading days”, Spina’s count; his listing’sfold index = 1 to period with perRank = 1stops beforeperiod(thinkScript’stois exclusive), so it counts the previous 251 values from a seed of 1, which isivpLind, 152/252 on the year: both display 60, and hisTosIVRankoverHighest(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 thanivr’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 exampleExampleSeries_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
candivin 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.
hvis 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, whichhv’s NaN covers.ratiois also NaN wherehvis 0 orivis NaN. - Anchors:
hvis 100 times thehvSoft6HistoricalVolatilitybit for bit, the same operations with theMul(100)last (“Close-to-close historical volatility (Natenberg)” on the realised-volatility page gives that form and its anchors), andHistoricalVolatilityshortens it toc.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), equalshvto rounding. Twenty unchanged closes (n zero returns) givehv0 andratioNaN, whereiv.Div(hv)alone reads 0.ratiois unit-free with both in points,vrpis in volatility points andvrpVarin 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 duplicatest6HistoricalVolatility.hvSitself is a fraction:iv.Div(hvS)withivin 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);fcErrputs 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” isiv.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), withvthe VIX in points andhcandhxthehvS0form oft6HistoricalVolatilityon 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
candivin 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, ormath.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
ivis NaN) and m forshare; NaN before. Unmasked,Averagepasses the raw 0/1 flags through on bars 0 to m - 2, and bar 0’s flag comparesDelta’s 0 with theShiftpad’s +0. - Anchors: Natenberg’s 45-dollar stock at 37% with
tau1/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; withtau1/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%,upLless 100 is 1.25 withtau1/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 (ann16, m 5), which are +0.98, -0.65, -0.70, +0.25 and -0.85, leaveshareNaN 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 daysupLless 100 is 5.7338217908099551 (her 5.73), and on her 275 at 19% over 45 daysupLless 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 (tau1) 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 ativ20 andann16 gives a 0 flag (strictGT).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,tau1/16 andann16. Spina’s 30 calendar days are not 21 of 252 trading days, which give 5.77 for her 5.73. The dossier’s exceedancec.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, sosharecounts 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
st0and the strangles one and two strikes out,st1andst2,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 exampleExampleSeries_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);nbars (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;
LogRatiois NaN at bar 0, whereLog().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 inilliq, whereDivalone would count a 0, andMaskNonFinite(mv.Log())makes a window of unchanged closes NaN inliq, whereDivwould read no liquidity. - Anchors: on openquant’s hand vector (closes 100, 110 and 99, dollar volume 1e6, n = 2)
illiqis (ln 1.1 + |ln 0.9|)/2e6 to rounding of the logarithm andliqis 99,999.99999999997;LogRatioequalsLog().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), whereilliqis 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;
hasbis NaN while a window holds bar 0’s NaN return. - Anchors:
kyleequals the ratio of window sumsdp.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
bfrom the bar’s own close change both lambdas are sign-free:kyleisdp.Abs().BetaOrigin(v, n)andhasbisr.Abs().BetaOrigin(dv.Sqrt(), n)bit for bit oncebis 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 (Divby 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 andr.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 exampleExampleSeries_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 onek(2) for both pairs; population standard deviations, as Bollinger and TradingView use. - First valid index:
n2- 1 (NaN before). Unmasked, bars 0 ton1- 2 read exactly 0, because every band is the close, and barsn1- 1 ton2- 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)andc.Bollinger(n2, -2.1)asup2, lo2changes 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;
nandk(20 and 2);mfor 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 forpctBandwidth,nforimpulse, max(n- 1,m) formom; NaN before. Unmasked,pctBis 0/0, whichDivgives as 0, on bars 0 ton- 2 (the bands are the close there); the unmaskedimpulsereads +0 at bar 0 (Delta’s pad) and NaN on the other warm-up bars, soSetN(n-1, math.NaN())gives the same series; withmaboven- 1 the unmaskedmomreads 0 on barsn- 1 tom- 1, a false “no momentum”. - Anchors: on Bollinger’s 1992 DJIA sidebar (24 September to 1 November 1991),
Average(20),StdDev(20)andBollinger(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.pctBequalsc.Standardise(n).Div(k + k).Add(0.5)to rounding except on a flat window, wherepctB,widthandmomare 0 and the z-score form 0.5.widthis 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)andwidth.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;
nin bars (15);pas a fraction (0.02). The percentage is of the average, not of the price. - First valid index:
n- 1 form,upandlo(NaN before, where MetaStock’s average is undefined);buy,shortandposread 0 through barn- 1, so the first possible signal is barn.Shift(1)pads bar 0 with 0 on both sides, and 0 < 0 does not hold. - Anchors:
c.CrossesAboveS(up)equalsbuyexcept 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 wherebuyandshortread 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 asCrossesAboveSandCrossesBelowSentries 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())withmmasked first, first validn+ 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 exampleExampleSeries_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;
alphain (0, 1], his grid 2/(N + 1) for N = 4, 6, …, 20;beta0.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 formc.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 withalphaandbeta0.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 ofXAverageAlphapasses 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
limitin 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);posreads 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)andavg.Sub(risk)arec.Bollinger(n, k)andc.Bollinger(n, -k)bit for bit from bar n - 1, whilec.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 ist6VolatilityBreakout(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)andc.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 exampleExampleSeries_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,
sm3,a25,lim20,adxCap1.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 forpos(180), max(n - 1, a) forsdAdx(29;ADXreads 0 before a, a false 0 that would pass the cap when a > n - 1), 180 forsqueeze; NaN before for the values, 0 for the flag. Katsanos’s “170 days of data” covers his 170-day regression slope only. - Anchors:
posequals 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 150posis NaN wheresdc.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) checkssdcat n = 20, on the sheet’s typical price, as a normalised deviation: its printed SD over its printed average. - Traps:
widthis 2k timessdcto rounding only: the band subtraction loses the low bits as the bands narrow (bit-equal on a few percent of bars). The houseADXstarts at a from zero-started averages; Wilder’s seed (t6WilderDMIon 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 exampleExampleSeries_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 foratLow(143) and n + q - 1 fornewLow(144); 0 before. - Anchors:
atLowiswidthat its q-bar minimum,width.EQ(width.Lowest(q))bit for bit, andnewLowiswidth.LT(width.Lowest(q).Shift(1))from n + q - 1. On a flat stretchwidthis 0,atLow1 throughout andnewLow1 on the first zero only. - Traps: unmasked,
widthis 0/0 = 0 on bars 0 to n - 2, whereatLowwould 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),
kBB2,kKC1.5.KeltnerisKeltnerSS, a simple-average midline plus a simple average ofTrueRange, on the midline ofBollinger. - 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 forfire, 2n - 2 forval,longandshort(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.fireequalssq.Delta(1).LT(0)bit for bit, andvalmatches 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]ish[0] - l[0]and the first valid index is n - 1, and itssq.SetN(21, math.NaN())makesfireNaN. pandas-ta 0.4.71b0 ignoresbb_stdanduse_tr(passlower_std,upper_stdandtr=Falseinstead), takes the sample deviation without TA-Lib and setsSQZ_NOin 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 exampleExampleSeries_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,
kBB2, 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)ist6KeltnerSqueeze’ssqat 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 defaultsma(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, lowland closecand the lengthx(“a few periods”; 5, which his Figure 1 and the Value Chart’s default fit);cvireads in units of 0.2 average true ranges. - First valid index:
x - 1(4).ATRtakes bar 0’s true range as its high less its low, so the sidebar, whose first true range needs a previous close, agrees from barx. - Anchors: the consensus is the middle of the Keltner channel,
mid.KeltnerSS(h, l, c, x, x, 0)withmid := h.Add(l).Mul(0.5), bit for bit, so off ties and flat windowscvi.GT(j)equalsc.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.printedis0.2 cvito rounding. - Traps:
c.Sub(f).Div(v.Mul(0.2))andc.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) throughDiv.Averagepasses its input through before barx - 1, hence the mask. - Variant: the normalised candles are
o.Sub(f).Div(v.Mul(0.2))and the same withhandl. - 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 formrg.Mul(rg.GTE(dc)).Add(dc.Mul(rg.LT(dc)))withrg := h.Sub(l)anddc := 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 = 5gives 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 exampleExampleSeries_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
setupflags, the value indicatorcvi,level(-8) and the windoww(5);entryis 0 or 1. - First valid index: the first setup plus 1; before the first setup
BarsSinceis NaN and nothing fires. - Anchors: a setup gives at most one entry, only on bars 1 to
wafter it, and a new setup restarts the count; the setup bar itself does not enter, even withcvibelow 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 forx <= 6: on a breakout bar the close is above thex - 1previous medians and at least the bar’s low, soc - f > -(h - l)/(2x) >= -tr/(2x)withtrthe bar’s true range, while0.2 atr >= 0.2 tr/x, hencecvi > -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);
ivLenandvolLen(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 ofHighestVariableis 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
okkeeps out the expanding windows andAverage’s pass-through), NaN before the first decision; the flags then need q bars before the bar, and they andposread 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)andl.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 isValueWhen, not a mask. - Variant: where both flags fire on one bar,
PositionSignedstays flat when flat and reverses in a position; Castleman does not say, andbuy.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 exampleExampleSeries_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;HighestVariableclamps it to [6, 50] and truncates a fraction, henceRoundin 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;
posreads 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 theSetN(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 onl.LT(l.Shift(1).SetN(1, math.NaN()).LowestVariable(p.Mul(3.4), 1, 200))withPosition(hisBuyfills 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);
holdin 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 makeupanddn0 on bar 0, sobuyandsellread 0 on bars 0 and 1);poshas no warm-up and reads 0 until the first signal (bar 2 at the earliest);stopLandstopSare NaN whileposis flat and defined on every trade, long or short, each the entry bar’sLowest(2)of the lows orHighest(2)of the highs: a long trade’s stop isstopLand a short’sstopS. 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] andstopS[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).buyequals the dossier’sup.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 abovel.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”) takesup2 := c.GT(h.Highest(2).Shift(1)).SetN(2, 0)and its mirrordn2onl.Lowest(2), and its book equalsup2.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 unmaskedbuyis [0 1 1] on the three weeks, a signal a week early. The stop is held from the entry, where the dossier’sl.Lowest(2)is a moving two-week low, and its “latch the value at entry (strategy layer)” is stale:PositionSignedRulesholds 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))orc.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)andlast.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
- the Eurodollar one-close two-week channel, stop and reverse:
- 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 exampleExampleSeries_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 ofStdDevinsideBollinger(TradeStation’sStdDevSis the sample form). - First valid index: n for
f0andf1, n - 1 forf2, 0 before. The mask off0follows theShift: 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 theShiftstill leaves bar 0. The masks off1andf2are no-ops on positive prices (the average and the band pass the close through on bars 0 to n - 2, andc > cfails): the test asserts the masked and unmasked forms equal. - Anchors:
f1is his “both the current close above the current moving average value and the previous close above the previous moving average”;c.Bollinger(n, k)isc.Average(n).Add(c.StdDev(n).Mul(k))bit for bit. The population and sample deviations splitf2on bar 199 of the 700-bar fixture group and on none of 2,800 probe walk bars. His book iso.Position(f.Shift(1), nil, 21): bought at the next open and sold at the open afterBarsSinceEntryreachesBarsIN20, 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 ofLenfrom 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 exampleExampleSeries_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;
shortandlongin 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;statereads 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.
chgequals his loop to rounding only, by about 2e-15 at log prices near 4.6 (Averagekeeps 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 boundchgat 1e-13 relative, since Go’smath.Logis not correctly rounded, and assertstatebit 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 makeschg-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’sd := c.Log().Average(s).Sub(c.Log().Average(l))withGTEandLTEis CD_MA’s difference, not DEV_MA’s strict ratio, and itss = 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 asPositionSigned, 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 crossingc.CrossesAboveS(hh).Sub(c.CrossesBelowS(ll)), withhh := c.Highest(w).Shift(1).SetN(w, math.NaN())and its mirrorll, 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 exampleExampleSeries_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;
fastandslow(50, 100);entryN(100) andexitN(50). Clenow sizes by a 20-day ATR, outside the recipe. - First valid index: max(slow, entryN) - 1 for
leandse(99), the first bar they can fire, and the next bar forpos(next-bar fills); 0 before. Unmasked, MACD’s bars fast - 1 to slow - 2 areXAverage(fast)less the close and fire, and so doesHighest’s expanding warm-up (10 of bars 0 to 98 on the test path); the levels’ masks keepleat 0 before entryN - 1 even under a shorter trend filter. - Anchors:
m.GT(0)isc.XAverage(50).GT(c.XAverage(100))bit for bit;c.GTE(c.Highest(n))isc.EQ(c.Highest(n));posequals 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:posgoes 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 beforePositionSignedRules(t6ClenowTrend, which the clean-room references pin), withrev := pos.Mul(pos.Shift(1)).LT(0)marking its reversals: on the test pathrevis 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 exampleExampleSeries_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,upanddn: 350, 7 and 3; 350, 2.5 and 2.5; 80, 1 and 1; 80, 2 and 2. - First valid index: n - 1 for
topandbot(349, 79), NaN before;poscan 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
StdDevunit the bands arec.Bollinger(n, up)andc.Bollinger(n, -dn)bit for bit, and with theATRunitc.KeltnerSS(h, l, c, n, n, up)andc.KeltnerSS(h, l, c, n, n, -dn); for all four unitsposequals 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, andPositionSignedreverses. - 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()))andsx := c.GT(c.Bollinger(350, -x).SetN(349, math.NaN())). - Variant: TradeStation’s reading,
unit := c.ATR(h, l, c, 20), whose bands arec.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) andexitN(10);fastandslow(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:
leish.GT(h.Highest(20).Shift(1)).Mul(m.GT(0))bit for bit from bar 20 (Breakout’s identity);posequals a direct loop of the rules bit for bit; every closed trade ofc.Position(le, nil, 80)lasts exactly 80 bars (17 trades on the test path), andtimedholds 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
timedan opposite filtered breakout before the 80th bar reverses the trade, which the book does not discuss.Breakoutreads NaN on a non-finite bar andGTturns it into no break. The 2N stop is not part ofpos. - 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;
posreads 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,
hiKis alsow.ValueWhen(nw).Maximum(w.ValueWhen(nw, 1)).Maximum(w.ValueWhen(nw, 2)).Maximum(w.ValueWhen(nw, 3))withw := h.HighestSince(nw).Shift(1), bit for bit;posequalsh.LevelBreakDirection(l, hiK.Shift(-1), loK.Shift(-1))on every bar (LevelBreakDirectiontests 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)andwl.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,brfandpob(NaN before) and forbullandbear(0 before); n for the crossings andpos(0 before). - Anchors:
blfequalsh.Highest(n).Add(h.Lowest(n)).Div(2)bit for bit, andbrflikewise on the lows. The printedpobequalsblf.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.posequals a loop of Downs’s rule,buy.Sub(sell).Hysteresis(0, 0)and, from its first signal, the paint latchbull.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
Hysteresistwin holds. The ports’ level order(HHV - LLV)/2 + LLVequalsDonchianMiddleonly 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 ispos.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 exampleExampleSeries_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) andm(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;
posreads 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;
armLequals MetaStock’s Cross formsetup.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 makeh.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 asHIGH < 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) andv(his code’s 10). - First valid index: 10(n - 1) for
av; 10(n - 1) or v - 1, whichever is later, forpobc: 110 for n = 12 and v = 10. Where v - 1 is laterpobcis 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;
SAverageadds 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 givesav4607.6697509765627 andpobc394.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 throughDiv, 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))withraw := 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
hand lowslof 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,spandttf(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 beforeHysteresis.posreads 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 ispos.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)andh.PositionSigned(up, dn, dn, up)withup := ttf.GT(100.0)anddn := 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, andRatioreads 0, “consolidation”, for 2n bars after a missing high or low whereDivkeeps 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<= -100is 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 withposfrom 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 addingSqrt(-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 exampleExampleSeries_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 displacementdin 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 forkijun, d + k - 1 foraNowand d + b - 1 (76 at the defaults) forbNowandstate, NaN before;tkreads 0 until bar k andchikouuntil 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 makesstateNaN on the bars whose cloud holds it, where withoutMaskNonFiniteit 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, whileh.DonchianMiddle(26)is a one-series midpoint and not the standard line (it is Downs’s bull fear level at n = 26, ont6-fear-levels.mdin this category);chikouequalsc.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 areMaximumandMinimum: the mean plus or minus half the difference is not exact, and withaNow79.01,bNow79 and a close of 79.01 its top reads 79.009999999999991, which moves the close from the edge (0) to above the cloud (+1).CrossesAboveSfires 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)andspanB.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 exampleExampleSeries_Shift_ichimoku.