Stops and trade state

All functions · nseries package

Sections: Stops and Reversals · SuperTrend · Trade State

Stops and Reversals

ATRTrailingStop

func (s Series) ATRTrailingStop(h, l Series, n int, k float64) Series

ATRTrailingStop returns the close-anchored ATR trailing stop, Vervoort’s SVE_Stop_trail_ATR. It is not TradingView’s built-in Volatility Stop (VolatilityStop), which anchors on the highest or lowest close since the flip, tests the updated stop and goes long on a tie. The receiver is the close: the anchor of the candidate stops, the trigger, and the close passed to ATRWilder. h and l are the highs and lows, n is the length of the ATR and k its multiple; Vervoort’s defaults are n = 5 and k = 3.5. The line is a price.

From bar n it is FlipStop on the close with the long candidate k times ATRWilder(h, l, s, n) below the close and the short candidate the same distance above it, so ATRTrailingStop(h, l, n, k)[n:] equals s[n:].FlipStop(longStop[n:], shortStop[n:]) bit for bit. Its start t0 is the first bar from n on at which the close and both candidates are finite: usually bar n, but later when ATRWilder with n = 1 recovers after a bad bar or when the distance or a candidate overflows. Bars n to t0-1 are NaN, and after t0 a non-finite value makes its bar and every later bar NaN, as in FlipStop. (SuperTrendLine instead has a fixed start at bar n, so that a bad value there poisons every later bar.)

The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. If n < 1, n >= len(s), or k is negative or not finite, the result is len(s) zeros; if h or l is not as long as the receiver, it is an empty series.

Vervoort’s own ATR is an exponential moving average of the true range of length 2n-1 and MetaStock’s is Wilder’s, so the values differ from those on his platform until the seeds decay, and also wherever FlipStop departs from his code.

The method is recursive (path-dependent): a slice of the history that starts later seeds its ATR on a later bar, so its position can differ from that of the full series; while both hold the same position from the same flip its line differs by at most k times the ATR-seed difference on the flip bar (the ratchet can hold that difference unchanged while the seed difference itself decays by the factor 1 - 1/n per bar), and a close that lands between the two lines flips them on different bars, so the positions can differ again after a common flip.

Source: Vervoort, “Average True Range Trailing Stops”, Technical Analysis of Stocks & Commodities, June 2009 (part 2 of a series running May to July 2009).

ATRTrailingStopDirection

func (s Series) ATRTrailingStopDirection(h, l Series, n int, k float64) Series

ATRTrailingStopDirection returns the position of ATRTrailingStop with the same arguments: +1 while long and -1 while short. The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. There 0 means no position yet. Bars n to t0-1, where t0 is the start described for ATRTrailingStop, are NaN, as are the bar of a later non-finite value and every bar after it. Invalid parameters give len(s) zeros and a length mismatch an empty series, as in ATRTrailingStop. The method is recursive (path-dependent): a slice of the history that starts later seeds its ATR on a later bar, so its position can differ from that of the full series, and even after both flip the same way on the same bar a close that lands between their two lines (which differ by up to k times the ATR-seed difference on the flip bar, a difference the ratchet can hold) flips them on different bars.

BasingPointDirection

func (s Series) BasingPointDirection(l Series, barsAway int, stopFraction float64) Series

BasingPointDirection returns the side held by Magee’s basing-point stop-and-reverse: 1 while long, -1 while short and NaN before the first basing point seeds it. The receiver holds the highs and l the lows, the layout of the two-series swing methods. The procedure is the trailing stop of Edwards, Magee and Bassetti in Technical Analysis of Stock Trends, 11th edition (chapters 27 and 28, and Bassetti’s study in chapter 5): under a rising market a basing point is the low of a reaction that the market has then left behind, the stop sits a fraction below a basing point and is raised only, and a penetration of the stop reverses to short, where the mirror rules apply.

A candidate low is the lowest low since the last wave high (a strictly higher high) or since the last confirmation, whichever is later; an equal low keeps the earlier candidate, an equal high is not a new wave high, and the bar that makes a wave high is not a candidate. The candidate is confirmed as a basing point once barsAway bars have traded wholly above the candidate bar’s high (a low strictly above it; the bars away need not be consecutive) before any lower low: a lower low moves the candidate, a new wave high before the count is complete discards it, and the bar that completes the count confirms even when it also makes a new high. Magee’s three-days-away rule is barsAway = 3 on daily bars; Bassetti’s study runs weekly bars with barsAway = 3, and its first stop (50.35 from a wave low of 53.00) fixes stopFraction = 0.05. The short side mirrors every rule: its candidate is the highest high since the last wave low, a bar away trades wholly below it, and its stop sits the fraction above the basing point. Both sides are tracked on every valid bar whatever the direction held, and one wave can yield several basing points. These are Bassetti’s Variant 1, basing points at reaction lows; Magee’s basing point at the low of a new-high day (Bassetti’s Variant 2) is not implemented.

The stop-and-reverse: the first basing point seeds the direction on its own side (when a long and a short basing point confirm on one bar the direction stays unset); a penetration of the stop in force, a low strictly below the long stop or a high strictly above the short stop, flips the direction on that bar, at most once per bar, and the new side’s stop is not tested on its flip bar. A touch does not flip, unlike FlipStop, which flips a long on a trigger equal to its line, and there is no close or touch form. The new stop comes from the latest basing point of the new side, however old, or, before any exists, from the extreme of the trade just closed, its flip bar included (the new trade’s extremes start on the flip bar, the first trade’s on the seed bar). The timing is that of FlipStop rather than ParabolicSAR: the stop posted on bar t is in force during bar t+1, so a flip on bar t answers the stop posted on the bar before. BasingPointStop returns the levels.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is an empty Series; when barsAway < 1, or stopFraction is not finite or lies outside 0 <= stopFraction < 1 (-0 is valid), the result is all +0. A bar is valid when l[t] <= h[t] (false when either is NaN); an invalid bar is skipped by the trackers and by the stop-and-reverse, and the direction holds its value from the bar before (NaN on bar 0). Unlike FlipStop, ParabolicSAR and the volatility stops, where a non-finite bar affects every later bar, the run recovers on the next valid bar. Infinities are ordered values: a +Inf high is a new wave high, and an infinite basing point gives an infinite stop that no bar penetrates. Warm-up: NaN until the first basing point seeds a direction. Timing: the direction posted on bar t is the one held at t’s close, after any flip on bar t. The method is path-dependent: the wave references start at the slice’s first valid bar and the warm-up is the first basing point inside the slice, so a slice and the full history can differ until both hold the same direction and stop, the same wave high and wave low, the same candidates, the same latest basing point of each side and, while a side has no basing point, the same extremes of the open trade; there is no convergence bar. The offset assumes positive prices: on a negative price the long stop sits above the basing point. A new stop already on the wrong side of the flip bar’s price is not checked, so the next bar flips back unless price moves past it. There is no identity with MarketStructure, the bar-count swing events, SwingTrend or LevelBreakDirection (a basing point counts bars that trade wholly beyond the candidate, a separation rule, not bars on either side of an extreme or a break of a level). Bassetti’s Table 5.1 cannot be reproduced (his weekly Dow Industrials series is not available), and his Table 5.2 is Variant 2.

BasingPointHighEvent

func (s Series) BasingPointHighEvent(l Series, barsAway int) Series

BasingPointHighEvent returns 1 on each bar that confirms a short basing point of Magee’s procedure and 0 on every other bar; it is not NaN anywhere. The receiver holds the highs and l the lows. A candidate high is the highest high since the last wave low or since the last confirmation, whichever is later (an equal high keeps the earlier candidate, and the bar that makes a wave low is not a candidate), and it is confirmed on the bar that completes barsAway bars away, bars whose high is strictly below the candidate bar’s low, provided no higher high and no new wave low came first (the bar that completes the count confirms even when it also makes a new low; a higher high moves the candidate to itself). The bars away need not be consecutive, so this is not the bar-count rule of SwingHighEvent and there is no identity with it or with MarketStructure. The events are exported because two consecutive basing points at one price leave BasingPointHighValue unchanged, so no Delta of the value recovers them; the k-th previous basing point is the value’s ValueWhen(event, k). A confirmation is an event of its own bar only, so ValueWhen on the highs at these events gives the high of the confirmation bar rather than the basing point; BasingPointHighValue holds the basing points themselves.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is an empty Series; when barsAway < 1 the result is all +0. An invalid bar (a low above the high, or a NaN) is 0 and is neither a bar away nor a candidate nor a wave low. Warm-up: 0 until the first confirmation, which cannot come before bar barsAway + 1 (the first valid bar opens the wave and is not a candidate). Timing: the event is posted on the confirmation bar, barsAway or more bars after the extreme, with no look-ahead. The view is path-dependent: the wave reference starts at the slice’s first valid bar and the warm-up is the first confirmation inside the slice.

BasingPointHighValue

func (s Series) BasingPointHighValue(l Series, barsAway int) Series

BasingPointHighValue returns the value of the most recent short basing point of Magee’s procedure, the high of the candidate bar, posted on the bar that confirms it and held until the next one; it is NaN before the first. The receiver holds the highs and l the lows. The view is the one ValueWhen gives when the source is the candidate bar’s high rather than the confirmation bar’s high (BasingPointHighEvent marks the confirmation bars); the k-th previous basing point is this value’s ValueWhen(event, k) with that event. The mirror of the closing violation given under BasingPointLowValue is c.GT(h.BasingPointHighValue(l, k).Shift(1)).SetN(1, 0), a close above the basing point known at the previous close. There is no identity with MarketStructure or the bar-count swing events.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is an empty Series; when barsAway < 1 the result is all +0. An invalid bar (a low above the high, or a NaN) holds the value from the bar before (NaN on bar 0). Warm-up: NaN until the first confirmation, which cannot come before bar barsAway + 1. Timing: the basing point is posted on its confirmation bar, barsAway or more bars after its extreme bar, with no look-ahead, and its value is copied verbatim from the highs. The view is path-dependent: the wave reference starts at the slice’s first valid bar and the warm-up is the first basing point inside the slice.

BasingPointLowEvent

func (s Series) BasingPointLowEvent(l Series, barsAway int) Series

BasingPointLowEvent returns 1 on each bar that confirms a long basing point of Magee’s procedure and 0 on every other bar; it is not NaN anywhere. The receiver holds the highs and l the lows. A candidate low is the lowest low since the last wave high or since the last confirmation, whichever is later (an equal low keeps the earlier candidate, and the bar that makes a wave high is not a candidate), and it is confirmed on the bar that completes barsAway bars away, bars whose low is strictly above the candidate bar’s high, provided no lower low and no new wave high came first (the bar that completes the count confirms even when it also makes a new high; a lower low moves the candidate to itself). The bars away need not be consecutive, so this is not the bar-count rule of SwingLowEvent and there is no identity with it or with MarketStructure. The events are exported because two consecutive basing points at one price leave BasingPointLowValue unchanged, so no Delta of the value recovers them; the k-th previous basing point is the value’s ValueWhen(event, k). A confirmation is an event of its own bar only, so ValueWhen on the lows at these events gives the low of the confirmation bar rather than the basing point; BasingPointLowValue holds the basing points themselves.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is an empty Series; when barsAway < 1 the result is all +0. An invalid bar (a low above the high, or a NaN) is 0 and is neither a bar away nor a candidate nor a wave high. Warm-up: 0 until the first confirmation, which cannot come before bar barsAway + 1 (the first valid bar opens the wave and is not a candidate). Timing: the event is posted on the confirmation bar, barsAway or more bars after the extreme, with no look-ahead. The view is path-dependent: the wave reference starts at the slice’s first valid bar and the warm-up is the first confirmation inside the slice.

BasingPointLowValue

func (s Series) BasingPointLowValue(l Series, barsAway int) Series

BasingPointLowValue returns the value of the most recent long basing point of Magee’s procedure, the low of the candidate bar, posted on the bar that confirms it and held until the next one; it is NaN before the first. The receiver holds the highs and l the lows. The view is the one ValueWhen gives when the source is the candidate bar’s low rather than the confirmation bar’s low (BasingPointLowEvent marks the confirmation bars); the k-th previous basing point is this value’s ValueWhen(event, k) with that event. Magee’s closing violation of the latest basing point, as an exit flag, is

c.LT(h.BasingPointLowValue(l, k).Shift(1)).SetN(1, 0)

with c the closes: the Shift(1) tests the basing point known at the previous close, so the flag is 1 on each close below it, and SetN masks bar 0, which Shift pads with 0. There is no identity with MarketStructure or the bar-count swing events.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is an empty Series; when barsAway < 1 the result is all +0. An invalid bar (a low above the high, or a NaN) holds the value from the bar before (NaN on bar 0). Warm-up: NaN until the first confirmation, which cannot come before bar barsAway + 1. Timing: the basing point is posted on its confirmation bar, barsAway or more bars after its extreme bar, with no look-ahead, and its value is copied verbatim from the lows. The view is path-dependent: the wave reference starts at the slice’s first valid bar and the warm-up is the first basing point inside the slice.

BasingPointStop

func (s Series) BasingPointStop(l Series, barsAway int, stopFraction float64) Series

BasingPointStop returns the stop level of Magee’s basing-point stop-and-reverse, the companion of BasingPointDirection: while long it is the highest of (1 - stopFraction) times the long basing points confirmed during the run (so the latest basing point counts only when it raises the stop), while short the lowest of (1 + stopFraction) times the short basing points of the run, each product rounded once to float64 by an explicit conversion, and NaN before the first basing point seeds a direction. On a flip without a basing point of the new side the stop starts from the closed trade’s extreme, its flip bar included. The stop posted on bar t is the one computed at t’s close and in force during bar t+1 (the timing of FlipStop rather than ParabolicSAR); on a flip bar it is the new side’s first stop. Within a run of one direction the long stop is raised only and the short stop lowered only (an equal level leaves it unchanged, -0 against +0 included), so on a long run whose first stop comes from a long basing point (every run but one that starts on the trade-extreme fallback) the stop equals, by value,

h.BasingPointLowValue(l, k).Mul(1 - x).HighestSinceEntry(pos)

with x the stopFraction and pos marking the run: Magee’s long-only trailing stop since an entry (a -0 stop is kept where the recipe’s max gives +0). Two witnesses from the book: with stopFraction = 0.05 a basing point at 26.75 gives the stop 25.412499999999998, which Figure 28.2 prints as 25.41, and a wave low of 53 gives 50.349999999999994, which Bassetti’s table prints to two decimals as 50.35; the levels are kept at full precision here. On Bassetti’s tables and the identities see BasingPointDirection.

Validation: when len(l) differs from len(s), or the receiver is empty, the result is an empty Series; when barsAway < 1, or stopFraction is not finite or lies outside 0 <= stopFraction < 1 (-0 is valid), the result is all +0. An invalid bar (a low above the high, or a NaN) is skipped and the stop holds its value from the bar before (NaN on bar 0). Warm-up: NaN until the first basing point. Timing: the level posted on bar t is computed at t’s close and is in force during bar t+1. The offset assumes positive prices, so on a negative price the long stop sits above its basing point; the method is path-dependent in the way described under BasingPointDirection.

FlipStop

func (s Series) FlipStop(longStop, shortStop Series) Series

FlipStop returns a generic ratchet-and-flip trailing stop: the recursion of Vervoort’s SVE_Stop_Trail% and SVE_Stop_trail_ATR, with an explicit direction state. The receiver is the trigger price (usually the close); longStop and shortStop are the candidate stops for a long and a short position on each bar. The candidates are taken as given: a long candidate above the trigger or a short candidate below it is not rejected, and simply flips the position on the next bar unless the trigger moves past it.

The position starts long at the first bar t0 at which the trigger and both candidates are finite, with line[t0] = longStop[t0]. The bars before t0 are NaN, so a leading run of NaN inputs (such as another indicator’s NaN warm-up) propagates; if there is no such bar, every bar is NaN. While long, the line only rises: it is the running maximum of the long candidates since the last flip, and the position flips to short, with that bar’s short candidate as its line, when the trigger falls to or through the previous bar’s line. While short, the line only falls: it is the running minimum of the short candidates since the last flip, and the position flips to long, with that bar’s long candidate as its line, when the trigger rises above the previous bar’s line. So on a tie a long position flips (Vervoort: “lower than or equal to the previous value”) and a short position stays short. Vervoort’s MetaStock code loosens a short line to C + loss (the close plus the stop distance) on an exact tie; this method keeps the short line. It also departs from his code where a bar’s stop distance is zero, which his two-close test cannot resolve, and at the start, where his PREV starts at 0, so his first line is Max(0, C - loss). The third article, “Trailing Resistance And Support Stops” (July 2009), whose TR&NDS line is triggered by the high, sends a tie of the high to the support side instead (If(H>=PREV,support,resistance) in its final listing), the opposite of this method’s tie rule; the negated form h.Mul(-1).FlipStop(res.Mul(-1), sup.Mul(-1)).Mul(-1) reproduces it, and that recipe is listed under “Compositions” in the “Stops and Reversals” category.

line[t-1] is the stop in force during bar t, computed at the close of bar t-1; a flip happens at the close of bar t (a close-basis fill), and line[t] on a flip bar is the first stop of the new position. If the trigger or a candidate is not finite at a bar after t0, that bar and every later bar are NaN: the recursion does not recover. If longStop or shortStop is not as long as the receiver, the result is an empty series.

FlipStop has no warm-up of its own beyond the NaN start. The +0 warm-up of a house indicator is finite and would start the recursion inside the warm-up, so mask such a warm-up with NaN first, for example with SetN(n, math.NaN()).

The method is recursive (path-dependent): a slice of the history that starts later can differ from the full series until both hold the same position with the same line on the same bar (as after both flip the same way on the same bar), after which they agree. LevelBreakDirection latches on strict breaks of two given level series without a ratchet, and BasingPointDirection on Magee’s basing points; both name this method’s tie rule as the one they do not follow. NoiseChannel and NoiseChannelDirection are this method over Meyers’s noise-channel levels.

Source: Vervoort, “Using Initial And Trailing Stops” and its sequels “Average True Range Trailing Stops” and “Trailing Resistance And Support Stops”, Technical Analysis of Stocks & Commodities, May to July 2009.

FlipStopDirection

func (s Series) FlipStopDirection(longStop, shortStop Series) Series

FlipStopDirection returns the position of FlipStop with the same arguments and rules: +1 while long and -1 while short. The bars before the start (the first bar at which the trigger and both candidates are finite) are NaN, as are the bar of a later non-finite input and every bar after it; a length mismatch gives an empty series. A flip at the close of bar t shows as a change of sign from bar t-1 to bar t. The method is recursive (path-dependent): a slice of the history that starts later can differ from the full series until both hold the same position with the same line on the same bar, after which they agree.

NoiseChannel

func (s Series) NoiseChannel(h, l Series, nhi, nlo int, xoh, xod float64) Series

NoiseChannel returns Meyers’s ratcheted noise-channel stop-and-reverse line: FlipStop on the receiver over the lowest low of nlo bars less xod and the highest high of nhi bars plus xoh.

The receiver is the trigger, usually the close, and h and l are the highs and lows of the same bars. With k = max(nhi, nlo) - 1 the result is, for valid parameters, bit for bit, s.FlipStop(l.Lowest(nlo).Sub(xod).SetN(k, math.NaN()), h.Highest(nhi).Add(xoh).SetN(k, math.NaN())). The sell level, the lowest of the last nlo lows less xod, is FlipStop’s long candidate, and the buy level, the highest of the last nhi highs plus xoh, its short candidate; both are NaN on bars 0 to k-1. Both windows include the current bar, since FlipStop tests bar t against the previous bar’s line. The offsets xoh and xod are in the receiver’s units.

The system starts long at the first bar t0 from k on at which the trigger and both levels are finite, with the line at that bar’s sell level. While long, the line is the highest sell level since the last flip, and a close at or below the previous bar’s line flips the position short, the line becoming that bar’s buy level; while short, the line is the lowest buy level since the flip, and a close above the previous bar’s line flips it long, the line becoming that bar’s sell level.

Meyers described the system in “The German Mark Noise Channel Breakout System” (Technical Analysis of Stocks & Commodities, April 1998) and “The Bond Futures Noise Channel-2 Breakout System” (September 1998); his first, symmetric system is the same call with nhi = nlo and xoh = xod. The method departs from his rules in three ways: it triggers on the receiver, usually the close, where his rules read as intraday stops at the levels; it starts long where he waits flat for the first break; and it takes FlipStop’s ties. After a flip it resets the line from the flip bar’s own level, a point his articles leave open. The unratcheted latch h.LevelBreakDirection(l, buy, sell), with buy and sell the two levels, and the Donchian close system are compositions under “Stops and Reversals” in the manual.

Bars before t0 are NaN, so the first valid index depends on the data: it is usually k, and a NaN before the start only moves it (a NaN close at bar k starts the system at k+1). After the start, a NaN high, low or close, or any other trigger or level that is not finite, makes that bar and every later bar NaN.

Validation: a length of h or l other than len(s) gives Series{}, checked first, since h and l are columns of the trigger’s bar table and are not right-aligned; nhi < 1, nlo < 1, or an offset that is negative or not finite gives len(s) zeros, and -0 is a valid offset; a window longer than the series gives NaN on every bar, not zeros as in ATRTrailingStop, because there is no start; and an empty series gives an empty result.

NoiseChannel is recursive (path-dependent): each value depends on every bar since the start. It is causal, costs O(1) a bar, with Highest and Lowest amortised, and returns a new Series without modifying its inputs.

NoiseChannelDirection

func (s Series) NoiseChannelDirection(h, l Series, nhi, nlo int, xoh, xod float64) Series

NoiseChannelDirection returns the position of NoiseChannel, +1 long and -1 short.

With k = max(nhi, nlo) - 1 the result is, for valid parameters, s.FlipStopDirection over NoiseChannel’s two levels, bit for bit: s.FlipStopDirection(l.Lowest(nlo).Sub(xod).SetN(k, math.NaN()), h.Highest(nhi).Add(xoh).SetN(k, math.NaN())). It shares NoiseChannel’s start, ties and NaN rules: it is NaN before the data-dependent start and starts long there, and after the start a bar at which the trigger or a level is not finite makes that bar and every later bar NaN. A flip at the close of bar t shows as a sign change from bar t-1 to bar t.

With h = l = s, nhi = nlo = n and zero offsets it is the ratcheted Donchian close system s.FlipStopDirection(s.Lowest(n).SetN(n-1, math.NaN()), s.Highest(n).SetN(n-1, math.NaN())), bit for bit.

Validation is NoiseChannel’s: a length of h or l other than len(s) gives Series{}, invalid parameters give len(s) zeros, a window longer than the series gives NaN on every bar, and an empty series gives an empty result. It is recursive (path-dependent) and causal, costs O(1) a bar amortised and returns a new Series without modifying its inputs.

ParabolicSAR

func (s Series) ParabolicSAR(h, l Series, afStart, afStep, afMax float64) Series

ParabolicSAR returns Wilder’s Parabolic time/price system, the stop and reverse level (SAR), of the highs h and lows l, in TA-Lib’s state machine (TA_SAR). Index 0 is the oldest bar and the last index the most recent. The system is always in the market, long or short. While long, the SAR trails below price and accelerates towards the trade’s extreme point (EP, the highest high since the trade began); the short side mirrors it with the lowest low. afStart is the initial acceleration factor (AF), afStep its increment and afMax its cap; Wilder’s defaults are 0.02, 0.02 and 0.2. The AF steps only on a bar that makes a new extreme (a new high while long, a new low while short) and is capped at afMax. After each bar the next SAR is clamped outside the ranges of the last two bars: while long it is not above their lows, while short it is not below their highs. At bar 1 both clamp bars are bar 1, as in TA-Lib, so bar 0’s range does not clamp.

The start is TA-Lib’s minus-DM rule over bars 0 and 1: with up = h[1] - h[0] and dn = l[0] - l[1], the system starts short if dn > 0 and up < dn, otherwise long, so a tie starts long. A long start has sar = l[0] and ep = h[1]; a short start has sar = h[0] and ep = l[1]. Other platforms start differently (Tulip compares h+l, TradingView compares closes, pandas-ta seeds the EP from bar 0), so their values differ from these at least until the first common reversal. They can also differ after it, because those platforms reverse and clamp differently (pandas-ta clamps on one bar, and pandas-ta and TradingView test the stop before clamping it) and do not use a fused multiply-add.

A touch reverses: while long, a bar whose low is at or below the SAR reverses the position to short; while short, a bar whose high is at or above the SAR reverses it to long. Tulip, pandas-ta and TradingView reverse only on a strict penetration. On a reversal bar the output is the new stop of the opposite position (as in TA-Lib, Tulip, pandas-ta and TradingView): the old trade’s EP, moved if need be so that it is not inside the ranges of that bar and the one before (at bar 1, of bar 1 only). It uses the bar’s own high or low, so it is known only at that bar’s close. The stop that was hit, the fill, is not one of the outputs: it is the stop computed at the previous bar’s close, which in general differs from the previous bar’s value (the two are equal when a clamp holds the stop). From bar 2 on, detect reversals from a change in ParabolicSARDirection; a reversal on bar 1 does not show, because bar 0 reads 0. FlipStop, ATRTrailingStop, SuperTrendLine, WilderVolatilityStop, VolatilityStop and BasingPointStop time their lines differently: on every bar they return the stop computed at that bar’s close, in force during the next bar.

The warm-up reads 0, not NaN: bar 0 is 0 and the first valid index is 1.

