Trade State

All functions · nseries package

Positions, stops and shadow trades

The methods of this category build a position from flags (Position, PositionSigned), 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). 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.

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.
  • 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 needs a loop outside this package. 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. The sources that re-enter are 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.

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. Re-entry inside a base trade after a stop needs a loop outside this package.

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.

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.

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.