Signals and events
All functions · nseries package
Sections: Crossover Detection · Event Operators · Consecutive · N of M · Up / Down · Bar Classification
Crossover Detection
CrossesAbove
func (s Series) CrossesAbove(val any) bool
CrossesAbove returns a boolean to indicate whether the second-last series value was less than or equal to the provided value/series and the last series value is greater than the provided value/value.
CrossesAboveS
func (s Series) CrossesAboveS(val any) Series
CrossesAboveS returns a series to indicate whether the prior series value was less than or equal to the provided value/series and the current series value is greater than the provided value/series. The series returned will contain 1.0 where the test is true and 0.0 where it is false, and always has the receiver’s length. The value may be any built-in or defined real numeric scalar type, or another Series; any other type, including a plain []float64, gives all zeros, as does a receiver of fewer than two bars. A Series value is right-aligned with the receiver at the most recent bar and compared over the bars they share. A cross also needs its prior bar inside that overlap, so the first shared bar and every position before it are 0.0, and an overlap of fewer than two bars gives all zeros. When both series have at least two bars, the value at the most recent bar agrees with CrossesAbove. Series are paired by position only; see “Time alignment” in the manual.
CrossesBelow
func (s Series) CrossesBelow(val any) bool
CrossesBelow returns a boolean to indicate whether the second-last series value was greater than or equal to the provided value/series and the last series value is less than the provided value/series.
CrossesBelowS
func (s Series) CrossesBelowS(val any) Series
CrossesBelowS returns a series to indicate whether the prior series value was greater than or equal to the provided value/series and the current series value is less than the provided value/series. The series returned will contain 1.0 where the test is true and 0.0 where it is false, and always has the receiver’s length. The value may be any built-in or defined real numeric scalar type, or another Series; any other type, including a plain []float64, gives all zeros, as does a receiver of fewer than two bars. A Series value is right-aligned with the receiver at the most recent bar and compared over the bars they share. A cross also needs its prior bar inside that overlap, so the first shared bar and every position before it are 0.0, and an overlap of fewer than two bars gives all zeros. When both series have at least two bars, the value at the most recent bar agrees with CrossesBelow. Series are paired by position only; see “Time alignment” in the manual.
Hysteresis
func (s Series) Hysteresis(on, off float64) Series
Hysteresis returns a two-threshold latch: +1 from a value above on, -1 from a value below off, holding its state otherwise, and 0, the undetermined state, until the first such value. The thresholds are in the receiver’s units and the higher one comes first: on and off must be finite with on >= off, and any other pair, a reversed (low, high) pair included, returns a series of zeros.
Ties are strict: a value equal to a threshold holds the state, and so does NaN (both comparisons are false); +Inf and -Inf are ordered values and decide. With on == off every bar decides except a NaN or a value equal to the threshold. For thresholds that decide at equality, move both inwards by one ulp:
s.Hysteresis(math.Nextafter(on, math.Inf(-1)), math.Nextafter(off, math.Inf(1)))
is the inclusive latch provided math.Nextafter(on, math.Inf(-1)) >= math.Nextafter(off, math.Inf(1)), in effect when on - off spans at least two ulps. For any valid pair, the ternary form
s.If(func(v float64) bool { return v >= on }, 1.0,
s.If(func(v float64) bool { return v <= off }, -1.0, 0.0)).Hysteresis(0, 0)
is the inclusive latch.
The output is -1, 0 or +1 (0 as +0) and is not NaN. The 0 before the first decision is the undetermined state, not a warm-up: bar 0 can decide, and the first valid index is 0. Mask an upstream warm-up first, as in osc.SetN(F, math.NaN()).Hysteresis(on, off) with F the upstream first valid index, because warm-up zeros latch -1 whenever off > 0 and +1 whenever on < 0.
The latch remembers its last decision, so its value depends on where the history starts (it is not start-invariant), and a change of one ulp in a value near a threshold can change every later bar until the next decision.
As an event latch, for 0/1 flags entry and exit that are not both 1 on any bar, entry.Sub(exit).Hysteresis(0, 0).GT(0) is 1 from each entry until the next exit and 0 elsewhere. ADXTrendMode (its rules 1 to 3) and BreakoutModeIndex (its low-volatility latch) give this form as recipes, and HACO is this latch over Vervoort’s up and down switches. Position and PositionSigned are the trade latches over such flags. On a bar where both flags fire this latch holds its state, while Position enters when flat and exits when long, and s.PositionSigned(up, dn, dn, up) stays flat when flat and reverses when in position. LevelBreakDirection is this latch over the strict breaks of two level series and MarketStructure over the paired swing comparisons; their docs give the Hysteresis forms. PointAndFigureSignal’s doc writes its breakout latch in this form, the kagi recipes of the manual’s “Kagi” category rebuild KagiYang over it, and DarvasBoxBreakout’s doc gives the direction of the last box break as its Hysteresis(0, 0). ElliottTrend is not this latch over its two flip conditions: it seeds only at an n-bar extreme of the oscillator.
Each bar costs O(1) time, and a call allocates only the result.
Event Operators
The methods of this category read a condition series bar by bar, and a value of the condition is an event when it is neither 0 nor NaN: +Inf and -Inf are events, 0, -0 and NaN are not, and the current bar counts (BarsSince reads its own receiver as the event series). The condition is right-aligned with the receiver at the most recent bar, so receiver bars before the overlap see no event. A value latched at an event (ValueWhen, ValueWhenStepUp, BarsSince, SumSince and the others) is NaN before the first event, and the Where methods (AverageWhere, StdDevWhere, QuantileWhere) hold the last event bar’s value on the bars between events and are NaN until their window is full. ValueWhenStepUp is ValueWhen’s hold with a geometric step-up: between events it multiplies the held value by a ratio on each bar on which a trigger series runs strictly above another ratio times it, for Vervoort’s TR&NDS support on stops-and-reversals/t6-trnds-stop.md. FuncWhere applies a function to the receiver’s values on the event bars, taken in order as one series, and holds each result until the next event, so ValueWhen and the Where methods are special cases of it. Series are paired by position only; see “Time alignment”. A level sampled at the first event and held until the receiver leaves it, which ValueWhen’s hold of the latest event does not give, is a composition on PositionSignedRules: Saitta’s first-detection range on true-range/t6-range-contraction.md is a long trade entered on the detection, with the five-bar low as its stop and the five-bar high as its target, both read strictly on the close, and EntryValue reading the pair back.
Essential and flat bars (Yang)
Z. George Yang (“Filtered Market Statistics and Technical Trading Rules”, SSRN 2260609, 2013) calls a day “nearly flat” when its gain or loss is less than a threshold of about 20% of a daily volatility measure, and filters such days out of the price input and the day counting of his rules; the days that remain are his “essential” days. Here a bar is essential when its move is beyond its threshold and flat when it is within it. The recipes write the classification as a signed mask m (+1 above the threshold, -1 below its negative, 0 between, NaN where the move or the threshold is undefined) and its event series e := m.Abs().GT(0) (1 on an essential bar, 0 on a flat or undefined bar), which the methods of this category take as their condition: c.AverageWhere(e, 200) is the mean of the last 200 essential closes, and e.BarsSince(199).Add(1) the number of bars they cover.
These recipes are Yang’s (2013) construction and its relatives, documented as research and strategy-building tools. Yang’s in-sample results replicate (SPX 1990-2012, gross of costs: two-day runs under the 22% VIX filter 9.9% a year against 6.4% unfiltered; Yang printed 10.31% and 6.56%). Out of sample (2 January 2013 to 16 October 2026, SPX and seven ETFs, 1,666 cells, net of 1 bp per unit of turnover) no filter shows evidence of value for any rule: the VIX-filtered runs rule is worse than the unfiltered rule (Sharpe difference on SPX -0.32, 90% interval -0.67 to -0.02), the filtered averages lose in most cells, the long channels cannot be decided (6 to 14 position changes in nearly 14 years at a 200-day look-back), and the share of flat days predicts lower volatility and nothing beyond trailing volatility. The lag-2 autocorrelation of SPX returns that the filter sharpened in sample turned positive after 2012 (+0.076 on all days, +0.070 on the days the VIX filter keeps). Katsanos’s volume flow indicator (Stocks & Commodities, June 2004) used a cut-off of 0.2 times a rolling standard deviation of the typical price’s log changes, scaled by the close, nine years earlier, on the change of the typical price. No trading edge is demonstrated. Source: GLE-539, out-of-sample study of 17 October 2026.
The interval is a percentile bootstrap interval of the paired difference, not adjusted for the 1,490 comparisons; after a Holm adjustment no headline cell is significant in either direction, so the out-of-sample case rests on consistent signs and on intervals that exclude the in-sample effect sizes, not on any single significant test. The study used the SPX price index and the split- and dividend-adjusted closes of the ETFs.
The four pages share these conventions. x is the move, the mask’s receiver in the caller’s units (c.LogRatio(c.Shift(1)) for log moves of closes, NaN on bar 0 by itself), and thr the threshold in the same units, a scalar or a series right-aligned at the most recent bar. The masks are strict on both sides, as Yang’s Table 1 is; the inclusive test of the study and of an algolang strategy that implements Yang’s two-day runs rule has a section of its own. Warm-ups that would read as values are masked with SetN(k, math.NaN()), and every section gives each output’s first valid index. A decision at bar t uses data through t, Yang’s same-close convention, so trade the next bar with Shift(1). Closes, VIX closes, highs, lows and volumes are paired by position, and the join by date is the caller’s.
The recipes are under “Compositions” below, on four pages: e1-essential-bars.md (the masks: Yang’s rolling, fixed and implied-volatility thresholds, the inclusive test, the range and volume masks, Katsanos’s cut-off and the up-down classification), e1-essential-runs.md (the signed run length and Yang’s two-day runs rule), e1-essential-windows.md (filtered moving averages, Yang’s channel, momentum over essential closes and the moments of essential moves) and e1-quiet-gauges.md (the flat share, the event-clock span and soft weighting).
AverageWhere
func (s Series) AverageWhere(cond Series, k int) Series
AverageWhere returns the mean of the receiver’s values on the last k bars where cond fires (k >= 1), or on every such bar so far (k = 0). The alignment of cond, the sample (the current bar counts, and NaN and infinities enter it verbatim), the window and the timing are QuantileWhere’s: NaN until the window is full (k events, or one for k = 0), the statistic on an event bar with a full window, and on every other bar the value of the most recent event bar, held verbatim, so out equals out.ValueWhen(cond) wherever a window has been full.
The statistic is Average’s: a compensated rolling sum of the window divided by its size. A NaN in the window, or infinities of both signs, give NaN; otherwise an infinity gives that infinity. On an all-true mask the result equals s.Average(k) bit for bit from bar k-1 on, and for k = 0 the m-th event bar gives Series(sample[:m]).Average(m)[m-1]. Validation: k < 0 returns make(Series, len(s)), all +0; a k larger than the number of events gives NaN everywhere. Cost: amortised O(1) per bar. The expanding form depends on where the history starts; k >= 1 depends only on the last k events. FuncWhere is the general form: with the callback x.Average(k) and k >= 1 it equals AverageWhere from the k-th event bar on, any NaN matching any NaN.
BarsSince
func (s Series) BarsSince(back ...int) Series
BarsSince returns, for each bar, the number of bars since the most recent event in the receiver, which is itself the event series: bar j is an event bar when s[j] is non-zero and not NaN, so +Inf and -Inf are events while 0, -0 and NaN are not. The current bar counts as an event bar, so with the default back the result is 0 on an event bar, 1 on the next bar, and so on. With back 1 it is the number of bars since the event before the most recent one. The values are exact non-negative integers (as float64) or NaN.
back is an nseries extension: neither TradingView’s ta.barssince nor AmiBroker’s BarsSince takes one. It selects the occurrence and is numbered as ValueWhen’s back: from 0, as TradingView’s ta.valuewhen occurrence, so 0 (the default) is the most recent event and 1 the one before it, one less than AmiBroker ValueWhen’s 1-based n. Only the first value of back is read; any further values are ignored. A back below zero is invalid and returns aligned zeros. Any non-negative back is valid, and one at or beyond the length of the receiver gives all NaN.
The result is NaN until the first event (until the (k+1)-th event when back is k), and NaN rather than 0 because the start depends on the data. The age is latched at events, so under a sliding slice of history the output depends on the first event inside the slice: bars before it read NaN, even where an earlier event lies outside the slice. BarsSinceEntry and BarsSinceExit count from a position’s entries and exits, and SwingHighAge and SwingLowAge from a confirmed swing’s extreme bar.
Sources: TradingView Pine Script v6 ta.barssince; AmiBroker AFL BarsSince; and, for the confirmed pivots the latch is often applied to, the rolling-window rule of Tsinaslanidis and Zapranis, Technical Analysis for Algorithmic Pattern Recognition (Springer, 2016), ch. 2 (their pattern rules on the most recent pivots are in chs 5 and 6).
ChangeSince
func (s Series) ChangeSince(cond Series) Series
ChangeSince returns, for each bar, the change of the receiver since the most recent bar on which cond is an event: the receiver’s value minus its value at that event bar. It equals s.Sub(s.ValueWhen(cond)) bit for bit, apart from the payload of a NaN, in one pass and one allocation. A value of cond is an event when it is non-zero and not NaN, so +Inf and -Inf are events while 0, -0 and NaN are not. The current bar counts as an event bar, so the result is 0 on an event bar where the receiver is finite. ChangeSince has no back argument; ValueWhen takes one. ConsecUpGain and ConsecDownLoss are this change since the last bar that was not an up bar or a down bar, with their own rule at an infinity.
The result is NaN wherever either operand is NaN, and an infinity minus the same infinity is NaN. A NaN held from an event bar therefore gives NaN until the next event.
The result is NaN until the first event, and NaN rather than 0 because the start depends on the data. The reference value is latched, so under a sliding slice of history the output depends on the first event inside the slice: bars before it read NaN, even where an earlier event lies outside the slice.
ChangeSince is an nseries composition of ValueWhen, and its sources are ValueWhen’s: TradingView Pine Script v6 ta.valuewhen; AmiBroker AFL ValueWhen; and, for the confirmed pivots the latch is often applied to, the rolling-window rule of Tsinaslanidis and Zapranis, Technical Analysis for Algorithmic Pattern Recognition (Springer, 2016), ch. 2 (their pattern rules on the most recent pivots are in chs 5 and 6).
cond is right-aligned with the receiver at the most recent bar: the last values are paired, then the second-last, and so on. A cond longer than the receiver contributes only its last len(receiver) values, and a shorter cond gives the same result as cond with zeros prepended. Receiver bars before the overlap (and every bar, for an empty cond) see no event and so read NaN until the first paired event. Series are paired by position only; see “Time alignment” in the manual.
FuncWhere
func (s Series) FuncWhere(cond Series, fn func(Series) Series) Series
FuncWhere applies fn once to the receiver’s values on the bars where cond fires, taken in order as one series, and spreads the result back over the receiver’s bars, each event’s value held verbatim until the next event.
An event is a non-zero, non-NaN value of cond: +Inf and -Inf are events while 0, -0 and NaN are not. Negative, subnormal and non-unit values also fire. The current bar counts, so an event is visible on its own bar. Let E be the number of event bars after alignment.
cond is right-aligned with the receiver at the most recent bar: the last values are paired, then the second-last, and so on. A cond longer than the receiver contributes only its last len(receiver) values, and a shorter cond gives the same result as cond with zeros prepended. Receiver bars before the overlap (and every bar, for an empty cond) see no event and so read NaN until the first paired event. Series are paired by position only; see “Time alignment” in the manual.
A nil fn, including a typed nil, is checked first and returns make(Series, len(s)), all +0, whether or not cond has events. A nil or empty receiver then returns a non-nil empty Series, and fn is not called. With no event bars, every result bar is math.NaN() and fn is not called. Otherwise fn is called once; a returned Series whose length is not E gives make(Series, len(s)), all +0. Nil, empty, shorter and longer returns are all invalid. A panic in fn propagates.
With E >= 1 and a non-nil fn, fn is called exactly once with a fresh Series of length E containing the event values in order, copied verbatim, including NaN payloads, +Inf, -Inf and -0. The argument shares no storage with s, cond or the result, even when every bar fires. fn may modify, keep or return it. After the call, FuncWhere reads fn’s returned Series for output values and does not otherwise access the argument or modify either series. The event bars are fixed before fn is called, and s and cond are not read after it returns, so fn may also write s and cond, as in s.FuncWhere(s, fn), without moving its values to other bars.
fn must return a Series of length E. Its j-th value appears on the j-th event bar and is held verbatim until the next event, or the end of the receiver after the last event. This includes fn’s own warm-up values, which FuncWhere does not mask. A NaN keeps its payload, -0 keeps its sign, and an infinity stays an infinity. The result is a fresh Series of len(s) bars sharing no storage with s, cond, fn’s argument or fn’s returned Series. Mutating the result affects none of them, and later changes to any of them do not affect the result. Separate calls return separate storage.
The result is NaN until the first event, and NaN rather than 0 because the start depends on the data.
The value is latched, so under a sliding slice of history the output depends on the first event inside the slice: bars before it read NaN, even where an earlier event lies outside the slice.
With func(x Series) Series { return x }, the result equals s.ValueWhen(cond) bit for bit on every bar for every receiver and cond, NaN payloads included. With x.Shift(k), k >= 0, it equals s.ValueWhen(cond, k) from the (k+1)-th event bar on, with NaN matching NaN. Before then, from the first event, it holds Shift’s +0 padding where ValueWhen reads NaN. At k = 0 the identity holds on every bar; if k >= E there is no (k+1)-th event bar. Here, NaN matching NaN means bit equality except that any NaN matches any NaN; +0 and -0 remain different.
With x.Average(k) and 1 <= k <= E, the result equals s.AverageWhere(cond, k) from the k-th event bar on, with NaN matching NaN. With x.Quantile(k, q), 1 <= k <= E and q in [0, 1], it likewise equals s.QuantileWhere(cond, k, q); both write a zero as +0. With x.StdDev(k) and 2 <= k <= E, the same agreement holds with s.StdDevWhere(cond, k) from the k-th event bar on. StdDev(1) is all zeros for any input, while StdDevWhere(cond, 1) is NaN for a non-finite event value; from the first event, they agree at k = 1 only where the most recent event value is finite.
Before the k-th event bar, from the first event, it holds Average’s pass-through or Quantile’s and StdDev’s zeros, while the Where methods read NaN. There is no identity at k = 0: Average(0) copies its input, whereas AverageWhere(cond, 0) is the expanding mean. For k > E there is no k-th event bar.
When cond is at least as long as s and every value in its overlap fires, the result equals fn(s) bit for bit for a non-nil fn returning the right length. With a shorter all-firing cond of length m, the first len(s)-m bars read NaN and the rest equal fn(s[len(s)-m:]) bit for bit. This can differ from the last m values of fn(s): XAverage’s seed and CumSum depend on where their input starts.
For a signed mask m containing +1, -1, 0, -0 or NaN, let e := m.Abs().GT(0). Then m.FuncWhere(e, func(x Series) Series { return x.StateAge().Mul(x) }) equals the SumSince form of the essential runs recipe bit for bit on every bar: d := m.ValueWhen(e), start := d.Delta().Abs().GT(0).Add(e.CumSum().EQ(1).Mul(e)), and run := e.SumSince(start).Mul(d). Both read NaN before the first essential bar and exact signed run lengths after it, because StateAge counts the consecutive equal signs in the gathered series.
For closes c, a 0/1 event series e of the same length and L >= 1, c.FuncWhere(e, func(x Series) Series { return x.Highest(L).Shift(1).SetN(L, math.NaN()) }) gives the highest of the L event closes before each event bar, held between events. SetN masks the L places that Shift’s +0 padding and Highest’s running maxima would fill. For non-zero closes, NaN closes included, it equals c.QuantileWhere(e, L, 1).Shift(1).SetN(1, math.NaN()).ValueWhen(e) on every bar, with NaN matching NaN, except while an infinite close on an event bar is in the window: QuantileWhere reads NaN and Highest does not. So c = [1, 2, +Inf, 3, 4, 5, 6] with e all 1 and L = 2 gives [NaN, NaN, 2, +Inf, +Inf, 4, 5] here and [NaN, NaN, 2, NaN, NaN, 4, 5] for the QuantileWhere form. An essential-bar mask of a move excludes such events, but a range or volume mask can admit them. QuantileWhere writes a zero bound as +0 where Highest keeps -0.
The corresponding Lowest form gives the lower bound. From the (L+1)-th event bar on, these bounds agree with the recipe’s unheld bounds on event bars. Under the same conditions, the flags up := c.GT(hi).Mul(e) and dn := c.LT(lo).Mul(e), and the latch c.PositionSigned(up, dn, dn, up), agree for either choice of hi and lo.
The result is causal, prefix-invariant and start-invariant exactly when fn is, since fn sees the whole event history at once. With a causal fn, changes after bar t in s or cond, with lengths kept, leave bars 0 to t unchanged bit for bit, and when len(cond) == len(s), s[:m].FuncWhere(cond[:m], fn) equals the first m bars of the full result. Changing cond’s length can move the alignment.
Causal callbacks include the identity, Shift with a non-negative lag, Average, StdDev, Quantile, the channel and runs callbacks, Skewness, Kurtosis, Correlation with x.Shift(1), XAverage and CumSum. A centred or two-sided fn, Shift with a negative lag, or a callback whose values depend on len(x) leaks the future into held bars. XAverage and CumSum depend on where their input starts, and StateAge’s first spell is left-censored, so with these callbacks the result depends on the first event inside a slice of history.
FuncWhere makes three linear passes, taking O(len(s)) time plus the cost of fn on E values. It makes two allocations of its own plus any made by fn; a nil fn or no event needs only one. There is no arithmetic on values, so the result has the same bits on every architecture when fn does. It keeps no state between calls and is safe for concurrent use when fn is.
For a 0/1 event series e of the receiver’s length and a result out, out.If(func(_ float64, i int) bool { return e[i] != 0 }, out, math.NaN()) keeps event values verbatim and writes NaN elsewhere. e must be a 0/1 series because e[i] != 0 treats a NaN in e as an event. Multiplying with out.Mul(e) does not implement this, since a finite held value becomes 0 between events.
Directional change uses intrinsic time, in which “time is defined by events”: Chen and Tsang, Detecting Regime Change in Computational Finance (CRC Press, 2021), Appendix A.
Activity clocks sample by trading activity rather than by the clock: Lopez de Prado, Advances in Financial Machine Learning (Wiley, 2018), ch. 2, pp. 26-27, citing Mandelbrot and Taylor (1967), Clark (1973), Ane and Geman (2000), and Easley, Lopez de Prado and O’Hara (2011, 2012). These activity-clock sources are cited through Lopez de Prado.
Ehlers’s undersampling in “Just Ignore Them”, Technical Analysis of Stocks & Commodities, April 2023, holds the close of every fifth bar: Sample = Sample[1]; If CurrentBar / 5 = IntPortion(CurrentBar / 5) Then Sample = Close. For a flag k on every fifth bar this is c.ValueWhen(k), or FuncWhere with the identity callback. His Hann filters run on the held series in calendar time. FuncWhere reproduces his sampling; placing a filter inside fn instead runs that filter on the event clock and does not reproduce his indicator.
Clare, Seaton, Smith and Thomas, “Breaking into the blackbox: Trend following, stop losses and the frequency of trading: the case of the S&P500”, Journal of Asset Management 14(3), 182-194 (2013), working paper SSRN 2126476 (2012), find end-of-month decision rules superior to rules that trade more often for trend-following rules on the S&P 500, as stated in their abstract.
Yang, “Filtered Market Statistics and Technical Trading Rules”, SSRN 2260609 (2013), counts only days whose move clears a threshold, his “essential” days.
Callers build calendar masks from the Comparison category’s “Calendar keys”: weekday.Equal(5) for Fridays and tdr.Equal(0) for each month’s last session, the monthly decision bars of Clare et al. The flag on every fifth bar for Ehlers’s sampling is also the caller’s, since the package has no bar-index generator.
Directional-change confirmations can use c.DirectionalChangePointsScaled(h).Delta().Clamp(-1, 1).Abs(), which is 1 on each bar where the state changes and 0 on every other bar, bar 0 included, since Delta reads 0 there. The essential-bar mask e := m.Abs().GT(0) is used by the recipes under “Event Operators”. Those pages also use FuncWhere for the skewness, kurtosis and serial correlation of the last n essential moves and an exponential average on the event clock.
Verification: for every receiver and every cond, NaN, +Inf, -Inf and -0 included, the identity callback equals s.ValueWhen(cond) bit for bit on every bar, NaN payloads included. With any fn returning length E, the j-th event bar holds fn’s j-th value until the next event bar.
HighestSince
func (s Series) HighestSince(cond Series) Series
HighestSince returns, for each bar, the highest value of the receiver from the most recent bar on which cond is an event up to the current bar. A value of cond is an event when it is non-zero and not NaN, so +Inf and -Inf are events while 0, -0 and NaN are not. The current bar counts as an event bar, the event bar is included in the span (on an event bar the result is the receiver’s own value, verbatim), and each event restarts the span.
The result is defined bit for bit by a recursion: on an event bar it is the receiver’s value; on a later bar of the span it is max(previous result, receiver’s value) with Go’s builtin max; before the first event it is NaN. So a NaN in the span gives NaN from that bar until the next event restarts the span, as Highest gives NaN while a NaN is in its window; max prefers +0 over -0, the same ordering as Highest; and infinities are ordinary values.
The result is NaN until the first event, and NaN rather than 0 because the start depends on the data. The span is latched at events, so under a sliding slice of history the output depends on the first event inside the slice: bars before it read NaN, even where an earlier event lies outside the slice. HighestSinceEntry is this recursion over the trades of a position, NaN while the position is flat.
Sources: AmiBroker AFL HighestSince; and, for the confirmed pivots the latch is often applied to, the rolling-window rule of Tsinaslanidis and Zapranis, Technical Analysis for Algorithmic Pattern Recognition (Springer, 2016), ch. 2 (their pattern rules on the most recent pivots are in chs 5 and 6).
cond is right-aligned with the receiver at the most recent bar: the last values are paired, then the second-last, and so on. A cond longer than the receiver contributes only its last len(receiver) values, and a shorter cond gives the same result as cond with zeros prepended. Receiver bars before the overlap (and every bar, for an empty cond) see no event and so read NaN until the first paired event. Series are paired by position only; see “Time alignment” in the manual.
LowestSince
func (s Series) LowestSince(cond Series) Series
LowestSince returns, for each bar, the lowest value of the receiver from the most recent bar on which cond is an event up to the current bar. A value of cond is an event when it is non-zero and not NaN, so +Inf and -Inf are events while 0, -0 and NaN are not. The current bar counts as an event bar, the event bar is included in the span (on an event bar the result is the receiver’s own value, verbatim), and each event restarts the span.
The result is defined bit for bit by a recursion: on an event bar it is the receiver’s value; on a later bar of the span it is min(previous result, receiver’s value) with Go’s builtin min; before the first event it is NaN. So a NaN in the span gives NaN from that bar until the next event restarts the span, as Lowest gives NaN while a NaN is in its window; min prefers -0 over +0, the same ordering as Lowest; and infinities are ordinary values.
The result is NaN until the first event, and NaN rather than 0 because the start depends on the data. The span is latched at events, so under a sliding slice of history the output depends on the first event inside the slice: bars before it read NaN, even where an earlier event lies outside the slice. LowestSinceEntry is this recursion over the trades of a position, NaN while the position is flat.
Sources: AmiBroker AFL LowestSince; and, for the confirmed pivots the latch is often applied to, the rolling-window rule of Tsinaslanidis and Zapranis, Technical Analysis for Algorithmic Pattern Recognition (Springer, 2016), ch. 2 (their pattern rules on the most recent pivots are in chs 5 and 6).
cond is right-aligned with the receiver at the most recent bar: the last values are paired, then the second-last, and so on. A cond longer than the receiver contributes only its last len(receiver) values, and a shorter cond gives the same result as cond with zeros prepended. Receiver bars before the overlap (and every bar, for an empty cond) see no event and so read NaN until the first paired event. Series are paired by position only; see “Time alignment” in the manual.
QuantileWhere
func (s Series) QuantileWhere(cond Series, k int, q float64) Series
QuantileWhere returns the q-quantile of the receiver’s values on the last k bars where cond fires (k >= 1), or on every such bar so far (k = 0). cond is right-aligned with the receiver, receiver bars before its overlap see no event, and a value fires when it is neither 0 nor NaN. On each bar where cond fires (the current bar counts) s[t] joins the sample verbatim: a NaN or an infinity enters it and the event counts. The window is the last k sample values, or the whole sample for k = 0.
The statistic is Quantile’s, type 7: linear interpolation between the order statistics of the window sorted ascending, with Quantile’s snapping of the rank, computed exactly as s.Quantile(m, q) computes it for a window of m values, m = k or, for k = 0, the number of events so far. A window holding a NaN or an infinity gives NaN, and a zero result is +0. On an all-true mask (cond of ones, of the receiver’s length) the result equals s.Quantile(k, q) bit for bit from bar k-1 on, and for k = 0 the m-th event bar gives Series(sample[:m]).Quantile(m, q)[m-1].
Timing and warm-up: the result is NaN until the window is full (k events for k >= 1, one event for k = 0). On an event bar with a full window it is the statistic of the window; on every other bar it holds the value of the most recent event bar verbatim, NaN included, so out equals out.ValueWhen(cond) wherever a window has been full. A k larger than the number of events gives NaN everywhere. Validation: k < 0, or q NaN, infinite or outside the range [0, 1], returns make(Series, len(s)), all +0. Cost: as Quantile, O(log k + k) per event; for k = 0, O(E) per event, E the number of events.
Balsara’s probability stop reads a percentile of the winners’ MAE (see TradeAdverseExcursion). His Table 6.1 lists ten winners with MAE 100 four times, 200 three times, 500 twice and 1500 once. With mask firing on their exit bars:
mae.QuantileWhere(mask, 10, 0.9) // type 7: about 600 (599.9999999999997 in binary64)
mae.QuantileWhere(mask, 10, 8.0/9) // exactly 500, Balsara's reading
The second is Balsara’s own reading, the order statistic ceil(kq) - 1 of the window sorted ascending and counted from 0 (type 1): for k = 10 and q = 0.9 that is index 8, the value 500, which q = 8.0/9 selects. Any order statistic j of a full window of k >= 2 values is exact with
QuantileWhere(cond, k, float64(j)/float64(k-1))
since the rank snaps to j; type 1 at q in (0, 1] takes j = ceil(kq) - 1 (and j = 0 at q = 0). The expanding form has no such fixed q, since its sample size moves.
The number of events so far, for cond of the receiver’s length, is cond.Abs().GT(0).CumSum(), and the window is full from the bar where that count reaches k (1 for k = 0); the events in the last n bars are cond.Abs().GT(0).Sum(n). The expanding form (k = 0) depends on where the history starts, as CumSum does; k >= 1 depends only on the last k events, so a slice of history agrees with the full series from its k-th event. FuncWhere is the general form: with the callback x.Quantile(k, q) and k >= 1 it equals QuantileWhere from the k-th event bar on, any NaN matching any NaN.
StdDevWhere
func (s Series) StdDevWhere(cond Series, k int) Series
StdDevWhere returns the population standard deviation of the receiver’s values on the last k bars where cond fires (k >= 1), or on every such bar so far (k = 0). The alignment of cond, the sample (the current bar counts), the window and the timing are QuantileWhere’s: NaN until the window is full (k events, or one for k = 0), the statistic on an event bar with a full window, and on every other bar the value of the most recent event bar, held verbatim, so out equals out.ValueWhen(cond) wherever a window has been full.
The statistic is StdDev’s: the square root of the population variance of the window (divisor its size), with StdDev’s precision. Any NaN or infinity in the window gives NaN, and a window of one finite value gives +0, so k = 1 is valid. On an all-true mask the result equals s.StdDev(k) bit for bit from bar k-1 on for k >= 2, and for k = 0 the m-th event bar, m >= 2, gives Series(sample[:m]).StdDev(m)[m-1]. Validation: k < 0 returns make(Series, len(s)), all +0; a k larger than the number of events gives NaN everywhere. Cost: amortised O(1) per bar. The expanding form depends on where the history starts; k >= 1 depends only on the last k events. FuncWhere is the general form: with the callback x.StdDev(k) and k >= 2 it equals StdDevWhere from the k-th event bar on, any NaN matching any NaN.
Ruggiero’s adaptive stop sets the stop distance from the adverse moves of past winning trades. As a bar-level recipe, with o, l and c the open, low and close and sig 1 on the bars traded long from the open to the close:
adv := o.Sub(l)
win := c.GT(o).Mul(sig)
m := adv.AverageWhere(win, 0)
sd := adv.StdDevWhere(win, 0)
D := m.Add(sd).Shift(1)
g := win.CumSum().Shift(1).GT(30)
D = D.If(math.IsNaN, 0.0, D).Mul(g).Add(g.Mul(-1).Add(1).Mul(2.0))
hit := l.LTE(o.Sub(D)).Mul(sig)
This is the textbook form: the population mean plus one standard deviation of the adverse move over the winners strictly before the bar, so D is known at the bar’s open; it is active from the 31st winner, with a 2.00-point stop before that (the NaN-to-0 step is needed because 0 times NaN is NaN), and the stop is a resting order, touched when the low reaches it. His trades last one bar, which Position cannot express. His printed code departs from the textbook form in four ways: AveWin/WinNo+1 adds one point to the mean rather than dividing by WinNo+1; both loops run to WinNo, reading an unset element as an extra 0; neither sum is reset between bars, so both carry over; and the stop activates from the 31st winner while the text says 30. His comment that one standard deviation stops out 5% of trades is also wrong for a normal distribution (about 16%; 5% needs about 1.645 deviations). Source: Ruggiero, Cybernetic Trading Strategies (Wiley, 1997), ch. 9, Table 9.6.
SumSince
func (s Series) SumSince(cond Series) Series
SumSince returns, for each bar, the sum of the receiver from the most recent bar on which cond is an event up to the current bar. A value of cond is an event when it is non-zero and not NaN, so +Inf and -Inf are events while 0, -0 and NaN are not. The current bar counts as an event bar, the event bar is included in the span (on an event bar the result is the one-term sum of the receiver’s value, so -0 gives +0 as Sum(1) does), and each event restarts the sum.
The summation is compensated and uses the accumulator behind Sum, so at bar i, with e the latest event bar and L = i-e+1, the result equals Series(s[e:i+1]).Sum(L)[L-1], Sum over the span alone, bit for bit. A NaN in the span, or both +Inf and -Inf, gives NaN; otherwise any +Inf gives +Inf and any -Inf gives -Inf, until the next event restarts the sum.
The event bar is included, as in AmiBroker’s SumSince with incFirst = True. AmiBroker’s default is incFirst = False, which leaves the event bar out and gives 0 on it, so this method differs from AmiBroker’s default.
The result is NaN until the first event, and NaN rather than 0 because the start depends on the data. The sum is latched at events, so under a sliding slice of history the output depends on the first event inside the slice: bars before it read NaN, even where an earlier event lies outside the slice.
Sources: AmiBroker AFL SumSince; TradingView’s anchored ta.vwap for the inclusive event bar; and, for the confirmed pivots the latch is often applied to, the rolling-window rule of Tsinaslanidis and Zapranis, Technical Analysis for Algorithmic Pattern Recognition (Springer, 2016), ch. 2 (their pattern rules on the most recent pivots are in chs 5 and 6).
cond is right-aligned with the receiver at the most recent bar: the last values are paired, then the second-last, and so on. A cond longer than the receiver contributes only its last len(receiver) values, and a shorter cond gives the same result as cond with zeros prepended. Receiver bars before the overlap (and every bar, for an empty cond) see no event and so read NaN until the first paired event. Series are paired by position only; see “Time alignment” in the manual.
ValueWhen
func (s Series) ValueWhen(cond Series, back ...int) Series
ValueWhen returns the value of the receiver at the most recent bar on which cond is an event, held until the next event; with back it returns the value at an earlier event instead. A value of cond is an event when it is non-zero and not NaN, so +Inf and -Inf are events while 0, -0 and NaN are not. The current bar counts as an event bar: on an event bar the most recent event is that bar itself, so the result there is the receiver’s own value.
The value is sampled verbatim, including NaN, +Inf, -Inf and -0. A NaN in the receiver on an event bar is held like any other value and still counts as an occurrence, as measured on TradingView by the PyneCore runtime; values on non-event bars do not matter.
back selects the occurrence and is numbered from 0, as TradingView’s occurrence argument: 0 (the default) is the most recent event, 1 the one before it. AmiBroker’s n is 1-based, so the nseries back equals AmiBroker’s n minus 1. Only the first value of back is read; any further values are ignored. A back below zero is invalid and returns aligned zeros. Any non-negative back is valid, and one at or beyond the length of the receiver gives all NaN.
The result is NaN until the first event (until the (k+1)-th event when back is k), and NaN rather than 0 because the start depends on the data. The value is latched, so under a sliding slice of history the output depends on the first event inside the slice: bars before it read NaN, even where an earlier event lies outside the slice. EntryValue samples a series at a position’s entries, SwingHighValue and SwingLowValue at confirmed swings, and QuantileWhere, AverageWhere and StdDevWhere are statistics over the last k sampled values. DeMark’s TD risk levels are latched in the same way: with cd := c.DeMarkCountdown(h, l), DeMarkCountdownRiskLevel equals its own ValueWhen(cd.Abs().EQ(13)) on every bar. ValueWhenStepUp holds the same sampled value and steps it up geometrically while a trigger runs above a multiple of it, for Vervoort’s TR&NDS support. FuncWhere applies any function to the sampled values, taken in order as one series, and holds each result until the next event; with the identity callback it equals ValueWhen(cond) on every bar.
Sources: TradingView Pine Script v6 ta.valuewhen; AmiBroker AFL ValueWhen; and, for the confirmed pivots the latch is often applied to, the rolling-window rule of Tsinaslanidis and Zapranis, Technical Analysis for Algorithmic Pattern Recognition (Springer, 2016), ch. 2 (their pattern rules on the most recent pivots are in chs 5 and 6).
cond is right-aligned with the receiver at the most recent bar: the last values are paired, then the second-last, and so on. A cond longer than the receiver contributes only its last len(receiver) values, and a shorter cond gives the same result as cond with zeros prepended. Receiver bars before the overlap (and every bar, for an empty cond) see no event and so read NaN until the first paired event. Series are paired by position only; see “Time alignment” in the manual.
ValueWhenStepUp
func (s Series) ValueWhenStepUp(cond, trigger Series, above, step float64) Series
ValueWhenStepUp holds the receiver’s value from the most recent event of cond, as ValueWhen does, and multiplies the held value by step on each bar without an event on which trigger exceeds the held value times above.
An event is a value of cond that is non-zero and not NaN: +Inf and -Inf are events while 0, -0 and NaN are not. The current bar counts, so on an event bar the result is the receiver’s value there, sampled verbatim (NaN with its payload, +Inf, -Inf and -0 included), and the step is not tested there, whatever trigger is. On a bar without an event the result is float64(step * prev) when trigger has a value there that is strictly above float64(above * prev), and prev otherwise, where prev is the result of the bar before, after any step there. So a trigger that stays above the threshold steps on consecutive bars, each step tested against the stepped value (compounding). The receiver’s values on bars without an event are not read. A step below 1 and an above below 1 are both valid: on a positive held value the step moves the value down and the above tests a trigger below the held value, and on a negative one each acts the other way (a held -8 with step 0.5 steps to -4). The parameters above and step are ratios; trigger is in the receiver’s units.
The threshold float64(above * prev) and the stepped value float64(step * prev) are each one binary64 product, rounded once, to nearest with ties to even, and nothing else is computed (every other value is a copy or a comparison, and there is no sum to fuse a product into), so the result is the same on every architecture.
Zeros and infinities follow those products with no special case: a held +Inf does not step, since no trigger is above its threshold of +Inf; a held -Inf, +0 or -0 keeps its value when it steps; and with above +Inf a held zero gives a NaN threshold and does not step. A threshold that overflows to +Inf blocks the step like any other +Inf, a stepped value that overflows holds +Inf or -Inf, and one that underflows holds a zero of its own sign.
The result is NaN until the first event, and NaN rather than 0 because the start depends on the data. A NaN sampled on an event bar is held until the next event, since its threshold and its stepped value are NaN and no comparison with NaN is true; the next event samples afresh. A trigger that is NaN or unpaired on a bar does not step there, and a nil or empty trigger does not step on any bar.
The parameter above is valid when it is above 0, positive subnormals and +Inf included, and step is valid when it is above 0 and finite. Other values (NaN, +0, -0, negative values and -Inf for either, and +Inf for step) give aligned zeros (+0 on every bar, the receiver’s length), as ValueWhen’s negative back does. An empty or nil receiver gives an empty result that is not nil.
With step 1 the result is s.ValueWhen(cond) bit for bit for every valid above, since 1 times a value is that value, and with a nil or empty trigger, or one that is NaN or -Inf on every bar, it is s.ValueWhen(cond) likewise for every valid above and step. With above +Inf the result is s.ValueWhen(cond) wherever the held value is not negative, since the threshold is then +Inf or NaN and no trigger is above either. For a held finite negative value +Inf times it is -Inf, so every paired trigger above -Inf steps it, +Inf included; a held -Inf keeps its value, so there the identity holds as well. The negation mirror is a resistance: x.Mul(-1).ValueWhenStepUp(cond, trigger.Mul(-1), above, step).Mul(-1) is x on an event bar, otherwise step times the held value when trigger is strictly below above times it, otherwise the held value. The mirror is exact for every valid above and step, since negation is exact and rounding to nearest is symmetric, as FlipStop’s doc gives its negated form for its tie. So a resistance on candidates x that steps down 5% when the high h falls more than 10% below it is x.Mul(-1).ValueWhenStepUp(cond, h.Mul(-1), 0.9, 0.95).Mul(-1).
The worked use is Vervoort’s TR&NDS support. With the lows l and the pivot piv, gap window gap and candidate cand of the TR&NDS recipe, under “Compositions” in the “Stops and Reversals” category (piv marks a pivot low two bars back, gap a low above 1.0013 times the previous high, and cand is the pivot low on a pivot, else 0.9945 times the previous high on a gap window, else a +0 that no event samples), cand.ValueWhenStepUp(piv.Maximum(gap), l, 1.1, 1.05) is his support: the pivot low, else the gap stop, else 1.05 times the support when the low runs more than 10% above it, else the support, the last two his listing’s If(L>PREV1.1,PREV1.05,PREV). It equals his listing from its first event on; before it his PREV is 0 where this result is NaN (or, after a gap on bars 1 to 3, which his code does not read, the value held from that gap).
The held value is a level at bar granularity, as the trade-state latches are a signal state at bar granularity: it is not a fill model. Fills, slippage, the order of touches inside a bar, resting orders across bars and overlapping trades are algolang’s (GLE-524, rows 29 and 30 of the T6 checkpoint’s table).
The value is latched, so under a sliding slice of history the output depends on the first event inside the slice: bars before it read NaN, even where an earlier event lies outside the slice, and from that event on the slice gives the full series’ values, since each event restarts the held value.
Each bar is O(1), and the method allocates only its result.
Source: Vervoort, “Trailing Resistance And Support Stops”, Technical Analysis of Stocks & Commodities, July 2009, the support of the final SVE_TRENDS_Trail listing.
Both cond and trigger are right-aligned with the receiver at the most recent bar on their own, as ValueWhen’s cond is: a longer series contributes its last len(receiver) values, and on the receiver bars before a shorter series begins, cond has no event and trigger does not step. No length is an error. Series are paired by position only; see “Time alignment” in the manual.
Compositions
Z. George Yang (“Filtered Market Statistics and Technical Trading Rules”, SSRN 2260609, 2013; the page numbers are those of the PDF, which prints none) calls a day “nearly flat” when its gain or loss is within a threshold, and the other days, the ones his filtered rules count, “essential”. The masks below build that classification over the shipped methods. c is the closes, x the move, the mask’s receiver in the caller’s units (x := c.LogRatio(c.Shift(1)) for log moves of closes, NaN on bar 0 by itself), and thr the threshold, a scalar or a series in the same units, right-aligned at the most recent bar. The signed mask m is +1 where the move is above thr, -1 where it is below -thr and +0 between, strict on both sides (Yang’s Table 1 puts a move equal to the threshold in the filtered set), and NaN where x or thr is not finite; the event series e := m.Abs().GT(0) is 1 on an essential bar and 0 on a flat or undefined bar, so every event operator skips an undefined bar. Warm-ups that would read as values are masked with SetN(k, math.NaN()), and each section gives every output’s first valid index and what it reads before it. A decision at bar t uses data through t, as Yang’s same-close rules do; trade the next bar with Shift(1). Closes, VIX closes, highs, lows and volumes are paired by position, and the join by date is the caller’s (see “Time alignment”). The study figures quoted below are evidence, not reference data; the category’s introduction gives the rest of them.
Yang’s rolling threshold
Yang’s first dynamic threshold is “20% of SPX daily return standard deviation in the 60-day look-back rolling window, such that the threshold calculation is updated daily” (p. 12). He does not say whether the window includes the current day or which divisor it uses; the recipe takes the study’s reading, the sample standard deviation of the n moves that end on the bar before, so the threshold is known before the bar’s move. e1EssentialRolling returns the move, the threshold, the signed mask and the event series.
x := c.LogRatio(c.Shift(1))
thr := x.StdDevSample(n).Shift(1).SetN(n+1, math.NaN()).Mul(f)
m := x.GT(thr).Sub(x.LT(thr.Mul(-1))).MaskNonFinite(x, thr)
e := m.Abs().GT(0)- Inputs: closes
c, positive where defined (a zero, negative, NaN or infinite close makes the moves into and out of it NaN); the windown>= 2 (Yang’s 60) and the fractionf(Yang’s 0.20). - First valid index: 1 for
x(NaN on bar 0); n + 1 forthrandm(NaN before);ereads 0 before n + 1. - Anchors:
thron bar t is f times the sample deviation ofx[t-n]tox[t-1], the n moves that end on bar t - 1, with the study’s divisor n - 1; the populationx.StdDev(n)differs by the factor sqrt(n/(n-1)), and on the study’s SPX data it moves the mask on 13 bars.eequalsx.Abs().GT(thr)bit for bit on every bar, becauseLogRatiois finite wherever it is not NaN. - Traps: unmasked,
StdDevSample’s warm-up reads 0, sox.StdDevSample(n).Shift(1).Mul(f)makes every non-zero move on bars 1 to n - 1 essential; bar n is NaN by itself, because the window that ends on bar n - 1 holds bar 0’s NaN move. The study and an algolang strategy that implements Yang’s two-day runs rule take the logarithm of the quotient,c.Div(c.Shift(1)).Log(), which differs fromLogRatio’s difference of logarithms in the last bits (and is infinite, not NaN, at a zero or infinite close); no mask moved on SPX or SPY. On the study’s data no return tied its threshold on any series the filter was run on. A threshold must be computable without the mask: an anchored threshold, the move since the last essential close, feeds the mask back into its receiver, so it is not a composition here; the study’s anchored filter behaved like the rolling one out of sample (about 20% of days flat; against the unfiltered rules, the runs rule -0.19 and the 25/200 averages, long or flat, -0.12 in net Sharpe ratio on SPX). - Evidence: out of sample on SPX (2013 to 2026, net of costs) the two-day runs rule under this threshold had a Sharpe ratio of -0.04 against 0.14 unfiltered. Evidence, not a reference; the introduction has the rest.
- Pinned by
TestRecipeE1EssentialRolling; runnable exampleExampleSeries_StdDevSample_essentialBars.
A fixed threshold
Yang’s first filter is a constant: “It can be around 20% of sample daily return’s standard deviation of the full set” (p. 7), and for SPX over 1990-2012 he takes 0.25%, “21% of the full set’s standard deviation” (p. 8). e1EssentialFixed compares the move with a scalar d in its units.
m := x.GT(d).Sub(x.LT(-d)).MaskNonFinite(x)
e := m.Abs().GT(0)- Inputs: moves
x(for log moves of closes,c.LogRatio(c.Shift(1))) and a scalardin the same units (Yang’s 0.0025 for SPX). - First valid index: the first bar where
xis finite (1 for moves of closes);mis NaN before ande0. - Anchors: for a simple-return receiver
r := c.Div(c.Shift(1)).Sub(1), |ln(1 + r)| > d is r > e^d - 1 or r < e^-d - 1, which is asymmetric (d = 0.0025 gives about 0.0025031 and -0.0024969), so the test isr.GT(math.Expm1(d)).Sub(r.LT(math.Expm1(-d))); it is equivalent to rounding only, not bit for bit. - Traps: Yang’s 0.25% is the full 1990-2012 standard deviation times a fraction near the in-sample optima of his Figure 2(b) (20% for 1990-1999 and 22% for 2000-2012, chosen to maximise the runs rule’s annualised returns), so it looks ahead; out of sample it is legitimate only as a constant fixed in 2012, for SPX. The threshold is not masked: a NaN or +Inf
dmakes every finite move flat, and a zerodmakes every non-zero finite move essential. - Evidence: out of sample on SPX (net of costs) the two-day runs rule under the fixed 0.25% had a Sharpe ratio of -0.07 against 0.14 unfiltered. Evidence, not a reference.
- Pinned by
TestRecipeE1EssentialFixed.
The implied-volatility threshold
Yang’s second dynamic threshold is “22% of the current SPX index option implied volatility, which we use the same-day CBOE VIX Index close quote” divided by 100 times the square root of 252 (p. 12), updated daily. e1EssentialImplied turns the VIX close into a daily fraction and compares the move with it.
thr := vix.Mul(f).Div(100 * math.Sqrt(252))
m := x.GT(thr).Sub(x.LT(thr.Mul(-1))).MaskNonFinite(x, thr)
e := m.Abs().GT(0)- Inputs: moves
xand VIX closesvixof one length, paired by position (join them by date first); the fractionf(Yang’s 0.22). - First valid index: the first bar where
xandvixare both finite (1 for log moves of closes when every bar has a VIX close);mis NaN before ande0. - Anchors:
thris bit for bit the arithmetic of the study and of an algolang strategy that implements Yang’s two-day runs rule,f * vix / (100 * sqrt(252)), one product and one quotient: a VIX close of 20 gives 0.0027717394687343331. With this threshold the flat share measures realised against implied volatility (e1-quiet-gauges.md). - Traps: VIXCLS is the 16:15 close, fifteen minutes after the cash close (the study’s previous-day variant changes little). The calendars differ: 34 VIX dates since 1990 are not SPX trading days, the first in 2004, and 3 SPX days have no VIX close, so join by date first. A missing close left NaN makes its bar flat (
mNaN,e0); the study carries the last close forward, which isvix.ValueWhen(vix), since a VIX close is positive. A VIX value of 0 makes the threshold 0, so every non-zero move is essential, and +Inf makes the bar undefined (mNaN). - Evidence: out of sample on SPX (net of costs) the two-day runs rule under this threshold had a Sharpe ratio 0.32 below the unfiltered rule’s (90% interval -0.67 to -0.02). Evidence, not a reference.
- Pinned by
TestRecipeE1EssentialImplied.
The inclusive test
Yang’s prose and his Table 1 differ at the tie: a day “less than a threshold” is nearly flat (p. 1), which makes an equal move essential, while Table 1 (p. 21) heads the retained set “> 0.25%” and puts equality in the filtered set. The study and an algolang strategy that implements Yang’s two-day runs rule test |x| >= thr. e1EssentialInclusive is the inclusive test as a signed mask over the shipped GTE and LTE.
m := x.GTE(thr).Sub(x.LTE(thr.Mul(-1))).MaskNonFinite(x, thr)
e := m.Abs().GT(0)- Inputs: moves
xand a threshold seriesthrin the same units, paired at the most recent bar. - First valid index: the first bar where
xandthrare both finite;mis NaN before ande0. - Anchors: for every threshold that is not negative, bit for bit the strict mask with the threshold nudged one float64 down,
thr.Map(down), wheredownmoves every positive finite value one float64 towards -Inf (math.Nextafter(v, math.Inf(-1))) and leaves 0, +Inf and NaN alone, asDirectionalChangePointsScaled’s doc does for kernels whose test is fixed; and a direct loop of [x >= thr] - [x <= -thr]. - Traps: it differs from the strict mask only on an exact tie with a positive threshold; at a zero threshold a zero move is flat under both (
GTE(0)andLTE(-0)both hold, so m = +0); only|x| >= thras the study and the strategy write it makes a zero move essential there, and the strategy counts it as a down day. A zero move is flat under both tests whenever thr > 0. - Evidence: on the study’s data no return tied its threshold on any series the filter was run on, so the strict and the inclusive masks were the same there (the range mask did tie; see the next section). Evidence, not a reference.
- Pinned by
TestRecipeE1EssentialInclusive; runnable exampleExampleSeries_GTE_inclusiveTie.
Range and volume masks
The study added two masks that do not read the move (neither is Yang’s): a bar is essential when its range h - l is at least half the mean range of the 20 bars before it, or its volume at least 0.6 times the mean volume of the 50 bars before it. e1RangeVolumeMasks writes both with the page’s strict test.
rng := h.Sub(l)
e4 := rng.GT(rng.Average(nr).Shift(1).SetN(nr, math.NaN()).Mul(fr))
e5 := v.GT(v.Average(nv).Shift(1).SetN(nv, math.NaN()).Mul(fv))- Inputs: highs
h, lowsland volumesvof one length, paired by position; the windowsnrandnvand the fractionsfrandfv(the study’s 20 and 0.5, 50 and 0.6). - First valid index: nr for
e4and nv fore5; both read 0 before (the lagged means are NaN there, whereAveragewould pass its input through andShiftpad 0). - Anchors: the means end on bar t - 1. The masks are unsigned; signed for the runs they are
m4 := e4.Mul(x.GT(0).Sub(x.LT(0))).MaskNonFinite(x), withm4.Abs().GT(0)as the event series, so an essential bar with an unchanged close is invisible to the runs, as in the study’s runs rule. - Traps: strict, the page’s tie: a range equal to its fraction of the mean is flat. A NaN in a window or on the bar reads 0, and a zero lagged mean volume makes every positive volume essential. The range mask and the volume mask read different things. Low-volume moves tend to persist rather than reverse (Campbell, Grossman and Wang, 1993), the pre-FOMC drift came in quiet, low-volume hours (Lucca and Moench, 2015), and Kaufman found on SPY, for a long-only system that buys at a close on lower volume, that “when volume declined, average profits were 21.6 basis points” (Stocks & Commodities, March 2023), so a volume mask removes moves a trend rule wants.
- Variant: the study’s “not less than”,
rng.GTE(...)andv.GTE(...), which differs only on an exact tie of the range with its fraction of the mean (two SPY bars, in 1993 and 1999, prices in 32nds; no volume tie). - Variant: the study’s guard on a positive mean volume,
v.GTE(mv.Mul(fv)).Mul(mv.GT(0))withmv := v.Average(nv).Shift(1).SetN(nv, math.NaN()). - Evidence: out of sample the range mask filtered 5 to 10% of days and was the closest to harmless; the volume mask was mildly harmful (one-sided family bootstrap p, unadjusted, 0.025 for the filtered averages and 0.028 for the momentum rule, on the seven ETFs). Evidence, not a reference.
- Pinned by
TestRecipeE1RangeVolumeMasks.
Katsanos’s cut-off (volume flow indicator)
Markos Katsanos (“Using Money Flow To Stay With The Trend”, Stocks & Commodities V22:6, June 2004) “introduced a volatility threshold that will exclude minimal price changes” from his volume flow indicator: a bar’s volume counts up or down only when the typical price changes “by more than the cutoff”. His MetaStock code (also in Intermarket Trading Strategies, Wiley, 2008, p. 336) has CUTOFF:=COEF*VINTER*C with VINTER:=Stdev(INTER,30) and COEF 0.2. e1KatsanosMask is that test as a signed mask.
tp := h.Add(l).Add(c).Div(3)
d := tp.Delta()
cut := tp.Log().Delta().StdDev(30).SetN(30, math.NaN()).Mul(f).Mul(c)
m := d.GT(cut).Sub(d.LT(cut.Mul(-1))).MaskNonFinite(d, cut)
e := m.Abs().GT(0)- Inputs: highs
h, lowsland closescof one length, paired by position, positive where defined; the coefficientf(Katsanos’s 0.2). - First valid index: 1 for
d(Deltareads 0 on bar 0); 30 forcutandm(NaN before);ereads 0 before 30. - Anchors:
mis the sign ofVolumeFlowIndicator’s per-bar score. Its scoresm.Mul(vcap), withvcap := v.Minimum(v.Average(n).Shift(1).Mul(vcoef))the volume capped at vcoef times the lagged n-bar mean, zeroed before bar max(n, 30), summed over n bars, divided by the lagged mean volume and smoothed by a loop seeded with the mean of the first three ratios (orXAverage(3)on the ratios from the first full sum), reproduceVolumeFlowIndicatorbit for bit on every bar for finite positive volumes; the exceptions are a zero lagged mean volume (the method gives NaN to the end,Divgives 0) and a NaN volume that only lifts the method’s cap. Negative volumes are summed as given, as the method’s doc says. - Traps: it differs from Yang’s in the receiver and the window, not the tie: the typical price’s change in price units against sigma times the close; sigma of the typical price’s log changes over 30 bars including the current one, population, as
VolumeFlowIndicatortakes MetaStock’sStdev(Katsanos does not state the divisor); strict, asMF>CUTOFFis. The sumh.Add(l).Add(c)is the method’s order; another order can differ in the last bit. - Evidence: Katsanos calls 0.2 “a universal optimum value derived by testing and is valid for all time frames”; his tests print no data. Evidence, not a reference.
- Pinned by
TestRecipeE1KatsanosMask.
The up-down classification
UpDownSum classifies each close as up, down or unchanged by its change against tol percent of the previous close’s magnitude, “multiplied before it is divided”, as UpDown’s doc says. e1UpDownMask writes that classification as a signed mask, in the form the runs and the windows of the other pages take.
dc := c.Delta()
thr := c.Shift(1).Abs().Mul(tol).Div(100)
ud := dc.GT(thr).Sub(dc.LT(thr.Mul(-1)))- Inputs: closes
cand the tolerancetolin percent (0 counts every change). - First valid index: 1 (bar 0 reads 0).
- Anchors:
udequalsc.UpDownSum(1, tol)bit for bit on every bar, NaN and infinite closes included. - Traps: the order is
UpDown’s, multiply then divide:Mul(tol / 100)is a different threshold in the last bit on many bars, and it changes a class: at tol 0.3 the change from 10.01 to 10.04003 (0.03003) is up here and unchanged underMul(tol / 100). Unmasked, asUpDownSumreads a NaN change as unchanged. - Variant:
ud.MaskNonFinite(dc, thr)keeps a NaN change missing. - Evidence: none; the study did not run this mask (the introduction’s figures, evidence, not a reference, are for the masks it ran).
- Pinned by
TestRecipeE1UpDownMask; runnable exampleExampleSeries_UpDownSum_essentialMask.
The runs recipes count essential bars. Their input is a signed mask m from e1-essential-bars.md, under any threshold: +1 on an essential up bar, -1 on an essential down bar, 0 on a flat bar and NaN on an undefined one, with e := m.Abs().GT(0) as the event series. A flat or undefined bar holds the run, as Yang’s filtered runs rule does: “just skip a"nearly flat” day in the runs day-counting and resume or continue when a normal day (with daily return beyond threshold magnitude) comes" (Yang, “Filtered Market Statistics and Technical Trading Rules”, SSRN 2260609, 2013, p. 11 of the PDF). The run and its direction are NaN before the first essential bar, and the held position before the first signal. A mask that does not read the move, as the range and volume masks, is signed first (e1-essential-bars.md), so an essential bar with an unchanged close is invisible to the run. A decision is made at the close of bar t; trade the next bar with Shift(1).
The signed run length
Yang’s runs rule counts the consecutive days the index “has been down” or up (p. 10), and his filter counts essential days only. e1EssentialRuns returns the event series, the held direction d (the sign of the last essential bar), the start flag and the signed run length: +k on the k-th essential bar of an up run and -k on the k-th of a down run.
e := m.Abs().GT(0)
d := m.ValueWhen(e)
start := d.Delta().Abs().GT(0).Add(e.CumSum().EQ(1).Mul(e))
run := e.SumSince(start).Mul(d)- Inputs: a signed mask
m(+1, -1, 0 or NaN) from any of the essential-bar masks. - First valid index: the first essential bar for
dandrun(NaN before);eandstartread 0 before it. - Anchors:
runequals a loop of the count (a flat or undefined bar holds it, a change of direction restarts it at- bit for bit, on the test’s 22-bar hand fixture (flat bars inside a run, a direction change, an undefined bar, the first run, an infinite move) and on long tapes.
startis 1 on the first essential bar and on every essential bar whose direction differs from the last essential bar’s.
- bit for bit, on the test’s 22-bar hand fixture (flat bars inside a run, a direction change, an undefined bar, the first run, an infinite move) and on long tapes.
- Traps:
Deltaat the first essential bar is +1 or -1 minus NaN, whichGTreads as false, so theCumSumterm starts the first run. The event series must come fromm:x.Abs().GT(thr)calls an infinite move essential whilemis NaN there, so the bar is counted with a NaN direction (the hand fixture’s run reads NaN and then 4 where the count holds 2 and then reads 3). - Variant: the run length on the event clock,
m.FuncWhere(e, func(x Series) Series { return x.StateAge().Mul(x) }), equalsrunbit for bit on every bar:StateAgeof the signs of the essential bars, taken in order, is the run length by definition. - Evidence: in the study’s replication the runs rule under the 22% VIX threshold returned 9.9% a year on SPX over 1990-2012, gross of costs (Yang printed 10.31%); on SPX and SPY this count gives the study’s positions on every bar after the first signal. Evidence, not a reference.
- Pinned by
TestRecipeE1EssentialRuns; runnable exampleExampleSeries_SumSince_essentialRuns.
Yang’s two-day runs rule
Yang’s rule (p. 10): “Long 100% in SPX at the market close of a trading day when the index has been down for n days (including the trading day); switch to 100% short in SPX at the market close of a trading day when the index has been up for n days (including the trading day)”, and the position “is continued” otherwise; he studies n = 2. Over essential bars “has been down for n days” is run <= -n. e1TwoDayRule returns the signal and the held position.
sig := run.LTE(float64(-n)).Sub(run.GTE(float64(n)))
pos := sig.ValueWhen(sig.Abs().GT(0))- Inputs: the run length
runofe1EssentialRunsand the run lengthnthat triggers (Yang’s 2). - First valid index:
posis NaN until the first signal;sigreads 0 before it. - Anchors: long after n essential down bars and short after n up, held:
poswith NaN read as 0 equals a loop of the study’s runs rule bit for bit on the 32-bar study fixture and on long tapes. The signal fires again on the (n + 1)-th bar of a run without changingpos, as Yang’s “has been down for n days” does. - Traps: the study and the strategy are flat before the first signal, so a P&L reads
pos.If(math.IsNaN, 0.0, pos). An algolang strategy that implements Yang’s two-day runs rule resets its counter after each signal, so it fires on the second, fourth and sixth bars of a long run and adds to the position each time; that is the strategy’s trading logic, not the indicator, and Yang holds; the held sign is the same. It also counts an unchanged close as a down day and tests inclusively (e1-essential-bars.md). - Evidence: out of sample on SPX, net of costs, the VIX-filtered rule returned -4.3% a year against +1.0% unfiltered. Evidence, not a reference.
- Pinned by
TestRecipeE1TwoDayRule; runnable exampleExampleSeries_ValueWhen_twoDayRule.
These windows count essential bars: a window of N holds the closes or moves of the last N essential bars, and the current bar counts when it is essential. Their inputs are closes c or moves x and the event series e := m.Abs().GT(0) of a mask from e1-essential-bars.md, paired at the most recent bar. On a flat or undefined bar each statistic holds its last value (the Where methods hold the last event bar’s value verbatim), and each is NaN until its window is full. The window length confounds any comparison with a calendar window: an N-essential-bar window spans about N / (1 - q) trading bars for a flat share q, 246 to 293 days for N = 200 on SPX across the study’s rolling, VIX, anchored and fixed filters (235 to 293 across the eight series). Yang put a 0.25% filter’s 25/200 pair at about a 30/250 calendar pair, for a filter that excludes “over 20%” of days (“Filtered Market Statistics and Technical Trading Rules”, SSRN 2260609, 2013, p. 15 of the PDF); at his own 27% flat share the same arithmetic gives about 34/274, and the study measured 278 days in sample and 293 out of sample for N = 200. Compare a filtered rule with calendar windows of the same mean span (e1-quiet-gauges.md measures the span).
Filtered moving averages
Yang filters his dual moving average cross rule (p. 14): “If a"nearly flat” day is within the range of look-back for simple moving average calculation, the index price of that day is ignored", and the window is extended back to earlier “essential” days, so that “The total number of daily close prices in the moving average calculation is still the same as the stated look-back length.” He fixes the long average at 200 days, varies the short one from 5 to 50 days, and below the long average tests flat, 50% short and 100% short. e1FilteredCross returns the two averages of the last ns and nl essential closes and both positions.
sS := c.AverageWhere(e, ns)
sL := c.AverageWhere(e, nl)
long := sS.GT(sL)
ls := sS.GT(sL).Sub(sS.LTE(sL))- Inputs: closes
cand the event seriese, paired at the most recent bar; the lengthsnsandnl(Yang’s 25 and 200, among others). - First valid index: the ns-th essential bar for
sSand the nl-th forsL(NaN before);longandlsread 0 before the nl-th essential bar. - Anchors: Yang’s “backfill” is this window. On an all-true
e,AverageWhereequalsAveragebit for bit from bar k - 1 for a window of k. The averages equal loops of the mean of the last ns and nl essential closes, and the positions loops of the study’s rule, bit for bit on the test tapes. - Traps: a tie
sS == sLgoes short inls(LTE), as in the study, whilelongreads 0 there; on a flat bar both averages hold, so the decision cannot change there. Kaufman (“Do Small Price Changes Matter Or Are They Just Noise?”, Stocks & Commodities V41:2, February 2023) is the closest magazine precedent, an average of kept closes, but as printed his keep-ruleclose/close[1] -1 > minPCmovehas no absolute value, so it keeps only up closes. - Variant: an exponential average on the event clock,
c.FuncWhere(e, func(y Series) Series { return y.XAverage(n).SetN(n-1, math.NaN()) }), seeded on the n-th essential bar withXAverage’s seed, the plain left-to-right sum of the first n essential closes over n, and updated on essential bars only. It is not the average of the held closes in calendar time,XAverage(n)ofc.ValueWhen(e)from the first essential bar on, and the two differ on most bars after the seed. Written whole,c.ValueWhen(e).XAverage(n)is NaN from bar n - 1 on unless bar 0 is essential: the held closes are NaN before the first essential bar, and the NaN enters its seed. - Evidence: out of sample the filtered 25/200 pair was worse than the unfiltered one in 14 of 16 cells under the rolling filter; on SPX long or flat, net Sharpe ratio 0.67 against 0.79. Evidence, not a reference.
- Pinned by
TestRecipeE1FilteredCross; runnable exampleExampleSeries_AverageWhere_filteredCross.
Yang’s channel
Yang’s price channel (p. 16) takes “the highest /lowest index price over the past L = 200 days (excluding the same day)”, M_t and m_t, and switches a long position to short when the close is below m_t and a short one to long when it is above M_t; filtered, “prices of the"nearly flat” days are ignored and the look-back window is extended back-ward as necessary to include prices of the same number of earlier “essential” days" (p. 17). e1EssentialChannel returns the bounds, the two breakout flags and the latch.
hi := c.QuantileWhere(e, L, 1).Shift(1).SetN(1, math.NaN())
lo := c.QuantileWhere(e, L, 0).Shift(1).SetN(1, math.NaN())
up := c.GT(hi).Mul(e)
dn := c.LT(lo).Mul(e)
pos := c.PositionSigned(up, dn, dn, up)- Inputs: closes
cand the event seriese, paired at the most recent bar; the lengthL(Yang’s 200). - First valid index: the bar after the L-th essential bar for
hiandlo(NaN before); the first test is on the (L + 1)-th essential bar, andposreads 0 until the first breakout. - Anchors: on an essential bar
hiandloare Yang’s M_t and m_t, the extremes of the L essential closes before it.posequalsup.Sub(dn).Hysteresis(0, 0)bit for bit (the flags are not both 1 on any bar) and a loop of Yang’s definition. The event-time bounds, held on flat bars, arehi.ValueWhen(e)andlo.ValueWhen(e). - Traps: on a flat bar the calendar shift reads the window that ends at the last essential bar, so the flags are multiplied by
e: the study tests essential bars only, and Yang does not say; testing flat bars too moved 0 to 7 bars of timing on SPX and SPY. The tests are strict, so a close equal to a bound holds the position. A NaN or infinite close on an essential bar (possible with a range or volume mask) makes the bounds NaN for the next L essential bars, so the channel is silent while it is in the window. - Variant: Yang’s text read as testing every bar,
up := c.GT(hi)anddn := c.LT(lo). - Variant: the bounds on the event clock,
c.FuncWhere(e, func(x Series) Series { return x.Highest(L).Shift(1).SetN(L, math.NaN()) })and itsLowesttwin, equalhi.ValueWhen(e)andlo.ValueWhen(e)bit for bit on every bar for closes that are not zero and give the sameup,dnandpos, except while an infinite close on an essential bar (possible with a range or volume mask) is in the window: there these bounds keep a value where theQuantileWherebounds read NaN, so a breakout can fire and moveposwhere the recipe’s flags stay 0. TheSetNmasks the first L essential bars, whichShift’s padding andHighest’s running maxima would fill. - Evidence: out of sample the long channels cannot be decided: 6 to 14 position changes in nearly 14 years at L = 200. Evidence, not a reference.
- Pinned by
TestRecipeE1EssentialChannel; runnable exampleExampleSeries_QuantileWhere_essentialChannel.
Momentum over essential closes
Not in Yang: the study extended his filter to a momentum rule, long when the last essential close exceeds the essential close k essential bars before it. e1EssentialMomentum returns the ratio of the two closes and the long flag, both held on flat bars.
mom := c.ValueWhen(e).Div(c.ValueWhen(e, k))
long := c.ValueWhen(e).GT(c.ValueWhen(e, k))- Inputs: closes
c, positive, and the event seriese, paired at the most recent bar; the lagkin essential bars. - First valid index: the (k + 1)-th essential bar for
mom(NaN before);longreads 0 before it. - Anchors: the last essential close over the one k essential bars before it, held on flat bars:
momandlongequal a loop over the essential closes bit for bit, andlongequals the study’s momentum rule from the first essential bar. - Traps:
c.Div(c.ValueWhen(e, k))reads a flat bar’s own close and is not this: it compares a flat close with an essential one. A zero lagged close makesmom0, byDiv’s zero rule. - Evidence: filtering it does not help out of sample: on SPX the rule over 252 essential closes under the rolling filter had a net Sharpe ratio of 0.62 against 0.79 for 252 calendar bars unfiltered. Evidence, not a reference.
- Pinned by
TestRecipeE1EssentialMomentum.
Mean and standard deviation of essential moves
Yang’s Table 1 (p. 21) gives the mean, standard deviation, skew and excess kurtosis of the retained set, the moves of the essential days, over each of his periods. e1EssentialMoments gives the mean and the population standard deviation of the moves on the last n essential bars, held on flat bars.
mean := x.AverageWhere(e, n)
sd := x.StdDevWhere(e, n)- Inputs: moves
xand the event seriese, paired at the most recent bar; the windownin essential bars, n >= 0. - First valid index: the n-th essential bar (the first for n = 0); NaN before.
- Anchors: on an all-true
e,StdDevWhereequalsStdDevbit for bit from bar n - 1, n >= 2, andAverageWhereequalsAverage. Yang’s retained-set statistics are the expanding forms, n = 0. - Traps:
sdis the population deviation; the sample form issdtimes sqrt(n/(n-1)),sd.Mul(math.Sqrt(float64(n) / float64(n-1))), to rounding. A NaN move in the window makes both NaN, and +Inf makes the mean +Inf andsdNaN. - Variant: the skewness and excess kurtosis of the last n essential moves, the other two statistics of Yang’s Table 1 over a window,
x.FuncWhere(e, func(y Series) Series { return y.Skewness(n).SetN(n-1, math.NaN()) })andx.FuncWhere(e, func(y Series) Series { return y.Kurtosis(n).SetN(n-1, math.NaN()) }), bit for bit the population statistic of a fresh window of those n moves, held on flat bars and NaN before the n-th essential bar. - Variant: the serial correlation of essential moves,
x.FuncWhere(e, func(y Series) Series { return y.Correlation(y.Shift(1), n).SetN(n, math.NaN()) }), bit for bit the Pearson correlation of the last n essential moves with the n essential moves one place earlier, NaN before the (n + 1)-th essential bar:Shift’s padding enters the first window, so n places are masked. - Evidence: in Yang’s Table 1 (SPX, 1990-2012, 0.25%) the retained set’s standard deviation is 1.368% against 1.171% for all days; the study matched his all-days figures with simple returns, not log returns. Evidence, not a reference.
- Pinned by
TestRecipeE1EssentialMoments.
These gauges describe the event clock of an essential-bar mask, how much of the market’s movement falls inside its threshold and how many trading bars its essential bars cover; they are diagnostics, not trading signals. Their inputs are the signed mask m or the event series e := m.Abs().GT(0) of a mask from e1-essential-bars.md, or the closes c for the soft weighting. Their warm-ups are NaN, and an undefined bar of m (NaN) counts neither as flat nor as essential: it makes a flat share undefined and is skipped by the event clock.
The flat share
The study’s quiet-market gauge is the share of flat bars among the last n bars, 20 in the study. e1FlatShare returns it from the signed mask, with the bars where the mask is undefined kept missing.
q := m.EQ(0).MaskNonFinite(m).Average(n).SetN(n-1, math.NaN())- Inputs: a signed mask
m(+1, -1, 0 or NaN) and the windown>= 1. - First valid index: n - 1 bars after the first bar where
mis defined, when the bars after it are defined (80 with the 60-bar rolling threshold and n = 20); in general the first bar that ends n defined bars. NaN before. - Anchors:
qis the number of flat bars over n, bit for bit against a loop of the count (Average’s compensated sum of 0s and 1s is exact); -0 counts as flat. - Traps: undefined bars give NaN, not flat.
1 - e.Average(n),e.Average(n).Mul(-1).Add(1), differs in the last bit for some counts (it computes 1 - k/n, not (n - k)/n) and passesAverage’s warm-up through as shares of 0 or 1. The meaning depends on the threshold: under a rolling sigma it measures conditional peakedness (a Gaussian gives 15.9% at 0.2 sigma, SPX 18 to 21%), under a fixed threshold the volatility level, and under the VIX threshold realised against implied volatility, a proxy of the variance risk premium. This gauge shows no calm before the storm: Crabel’s and Connors and Raschke’s compression set-ups are range objects (bar-classification/t6-narrow-range-bars.md). - Evidence: a high flat share predicts lower volatility, not higher: out of sample on SPX, 20 days ahead, the Spearman correlation is -0.12, and nothing is left once trailing volatility is controlled for. Evidence, not a reference.
- Pinned by
TestRecipeE1FlatShare.
The event-clock span
The span is the number of trading bars that the last k essential bars cover, the length that an N-essential-bar window stands for on the calendar (e1-essential-windows.md). e1EventSpan counts it from the k-th most recent essential bar to the current bar, both included.
span := e.BarsSince(k - 1).Add(1)- Inputs: the event series
eof an essential-bar mask and the countk>= 1. - First valid index: the k-th essential bar (NaN before).
- Anchors:
spanon bar t is t - b + 1, with b the k-th most recent essential bar at or before t, bit for bit against a count; with k = 1 it is 1 on an essential bar and grows by 1 on each flat bar. - Traps: it averages about k / (1 - q) for a flat share q, and more when flat bars cluster.
- Evidence: on SPX with k = 200 the span averaged about 246 bars in sample and 253 out of sample under the rolling filter, and 278 and 293 under Yang’s fixed 0.25%. Evidence, not a reference.
- Pinned by
TestRecipeE1EventSpan; runnable exampleExampleSeries_BarsSince_eventSpan.
Soft weighting
The study’s soft-weighted averages replace the mask with a weight, min(|x| / sigma, cap), with sigma the rolling threshold’s standard deviation, so that a nearly flat bar counts a little rather than not at all; the two averages are those of the closes over ns and nl bars weighted by it. e1SoftWeighted returns the move, the weight, the two averages and the long flag.
x := c.LogRatio(c.Shift(1))
sig := x.StdDevSample(n).Shift(1).SetN(n+1, math.NaN())
w := x.Abs().Div(sig).Clamp(0, cp)
sS := c.VWMA(w, ns).SetN(ns-1, math.NaN())
sL := c.VWMA(w, nl).SetN(nl-1, math.NaN())
long := sS.GT(sL)- Inputs: closes
c, positive where defined; the windown>= 2 of sigma, the capcp(the study’s 3; +Inf for no cap) and the lengthsnsandnl(25 and 200). - First valid index: 1 for
x; n + 1 forw; n + ns forsSand n + nl forsL(NaN before);longreads 0 before n + nl. - Anchors: with cap +Inf and a constant sigma that is a power of two,
c.VWMA(x.Abs().Div(sig).Clamp(0, cp), k)equalsc.VWMA(x.Abs(), k)bit for bit (to rounding for other constants). - Traps: unmasked,
VWMA’s warm-up reads 0, soc.VWMA(w, 25).GT(c.VWMA(w, 200))fires long against a 200-bar mean of 0 (on bars 85 to 198 with n = 60). A zero sigma (n equal moves) gives the weight 0 underDiv’s zero rule, where the study’s code gives the cap (or NaN for a zero move). A NaN sigma gives a NaN weight, which makes everyVWMAwindow holding it NaN; that is whysSstarts at n + ns, and a NaN close makes the next ns and nl windows NaN. The weight is a choice: Bisse’s move-adjusted average (Stocks & Commodities V23:2, February 2005) weights by the preceding absolute move, Arms by volume, and Avellaneda and Lee weight returns inversely by volume. - Evidence: out of sample on SPX the soft-weighted 25/200 pair’s net Sharpe ratio was 0.08 below the unfiltered pair’s long or flat and 0.17 below it long or short. Evidence, not a reference.
- Pinned by
TestRecipeE1SoftWeighted; runnable exampleExampleSeries_VWMA_softWeighted.
Consecutive
ConsecDown
func (s Series) ConsecDown() Series
ConsecDown returns a series of the counts of bars that are consecutively down.
ConsecDownLoss is the loss over the run this counts: on data without infinities it is s.Mul(-1).ChangeSince(s.ConsecDown().EQ(0)) bit for bit.
ConsecDownLoss
func (s Series) ConsecDownLoss() Series
ConsecDownLoss returns the loss accumulated over the current down run of s, the closed form s[a] - s[t] with a the latest bar at or before t that is not a down bar.
Bar t is a down bar when t is at least 1, s[t] and s[t-1] are both finite (neither NaN nor an infinity) and s[t] < s[t-1], strictly, as in ConsecDown. Every other bar ends the run: bar 0, an unchanged close, a close that is NaN or an infinity, and the bar after such a close. The loss is non-negative, Pee’s -CF, counted as RSI counts its down moves rather than signed: 0 on every bar that is not a down bar and has a finite close, and positive on a down bar (+Inf when the subtraction overflows), in the units of s and unbounded above; every zero is +0.
The loss is one subtraction, s[a] - s[t], not a sum of the one-bar changes of the run. It is the -CF column of the spreadsheet in M.H. Pee, “Trend Continuation Factor”, Stocks & Commodities V20:3 (March 2002): the closed form equals his recursion bit for bit while the closes of the run stay within a factor of two of each other, and to rounding otherwise, since the recursion rounds once per bar of the run.
There is no warm-up: bar 0 is 0, or NaN when s[0] is not finite. A close that is NaN or an infinity gives NaN on its own bar and 0 on the next (when that close is finite), and no other bar is NaN; the bars after it belong to a new run.
On data without infinities (NaN allowed) the result equals s.Mul(-1).ChangeSince(s.ConsecDown().EQ(0)) bit for bit. The one difference is at an infinity: ConsecDown compares an infinity as a number, so a -Inf close after a finite one, or a finite close after +Inf, counts as a down bar there and the composition reads +Inf along the run that follows, while here the infinity and the bar after it both end the run.
The value at bar t depends on the anchor of its run, which can lie before any window of interest. A run in progress at the start of the slice is measured from bar 0, so the first run in the slice is truncated; the value is the same in every slice that holds the run’s anchor.
The cost is O(1) per bar and one allocation. ConsecUpGain is the mirror for up runs: s.Mul(-1).ConsecUpGain() equals this result bit for bit on every input. TrendContinuationPlus subtracts the window sum of this loss.
ConsecDownOrEqual
func (s Series) ConsecDownOrEqual() Series
ConsecDownOrEqual returns a series of the counts of bars that are consecutively down or equal.
ConsecUp
func (s Series) ConsecUp() Series
ConsecUp returns a series of the counts of bars that are consecutively up.
ConsecUpGain is the gain over the run this counts: on data without infinities it is s.ChangeSince(s.ConsecUp().EQ(0)) bit for bit.
ConsecUpGain
func (s Series) ConsecUpGain() Series
ConsecUpGain returns the gain accumulated over the current up run of s, the closed form s[t] - s[a] with a the latest bar at or before t that is not an up bar.
Bar t is an up bar when t is at least 1, s[t] and s[t-1] are both finite (neither NaN nor an infinity) and s[t] > s[t-1], strictly, as in ConsecUp. Every other bar ends the run: bar 0, an unchanged close, a close that is NaN or an infinity, and the bar after such a close. The gain is 0 on every bar that is not an up bar and has a finite close, and positive on an up bar (+Inf when the subtraction overflows), in the units of s, non-negative and unbounded above; every zero is +0.
The gain is one subtraction, s[t] - s[a], not a sum of the one-bar changes of the run. It is the +CF column of the spreadsheet in M.H. Pee, “Trend Continuation Factor”, Stocks & Commodities V20:3 (March 2002): the closed form equals his recursion bit for bit while the closes of the run stay within a factor of two of each other, and to rounding otherwise, since the recursion rounds once per bar of the run.
There is no warm-up: bar 0 is 0, or NaN when s[0] is not finite. A close that is NaN or an infinity gives NaN on its own bar and 0 on the next (when that close is finite), and no other bar is NaN; the bars after it belong to a new run.
On data without infinities (NaN allowed) the result equals s.ChangeSince(s.ConsecUp().EQ(0)) bit for bit. The one difference is at an infinity: ConsecUp compares an infinity as a number, so a +Inf close after a finite one, or a finite close after -Inf, counts as an up bar there and the composition reads +Inf along the run that follows, while here the infinity and the bar after it both end the run.
The value at bar t depends on the anchor of its run, which can lie before any window of interest. A run in progress at the start of the slice is measured from bar 0, so the first run in the slice is truncated; the value is the same in every slice that holds the run’s anchor.
The cost is O(1) per bar and one allocation. ConsecDownLoss is the mirror for down runs: s.Mul(-1).ConsecDownLoss() equals this result bit for bit on every input. TrendContinuationMinus subtracts the window sum of this gain.
ConsecUpOrEqual
func (s Series) ConsecUpOrEqual() Series
ConsecUpOrEqual returns a series of the counts of bars that are consecutively up or equal.
StateAge
func (s Series) StateAge() Series
StateAge returns the number of consecutive bars, including the current one, for which the series has held its current finite value, counting 1 on the first bar of each spell and NaN on a bar that is not finite.
The receiver is a state series, and any finite value is a state: 0/1 flags, ±1 signs, UpDown codes and non-integer labels all work. States are compared with exact float64 equality, so +0 and -0 are the same state while 0.1+0.2 and 0.3 are not; build states from exact integer codes. On bar t the age is one more than bar t-1’s when the value equals bar t-1’s, and 1 otherwise, so bar 0 reads 1 when it is finite. The values are exact integers stored as float64, at least 1, or NaN.
A bar whose value is NaN, +Inf or -Inf reads NaN (math.NaN(), whatever the payload of the input) and restarts the count, so the next finite bar reads 1 even when it repeats the value before the gap, and +Inf followed by +Inf is not a run. This is the restart class of missing observation: the bad bar reads NaN, and the bars after it may read less than they otherwise would, up to the first change of state. The first spell, and any spell that starts after a gap, is left-censored: its ages are lower bounds, because the spell may have begun before the data. So s[k:].StateAge() differs from s.StateAge()[k:] on the bars of a spell that began before bar k, and agrees from the first change of state after bar k.
There is no warm-up. The first valid index is 0 and the minimum length is 1; a nil or empty receiver gives an empty Series. Each bar reads only its own value, the previous bar’s value and the previous age, so the output is causal and prefix-invariant. It is not start-invariant: the count starts at the first bar, so its settling length is path-dependent, and the output settles at the first change of state after the first bar.
A warm-up or undetermined code must be masked to NaN before it is aged, or it is aged as a real state: SetN(1, math.NaN()) for a flag built on Delta, whose bar 0 is 0; SetN(n-1, math.NaN()) for GT(Average(n)), since Average returns its input before a full window; and SetN(n, math.NaN()) for UpDown(n) and UpDownSum(n), whose warm-up 0 is also a real code. RegimeDFA(n) reads 0 on bars 0 to n-2, which is also its neutral state, so the caller decides whether to mask it. GT, LT and EQ read a comparison with NaN as false, and UpDown classifies a NaN change as flat, so each turns a missing bar into a state; MaskNonFinite keeps it missing. DirectionalChange reads 0 before its first confirmation: that 0 is its undetermined state, not a run, so callers mask it to NaN before they age the state.
The age counts 1 on the first bar of a spell. TSSB’s SIGN_AGE of a series D counts from 0 with a cap, and from bar m-1 on it equals D.GT(0).StateAge().Sub(1).Min(float64(m-1)) for its lookback m, with zero counted as negative. BarsSince counts from 0 on its event bar: on finite data StateAge is 1 more than BarsSince of a series that is non-zero on bar 0 and on every change of state. For a finite position of 0, 1 and -1 values, BarsSinceEntry equals pos.StateAge().Sub(1) on the in-position bars. SwingHighAge and SwingLowAge count from a confirmed swing’s extreme bar instead, and so does DirectionalChangeAge, which counts from the bar of the last confirmed extreme, while StateAge of the DirectionalChange state counts from the confirmation bar.
The mean of StateAge over bars is not the mean length of a spell, because a long spell contributes many bars: on the up bars of AAPL’s 2023 daily closes (the 200 bars of the package’s test data) the mean age is 213/107, about 1.99, while the mean up spell lasts 107/48 bars, about 2.23. A dwell time is a statistic of completed spells, not of StateAge.
StateAge is a descriptive counter with no demonstrated trading edge. Geczy and Samonov’s signed duration of the market state is StateAge times the state’s sign, on monthly data; their hedge thresholds of 10 months up and 7 months down were chosen in-sample and are not defaults. Baker and Arnold’s short-term trend, at least 10 consecutive daily closes on one side of the 10-day average, becomes the causal flag StateAge >= 10, which fires from the tenth day; it is an integer predicate, so GTE(10) and GT(9) are the same test.
Sources: Aronson and Masters, Statistically Sound Machine Learning for Algorithmic Trading of Financial Instruments (2013), pp. 367-370, for SIGN_AGE; Geczy and Samonov, “Two Centuries of Price Return Momentum” (2016), whose duration is 1 in a state’s first month; Gustafson, “Price Persistency”, Technical Analysis of Stocks & Commodities (January 2002), whose up and down counters are ConsecUp and ConsecDown; and Baker and Arnold, “How Random Is The Dow?”, Technical Analysis of Stocks & Commodities (May 2002).
The compositions that use it are listed under “Compositions” in the “Consecutive”, “Regime Detection”, “Point and Figure” and “Elliott Wave” categories. DeMarkSetup’s doc writes DeMark’s setup count as the StateAge of the closes’ four-bar comparison, kept only on runs whose first bar is a price flip. DeMarkCountdown’s doc finds its deferred bars with cd.StateAge().GT(1), a count held from the bar before. Eliades’s sign of the bear, a recipe under “Breadth”, ages the streak of days whose advance/decline ratio stays inside its band.
Verification: with a = x.ConsecUpOrEqual(), b = x.ConsecDownOrEqual() and m = a.Add(b).Sub(a.Sub(b).Abs()).Div(2.0), the smaller of the two counts, x.StateAge() equals m.Add(1.0).Add(x.Sub(x)) bit for bit on every bar where it is not NaN, and is NaN wherever that is NaN, for every input, NaN, ±Inf and ±0 included. On finite data it equals e.BarsSince().Add(1) for e = x.Delta().SetN(1, 1), and it agrees with ConsecUp, ConsecDown, ConsecUpOrEqual and ConsecDownOrEqual on the up and down flags of finite closes and on UpDownSum(1).
Compositions
State age
StateAge counts the bars of the current spell of a state series, 1 on the spell’s first bar, and reads NaN, restarting the count, on a bar that is not finite. Build the state so that a missing bar stays missing: GT, LT and EQ read a comparison with NaN as false, so mask each comparison with the series it compared through MaskNonFinite; and mask any warm-up code to NaN with SetN before ageing it, or it is aged as a real state. The four recipes below take closes c or returns r; their outputs are exact counts and codes.
(a) Up-or-not-up age
d := c.Delta()
up := d.GT(0).MaskNonFinite(d).SetN(1, math.NaN())
age := up.StateAge()- First valid index: 1, the first bar with a change; bar 0 is NaN, since Delta’s bar 0 is a warm-up 0. The minimum length is 2.
- Missing closes: a NaN or infinite close makes its own change and the next bar’s non-finite, so both are masked, and the count restarts after them. A bare
d.GT(0)ages both, and bar 0, as not-up bars, and a test ofx > 0alone misses the infinities. - Anchors: on finite closes, from bar 1 on,
ageequalsc.ConsecUp()on up bars andc.ConsecDownOrEqual()on the others (E). On the 200 AAPL closes of 2023 in the package’s test data, the ages on the 107 up bars and the 92 other bars sum to 376. - Pinned by
TestRecipeStateAge; runnable exampleExampleSeries_StateAge_upFlag.
(b) Signed age
Geczy and Samonov’s signed duration of a state: the age times the sign.
sgn := r.GT(0).Mul(2).Sub(1).MaskNonFinite(r)
signedAge := sgn.StateAge().Mul(sgn)- First valid index: 0; the minimum length is 1.
- Zero: a zero return counts as negative, as in TSSB; Geczy and Samonov do not define the sign of zero.
- Missing returns: without
MaskNonFinite(r),r.GT(0).Mul(2).Sub(1)counts a NaN return as a down bar and extends the down spell through it. - Anchors:
signedAgeis±StateAgeof the sign, so its absolute value is at least 1 wherever it is not NaN (E). - Pinned by
TestRecipeStateAge; runnable exampleExampleSeries_StateAge_signedAge.
(c) TSSB’s SIGN_AGE
Aronson and Masters’ @SIGN_AGE(D(0, m)) (TSSB, pp. 367-370) counts from 0 and caps the count at m-1, with zero counted as negative. With d the series D, here the one-bar change:
d := c.Delta()
signAge := d.GT(0).StateAge().Sub(1.0).Min(float64(m - 1))- First valid index: m-1; the minimum length is m. Before bar m-1 TSSB duplicates the first value into the missing history, so its count is larger wherever the current run reaches bar 0.
- The receiver is the two-class
d.GT(0): a three-valued sign would differ on and after bars wheredis 0. - The cap is m-1: the book’s prose asks for a maximum count of 20, but its code,
PRICE_DIFF(0, 20)counting from 0, caps at 19, which is what this recipe does. - Anchors: from bar m-1 on it equals a direct count, from 0, of the consecutive earlier bars of the window with the current bar’s sign (E).
- Pinned by
TestRecipeStateAge; runnable exampleExampleSeries_StateAge_signAgeTSSB.
(d) Side of a moving average
Baker and Arnold’s short-term trend, at least 10 consecutive closes on one side of the 10-day average, as a causal flag:
side := c.GT(c.Average(n)).SetN(n-1, math.NaN())
age := side.StateAge()
trend := age.GTE(10)- First valid index: n-1, the first full window;
Averagereturns its input before a full window, sosideis a warm-up 0 there and is masked. Unmasked, on the AAPL closes with n = 19, bar 18 reads 19 rather than 1. - The flag fires from the tenth bar on one side;
age.GTE(10)andage.GT(9)are the same test on whole-number ages. Baker and Arnold’s whole-sample stays and their 10-or-more-day label classify whole runs in hindsight; the flag is their causal counterpart. - Rolling means: the side of a rolling float average can flip on a one-ulp difference, and one flipped state changes every later age in its run, so goldens across languages use only integer states or
Delta. - Pinned by
TestRecipeStateAge; runnable exampleExampleSeries_StateAge_movingAverageSide.
A run-count setup traded on a breakout bar: the recipe counts consecutive lower highs on consecutive lower volumes with ConsecDown, waits for a higher high on the next bar and books the trade from the open after it. Masked values are NaN before their first valid index (SetN(F, math.NaN())), and flags and books built on them read 0 there; the three outputs are 0/1 flags and a 0/1 book, NaN on no bar, since a missing high, close or volume fails every comparison it enters. Prices are positive and finite and volumes finite and non-negative, the volume threshold in the caller’s unit; lengths are in bars, his days being daily bars; series are paired by position, so the caller aligns them by timestamp first.
Declining-volume pullback (Palmquist)
Steve Palmquist (“Data-Driven Trading”, Stocks & Commodities V29:13, December 2011, pp. 34-39) sets up on “a series of three consecutive lower highs along with three consecutive days of lower volume” and triggers on “a higher high, which indicates that the series of lower highs has ended”, with two liquidity filters: a close above 5 dollars and “the 21-day simple moving average of the volume must be greater than 200,000”. “When a scan identifies the setup condition as having occurred yesterday and a trigger condition occurs today, a position is entered at the opening price tomorrow”, and it “is held for three days and then sold at the next open”. t6DecliningVolumePullback reads “three” as at least three (his Figure 4, read at high resolution, flags a run of five lower highs), ends both runs on the setup bar, reads “lower” and “higher” as strict, tests the filters on the trigger bar and books each trade with Position on the open.
setup := h.ConsecDown().GTE(3).Mul(v.ConsecDown().GTE(3))
trig := setup.Shift(1).Mul(h.GT(h.Shift(1)))
liq := c.GT(minClose).Mul(v.Average(21).SetN(20, math.NaN()).GT(minVolume))
sig := trig.Mul(liq)
pos := o.Position(sig.Shift(1), nil, 3)- Inputs: opens
o, highsh, closescand volumesvof one length;minClosein price units (5) andminVolumein the caller’s volume unit (200,000 shares, his database being “stocks trading above an average of 200,000 shares a day”). The unit of his AIQ scan is not certain: the volume axes of his charts look like hundreds of shares. - First valid index: 3 for
setup(three falls need four bars) and 20 forsig(the gate’s mask: the 21-bar average is NaN on bars 0 to 19, so the gate reads 0 there). The trigger needs no mask: the shifted setup is 0 on bars 0 to 3, althoughh.GT(h.Shift(1))reads 1 on bar 0 against the shift’s zero pad.posis 0 before the first entry, bar 21 at the earliest, and a signal on one of the last four bars has no closed trade. Unmasked, the gate reads the warm-up (Traps). - Anchors:
h.ConsecDown().GTE(3)equalsh.NofDownS(3, 3)bit for bit on every bar, the warm-up included (each says the last three comparisons are falls, and both are 0 before bar 3), sosetupis alsoh.NofDownS(3, 3).Mul(v.NofDownS(3, 3));sigequals a direct loop of his rules (h[t-1] < h[t-2] < h[t-3] < h[t-4], the same for v, h[t] > h[t-1], c[t] > minClose and the mean of v[t-20] to v[t] above minVolume, from bar 20) with zero mismatches on every bar of a 700-bar path, its non-integer-volume twin and two 60-bar paths. Two signals are at least four bars apart, since the trigger’s higher high ends the run and a new setup needs three more lower highs, soo.Position(sig.Shift(1), nil, 3)books every signal as its own trade: for a signal on bar t,posis 1 on bars t+1 to t+3 and 0 on t+4, where the hold closes it, ando.TradeResult(pos)posts o[t+4] - o[t+1] on bar t+4 (NaN on every other bar), bit for bit: his fill “at the opening price tomorrow” and his sale “at the next open” three bars after the entry, four after the signal. - Traps: the gate without its mask,
v.Average(21).GT(minVolume):Averagepasses the volume through on bars 0 to 19 (and copies the whole series below 21 bars), so the gate compares each bar’s own volume with the threshold there; on the 700-bar path it passes on 6 of bars 0 to 19, and the trigger on bar 12, on a volume of 450,000, would signal. An inclusive trigger,h.GTE(h.Shift(1)), fires on a high equal to the setup bar’s: on the 700-bar path bar 541 repeats bar 540’s high of 59.5 with the gate open, and the strictGTdoes not signal there. Both filters are strict too, as his “greater than” says. - Variant: exactly three,
setup := h.ConsecDown().EQ(3).Mul(v.ConsecDown().EQ(3)), flags a subset of the setups, and a pullback of four lower highs on four lower volumes then does not signal at all: the setup fires on the run’s third bar, whose next bar is the fourth lower high, not a trigger. On the highs it would not have flagged his Figure 4. - Variant: his uptrend filter, “the highest high of the last two weeks is higher than the highest high of the previous two weeks, and the lowest low of the last two weeks is higher than the lowest low of the previous two weeks”, on 10 bars ending on the trigger bar with the lows
l:sig.Mul(hh.GT(hh.Shift(10)).Mul(ll.GT(ll.Shift(10))))withhh := h.Highest(10)andll := l.Lowest(10), valid from bar 19, which the gate’s mask already covers. His Bollinger filter (“at least twice the daily range below the upper band”) leaves the trigger point, the day of the range and the band’s length and width unstated. - Evidence: his Figure 3 (“TriggeredDVPullback”, AIQ, every stock of his database of about 2,500 that met the setup in 2007, a period picked as one “where it should work”, no costs): 2,171 trades, 54.31% winners and an average of +0.45% a trade against +0.25% for the S&P 500 over the same days. His “Average Annual ROI” of 36.40% is the average trade times 365 over the 4.55 “periods” a trade (0.45 times 365 over 4.55 is 36.1), so the periods are calendar days and the ROI a per-trade return scaled to a year. The uptrend filter raised the winners from 54% to 57%; with both filters (Figure 5) 338 trades, 59.17% winners and +0.78% against +0.35%. His Figures 6 to 8 split 2002 to 2008 into bull, bear and trading-range periods picked by hand from a long-term chart (56.2% to 69.7% winners in the bull periods, 12% to 55% in the bear ones). All in sample, on his current list of stocks, and the volume condition was not tested apart. Evidence, not a reference.
- Pinned by
TestRecipeT6DecliningVolumePullback; runnable exampleExampleSeries_ConsecDown_decliningVolumePullback.
N of M
NofDown
func (s Series) NofDown(n, m int) bool
NofDown reports whether at least n of the last m comparisons were lower than the preceding value. Negative n or m, n > m, or unavailable history (m >= len(s)) returns false. With n=0 and m=0, a nonempty receiver returns true.
NofDownS
func (s Series) NofDownS(n, m int) Series
NofDownS returns aligned 0/1 flags for at least n of the last m comparisons being lower than the preceding value. Negative n, m <= 0, n > m, and unavailable history yield zeros. Positions before m also remain zero.
NofUp
func (s Series) NofUp(n, m int) bool
NofUp reports whether at least n of the last m comparisons were higher than the preceding value. Negative n or m, n > m, or unavailable history (m >= len(s)) returns false. With n=0 and m=0, a nonempty receiver returns true.
NofUpS
func (s Series) NofUpS(n, m int) Series
NofUpS returns aligned 0/1 flags for at least n of the last m comparisons being higher than the preceding value. Negative n, m <= 0, n > m, and unavailable history yield zeros. Positions before m also remain zero.
Compositions
The psychological line counts up closes among the last n, the count NofUpS thresholds. Its count is NaN before its first valid index and on every window holding a missing change, through MaskNonFinite; its flags read 0 there and are decided at bar t’s close. No average is involved, so no seed is chosen. Poster’s trend persistence rate under “Momentum” is a related up-minus-down count, and the overnight-legs page under “QStick” records the same bar-0 trap of c.GT(c.Shift(1)). Prices are positive and finite and lengths are in bars.
Psychological line (Valcu)
Dan Valcu (“The Spearman Indicator For Technical Analysis”, Stocks & Commodities V29:2, February 2011, pp. 34-41) describes the psychological line, “primarily used by Japanese technicians”, by what it counts: “It determines how many times price rises and falls during a time span of N bars”, and “for N = 12, a value of 9 is a warning for an approaching top and a 3 announces a low”. His chart plots the raw count with lines at 3 and 9. t6PsychologicalLine counts the one-bar changes above 0, so a flat close is not up, and returns the count, its percentage and his two alerts, at or beyond the levels (“reaches 3 and 9”).
d := c.Delta(1)
count := d.GT(0).Sum(n).MaskNonFinite(d.Sum(n)).SetN(n, math.NaN())
psy := count.Mul(100).Div(float64(n))
top := count.GTE(float64(hi))
low := count.LTE(float64(lo))- Inputs: the close
c, the lengthnand the alert countsloandhi(12, 3 and 9);countis an integer from 0 to n,psya multiple of 100/n andtopandloware 0 or 1. Arch-identical. - First valid index: n (12), the first bar with n changes; before it
Sumcounts partial windows. A missing close makes its own change and the next NaN, andMaskNonFinite(d.Sum(n))makescountNaN on the n + 1 windows that hold them; the windows then recover. - Anchors:
c.NofUpS(hi, n)equalstopbit for bit from bar n (c.NofUpS(k, n)iscount.GTE(k)for any k), andlowisc.NofUpS(lo+1, n)subtracted from 1.c.UpDownSum(n)equals the up count less the down count bit for bit, and2 count - nonly on windows without a flat close.psyis pandas-ta’spslarithmetic, 100 times the count over the length in one division, bit for bit from bar n. - Traps: the dossier’s
count.Mul(100.0 / float64(n))rounds 100/n first and misses the counts 5 and 10 at n = 12 by an ulp;Mul(100).Div(float64(n))is correctly rounded.c.GT(c.Shift(1))counts bar 0 as up, since theShiftpad is +0, wherec.Delta(1).GT(0)does not; the mask hides the difference here, not in an unmasked count. pandas-ta andNofUpSread a missing close as two changes that are not up. Thresholds onpsyare exact at n = 12 (25 and 75) but can fall between counts at other n; the alerts on the count are exact. - Variant: pandas-ta’s form with the open counts white candles,
c.Sub(o).GT(0).Sum(n).Mul(100).Div(float64(n)).SetN(n-1, math.NaN()), valid from n - 1: a different quantity, the share of bars that close above their own open. - Evidence: Valcu’s one chart (the S&P 500, 1997 to 2010, captioned weekly and titled monthly) shows the alerts at 3 and 9 and no trades. On a driftless walk without flat closes the alerts fire on about 15% of bars, the binomial chance of a count of at most 3 or at least 9 of 12 (0.146). Evidence, not a reference.
- Pinned by
TestRecipeT6PsychologicalLine; runnable exampleExampleSeries_NofUpS_psychologicalLine.
Up / Down
UpDown
func (s Series) UpDown(n int, tolerance ...float64) Series
UpDown returns a series of numbers where each 2 bits represents an up bar (2), a down bar (1), or a flat bar (0), for the preceding n bars. Optionally, a tolerance (in the form of value that represents a percentage) may be provided. For example, a tolerance of 5 dictates that a value must be 5% higher/lower than its predecessor to register as an up/down. The maximum number of values to look back to is 24. All three direction APIs use threshold = abs(previous)*tolerance/100: change > threshold is up, change < -threshold is down, otherwise flat. Negative, NaN, or infinite tolerance yields aligned zeros.
The tolerance is a percentage of the previous value’s magnitude, so 0.05 means 0.05%, not 5%; the threshold is |previous|*tolerance/100, multiplied before it is divided, and the comparison is strict, so a change equal to the threshold is flat. The warm-up reads 0, not NaN: bars 0 to n-1 are 0, which is also a real code, n flat bars.
The newest change is in the low two bits, so a down change followed by an up change is 1*4 + 2 = 6, and UpDownString prints the codes oldest first, “U” for up, “D” for down and “-” for flat (its “-” is a flat change, not Crabel’s sign for a down close). Crabel’s close-and-open pattern codes are listed under “Compositions” in the “Up / Down” category.
UpDownString
func (s Series) UpDownString(n int, tolerance ...float64) []string
UpDownString returns a string slice representing whether the last n values, when compared with the previous value, were up (“U”), down (“D”), or flat ("-"). Optionally, a tolerance (in the form of value that represents a percentage) may be provided. For example, a tolerance of 5 dictates that a value must be 5% higher/lower than its predecessor to register as an up/down. The maximum number of values to look back to is 24. A period outside the valid range 1..24, or an invalid (negative, NaN, or infinite) tolerance, yields an aligned slice of empty strings; a valid period and tolerance produce ordinary warm-up strings, including flat ("-") symbols where applicable.
The tolerance is a percentage, as in UpDown, compared strictly against |previous|*tolerance/100, multiplied before it is divided, so 0.05 means 0.05%, not 5%, and a change equal to the threshold is flat.
UpDownSum
func (s Series) UpDownSum(n int, tolerance ...float64) Series
UpDownSum returns a series which is a sum of +1 values for each bar that was higher than the previous bar and -1 for each bar that was lower than the previous bar, for n bars. For example, if n is 6 and 4 bars are higher than their previous, 1 is equal, and 1 is lower, the value at that bar would be +3. Optionally, a tolerance (in the form of value that represents a percentage) may be provided. For example, a tolerance of 5 dictates that a value must be 5% higher/lower than its predecessor to register as an up/down. UpDownSum uses the same mutually exclusive up/down/flat classification as UpDown (see UpDown). A negative, NaN, or infinite tolerance yields an aligned zero-filled result. Unlike UpDown and UpDownString, UpDownSum has no upper bound of 24 on n.
The tolerance is a percentage, as in UpDown, compared strictly against |previous|*tolerance/100, so 0.05 means 0.05%, not 5%. The warm-up reads 0, not NaN: bars 0 to n-1 are 0, which is also a real balance.
Compositions
Patterns of up and down closes, and of the open against the prior close, coded with UpDown: two bits a change, an up change 2, a down change 1 and a flat one 0, the newest change in the low two bits. Recipe values, levels and results are NaN before their first valid index (SetN(F, math.NaN())), and flags, states and latches read 0 there: the code is masked, since its unmasked warm-up holds real codes, and every shifted series a pad can satisfy is masked, or the flag is. Prices are positive and finite, on daily bars, and series are paired by position (the caller aligns them by timestamp). The patterns read the open o: each is known only at the open, which is also the entry, so a trade from the open to the close of that bar is a bar-level result, not a Position latch, which does not test an exit on its entry bar. Such results read 0 or -0 off the signal bars, the warm-up included, so their totals are a plain res.CumSum(), where a result with a NaN head would take res.SetN(F, 0).CumSum(). The opening-range members rest a stop a fixed distance beyond the open, touched when the high reaches it (GTE), so their fill is the level, the max(o, level) of a buy stop above the open; that level is a decimal sum, so a high exactly on it is decided by binary rounding. 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.
Close-and-open patterns (Crabel)
Toby Crabel (Day Trading with Short Term Price Patterns and Opening Range Breakout, Traders Press, 1990, ch. 5 “Price Pattern Studies I (Bonds open to close)”, pp. 35-38) writes “( + ) symbolizes an up closing; ( - ) a down closing relative to the previous day. The exception is the sign on the far right, which represents the open of that day and the point of entry for the trade.” His System #5, ‘- - -’, the first pattern of his Chart B, “is two lower closes with a lower open”, bought on that open and “exited on the close of the third day”; “No stops were used in the tests.” t6CrabelPattern reads a pattern of n signs as a base-4 code, the oldest close sign first with ‘+’ 2 and ‘-’ 1 and the open’s digit last, and returns the code, the flag for code v and the open-to-close result in points.
cc := c.UpDown(n - 1).Shift(1)
od := o.GT(c.Shift(1)).Mul(2).Add(o.LT(c.Shift(1)))
code := cc.Mul(4).Add(od).SetN(n, math.NaN())
sig := code.EQ(v)
res := c.Sub(o).Mul(sig)- Inputs: opens and closes; n, the signs in the pattern (n - 1 closes and the open; 2 to 5 in his tables), and
v, the pattern’s code: ‘+ +’ 10, ‘+ -’ 9, ‘- +’ 6, ‘- -’ 5; ‘- - -’ 21, ‘+ + -’ 41, ‘+ + +’ 42, ‘- - +’ 22; ‘- - - -’ 85. A sell pattern’s result isres.Mul(-1). - First valid index: n for
code(NaN on bars 0 to n-1) and forsig(0 there, since NaN equals no code).resreads 0 or -0 before n, not NaN, becausesigis 0 there; it is NaN only where a price is. Unmasked, the code holds real codes on bars 0 to n-1: bar 0 reads 2, the open against the zero pad, which for n = 2 is the code of a flat close and a higher open. - Anchors: on the eight constructed bars (closes 10, 9, 8, 7, 8, 9, 9, 8; opens 10, 9.5, 8.5, 7.5, 7.8, 8.5, 9.5, 8.5; n 3, v 21)
codereads [NaN NaN NaN 21 22 26 42 33],sig[0 0 0 1 0 0 0 0] andres[0 -0 -0 -0.5 0 0 -0 -0]: bar 3 is ‘- - -’, bar 6 ‘+ + +’, and bar 7, after an unchanged close, 33 (digits 2, 0 and 1).UpDownputs the newest change in the low two bits (c.UpDown(2)reads [0 0 5 5 6 10 8 1], a down change then an up one being 1 * 4 + 2 = 6), so the code reads Crabel’s signs left to right as base-4 digits, andcode.EQ(v)equals the product of the n sign flags, masked, for all eight 3-sign patterns, bit for bit (the codes are small integers). A flat close or an open equal to the prior close is a 0 digit and matches no pattern (Crabel says nothing of either), and a NaN close reads flat inUpDown, so it completes no pattern. The members’c.ConsecDown().GTE(k)equalsc.NofDownS(k, k)bit for bit on every bar, the warm-up included (both read 0 before bar k), as on the Consecutive paget6-declining-volume-pullback.md. - Traps:
UpDownStringprints “-” for a flat change and “D” for a down one, the reverse of Crabel’s signs (c.UpDownString(2)reads “DU” on bar 4 of the eight bars and “-D” on bar 7, oldest first). The dossier’sc.UpDown(k-1, 0)without.Shift(1)reads today’s close, unknown at the open (look-ahead), and itso.GT(c.Shift(1))alone is a binary digit that merges an equal open with a lower one. Table A enters on the open itself; the entries a fixed amount off the open are his opening-range chapters. An explicit product with a ‘+’ close sign compares a close with the zero pad on bars 0 to n-1 (unmasked, ‘+ + -’ fires on bar 2 against the zero pad whenever bar 1 closes up and bar 2 opens below that close), so its lags need masks, where theConsecUpform needs none. Davey’s #32 is notInsideBars, which compares inclusively with the prior non-inside mother bar: theInsideBarsreading of his rule adds signals (ties and mother-bar cases) and misses none of his, since his strict previous-bar set is a subset. WithUpDown’s default tolerance only an exactly equal close is flat: on decimal prices a one-cent change is a sign. - Variant: the product form,
c.ConsecDown().Shift(1).GTE(2).Mul(o.LT(c.Shift(1))), equalssigat v = 21 bit for bit with no mask (ConsecDownreads 0 on its warm-up); ‘+ + -’ isc.ConsecUp().Shift(1).GTE(2).Mul(o.LT(c.Shift(1))). - Variant: Connors and Alvarez’s S&P Short Strategy (Short Term Trading Strategies That Work, 2008, ch. 12, pp. 106-108), “The SPY (or the SPX) is below the 200-day MA.”, “The market closes up 4 or more days in a row.”, “Sell on the close.” and “Cover your short position when the SPY closes under its 5-period moving average.”, is
c.PositionSigned(nil, nil, c.LT(c.Average(200)).Mul(c.ConsecUp().GTE(4)), c.LT(c.Average(5))), first valid 199 with no mask (Averagepasses the close through before, andc.LT(c)is 0); the cover cannot fire on the entry bar, since four rising closes put the close above its 5-bar mean. - Members:
- Connors and Alvarez’s runs of down closes:
c.ConsecDown().GTE(k), which isc.NofDownS(k, k)bit for bit; - their lower lows and higher highs:
l.ConsecDown().GTE(k)andh.ConsecUp().GTE(k)(a flat bar resets a run); - Davey’s #32: bar t-1 strictly inside bar t-2 (masked
Shifts), then a close past its high or low; notInsideBars; - Davey’s other entries (#17, #29 to #31, #33, #37): strict comparisons of lags masked with
SetN, for the next open; - Crabel’s opening-range buy:
fill := sig.Mul(h.GTE(o.Add(k))),c.Sub(o.Add(k)).Mul(fill), k in price units: withsetup.Shift(1)forsigand k for x, thebuyResoft6CrabelORBonbar-classification/t6-narrow-range-bars.md, bit for bit; - his close-to-close patterns:
cc0 := c.UpDown(n).SetN(n, math.NaN()),c.Sub(c.Shift(1)).Mul(cc0.EQ(v).Shift(1)).
- Connors and Alvarez’s runs of down closes:
- Missing piece: a table keyed by every code at once, Crabel’s tables in one call, needs a keyed mean, the unshipped
StateMean; one pattern at a time isres.AverageWhere(sig, 0)(the average result in points, expanding) andc.Sub(o).GT(0).AverageWhere(sig, 0)(the fraction profitable, a draw counted as a loss). - Evidence: Table A (T-bond futures, 1978 to 1987, open to close, gross of commission and slippage, the side of each pattern chosen by its gross profit, in sample): ‘- - -’ was bought 277 times, 58% profitable, for 37,615 dollars, and some of the table’s average-loss cells are unreliable scans; in his 3-day opening-range table (bonds 1978 to 1986, S&P 1982 to 1988) ‘- - -’ is a buy above the open in bonds and a sale above the open in the S&P. Connors and Alvarez’s S&P short made 16 trades, 68.75% correct, for 169.91 S&P points from 1995 to 2007; in How Markets Really Work (2nd ed., 2012, chs 3-4; SPX, 1989 to 2011) the index rose 0.64% on average in the week after three down days (529 cases). Davey (Entry and Exit Confessions of a Champion Trader, 2019) prints no statistics for his entries, and his #29 short carries a stray “l” (his plain-English rule gives
o[1] < l[0]). Evidence, not a reference. - Pinned by
TestRecipeT6CrabelPattern; runnable exampleExampleSeries_UpDown_crabelPattern.
Bar Classification
InsideBars
func (s Series) InsideBars(args ...Series) []bool
InsideBars returns a slice of booleans indicating whether each bar is an inside bar. An inside bar is a bar whose high and low are within the range of the prior non-inside bar (the engulfing bar). The first bar is never an inside bar. The function accepts either two Series (high, low) or four Series (open, high, low, close). Only the high and low are used; providing open and close is accepted for call-site convenience when working with OHLC data.
OutsideBars
func (s Series) OutsideBars(args ...Series) []bool
OutsideBars returns a slice of booleans indicating whether each bar is an outside bar. An outside bar is a bar whose high is above the prior bar’s high and whose low is below the prior bar’s low. The first bar is never an outside bar. The function accepts either two Series (high, low) or four Series (open, high, low, close). Only the high and low are used; providing open and close is accepted for call-site convenience when working with OHLC data.
Compositions
The narrow-range recipes below classify single bars, or k-bar windows, by their high-low range: the range is h - l, not the true range, unless a section says otherwise. The comparisons are strict, as Crabel’s “narrower than” and “larger than any”, and ties are common on tick data: on ranges drawn at random from 0 to 5 ticks a strict NR4 flags 17% of bars against 34% for the non-strict reading. The flags are exactly 0 or 1, 0 before their first valid index and 0 where a compared range is NaN; levels are NaN before theirs. InsideBars is a different rule from the strict one-bar inside day used here: it is non-strict, also counts a bar inside an earlier engulfing (mother) bar, and returns []bool. The trades that follow these bars are opening-range breakouts on the next day, whose order of fills needs intraday data and is outside the package, except Crabel’s test of each side (“Opening-range breakout test (Crabel)”). Unlike D’Errico’s VoltyRatio (trade-state/t2-latched-entries.md) these are ordinal tests of single-bar or k-bar ranges against the ranges before them, with no ratio and no latch; the range ratios of D’Errico and Zaffino, Unger and Saitta are on true-range/t6-range-contraction.md.
Narrow-range and wide-spread bars (Crabel)
Toby Crabel (Day Trading with Short Term Price Patterns and Opening Range Breakout, Traders Press, 1990, ch. 15, pp. 105-112; “The Principle Of Contraction/Expansion”, Stocks & Commodities V8:9, September 1990, pp. 327-331) defines NR4 as “The narrowest daily range relative to the previous three day’s daily ranges compared individually”, NR7 as “A day with a daily range that is narrower than the previous six day’s daily ranges compared individually”, and WS4 and WS7 as “A day with a daily range that is larger than any of the previous three day’s daily ranges” (six for WS7). t6NarrowRange reads them strictly, the bar’s range against each of the n - 1 ranges before it.
rg := h.Sub(l)
b := rg.Breakout(n-1, 1)
nr := b.EQ(-1)
ws := b.EQ(1)- Inputs: highs and lows; n >= 2 counts the current bar (4 for NR4 and WS4, 7 for NR7 and WS7).
- First valid index: n - 1 (0 before). A NaN range on the bar or in its window makes
BreakoutNaN, so both flags read 0 on that bar and the n - 1 bars after it. - Anchors: on the test’s 25-bar tie fixture (prices in quarters and eighths, exact in binary) NR4 is 1 at bars 3, 7, 8, 18, 19 and 20, NR7 at 8, 18, 19 and 20, WS4 at 4, 9, 21 and 24 and WS7 at 24.
rg.LT(rg.Shift(1).Lowest(n-1))equalsnrbit for bit on finite non-negative ranges (the pad’s 0 keeps the warm-up at 0), but not with +Inf in the window, whereBreakoutreads NaN. - Traps: a flat bar is narrow unless an earlier range in the window is also 0. The dossier’s
r.GT(r.Shift(1).Highest(6))for WS7 reads 1 at bar 0 whenever the range is positive and needs.SetN(6, 0). Crabel’s averages for NR4 (64%) and WS4 (54%) do not follow from Table A’s cells (62.6; and 53.4 when a cell marked with the reverse direction is read as 100 less the cell, his convention, on which the control’s 57%, WS7’s 52% and NR7’s 64% reproduce). - Variant: the non-strict “narrowest daily range of the last four days” (Street Smarts),
rg.LTE(rg.Lowest(n)).SetN(n-1, 0): it adds bars 14, 17 and 23 of the fixture, and unmasked it reads 1 at bar 0. - Variant: Moore Research’s WR7, “the widest range of the last seven days” (Street Smarts, Appendix),
rg.GTE(rg.Highest(7)).SetN(6, 0)(bars 12 and 24 of the fixture), with a close reading the source does not define: above the previous close,c.GT(c.Shift(1)), or in the upper half of the bar,c.Sub(l).Mul(2).GT(rg). - Evidence: Crabel’s text on Table A (p. 108) gives average success rates of 57% for any day, 64% after NR4 and NR7, 54% after WS4 and 52% after WS7; he totals 710,000 dollars of gross profit on 7,313 trades after contractions against 102,000 dollars on 7,524 after expansions (the expansion totals reproduce from Tables C and E, 4,713 and 2,811 trades and 102,019 dollars, once their reverse rows count negative, as his text says: “profits indicated as a negative if the trade reflected other than a buy above the open or a sale below the open”; unsigned they sum to 337,451 dollars; the contraction count misses by 10 and the gross by 8,841 dollars). Gross, with no costs. Moore Research’s WR7 exhibits (Street Smarts, pp. 212-213) favour a close in the opposite direction the next day in most markets. Evidence, not a reference.
- Pinned by
TestRecipeT6NarrowRange; runnable exampleExampleSeries_Breakout_narrowRange.
Inside day and ID/NR4 (Crabel; Raschke and Connors)
Crabel (“Opening range breakout Part 4”, Stocks & Commodities V7:2, February 1989, pp. 47-49): “an inside day’s high is less than the previous day’s high and the inside day’s low is greater than the previous day’s low”. Raschke and Connors (Street Smarts, M. Gordon, 1995, ch. 19, pp. 153-159) combine it with NR4: “Combining the two conditions sets up an ID/NR4 day”, and Thomas Bulkowski adds “No ties are allowed” (“Tiny Chart Patterns: Stinkers Or Stalwarts?”, Stocks & Commodities V22:4, 2004, pp. 44-48). t6InsideDayNR4 gives the strict one-bar inside day and ID/NR4.
id := h.LT(h.Shift(1)).Mul(l.GT(l.Shift(1)))
nr4 := h.Sub(l).Breakout(3, 1).EQ(-1)
idnr4 := id.Mul(nr4)- Inputs: highs and lows with l <= h.
- First valid index: 1 for
id(bar 0 is 0: theShiftpad would need h < 0 < l), 3 foridnr4(0 before). - Anchors: on the tie fixture
idis 1 at 5, 6, 11, 13, 17, 19, 22 and 23 andidnr4at 19 only, whileInsideBarstimes NR4 is 1 at 3, 7, 8, 18, 19 and 20. Two counterexamples separate the rules: h = [10, 10] and l = [5, 6] giveInsideBars[false true] andid[0 0] (a tie); h = [10, 9, 9.5] and l = [5, 6, 5.5] give [false true true] and [0 1 0] (a mother bar). A strict inside day is narrower than the day before, soidnr4equalsid.Mul(rg.LT(rg.Shift(2))).Mul(rg.LT(rg.Shift(3))).SetN(3, 0)bit for bit. - Traps:
InsideBarsreturns[]boolunder the mother-bar rule, so it is neither this rule nor a straight-line composition. The dossier’s ID/NR4 withH <= H[1]is non-strict, and itsrng.LTE(rng.Lowest(4))reads 1 at bar 0. Raschke’s next-day levels,h.Shift(1).Add(tick)andl.Shift(1).Sub(tick), compose, but her same-day reversal (“we will reverse and go short”) needs the intrabar order and is outside the package. - Variant: Crabel’s glossary inside day, “an (NR) day which has its daily range completely within the previous daily range”, which allows one tied extreme and needs a smaller range:
h.LTE(h.Shift(1)).Mul(l.GTE(l.Shift(1))).Mul(rg.LT(rg.Shift(1)))withrg := h.Sub(l)(it adds bars 2, 3, 7, 8, 10, 14, 18 and 20 of the fixture). - Evidence: Crabel’s ID/NR4 tables (book, ch. 22, pp. 155 and 158): the opening-range breakout after ID/NR4 beat the same breakout on any day in 14 of 16 tests, tied once (S&P, open plus 80) and lost once (cattle, open plus 50), and beat all three other groups in 8 of 16 (Crabel’s “Eight of sixteen”); his “In 15 of 16 tests the ID/NR4 showed a higher probability of success” counts the tie (his NR4 chapter’s “15 of 16” is the NR4 column, whose one exception is the cattle buy 50 points above the open). Evidence, not a reference.
- Pinned by
TestRecipeT6InsideDayNR4.
k-bar narrow range (Crabel)
Crabel’s glossary (book, pp. 282-285): “2 BAR NR (2BNR) - is the narrowest range from high to low of any two day period relative to any two day period within the previous twenty days”, and likewise three days within twenty, four within thirty and eight within forty; chs. 24 and 25 test the 2-bar and 3-bar forms with an opening-range breakout. t6KBarNR reads “within the previous w days” as every k-day period wholly inside them, the periods that overlap the current one included: the m = w - k + 1 periods ending one to m bars back.
rk := h.Highest(k).Sub(l.Lowest(k))
nrk := rk.Breakout(w-k+1, 1).EQ(-1).SetN(w, 0)- Inputs: highs and lows; (k, w) = (2, 20), (3, 20), (4, 30) or (8, 40).
- First valid index: w (0 before):
HighestandLowestare partial on bars 0 to k - 2, so the reference windows first hold only full k-bar ranges at w. - Anchors: on the tie fixture the 2-bar and 3-bar NR within 20 bars fire at bar 20 only, and on the test path the flag equals a loop of “narrower than every k-bar range wholly inside the previous w bars” for all four settings. The other two readings differ on the fixture: m = w periods ending 1 to w bars back (the earliest starting before the window),
rk.Breakout(w, 1).EQ(-1).SetN(w+k-1, 0), gives none, and m = w - k (the diagram’s 20 bars including the pair),rk.Breakout(w-k, 1).EQ(-1).SetN(w-1, 0), gives bars 19 and 20. - Traps: without the
SetN(w, 0)the 2-bar NR also fires at bar 19 of the fixture, whose window holds bar 0’s partial range. “Narrowest” leaves ties open (strict here, as NR4). The dossier’sr2.Breakout(19, 1).EQ(-1)lacks the mask, and its sweep’sBreakout(20, 1)withSetN(21, 0)is another window. - Evidence: Crabel’s 2-bar NR tests (ch. 24; “2-bar NR and ORB”, Stocks & Commodities V8:2, February 1990, pp. 70-73) over four exits; chart examples only. Evidence, not a reference.
- Pinned by
TestRecipeT6KBarNR.
Doji/NR4 (Crabel)
Crabel’s Doji is “the difference between the open and close of yesterday is less than 8 tics (8 tic Doji)” (ch. 18; “Opening Range Breakout Part 3”, Stocks & Commodities V6:12, December 1988, pp. 462-465), and his Doji/NR4 (ch. 21, p. 150) is “a Doji and the narrowest day in 4 days”, with a body limit per market (5 ticks in bonds, 50 points in the S&P, 3 cents in beans, 25 points in cattle). t6DojiNR4 takes the strict “less than”.
doji := c.RealBody(o).LT(d)
nr4 := h.Sub(l).Breakout(3, 1).EQ(-1)
dojiNR4 := doji.Mul(nr4)- Inputs: opens, highs, lows, closes;
dthe body limit in price units, per market. - First valid index: 0 for
doji, 3 fordojiNR4(0 before). - Anchors: on the tie fixture with d 0.5
dojiis 1 at 3, 7, 8, 11, 14, 17 to 20, 22 and 23, anddojiNR4at 3, 7, 8, 18, 19 and 20;RealBodyisc.Sub(o).Abs()bit for bit. - Traps: the table legends’ “maximum difference” reads as
LTE, which adds the bodies of exactly 0.5 at bars 2, 10 and 13. Decimal ticks decide at random: |100.15 - 100.10| is 0.05000000000001137, which failsLT(0.05), while |100.05 - 100.10| is 0.04999999999999716, which passes; for “less than k ticks” pass the half-tick threshold, (k - 0.5) times the tick, as a number. On binary ticks (1/32, 1/8) a body is exact. - Evidence: Crabel’s Doji/NR4 Table A (p. 150), for example bonds sold 8 ticks below the open after a 5-tick Doji/NR4: 115 trades, 65% profitable. The “203 trades, 71%” row often quoted for it is ch. 18’s 8-tick Doji alone. Evidence, not a reference.
- Pinned by
TestRecipeT6DojiNR4.
Opening-range breakout test (Crabel)
Crabel’s tables test each side on its own: “trades were taken off the open at a predetermined level above and below the open” (ch. 15, p. 105), out on the close with no stop, and “if the market traded both above and below the opening range the given amount, both a buy and a sell could be registered” (“Opening range breakout Part 4”). t6CrabelORB gives each side’s result on the day after a setup: a stop x above or below the open, filled when the day’s high or low reaches it, out on the close, gross. Williams’s volatility breakout on true-range/t6-volatility-breakout-levels.md rests the same stop with x a fraction of yesterday’s range and no setup, always in the market; Crabel’s close-and-open patterns on up-down/t6-crabel-patterns.md gate the same level by a pattern flag known at the open.
armed := setup.Shift(1)
buyRes := c.Sub(o.Add(x)).Mul(armed.Mul(h.GTE(o.Add(x))))
sellRes := o.Sub(x).Sub(c).Mul(armed.Mul(l.LTE(o.Sub(x))))- Inputs: opens, highs, lows, closes;
setupa flag of this page (NR4, ID, ID/NR4, Doji/NR4);xthe offset in price units (8 and 16 ticks in bonds, 80 and 160 points in the S&P in the tables). - First valid index: 1 (bar 0 reads 0: the shifted setup is 0 there).
- Anchors: on the tie fixture with the NR4 setup and x 0.5 both sides are registered on bars 4, 9 and 21, with
buyRes3, 1.75 and 0.25 andsellRes-4, -2.75 and -1.25 there; on the test path both equal a loop of each side bit for bit. - Traps: a side not taken reads its difference times 0, which is -0 when the difference is negative: add or compare the results, do not test the sign bit. The tests used the open price and fixed offsets, not the opening range (the first 30 seconds to 5 minutes) or the Stretch. The opening-range breakout as one position (“The first stop that is traded is the position and the other stop is used as a protective stop”) needs the order of the two touches and is outside the package.
- Variant: Crabel’s Stretch in place of x, “the previous ten days and averaging the sum of the differences between the open for each day and the closest extreme to the open on each day”:
h.Sub(o).Minimum(o.Sub(l)).Average(10).Shift(1).SetN(10, math.NaN()), read on the trade day. - Evidence: this test produced Crabel’s Tables A to E and his ID/NR4 and Doji/NR4 tables (the sections above). Evidence, not a reference.
- Pinned by
TestRecipeT6CrabelORB.
Narrow-true-range open breakout (Pruitt)
George Pruitt (The Ultimate Algorithmic Trading System Toolbox, Wiley, 2016, ch. 3, Box 3.6, p. 111) prints, as pseudocode, “If yesterday’s True Range < MAV(TR,10), then CanTrade = True”, “If yesterday’s close >= prior day’s close, then CanBuy = True” and “buy 2 units tomorrow at open +0.2 *MAV(TR,10) stop”. t6NarrowTRBreakout gives the gate, the side and the two stop levels on the trade day, the average read at yesterday’s close, yesterday’s true range included.
tr := c.TrueRange(h, l, c)
atr := tr.Average(n)
can := tr.LT(atr).Shift(1)
long := can.Mul(c.GTE(c.Shift(1)).Shift(1)).SetN(2, 0)
short := can.Mul(c.LT(c.Shift(1)).Shift(1)).SetN(2, 0)
buyLvl := o.Add(atr.Shift(1).Mul(f)).SetN(n, math.NaN())
sellLvl := o.Sub(atr.Shift(1).Mul(f)).SetN(n, math.NaN())- Inputs: opens, highs, lows, closes; n 10, f 0.2.
- First valid index: 2 for
longandshort(0 before; the mask is redundant, sinceatr[0]istr[0]), n forbuyLvlandsellLvl(NaN before: the average passes the true range through before n - 1). - Anchors: on the tie fixture
longis 1 at 12, 15, 19, 21 and 24,shortat 11, 14, 18, 20 and 23,buyLvl[10]11.5 andsellLvl[10]10.5;tr.Average(n)isc.ATR(h, l, c, n)bit for bit, andlongandshortdo not fire on one day. - Traps: the box mixes “yesterday” and “tomorrow”, and its short branch ends “exitLong at entryPrice stop or MOC”. A fill is
long.Mul(h.GTE(buyLvl))at the level (above the open, so no gap through it); the scale-out at the entry plus 4.00, the breakeven stop and the exit on the close are intraday and outside the package. - Variant: Kaufman’s 3-day true-range compression (“A Fresh Look At Short-Term Patterns”, Stocks & Commodities V39:1, January 2021), “the most recent 3 days must each have a true range smaller than the 4th previous day”,
tr.Highest(3).LT(tr.Shift(3)): the Zorro code of the Traders’ Tips bit for bit from bar 3 (bars 3, 7, 8, 15, 19 and 20 of the fixture). - Evidence: an equity curve only (Figure 3.9, mini Russell, 5-minute bars, 2005 to 2015). Evidence, not a reference.
- Pinned by
TestRecipeT6NarrowTRBreakout.
Acme N narrow-range condition (Conway and Behle)
Mark Conway and Aaron Behle (Professional Stock Trading: System Design and Automation, Acme Trader, ch. 7, pp. 135-151) set a narrow-range condition of five tests, coded in Example 7.1 and the functions of ch. 11 as AcmeNarrowRange(NLength1, 1) and AcmeNarrowRange(NLength1, 0)[1], two strict inside days, AcmeNarrowRange(NLength2, 0), AcmeInsideDayNR(NLength3, 0) and AcmeRangeRatio(RatioLength1, RatioLength2) <= MaxRangeRatio, where a narrow range is Range < Lowest(Range, RangeLength - 1)[Index + 1] and the range ratio divides Average(Range, 1) by Average(Range, 7). t6AcmeN is the code’s reading.
rg := h.Sub(l)
rr := rg.Div(rg.Average(7)).SetN(6, math.NaN())
m4 := rg.Lowest(4).Shift(2)
nr25 := rg.LT(m4).Mul(rg.Shift(1).LT(m4)).SetN(5, 0)
id := h.LT(h.Shift(1)).Mul(l.GT(l.Shift(1)))
id2 := id.Mul(id.Shift(1))
nr10 := rg.Breakout(9, 1).EQ(-1)
idnr4 := id.Mul(rg.Breakout(3, 1).EQ(-1))
cond := nr25.Add(id2).Add(nr10).Add(idnr4).Add(rr.LTE(0.7)).GT(0)- Inputs: highs and lows; the code’s defaults (NR5 twice, NR10, ID/NR4, a 1:7 ratio at most 0.7).
- First valid index: 6 for
rr(NaN before), 9 forcond: each term is 0 before its own first valid index (nr255,id22,nr109,idnr43,rr6), so the union is complete from 9. - Anchors: on the tie fixture
condis 1 at 6, 7, 8, 11, 14, 17 to 20 and 23 andrr[6]is 28/41 bit for bit; a range of exactly 0.7 of its 7-bar mean (six ranges of 10.5 and one of 7) readsrr0.7 and fires through the code’s<=. - Traps: the prose differs from the code four times: it divides by an “Average True Range” (the code by the mean high-low range including the bar), tests “less than 0.7” (the code
<=), asks for “an NR5 bar and the previous bar an NR5 bar” (the coded pair, both narrower than the four bars before it, contains that rule) and says “narrowest” and “inside day” where the code tests strictly against the bars before (Range < Lowest(...),Low > Low[1] and High < High[1]), not a mother-bar inside bar.Div’s zero is the code’s own guard (If Range1 <> 0 and Range2 <> 0, else 0): a zero mean or range gives 0, which passes<= 0.7, so a flat bar is a narrow-range condition in both. - Missing piece: the NR% bar and the entry level need TradeStation’s
Volatility(20), which its help describes only as “a smoothed average of the TrueRange” weighted to the latest bar (the recursion is in the platform’s function source, not in the help); with a Wilder stand-in,rg.LTE(c.ATRWilder(h, l, c, 20).Mul(0.7))andh.Add(c.ATRWilder(h, l, c, 20).Mul(0.1)), not pinned. The trade filters (price, ADX, a 14-day volatility annualised by the square root of 365) belong to the system. - Evidence: Tables 7.2 to 7.4: on daily Nasdaq Composite bars from 1994 to 2002 the filters cut the trades from 291 to 140 and raised the profit factor from 2.01 to 2.91; costs not stated. Evidence, not a reference.
- Pinned by
TestRecipeT6AcmeN.
Bars classified against the bar before them: an outside day has a higher high and a lower low than the previous bar, and an inside day a lower high and a higher low. OutsideBars and InsideBars return []bool, which a straight-line recipe cannot combine with series, so the recipes write each flag as two comparisons with masked shifts (a Series-returning outside or inside bar is not missing: each is two comparisons). Recipe values, levels and results are NaN before their first valid index (SetN(F, math.NaN())), and flags, states and latches read 0 there: the shifted highs and lows are masked, so no pad can satisfy a comparison. Prices are positive and finite, on daily bars, and series are paired by position (the caller aligns them by timestamp); weekday keys are the caller’s (the Comparison intro’s “Calendar keys” table). Kaufman’s recipe reads highs, lows and closes and trades on the close; only Williams’s recipe reads the open o, buying at it, and rests an order: his stop, touched by the low, fills at min(o, level) on a gap through it, the mirror of a buy stop’s max(o, level), which PositionSignedExitValue posts from the rules’ Open. A close exactly on a decimal threshold such as 75% or 25% of the range is decided by binary rounding in every float form, and the page pins Kaufman’s own arithmetic. 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.
Outside day with the close in the top or bottom quarter (Kaufman)
Perry J. Kaufman (“A Fresh Look At Short-Term Patterns (With And Without A Trend Filter)”, Stocks & Commodities V39:1, January 2021, pp. 9-15) tests “Outside days, a higher high and a lower low, but the close in the upper or lower 25% of the range. We buy if upper, sell if lower.”, entered “on the close of the day of the reversal”, and his listing is strict: close > 0.75*(high - low) + low and close < 0.25*(high - low) + low. His trend filter, “We will buy only when the moving average is up and sell when it is down”, is the slope of the 80-day average on the signal bar, trend > trend[1] and trend < trend[1]. t6OutsideDay returns the bull and bear outside days and the average’s rising and falling flags.
hp := h.Shift(1).SetN(1, math.NaN())
lp := l.Shift(1).SetN(1, math.NaN())
out := h.GT(hp).Mul(l.LT(lp))
rng := h.Sub(l)
bull := out.Mul(c.GT(rng.Mul(1 - q).Add(l)))
bear := out.Mul(c.LT(rng.Mul(q).Add(l)))
ma := c.Average(n).SetN(n-1, math.NaN())
up := ma.GT(ma.Shift(1))
dn := ma.LT(ma.Shift(1))- Inputs: highs, lows and closes;
q0.25, the quarter (1 - 0.25is exactly 0.75 in binary64, sorng.Mul(1 - q)is his0.75*(high - low)), and n 80, the trend average’s length. - First valid index: 1 for
bullandbear(0 on bar 0, where the masks put NaN in place of the pads) and n forupanddn(0 before:mais NaN on bars 0 to n-2, so its first one-bar change is on bar n). The masks onhpandlpare no-ops on positive prices, where the test asserts the masked and unmasked flags equal; they decide only a bar 0 that straddles zero. The mask onmais not a no-op: unmasked,Averagepasses the close through on bars 0 to n-2, andupwould readc[t] > c[t-1]there. - Anchors:
outequalsh.OutsideBars(h, l), as 0 and 1, element for element on every bar, a bar 0 that straddles zero included (a NaN high or low fails both), while the unmaskedh.GT(h.Shift(1)).Mul(l.LT(l.Shift(1)))reads 1 on such a bar 0 (a high of 0.25 and a low of -1.25 against the zero pad);OutsideBarsreturns[]bool, so the comparison is the recipe and the identity is the test (all 1,094 bars of the test’s groups). No outside day has a flat range, since its high is above the previous high and its low below the previous low. At (low, high, close) = (10, 10.4, 10.3) the thresholdrng.Mul(1 - q).Add(l)is exactly the double 10.3 andbullreads 0, as exact arithmetic says of a close exactly at 75%, whilec.Stochastic(h, l, 1)reads 75.000000000000114, so itsGT(75)fires; at (10, 10.4, 10.1)bearreads 0 while the Stochastic reads 24.99999999999989 and itsLT(25)fires. On the test’s bars, bar 248 closes exactly on the 75% line and fires neither flag.ma.GT(ma.Shift(1))equalsc.GT(c.Shift(n))to rounding from bar n (in exact arithmetic the average’s change is the n-bar change over n). - Traps: the dossier’s
c.Stochastic(h, l, 1).GTE(75)andLTE(25)are inclusive where his code is strict, and read a rounded quotient: both fire on the two witnesses. At exact decimal 75% ties (200,000 constructed cent-priced bars) the strict flag fires wrongly on about 10% in his arithmetic and in Zorro’s0.25*l + 0.75*h, and on about 37% with the Stochastic or the division-freec.Sub(l).GT(rng.Mul(0.75)), so the page pins his. The trend is the average’s slope, not the close against it: Wealth-Lab’s Traders’ Tip testsc.GT(ma), a departure from his code. The strict previous-bar inside day of Street Smarts (“An inside day has a higher low than the previous day’s low and a lower high than the previous day’s high”),h.LT(hp).Mul(l.GT(lp)), is notInsideBars, which compares inclusively with the previous bar or with the prior non-inside “mother” bar: its inside days hold the strict ones and 20 more on the test’s bars, among them bar 253, inside the mother bar 251 but not inside bar 252, and bar 254, whose high equals the mother’s. The strict inside day with NR4, Raschke and Connors’s ID/NR4, is ont6-narrow-range-bars.md(“Inside day and ID/NR4 (Crabel; Raschke and Connors)”). - Variant: Kaufman’s case harness:
ent := c.Position(bull.Add(bear), nil, 5).Entries()gives his case bars (a case opens on the signal close and blocks new cases for five bars), equal to a replay of his listing (on probe paths, not pinned), andent.Mul(bull).Mul(up)andent.Mul(bear).Mul(dn)his trend cases. The trend gates the cases, not the signal:c.Position(bull.Mul(up), nil, 5)is not his trend set, since an off-trend case still blocks the next five bars in his harness, so the gated signal takes more entries. - Members:
- Bulkowski’s inclusive “within 25%”:
out.Mul(c.GTE(rng.Mul(1 - q).Add(l)))andout.Mul(c.LTE(rng.Mul(q).Add(l))); - Kaufman’s key reversal, sold or bought on the close:
out.Mul(c.LT(lp))andout.Mul(c.GT(hp)).
- Bulkowski’s inclusive “within 25%”:
- Evidence: Kaufman’s Figure 9 (SPY, QQQ, IWM, AAPL, AMZN, GE, WMT and TSLA, January 2000 to July 2020, 10,000 dollars a position, no costs, in sample): bull outside days were “net positive after 4 days” in all markets, and “Breakouts to the downside posted losses everywhere”; the trend filter cut the bull cases by about a third but the bear cases by about 60%, and the Cases column is identical across his Figures 9 to 11, so its counts are unreliable. Thomas N. Bulkowski (“Tiny Chart Patterns: Stinkers Or Stalwarts?”, Stocks & Commodities V22:4, April 2004, pp. 44-48) found that the close, “within 25% from the day’s high or low”, predicted the breakout direction 66% of the time up and 62% down, on bull-market data with no sample size printed. Evidence, not a reference.
- Pinned by
TestRecipeT6OutsideDay; runnable exampleExampleSeries_OutsideBars_closeLocation.
Outside day with a down close and a lower open (Williams)
Larry Williams (Long-Term Secrets to Short-Term Trading, 2nd ed., 2011, ch. 7, pp. 127-131) buys after “an outside day with a down close. The day’s high is greater than the previous day’s high and the low is lower than the previous day’s low and the close is below the previous day’s low”, when “tomorrow opens lower than the outside day’s down close and we buy on the next day’s opening”. In the S&P 500 “These results use a” 2,000-dollar “stop to exit or the first profitable opening exit rule”, and in bonds “we will take a” 1,500-dollar “loss or exit on the first profitable opening”. t6WilliamsOutsideDay returns the outside day, the buy and, on PositionSignedRules with the open as receiver, the long trade with his stop and bailout, the value each exit was read at and each trade’s result. It is the long side of his exits on true-range/t6-volatility-breakout-levels.md, where the open breakout and Chalek’s levels rest the same stop and bailout on PositionSignedRules.
hp := h.Shift(1).SetN(1, math.NaN())
lp := l.Shift(1).SetN(1, math.NaN())
od := h.GT(hp).Mul(c.LT(lp))
buy := od.Shift(1).Mul(o.LT(c.Shift(1)))
r := PositionRules{
LongStop: PositionLevel{Level: o.Sub(d), Touch: l},
LongTarget: PositionLevel{Level: o},
Open: o,
}
pos := o.PositionSignedRules(buy, nil, nil, nil, r)
ex := o.PositionSignedExitValue(buy, nil, nil, nil, r)
res := ex.Sub(o.EntryValue(pos).Shift(1))- Inputs: daily opens, highs, lows and closes;
d> 0, the stop’s distance below the entry open in price units (the point value is the caller’s). The entry fills at the open, so the open is the receiver and the entry value. - First valid index: 1 for
odand 2 forbuy(0 before);posreads 0 until the first buy, andexandresare NaN until the first exit and on every bar that is not an exit bar. - Anchors:
l.LT(lp)follows fromc.LT(lp)wherever the low is at most the close, so his three-part rule,h.GT(hp).Mul(l.LT(lp)).Mul(c.LT(lp)), isodbit for bit on the test’s groups.pos,exandresequal a loop of his rules (bought at the open, the stop read from the bar after the fill, the bailout on the first later open strictly above the entry open, flat after either until the next buy) bit for bit on the 320-bar group and two 3,000-bar walks, for four distances and with no stop. On the 320-bar group (d 1.5)odfires on 5 bars andbuyon 2, one trade closing on the stop and one on the bailout; on a walk (d 0.5)odfires on 205 bars andbuyon 13, for 13 trades, 4 closed on the stop and 9 on the bailout. The runnable example’s constructed bars give a bailout on the next open and a stop at its level. - Traps: the lower open is strict,
o.LT(c.Shift(1))(“opens lower than”), and so is the bailout (“the first profitable opening”: an open at the entry open is no profit), so the target has noTouchand is read on the receivero, while the stop, touched by the low, hasTouch: l. The composition used beforePositionSignedRules,o.Position(buy, o.GT(o.ValueWhen(buy))), re-anchors on a buy inside an open trade: without the stop it differs fromposon 293 bars of one walk. - Variant: his “any day but Thursday”,
buy.Mul(weekday.LT(4).Add(weekday.GT(4)))on the caller’s ISOweekdayof the entry bar (Monday 1, as in the Comparison intro). - Departures: the book is a bar-level state, and fills inside a bar are algolang’s. Bought at the open, the whole fill bar comes after the fill, but it is not tested, so a stop its low reaches is honoured on the next bar’s test, if still touched, or not at all (3 of the walk’s 13 fill bars reach the stop). A bar that opens above the entry open and later touches the stop exits at the stop, where his bailout at the open comes first (1 of the walk’s 13 trades). A buy on the bar of a stop or a bailout opens a trade only on a later buy. Weekday keys and point values are the caller’s.
- Evidence: Figure 7.2 (S&P 500, July 1982 to August 1998): 109 trades, 85% winners and 52,062.50 dollars, printed with commission and slippage of 0 although his book says 50 dollars is deducted; after publication, 53% winners on 76 trades; any day but Thursday, 50,037.50 dollars in 90 trades, 86% winners (Figure 7.3); in bonds from June 1990, “82 percent accuracy and” 212 dollars “average profit per trade on the 57 occurrences since 1990” (Figure 7.4, printed with a commission of 55 dollars). Evidence, not a reference.
- Pinned by
TestRecipeT6WilliamsOutsideDay; runnable exampleExampleSeries_PositionSignedRules_williamsOutsideDay.