Invalid parameters give aligned zeros, one 0 per bar of h: afStart, afStep and afMax must all be finite, with afStart > 0, afStep >= 0 and afMax > 0. As TA-Lib corrects incoherent values, an afStart or afStep above afMax is set to afMax. If h and l differ in length the result is an empty Series, and that check comes first. The receiver s is not used, as in ParkinsonVolatility.

With valid parameters and equal lengths, if h or l holds NaN or an infinity, the result is NaN from the first bad bar on, because the recursion does not recover; a bad bar 0 or 1 makes every bar from 1 NaN (bar 0 still reads 0). The result is also NaN from the bar after the internal stop, after its clamps, is not finite, and from a bar whose output is not finite (possible only on overflow near ±1.8e308).

The stop update uses a fused multiply-add, as current TA-Lib does, so the bits match TA-Lib on every platform. The method is recursive, and each reversal carries the previous trade’s EP into the new stop, so a slice of the history that starts later can differ from the full series until both reverse on the same bar with the same new stop.

Sources: Wilder, New Concepts in Technical Trading Systems (1978), through Hartle, “The Parabolic Trading System”, Technical Analysis of Stocks & Commodities, November 1993; Aan, “Parabolic Stop/Reversal”, Technical Analysis of Stocks & Commodities, November 1989, for the AF step and maximum ranges; TA-Lib TA_SAR and TA_SAREXT; Tulip psar; pandas-ta psar; TradingView ta.sar.

ParabolicSARDirection

func (s Series) ParabolicSARDirection(h, l Series, afStart, afStep, afMax float64) Series

ParabolicSARDirection returns the position of Wilder’s Parabolic time/price system after each bar: +1 while long and -1 while short. It runs the same state machine as ParabolicSAR, TA-Lib’s TA_SAR, with the same parameters: afStart is the initial acceleration factor (AF), afStep its increment and afMax its cap; Wilder’s defaults are 0.02, 0.02 and 0.2. The AF steps only on a bar that makes a new extreme (a new high while long, a new low while short) and is capped at afMax, and each next stop is clamped outside the ranges of the last two bars (at bar 1 both are bar 1, as in TA-Lib, so bar 0’s range does not clamp).

The start is TA-Lib’s minus-DM rule over bars 0 and 1: with up = h[1] - h[0] and dn = l[0] - l[1], the system starts short if dn > 0 and up < dn, otherwise long, so a tie starts long. The stop starts at bar 0’s low (long) or high (short) and the EP at bar 1’s high (long) or low (short). Other platforms start differently (Tulip compares h+l, TradingView compares closes, pandas-ta seeds the EP from bar 0), so values differ at least until the first common reversal, and can differ after it because those platforms reverse and clamp differently and do not use a fused multiply-add.

A touch reverses: while long, a bar whose low is at or below the stop reverses the position to short; while short, a bar whose high is at or above the stop reverses it to long. Tulip, pandas-ta and TradingView reverse only on a strict penetration. On a reversal bar the direction is the new position, which pairs with the new stop that ParabolicSAR returns on that bar. The stop that was hit, the fill, is not one of the outputs: it is the stop computed at the previous bar’s close, which in general differs from the previous bar’s SAR (the two are equal when a clamp holds the stop). From bar 2 on, a reversal shows as a change in the direction; a reversal on bar 1 does not show, because bar 0 reads 0. The direction views of FlipStop, ATRTrailingStop, SuperTrendLine, WilderVolatilityStop and VolatilityStop, and BasingPointDirection, share this timing (the position held after each bar’s close), though their lines are timed differently from ParabolicSAR: on every bar they return the stop computed at that bar’s close, in force during the next bar.

The warm-up reads 0, not NaN: bar 0 is 0 and the first valid index is 1. For the direction, 0 means no position yet.

Invalid parameters give aligned zeros, one 0 per bar of h: afStart, afStep and afMax must all be finite, with afStart > 0, afStep >= 0 and afMax > 0. As TA-Lib corrects incoherent values, an afStart or afStep above afMax is set to afMax. If h and l differ in length the result is an empty Series, and that check comes first. The receiver s is not used, as in ParkinsonVolatility.

With valid parameters and equal lengths, if h or l holds NaN or an infinity, the result is NaN from the first bad bar on, because the recursion does not recover; a bad bar 0 or 1 makes every bar from 1 NaN (bar 0 still reads 0). The direction is NaN wherever the SAR is NaN, including where the internal stop, after its clamps, or an output is not finite (possible only on overflow near ±1.8e308).

The stop update uses a fused multiply-add, as current TA-Lib does, so the bits match TA-Lib on every platform. The method is recursive, and each reversal carries the previous trade’s EP into the new stop, so a slice of the history that starts later can differ from the full series until both reverse on the same bar with the same new stop.

Sources: Wilder, New Concepts in Technical Trading Systems (1978), through Hartle, “The Parabolic Trading System”, Technical Analysis of Stocks & Commodities, November 1993; Aan, “Parabolic Stop/Reversal”, Technical Analysis of Stocks & Commodities, November 1989, for the AF step and maximum ranges; TA-Lib TA_SAR and TA_SAREXT; Tulip psar; pandas-ta psar; TradingView ta.sar.

ParabolicSARExt

func (s Series) ParabolicSARExt(h, l Series, start, offsetOnReverse, afInitLong, afLong, afMaxLong, afInitShort, afShort, afMaxShort float64) Series

ParabolicSARExt returns the Parabolic SAR as TA-Lib’s TA_SAREXT computes it: the system of ParabolicSAR with separate long and short acceleration parameters, a chosen start and an offset on reversal, and with the SAR negated while the position is short. The negation applies on every bar that ends short, including the reversal bar into short; the SAR is not negated while long, so for positive prices it is negative while short and positive while long. The sign therefore carries the direction only for positive prices; use ParabolicSARDirection when prices can be zero or negative. With start 0, offsetOnReverse 0 and equal parameters on both sides the result equals ParabolicSAR multiplied by ParabolicSARDirection, bit for bit, on finite inputs.

Each side has an initial acceleration factor (AF), a step and a cap: afInitLong, afLong and afMaxLong, and afInitShort, afShort and afMaxShort. Wilder’s defaults are 0.02, 0.02 and 0.2. The AF steps only on a bar that makes a new extreme (a new high while long, a new low while short) and is capped at that side’s maximum. After each bar the next SAR is clamped outside the ranges of the last two bars: while long it is not above their lows, while short it is not below their highs. At bar 1 both clamp bars are bar 1, as in TA-Lib, so bar 0’s range does not clamp.

start selects the first position. Zero uses TA-Lib’s minus-DM rule over bars 0 and 1: with up = h[1] - h[0] and dn = l[0] - l[1], the system starts short if dn > 0 and up < dn, otherwise long, so a tie starts long; the SAR starts at bar 0’s low (long) or high (short) and the EP at bar 1’s high (long) or low (short). A positive start goes long with the SAR at start and the EP at bar 1’s high; a negative start goes short with the SAR at -start and the EP at bar 1’s low. Other platforms start differently (Tulip compares h+l, TradingView compares closes, pandas-ta seeds the EP from bar 0), so values differ from theirs at least until the first common reversal, and can differ after it because those platforms reverse and clamp differently and do not use a fused multiply-add.

offsetOnReverse is a fraction of the SAR added to the new stop on a reversal into short and subtracted from it on a reversal into long.

A touch reverses: while long, a bar whose low is at or below the SAR reverses the position to short; while short, a bar whose high is at or above the SAR reverses it to long. Tulip, pandas-ta and TradingView reverse only on a strict penetration. On a reversal bar the output is the new stop of the opposite position (as in TA-Lib, Tulip, pandas-ta and TradingView), negated when that position is short. It uses the bar’s own high or low, so it is known only at that bar’s close. The stop that was hit, the fill, is not one of the outputs: it is the stop computed at the previous bar’s close, which in general differs from the previous bar’s value (the two are equal when a clamp holds the stop). From bar 2 on, detect reversals with ParabolicSARDirection; a reversal on bar 1 does not show, because bar 0 reads 0.

The warm-up reads 0, not NaN: bar 0 is 0 and the first valid index is 1.

Invalid parameters give aligned zeros, one 0 per bar of h: start, offsetOnReverse and the six acceleration parameters must all be finite, offsetOnReverse must be >= 0, and on each side the initial AF must be > 0, the step >= 0 and the maximum > 0. start may be any finite value, including a negative one. As TA-Lib corrects incoherent values, an initial AF or step above its side’s maximum is set to that maximum. If h and l differ in length the result is an empty Series, and that check comes first. The receiver s is not used, as in ParkinsonVolatility.

With valid parameters and equal lengths, if h or l holds NaN or an infinity, the result is NaN from the first bad bar on, because the recursion does not recover; a bad bar 0 or 1 makes every bar from 1 NaN (bar 0 still reads 0). The result is also NaN from the bar after the internal stop, after its clamps, is not finite, and from a bar whose output is not finite (possible only on overflow near ±1.8e308).

The stop update uses a fused multiply-add, as current TA-Lib does, so with offsetOnReverse 0 the bits match TA-Lib on every platform. The offset is fused here too, but TA-Lib writes it as a separate multiply and add, so with a non-zero offset TA-Lib’s own bits depend on whether its compiler fuses that line; these match a fused build. The method is recursive, and each reversal carries the previous trade’s EP into the new stop, so a slice of the history that starts later can differ from the full series until both reverse on the same bar with the same new stop.

Sources: Wilder, New Concepts in Technical Trading Systems (1978), through Hartle, “The Parabolic Trading System”, Technical Analysis of Stocks & Commodities, November 1993; Aan, “Parabolic Stop/Reversal”, Technical Analysis of Stocks & Commodities, November 1989, for the AF step and maximum ranges; TA-Lib TA_SAR and TA_SAREXT; Tulip psar; pandas-ta psar; TradingView ta.sar.

SuperTrendDirection

func (s Series) SuperTrendDirection(h, l, c Series, mult float64, n int) Series

SuperTrendDirection returns the trend of SuperTrendLine with the same arguments: +1 in an uptrend, where the line is the lower band, and -1 in a downtrend, where it is the upper band, as TA-Lib and pandas-ta sign it. TradingView’s ta.supertrend uses the opposite sign and returns -1 for an uptrend. The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. There 0 means no trend yet. The bar of a non-finite value and every bar after it are NaN, invalid parameters give len(s) zeros and a length mismatch an empty series, as in SuperTrendLine. The method is recursive (path-dependent): a slice of the history that starts later seeds its ATR on a later bar, so its trend can differ from that of the full series, and even after both flip the same way on the same bar a close that lands between their two lines (which differ by a multiple of the ATR-seed difference that the ratchet can hold) flips them on different bars.

SuperTrendLine

func (s Series) SuperTrendLine(h, l, c Series, mult float64, n int) Series

SuperTrendLine returns Olivier Seban’s SuperTrend in its ratcheting and flipping form, as TradingView’s ta.supertrend, Golson’s MetaStock code and TA-Lib’s TA_SUPERTREND compute it. The receiver is the basis, by convention the median price h.Add(l).Div(2); h, l and c are the highs, lows and closes, n is the length of ATRWilder(h, l, c, n) and mult its multiple. Seban’s defaults are n = 10 and mult = 3 (pandas-ta uses 7 and 3). The line is a price level. The existing SuperTrend method is the unratcheted band.

The raw bands lie mult times the ATR above and below the basis, and each ratchets on its own on every bar, the inactive one included: the lower band only rises unless the previous close fell below it, and the upper band only falls unless the previous close rose above it. The trend starts up at bar n, as in TA-Lib (TradingView and MetaStock start down). An uptrend turns down when the close falls below the lower band, and a downtrend turns up when the close rises above the upper band, both as updated on that bar; a close equal to the lower band stays up and one equal to the upper band stays down. The line is the lower band in an uptrend and the upper band in a downtrend.

The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. If n < 1, n >= len(s), or mult is negative or not finite, the result is len(s) zeros; if h, l, c and the receiver do not all have the same length, it is an empty series. Unlike ATRTrailingStop, the start is fixed at bar n: if the basis, the close or the ATR is not finite at a bar from n on, or the line computed there is not finite, that bar and every later bar are NaN (the recursion does not recover), so a bad value at bar n leaves no valid bar. A NaN high, low or close reaches the ATR as ATRWilder documents.

TA-Lib’s TA_SUPERTREND uses the same band and flip rules, the same start and the same ATR seed, but writes Wilder’s step as a different fused expression, so the line agrees with it to rounding, not bit for bit, and the trend agrees except where a close lies within rounding of a band.

The method is recursive (path-dependent): a slice of the history that starts later seeds its ATR on a later bar, so its trend can differ from that of the full series. Its line differs from the full series’ by mult times an ATR-seed difference: the raw bands’ difference decays by the factor 1 - 1/n per bar, but the ratchet holds a band’s difference unchanged until the band next moves, and the band in force after a flip can carry a difference from before the flip. A close that lands between the two lines flips them on different bars, so the trends can differ again after a common flip.

Sources: Star, “Stay On Track With The Supertrend Indicator”, Technical Analysis of Stocks & Commodities, July 2023, with Golson’s MetaStock code; Unger, “Developing A Trading System For Crude Oil Using The SuperTrend Indicator”, Technical Analysis of Stocks & Commodities, August 2025; TradingView ta.supertrend; TA-Lib TA_SUPERTREND.

VolatilityStop

func (s Series) VolatilityStop(h, l Series, n int, k float64) Series

VolatilityStop returns the volatility stop as TradingView’s built-in “Volatility Stop” (VStop) computes it, a stop-and-reverse line that is always in the market. The stop trails the significant close (SIC), the highest close since the last reversal while long and the lowest while short, by k times the average true range (ATR). At the close of bar t, in this order: the SIC is updated with the close; the stop is ratcheted, moving to the SIC minus k times the ATR of bar t (long) or the SIC plus that distance (short) only when that is in the position’s favour, so it does not move against the position; the close is tested against this updated stop (the same-bar test); and if the close is below a long stop, or at or above a short stop, the position reverses, the SIC restarts at the close and the stop resets, unratcheted, to the close minus or plus k times the ATR of bar t. So a long holds on a close equal to its stop and a short reverses, whereas ATRTrailingStop tests the close against the previous bar’s line, reversing a long on a close equal to it and holding a short. Because the test uses the stop already updated with the bar’s ATR, a fall in the ATR alone can lift a long stop above an unchanged close and reverse the position. With k = 0 a short lasts one bar: the next close always equals or exceeds the updated stop (the lowest close), so the position reverses to long at that close; this is the recursion’s behaviour, not a special case.

The receiver s is the close: the trigger, the source of the SIC and the close passed to ATRWilder(h, l, s, n). h and l are the highs and lows, n is the ATR length and k its multiple. TradingView’s defaults are n = 20 and k = 2.

The value on bar t is the stop computed at the close of bar t, in force during bar t+1, so the stop in force during bar t is the value on bar t-1, and on a reversal bar the value is the new position’s first stop (the reversal fills at the close of bar t). This is the convention of FlipStop, ATRTrailingStop, SuperTrendLine and TradingView’s plotted VStop. ParabolicSAR instead follows TA-Lib: on a bar without a reversal its value is the stop in force during that bar, computed at the previous close, and on a reversal bar it is the new position’s first stop.

The start is fixed at bar n, long (TradingView also starts long), with the SIC at s[n] and the stop k times the ATR below it. The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. If n < 1, n >= len(s), or k is negative or not finite, the result is len(s) zeros (k = -0 is valid). If h or l is not the same length as s, the result is an empty series, whatever n and k. If the close or the ATR of bar n or of a later bar, or the stop stored for that bar, is not finite, the result is NaN from that bar on: the recursion does not recover, even with n = 1, where ATRWilder does. Only the stored stop is tested: a non-finite candidate that the ratchet rejects leaves the previous stop in place.

TradingView’s script uses ta.atr, which includes bar 0’s true range h[0] - l[0] and seeds at bar n-1; it starts at bar 0, where its stop falls back to the source itself, and uses the unmultiplied true range as the distance on bars 1 to n-2, before the ATR exists, so its first value is at bar 0, not bar n; and it computes in its own arithmetic. Its values are therefore not reproduced exactly: after both hold the same position from the same reversal, the stops differ by at most k times the ATR-seed difference on the reversal bar, which the ratchet can hold unchanged while the seed difference itself decays by the factor 1 - 1/n per bar, and a close that lands between the two stops reverses them on different bars.

ATRTrailingStop anchors on the current close, not the SIC, and Vakkur’s “volatility stop” (Technical Analysis of Stocks & Commodities, October 1999), a stop ratcheted from the entry close and hit by the low (its short side by the high; his system is long-only), is a different construction. WilderVolatilityStop is Wilder’s original stop, unratcheted and tested against the stop computed at the previous close. The method is recursive (path-dependent): a slice of the history that starts later seeds its ATR on a later bar, so its position can differ from that of the full series; while both hold the same position from the same reversal its stop differs by at most k times the ATR-seed difference on the reversal bar (the ratchet can hold that difference unchanged while the seed difference itself decays by the factor 1 - 1/n per bar), and a close that lands between the two stops reverses them on different bars. The “Trade State” compositions of the manual rebuild this line from HighestSinceEntry and LowestSinceEntry over VolatilityStopDirection (the chandelier exit since entry).

Sources: TradingView’s built-in Volatility Stop; J. Welles Wilder, New Concepts in Technical Trading Systems (1978), as restated by Peter Aan, “Volatility System”, Technical Analysis of Stocks & Commodities, July 1989; Abraham, “Trading The Trend”, Technical Analysis of Stocks & Commodities, September 1998, for the re-anchoring on a reversal.

VolatilityStopDirection

func (s Series) VolatilityStopDirection(h, l Series, n int, k float64) Series

VolatilityStopDirection returns the position of VolatilityStop held after the close of each bar: +1 long and -1 short (TradingView’s uptrend flag). A reversal at the close of bar t shows as a change of sign from bar t-1 to the bar itself. The timing is that of the line and not that of ParabolicSAR, whose value on a bar without a reversal is the stop in force during that bar. A long holds on a close equal to its updated stop and a short reverses. The receiver s is the close, h and l are the highs and lows, n is the ATR length and k its multiple. The start is fixed at bar n, long, so bar n is +1. The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. There 0 means no position yet. Invalid parameters give len(s) zeros and mismatched lengths an empty series, and a non-finite close, ATR or stored stop on bar n or a later bar makes the result NaN from that bar on, with no recovery, as in VolatilityStop. The position is path-dependent: a slice of the history that starts later, or TradingView’s differently seeded ATR, can give a different position, and even after both reverse the same way on the same bar a close that lands between their two stops reverses them on different bars until the ATR seeds have decayed. See VolatilityStop for the construction, the settings and the sources.

WilderVolatilityStop

func (s Series) WilderVolatilityStop(h, l Series, n int, k float64) Series

WilderVolatilityStop returns the stop of Wilder’s Volatility System, a stop-and-reverse line that is always in the market. The stop trails the significant close (SIC), the highest close since the last reversal while long and the lowest while short, by k times the average true range (ATR): at the close of bar t it is the SIC minus k times the ATR of bar t while long and the SIC plus that distance while short. It is rebuilt from the SIC and the ATR at each close and is not ratcheted, so it loosens when the ATR rises (Aan: “the trailing stop loosens”). Each close is tested against the stop computed at the previous close, strictly both ways (Aan’s stop “is activated only on the close”; Abraham’s “closes below” and “closes above”): a long reverses on a close below it, a short on a close above it, and on a reversal the SIC restarts at the close. A close equal to the stop holds the position, long or short, whereas ATRTrailingStop reverses a long on a close equal to its line and holds a short. VolatilityStop is the ratcheted variant, in TradingView’s construction.

The receiver s is the close: the trigger, the source of the SIC and the close passed to ATRWilder(h, l, s, n). h and l are the highs and lows, n is the ATR length and k its multiple. Aan tested n from 6 to 20 and constants from 1.0 to 4.0; in his example an ATR of 200 points times a constant of 3.5 puts a long position’s stop 700 points below the highest close of the trade.

The value on bar t is the stop computed at the close of bar t, in force during bar t+1, so the stop in force during bar t is the value on bar t-1, and on a reversal bar the value is the new position’s first stop (the reversal fills at the close of bar t). This is the convention of FlipStop, ATRTrailingStop and SuperTrendLine. ParabolicSAR instead follows TA-Lib: on a bar without a reversal its value is the stop in force during that bar, computed at the previous close, and on a reversal bar it is the new position’s first stop.

The start is fixed at bar n, long (a convention: the sources do not fix the first position, and TradingView also starts long), with the SIC at s[n] and the stop k times the ATR below it. The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. If n < 1, n >= len(s), or k is negative or not finite, the result is len(s) zeros (k = -0 is valid). If h or l is not the same length as s, the result is an empty series, whatever n and k. If the close or the ATR of bar n or of a later bar is not finite, or the stop computed for that bar is not finite (an overflow), the result is NaN from that bar on: the recursion does not recover, even with n = 1, where ATRWilder does.

ATRTrailingStop anchors on the current close, not the SIC, and Vakkur’s “volatility stop” (Technical Analysis of Stocks & Commodities, October 1999), a stop ratcheted from the entry close and hit by the low (its short side by the high; his system is long-only), is a different construction. The method is recursive (path-dependent): a slice of the history that starts later seeds its ATR on a later bar, so its position can differ from that of the full series; while both hold the same position from the same reversal its stop differs by k times the ATR-seed difference, whose weight falls by the factor 1 - 1/n per bar, and a close that lands between the two stops reverses them on different bars. The “Trade State” compositions of the manual rebuild this line from HighestSinceEntry and LowestSinceEntry over WilderVolatilityStopDirection (the unratcheted chandelier exit since entry).

Sources: J. Welles Wilder, New Concepts in Technical Trading Systems (1978), as restated by Peter Aan, “Volatility System”, Technical Analysis of Stocks & Commodities, July 1989; Abraham, “Trading The Trend”, Technical Analysis of Stocks & Commodities, September 1998, for the re-anchoring on a reversal.

WilderVolatilityStopDirection

func (s Series) WilderVolatilityStopDirection(h, l Series, n int, k float64) Series

WilderVolatilityStopDirection returns the position of WilderVolatilityStop held after the close of each bar: +1 long and -1 short. A reversal at the close of bar t shows as a change of sign from bar t-1 to bar t. The timing is that of the line and not that of ParabolicSAR, whose value on a bar without a reversal is the stop in force during that bar. A close equal to the stop in force holds the position, long or short. The receiver s is the close, h and l are the highs and lows, n is the ATR length and k its multiple. The start is fixed at bar n, long, so bar n is +1. The warm-up reads 0, not NaN: bars 0 to n-1 are 0 and the first valid index is n. There 0 means no position yet. Invalid parameters give len(s) zeros and mismatched lengths an empty series, and a non-finite close, ATR or stop on bar n or a later bar makes the result NaN from that bar on, with no recovery, as in WilderVolatilityStop. The position is path-dependent: a slice of the history that starts later can hold a different position, and even after both reverse the same way on the same bar a close that lands between their two stops reverses them on different bars until the ATR seeds have decayed. See WilderVolatilityStop for the construction, the settings and the sources (Wilder through Aan, 1989; Abraham, 1998).

Compositions

The averages below are masked with NaN before their first valid index, before any latch reads them, so a flag on a masked bar reads 0 and LevelBreakDirection cannot break a NaN level; the latches and books read 0 until their first decision. LevelBreakDirection tests the previous bar’s levels, strictly, tests only the opposite side from a position, and reverses once on a bar that breaks both levels: on an outside week a weekly bar cannot say which level was touched first. Hysteresis(0, 0) on the difference of the two break flags holds its state on such a bar instead, and PositionSigned over the same flags is the latch exactly. Prices are positive and finite and lengths are in bars, weeks on the caller’s weekly bars; daily bars take the caller’s week-start flag nw, with the contract of the weekly channel of donchian-channels/t6-breakout-systems.md (“Weekly channel stop and reverse”). Close-tested signals fill at the next open through Shift(1) on their flags; Aan’s and Krausz’s levels rest for the next bar and the latch acts on the bar that breaks them, with the fill the caller’s (for Aan’s intraday stops, the level or the open on a gap: the resting-stop fill series of the trade-state pages). Not repeated here: Donchian’s four-week and Dreyfus’s 52-week rules (“Weekly channel stop and reverse”) and Penfold’s two-week stop (the Darvas Box introduction).

Weekly high/low moving-average stops (Aan)

Peter Aan (“Weekly high/low moving average”, Stocks & Commodities V7:12, December 1989, pp. 431-432) computes “moving averages of the weekly highs and lows, based on the most recently completed calendar week” and trades them as stops: “If short, reverse your position on an intraday buy stop placed at the n-week average of the highs of the most recent n calendar weeks”, the mirror if long, and “if, for instance, your current position is short, you will be concerned only with the high average”. t6AanWeekly rests each week’s two averages as the next week’s stops, latches the breaks with LevelBreakDirection, and gives the stop the position rests on.

bs := h.Average(n).SetN(n-1, math.NaN())
ss := l.Average(n).SetN(n-1, math.NaN())
pos := h.LevelBreakDirection(l, bs, ss)
stop := h.LevelBreakStop(l, bs, ss)
  • Inputs: weekly highs and lows (the caller’s weekly bars), positive and finite; n in weeks (3 in his example; he tested 2 to 20).
  • First valid index: n - 1 for bs and ss, NaN before (the mask covers Average’s pass-through); pos decides from bar n, Aan’s first tradable week, and reads 0 until the first single-sided break; stop is NaN while pos is 0. Unmasked, h.LevelBreakDirection(l, h.Average(n), l.Average(n)) reads the pass-through and decides from bar 1.
  • Anchors: on Aan’s Figure 1 the averages of weeks 3 and 4 are the exact quotients 23220/3, 22933/3, 23015/3 and 22683/3, which round to the printed 7,740, 7,644, 7,672 and 7,561; week 4’s low, 7,460, breaks the sell stop, so pos is 0, 0, 0, -1 and the buy stop 23015/3 rests through week 5 (derived: Aan prints the averages only). pos equals a loop of Aan’s rule and h.PositionSigned(up, dn, dn, up) with up := h.GT(bs.Shift(1).SetN(n, math.NaN())) and dn := l.LT(ss.Shift(1).SetN(n, math.NaN())), bit for bit; stop.Shift(1) is the level resting during each week, the level of the resting-stop fill series. Equal prices do not break: on constructed weeks an equal high and low while flat, an equal low while long and an equal high while short all hold.
  • Traps: an outside week reverses a position once (on the constructed weeks h = 10, 10, 10, 12, 11, 13 and l = 8, 8, 8, 9, 9, 7 at n = 3, bar 5 goes from 1 to -1), where the true outcome depends on the order of the two touches inside the week; up.Sub(dn).Hysteresis(0, 0) holds there instead, and on the test’s weekly walk it first departs from the latch on an outside week. The dossier’s up.Sub(dn).Hysteresis(1, -1) is 0 on every bar: a difference of 0/1 flags is not strictly beyond 1 or -1. Aan’s stops are intraday, tested by the high and the low; the close test is Krausz’s rule (below).
  • Variant: the touch reading, upT := h.GTE(bs.Shift(1).SetN(n, math.NaN())) and dnT := l.LTE(ss.Shift(1).SetN(n, math.NaN())) with h.PositionSigned(upT, dnT, dnT, upT); it differs from the strict book where a high or a low equals a stop, as happens on tick-sized prices.
  • Evidence: Aan’s Figure 4 (System Writer Plus, 12 futures markets, windows of 1983 or 1984 to 1988, 100 dollars a trade for slippage and commissions, the best of 2 to 20 weeks for each market, in sample): coffee netted 71,828 dollars at 2 weeks and silver lost 9,499 at 3 weeks. Evidence, not a reference.
  • Pinned by TestRecipeT6AanWeekly; runnable example ExampleSeries_LevelBreakDirection_aanWeekly.

Weekly stops on daily bars (Aan)

Aan’s levels are “the n-week average of the highs of the most recent n calendar weeks” (Stocks & Commodities V7:12, December 1989), and his stops are intraday, so the daily bars inside a week can be tested against them. On each week’s first session wh and wl are the high and the low of the week just completed, and AverageWhere(nw, n) averages the last n of them and holds the mean through the week. t6AanDaily tests each day’s high and low against the levels, so the two reversals of a week whose breaks fall on different days come in order.

wh := h.HighestSince(nw).Shift(1)
wl := l.LowestSince(nw).Shift(1)
bs := wh.AverageWhere(nw, n)
ss := wl.AverageWhere(nw, n)
pos := h.PositionSigned(h.GT(bs), l.LT(ss), l.LT(ss), h.GT(bs))
  • Inputs: daily highs and lows and nw, as the weekly channel defines it: a finite 0/1 flag, 1 on each week’s first session from a change of ISO week and 0 on the data’s first bar; a calendar week with no sessions is not counted; n in weeks.
  • First valid index: the first session of the (n+1)-th marked week for bs and ss (bars 13, 18 and 28 for n = 2, 3 and 5 on the test calendar, whose partial first week has three sessions), NaN before: wh is NaN on the first marked bar, which no event precedes, and the window holds that NaN until it leaves. pos reads 0 until the first single-sided break.
  • Anchors: the levels and the book equal a loop over completed calendar weeks bit for bit (n = 2, 3 and 5, with holidays and a closed week); pos is h.LevelBreakDirection(l, bs.Shift(-1), ss.Shift(-1)) on every bar (the levels change on a week’s first session, and LevelBreakDirection reads the previous bar’s level); for n = 3 bs is wh.ValueWhen(nw).Add(wh.ValueWhen(nw, 1)).Add(wh.ValueWhen(nw, 2)).Div(3) bit for bit on the test’s prices, which are multiples of 1/32 (to rounding in general: the sum of AverageWhere is compensated, the chain of Add is not).
  • Traps: with nw[0] = 1 the partial first week counts as a week and the first valid bar moves from 13 to 3 at n = 2; leave bar 0 unmarked. LevelBreakStop of the advanced form reads the Shift(-1) pad, 0, on the last bar: do not print it. A day that breaks both levels reverses a position and leaves a flat book flat, the outside week’s ambiguity one level down. A holiday-shortened week counts as a week, a week with one session sets its levels from that session alone, and a closed calendar week is not counted.
  • Evidence: Aan’s Figure 4 (previous section) tests weekly bars only; on the test’s daily walk (1,500 sessions, 302 marked weeks, about 3% of the days widened) the weekly recipe and this daily form end 33, 18 and 13 weeks in different states for n = 2, 3 and 5, a constructed measure of what the order of the touches changes. Evidence, not a reference.
  • Pinned by TestRecipeT6AanDaily.

Hi-lo activator (Krausz; Star)

Robert Krausz (“The New Gann Swing Chartist”, Stocks & Commodities V16:2, February 1998, pp. 57-66) defines the hi-lo activator as “a three-period simple moving average of the highs or the lows”, “plotted one period forward as a horizontal line, so that you have a horizontal price point to act with tomorrow”; the trend is up “until you see a close below the hi-lo activator sell-stop”. Thom Hartle (“Gann Swings And Intraday Trading”, Stocks & Commodities V17:10, October 1999) restates it: “the close has to be past the moving average”. Barbara Star (“Swing Trade With The Gann Hi-Lo Activator”, Stocks & Commodities V38:11, October 2020, pp. 9-13) prints the thinkScript study: the close against the previous bar’s averages, strictly, and the line the current average of the side the state rests on. t6HiLoActivator is that latch, LevelBreakDirection on the close, and line is the activator.

hi := h.Average(nh).SetN(nh-1, math.NaN())
lo := l.Average(nl).SetN(nl-1, math.NaN())
side := c.LevelBreakDirection(c, hi, lo)
line := c.LevelBreakStop(c, hi, lo)
  • Inputs: highs, lows and closes; nh and nl (3 and 3 for Krausz; pandas-ta and TradingView default to 13 and 21).
  • First valid index: nh - 1 for hi and nl - 1 for lo (NaN before, each average on its own mask); side decides from bar min(nh, nl) at the earliest and reads 0 until its first break; line is NaN while side is 0, and with 13/21 an up break can seed +1 while lo is still NaN, so line stays NaN until bar 20.
  • Anchors: Star’s listing equals side and line on every bar its state is not init, and TradingView’s “Gann High Low” script from the latch’s first decision on (before it, its nz zeros put every close above a level of 0 and the script long). pandas-ta’s hilo (PyPI 0.4.71b0; its GitHub repository is gone) holds the break bar’s level through bars without a break, which is ptaLine := lo.Mul(up).Add(hi.Mul(dn.Mul(up.Mul(-1).Add(1)))).ValueWhen(up.Add(dn).GT(0)) with up := c.GT(hi.Shift(1)).SetN(1, 0) and dn := c.LT(lo.Shift(1)).SetN(1, 0), bit for bit; it differs from line on bars without a break. line.Shift(1) is Krausz’s end-of-day plot, the level each close is tested against. Sierra Chart’s study takes the previous bar’s average on a break bar and holds it.
  • Traps: with equal lengths no close can be above the high average and below the low average at once; with 13/21 it can. On the constructed double break (closes falling 4 a bar from 200, a jump on bar 29) the latch, which tests only the side opposite its state, alternates 1, -1, 1, -1 on bars 29 to 32 where Star’s listing and pandas-ta, which test the up side first, stay long; from flat a double break leaves the latch at 0 where they go long. ptaLine is not the level in force. The helper is a latch with no book: a book needs Position or PositionSigned on the flips and a fill (Krausz’s close, or the level as a resting stop).
  • Variant: Krausz’s “two ticks” read as an offset on the close test, c.LevelBreakDirection(c, hi.Add(x), lo.Sub(x)) with x two ticks.
  • Variant: the real-time form, with the live bar in the averages, c.PositionSigned(c.GT(hi), c.LT(lo), c.LT(lo), c.GT(hi)).
  • Evidence: Krausz’s Figure 23 (three T-bond contracts, 1992 to November 1997, no costs) reports 119,812.50 dollars over 143 trades for his whole Basic plan (swing chart, activator and profit rules), not for the activator alone; Star’s chart readouts carry no bars. Evidence, not a reference.
  • Pinned by TestRecipeT6HiLoActivator; runnable example ExampleSeries_LevelBreakStop_hiLoActivator.

Moving-average channel 10/8/3 (Bernstein)

Jake Bernstein (Short-Term Futures Trading, Probus, rev. ed. 1993, ch. 4, “Moving Average Channel (MAC)”, pp. 73-77) builds a channel of a 10-bar average of the highs and an 8-bar average of the lows and adds “a three-unit period moving average of the close”: “When the three-unit period MA of the close closes above the MAH, a buy signal is generated”, a sell when it closes below the average of the lows (p. 77 prints “below the MAC”; the appendix, pp. 290-291, gives “3-close MA closes below 8-low MA”), and “Such an approach is best used as a reversing system (always in the market)”. t6BernsteinMAC masks the three averages until the longest is full and reverses on the opposite state of the bar before, filling at the next open.

k := max(nh, nl, nc) - 1
mah := h.Average(nh).SetN(k, math.NaN())
mal := l.Average(nl).SetN(k, math.NaN())
t3 := c.Average(nc).SetN(k, math.NaN())
up := t3.GT(mah)
dn := t3.LT(mal)
pos := c.PositionSigned(up.Shift(1), dn.Shift(1), dn.Shift(1), up.Shift(1))
  • Inputs: highs, lows and closes; nh, nl and nc (10, 8 and 3).
  • First valid index: k = max(nh, nl, nc) - 1 for the three averages (9), NaN before, and their flags read 0 there; pos can first be non-zero on bar k + 1 (10).
  • Anchors: the averages and the book equal a loop of the rule bit for bit; the trigger is compared with the same bar’s averages, at the close, strictly, and a position tests only the opposite state, as his ch. 11 reads it (“remained bullish, as the three-period of the close had not closed below the 8-period of the low”). Cross events, t3.CrossesAboveS(mah) and t3.CrossesBelowS(mal), the marks on his charts, give the same book from the bar after the first cross.
  • Traps: the channel’s two averages have different lengths and invert after a fast move, so the trigger can lie above the average of the highs and below that of the lows at once: on the constructed closes 0, 10, …, 110, then 53 four times (highs and lows 1 either side), bar 13 has 71.6 < 72 < 76 and both flags set; the book goes short at bar 14, and up.Sub(dn).Hysteresis(0, 0), which holds on a double state, a bar later. Bernstein does not give the fill; the next open is this page’s convention.
  • Variant: the same-bar close fill, the four flags without Shift(1).
  • Evidence: Bernstein makes “no claims whatsoever about accuracy of performance”; his chart readouts (Figures 4.20 to 4.22) are averages on a last bar without its bars, and Alex Saitta (“Range Breakout Trading In Treasury Bonds”, Stocks & Commodities V13:4, April 1995) uses the 10/8 channel as close-tested exits of a breakout entry. Evidence, not a reference.
  • Pinned by TestRecipeT6BernsteinMAC.

Three- and six-week crossover (Gartley; Penfold)

Brent Penfold (The Universal Tactics of Successful Trend Trading, Wiley, 2021, ch. 6, “Gartley’s Three- and Six-Week Crossover (1935)”, pp. 182-185) tests H. M. Gartley’s rule from Gartley’s 1935 chart, which plots the six-week averages of the highs and lows two weeks late: “When the three-week moving average of the mean closes on a Friday above the six-week moving average of the high (which is offset by two weeks), the model will go long on Monday’s open”, short on the mirror, “Always in the market: a stop and reverse”. t6GartleyCrossover reads the offset as Shift(2) and fills at the next week’s open.

mid := h.Add(l).Div(2).Average(ns).SetN(ns-1, math.NaN())
hiD := h.Average(nl).Shift(d).SetN(nl+d-1, math.NaN())
loD := l.Average(nl).Shift(d).SetN(nl+d-1, math.NaN())
up := mid.GT(hiD)
dn := mid.LT(loD)
pos := h.PositionSigned(up.Shift(1), dn.Shift(1), dn.Shift(1), up.Shift(1))
  • Inputs: weekly highs and lows (the caller’s weekly bars); ns, nl and d in weeks (3, 6 and 2).
  • First valid index: ns - 1 for mid (2) and nl + d - 1 for hiD and loD (7), NaN before (the mask covers the two pads of Shift and the pass-through of Average); pos can first be non-zero on bar 8.
  • Anchors: the helper equals Penfold’s rule as a loop bit for bit; hiD is at least loD on every bar (one window, highs above lows), so no week sets both flags and the book equals up.Sub(dn).Hysteresis(0, 0).Shift(1) and mid.LevelBreakDirection(mid, h.Average(6).Shift(1).SetN(6, math.NaN()), l.Average(6).Shift(1).SetN(6, math.NaN())).Shift(1) bit for bit.
  • Traps: the dossier’s unmasked flags fire on bars 0 and 1, where Shift(2) pads the levels with 0, and seed a long. The comparison is the current week’s mid against the levels displaced two weeks, not the previous week’s displaced level.
  • Variant: Penfold’s data are daily (“Data: Daily”): with wh := h.HighestSince(nw).Shift(1) and wl := l.LowestSince(nw).Shift(1), mid := wh.Add(wl).Div(2).AverageWhere(nw, 3), hiD := wh.AverageWhere(nw, 6).ValueWhen(nw, 2), loD likewise and the same-bar book h.PositionSigned(up, dn, dn, up) equal the weekly recipe on the completed weeks mapped onto their days, bit for bit, filling on each week’s first session.
  • Evidence: Penfold’s results block (24 US futures from 1980, his unprinted VBA, 51 dollars a trade deducted for brokerage and slippage): 3,387 trades averaging 319 dollars after costs. Evidence, not a reference.
  • Pinned by TestRecipeT6GartleyCrossover.

The composition below masks its channel levels with NaN before their first valid index, the bar on which both windows are full, so that FlipStop starts on full windows and the unratcheted latch cannot break on partial ones; the latch reads 0 until its first break. Prices are positive and finite, lengths are in bars and the offsets are in price units.

Ratcheted noise channel (Meyers)

Dennis Meyers (“The German Mark Noise Channel Breakout System”, Stocks & Commodities V16:4, April 1998, pp. 161-168) buys “If the DM price crosses above the highest high price of the DM of the last buy length (blen) days by an amount greater than or equal to xo”, sells on the mirror rule, and ratchets the channel within a position: “when short and when calculating the highest high price (hhp), that hhp cannot be higher than the previously calculated hhp”. His second article (“The Bond Futures Noise Channel-2 Breakout System”, Stocks & Commodities V16:9, September 1998, pp. 399-409) takes four parameters, nhi, xoh, nlo and xod; the first is the case nhi = blen, nlo = slen and xoh = xod = xo. t6NoiseChannel gives the ratcheted stop-and-reverse line and position of FlipStop on the close, and plain, the channel without the ratchet on the high and the low.

k := max(nhi, nlo) - 1
sellLvl := l.Lowest(nlo).Sub(xod).SetN(k, math.NaN())
buyLvl := h.Highest(nhi).Add(xoh).SetN(k, math.NaN())
line := c.FlipStop(sellLvl, buyLvl)
pos := c.FlipStopDirection(sellLvl, buyLvl)
plain := h.LevelBreakDirection(l, buyLvl, sellLvl)
  • Inputs: highs, lows and closes of one length; nhi and nlo in bars; xoh and xod in price units (Meyers’s optimum xo of 0.0090, “90 ticks”, or 70 ticks by segment, on the Deutschemark).
  • First valid index: k = max(nhi, nlo) - 1 for line and pos (NaN before): FlipStop starts long there with the line at the sell level and decides from bar k + 1. plain reads 0 until its first break, at bar k + 1 at the earliest. After a NaN input line and pos stay NaN, while plain holds its state through a NaN high or low.
  • Anchors: no Shift is needed: FlipStop tests bar t against the line of bar t - 1, built from the channel ending at t - 1, Meyers’s “last slen days”. While long the line is at or above the sell level and while short at or below the buy level (the ratchet), and a new position starts from the opposite level of the flip bar. plain is Meyers’s channel without the ratchet, exact to his intraday triggers and his flat start, with strict breaks on both sides; on closes with no offset it is c.Breakout(n, 1).Hysteresis(0, 0). On a hand case after his September sidebar the short line keeps the level of a high that has left the window, while a later, higher high short of the offset does not trigger.
  • Traps: the dossier’s h.Highest(blen).Shift(1) lags the levels twice: on the closes 10, 11, 12, 13, 11.5, 11.4 with three-bar channels and no offset the recipe sells at bar 5, below the lowest close of bars 2 to 4, and the shifted form does not.
  • Departures: the close trigger replaces Meyers’s intraday stops; FlipStop starts long where Meyers is flat until the first break; its ties flip a long on a close at the line and keep a short on one, his first article’s “greater than or equal to” on sells and his second’s “greater than” on buys; and a reversal starts from the flip bar’s own channel (on a constructed reversal at bar 4 the new short line is 12, where the old position’s level, 11, would buy at bar 5). On the test’s walk (20 and 10 bars, offsets 0.25) pos and plain differ on 36 of the 432 bars from plain’s first decision, so the close trigger and the ratchet together move the position on about one bar in twelve there.
  • Variant: close-only data with Meyers’s symmetric parameters is t6NoiseChannel(c, c, c, n, n, xo, xo), the ratcheted Donchian close system for xo = 0.
  • Missing piece: FlipStopHL(h, l, longStop, shortStop), a touch mode testing the low against the line while long and the high while short, with a flat start, for Meyers’s intraday reading.
  • Evidence: Meyers’s walk-forward tests (Deutschemark 1990 to 1997 with 8 out-of-sample trades; T-bonds 1989 to 1997 with 23) report that commissions and slippage of 200 dollars a round trip cut the out-of-sample profits by 5% and 11%; his trade tables reproduce as arithmetic (one price per reversal, always in the market after the first trade) but print no bars. Evidence, not a reference.
  • Method: for valid parameters line and pos are c.NoiseChannel(h, l, nhi, nlo, xoh, xod) and c.NoiseChannelDirection(h, l, nhi, nlo, xoh, xod), bit for bit on every bar.
  • Pinned by TestRecipeT6NoiseChannel; runnable example ExampleSeries_FlipStop_noiseChannel.

The two sections below are Sylvain Vervoort’s TR&NDS stop (“Trailing Resistance And Support Stops”, Stocks & Commodities V27:7, July 2009, pp. 26-30), with his final SVE_TRENDS_Trail listing pinned: its line ends with If(H>=PREV,support,resistance), so the high triggers it on both sides and a tie of the high with the line goes to the support. The book is long only and filled at the close, as in the tests of his series (“Using Initial And Trailing Stops”, Stocks & Commodities V27:5, May 2009: “Only long positions are traded at the closing price the day there is a buy or sell signal”). His modified ATR comes first: the resistance is the close plus k times it. The support’s step-up, 5% when the low runs more than 10% above it, is ValueWhenStepUp; MetaStock’s Wilders seed is not documented, and the recipe takes the seed of RMA, the mean of its first n values. Prices are positive and finite and lengths are in bars; values are NaN before their first valid index and the book reads 0 there. Related: FlipStop, the ratchet-and-flip line of his earlier stops, ATRTrailingStop, and the noise channel beside this page.

Modified ATR (Vervoort)

Vervoort (“Average True Range Trailing Stops”, Stocks & Commodities V27:6, June 2009, pp. 34-40, sidebar “ATR MODIFIED”) limits “the extreme price change of a single bar to a maximum of one and a half times the ATR moving average of the high minus low prices” (in his code the simple average of H-L), takes “just half the size of the gap” into the distances to the previous close, and ends with “Wilder’s average on the largest value: Wilders(diff2,period);”. t6ModifiedATR writes his Ifs as Minimum, Maximum and a 0/1 gap flag times the half gap, drops the first n - 1 bars, which MetaStock leaves undefined, and smooths with RMA.

rng := h.Sub(l)
hilo := rng.Minimum(rng.Average(n).Mul(1.5))
href := h.Sub(c.Shift(1)).Sub(l.Sub(h.Shift(1)).Div(2).Mul(l.GT(h.Shift(1))))
lref := c.Shift(1).Sub(l).Sub(l.Shift(1).Sub(h).Div(2).Mul(h.LT(l.Shift(1))))
diff2 := hilo.Maximum(href).Maximum(lref)
atr := c.Mul(0).Add(diff2.Window(1-n).RMA(n)).SetN(n+n-2, math.NaN())
  • Inputs: highs, lows and closes, positive and finite; n >= 2 (10 for TR&NDS, 5 in the June article).
  • First valid index: 2n - 2 (18 at n = 10), NaN before: RMA seeds on the mean of diff2 over bars n - 1 to 2n - 2, the first n bars MetaStock defines, and the mask covers the +0 that c.Mul(0).Add puts on the bars before.
  • Anchors: the helper equals a loop of the MetaStock listing with the seed and step of RMA bit for bit, and the seed is summed in order, as RMA sums it (on the seed values 1, 2^-53 and 2^-53 it is 1/3, not a third of the exact sum). On a bar with no gap and a slack cap diff2 is the true range, c.TrueRange(h, l, c) bit for bit (980 of the 1,191 bars of the test’s cent-tick walk; a gap or a binding cap only lowers it), and on a path of such bars the result is c.Mul(0).Add(c.TrueRange(h, l, c).Window(1-n).RMA(n)).SetN(n+n-2, math.NaN()) bit for bit.
  • Traps: n = 1 returns +0 on every bar, silently: Window(0) is empty, Add of an empty operand leaves zeros and SetN(0, NaN) masks nothing. MetaStock’s seed is unknown: seeding on the first defined value instead moves the value of bar 18 by 16.7% on the cent-tick walk and keeps it more than 1e-6 away until bar 132 (2.7% to 17%, and bars 114 to 128, on four other walks).
  • Evidence: Vervoort’s June 2009 tests (25 stocks, January 2003 to November 2008, 1,000 dollars a stock, 0.1% each way): the modified ATR(5) x 3.5 trail made 67,994 dollars, 272%, against 61,730, 247%, for his standard ATR trail. Evidence, not a reference.
  • Pinned by TestRecipeT6ModifiedATR.

TR&NDS trailing stop (Vervoort)

Vervoort’s support is “a low price that is equal or lower than the two prices preceding and following this low price”, or on a gap window (“a minimum gap requirement of 0.13%”) a stop “0.55% below the previous high”; between them “Price can move up for a number of days without creating a pivot or gap window support”, so “if I allowed a maximum loss of 10%, the previous stop level will move up by half that amount, or 5% higher”, his listing’s If(L>PREV*1.1,PREV*1.05,PREV). The resistance is the close plus 2.8 modified ATRs of 10 bars; and “the uptrend is only broken when the high price falls through the previous day’s trailing stop”. His rule: “buy when the high price moves above TR&NDS and sell when the high price falls below TR&NDS”. t6TrendsTrail holds the support from its latest pivot or gap with ValueWhenStepUp and steps it up 5% on each bar whose low is more than 1.1 times it, runs FlipStop on the negated high with the negated candidates swapped, so that a high at or above the previous line goes to the support and a high below it to the resistance, and books his long-only trades at the close.

piv := l.Shift(2).LTE(l.Lowest(5)).SetN(4, 0)
gap := l.GT(h.Shift(1).Mul(1.0013)).SetN(1, 0)
cand := l.Shift(2).Mul(piv).Add(h.Shift(1).Mul(0.9945).Mul(gap.Mul(piv.Mul(-1).Add(1))))
sup := cand.ValueWhenStepUp(piv.Maximum(gap), l, 1.1, 1.05)
res := c.Add(atr.Mul(k))
nh := h.Mul(-1)
line := nh.FlipStop(res.Mul(-1), sup.Mul(-1)).Mul(-1)
dir := nh.FlipStopDirection(res.Mul(-1), sup.Mul(-1)).Mul(-1)
prev := line.Shift(1).SetN(1, math.NaN())
pos := c.Position(h.GT(prev), h.LT(prev))
  • Inputs: highs, lows and closes, positive and finite; atr, the modified ATR of the previous section (n = 10); k (2.8).
  • First valid index: the first pivot or gap for sup (bar 1 at the earliest, on a gap window between bars 0 and 1: the pivot is masked on bars 0 to 3 and the gap only on bar 0, where Shift pads with +0, so a gap on bars 1 to 3 sets a support where his MetaStock code, which reads Ref(L,-4), defines none before bar 4); that of atr for res; for line and dir the first bar t0 where both are finite, NaN before; pos reads 0 to t0 (prev is masked on bar 0, where the pad would buy) and can first enter on t0 + 1.
  • Anchors: on the test’s constructed hand case (35 bars: a pivot, two step-ups, a gap, an equal-low pivot, an exit, a re-entry, two step-ups and a tie) the recipe and a loop of his final listing beside it reproduce, to ten digits, a table computed by two independent translations of his listing; the line and direction equal an explicit two-state loop bit for bit; sup is his support bit for bit from its first event (the table’s “S, Vervoort” column: 9.5 stepping to 9.975 and 10.47375 on bars 6 and 7, 11 to 11.55 and 12.1275 on bars 22 and 24), where his PREV is 0 before it and sup is NaN; the pivot contains SwingLowEvent(2) and adds the equal lows. With the step-up his code over sup equals his code over his own support, line and book, on every bar of the hand case, of the tie case tt, of the cent-tick walk tk and of the test’s four walks of 3,000 bars, so the line departs from his code only at the traps below. pos equals dir.GT(0) except after a tie that moves the line to the support without a buy.
  • Traps: the start: FlipStop starts in its own long state, here the resistance side, flat, where his PREV of 0 takes his first branch and starts on the support; the two lines first coincide 5 bars after the start on the hand case, 15 on the test’s cent-tick walk and 13 to 45 on its four walks, and agree from then on but for the departures below. Over the same support the line departs from his code only at that start, on a tie of the high with the line (his third branch resets a ratcheted support line to the support), on the bar after one (his two-bar test fails), and where a line ends beyond its own bar’s high (a resistance below the high, or a flip onto a support above it); the hand case’s tie on bar 32 exits on bar 33 where his code exits on bar 34. The pivot is not the strict SwingLowEvent(2); the dossier’s piv and gap without SetN fire on the first bars against the pad. The p. 28 draft’s third branch tests the close, If(C>PREV,support,resistance), and moves the book (on 144 of the 1,200 bars of the cent-tick walk).
  • Variant: h.FlipStop(sup, res), the dossier’s sketch: it starts on the support, as his code does, but inverts both ties (a support-side tie exits, a resistance-side tie stays).
  • Variant: the support without its step-up, sup := cand.ValueWhen(piv.Maximum(gap)), the form this page printed before ValueWhenStepUp (t6TrendsStop, which the clean-room references pin): it equals sup up to the first step-up after each event and lies below it from there to the next event. On the test’s runs the step-up moves the line on 5 of the hand case’s 35 bars, 46 of the cent-tick walk’s 1,200 and 0, 21, 23 and 0 of the four walks’ 3,000, and the book on none of them; on the constructed ties of tt it raises the support at bar 6 (9.6 to 10.08), the tie of bar 7 is gone and the book first buys on bar 16, not 8.
  • Evidence: his Figure 7 (25 volatile stocks, January 2003 to November 2008, 1,000 dollars a stock, 0.1% each way, long only, at the close): 69,952 dollars, 280%, about 28 trades a stock (the 25 printed net profits sum to 69,951.28). Evidence, not a reference.
  • Pinned by TestRecipeT6TrendsStop; runnable example ExampleSeries_FlipStop_trnds.

SuperTrend

SuperTrend

func (s Series) SuperTrend(h, l, c Series, mult float64, trLen int) Series

SuperTrend returns the receiver series offset by mult x SMA(TrueRange, trLen), i.e. a single ATR-offset band (equivalent to KeltnerSS with priceLen 1), consistent with the TradeStation indicator it is verified against.

Note that this is NOT Olivier Seban’s published SuperTrend indicator, which uses a (high+low)/2 basis, paired upper/lower bands with ratcheting, and a trend flip when the close crosses the opposite band. Values from this function will not match SuperTrend implementations found on other platforms. For the ratcheting and flipping indicator (TradingView’s ta.supertrend, TA-Lib’s TA_SUPERTREND), use SuperTrendLine and SuperTrendDirection.

The band is added with Add, so it is right-aligned with the receiver at the most recent bar: when h, l and c are shorter than s, the receiver’s leading bars without a band are zero. When h, l and c differ in length from each other, TrueRange is empty and the result is all zeros. KeltnerSS, which requires all four lengths to be equal, gives zeros whenever any differs.

Bars 0 to trLen-2 are s[i] + mult*TrueRange[i], because the true-range average passes its input through.

Trade State

Positions, stops and shadow trades

The methods of this category build a position from flags (Position; PositionArmed, with an armed state from which a re-entry flag takes the next trade after a stop; PositionSigned; and PositionSignedRules with a rule set of stops and targets frozen at entry, a time limit that an own entry can renew, a minimum hold and three reversal rules), read the trade open on each bar (EntryValue, BarsSinceEntry, HighestSinceEntry, OpenFavourableExcursion and the others) and post each closed trade on its exit bar (TradeResult, TradeFavourableExcursion, and PositionSignedExitValue, the value at which the rule set closed it). The rule set is a PositionRules value, whose zero value is the rule set of PositionSigned, and each of its four levels a PositionLevel. The compositions at the end of the category use them for stops that cut a base position, for latched entries, and for filters that take or skip the trades of an unfiltered shadow system. They share the conventions below, which their sections cite rather than repeat.

These latches are a signal state at bar level: from flags, a rule set and levels frozen at entry they say which trade a rule holds on each bar, and at most the level a resting exit was read at. They are 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 stop pattern. A stop cuts a base position pos with three statements, as EntryValue’s doc prints them: the rule’s raw flags masked to the bars after the entry bar, a latch from the first hit until the base’s next entry, and the cut.

hit := raw.Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • The mask comes first. SumSince includes its event bar, so a raw hit on an entry bar would otherwise latch from it, and a trade cut on its entry bar would vanish from stopped, with no entry and no result. So the first bar that can be cut is the bar after the entry, and stopped.Entries() equals pos.Entries(); the entry-bar term of the excursion stops is the documented exception.
  • stopped is pos until the hit and flat from there to the base’s exit. A trade that is not hit keeps its TradeResult bit for bit, and a stop switched off (a NaN unit, or a +Inf threshold while the excursion stays finite) gives stopped == pos.
  • A cut bar is 0, or -0 on the cut bars of a short trade (-1 * 0), which every view reads as flat. Compare positions by value, or normalise them with Add(0).

Three exit timings, named in words in every section:

  • close test, close fill: the rule tests a bar’s close and the trade leaves at that close, so c.TradeResult(stopped) posts the exit.
  • resting stop: a level set at the close of bar t-1 rests during bar t and is touched by the low (by the high for a short trade), touch-inclusive; the rule reads the level with Shift(1). The fill, the level or the open when the bar opens beyond it, is the caller’s: the resting-stop fill series gives it, and for a level frozen at entry PositionSignedExitValue posts it.
  • close test, next-open exit: the rule tests a close and the trade leaves at the next open. The hit is shifted, not the cut, so o.TradeResult(stopped) posts the fill; shifting the cut instead would carry a cut onto the next trade’s entry bar.

Re-entry. A cut trade stays flat until the base’s next entry. The recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat; where one does, the source re-enters inside the base trade, which PositionSignedRules does for a stop at a level frozen at entry: its stopped trade is flat and takes the next entry flag, as Position takes the next entry after an exit flag. The stop pattern stays the form for the other stops, and the condition is the test of where it suffices. For a long base and its long entry flag the condition is a diagnostic to sum (the short side is the mirror with LT(0)):

reentry := entry.Mul(pos.GT(0)).Mul(stopped.EQ(0)).Mul(pos.BarsSinceEntry().GT(0))
atExit := entry.Mul(pos.Exits()).Mul(pos.Shift(1).GT(0)).Mul(stopped.Shift(1).EQ(0))

atExit counts the base’s own exit bar after a cut: the base, exiting there, ignores the entry flag, and a source that has been flat since its cut takes it. Where both sum to 0, the stopped position is the source’s position bar for bar. With a level frozen at entry and the stop’s touch, the stopped position equals PositionSignedRules with that level as its stop, by value, on every bar before the first bar on which either fires, and can part from that bar on, where the latch re-enters and the pattern waits for the base’s next entry. Where a source re-enters after a stop on a re-entry rule of its own rather than on its entry flag, as Kaufman’s volatility re-entry on volatility-estimators/t6-volatility-gates.md does, PositionArmed is that latch: its stop flag closes the trade and arms it, and its re-entry flag takes the next trade, both flags computable without the latch’s output. The sources that re-enter by their own entry rules stay loops outside this package: Pruitt’s level entries, Davey’s stop-and-reverse and flat-gated entries, D’Errico’s base without an exit, the re-issued stop orders of LUXOR and Carver’s Starter System. Saitta’s held range on true-range/t6-range-contraction.md is a composition on the latch: a long trade entered on the detection, with the range’s low as its stop and its high as its target, both read on the close, and EntryValue reading the pair back.

Units. Every threshold, floor, tier, distance and stop that a source gives in dollars is taken in price units here; a point value is the caller’s conversion. The volatility unit is a parameter of each recipe, and the sources use different ones: ATR (TradeStation’s AvgTrueRange, a simple average of TrueRange), ATRWilder (TA-Lib’s Wilder average, bar 0’s true range left out) and c.TrueRange(h, l, c).RMA(n) (TradingView’s and StockCharts’). ATRWilder and the RMA form differ by more than one part in a million for roughly 8n to 9n bars after the start (and in the last bits for about 30n), and ATR is another average on every bar, so a stop needs the unit its source calibrated. Mask a unit’s warm-up with NaN, which reads as “no stop” because a comparison with NaN is 0, or with +Inf for a ratcheted line.

Arithmetic. In these blocks every product with a series is a Mul call and every sum or difference an Add or a Sub, each rounded once, and no call fuses a product into a sum, so a printed form is its source’s arithmetic without fusion. A product of two scalars, as float64(5 * pt), is the argument of a conversion, which rounds it once too.

Pads and fill prices. Shift(1) pads bar 0 with +0, which compares as a level of 0: l.LTE(0) is 0 and h.GTE(0) is 1 for positive prices. Every bar-0 read of a padded level in these recipes is harmless (the stop mask removes it, or a padded flag multiplies it), and a fill price built on one, as in the stretch fills and LUXOR, is meaningful on fill bars only.

Slices. A stop needs one complete trade inside the slice: a trade open on its first bar is taken to begin there, with that bar’s anchor. A shadow filter also depends on the first shadow trade inside the slice, whose gate starts open.

Printed numbers. A number a section quotes from a source is printed; a fixture built around printed numbers, or a hit price the source does not give, is constructed, and the sections say so. Printed numbers that a test asserts against a composition were read by two readers; Penfold’s Table 9.9, which a test checks only for adding up, had one reader and that sum as its check. StockCharts’ two-decimal rows are compared to two decimals. TradeStation, TRDSIM and AmiBroker results are evidence only, not reference data.

BarsSinceEntry

func (pos Series) BarsSinceEntry() Series

BarsSinceEntry counts the bars since the entry of the trade that is open on each bar. On an in-position bar t it is t-e, with e the entry bar of the trade open at t, so it is 0 on an entry bar, a reversal bar included; it is NaN on every flat bar, the exit bar included. The receiver is the position, read by sign as described for Entries (NaN, 0 and -0 are flat, the size is not read) with no alignment, and the result has the receiver’s length. The values are exact non-negative integers stored as float64, or NaN. There is no warm-up: a trade open on bar 0 starts its count there.

It is pos.Entries().BarsSince() masked to NaN where the position is flat. StateAge counts 1 on the first bar of a spell, so for a position of 0, 1 and -1 values pos.StateAge().Sub(1) gives the same values on in-position bars. Timing is that of Position: a hold of k closes a trade on the bar on which this count would reach k, and a signal passed with Shift(1) puts the entry bar one bar later.

BarsSinceExit

func (pos Series) BarsSinceExit() Series

BarsSinceExit counts the bars since the most recent exit: t-x, with x the most recent exit bar at or before t, on every bar, flat or in position, so it is 0 on an exit bar (a reversal bar included) and keeps counting through the next trade. It is NaN before the first exit, which is its warm-up. The receiver is the position, read by sign as described for Entries (NaN, 0 and -0 are flat, the size is not read) with no alignment, and the result has the receiver’s length. The values are exact non-negative integers stored as float64, or NaN.

It equals pos.Exits().BarsSince(). Timing is that of Position: an exit bar is the first bar on which the trade is no longer held (or is held the other way), so it is the bar of the signal for a sell on the close and the bar after it for a signal passed with Shift(1).

Entries

func (pos Series) Entries() Series

Entries marks the entry bars of a position series: 1 on every bar on which a trade opens, and 0 (+0) elsewhere. The receiver is the position, read by sign and with no alignment (the result has len(pos)): a bar is in position when its value is non-zero and not NaN, so NaN, 0 and -0 are flat while +Inf and -Inf are in position; a positive value is long and a negative one is short, and the size is not read, so 0.5, 1 and 2 are all long. A regime code such as -1 is therefore read as short: when a series is not a position, gate it with GT(0) first. The direction of a position as +1, -1 or 0 is pos.GT(0).Sub(pos.LT(0)), which reads NaN as 0 and an infinite position by its sign, where pos.Div(pos.Abs()) gives NaN.

A bar is an entry when it is in position and either it is bar 0, or the bar before is flat, or the bar before has the other direction. A change of size within one direction is no event, a NaN inside a trade closes it and the next in-position bar opens a new trade, and a reversal (a change of direction with no flat bar between) is an entry and an exit on one bar, so it is 1 in both Entries and Exits. The result is not NaN anywhere, and there is no warm-up: bar 0 is an entry when pos[0] is in position, unlike pos.Delta(1).GT(0), which is 0 on bar 0 because Delta leaves bar 0 at 0. Timing follows Position: an entry bar is the first bar on which the position is held, which is the bar of the signal for a buy on the close and the bar after it for a signal passed with Shift(1).

EntryValue

func (s Series) EntryValue(pos Series) Series

EntryValue is the value of the receiver on the entry bar of the trade that is open on each bar: s[e] verbatim, with e the entry bar of the trade open at i, NaN, infinities and -0 included. For pos of the receiver’s length it is s.ValueWhen(pos.Entries()) masked to NaN where the book is flat (a longer pos reads its first paired bar as an entry, which pos.Entries() does not), and it plays the part of the TradeStation EntryPrice, a behavioural reference only. Unlike EntryPrice it is NaN while flat; the entry value of the last trade, held after its exit (as a re-entry at the original entry price needs), is s.ValueWhen(pos.Entries()).

pos is right-aligned with the receiver on its own, at the most recent bar (a longer pos contributes only its last len(s) values, and a shorter or empty one leaves the earlier receiver bars without a paired value), and is read by sign as described for Entries over the aligned values, so the first paired bar is an entry when it is in position. On every receiver bar that is flat (before the overlap, flat in pos, or the exit bar) the result is NaN. A reversal bar starts the new trade, so there the result is the new trade’s entry value, which is s[i] itself. There is no other warm-up.

Timing: pos[t] is the position held after bar t’s decision. For a buy on the close, pass the close as the receiver; for a buy at the next bar’s open, build pos from signal.Shift(1) and pass the open.

The usual use is a fixed stop. This recipe leaves a stopped position flat from the stop bar until the next entry of the base system, and tests the stop only on the bars after the entry:

pos := c.Position(entry, exit)
hit := c.LT(c.EntryValue(pos).Sub(stop)).Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))

Here stop is a distance in price units. PositionSignedRules is the latch that re-enters after a stop inside a base trade, with the stop level sampled on the entry bar and frozen for the trade.

Exits

func (pos Series) Exits() Series

Exits marks the exit bars of a position series: 1 on every bar on which a trade closes, and 0 (+0) elsewhere. The receiver is the position, read by sign and with no alignment (the result has len(pos)), as for Entries: NaN, 0 and -0 are flat, +Inf and -Inf are in position, a positive value is long, a negative one is short, and the size is not read. A regime code such as -1 is therefore read as short, so gate it with GT(0) first when it is not a position.

A bar is an exit when it is not bar 0, the bar before is in position, and this bar is flat or has the other direction. So bar 0 cannot be an exit, a NaN inside a trade closes it, and a reversal bar is an exit as well as an entry. The result is not NaN anywhere, and there is no warm-up. Timing follows Position: an exit bar is the first bar on which the trade is no longer held (or is held the other way), which is the bar of the signal for a sell on the close and the bar after it for a signal passed with Shift(1). Exits is not a Delta test: pos.Delta(1).LT(0) would flag a fall in size from 2 to 1, which is no exit, and would miss the exit of a short trade, whose step is positive.

HighestSinceEntry

func (s Series) HighestSinceEntry(pos Series) Series

HighestSinceEntry is the highest value of the receiver since the entry of the trade open on each bar, the entry bar included. On the entry bar it is s[i] verbatim; on a later bar of the trade it is max(out[i-1], s[i]) with Go’s builtin max, so a NaN in s from the entry bar on gives NaN until the trade ends, max prefers +0 over -0, and infinities are ordinary values. For pos of the receiver’s length it is exactly the recursion of s.HighestSince(pos.Entries()) over the entry bars, masked to NaN where the book is flat.

pos is right-aligned with the receiver on its own, at the most recent bar, and is read by sign as described for Entries over the aligned values: a longer pos contributes only its last len(s) values, and a shorter or empty one leaves the earlier receiver bars without a paired value. The result is NaN on every receiver bar that is flat (before the overlap, flat in pos, or the exit bar). A reversal bar starts the new trade, so there the result is s[i] of the new trade. There is no other warm-up.

The entry bar is included. With a buy-on-close entry the entry bar’s high and low happened before the fill, so a close-anchored excursion passes the close: pass the close as the receiver, and the entry bar contributes the fill itself. A next-open entry (flags shifted by one with Shift(1), anchored on the open, with the high as the receiver) is right to include the entry bar, because the whole bar comes after the fill. Timing: pos[t] is the position held after bar t’s decision.

LastTradeResult

func (s Series) LastTradeResult(pos Series) Series

LastTradeResult is the result of the most recent closed trade: posted on its exit bar, as TradeResult computes it, and held on every later bar, flat or in position, until the next exit replaces it. It is NaN before the first exit, which is its warm-up, and a trade still open on the last bar is not closed and has no result. A result that is NaN replaces the held value like any other, because exits are read from pos. pos is right-aligned with the receiver on its own, at the most recent bar, and is read by sign as described for Entries over the aligned values; the result is in the receiver’s units, per unit of position, and the size of pos is not read. Timing is that of TradeResult: pos[t] is the position held after bar t’s decision.

It gives the last-trade-loser gate of Turtle System 1 (Pruitt, The Ultimate Algorithmic Trading System Toolbox, 2016, ch. 3). Pruitt skips an entry after a winner; LT(0) also skips after a draw, and LTE(0) lets a draw through. With shadow the position of the unfiltered system, the gate on bar t is 1 when the last trade closed through bar t was a loss or a draw, and on every bar before the shadow’s first exit, where the result is NaN: there is no last trade to have won, so Pruitt’s filter takes the first signal. The gate is open from the start and is not shifted:

last := c.LastTradeResult(shadow)
none := shadow.Exits().CumSum().EQ(0)
gate := last.LTE(0).Add(none).GT(0)

On a reversal bar the unshifted gate reads the trade that closes there, whose exit fill comes before the new entry’s within the bar: the previous signal of the entry. The filtered system takes or skips whole shadow trades, the masked shadow:

take := gate.ValueWhen(shadow.Entries()).GT(0)
real := shadow.Mul(take)

Every real trade is then a shadow trade with the same bars and result, and the skipped ones are shadow.Mul(take.EQ(0)); real is -0 on the masked bars of a skipped short trade. The “Trade State” compositions of the manual give this filter with the Turtle failsafe.

The gate shifted by one bar is the next-bar order form: TradeStation places the orders for bar t at the close of bar t-1, so the gate on bar t reads the last trade as it stood after bar t-1. On a shadow built by Position, which cannot reverse, a trade that closes on bar t is still the last trade on bar t+1, its earliest re-entry, so a gate and its shifted form agree on every entry bar, and the filtered system can take the gated entries (here with LT(0), which also skips after a draw):

shadow := c.Position(entry, exit)
last := c.LastTradeResult(shadow)
gate := last.LT(0).Shift(1).Add(shadow.Exits().CumSum().EQ(0)).GT(0)
pos := c.Position(entry.Mul(gate), exit)

Without the Add, the shifted gate is 0 until the first shadow trade has closed, since the result is NaN before the first exit. On a reversal bar the two gates differ: the shifted gate reads the trade before last, so a shadow that can reverse (one built by PositionSigned) needs the unshifted gate.

The percent result on exit bars, as a fraction of the entry value (scale it by 100 for percent points), is:

res := s.TradeResult(pos)
pct := res.Div(s.ValueWhen(pos.Entries()).Shift(1))

The Shift(1) matters on a reversal bar: there the entry flag of the new trade fires on the exit bar itself, so s.ValueWhen(pos.Entries()) already holds the new trade’s entry value, and the value held on the bar before is the entry value of the trade that is closing. Div’s zero rule makes bar 0 (the Shift pad) and a zero entry value read 0 rather than NaN or an infinity.

LowestSinceEntry

func (s Series) LowestSinceEntry(pos Series) Series

LowestSinceEntry is the lowest value of the receiver since the entry of the trade open on each bar, the entry bar included. On the entry bar it is s[i] verbatim; on a later bar of the trade it is min(out[i-1], s[i]) with Go’s builtin min, so a NaN in s from the entry bar on gives NaN until the trade ends, min prefers -0 over +0, and infinities are ordinary values. For pos of the receiver’s length it is exactly the recursion of s.LowestSince(pos.Entries()) over the entry bars, masked to NaN where the book is flat.

pos is right-aligned with the receiver on its own, at the most recent bar, and is read by sign as described for Entries over the aligned values: a longer pos contributes only its last len(s) values, and a shorter or empty one leaves the earlier receiver bars without a paired value. The result is NaN on every receiver bar that is flat (before the overlap, flat in pos, or the exit bar). A reversal bar starts the new trade, so there the result is s[i] of the new trade. There is no other warm-up.

The entry bar is included. With a buy-on-close entry the entry bar’s high and low happened before the fill, so a close-anchored excursion passes the close: pass the close as the receiver, and the entry bar contributes the fill itself. A next-open entry (flags shifted by one with Shift(1), anchored on the open, with the low as the receiver) is right to include the entry bar, because the whole bar comes after the fill. Timing: pos[t] is the position held after bar t’s decision.

OpenAdverseExcursion

func (s Series) OpenAdverseExcursion(pos, h, l Series) Series

OpenAdverseExcursion returns, on every in-position bar of a trade, the worst excursion the trade has suffered so far: the open-trade view of TradeAdverseExcursion, and, by analogy with MaxPositionProfit, the price-unit counterpart of TradeStation’s MaxPositionLoss; a larger value is a deeper excursion against the trade. It reads its inputs as OpenFavourableExcursion does, with the sides swapped: on a bar t of a trade entered on bar e, e <= t, the result is one subtraction, E - minL for a long trade and maxH - E for a short one, with E = s[e], minL the lowest l and maxH the highest h over e..t, both ends included, Go’s min and max applied left to right from the entry bar’s own value. pos is right-aligned with the receiver and read by sign. Every flat bar is NaN, the exit bar included, and a reversal bar starts the new trade, so there the result is s[t] - l[t] for a new long trade and h[t] - s[t] for a new short one; the entry bar is the first valid bar, with no other warm-up.

Only the path series of the side in position is read: l on long bars and h on short bars, so a NaN in the other series does not affect the result. A NaN anchor on the entry bar, or a NaN in the side’s path on any bar from the entry bar on, gives NaN from there to the trade’s end. The result is not floored: an anchor outside the entry bar’s range can give a negative value. Infinities and signed zeros follow the subtraction and Go’s max and min, as in OpenFavourableExcursion.

Validation comes first: when len(h) or len(l) differs from len(s), the result is Series{}, empty and not nil; pos is not a length error. One pass over the bars, O(N) time and O(1) extra memory; the result is fresh and prefix-causal, the inputs are not modified, and concurrent calls are safe.

For a pos of the receiver’s length, with ev, hi and lo as in OpenFavourableExcursion, the result is ev.Sub(lo) on long bars and hi.Sub(ev) on short bars, bit for bit. The signed composition it replaces, ev.Sub(lo).Mul(pos.GT(0)).Add(hi.Sub(ev).Mul(pos.LT(0))), agrees by value on finite data, but a NaN high inside a long trade makes it NaN to the trade’s end, since NaN times 0 is NaN. The view posts each trade’s excursion before the exit bar: TradeAdverseExcursion posts the closed excursion over e..x on the exit bar x, where this view is NaN unless x is a reversal bar, where it reads the new trade; where the exit bar makes no new adverse extreme, the closed value equals this view on bar x-1. The value on bar t includes bar t’s own high and low, so a stop resting during the next bar reads it with Shift(1).

OpenFavourableExcursion

func (s Series) OpenFavourableExcursion(pos, h, l Series) Series

OpenFavourableExcursion returns, on every in-position bar of a trade, the best excursion the trade has made so far: the open-trade view of TradeFavourableExcursion, and the price-unit counterpart of TradeStation’s MaxPositionProfit, the maximum profit of the open position, which Davey’s exits compare with amounts converted to dollars per contract. The receiver s is the anchor, the series a trade is entered at (c for a fill on the close, o for a fill at the next bar’s open, with pos built from flags passed with Shift(1)), and h and l are the path, the highs and lows.

pos is right-aligned with the receiver at the most recent bar, receiver bars before the overlap being flat, and read by sign: long where it is positive, short where it is negative, flat where it is 0, -0 or NaN; the size is not read. On a bar t of a trade entered on bar e, e <= t, with E = s[e], maxH the highest h over e..t and minL the lowest l over e..t (both ends included, Go’s max and min applied left to right from the entry bar’s own value), the result is one subtraction: maxH - E for a long trade and E - minL for a short trade. Every flat bar is NaN, the exit bar included, as in EntryValue and BarsSinceEntry. A reversal bar starts the new trade, so there the result is h[t] - s[t] for a new long trade and s[t] - l[t] for a new short one. The entry bar is the first valid bar, with no other warm-up.

Only the path series of the side in position is read: h on long bars and l on short bars, so a NaN in the other series does not affect the result. A NaN anchor on the entry bar, or a NaN in the side’s path on any bar from the entry bar on, gives NaN from there to the trade’s end. The result is not floored: an anchor outside the entry bar’s range can give a negative value. Infinities are ordinary values (an infinity minus the same infinity is NaN), and signed zeros follow the subtraction and Go’s max (+0 over -0) and min (-0 over +0).

Validation comes first: when len(h) or len(l) differs from len(s), the result is Series{}, empty and not nil, and a nil h or l has length 0. pos is not a length error: a shorter pos leaves the earlier bars flat and a longer one is read from its most recent bars. The method makes one pass over the bars, O(N) time and O(1) extra memory; the result is fresh and prefix-causal, the inputs are not modified, even when they alias each other, and concurrent calls are safe.

For a pos of the receiver’s length, with ev := s.EntryValue(pos), hi := h.HighestSinceEntry(pos) and lo := l.LowestSinceEntry(pos), the result is hi.Sub(ev) on long bars and ev.Sub(lo) on short bars, bit for bit. The signed composition it replaces, hi.Sub(ev).Mul(pos.GT(0)).Add(ev.Sub(lo).Mul(pos.LT(0))), agrees by value on finite data, but a NaN low inside a long trade makes it NaN to the trade’s end, since NaN times 0 is NaN; this view reads only the side in position.

The view posts each trade’s excursion on the trade’s bars, before the exit bar: the closed view TradeFavourableExcursion posts the excursion over e..x on the exit bar x, where this view is NaN, unless x is a reversal bar, where this view already reads the new trade. On an exit bar x of a long trade with h[x] <= hi[x-1] (a short trade with l[x] >= lo[x-1]) the closed value equals this view on bar x-1 bit for bit; in general it is max(hi[x-1], h[x]) - ev[x-1], which recomputing from this view gives only to rounding.

The value on bar t includes bar t’s own high and low, so a stop resting during the next bar reads it with Shift(1): o.OpenFavourableExcursion(pos, h, l).GTE(thr) is the breakeven arming of a next-open trade. On a buy-on-close entry the entry bar’s high precedes the fill: pass the close as h and l for a close-only excursion.

OpenMAE

func (s Series) OpenMAE(pos, h, l Series) Series

OpenMAE is an alias for OpenAdverseExcursion: it returns s.OpenAdverseExcursion(pos, h, l) bit for bit, with the same validation (Series{} when len(h) or len(l) differs from len(s)), warm-up and timing.

OpenMFE

func (s Series) OpenMFE(pos, h, l Series) Series

OpenMFE is an alias for OpenFavourableExcursion: it returns s.OpenFavourableExcursion(pos, h, l) bit for bit, with the same validation (Series{} when len(h) or len(l) differs from len(s)), warm-up and timing.

Position

func (s Series) Position(entry, exit Series, hold ...int) Series

Position is the long-only latch of a rule-based book: from an entry flag and an exit flag it says, bar by bar, whether a trade is open. The receiver gives the length and the alignment of the result, and its values are not read. The result is 0 (flat, +0) or 1 (long) on every bar and is not NaN anywhere.

A flag fires when it is non-zero and not NaN, so +Inf, -Inf and negative values fire while 0, -0 and NaN do not; the size and the sign of a flag are not read. Each flag is right-aligned with the receiver on its own, at the most recent bar: a flag series longer than the receiver contributes only its last len(s) values, a shorter one behaves as if it had leading values that do not fire, and an empty or nil one does not fire on any bar.

The latch is flat before bar 0. On a flat bar an entry flag opens the trade, and the exit flag is not tested on that bar. On a long bar the exit flag (or the time limit) closes the trade, and the entry flag is ignored. So with both flags on a flat bar the latch enters, with both on a long bar it exits, an exit flag while flat does nothing, and an entry flag while long does nothing (no pyramiding). Two trades are always separated by at least one flat bar, and the earliest re-entry after an exit is the next bar.

The optional hold is a time limit in bars; only hold[0] is read, and further values are ignored. With hold k > 0 the trade also exits on bar e+k, where e is its entry bar, which is the bar on which BarsSinceEntry would reach k, unless its exit flag closed it earlier. Connors and Alvarez’s rules “Buy on the close” and “Exit on the close 5 trading days later” (Short Term Trading Strategies That Work, 2008, ch. 8) are hold 5 with the signal as the entry flag and a nil exit flag. No hold, or hold 0, means no time limit, and a negative hold gives an all-flat result. Any hold is safe, math.MaxInt included, because bar differences are compared rather than sums. TradeStation’s BarsSinceEntry >= k test with next-bar fills (as in Davey’s code) holds one bar longer than hold k with close fills.

There is no warm-up: bar 0 can enter and the first valid index is 0. The result at bar t is the position held after bar t’s decision, and a flag acts on its own bar. “Buy on the close” passes the signal as the entry flag; “buy at the next bar’s open” passes signal.Shift(1) instead, which pads with +0 and so does not fire, and then passes the open as the receiver of the views (EntryValue and the others) so that they are anchored on the fill.

For flags whose values are exactly 0 or 1, that are not both 1 on any bar, and that have the same length as s, the latch agrees bit for bit with the Hysteresis latch:

s.Position(entry, exit) == entry.Sub(exit).Hysteresis(0, 0).GT(0)

The identity is stated for 0/1 flags that are not both 1 on a bar; a same-bar tie or a negative flag value breaks it. On a bar where both fire the two differ: Hysteresis holds its state, while Position enters when flat and exits when long.

Under a sliding slice of history the position is undefined until the first entry inside the slice, and the views until that trade’s exit, so the warm-up is one complete trade. With a hold the time limit counts from the slice’s own entry bar, so an entry flag that fires inside a trade (ignored in the full history) can keep a slice out of step for longer. To carry a known open position across the start of a slice, force an entry on the first bar, for example entry.SetN(1, 1); with a hold this starts the clock on bar 0, not on the true entry bar. PositionSigned adds short trades and reversals. PositionArmed adds an armed state after a stop, from which a re-entry flag takes the next trade.

PositionArmed

func (s Series) PositionArmed(entry, exit, stop, arm, reentry Series, hold ...int) Series

PositionArmed is the long-only latch of Position with a third state, armed, that a stop or an arm flag opens and from which a re-entry flag takes the next trade.

The receiver gives the length of the result and the alignment of the flags; its values are not read. The result holds 1 on each bar on which a trade is open and +0 otherwise, flat or armed alike; it is not NaN anywhere, it is not -0 anywhere, and the armed state is not shown. An empty or nil receiver gives an empty, non-nil result. The latch starts flat and disarmed before bar 0 and holds, after each bar, one of three states: flat (disarmed), armed (flat, with the re-entry path open) or in (a trade is open). There is no warm-up: bar 0 can enter. The result at bar t is the position held after bar t’s decision, and a flag acts on its own bar; for a fill at the next bar’s open pass the flags with Shift(1), as for Position.

A flag fires when it is non-zero and not NaN: +Inf, -Inf, negative and fractional values fire; 0, -0 and NaN do not. The size and the sign of a flag are not read. Each of the five flags is right-aligned with the receiver on its own, as the flags of Position are: a longer flag contributes its last len(s) values, a shorter one leaves the earliest bars without a value, and an empty or nil one has no value on any bar. A bar without a value does not fire, and no length is validated.

On each bar the state held after the bar before decides which flags are read, and the first rule of that state that holds decides the bar. While flat the latch reads the entry flag, which opens a trade, and otherwise the arm flag, which arms it; the exit, stop and re-entry flags are not read while flat, so a re-entry flag before any stop or arm does not open a trade, and a stop while flat does not arm. While armed it reads the entry flag and the re-entry flag, either of which opens a trade, and nothing else: the latch stays armed until a trade opens, so neither the exit flag nor the stop flag disarms it, a repeated arm does nothing, and there is no time limit while armed. While in a trade it reads the exit flag and the time limit, either of which closes the trade and leaves the latch flat and disarmed, and otherwise the stop flag, which closes the trade and leaves the latch armed; the entry, re-entry and arm flags are not read while in a trade.

So an exit wins over a stop on one bar, and the time limit wins over a stop on its bar. No entry is read on the bar on which a trade closes, by the exit, the time limit or the stop, so two trades are separated by at least one flat or armed bar, as in Position. A trade’s exit, stop and time limit are first read on the bar after its entry bar, from flat or from armed. A trade opened from armed is a new trade in every respect: its entry bar is the bar on which it opened, Entries marks it, and its hold counts from it. An entry and an arm on one flat bar open a trade, and the arm is not remembered; an arm and a re-entry on one flat bar arm the latch, and the re-entry is read from the next bar on; an entry and a re-entry on one flat bar open one trade.

The hold is the time limit of Position; only hold[0] is read, and further values are ignored. With hold k > 0 a trade also closes on bar e+k, where e is the bar on which it opened, from flat or from armed, unless its exit or its stop closed it earlier; an entry flag while in a trade does not restart the count, and a trade closed by its stop takes its time limit with it, since the time limit is read only in a trade. The time limit is an exit: it leaves the latch flat and disarmed. No hold, or a hold of 0, means no time limit. A negative hold gives a result of all +0 of the receiver’s length; it is the only invalid input. math.MaxInt is a valid, inert hold: bar differences are compared rather than sums, so no sum overflows.

s.PositionArmed(entry, exit, nil, nil, nil, hold…) equals s.Position(entry, exit, hold…) for every hold, and so does s.PositionArmed(entry, exit, nil, nil, reentry, hold…): without a stop or an arm flag the latch is not armed, so a re-entry flag alone changes nothing. An armed state without a re-entry flag is a flat one: s.PositionArmed(entry, exit, stop, arm, nil, hold…) equals s.Position(entry, closes, hold…), where closes is 1 on the bars on which exit or stop fires and +0 elsewhere, which for flags of 0 and 1 at the receiver’s length is s.Position(entry, exit.Add(stop).GT(0), hold…); the same holds with a re-entry flag that fires only where the entry flag does. s.PositionArmed(nil, exit, nil, arm, entry), with any hold, is the gated latch: an entry is taken on a bar on which no trade is open only if the arm flag fired on a bar after the previous trade’s exit bar, or on any bar before the first trade, and before the entry bar, and a trade closes on its exit flag or its time limit; an arm on a bar of an open trade, on the exit bar itself or on the entry bar does not count.

Every flag must be computable without the latch’s output: a flag that reads the result, a level frozen at the latch’s own entries, or anything else the latch decides is a feedback loop, which needs a loop of its own rather than this method. Kaufman’s trend system with a volatility exit and re-entry (“Is It Too Volatile To Trade?”, 2022, Listing 2) is c.PositionArmed(up.Mul(av.LT(lvlX)), dn, av.GT(lvlX), nil, av.LT(lvlR)), with up and dn the bars on which his moving average rises and falls, av the annualised volatility, lvlX his exit level (the median of av plus two of its standard deviations) and lvlR his re-entry level (the median plus one): his signal, set by a trend entry, is reset by a trend exit while long, kept by a volatility exit and reset by a bar with both, so his flat book with the signal set is the armed state; from armed either path opens a trade, the re-entry ignoring the average’s direction, and from flat the trend entry alone; a nil re-entry flag is the re-entry path off. The Holy Grail of Raschke and Connors (Street Smarts, 1995, ch. 10), whose rule 6 asks for the ADX to turn up above 30 again after a successful trade before another entry, is the case in point of that rule; the Holy Grail’s re-arm after a trade’s exit is such a loop: its exit reads levels frozen at fills the gate decides, so its page keeps c.Position(arm, brk).

These latches are a signal state at bar level: from flags, a rule set and levels frozen at entry they say which trade a rule holds on each bar, and at most the level a resting exit was read at. They are 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).

Under a sliding slice of history the latch is path-dependent: the position is undefined until the first entry inside the slice, so the warm-up is one complete trade, and with a hold the clock starts on the slice’s own entry bar. The armed state, which is not shown, adds to it: a slice that starts while the full history is armed, after a stop and before the re-entry, starts flat and disarmed and takes no re-entry until it is armed inside the slice. To carry a known open trade across the start of a slice, force an entry on its first bar with entry.SetN(1, 1), where a hold then starts; to carry a known armed state, force an arm with arm.SetN(1, 1), or with make(Series, len(s)).SetN(1, 1) for a nil arm flag, after which a re-entry is read from the next bar on, so a re-entry on the slice’s first bar needs entry.SetN(1, 1) instead.

Each bar costs O(1), the state is O(1), and PositionArmed allocates only its result, a new Series that shares storage with no input; no input is modified.

PositionSigned

func (s Series) PositionSigned(longEntry, longExit, shortEntry, shortExit Series, hold ...int) Series

PositionSigned is the long and short latch with reversals: from four flags it says, bar by bar, whether the book is long (+1), short (-1) or flat (0, +0). The receiver gives the length and the alignment of the result and its values are not read, as with Position; the result is not NaN on any bar, and there is no warm-up. Each flag is right-aligned with the receiver on its own, at the most recent bar, and fires when it is non-zero and not NaN; a longer flag series contributes only its last len(s) values, and an empty or nil one does not fire on any bar. The optional hold is read and validated as for Position: hold[0] is a time limit in bars, none or 0 means no limit, and a negative hold gives an all-flat result.

The book is flat before bar 0, and on each bar the transition depends on the position held after the bar before. When flat, the long entry opens a long trade and the short entry opens a short trade; with both entries on one bar the book stays flat, and the exits are not read while flat. When long, a short entry reverses the book to short whatever the exits do, including on the bar on which the time limit is reached; otherwise the long exit, or the time limit, closes the trade. When short, a long entry reverses the book to long, and otherwise the short exit, or the time limit, closes the trade. The own entry flag is ignored while in position, and on a reversal bar the new trade’s own exit is not tested, so a short entry with a short exit on a long bar gives -1. A reversal closes one trade and opens the next on the same bar, and it is the only way a trade can start on the bar another ends. This is rule 3 of Williams (Long-Term Secrets to Short-Term Trading, ch. 11): “Exit and reverse if you get an opposite signal”.

Timing is that of Position: the result at bar t is the position held after bar t’s decision, and a flag acts on its own bar. For a buy at the next bar’s open, pass the signals with Shift(1).

The reductions are exact. PositionSigned(entry, exit, nil, nil, hold…) equals Position(entry, exit, hold…), and PositionSigned(nil, nil, entry, exit, hold…) equals make(Series, len(s)).Sub(s.Position(entry, exit, hold…)), the same trades held short, with shorts as -1 and flat as +0; no hold is the same as hold 0. For flags up and dn whose values are exactly 0 or 1 and that are not both 1 on any bar, with all four flags the same length as s, the latch agrees bit for bit with the Hysteresis latch:

s.PositionSigned(up, dn, dn, up) == up.Sub(dn).Hysteresis(0, 0)

On a bar where both up and dn fire while in position they differ: Hysteresis(0, 0) holds its state, while PositionSigned reverses. While flat both stay at 0.

PositionSignedRules is this latch with a rule set (stops and targets sampled at entry and frozen for the trade, a time limit that an own entry can renew, a minimum hold and three reversal rules), and its zero rule set gives this latch bit for bit.

PositionSignedExitValue

func (s Series) PositionSignedExitValue(longEntry, longExit, shortEntry, shortExit Series, r PositionRules) Series

PositionSignedExitValue is the value at which PositionSignedRules closes each trade, posted on the trade’s exit bar, and NaN on every other bar.

It runs the same latch as PositionSignedRules, with the same arguments, as FlipStopDirection runs the latch of FlipStop, and so recomputes it: there is no cache shared between calls. An exit bar is a bar on which a rule closed the trade, a reversal bar included, where the value is that of the closing trade. A renewal is not an exit, and nothing is posted on a flat bar. Invalid rules give all NaN, of the receiver’s length, and an empty or nil receiver gives an empty, non-nil result.

The value is the one at which the rule that closed the trade was read. For a stop or a target with a Touch it is the frozen level, or the open of that bar (the Open series of the rules) when the bar opens through the level: strictly below a long stop or a short target, strictly above a short stop or a long target. An open exactly at the level posts the level. A NaN or missing open does not open through a level, so the level is posted. A bar on which a stop fires exits at the stop whichever rule closed the trade, so a reversal by an opposite entry on a stop bar posts the stop’s value, as a plain stop does. In every other case, that is for a stop or a target without a Touch, an exit flag, an opposite entry without a stop and the time limit, it is the receiver’s value on that bar.

Values are copied with no arithmetic, so a NaN keeps its payload and -0 is kept. With no Touch on any level, the result is the receiver on the bars where pos.Exits() is 1 and NaN elsewhere, where pos is the result of PositionSignedRules; then, with ev := s.ValueWhen(pos.Entries()).Shift(1), on each exit bar of a long trade ex.Sub(ev) equals s.TradeResult(pos), and on each exit bar of a short trade ev.Sub(ex) does, bit for bit, where ex is the result of this method. The Shift(1) reads the entry value of the closing trade on a reversal bar (V-141); the composition is the one of V-867 row 24.

The value is the level at which a resting exit was read; 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). PositionSignedRules describes the rules, the options and the alignment of every series.

Each bar costs O(1), and PositionSignedExitValue allocates only its result.

PositionSignedRules

func (s Series) PositionSignedRules(longEntry, longExit, shortEntry, shortExit Series, r PositionRules) Series

PositionSignedRules is the long and short latch of PositionSigned with a rule set: a time limit that an own entry can renew, a minimum hold, three reversal rules, and stops and targets frozen at entry.

The receiver is the trigger price, usually the close. Its values are read only by a stop or target that has no Touch and by PositionSignedExitValue; otherwise it gives the length of the result and the alignment of every other series. The result is +1 while the book is long, -1 while it is short and +0 while it is flat, and it is not NaN anywhere. There is no warm-up: bar 0 can enter.

The flags, the Level and Touch of the four levels, and Open are right-aligned with the receiver on their own: a series longer than the receiver contributes its last values, a shorter one leaves the earliest bars without a value, and an empty or nil one has no value on any bar. Lengths are not errors. A flag fires when it is non-zero and not NaN (+Inf, -Inf, -1, 2 and 0.5 fire; 0, -0 and NaN do not), and a bar without a value does not fire. A Level without a value reads as NaN, which is no level.

On a flat bar a long entry without a short entry opens a long trade, a short entry without a long entry opens a short trade, and both or neither leave the book flat. Exits, stops, targets and the time limit are not read while flat. A trade opened from flat, by a reversal or by a stop and reverse samples the stop and the target Level of its side on that bar, and the levels are frozen for the trade. No exit of any kind is read on a trade’s entry bar, a reversal bar included, so every trade lasts at least one bar. With flags shifted for fills at the next open, the first bar on which a stop or an exit can act is the bar after the fill.

On each later bar the first of these rules that applies decides. (1) An opposite entry reverses the trade, unless OppositeFlattens is set, and with OppositeOnStopOnly only on a bar on which the stop fires. (2) A stop closes the trade, or reverses it with StopReverses. (3) With OppositeFlattens an opposite entry closes the trade and is not taken. (4) A target closes the trade, once MinHold has passed. (5) The exit flag closes the trade, once MinHold has passed. (6) On the bar on which Hold runs out the trade closes, unless Renew is set and an own entry fires on that bar, when the count restarts there and the trade continues. (7) Otherwise the trade continues, and an own entry is ignored, so there is no pyramiding. A short book mirrors every rule.

An opposite entry wins over a stop, so a reversal survives a stop on the same bar, and with OppositeOnStopOnly the stop is what lets it reverse. A stop wins over OppositeFlattens, a target and an exit flag. OppositeFlattens wins over a target, an exit flag and a renewal. A target wins over an exit flag, an exit flag on the limit bar wins over a renewal, and a renewal needs the own entry on the limit bar itself. MinHold gates (4) and (5) only; the stop, an opposite entry and the time limit are not gated.

The four tests are touch-inclusive with a Touch and strict on the receiver without one. A long stop fires when the touch is at or below the frozen level, or without a Touch when the receiver is strictly below it. A short stop fires at or above the level, or when the receiver is strictly above it. A long target fires at or above the level, or when the receiver is strictly above it. A short target fires at or below the level, or when the receiver is strictly below it. A NaN on either side does not fire, and the trade continues. Infinities are ordinary values: a -Inf long stop without a Touch does not fire, and with a Touch it fires only on a touch of -Inf. The other tie is the caller’s one-ulp move of the level with math.Nextafter, which is exact for every finite level; an infinite level moved towards its own infinity stays there, so a -Inf long stop moved down still fires on a touch of -Inf.

Hold counts from the entry bar or from the last renewal, and MinHold from the entry bar: the gate is pos.BarsSinceEntry() >= MinHold, where pos is the result. A renewal restarts only the count of Hold, with no new sample at a renewal, so the levels stay as sampled on the entry bar. MinHold 0 and 1 are no restriction, and a MinHold above Hold is allowed: the time limit still closes the trade at its count.

The rules are invalid when Hold or MinHold is negative, or when OppositeFlattens and OppositeOnStopOnly are both set. For invalid rules PositionSignedRules gives a result of all +0, of the receiver’s length. Everything else is valid: Hold 0 is no limit, and math.MaxInt is valid for both Hold and MinHold, inert for Hold and holding back every exit flag and target for MinHold. An empty or nil receiver gives an empty, non-nil result.

With the zero PositionRules the result is that of PositionSigned: s.PositionSignedRules(le, lx, se, sx, PositionRules{}) equals s.PositionSigned(le, lx, se, sx). With Hold k it equals s.PositionSigned(le, lx, se, sx, k), and with nil short flags and Hold k it equals s.Position(entry, exit, k). For flags up and dn that are exactly 0 or 1 and not both 1, s.PositionSignedRules(up, dn, dn, up, PositionRules{}) equals up.Sub(dn).Hysteresis(0, 0).

Frozen levels can be read back. On every long bar of pos, r.LongStop.Level.EntryValue(pos) is the stop level in force, and on the exit bar of a long trade r.LongStop.Level.EntryValue(pos).Shift(1) is the level that bar was tested against, because EntryValue reads a reversal bar as the new trade’s entry. The same holds for LongTarget, and for ShortStop and ShortTarget on short bars.

Levels must be computable without the latch’s output: a level that reads the position it feeds is a feedback loop. Meyers’s 2% stop and reverse is the worked example, with hi marking the bars of new highs and ShortStop: PositionLevel{Level: h.ValueWhen(hi).Mul(1.02), Touch: h}. The stop level is sampled while long and frozen while short: the level in force is the one of the sell bar.

The stop pattern of the Trade State family, a PositionSigned book cut at the first bar whose touch reaches a frozen level, equals this function until its re-entry diagnostic fires. After that the pattern waits for the base book’s next entry, where this function re-enters on the next entry flag.

The sources are Wilder’s Extreme Point rule as Summers programs it (OppositeOnStopOnly, with the stops one ulp outside the extreme point), Meyers’s 2% stop and reverse (StopReverses), Panagakis’s renewed eight-week hold (Hold 8 with Renew), Clenow’s exit to flat on an opposite entry (OppositeFlattens) and Stefanel’s minimum hold (MinHold 31 on shifted flags, because the exit acts on the bar after the one that has held more than 29 days).

The latch works in price units; a point value is the caller’s. Under a sliding slice of history it is path-dependent: the position is undefined until the first entry inside the slice, so the warm-up is one complete trade. To carry a known open position across the start of a slice, force an entry on the first bar with entry.SetN(1, 1); the time limit, the minimum hold and the levels then start on that bar. Passing every input with x.Shift(1) gives the result with Shift(1), the form for fills at the next open.

These latches are a signal state at bar level: from flags, a rule set and levels frozen at entry they say which trade a rule holds on each bar, and at most the level a resting exit was read at. They are 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).

Each bar costs O(1), the state is O(1), and PositionSignedRules allocates only its result.

TradeAdverseExcursion

func (s Series) TradeAdverseExcursion(pos, h, l Series) Series

TradeAdverseExcursion returns the maximum adverse excursion (MAE) of each trade of pos, posted on the trade’s exit bar: how far the price went against the trade while it was open, measured from the entry value. The receiver is the anchor, the series the trade is entered at (c for fills on the close, o for fills at the next bar’s open, with the signal shifted by one as the flag), and h and l are the path, the highs and lows of the receiver’s bars. Close-only MAE, as Balsara’s 1992 article tabulates it, is c.TradeAdverseExcursion(pos, c, c).

pos is read as by the trade-state views EntryValue, BarsSinceEntry and the others: it is right-aligned with the receiver, receiver bars before the overlap are flat, a bar is in position when its value is neither 0 nor NaN, the direction is its sign and the size is not read; pos[t] is the position after bar t’s decision. A trade runs from its entry bar e to the bar before its exit bar x, the first later bar that is flat or of the other direction (a reversal bar closes one trade and opens the next). On x, with E = s[e], minL the lowest l and maxH the highest h over e..x, both ends included (Go’s min and max, applied left to right), the result is one subtraction:

  • long trades: E - minL;
  • short trades: maxH - E.

Every other bar is NaN: flat bars, the bars of an open trade and every bar of a trade still open on the last bar, which has no exit. On a reversal bar the value is the closing trade’s. The exit bar’s own low (its high for a short trade) is part of the path. To let the entry bar contribute only the fill, as when the entry is on the close, substitute l2 := l.Mul(en.Mul(-1).Add(1)).Add(c.Mul(en)) for l (and likewise for h), with en := pos.Entries(); on finite data only, since 0 times NaN is NaN.

The value is in the receiver’s units, per unit of position, and is not floored: with the anchor inside the entry bar’s range it is at least 0, but an anchor outside that range (an entry at a price the bar did not trade) gives a negative value, which is kept, as Sweeney’s printed trade list keeps an excursion of -4 points. A NaN anchor at e gives NaN, and so does a NaN in the path series the side reads (l for a long trade, h for a short one) at any bar of e..x; the other path series does not affect the value. Infinities are ordinary values, and an infinity minus the same infinity is NaN. A NaN in pos inside a trade is flat: that bar is the exit bar and lies inside the window. There is no warm-up beyond the trade itself.

Validation: if len(h) or len(l) differs from len(s) the result is Series{}, checked before anything else, since h and l are columns of the receiver’s bar table, as ATRTrailingStop checks its h and l against the receiver and TrueRange and ADX check their columns against one another; pos is right-aligned and is not a length error. An empty receiver with empty h and l gives an empty series. Cost: one pass, O(N).

For pos of the receiver’s length, with ev := s.EntryValue(pos), lo := l.LowestSinceEntry(pos) and hi := h.HighestSinceEntry(pos), the result on every exit bar x is, bit for bit,

long:  ev.Shift(1)[x] - min(lo.Shift(1)[x], l[x])
short: max(hi.Shift(1)[x], h[x]) - ev.Shift(1)[x]

The views are NaN on the exit bar and belong to the new trade on a reversal bar, so the trade’s state through x-1 is read with Shift(1) and the exit bar’s own value completes the window. On the bars of an open trade, the running (open) MAE of a signed position is

ev.Sub(lo).Mul(pos.GT(0)).Add(hi.Sub(ev).Mul(pos.LT(0)))

on finite data: a NaN in the side not in position makes the product NaN, where the closed excursion ignores it. OpenAdverseExcursion is that running view as a method and reads only the side in position, and OpenFavourableExcursion is its favourable counterpart.

TradeStation’s MaxPositionLoss reports the worst open loss of a whole position in currency, as a negative number; this method posts the closed trade’s excursion once, per unit and positive when adverse, on its exit bar, which is the sample the stops below need. To compare trades across price levels or volatility regimes, scale on the exit bars by the anchor or the ATR known at the entry bar (Shift(1), so that a reversal bar reads the closing trade’s entry), as a fraction or in ATRs:

mae.Div(c.ValueWhen(pos.Entries()).Shift(1))
mae.Div(atr.ValueWhen(pos.Entries()).Shift(1))

Div reads 0 where the divisor is 0 (bar 0, the Shift pad, and an ATR’s warm-up), so trim those bars; ATR and ATRWilder differ, so use the same one for calibration and for the stop. Jaekle and Tomasini (Trading Systems, 2nd ed., 2019, ch. 3.5) work in percent of the entry price and then sweep a percent stop in sample, choosing the middle of the stable region; the sweep is an optimisation over a back-test, not an indicator, and is not provided.

Sweeney’s MAE stop and Balsara’s probability stop set the stop distance from the MAE of past winning trades, Balsara’s from the 90th percentile of the winners’ MAE. The recipe for long trades filled on the close:

pos  := c.Position(entry, exit)
mae  := c.TradeAdverseExcursion(pos, c, c)
res  := c.TradeResult(pos)
D    := mae.QuantileWhere(res.GT(0), k, 0.9)
Dst  := D.ValueWhen(pos.Entries())
open := c.EntryValue(pos).Sub(c.LowestSinceEntry(pos))
hit  := open.GT(Dst).Mul(pos.BarsSinceEntry().GT(0))
cut  := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))

The sample is the winners’ MAE: res.GT(0) is 0 for a draw, so draws count as losers, as Sweeney counts them. The distance is the one known at the entry bar (Dst), held for the trade. The trigger is strict: Balsara stops above his level, Sweeney “just beyond” it. A NaN distance means no stop, because GT(NaN) is 0: fewer than k winners so far, or a NaN or infinite MAE among the last k winners (res.GT(0).Mul(mae.Abs().LT(math.Inf(1))) as the mask drops such trades from the sample); a fallback is D.If(math.IsNaN, fallback, D) before ValueWhen. The entry bar is not tested. An unfloored sample can give a negative D, which stops a trade on its first tested bar; floor it with D.Max(0) if that is not wanted. Once stopped, the position stays flat until the base system’s next entry, so the base trade count is unchanged; re-entry inside a base trade needs a loop outside this package. With PositionSigned, replace open by the signed open MAE above, or by c.OpenAdverseExcursion(pos, c, c); the other lines carry over unchanged, and on a reversal bar the new trade’s Dst already includes the trade closing there.

Sources: Sweeney, “Where To Put Your Stops”, Technical Analysis of Stocks & Commodities, October 1985, and “Reversing Your Losses”, April 1992 (the trade list with the -4 row); Balsara, Money Management Strategies for Futures Traders (Wiley, 1992), ch. 6, and “Using Probability Stops In Trading”, Technical Analysis of Stocks & Commodities, May 1992 (tests on closes only).

TradeFavourableExcursion

func (s Series) TradeFavourableExcursion(pos, h, l Series) Series

TradeFavourableExcursion returns the maximum favourable excursion (MFE) of each trade of pos, posted on the trade’s exit bar: how far the price went in the trade’s favour while it was open, measured from the entry value. The receiver is the anchor and h and l are the path, read as by TradeAdverseExcursion, whose reading of pos, timing, validation and NaN rules apply unchanged. On the exit bar x of a trade entered on bar e, with E = s[e], maxH the highest h and minL the lowest l over e..x, both ends included, the result is one subtraction:

  • long trades: maxH - E;
  • short trades: E - minL.

Every other bar is NaN, including the bars of an open trade and every bar of a trade still open on the last bar; on a reversal bar the value is the closing trade’s. The value is in the receiver’s units, per unit of position, and is not floored. A NaN anchor at e, or a NaN in the path series the side reads (h for a long trade, l for a short one) at any bar of e..x, gives NaN; the other path series does not affect it, so a NaN in l inside a long trade leaves its MFE as computed. If len(h) or len(l) differs from len(s) the result is Series{}; pos is right-aligned. There is no warm-up beyond the trade itself. Cost: one pass, O(N).

For pos of the receiver’s length, with ev := s.EntryValue(pos), hi := h.HighestSinceEntry(pos) and lo := l.LowestSinceEntry(pos), the result on every exit bar x is, bit for bit,

long:  max(hi.Shift(1)[x], h[x]) - ev.Shift(1)[x]
short: ev.Shift(1)[x] - min(lo.Shift(1)[x], l[x])

TradeStation’s MaxPositionProfit reports the best open profit of a whole position in currency, as a positive number; this method posts the closed trade’s excursion once, per unit, on its exit bar. With the anchor inside each bar’s range, a trade’s MFE is at least its TradeResult and its MAE at least minus that result.

The MFE add-on (Stendahl and Zamansky) exits or adds once the open favourable excursion reaches a level set from past winners. Only the level is this method’s part; for long trades filled on the close:

lvl  := c.TradeFavourableExcursion(pos, h, l).QuantileWhere(c.TradeResult(pos).GT(0), k, q)
held := lvl.ValueWhen(pos.Entries())
fire := h.HighestSinceEntry(pos).Sub(c.EntryValue(pos)).GT(held)

The open view gives the same flag: on long bars fire equals c.OpenFavourableExcursion(pos, h, l).GT(held) bit for bit.

TradeMAE

func (s Series) TradeMAE(pos, h, l Series) Series

TradeMAE is an alias for TradeAdverseExcursion, with the same result bit for bit and the same validation, NaN, warm-up and timing rules.

TradeMFE

func (s Series) TradeMFE(pos, h, l Series) Series

TradeMFE is an alias for TradeFavourableExcursion, with the same result bit for bit and the same validation, NaN, warm-up and timing rules.

TradeResult

func (s Series) TradeResult(pos Series) Series

TradeResult posts, on each exit bar, the result of the trade that the exit closes, and is NaN on every other bar. With e the entry bar and x the exit bar (receiver indices), the result is s[x] - s[e] for a long trade and s[e] - s[x] for a short one: one subtraction, in that order, whose own result is written without normalising it. So a draw (equal values) gives +0 in both directions, except that -0 - (+0) is -0. NaN at either bar gives NaN, and an infinity minus the same infinity gives NaN. A result that is NaN is still a closed trade, because exits are read from pos and not from the result. On a reversal bar the old trade’s result is posted: the old trade exits at s[x] and the new trade enters at that same s[x]. The result is in the receiver’s units, per unit of position, and the size of pos is not read.

pos is right-aligned with the receiver on its own, at the most recent bar, and is read by sign as described for Entries over the aligned values: a longer pos contributes only its last len(s) values, and a shorter or empty one leaves the earlier receiver bars without a paired value, so a trade that began before the overlap is taken to begin on the first paired bar. A trade still open on the last bar is not closed and has no result. There is no warm-up: the first exit bar already carries its result.

Timing: pos[t] is the position held after bar t’s decision. For a buy on the close, pass the close as the receiver; for a buy at the next bar’s open, build pos from signal.Shift(1) and pass the open. LastTradeResult holds the same result on every later bar.

A per-unit equity line, one step per closed trade, is the running sum of the results:

res := s.TradeResult(pos)
equity := res.If(math.IsNaN, 0.0, res).CumSum()

The NaN step is needed because res is NaN on every bar that is not an exit bar (and on an exit whose result is NaN), and those NaN values would otherwise enter the running sum; the If replaces them with 0.0, so the equity steps only on the exit bars.

Compositions

Entry-anchored bracket on the close (Pruitt)

George Pruitt’s oscillator systems (The Ultimate Algorithmic Trading System Toolbox, 2016, ch. 2, Boxes 2.2 to 2.7, and the CCI example of ch. 7) leave a long trade “If close > entry price + 3 ATR” (the target) or “If close < entry price - 1 ATR” (the stop), market on close, and a short trade on the mirrored tests; his CCI trend system (Box 2.6) uses 5 and 3. bracketStop cuts a base position pos on those two strict close tests, long trades through pos.GT(0) and short trades through pos.LT(0), with the volatility unit as a parameter.

ev := c.EntryValue(pos)
tgtL := c.GT(ev.Add(unit.Mul(kT)))
stpL := c.LT(ev.Sub(unit.Mul(kS)))
tgtS := c.LT(ev.Sub(unit.Mul(kT)))
stpS := c.GT(ev.Add(unit.Mul(kS)))
legs := tgtL.Add(stpL).Mul(pos.GT(0)).Add(tgtS.Add(stpS).Mul(pos.LT(0)))
hit := legs.Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • First valid index: the bar after each trade’s entry bar, and no bar before the unit’s first valid value: with Pruitt’s unit, atr := c.ATR(h, l, c, 10).SetN(9, math.NaN()), that is bar 9.
  • Timing: close test, close fill. The hit bar is the first flat bar of stopped, so c.TradeResult(stopped) posts c[t] - c[e] there for a long trade, Pruitt’s “Sell MOC”.
  • Unit: the current bar’s. Pruitt’s avgTrueRange(10) is TradeStation’s simple average of the true range, c.ATR(h, l, c, 10), whose pass-through warm-up the SetN mask turns into “no bracket” (a comparison with NaN is 0). His dollar stop of Box 3.3 is a constant distance in price units (the point value is the caller’s), a percent bracket is ev.Mul(1 - p) and ev.Mul(1 + q), and a unit frozen at entry is unit.ValueWhen(pos.Entries()).
  • Both legs: strict, as printed. One close cannot be above the target and below the stop while the unit is at least 0, so tgtL.Mul(stpL) is 0 on every bar. kT = +Inf disables the target, and the stop leg alone is then EntryValue’s stop recipe with stop = unit.Mul(kS).
  • Departures: his RSI listing (Box 2.2 and Appendix C) tests the short block with marketPosition = 1, so as printed its shorts leave only by reversal, and Box 2.3’s prose swaps the two legs against its code and its table. The recipe follows the code of Box 2.6 and ch. 7.
  • Evidence: Pruitt’s tables are TradeStation runs on 35 futures with no stated costs and no comparison with another exit; summed over the markets (the book prints no totals), Table 2.2 (RSI, 3 and 1) loses 376,993.15 dollars and Table 2.6 (CCI trend, 5 and 3) makes 346,559.40. He adds that a 1:3 ratio “works better” for short- to intermediate-term systems. Davey’s random-entry study (Algo Trading Cheat Codes, 2021, ch. 9) finds “no universal optimum”: the best reward-to-risk ratio is below 1 in five of seven markets, in sample, and his “Chance Win” column is a share of profitable runs, not a trade win rate (his ES row gives a 2-ATR target where his text says 1). On a driftless walk the bracket’s results sum to exactly zero, with the winners’ share tending to (b+1)/(a+b+2) for distances a and b (TestTradeStateSpecBracket pins both); Davey’s harness gates entries on being flat, which needs a loop.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package. Pruitt’s entries are level conditions that fire again after a stop.
  • Pinned by TestRecipeTradeStateBracket; runnable example ExampleSeries_EntryValue_bracket.

Bracket with resting orders (Davey, D’Errico)

Davey’s SetStopLoss and SetProfitTarget (Algo Trading Cheat Codes, ch. 8 and 9) and Domenico D’Errico’s “next bar” stop and limit orders (TradeStation EasyLanguage for Algorithmic Trading, 2024, ch. 7) rest a stop and a target for the next bar, set at the close from the entry value and the unit as they stand then. The stop fills on a touch and the target only when exceeded: “Profit targets are assumed to be hit only if the price is exceeded by one tick” (Davey). For a long trade entered at the next open:

ev := o.EntryValue(pos)
stp := ev.Sub(unit.Mul(kS)).Shift(1)
tgt := ev.Add(unit.Mul(kT)).Shift(1)
hitS := l.LTE(stp)
hitT := h.GT(tgt)
both := hitS.Mul(hitT)
hit := hitS.Add(hitT).GT(0).Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • First valid index: the bar after the entry. On the entry bar the shifted levels read the flat bar before (NaN), and on bar 0 the Shift pad (+0); the mask removes both.
  • Timing: resting stop. stopped is flat from the bar that touches a level; the fill, min(o, stp) for the stop and max(o, tgt) for the target, is the caller’s (the resting-stop fill series of the excursion stops).
  • Both legs: both flags a bar whose range reaches both levels. OHLC cannot order them, and the default reading is the stop: Katz and McCormick’s simulator takes the least profitable order, and AmiBroker checks its “Max. loss stop” before its profit target. stopped is flat from that bar either way.
  • Units: Davey’s AvgTrueRange(15) is c.ATR(h, l, c, 15).SetN(14, math.NaN()). D’Errico’s code calls AvgTrueRange(range,200), which is not valid EasyLanguage (the function takes one argument), where his prose says average(range,200): h.Sub(l).Average(200).SetN(199, math.NaN()). His percent levels are ev.Mul(1 - p) and ev.Mul(1 + q).
  • Short trades: the mirror, h.GTE(ev.Add(unit.Mul(kS)).Shift(1)) and l.LT(ev.Sub(unit.Mul(kT)).Shift(1)), selected by pos.LT(0) as in the close bracket.
  • D’Errico’s trail: his HTrade, reset to the high on every flat bar, includes the signal bar’s high; for a next-open entry it is h.Highest(2).HighestSinceEntry(pos), and his trail HTrade - 3*ATR is a since-entry chandelier on that series.
  • Departures: D’Errico’s prose swaps his stop and trail multiples (2 and 3 ATR) and gives a 3% stop where his code has 2%. His trade lists agree with the code, and they fill at the open on a gap (two trail exits at 176.75 from different levels).
  • Evidence: in Davey’s exit study (40 futures, five bar sizes, 2010 to 2020, costed, in sample) the ATR stop and target together are the worst of the simple exits by both of his measures; D’Errico’s lists are TradeStation output on data the book does not print.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package. Davey’s random-entry harness and D’Errico’s system, which has no exit of its own, are both defined by their stops.
  • Pinned by TestRecipeTradeStateRestingBracket.

Breakeven stop on closes (Davey)

Davey’s breakeven exit (Algo Trading Cheat Codes, ch. 8): “when the profit reaches a certain point, if the profit goes back to” zero, “the position is exited”, which he runs with TradeStation’s SetBreakEven. breakevenClose is its form on closes: it arms once the open favourable excursion of the closes reaches thr and leaves on the first later close at or beyond the entry value.

ev := c.EntryValue(pos)
mfe := c.OpenFavourableExcursion(pos, c, c)
opp := c.Sub(ev).Mul(pos.GT(0).Sub(pos.LT(0)))
arm := mfe.GTE(thr)
hit := opp.LTE(0).Mul(arm).Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • First valid index: the bar after the entry.
  • Timing: close test, close fill; c.TradeResult(stopped) is at most 0 on every bar the stop cuts.
  • Comparisons: the arming reaches the threshold (GTE), Pruitt’s “exceeds or matches” and TradeStation’s SetBreakEven page (“once a specified profit floor has been reached”); TradeStation’s Stops & Targets page says “exceeded”, the recorded disagreement. The trigger is touch-inclusive, an open profit at or below 0 (opp is the profit of either side, exactly), and the stop is the entry value itself; with costs it is ev.Add(cost).
  • No shift: for thr > 0 the arming and the trigger cannot fall on one close (a close at or below the entry value cannot raise the excursion), so arm is read on its own bar. thr = 0 arms on every bar before a NaN close and is a stop at the entry value that a close equal to it triggers; a NaN close poisons the running maximum, so the arming stays off from that bar to the trade’s end. thr = +Inf disables the stop while the excursion stays finite (an excursion that overflows to +Inf near the float64 limit arms it). The mixed form, arming on highs with a close trigger, can arm and trigger on one bar: delay its arming with Shift(1).
  • Thresholds: in price units; Davey’s 500 to 4,500 dollars per contract and Pruitt’s 700 need the point value, the caller’s conversion. A constant threshold latches by itself, because the excursion only rises within a trade; a threshold that varies by bar (Pruitt suggests an average range) needs arm.SumSince(pos.Entries()).GT(0) or the threshold held from the entry with ValueWhen(pos.Entries()). The profit protector with r = 0 and its floor at thr is this stop with a strict trigger and the next-open timing.
  • Evidence: Davey’s breakeven exit is third of fifteen exits by both of his measures, behind stop and reverse and the dollar target, in sample on 40 futures and five bar sizes from 2010 to 2020; within it his thresholds of 500 and 1,000 dollars ranked above the larger ones. Razaqzada (Stocks & Commodities V37:5) argues against moving a stop to breakeven without a technical reason, with no test. Mustapha’s systems (Stocks & Commodities V34:7, V36:4, V43:1) set a breakeven stop after a gain of 60 to 100 pips, rules without tests.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package. Davey’s momentum, breakout and volatility entries are level conditions that fire again.
  • Pinned by TestRecipeTradeStateBreakeven; runnable example ExampleSeries_HighestSinceEntry_breakeven.

Breakeven stop with a resting stop (SetBreakEven, Arnold)

TradeStation’s SetBreakEven, as Davey runs it, is a resting order: once the open profit reaches the floor, a stop rests at the entry price. Curtis Arnold (“Placing Stop-Loss Orders”, Stocks & Commodities V11:4) moves the stop “to break even when price has moved from your entry point twice the amount of your initial risk”: with crude oil bought at 21.00 and an initial stop at 20.50, the stop moves to 21.00 “as soon as the market reaches” 22.00. breakevenStop arms on highs (lows for a short) and rests the stop at the entry value from the next bar.

ev := o.EntryValue(pos)
mfe := o.OpenFavourableExcursion(pos, h, l)
arm := mfe.GTE(thr)
touch := l.LTE(ev).Mul(pos.GT(0)).Add(h.GTE(ev).Mul(pos.LT(0)))
hit := touch.Mul(arm.Shift(1)).Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • First valid index: the bar after the entry.
  • Timing: resting stop. The arming is read at the close and the stop is tested from the bar after it (arm.Shift(1)), so a bar that arms and comes back to the entry value survives: OHLC cannot order the two, and Davey’s tests order such bars with TradeStation’s look-inside-bar backtesting. The fill, min(o, ev) for a long, is the caller’s, so after a gap the result is below 0, not exactly 0.
  • Arnold’s numbers: on bars constructed around his prices (entry at the open 21.00 on bar 1, a high of 22.00 on bar 2, a low of 21.00 on bar 4), breakevenStop(o, h, l, pos, 1.0) is 0 1 1 1 0 0: R is 0.50 and the arming test 22.0 >= 21.0 + 1.0 is exact in binary64. The short mirror on negated prices is 0 -1 -1 -1 0 0.
  • Partial exits: Pruitt’s Box 3.6 arms its breakeven stop on a partial exit (one of two units), which needs position size, outside this package.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStateBreakeven.

Pruitt’s three-stop combination

Pruitt’s Box 1.3, a system like one that sold for ten thousand dollars in the 1990s, sets an initial stop “at the lowest low of the past 13 days”, pulls its stops to breakeven “Once profit exceeds or matches” 700 dollars, and for a long trade uses “the greater of” the breakeven stop, the initial stop and the “Lowest low of a moving window of the past 23 days”; it enters at the next day’s open. For a long trade, with thr in price units:

ev := o.EntryValue(pos)
mfe := o.OpenFavourableExcursion(pos, h, l)
init := l.Lowest(13).Shift(1).ValueWhen(pos.Entries())
trail := l.Lowest(23).Shift(1)
arm := mfe.GTE(thr)
touch := l.LTE(init).Add(l.LTE(trail)).Add(l.LTE(ev).Mul(arm.Shift(1)))
hit := touch.Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • The greatest of three stops: a low touches the greatest of three long stops exactly when it touches one of them, so the cut needs only the sum of the three touch flags. The stop price, needed only for a fill, is the greatest of the levels in force (an If with an index literal, since Max takes a scalar).
  • Readings: the initial stop is read on the signal bar, the bar before the entry (Shift(1) before ValueWhen), and held, which is what “initial” suggests; the 23-day window moves with each close and rests for the next bar; the breakeven arms on highs, as breakevenStop does. Pruitt states none of the three. A short trade is the mirror with h.Highest(13), h.Highest(23) and h.GTE.
  • First valid index: the bar after the entry. A trade entered before bar 13 (or 23) reads the partial windows of Lowest.
  • Timing: resting stop. The breakeven, the initial stop and the trail can be touched on one bar; they meet only in the fill, not in the cut.
  • Evidence: Pruitt reports no test of the breakeven component and judges the whole system’s walk-forward result “an emphatic no!”. He suggests a volatility unit for the 700-dollar threshold; then latch the arming or hold the threshold from the entry, as for the close breakeven.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStatePruittCombination.

Arnold’s fourth-day rule

Arnold adds a time element: “On the fourth day after you entered the market, if the price is above where you entered but has not reached your objective of twice the initial risk, you should move your stop to break even”, and if the price is below the entry, he waits until it “has risen above your entry”. The block keeps his initial stop as a second resting leg, risk below the entry value, and arms the breakeven leg when the high reaches twice the risk or, from the fourth day on, when a close is above the entry, latched for the trade:

ev := o.EntryValue(pos)
mfe := o.OpenFavourableExcursion(pos, h, l)
init := ev.Sub(risk)
armRisk := mfe.GTE(float64(2 * risk))
armTime := pos.BarsSinceEntry().GTE(4).Mul(c.GT(ev))
arm := armRisk.Add(armTime).GT(0).SumSince(pos.Entries()).GT(0)
touch := l.LTE(init).Add(l.LTE(ev).Mul(arm.Shift(1)))
hit := touch.Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • The fourth day: read as BarsSinceEntry of at least 4, with the entry day as day 0. Arnold does not say how he counts, and GTE(3) is the other reading.
  • Arnold’s example: on the constructed bars of the section above (with risk = 0.5 and the close equal to the open) no low reaches the initial stop at 20.50, so this block and breakevenStop alone give the same 0 1 1 1 0 0.
  • First valid index: the bar after the entry.
  • Timing: resting stop, both legs; 2 * risk is formed as one product.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStateArnold.

Give-back stop (Katz and McCormick)

Jeffrey Owen Katz and Donna McCormick (“Using Trailing Stops In Exit Strategies”, Stocks & Commodities V16:4) place each bar’s stop a fraction f of the way from a baseline money stop to the best price of the trade so far: entered at the open at 1,000 with the baseline at 998 and a high of 1,008, “we take 1,008 and subtract our baseline stop (998, the original stop), multiply by the percentage factor”, 0.50, and add 998, “a stop value for the second day of 1,003”; had the second day’s high been 1,010, the stop for the third day would be 1,004. Their C code computes (1.0 - pfac) * (ep - is[iis] * DOLLARS) + pfac * mfe, the parenthesis being the baseline stop, with the excursion seeded at the entry price and updated with each high, and places it for the next bar. giveBackStop is that code on both sides.

ev := o.EntryValue(pos)
bL := ev.Sub(dist)
bS := ev.Add(dist)
bestL := h.HighestSinceEntry(pos)
bestS := l.LowestSinceEntry(pos)
lvlL := bL.Mul(1 - f).Add(bestL.Mul(f))
lvlS := bS.Mul(1 - f).Add(bestS.Mul(f))
touch := l.LTE(lvlL.Shift(1)).Mul(pos.GT(0)).Add(h.GTE(lvlS.Shift(1)).Mul(pos.LT(0)))
hit := touch.Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • Arithmetic: the code form. 1 - f is formed at run time from the parameter, as their (1.0 - pfac), each product is rounded once by Mul and the sum once by Add, which is their C without fusion. So f = 0 is the fixed money stop (lvlL is bL) and f = 1 a stop at the best high so far (lvlL is bestL), both exactly while bL and bestL are finite: an infinite unused term times 0 is NaN, so with bL at -Inf the f = 1 level is NaN. Their prose form b + f*(best - b) can differ in the last bit at other factors: with f = 0.2, an entry at 1,000, dist 2 and a best of 1,008, the code form is one ulp above 1,000 and the prose form 1,000.
  • The level does not fall within a trade for 0 <= f <= 1, so nothing needs a ratchet. A NaN high inside a long trade makes the best price NaN until the trade ends and turns its stop off from the bar after it.
  • First valid index: the bar after the entry. Their initial stop is live on the entry bar, after the open fill; that is the entry-bar term below, not this helper.
  • Timing: resting stop. Their time exit, “If we have been in a trade more than four days, we exit at the next open”, is o.Position(sig.Shift(1), nil, 5); their code names exitlongclose, a close, for it (a departure the recipe does not follow). A time exit and a stop meet on one bar only in the fill.
  • Units: dist is in price units. Their prose prices the S&P 500 at 250 dollars a point (the 500-dollar baseline is 2 points, and 1,003 locks in 750 dollars), while their code’s comments (25.0 for 0.05 points) imply 500 dollars a point, so their grid cannot be refitted without settling which.
  • Evidence: their grid (S&P 500 daily, 1983 to 1999, no costs, worst-case fills) is one market in sample. Their entries are spaced beyond the four-day hold, so “all combinations of initial stops and percentage factors tested will involve the same total number of trades”: on that harness the recipe is their system, and the trade count is the same for every (dist, f).
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStateGiveBack; runnable example ExampleSeries_OpenFavourableExcursion.

Profit-activated trail (Pruitt)

Pruitt’s breakout system of ch. 7 rests an initial stop numAtrStop units below the entry price and, once “maxContractProfit achieves a level equal to, in this case, five ATRs”, a trailing stop at entryPrice + (maxContractPoints-numAtrTrail * avgTrueRange(atrLen)), both for the next bar (3, 5 and 2 units of avgTrueRange(10)). activatedTrail is his code as printed, on both sides.

ev := o.EntryValue(pos)
mfe := o.OpenFavourableExcursion(pos, h, l)
arm := mfe.GTE(unit.Mul(m))
initL := ev.Sub(unit.Mul(kInit))
initS := ev.Add(unit.Mul(kInit))
trailL := ev.Add(mfe.Sub(unit.Mul(k)))
trailS := ev.Sub(mfe.Sub(unit.Mul(k)))
hitL := l.LTE(initL.Shift(1)).Add(l.LTE(trailL.Shift(1)).Mul(arm.Shift(1))).Mul(pos.GT(0))
hitS := h.GTE(initS.Shift(1)).Add(h.GTE(trailS.Shift(1)).Mul(arm.Shift(1))).Mul(pos.LT(0))
hit := hitL.Add(hitS).Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • As printed: the activation is tested again on every bar and the trail is not ratcheted, so a rising unit loosens the trail or switches it off. His prose, “value1 locks into place”, departs from his code, and so does his prose formula entryPrice + (Highest level during trade - 2*ATR), which adds a price to a price. The ratcheted variant holds the effective long level, the trail where armed and the initial stop elsewhere, at its running maximum with HighestSinceEntry(pos).
  • The two stops: while armed the trail is at or above the initial stop whenever m >= k (his 5 and 2, since trail - init >= (m - k + kInit)*unit), so the sum of the two touches is a test against the higher level. A gap can touch both on one bar; the fill is the caller’s.
  • First valid index: the bar after the entry; with his unit, c.ATR(h, l, c, 10).SetN(9, math.NaN()), the levels exist from bar 9 and rest from bar 10. A NaN unit turns both stops off on the following bar, where its levels would rest.
  • Timing: resting stop: the levels, the unit and the activation are those of the bar before.
  • No activation: with m = 0 and an initial stop out of reach the trail is the unratcheted chandelier on highs with a simple-average unit: ev + (mfe - k*unit) against best - k*unit, equal in exact arithmetic and to rounding in binary64.
  • Evidence: none; the book reports no test of this stop.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package. His breakout system has no exit of its own, so its trades are defined by its stops.
  • Pinned by TestRecipeTradeStateActivatedTrail.

Profit protector (Davey)

Davey’s Exit #9, “Profit Protector” (Entry and Exit Confessions of a Champion Trader): once the maximum position profit reaches ppfloor, “If the current open position profit divided by the maximum position profit is less than ppratio, then exit the position at the open of the next bar” (keep 60% of the maximum). ratioGiveBack tests it at each close, on both sides.

ev := o.EntryValue(pos)
mfe := o.OpenFavourableExcursion(pos, h, l)
opp := c.Sub(ev).Mul(pos.GT(0).Sub(pos.LT(0)))
hit := opp.LT(mfe.Mul(r)).Mul(mfe.GTE(floor))
act := hit.Shift(1).Mul(pos.BarsSinceEntry().GT(0))
cut := act.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • The product form: opp.LT(mfe.Mul(r)), not the printed quotient opp.Div(mfe).LT(r). They agree wherever the excursion is above 0, apart from a last-bit difference when the open profit lies on the boundary (the spec test pins them equal on quarter-tick data); at an excursion of 0, Div’s zero rule reads the quotient as 0, which passes LT(r) on the first tested bar when the floor is 0, while the product form leaves only on an open loss (“keep r of nothing”).
  • Timing: close test, next-open exit. The hit is shifted, not the cut (act), so stopped is flat from the bar after the test and o.TradeResult(stopped) posts the next open; shifting the cut instead would carry a cut onto the next trade’s entry bar when two trades are back to back.
  • First valid index: the bar after the entry. With a next-open entry the close test may run on the entry bar itself (its close follows the fill), and the shift moves its effect to the bar after.
  • Comparisons: the floor is reached (GTE, his code; his prose says “greater than”) and the ratio test is strict. The floor is in price units, and floor = +Inf disables the stop while the excursion stays finite (an excursion that overflows to +Inf near the float64 limit arms it). With r = 0 and the floor at a breakeven threshold this is the close breakeven with a strict trigger, a bar later.
  • Evidence: none in the book.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStateProfitProtector.

Davey’s give-back family

Davey’s other give-back exits share the protector’s timing. Exit #8, “Don’t Give It All Back”, leaves when the maximum position profit less the open profit is greater than xATR times avgtruerange(15); the trailing exit of Algo Trading Cheat Codes (ch. 8) when the open profit is below the maximum less a fixed amount, InputVar4*500; and Exit #11, “Tiered Exit”, is the protector with its ratio raised to 0.60, 0.75 and 0.90 as the maximum passes three floors (1,000, 2,000 and 3,000 dollars). The block is the tiered exit; put giveBack or trail in place of tiered in the act statement for the other two.

ev := o.EntryValue(pos)
mfe := o.OpenFavourableExcursion(pos, h, l)
opp := c.Sub(ev).Mul(pos.GT(0).Sub(pos.LT(0)))
giveBack := mfe.Sub(opp).GT(unit.Mul(k))
trail := opp.LT(mfe.Sub(x))
tier := mfe.GTE(f3).If(func(v float64) bool { return v != 0 }, r3,
    mfe.GTE(f2).If(func(v float64) bool { return v != 0 }, r2, r1))
tiered := opp.LT(mfe.Mul(tier)).Mul(mfe.GTE(f1))
act := tiered.Shift(1).Mul(pos.BarsSinceEntry().GT(0))
cut := act.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • The trailing exit’s table: Davey’s Figure 72, with the trailing amount at 2,500, lists for a maximum of 0, 500, …, 4,000 an exit below -2,500, -2,000, …, 1,500: the maximum less 2,500 on every row, which is trail in price units with a point value of 1.
  • The tier does not fall within a trade, because the maximum only rises, so the tiered exit only tightens. The tiers are two nested If calls on comparisons of the excursion.
  • Timing: close test, next-open exit, as the protector. First valid index: the bar after the entry; with his unit, c.ATR(h, l, c, 15).SetN(14, math.NaN()), giveBack can first fire on bar 14.
  • Units: the amounts, floors and tiers are in price units.
  • Evidence: Davey’s study (Algo Trading Cheat Codes, ch. 8) puts the trailing exit in his middle group, below the breakeven exit and stop and reverse, in sample; the Confessions exits come with no test.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStateDaveyFamily.

Wave stop (Labunsky and Hamilton)

Eugene Labunsky and Tom Hamilton (“Surfing Your Position With Wave Stops”, Stocks & Commodities V27:9) watch “the profit drawdown of the position” once the price reaches an initial profit target (4%): a drawdown of x% (30%) from the profit’s high-water mark marks a correction, and a new high-water mark after it starts the next wave; the exit is a drawdown of y%, 60% in wave 1, 50% in wave 2, 40% in wave 3 and 30% after it. In their example a stock bought at 100 rises to 110, “If the profit drops by 30% to 107, you watch it closely. If it drops 60% to 104, then you sell.” On closes, for a long trade:

ev := c.EntryValue(pos)
act := c.GTE(ev.Mul(1.04)).SumSince(pos.Entries()).GT(0)
profit := c.Sub(ev)
hwm := c.OpenFavourableExcursion(pos, c, c)
dd := hwm.Sub(profit).Div(hwm)
corr := dd.GTE(0.3).Mul(act)
nh := hwm.GT(hwm.Shift(1)).Mul(pos.BarsSinceEntry().GT(0))
mark := nh.Add(pos.Entries()).GT(0)
waves := nh.Mul(corr.SumSince(mark).Shift(1).GT(0)).SumSince(pos.Entries())
y := waves.If(func(w float64) bool { return w == 0 }, 0.6,
    waves.If(func(w float64) bool { return w == 1 }, 0.5,
        waves.If(func(w float64) bool { return w == 2 }, 0.4, 0.3)))
hit := dd.GTE(y).Mul(act).Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • Readings: closes for the profit, the target and the exit (“it must be monitored every day”); the exit at a drawdown of y% or more (“50% or greater”; the text also has “exceed” and “drops below”); a wave ends at a new high-water mark after a correction of at least x% (their Figure 3, “shift to next wave”). The article states none of these exactly. The activation is latched with SumSince, and waves counts the completed corrections.
  • Their example: on constructed closes 100, 102, 104, 107, 110, 108, 107, 105, 104, 103, entered at 100, the drawdown is 0.3 at 107 and exactly 0.6 at 104, where the trade leaves. On a constructed second wave (100, 104, 110, 107, 112, 108, 106) the correction to 107 and the new high at 112 move the exit to 50%, at 106.
  • A price level: while the high-water mark is above 0, dd >= y is a close at or below ev + (1 - y)*hwm, so the wave stop is the protector’s ratio form with r = 1 - y, a 4% activation, a touch-inclusive trigger and a ratio that tightens each wave; c.LTE(ev.Add(hwm.Mul(1 - y))) gives the same flags on the example.
  • Div: the drawdown divides by the high-water mark, which the activation keeps at 4% of the entry value or more wherever the result is read.
  • First valid index: the bar after the entry. Timing: close test, close fill.
  • Evidence: none; no test is reported.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStateWaveStop.

Chandelier exit since entry (Le Beau)

Chuck Le Beau’s chandelier exit hangs a multiple of the average true range below the highest high of a long trade (above the lowest low of a short one). His own definition is not in the library; the form here is the one AmiBroker’s one-line trailing stop runs, ApplyStop(stopTypeTrailing, stopModePoint, 3*ATR(14), True, True), as Pruitt prints it (ch. 4): a distance read again on every bar, checked against the bar’s range and filled at the stop level. chandelierStop ratchets the line, as Clenow’s stop does (Following the Trend, ch. 9: it moves only towards the price).

lineL := h.HighestSinceEntry(pos).Sub(dist).HighestSinceEntry(pos)
lineS := l.LowestSinceEntry(pos).Add(dist).LowestSinceEntry(pos)
touch := l.LTE(lineL.Shift(1)).Mul(pos.GT(0)).Add(h.GTE(lineS.Shift(1)).Mul(pos.LT(0)))
hit := touch.Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
line := pos.GT(0).If(func(v float64) bool { return v != 0 }, lineL, lineS)
  • Warm-up: mask the distance’s warm-up with +Inf, not NaN: dist := c.ATRWilder(h, l, c, n).SetN(n, math.Inf(1)).Mul(k). The highest high less +Inf is -Inf, no stop, and the ratchet’s max recovers at the first finite distance; a NaN distance makes the ratcheted line NaN from that bar to the trade’s end (for a trade entered in the warm-up, the whole trade), because Go’s max keeps a NaN. This needs k > 0 (0 * Inf is NaN). AmiBroker’s ATR is Wilder’s, ATRWilder.
  • The volatility stop: on closes (h = l = c), with pos := c.VolatilityStopDirection(h, l, n, k) and dist := c.ATRWilder(h, l, c, n).Mul(k), line is c.VolatilityStop(h, l, n, k) bit for bit from bar n until the first non-finite value: the since-entry views restart at a reversal bar’s close, as the volatility stop restarts. With a constant distance (k = 0 included) the ratchet changes nothing.
  • Close entries: with an entry on the close the entry bar’s high and low come before the fill; pass the close in place of the entry bar’s high and low (TradeAdverseExcursion’s doc gives the substitution).
  • First valid index: the bar after the entry, and bar n + 1 for a trade entered in the warm-up.
  • Timing: resting stop. The line set at the close of t-1 rests during bar t and is touched by the low (AmiBroker’s “price equal to stop level”); the fill is the resting-stop fill series below. line is NaN on flat bars.
  • Evidence: Davey (Algo Trading Cheat Codes, ch. 8) ranks a chandelier exit twelfth of fifteen by average return on account, in sample, without defining the one he ran; Clenow’s later trend model has no stop.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStateChandelier; runnable example ExampleSeries_LowestSinceEntry_chandelier.

Chandelier exit, unratcheted

Pruitt’s code, Wilder’s volatility stop and StockCharts’ line do not ratchet. Drop the last call of the two line statements:

lineL := h.HighestSinceEntry(pos).Sub(dist)
lineS := l.LowestSinceEntry(pos).Add(dist)
touch := l.LTE(lineL.Shift(1)).Mul(pos.GT(0)).Add(h.GTE(lineS.Shift(1)).Mul(pos.LT(0)))
hit := touch.Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
line := pos.GT(0).If(func(v float64) bool { return v != 0 }, lineL, lineS)
  • Wilder’s stop: on closes, with pos := c.WilderVolatilityStopDirection(h, l, n, k) and dist := c.ATRWilder(h, l, c, n).Mul(k), line is c.WilderVolatilityStop(h, l, n, k) bit for bit from bar n until the first non-finite value. Both identities hold because the kernel and Mul round the product k * a once and the since-entry views restart where the stop reverses; they test the line, not the latch, since the direction comes from the stop itself.
  • Warm-up: without a ratchet a NaN mask is safe; +Inf also works.
  • Against the ratchet: the ratcheted line is at or above this one on long bars (at or below on short bars), and the two differ only where a rising distance loosens the stop.
  • First valid index: the bar after the entry. Timing: resting stop.
  • Re-entry: the recipe equals the source’s system wherever no same-direction entry flag fires while the base is in that direction and the stopped position is flat (the condition of the introduction); where one does, the source re-enters inside the base trade, which needs a loop outside this package.
  • Pinned by TestRecipeTradeStateChandelierVolatilityStop.

Chandelier exit per bar (StockCharts)

StockCharts’ ChartSchool defines the chandelier on a rolling window, “Chandelier Exit (long) = 22-day High - ATR(22) x 3” and “Chandelier Exit (short) = 22-day Low + ATR(22) x 3”, with an ATR whose first value is the average of the first 22 true ranges and which then smooths as Wilder’s: c.TrueRange(h, l, c).RMA(22). There is no trade state:

atr := c.TrueRange(h, l, c).RMA(22).SetN(21, math.NaN())
longX := h.Highest(22).Sub(atr.Mul(3))
shortX := l.Lowest(22).Add(atr.Mul(3))
  • First valid index: 21, the first full average. Without the mask the warm-up reads 0 and puts the line at the high.
  • The worked rows: in StockCharts’ QQQ and GLD spreadsheet every printed exit is the printed 22-day high (low) less (plus) the printed ATR x 3, to the cent, and the 22-day high takes the row’s own high on 3/28, 4/1, 4/2 and 4/10 (the low on 4/3 and 4/4), so the window includes the current bar. The line is not ratcheted: QQQ’s falls from 67.15 on 4/2 to 67.12 the next day. The rows print none of the earlier bars, so they cannot witness the ATR, and the printed ATR x 3 comes from the unrounded ATR (0.71 x 3 is printed 2.14).
  • Exit test: the caller’s: a close below the same day’s line, or a touch of the previous day’s, l.LTE(longX.Shift(1)); StockCharts says neither. Katsanos’s weekly close form (Stocks & Commodities V42:7) is c.LT(c.Highest(5).Sub(atr.Mul(1.5))).
  • Since entry: on a trade’s bar with BarsSinceEntry equal to 21, h.Highest(22) is h.HighestSinceEntry(pos); earlier in the trade the window reaches before the entry, and later it forgets the trade’s high.
  • Units: ATRWilder (TA-Lib’s convention) leaves bar 0’s true range out and starts a bar later; StockCharts’ recursion is the RMA form.
  • Pinned by TestRecipeTradeStateChandelierPerBar.

Frozen-gap trailing stop (Carver)

This is not Robert Carver’s Starter System: it is the Starter System’s trailing stop on a base that closes on its own signal. In Leveraged Trading (Harriman House, 2019, ch. 5 and 6) the stop loss level of a long trade is the “highest daily closing price since the trade was opened” less a gap that “will remain fixed throughout the life of the trade”, and “If at any point the daily closing price falls below the stop loss level, we close the position”; the gap is 0.5 times the instrument’s annual standard deviation of returns, in price units. The Starter System itself needs a loop in two places: a trade is kept through an opposite signal of its moving-average rule until its stop, and after a stop the opposite trade opens on the stop bar when the rule already points the other way.

held := gap.ValueWhen(pos.Entries())
lineL := c.HighestSinceEntry(pos).Sub(held)
lineS := c.LowestSinceEntry(pos).Add(held)
beyond := c.LT(lineL).Mul(pos.GT(0)).Add(c.GT(lineS).Mul(pos.LT(0)))
hit := beyond.Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
line := pos.GT(0).If(func(v float64) bool { return v != 0 }, lineL, lineS)
  • The gap: pass his volatility in price units, per bar, times 0.5: c.Mul(c.Delta(1).Div(c.Shift(1)).StdDevSample(25).Mul(16).SetN(25, math.NaN())).Mul(0.5) (25 daily returns, the sample standard deviation, times 16); the helper reads it on the entry bar. A NaN gap means no stop for that trade, so a trade entered before bar 25 has none.
  • His gold example: long gold at 1,300 with an instrument risk of 11% a year is 143 in price, a gap of 71.50, an initial stop of 1,228.50, moved to 1,278.50 when the price reaches 1,350. On constructed closes 1,290, 1,300 (the entry), 1,310, 1,350, 1,340, 1,300, 1,270, the trade is stopped on the close 1,270 (constructed: he prints no price at which the stop is hit). His Table 18’s five initial stops, from gold short at 1,337.74 to the S&P 500 long at 253.92, follow from his risk and price columns.
  • Which bar’s line: with the gap held, a close beyond the previous bar’s line is a close beyond this bar’s, so the two tests give the same stop bars.
  • First valid index: the bar after the entry. Timing: close test, close fill: the stop bar is the close test, and his fill on the next session (“You must close it immediately”) is the caller’s.
  • The base: his continuous moving-average rule (ch. 9: 16 against 64 days, long above, short below, reversing on the flip) is c.PositionSigned(mac.GT(0), nil, mac.LT(0), nil) with mac := c.Average(16).Sub(c.Average(64)).SetN(63, math.NaN()); with this stop it is flat from the stop until the next flip, which keeps the Starter System’s “Do not open a new trade in the same direction as a recently closed trade”. Carver tests no such hybrid.
  • Departures: his formula 24 says “Highest price” where his text says the highest daily closing price; the recipe follows the text. The footnotes of his Tables 20 and 22 date the prices to 2019 under tables headed 2018.
  • Evidence: Carver drops the stop in his later systems (a Sharpe ratio of 0.24 with it and 0.27 without, none of the differences significant).
  • Pinned by TestRecipeTradeStateFrozenGap; runnable example ExampleSeries_HighestSinceEntry_carver.

The resting-stop fill series

A resting stop flags the bar it is touched on; its fill is the stop’s level, or the open when the bar opens beyond it. For a long base entered at the next open, with lvl the level resting during each bar (the line’s Shift(1)):

stopped, line := chandelierStop(h, l, pos, dist)
lvl := line.Shift(1)
stopBar := stopped.Exits().Mul(pos.Exits().EQ(0))
lvlFill := lvl.If(func(v float64, i int) bool { return v < o[i] }, lvl, o)
fill := stopBar.If(func(v float64) bool { return v != 0 }, lvlFill, o)
res := fill.TradeResult(stopped)
  • The stop’s own bars: stopBar keeps the stop’s exits and drops the base’s. On a bar where the base exits at the open and the stop is touched later, the open comes first, and the base’s exit is the fill.
  • lvlFill is min(o, lvl), selected without arithmetic, so the fill is the level or, after a gap, the open; a NaN level reads the open.
  • fill is meaningful on entry and exit bars only, the bars TradeResult reads: the open on entry bars and on the base’s exits, the stop’s fill on stop bars.
  • Other stops: every resting stop works the same with its own level: the give-back level, the higher touched level of Pruitt’s two stops, the entry value of a breakeven. Where two legs meet on one bar (a stop and a target), the caller chooses; the worst case is the stop.
  • First valid index: the stop’s. Timing: resting stop.
  • Pinned by TestRecipeTradeStateFillSeries.

The entry-bar term

With an entry at the next open the whole entry bar follows the fill, and a single resting level placed with the entry order (Katz and McCormick’s initial stop, TradeStation’s built-in stops) can be touched on that bar. The helpers mask the entry bar; the term adds such a touch after the mask. For the give-back stop on a long base:

ev := o.EntryValue(pos)
best := h.HighestSinceEntry(pos)
lvl := ev.Sub(dist).Mul(1 - f).Add(best.Mul(f))
touch := l.LTE(lvl.Shift(1))
hit0 := l.LTE(ev.Sub(dist)).Mul(pos.Entries())
hit := touch.Mul(pos.BarsSinceEntry().GT(0)).Add(hit0)
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • The consequence: a trade cut on its entry bar vanishes from stopped, with no entry and no result, so stopped.Entries() no longer equals pos.Entries(). Tally such trades from hit0: each lost dist, filled at the baseline, which is below the open fill.
  • Order: hit0 is added after the mask, unmasked, and only one level may be read on the entry bar: a second leg or an arming on that bar cannot be ordered from OHLC.
  • Without the term the block is giveBackStop on a long base.
  • First valid index: the entry bar. Timing: resting stop.
  • Pinned by TestRecipeTradeStateEntryBarTerm.

Limit entry below the prior close (Connors and Alvarez)

Larry Connors and Cesar Alvarez (Short Term Trading Strategies That Work, 2008, ch. 8 and 13) buy a pullback “on a limit the next day” 1%, 3%, 5%, 7% or 10% below the previous day’s close, after a close at a 10-day low above the 200-day average, and exit five days later. limitStretchFill gives the one-bar order’s fill flag and fill price for a 0/1 setup flag and a stretch x.

lim := c.Shift(1).Mul(1 - x)
fill := setup.Shift(1).Mul(l.LTE(lim))
px := o.If(func(v float64, i int) bool { return v > lim[i] }, lim, o)
  • Fill: a touch fills (LTE); neither source says whether a touch or a trade through is needed, and l.LT(lim) is the stricter reading. The order lives for one bar.
  • Fill price: min(o, limit), selected without arithmetic, so it is exact: the limit, or the open when the bar opens below it (on a tie between zeros of opposite sign, the open’s sign). The arithmetic forms round. px is meaningful on fill bars only; it reads 0 on bar 0, where the limit is the Shift pad.
  • The limit: the prior close times 1 - x, with 1 - x formed first, as the platforms compute it; c[t-1] - c[t-1]*x can differ by an ulp, which changes a fill on a low exactly at the limit.
  • First valid index: 1; bar 0 cannot fill. With their setup, c.LTE(c.Lowest(10)).Mul(c.GT(c.Average(200))) (ties included; their volume filter v.Average(100).GTE(250000) counts the setup day in its window, where Test 1’s “previous 100 trading days” would shift it by one, and the price filter is c.GT(5); both are the caller’s), the first setup is bar 199, through Average’s pass-through bars, and the first fill bar 200.
  • Evidence: their tables (US stocks, 1995 to 2007, no costs) are per-signal statistics, and their fill counts fall as the stretch grows, as nested fills must.
  • Pinned by TestRecipeTradeStateStretchFill; runnable example ExampleSeries_Position_limitStretch.

Stop entry above the prior close (Connors)

Their Test 1 buys “on a limit order the next day 1% above the previous day’s close”, which, above the market, is in effect a buy stop; Connors and Chris Cain (The Alpha Formula, ch. 8) put “a sell limit order 3.0% above the current price” for a short entry, cancelled at the end of the day. Both fill when the high reaches the level, at max(o, level); only the side entered differs.

lvl := c.Shift(1).Mul(1 + x)
fill := setup.Shift(1).Mul(h.GTE(lvl))
px := o.If(func(v float64, i int) bool { return v < lvl[i] }, lvl, o)
  • The mirror: limitStretchFill(o.Mul(-1), h.Mul(-1), c.Mul(-1), setup, -x) gives the same flags and, on fill bars, minus the same prices.
  • Fill price: max(o, level), exact (a tie between zeros of opposite sign keeps the open’s sign); meaningful on fill bars only (on bar 0 the level is the Shift pad).
  • Anchor: The Alpha Formula’s “current price” at “the beginning of the day” is ambiguous; the open-anchored form o.Mul(1 + x) is tested on the same bar.
  • First valid index: 1.
  • Pinned by TestRecipeTradeStateStretchFill.

Stretch-entry trades

The five-day trades from the fills, and their results anchored on the fill price:

fill, px := limitStretchFill(o, l, c, setup, x)
pos := c.Position(fill, nil, 5)
anchor := pos.Entries().If(func(v float64) bool { return v != 0 }, px, c)
res := anchor.TradeResult(pos)
pct := res.Div(anchor.ValueWhen(pos.Entries()).Shift(1))
signal := fill.Shift(5).If(func(v float64) bool { return v != 0 }, c.Div(px.Shift(5)).Sub(1), math.NaN())
  • The hold counts from the fill bar: c.Position(fill, nil, 5) exits on the close of bar e + 5, their “Exit on the close 5 trading days later”, which is why chapter 13’s “5 Day Hold” row repeats chapter 8’s 5% row’s 1.97% and 59% (its count, 63,101, is 53 short of chapter 8’s 63,154). Their chapter 13 exits are exit flags in place of the hold: c.GT(c.Average(5)), c.RSI(2).GT(70), c.GTE(c.Highest(7)).
  • The latch: no exit on the fill day, a fill while long is ignored (no pyramiding), and the earliest re-entry is the bar after an exit. The anchor is the fill on entry bars and the close elsewhere, so res is c[e+5] - px[e] and pct its fraction of the fill price.
  • Per signal: their counts look per signal, overlapping trades included. signal is each fill’s own five-day return, posted five bars after it; where no fill falls inside an open trade it equals pct on the exit bars to rounding (c/px - 1 against (c - px)/px).
  • First valid index: the first fill bar; the first result is five bars after it.
  • Pinned by TestRecipeTradeStateStretchTrades.

LUXOR entries (Jaekle and Tomasini)

Urban Jaekle and Emilio Tomasini (Trading Systems, 2nd ed., 2019, Text 3.1) confirm a moving-average crossover with a stop entry: on the crossover bar “BuyStop = High + 1 point; BuyLimit = High + 5 points” are latched, and “If GoLong and C < BuyLimit then Buy (“Long”) next bar at BuyStop Stop”; sells mirror. The system is always in and reverses on each opposite fill. luxorEntries is Text 3.1 without its time window.

fast := c.Average(fastN)
slow := c.Average(slowN)
up := fast.CrossesAboveS(slow).SetN(slowN, 0)
dn := fast.CrossesBelowS(slow).SetN(slowN, 0)
bStop := h.ValueWhen(up).Add(pt)
bLim := h.ValueWhen(up).Add(float64(5 * pt))
sStop := l.ValueWhen(dn).Sub(pt)
sLim := l.ValueWhen(dn).Sub(float64(5 * pt))
armL := fast.GT(slow).Mul(c.LT(bLim))
armS := fast.LT(slow).Mul(c.GT(sLim))
lvlL := bStop.Shift(1)
lvlS := sStop.Shift(1)
le := armL.Shift(1).Mul(h.GTE(lvlL))
se := armS.Shift(1).Mul(l.LTE(lvlS))
pxL := o.If(func(v float64, i int) bool { return v < lvlL[i] }, lvlL, o)
pxS := o.If(func(v float64, i int) bool { return v > lvlS[i] }, lvlS, o)
pos := c.PositionSigned(le, nil, se, nil)
fillP := pos.GT(0).If(func(v float64) bool { return v != 0 }, pxL, pxS)
  • The point: pt is one point in price units, one PriceScale unit in TradeStation: 0.0001 on a four-decimal GBP/USD feed. The chase limit’s 5 * pt is one product.
  • First valid index: slowN + 1, the first fill bar. SetN(slowN, 0) masks the crossover events: Average passes the input through before bar slowN - 1, which would make false crosses there, so the first crossover is bar slowN.
  • The latch: the levels latch on the crossover bar itself, and each side’s persists through the other side’s regime, as the EasyLanguage variables do. The crossover bar is always armed for pt > 0 (the close is at most the high).
  • Fills: at max(o, stop) and min(o, stop), exact by value; on a tie between zeros of opposite sign, such as a -0 open and a +0 stop, the selection keeps the open’s sign. fillP is meaningful on entry bars only; the short fill price reads 0 on bar 0, the Shift pad.
  • Readings: simple averages (the book: “the type of moving average did not matter”; its source used triangular ones), CrossesAboveS’s ties (the prior bar at or below, the current above), and a fill on the side already held ignored. The prose snippet (EntryLevel = High, no limit) differs from Text 3.1; the recipe follows Text 3.1. The time window needs timestamps, and in Text 3.1 it wraps the averages and the latches too, so a session mask on the flags would not be the book’s rule.
  • Evidence: the walk-forward is one FX pair (GBP/USD, 30-minute bars, 2002 to 2008), and no bar data are printed.
  • Pinned by TestRecipeTradeStateLuxor; runnable example ExampleSeries_ValueWhen_luxor.

LUXOR slow-average exits

The book’s Appendix 3 runs LUXOR with only the commented exits of Text 3.1, “If MP = 1 then Begin Sell next bar at Slow - 1 point Stop” and “Buy to Cover next bar at Slow + 1 point Stop” while short. Put these statements in place of the pos statement of luxorEntries:

lvlLX := slow.Sub(pt).Shift(1)
lvlSX := slow.Add(pt).Shift(1)
lx := l.LTE(lvlLX)
sx := h.GTE(lvlSX)
pos := c.PositionSigned(le, lx, se, sx)
pxLX := o.If(func(v float64, i int) bool { return v > lvlLX[i] }, lvlLX, o)
pxSX := o.If(func(v float64, i int) bool { return v < lvlSX[i] }, lvlSX, o)
  • Re-entry: the exits, like the entries, read only prices, so a later fill after an exit opens a new trade and no loop is needed.
  • Fills: a trade’s exit fill is pxLX (pxSX) on an exit bar that is not an entry bar, and the opposite entry’s fill on a reversal bar. On a bar where an opposite entry and the own exit both fire the position reverses.
  • Pads: sx reads the Shift pad on bar 0 as a level of 0, which a positive high touches; it is harmless, because no position can be short on bar 0.
  • Departures: the code, not the prose (“Trades are only exited when the price crosses the slower moving average”).
  • First valid index: as the entries.
  • Pinned by TestRecipeTradeStateLuxorSlowExits.

LUXOR percent exits

Chapter 3.5 adds “a stop loss of 0.3%”, “a trailing stop of 0.8%” that “rises proportionately” with the market and “1.9% profit target”, with no code printed. On the always-in base, flat from a hit until the next reversal, reading the percents of the entry fill and of the best price through the bar before:

pos, fillP := luxorEntries(o, h, l, c, fastN, slowN, pt)
ent := pos.Entries()
ev := fillP.EntryValue(pos)
hiA := ent.If(func(v float64) bool { return v != 0 }, fillP, h)
loA := ent.If(func(v float64) bool { return v != 0 }, fillP, l)
hi := hiA.HighestSinceEntry(pos)
lo := loA.LowestSinceEntry(pos)
hitL := l.LTE(ev.Mul(1 - rs)).Add(l.LTE(hi.Shift(1).Mul(1 - tr))).Add(h.GTE(ev.Mul(1 + tg))).Mul(pos.GT(0))
hitS := h.GTE(ev.Mul(1 + rs)).Add(h.GTE(lo.Shift(1).Mul(1 + tr))).Add(l.LTE(ev.Mul(1 - tg))).Mul(pos.LT(0))
hit := hitL.Add(hitS).Mul(pos.BarsSinceEntry().GT(0))
cut := hit.SumSince(pos.Entries()).GT(0)
stopped := pos.Mul(cut.Mul(-1).Add(1))
  • Not what the book ran: its entry orders are placed again after a stop, so its system re-enters the same side inside one base trade. Its net profit over its average trade implies about 1,028 trades with the risk stop and 1,039 with the trail as well, against 902 without them, where this recipe keeps the base’s count; the exact system needs a loop outside this package.
  • Readings: the trail is a percentage of the best price, the fill on the entry bar and the highs after it, through the bar before; on the first bar after the entry it is the fill times 1 - tr, the book’s “initially set at a fixed percentage below the entry price”. The book does not say which bar’s extreme it uses.
  • Timing: resting stop, three legs; where two meet on one bar, the caller orders the fills.
  • Departures: the book’s largest loss with the risk stop, 810 dollars, does not add up to its 600-dollar stop and a 250-dollar gap.
  • First valid index: the bar after each entry.
  • Pinned by TestRecipeTradeStateLuxorPercentExits.

VoltyRatio signals (D’Errico)

D’Errico’s reversal strategy (TradeStation EasyLanguage for Algorithmic Trading, ch. 5): a volatility compression, the ratio of a short range to a long one below a trigger, “if VoltyRatio<VoltyTrigger then begin Resistance=Highest(H,Len1); Support=Lowest(L,Len1)”, restarts a count, and while count<WaitBars a close above the resistance and below its pull-back average buys at the next open (the short mirror). The lengths are the parameters l1 and l2.

v1 := h.Highest(l1).Sub(l.Lowest(l1))
v2 := h.Highest(l2).Sub(l.Lowest(l2))
vr := v1.Div(v2).ValueWhen(v2.GT(0))
comp := vr.LT(trig).SetN(l2-1, 0)
resist := h.Highest(l1).ValueWhen(comp)
support := l.Lowest(l1).ValueWhen(comp)
age := comp.BarsSince()
sma := c.Average(pull)
long := age.LT(float64(wait - 1)).Mul(c.LT(sma)).Mul(c.GT(resist))
short := age.LT(float64(wait - 1)).Mul(c.GT(sma)).Mul(c.LT(support))
  • Update then test: the printed order. His count is reset to 0 and then incremented on the same bar, so after the update it is age + 1, and count<WaitBars is age < wait - 1; his prose, “the number of days that have elapsed”, describes age. His complete listing with Zaffino (Stocks & Commodities V36:9) also updates the levels before the breakout test.
  • The ratio is held where the long range is 0 (“if Volty2<>0”). For l1 <= l2 it is at most 1, so a trigger above 1 marks every bar as a compression and nothing fires: his tables at a trigger of 1.1 cannot come from the printed code. A compression bar does not fire, since its resistance is that bar’s own high.
  • First valid index: l2, the first signal bar; SetN(l2-1, 0) keeps the windows’ partial warm-up out of the compressions. The levels are NaN until the first compression; the book does not print their declarations, and the usual EasyLanguage declaration would start them at 0 (a departure).
  • The trade: his timed exit, “If BarsSinceentry = BarsIN then Sell (“BarsIN”) next bar open”, with the entry at the next open, is c.Position(long.Shift(1), nil, barsIn+1) anchored on the open: o.TradeResult posts o[e+barsIn+1] - o[e].
  • Evidence: his text selects lengths 14 and 46 where his figures ran 12 and 26 (and print two HD results, 4.19 and 5.2, for one setting); the out-of-sample test is cross-sectional (the Dow 30); and the printed code cannot produce his tables at 1.1.
  • Pinned by TestRecipeTradeStateVoltyRatio; runnable example ExampleSeries_BarsSince_voltyRatio.

VoltyRatio previous-bar reading

Test then update: the levels and the count as they stood at the close of the bar before. After the statements of voltyRatioSignals:

resistP := resist.Shift(1).SetN(1, math.NaN())
supportP := support.Shift(1).SetN(1, math.NaN())
ageP := age.Shift(1).SetN(1, math.NaN())
longP := ageP.LT(float64(wait - 1)).Mul(c.LT(sma)).Mul(c.GT(resistP))
shortP := ageP.LT(float64(wait - 1)).Mul(c.GT(sma)).Mul(c.LT(supportP))
  • The pad: SetN(1, math.NaN()) replaces the Shift pad, which would read a close above 0 as a breakout.
  • The two orders: long is longP.Mul(comp.Mul(-1).Add(1)).Mul(age.LT(float64(wait - 1))) on every bar: the printed order fires where this one fires, the bar is not a compression and the age is below wait - 1. This reading can also fire on a compression bar and at an age of wait - 1.
  • Above 1: this reading is empty too above a trigger of 1, given valid bars and l1 >= pull - 1.
  • First valid index: l2.
  • Pinned by TestRecipeTradeStateVoltyRatioPrevious.

Last-trade-loser filter (Faith, Penfold)

Curtis Faith (Way of the Turtle, 2007, “Original Turtle Trading Rules”) gives System 1’s filter: “System 1 breakout entry signals would be ignored if the last breakout would have resulted in a winning trade”, where the last breakout counts “regardless of whether that particular breakout was actually taken or was skipped because of this rule”. The filter therefore needs the unfiltered system, the shadow, and takes or skips each shadow trade by the result of the shadow trade before it. Brent Penfold (The Universal Tactics of Successful Trend Trading, 2021, ch. 9) applies it to stop-and-reverse systems: “Only trade if the previous change in daily Dow trend was a loss”. For System 1 the shadow is c.PositionSigned(h.Breakout(20, 1).GT(0), l.Breakout(10, 1).LT(0), l.Breakout(20, 1).LT(0), h.Breakout(10, 1).GT(0)).

last := c.LastTradeResult(shadow)
none := shadow.Exits().CumSum().EQ(0)
gate := last.LTE(0).Add(none).GT(0)
take := gate.ValueWhen(shadow.Entries()).GT(0)
real := shadow.Mul(take)
  • The gate: 1 after a loss or a draw (LTE(0), the rule of Pruitt’s real trades in The Ultimate Algorithmic Trading System Toolbox, ch. 3 and Appendix C; LT(0) skips after a draw too) and before the first shadow exit (none, Pruitt’s initially true flag), so the first shadow trade is real. It is not shifted: on a reversal bar it reads the trade closing there, whose exit fill comes before the new entry’s within the bar, and that is Penfold’s “previous signal”. The shifted gate of LastTradeResult’s doc is the next-bar order form: on a reversal bar it reads the trade before last, and on a base that cannot reverse it agrees on every entry bar.
  • Whole shadow trades: take is the gate on each shadow trade’s entry bar, held through the trade, so every real trade is a shadow trade with the same bars and result, and the skipped ones are shadow.Mul(take.EQ(0)). The two partition the shadow’s trades and results: Penfold’s Table 9.9 adds up so (11,306 trades after a loss and 6,703 after a profit make the 18,009 of his daily Dow trader).
  • Signed zeros: real is -0 on the masked bars of a skipped short trade; compare positions by value or normalise with Add(0).
  • Gated flags: c.PositionSigned(le.Mul(gate), lx, se.Mul(gate), sx) is the same on stop-and-reverse bases, but on a Turtle shadow it can enter a shadow trade late, on a bar that exits one trade and breaks out again.
  • The 2N stop: Faith’s losing breakout “moved 2N against the position before a profitable 10-day exit”. Cutting the shadow with a 2N stop (the stop leg of the close bracket, with c.TrueRange(h, l, c).RMA(20) as N held from the entry) is exact to the stop bar, but the Turtle rules take a same-direction breakout again before the 10-day exit, which needs a loop outside this package.
  • Departures: Pruitt’s Appendix C listing zeroes its virtual position (virTmp = 0) in both branches of its loop on every bar, so as printed no virtual trade can close and nothing re-arms real trading but a real 55-day trade that closes at a loss or a draw.
  • First valid index: 0 for the gate; the shadow’s first entry is the first real one.
  • Evidence: Penfold’s filter helps on daily bars and hurts on monthly ones, in sample (his “Impact”, +268% daily and -30% monthly, compares the trades after a loss with those after a profit); Pruitt’s Tables 3.1 and 3.3 are one test period of TradeStation runs.
  • Pinned by TestRecipeTradeStateLastTradeLoser; runnable example ExampleSeries_LastTradeResult_turtle.

Turtle failsafe

Faith: “if a System 1 entry breakout was skipped because the previous trade had been a winner, an entry would be made at the 55-day breakout to avoid missing major moves”. turtleFailsafe enters on a 55-day breakout only inside a skipped shadow trade of its side and leaves on System 1’s 10-day breakout, with real from the filter above.

up55 := h.Breakout(55, 1).GT(0)
dn55 := l.Breakout(55, 1).LT(0)
up10 := h.Breakout(10, 1).GT(0)
dn10 := l.Breakout(10, 1).LT(0)
skip := shadow.Mul(real.EQ(0))
failsafe := c.PositionSigned(up55.Mul(skip.GT(0)), dn10, dn55.Mul(skip.LT(0)), up10)
  • No feedback: the failsafe trade’s result does not feed the gate, which is about the last breakout; the failsafe and real do not overlap, so System 1 with its failsafe is real.Add(failsafe).
  • First valid index: 55, the first 55-day breakout.
  • Departures: Pruitt’s Appendix C gives the 55-day trade the 20-day exit and lets its result set the filter, which makes the gate depend on trades that depend on the gate: a loop outside this package. An outside bar that breaks both 20-day levels leaves the shadow flat; the Turtles held whichever broke first, which OHLC cannot say. Pruitt’s Box 3.2 prints System 2’s short exit as the 10-day high, against the long exit’s 20-day low and Appendix C’s highest(high,20), a misprint.
  • Pinned by TestRecipeTradeStateTurtleFailsafe.

Equity-average filter (Ruggiero)

Murray Ruggiero (Cybernetic Trading Strategies, 1997, ch. 10) tracks the closed-trade equity of each side of an unfiltered 20-day channel breakout: “We take only long trades when a faster period moving average of long equity is greater than a slower period one”, the short side likewise, with 130- and 150-bar averages, and the system is unfiltered for its first 10 trades. shadowEquityFilter is that rule over an n-bar channel.

up := h.Breakout(n, 1).GT(0)
dn := l.Breakout(n, 1).LT(0)
sh := c.PositionSigned(up, nil, dn, nil)
buyLev := h.Highest(n).Shift(1)
sellLev := l.Lowest(n).Shift(1)
buyFill := o.If(func(v float64, i int) bool { return v < buyLev[i] }, buyLev, o)
sellFill := o.If(func(v float64, i int) bool { return v > sellLev[i] }, sellLev, o)
fill := sh.GT(0).If(func(v float64) bool { return v != 0 }, buyFill, sellFill)
res := fill.TradeResult(sh)
res0 := res.If(math.IsNaN, 0.0, res)
resL := res0.Mul(sh.Shift(1).GT(0))
resS := res0.Mul(sh.Shift(1).LT(0))
eqL := resL.CumSum()
eqS := resS.CumSum()
okL := eqL.Average(a).GT(eqL.Average(b)).SetN(b-1, 0)
okS := eqS.Average(a).GT(eqS.Average(b)).SetN(b-1, 0)
early := sh.Exits().CumSum().LT(10)
gL := okL.Add(early).GT(0).Shift(1).SetN(1, 1)
gS := okS.Add(early).GT(0).Shift(1).SetN(1, 1)
real := c.PositionSigned(up.Mul(gL), dn, dn.Mul(gS), up)
  • The fills: the shadow fills at the channel level, or at the open on a gap (his MaxList(BuyLev,Open)), and every exit is a reversal, so one fill series serves both ends of a trade; the split by side reads the closing trade’s side on its exit bar.
  • What the gate reads: for a < b the difference of the two averages is a triangular-weighted sum of the results closed in the last b - 1 bars. For 130 and 150 today’s result has a weight of 1/975, and the results 100 to 149 bars back weigh most, so his “a mechanical way of saying that the last trade was profitable” holds only loosely, and the rule is not the last-trade-loser filter. With a = b nothing trades after the tenth closed trade.
  • Timing: Shift(1): the orders for bar t are set from the equity through t-1, as his code places them.
  • Gated flags: as printed, the real system can join a shadow trade late, at a later breakout of the same side. The masked-shadow form of the last-trade-loser filter, taking whole shadow trades, is the alternative.
  • Departures: his printed function bodies are swapped (the one named for long trades sums the short trades’ results), so read at face value his rules invert; the recipe follows his prose, and his Tables 10.3 and 10.5 cannot tell which version ran. His real system’s misprints (-1 point on a buy stop, Lowest(High,20)+1 for the long exit) and his overwrite of both entry prices when both breakouts fall on one bar are not reproduced.
  • First valid index: n for the shadow’s first entry; SetN(b-1, 0) keeps Average’s pass-through bars out of the gates, and the tenth shadow exit ends the unfiltered start.
  • Exact sums: on integer prices the equity lines and their averages are exact.
  • Evidence: Ruggiero chose his lengths on the same D-Mark data (1980 to 1996) his tables report.
  • Pinned by TestRecipeTradeStateShadowEquity; runnable example ExampleSeries_TradeResult_shadowEquity.

Equity-change filter (Ruggiero)

Ruggiero’s first rule: “Take only short trades when the system has been profitable on the long side but has been losing money on the short side. Using the same logic, we take only long trades when the system is making money on the short side and losing on the long side”, with the equity changes over 120 bars (long) and 180 bars (short). Put these statements in place of the eqL, eqS, okL and okS statements of shadowEquityFilter:

chgL := resL.Sum(120)
chgS := resS.Sum(180)
okS := chgS.LT(0).Mul(chgL.GT(0))
okL := chgL.LT(0).Mul(chgS.GT(0))
  • A window of results: resL.Sum(120) is eqL.Sub(eqL.Shift(120)), the change of long equity over 120 bars, bit for bit on integer prices from bar 120, so the rule depends only on the results closed in the window.
  • Departures: his Table 10.2 code, read with its function bodies as printed, takes the opposite sides, as above.
  • First valid index: as the equity-average filter; before bar 120 (180) the sums are the running totals.
  • Pinned by TestRecipeTradeStateEquityChange.

Fitness feedback control (Durenard)

Eugene Durenard (Professional Automated Trading, 2013, ch. 12) stops real trading “when the measure of fitness of S falls under a certain threshold” and relaunches it when the fitness “passes above a threshold”; “The simulated strategy runs continuously, whether real trades are allowed or not”. His trend-following fitness is the NAV less 1 + lambda times its exponential average, “at each price event”. On a shadow sh, per unit of position:

pl := sh.Shift(1).Mul(c.Delta(1)).CumSum()
nav := pl.Div(capital).Add(1)
fit := nav.Sub(nav.XAverageAlpha(alpha).Mul(1 + lambda))
live := fit.Hysteresis(phiOn, phiOff).GT(0)
real := sh.Mul(live.Shift(1))
  • Readings: the NAV is rebased at 1 with the caller’s capital; the book does not define the rebasing, which decides what his 5% band means. Not live until the first decision, as Hysteresis starts.
  • The control: Hysteresis is strict on both sides, as his fitness “under” one threshold and “above” the other. With his tested thresholds phiOn = phiOff = 0 it is the sign of fit, a tie holding the state. A relaunch joins the shadow’s open trade.
  • Trade events: his per-trade fitness needs an exponential average over exit events, which is not a method here; a mean over the last k events, nav.AverageWhere(sh.Exits(), k), is a departure, not his rule. His rolling profit factor over the last k trades is res.Max(0).AverageWhere(ex, k).Div(res.Min(0).Mul(-1).AverageWhere(ex, k)) with ex := sh.Exits(), guarded by the losses being above 0, because Div reads 0 where no trade in the window lost. His path-length fitness (PLF) is not given: its exponent and its path length are not defined in print.
  • First valid index: 1 (live.Shift(1)). XAverageAlpha seeds with the first value; he gives no seed.
  • Evidence: his 200,000-run test prints equity curves and no efficiency figure.
  • Pinned by TestRecipeTradeStateFitnessControl.

Record-drawdown breach (Kaufman)

Perry Kaufman (“Is It Broken?”, Stocks & Commodities V44:10) reads a system as possibly broken when “The maximum drawdown of the 15-minute breakout has exceeded its previous pattern”, with the drawdown in money from the equity’s running peak (his Figure 2). On the shadow’s closed-trade equity, with a fixed window n:

res := c.TradeResult(sh)
eq := res.If(math.IsNaN, 0.0, res).CumSum()
dd := eq.Sub(eq.Highest(n))
worst := dd.Lowest(n).Shift(1)
breach := dd.LT(worst)
  • Strict: a drawdown equal to the worst of the window does not fire.
  • Window and burn-in: a fixed n keeps the result the same on any slice that covers the window. The burn-in is the caller’s (“We might first excuse a small penetration of the drawdown lows”): with worst 0 on bar 0 (the Shift pad), the first dip below the first peak is a record.
  • Money, not a ratio: on a profit line from 0, eq.Div(eq.Highest(n)) reads 0 through Div’s zero rule; use a ratio only on a NAV that stays above 0. Subtract costs on the exit bars before CumSum.
  • On shadow equity gating real trades it is a function of the shadow; on the gated system’s own equity it is recursive, a loop outside this package. His deleveraging (after 1.5 standard deviations) and the 15% stop are money management.
  • Departures: the table printed as his Figure 5 does not match its caption, Moritz Siebert’s Monte Carlo study with a 59% chance of a positive month: no 59% appears in the table, and nothing says what its columns are.
  • First valid index: 0. Evidence: one anecdote, untested.
  • Pinned by TestRecipeTradeStateDrawdownBreach.