Series operations

All functions · nseries package

Sections: Core · Arithmetic · Comparison · Conditional · Function Application · Map · Slice Operations · Sorting · Highest / Lowest · Index Of · Variable-Length Windows

Core

Append

func (s Series) Append(s2 Series) Series

Append appends a series.

At

func (s Series) At(n int) Series

At is an alias for Lookback

Bool

func (s Series) Bool(lookback ...int) bool

Bool is an alias for ValueBool

Copy

func (s Series) Copy() Series

Copy makes a copy of a series.

LookBack

func (s Series) LookBack(n int) Series

LookBack removes the last n values from the series. A negative n returns the series unchanged.

New

func New(in []float64) Series

New creates a new Series from a slice of float64 values.

Ratio

func (s Series) Ratio(s2 Series) Series

Ratio returns a series where the specified series s is divided by the series s2. The two series are right-aligned at the most recent bar: the last value of s is divided by the last value of s2, the second-last by the second-last, and so on over the min(len(s), len(s2)) bars they share. The result has the length of s, with zeros before that overlap, so an empty s2 gives a zero-filled series. A zero divisor, or a quotient that is not finite (NaN, or an overflow to an infinity), gives zero. Series are paired by position only; see “Time alignment” in the manual.

SAverage

func (s Series) SAverage(ss ...Series) Series

SAverage returns a series where the receiver is averaged, bar by bar, with an arbitrary number of provided series. All of the series are right-aligned at the most recent bar: with m the length of the shortest of them, the last m values of each are averaged, and the first len(s)-m positions of the result are zero, so an empty provided series gives a zero-filled series of the receiver’s length. At each position the values are summed in argument order, receiver first, and the sum is divided by the number of series. With no provided series the result is a copy of the receiver, and an empty receiver is returned as it is. Series are paired by position only; see “Time alignment” in the manual. Averaging m 0/1 flags and multiplying by m gives the count of set flags only to rounding for some m (22 members with 15 set give 14.999999999999998), and .Round() restores the integer; the diffusion-count recipes under “Compositions” in the “Breadth” category do so.

SetN

func (s Series) SetN(n int, val float64) Series

SetN sets the first n values of a series to the specified value.

Shift

func (s Series) Shift(n int) Series

Shift shifts a series by the specified number of elements. Where the shift amount is a positive number, that many entries are removed from the right-hand end of the series, and the start of the series is padded with zeros to maintain the same length as the input. Where the shift amount is a negative number, that many entries are removed from the left-hand end of the series, and the end of the series is padded with zeros to maintain the same length as the input. A shift of zero returns a copy of the series unchanged.

Sum

func (s Series) Sum(n int) Series

Sum returns the sum of the most recent n observations, using all available observations during startup. Values of n below one return aligned zeros.

Value

func (s Series) Value(lookback ...int) float64

Value returns the last value in the series (or zero if the series is empty). An optional lookback may be provided; a lookback outside the bounds of the series returns zero.

ValueAverage

func (s Series) ValueAverage(n ...int) float64

ValueAverage returns the average of the values in the series. Optionally, a value n may be provided, in which case only the last n values will be averaged (unless n == 0, in which case all values will be averaged). An empty series returns zero.

ValueBool

func (s Series) ValueBool(lookback ...int) bool

ValueBool returns a boolean based on whether the last value in the series is greater than zero. An optional lookback may be provided; a lookback outside the bounds of the series returns false.

ValueSum

func (s Series) ValueSum(n ...int) float64

ValueSum returns the sum of the values in the series. Optionally, a value n may be provided, in which case only the last n values will be summed (unless n == 0, in which case all values will be summed).

Window

func (s Series) Window(n int) Series

Window returns the last n values from the series if n is positive or all but the first n values if n is negative.

Arithmetic

Add

func (s Series) Add(y any) Series

Add adds the provided value to each value in the series. The value may be any built-in or defined real numeric scalar type, or another Series; the result always has the receiver’s length. A Series operand is right-aligned with the receiver at the most recent bar: the last values of the two series are added, then the second-last, and so on over the min(len(s), len(y)) bars they share. Positions before that overlap are zero, a longer operand contributes only its trailing bars, and an empty Series operand gives a zero-filled series. A plain []float64 is not a Series: like any other unsupported type it gives a zero-filled series, so convert it with Series(v) or New first. Series are paired by position only; see “Time alignment” in the manual’s “Relative: Alignment” section before combining series with different calendars, missing bars or their own release schedules.

Div

func (s Series) Div(y any) Series

Div divides each value in the series by the provided value. Where the divisor is exactly zero (+0 or -0), the returned value is zero; otherwise the usual IEEE rules apply, so NaN and infinities propagate. The value may be any built-in or defined real numeric scalar type, or another Series; the result always has the receiver’s length. A Series operand is right-aligned with the receiver at the most recent bar, as for Add: each receiver value is divided by the value of y at the same distance from the most recent bar, positions before the overlap are zero, and an empty Series operand gives a zero-filled series. A plain []float64 is not a Series and, like any other unsupported type, gives a zero-filled series. Series are paired by position only; see “Time alignment” in the manual.

The zero rule differs from TA-Lib’s DIV and from pandas, which give an infinity or NaN. It means a zero-filled warm-up divided by another reads 0, which passes comparisons such as LT(1), whatever the numerator (a NaN numerator over a zero divisor also gives 0); mask the quotient’s warm-up with SetN(k, math.NaN()) after dividing, since a comparison with NaN gives 0.

Mul

func (s Series) Mul(y any) Series

Mul multiplies each value in the series by the provided value. The value may be any built-in or defined real numeric scalar type, or another Series; the result always has the receiver’s length. A Series operand is right-aligned with the receiver at the most recent bar, as for Add: values at the same distance from the most recent bar are multiplied, positions before the overlap are zero, and an empty Series operand gives a zero-filled series. A plain []float64 is not a Series and, like any other unsupported type, gives a zero-filled series. Series are paired by position only; see “Time alignment” in the manual.

Sub

func (s Series) Sub(y any) Series

Sub subtracts the provided value from each value in the series. The value may be any built-in or defined real numeric scalar type, or another Series; the result always has the receiver’s length. A Series operand is right-aligned with the receiver at the most recent bar, as for Add: each value of y is subtracted from the receiver’s value at the same distance from the most recent bar, positions before the overlap are zero, and an empty Series operand gives a zero-filled series. A plain []float64 is not a Series and, like any other unsupported type, gives a zero-filled series. Series are paired by position only; see “Time alignment” in the manual.

Comparison

Calendar keys

nseries holds no timestamps, so a seasonal or calendar rule reads the calendar from key series that the caller supplies: one value per bar, paired with the prices by position and right-aligned at the most recent bar, like any other two-series operand (see “Time alignment” in the manual). algolang is to produce the keys from the bars’ own exchange calendar (its key-generation work is planned); nseries only consumes them. Each key is a small integer held in a float64:

KeyValuesMeaning
month1 to 12month of the session date, January 1; a monthly bar takes the month of its last session
weekday1 to 7ISO weekday of the session date, Monday 1 to Sunday 7 (Go’s time.Weekday counts Sunday 0: use (wd+6)%7 + 1)
dom1 to 31calendar day of the month
doy1 to 366calendar day of the year (one higher after February in a leap year)
tdm1, 2, …trading day of the month: 1 on the month’s first session
tdr0, 1, …trading days left in the month: 0 on its last session, so tdm + tdr is the month’s number of sessions
tdy1, 2, …trading day of the year: 1 on the year’s first session
tdh0, 1, …trading days left before the next holiday closure: 0 on the last session before it

The keys belong to the exchange’s session date: a session that opens on Sunday evening is Monday’s, a half day is an ordinary session, and intraday bars repeat their session’s keys. A NaN key means unknown and fails every comparison, since a comparison with NaN gives 0; a gate that negates with Equal(0), as firstDay below does, therefore passes an unknown key, so multiply it by the key’s own GTE(0) (1 for every valid key, tdr’s 0 included, and 0 for NaN) when keys can be missing. The trading-day keys depend on the exchange’s holidays, so take them from the same calendar as the bars; an unscheduled closure changes tdr after the fact. A rule on the k-th-to-last trading day of the month (Williams’s fifth-to-last day, for example) is tdr.Equal(k - 1).

The holiday set of tdh belongs to the rule that reads it: Kaeppel’s eight holidays, for example, are New Year’s Day, Presidents’ Day, Good Friday (he writes “Easter”), Memorial Day, Independence Day, Labor Day, Thanksgiving and Christmas, where Fosback’s nine add Election Day. An unscheduled closure is not in the set, and one between the bar and the holiday shortens tdh after the fact, as it changes tdr. tdh is NaN beyond the calendar’s horizon, and a half day is an ordinary session. The k-th session before the holiday is tdh.Equal(k - 1). tdh cannot be derived from the other keys: it needs the forward holiday schedule.

A gate is a 0 or 1 series built from the comparisons, and a rule applies it with Mul:

halloween := month.GTE(11).Add(month.LTE(4))                                  // November to April
turnOfMonth := tdr.LTE(1).Add(tdm.LTE(3)).GT(0)                               // the last two and first three sessions
firstDay := tdm.Equal(1).Mul(month.Equal(1).Add(month.Equal(3)).Equal(0))     // first session, not January or March
tueThuFri := weekday.Equal(2).Add(weekday.Equal(4)).Add(weekday.Equal(5)).GT(0) // Tuesday, Thursday or Friday
early := tdm.GTE(2).Mul(tdm.LTE(5))                                           // sessions 2 to 5 of the month
entry := signal.Mul(turnOfMonth)

The block names every gate for reading; a program keeps the ones it uses. A window across the year end, such as November to April, adds its two disjoint ranges; Mul of the two would be empty. A set of separate values adds one Equal per value and closes with GT(0), which also guards against a value listed twice and against overlapping conditions, as in turnOfMonth. A 0 gate does not clear a NaN or infinite signal (Inf*0 is NaN). Each gate is a function of its own bar alone, so it does not depend on where the history starts.

EQ

func (s Series) EQ(y any) Series

EQ is an alias for Equal

Equal

func (s Series) Equal(y any) Series

Equal tests whether each entry in the receiver series is equal to a provided value or series, giving 1.0 where it is and 0.0 where it is not (NaN is never equal to anything). Numeric operands may have built-in or defined real numeric types, including uintptr. A Series operand is right-aligned with the receiver at the most recent bar: the last values of the two series are compared, then the second-last, and so on over the bars they share. Positions before that overlap are 0.0, and an empty Series operand yields receiver-length zeros. A plain []float64 is not a Series and, like any other unsupported type, yields receiver-length zeros. The result always has the receiver’s length. Series are paired by position only; see “Time alignment” in the manual.

GreaterThan

func (s Series) GreaterThan(y any) Series

GreaterThan tests whether each entry in the receiver series is greater than a provided value or series, giving 1.0 where it is and 0.0 where it is not (a comparison involving NaN is false). Numeric operands may have built-in or defined real numeric types, including uintptr. A Series operand is right-aligned with the receiver at the most recent bar, as for Equal: positions before the overlap are 0.0, and an empty Series operand yields receiver-length zeros. A plain []float64 is not a Series and, like any other unsupported type, yields receiver-length zeros. Series are paired by position only; see “Time alignment” in the manual.

GreaterThanOrEqual

func (s Series) GreaterThanOrEqual(y any) Series

GreaterThanOrEqual tests whether each entry in the receiver series is greater than or equal to a provided value or series, giving 1.0 where it is and 0.0 where it is not (a comparison involving NaN is false). Numeric operands may have built-in or defined real numeric types, including uintptr. A Series operand is right-aligned with the receiver at the most recent bar, as for Equal: positions before the overlap are 0.0, and an empty Series operand yields receiver-length zeros. A plain []float64 is not a Series and, like any other unsupported type, yields receiver-length zeros. Series are paired by position only; see “Time alignment” in the manual.

GT

func (s Series) GT(y any) Series

GT is an alias for GreaterThan

GTE

func (s Series) GTE(y any) Series

GTE is an alias for GreaterThanOrEqual

LessThan

func (s Series) LessThan(y any) Series

LessThan tests whether each entry in the receiver series is less than a provided value or series, giving 1.0 where it is and 0.0 where it is not (a comparison involving NaN is false). Numeric operands may have built-in or defined real numeric types, including uintptr. A Series operand is right-aligned with the receiver at the most recent bar, as for Equal: positions before the overlap are 0.0, and an empty Series operand yields receiver-length zeros. A plain []float64 is not a Series and, like any other unsupported type, yields receiver-length zeros. Series are paired by position only; see “Time alignment” in the manual.

LessThanOrEqual

func (s Series) LessThanOrEqual(y any) Series

LessThanOrEqual tests whether each entry in the receiver series is less than or equal to a provided value or series, giving 1.0 where it is and 0.0 where it is not (a comparison involving NaN is false). Numeric operands may have built-in or defined real numeric types, including uintptr. A Series operand is right-aligned with the receiver at the most recent bar, as for Equal: positions before the overlap are 0.0, and an empty Series operand yields receiver-length zeros. A plain []float64 is not a Series and, like any other unsupported type, yields receiver-length zeros. Series are paired by position only; see “Time alignment” in the manual.

LT

func (s Series) LT(y any) Series

LT is an alias for LessThan

LTE

func (s Series) LTE(y any) Series

LTE is an alias for LessThanOrEqual

Compositions

The calendar windows below are gates on the key series of the “Calendar keys” introduction above (month, weekday, dom, tdm, tdr, tdy and tdh), which the caller builds from the bars’ dates and pairs with the prices by position; the seasonal indexes, averages over the same keys, are on t6-seasonal-indexes.md below. Each section gives its first valid index; prices are positive and finite.

Halloween switch (Hirsch, Vakkur)

Yale Hirsch’s switch, as Bob Kargenian reports it (“The Stock Market And Seasonality”, Stocks & Commodities V15:2, February 1997, pp. 68-72), is “to be invested in the market only from November 1 through April 30, and to be in cash equivalents for the other six months”. Mark Vakkur (“Sell In May And Go Away”, Stocks & Commodities V31:6, May 2013, pp. 22-24) tests it on the S&P 500’s levels at the end of each April and October from 1979 to 2012, counting “one year’s November-April period as the six-month span ending on April of that year”. t6Halloween returns the gate, the position after the close and the gated return.

fav := month.GTE(11).Add(month.LTE(4))
pos := fav.Sub(month.Equal(4).Mul(tdr.Equal(0))).Add(month.Equal(10).Mul(tdr.Equal(0)))
pnl := c.RateOfChange(1).Mul(fav)
  • Gate and position: fav is 1 on the bars whose own close-to-close return the rule earns, and pnl is that return in percent (bar 0 reads 0). pos is 1 when the book is long after bar t’s close, what a program reads at the close: here from the close of October’s last session to the close of April’s last. c.RateOfChange(1) multiplied by pos.Shift(1) equals pnl bit for bit wherever pos.Shift(1) equals the gate, which holds from bar 1 on gap-free session keys (both multiply by exactly 0 or 1).
  • Keys: month and tdr. A NaN month reads 0 in both flags, and a NaN tdr drops the October and April corrections of pos on that bar. The two month ranges are added; Mul of them would be empty.
  • First valid index: 0.
  • Bars: the gate is the same on daily, monthly or semi-annual bars, each keyed by its last session. pos is exact on such bars too, because a monthly or semi-annual bar has tdr 0: on Vakkur’s semi-annual series it is 1 on the October bars and 0 on the April bars.
  • Anchors: the gate equals the Equal chain over its six months closed with GT(0). The May-to-October book is the explicit range month.GTE(5).Mul(month.LTE(10)); fav.Equal(0) would put an unknown month in it.
  • Variant: Jay Kaeppel (“The Stock Market, The Calendar, And You”, Stocks & Commodities V20:12, December 2002, pp. 52-61) holds to the third session of May (“sell at the close on the third trading day the following May”): the gate is fav.Add(month.Equal(5).Mul(tdm.LTE(3))).
  • Variant: Kaeppel’s power zone and dead zone (V38:1, January 2020, pp. 18-21) hold mid-caps from November through May and Treasuries from June through October, eq.RateOfChange(1).Mul(month.GTE(11).Add(month.LTE(5))) plus bd.RateOfChange(1).Mul(month.GTE(6).Mul(month.LTE(10))), with the explicit complement and both series on one month-end calendar.
  • Missing piece: Kaeppel’s composite score (V20:12) sums this window, the turn of the month, the pre-holiday window and a presidential-cycle window and holds at 2 or more; the presidential window needs a year key, which the keys table does not hold.
  • Evidence: Vakkur’s 33 half-year pairs (price index; no dividends, interest or costs) average 7.3% for November to April against 1.9% for May to October, and November to April is ahead in 22 of the 33 years. The arithmetic of his table is the test’s witness: its 66 cells are reproduced from his levels within 0.155 percentage points, the rounding of the printed levels and percentages. The returns are evidence, not a reference.
  • Pinned by TestRecipeT6Halloween and TestRecipeT6CalendarPositionGate; runnable example ExampleSeries_GTE_halloween.

Trend-confirmed switch (Harding)

Sy Harding (“Seasonality With Sy Harding”, interviewed by Jayanthi Gopalakrishnan and Bruce Faber, Stocks & Commodities V30:6, May 2012, pp. 44-49) found that “the best day to consider entering is October 16, and the best day to consider exiting is April 20”, and confirms both with MACD: a reentry “does not take place until the MACD triggers its next buy signal”, and in the spring “the exit does not take place until the MACD triggers its next sell signal”. His table reads “the level of the DJIA each time” a signal fires; the page prices each signal at that day’s close. t6Harding is the long-only position.

macd := c.MACD(12, 26).Window(-25)
sig := macd.XAverage(9).Window(-8)
buy := macd.GT(sig)
sell := macd.LT(sig)
win := month.Equal(10).Mul(dom.GTE(16)).Add(month.GTE(11)).Add(month.LTE(3)).Add(month.Equal(4).Mul(dom.LT(20)))
out := month.Equal(4).Mul(dom.GTE(20)).Add(month.GTE(5).Mul(month.LTE(9))).Add(month.Equal(10).Mul(dom.LT(16)))
pos := c.Position(win.Mul(buy), out.Mul(sell))
  • Position: pos is 1 when the book is long after bar t’s close, and its P&L is c.RateOfChange(1).Mul(pos.Shift(1)) (Harding takes the signals from the DJIA and holds a fund that tracks it). Inside the window a sell state does not exit and outside it a buy state does not enter; with no buy state before 20 April the season is skipped, and the exit waits for the first sell state from 20 April, “in some years … as late as late June”.
  • Keys: month and dom, so the edges are dates: a weekend 16 October means the next session. out is the explicit complement of win: on a bar with a NaN month, or a NaN dom in April or October, both are 0 and the trade holds, where win.Equal(0) in place of out would exit.
  • First valid index: 33, the first bar that can enter. Both MACD stages are trimmed with Window, so buy and sell are shorter, right-aligned and 0 before their overlap, and the flags read 0 on bars 0 to 32.
  • Anchors: the flags have the receiver’s length and do not both fire on one bar, so pos equals win.Mul(buy).Sub(out.Mul(sell)).Hysteresis(0, 0).GT(0) and the (0.5, -0.5) latch bit for bit. win.Add(out) is 1 on every session with valid keys.
  • Traps: the doy form doy.GTE(289).Add(doy.LT(110)) moves both edges a day early in leap years; it differs from the dated window on 17 sessions of 1980 to 2026, all in leap years. macd and sig are recursive, so a near tie can flip a comparison and change a whole season: the fixture’s smallest gap is 9.0e-5, and the multiply-add that Go fuses in XAverage’s step on arm64 moves the gap by about 5e-14. The latch is not start-invariant: a history that starts inside a season reads flat until the next entry.
  • Parameters: Harding does not publish his MACD lengths or signal line; 12, 26 and a 9-bar exponential signal are assumed.
  • Variant: Gerald Gardner (“A Seasonal Strategy With Leveraged ETFs”, Stocks & Commodities V30:11, October 2012, pp. 15-21) buys after an October close above its 50-bar average and sells after the first May bar, both at the next open: o.Position(month.Equal(10).Mul(c.GT(c.Average(50))).Shift(1), month.Equal(5).Shift(1)), whose strict GT reads 0 on Average’s pass-through bars 0 to 48.
  • Evidence: Harding’s table shows his strategy up 190.6% from 1999 to 2011 against 64.4% for the DJIA (his own figures, with no prices printed). Evidence, not a reference.
  • Pinned by TestRecipeT6Harding.

Turn of the month (Fosback)

Norman Fosback (Stock Market Logic, 1976, ch. 44, “The Seasonality Indicator”, p. 155) hypothesises “superior stock market performance on the last trading day of each month and on the first four trading days of each month”. Bob Kargenian (Stocks & Commodities V8:7, July 1990) and Kaeppel (V20:12, December 2002) trade it from close to close; Kaeppel’s rule reads “Buy at the close on the next to last trading day of each month, and sell at the close on the fourth trading day of the next month”. t6TurnOfMonth gives the five earning sessions, the position (the keys page’s turnOfMonth) and the gated return.

tom := tdr.Equal(0).Add(tdm.LTE(4)).GT(0)
pos := tdr.LTE(1).Add(tdm.LTE(3)).GT(0)
pnl := c.RateOfChange(1).Mul(tom)
  • Gate and position: tom is 1 on the bars whose own close-to-close return the rule earns, and pnl is that return in percent (bar 0 reads 0). pos is 1 when the book is long after bar t’s close, what a program reads at the close: from the close of the second-to-last session to the close of the fourth. c.RateOfChange(1) multiplied by pos.Shift(1) equals pnl bit for bit wherever pos.Shift(1) equals the gate, which holds from bar 1 on gap-free session keys (both multiply by exactly 0 or 1).
  • Keys: tdm and tdr; a NaN key reads 0 in its term. GT(0) keeps each flag at 1 in a month of four sessions or fewer, where both terms hold on one bar.
  • First valid index: 0.
  • Anchors: Krista and Konrad Sherinian (“Weekly And Monthly Seasonal Cycles”, Stocks & Commodities V29:11, October 2011, p. 35) number January 2011’s sessions 1 to 20, skipping Martin Luther King Day, the 17th; the exchange calendar’s tdm and the dom form below give the same numbers.
  • Variant: the window from the close of the session with tdr equal to a to the close of the next month’s b-th session (b >= 1) is the position tdr.LTE(a).Add(tdm.LTE(b - 1)).GT(0); Fosback’s is a = 1, b = 4.
  • Variant: the trading day of the month from dom, when the caller lacks tdm, is dom.Equal(dom.Shift(1)).Equal(0).Mul(dom.GTE(0)).SumSince(dom.LT(dom.Shift(1))): it counts sessions since the bar where dom fell, is NaN until the first month change and exact on daily and intraday bars with valid keys. A bar with a NaN dom is not counted, so the month’s later sessions read one low, and a NaN on a month’s first session or on the bar before it hides the month change. dom.ConsecUp().Add(1) reads 1 on bar 0, numbers the first partial month from the history’s start (a history from 12 January 2011 reads 1 to 13 where tdm is 8 to 20) and restarts on every repeated key of intraday bars.
  • Evidence: Kaeppel’s window on the Nasdaq Composite (1972 to 2002; no interest, dividends or costs) was in the market “only about 24% of the time” for “roughly 90% of its gain”; on the exchange calendar the gate holds 2,520 of the 10,581 sessions of 1985 to 2026 (23.8%). Fosback’s test is the S&P 500 from 1928 to 1975. Evidence, not references.
  • Pinned by TestRecipeT6TurnOfMonth and TestRecipeT6CalendarPositionGate; runnable example ExampleSeries_LTE_fosbackMonthEnd.

Late-month window (D’Errico)

Domenico D’Errico (“How AI Agents Can Reproduce, Validate, And Monitor Trading Ideas”, Stocks & Commodities V44:10, October 2026, pp. 36-42) retests his raw late-month cycle: “Enter once per month on the first available session dated from the 20th through 28th” and “Buy at that session’s close and exit at the close eight trading sessions later”, one position at a time. He warns that “A calendar date from 20 to 28 must not be confused with the 20th through 28th trading sessions of the month.” t6LateMonth returns the entry flag, the position and its return.

late := dom.GTE(20).Mul(dom.LTE(28))
entry := late.GT(late.Shift(1))
pos := c.Position(entry, nil, 8)
pnl := c.RateOfChange(1).Mul(pos.Shift(1))
  • Position: pos is 1 when the book is long after bar t’s close: on the entry bar e to e + 7, and 0 from e + 8, the exit bar, at whose close the trade leaves. pnl is the percent return of each held bar, read through pos.Shift(1). Eight sessions span at least ten calendar days, so no exit falls inside the same month’s window.
  • Keys: dom only, calendar days. A NaN dom is outside the window, so that month’s entry moves to the next session dated the 20th to the 28th.
  • First valid index: 0.
  • Anchors: pos.Shift(1) equals entry.Shift(1).Sum(8).GT(0) while entries are at least nine bars apart, as monthly entries are; entry equals late.CrossesAboveS(0.5) from bar 1.
  • Trap: a history whose bar 0 is dated the 20th to the 28th enters on bar 0, because the Shift pad reads as outside the window, though bar 0 may not be the month’s first session in it; late.CrossesAboveS(0.5) does not fire on bar 0.
  • Exposure: D’Errico’s “approximate market exposure” of 37.2% reproduces in calendar days: over 2 October 2017 to 10 July 2026 the 105 trades span 1,192 of 3,203 days, while the held bars are 840 of 2,204 sessions (38.1%). Position’s hold counts bars, so on intraday bars the trade leaves after eight bars, not eight sessions.
  • Variant: his gated entries take a signal inside the window, such as RSI(5) below 30 with its warm-up masked, c.RSI(5).SetN(5, math.NaN()).LT(30).Mul(late), since RSI reads exactly 0 on bars 0 to 4.
  • Evidence: on SPY over that window (adjusted closes, 10 basis points a trade) the raw cycle made 105 trades, 64.8% of them winners, averaging 0.49% net, against 0.27% in his 1997 to 2017 sample; on the exchange calendar the entry fires 105 times there (72 on the 20th, 15 on the 21st, 18 on the 22nd) and every trade completes. Evidence, not a reference.
  • Pinned by TestRecipeT6LateMonth; runnable example ExampleSeries_Position_lateMonth.

Pre-holiday sessions (Fosback, Kaeppel)

Fosback (Stock Market Logic, ch. 44, pp. 155-161) finds “greater than average returns on the two trading days immediately preceding a holiday market closing”, over 419 closings of nine holidays from 1928 to 1975. Kaeppel (V20:12, 2002) trades it: “Buy at the close on the third day before an exchange holiday. Sell at the close two days later. For example, if Christmas were on a Wednesday, you would buy at the close on the Friday prior and then sell at the close on Tuesday (December 24).” t6PreHoliday reads the key tdh, the trading days left before the next holiday closure (see “Calendar keys”).

pre := tdh.LTE(1)
pos := tdh.GTE(1).Mul(tdh.LTE(2))
pnl := c.RateOfChange(1).Mul(pre)
  • Gate and position: pre is 1 on the bars whose own close-to-close return the rule earns, and pnl is that return in percent (bar 0 reads 0). pos is 1 when the book is long after bar t’s close, what a program reads at the close: from the close of the third session before the closure to the close of the second-to-last. c.RateOfChange(1) multiplied by pos.Shift(1) equals pnl bit for bit wherever pos.Shift(1) equals the gate, which holds from bar 1 on gap-free session keys (both multiply by exactly 0 or 1).
  • Keys: tdh over the rule’s holiday set, here Kaeppel’s eight: New Year’s Day, Presidents’ Day, Good Friday (Kaeppel writes “Easter”), Memorial Day, Independence Day, Labor Day, Thanksgiving and Christmas (Fosback’s nine add Election Day). A NaN tdh reads 0 in both flags. Fosback’s Day 1 is tdh.Equal(1) and his Day 2 tdh.Equal(0). The counts below hold for Kaeppel’s set: a tdh built from the exchange’s full list (Martin Luther King Day from 1998, Juneteenth from 2022) adds sessions.
  • First valid index: 0.
  • Anchors: in December 2002 tdh is 2, 1 and 0 on Friday 20, Monday 23 and Tuesday 24 December (an early close is an ordinary session), so pos holds after the closes of 20 and 23 December and pre earns 23 and 24 December, as Kaeppel says. pre equals tdh.Equal(0).Add(tdh.Equal(1)). On the exchange calendar pre covers 16 sessions in 40 of the years 1980 to 2025, and 14 in the six years before a Saturday New Year’s Day, which closes nothing.
  • Variant: Kargenian’s one-session form, the day before each holiday ("‘Tis The Season", Stocks & Commodities V8:7, July 1990, pp. 255-256, following Zweig), earns tdh.Equal(0); he also holds the session after Thanksgiving and after Christmas.
  • Evidence: Kaeppel’s window was in the Nasdaq Composite 6.3% of the time from 1972 to 2002 and averaged +0.171% a day, against +0.032% on the other days (no costs). Kargenian’s pre-holiday futures trades since 1982 summed to -4.25 points before costs (V15:2, 1997): “the strength prior to major holidays no longer appears to be present”. Evidence, not references.
  • Pinned by TestRecipeT6PreHoliday and TestRecipeT6CalendarPositionGate; runnable example ExampleSeries_LTE_preHoliday.

Santa Claus rally (Vakkur; the Almanac)

Vakkur (“Exploiting The Holidays With The Stock Market”, Stocks & Commodities V29:4, Bonus Issue 2011, pp. 44-46) numbers “the days prior to Christmas C-3 (for the third to last trading day before Christmas) to C-1” and “C+1 indicating the first trading day after Christmas, out to C+3”; his best strategy is “to go long three trading days prior to Christmas, exit the market three trading days after Christmas, then go long for the first trading day of the new year”. The Stock Trader’s Almanac’s window, the last five sessions of the year and the first two of the next, is the second gate; no source read here prints its definition. t6Santa returns both gates.

cMinus := month.Equal(12).Mul(dom.LTE(24)).Mul(tdh.LTE(2))
after := month.Equal(12).Mul(dom.GTE(26))
cPlus := after.Mul(after.Shift(3).Equal(0))
santa := cMinus.Add(cPlus).Add(tdy.Equal(1))
almanac := month.Equal(12).Mul(tdr.LTE(4)).Add(tdy.LTE(2))
  • Gates: each is 1 on the seven sessions a year whose own close-to-close returns the window earns, so its P&L is c.RateOfChange(1).Mul(santa) (bar 0 reads 0). Vakkur’s six-day column is his “six-day change” over the span, a purchase at the close of C-4 sold at the close of C+3; its one-decimal cells do not separate a compounded change from a sum.
  • Keys: month, dom, tdr, tdy and tdh, with Christmas in the holiday set; almanac reads the documented keys only. A NaN key reads 0 in its term. cPlus negates after.Shift(3) with Equal(0), which is safe because after is a product of comparisons and is not NaN on any bar, unlike a key.
  • First valid index: 0, for a history that does not start inside 26 to 31 December (see the trap).
  • Anchors: dom.LTE(24) keeps C-3 to C-1 on Christmas’s closure whether it falls on the 24th (a Saturday Christmas), the 25th or the 26th (a Sunday Christmas), and keeps the sessions before the New Year’s closure out. In 2002 C-3 to C-1 are 20, 23 and 24 December, Kaeppel’s dates. On the exchange calendar both gates hold seven sessions in each year from 1980 to 2025, and C+3 is always in December: the years with five December sessions after the 25th are the Saturday-Christmas years 1982, 1993, 1999, 2004, 2010 and 2021, and Vakkur’s sidebar fills its unnamed column, C+5, in 1993, 1999 and 2004 (it leaves 1982’s blank, though 1982 is one of them).
  • Trap: the C+1 counter reads the start of the run of December sessions after the 25th, so a history that starts inside 26 to 31 December misreads it (the Shift pad reads as no earlier December session): a history from 28 December 2005, C+2, flags C+4. Start the history outside those dates.
  • Variant: Robert Steelman’s Christmas trade (Stocks & Commodities V24:2, February 2006) buys the open of C-2 and sells the close of C-1: c.Sub(o) on the bar month.Equal(12).Mul(dom.LTE(24)).Mul(tdh.Equal(1)) plus c.Delta(1) on the next.
  • Evidence: in Vakkur’s sidebar (S&P 500, 1980 to 2009, no costs) C-3 to C+3 rose in 21 of 30 years and averaged 1.12%. Evidence, not a reference.
  • Pinned by TestRecipeT6Santa.

Options-expiration week (Steelman, Moy)

Steelman (“Seasonal Patterns In The Markets”, Stocks & Commodities V24:2, February 2006, pp. 72-77) buys “the Monday preceding the third Friday of the month” at the open and sells at the close, and “In January and February, because of the holidays, this trade could take place on a Tuesday”. David Moy (“OEX Options Expiration Trading Patterns”, Stocks & Commodities V13:10, October 1995, pp. 438-442) would “buy the options expiration week and sell the post-expiration week”, trading at the close of the week before. t6Expiration returns both week flags, Steelman’s entry and its points.

fri := dom.Add(5).Sub(weekday)
optx := fri.GTE(15).Mul(fri.LTE(21))
postx := fri.GTE(22).Mul(fri.LTE(28))
entry := optx.Mul(weekday.LTE(weekday.Shift(1)))
pts := c.Sub(o).Mul(entry)
  • Gates: the trade opens and closes on the entry bar, so pts needs no Shift; in percent it is c.Div(o).Sub(1).Mul(100).Mul(entry). The week flags are earning gates for Moy’s weeks: a week’s type is known at the close before it, because the calendar is.
  • Keys: dom and weekday (ISO, Monday 1; Go’s time.Weekday needs the keys page’s conversion). fri is the day of the month of the week’s Friday when that Friday lies in the bar’s month; a week that straddles a month end has its Friday on the 1st to the 7th or past the 28th, so both flags read 0 on all its bars. A NaN key reads 0 in every flag, and a NaN weekday also blocks the next bar’s entry.
  • First valid index: 0 for the week flags; bar 0 cannot enter, because the Shift pad is below every weekday.
  • Anchors: on the exchange calendar the flags agree with the dates on every session of 1980 to 2026. From 1985 to 2004 entry fires 240 times, once a month: 228 Mondays and 12 Tuesdays after Presidents’ Day or Martin Luther King Day, from 18 February 1986 to 17 February 2004. It also fires on Monday 17 September 2001, the reopening, which Steelman did not trade (his 239). His ninth-day short, dom.Equal(9), gives his 169 trades and each month’s count.
  • Trap: on intraday bars weekday.LTE(weekday.Shift(1)) fires on every bar of the session; there the first session of the week is doy.Equal(doy.Shift(1)).Equal(0).Mul(weekday.LTE(weekday.Shift(1))), whose Equal(0) reads a NaN doy as a new session: multiply by doy.GTE(0) when keys can be missing.
  • Variant: Moy’s weekly switch, short in post-expiration weeks and long in the others, earns c.Delta(1).Mul(postx.Mul(-2).Add(1)) in points. His weeks from 1 January 1992 to 12 May 1995 are 40 expiration, 40 post-expiration and 96 other weeks on the exchange calendar, his sample sizes.
  • Missing piece: the expiration day itself (a Thursday when the third Friday is a holiday, such as Good Friday) needs a caller key; neither source trades it.
  • Evidence: Steelman’s Mondays gained 406.26 S&P 500 points in 239 trades from 1985 to 2004, 156 of them up (no costs); Moy’s OEX expiration weeks averaged 2.03 points against -1.62 in the week after (1986 to 1995). Evidence, not references.
  • Pinned by TestRecipeT6Expiration; runnable example ExampleSeries_Sub_expirationWeek.

North’s two readings below are the presignal area, one oscillator against its own average, and the Insync index, ten votes: six by zone and four by a crossing of their own averages. Its %B, c.Standardise(20).Mul(25).Add(50), is the pctB of the %b section of bollinger-bands/t6-bands-and-envelopes.md on the 100 scale, to rounding off flat windows; its ease of movement is Arms’s raw line, whose scaled form, his 13-day sum and Meibuhr’s worksheet are in “Ease of movement (Arms)” under “Volume Flow”, where Arms’s order at a scale of 1 agrees with this line to rounding. Each section gives its first valid index; prices are positive and finite and lengths are in bars.

Presignal area (North)

Norm North (“The Insync Index”, Stocks & Commodities V13:1, January 1995, pp. 9-13) says an indicator “is said to enter its presignal area when it is committed to fire a signal”. For a zero-crossing oscillator against its trailing moving average, “the prebuy signal area starts when the indicator is below the buy line and the buy line first goes below zero”, “the presell signal area starts when the indicator is above the sell line and the sell line first goes above zero”, and “a signal is issued at the end of a presignal area”, when the indicator crosses the line. t6Presignal is that latch for a line x and its m-bar simple average.

line := x.Average(m).SetN(f+m-1, math.NaN())
xm := x.SetN(f+m-1, math.NaN())
pre := xm.Position(xm.GT(line).Mul(line.GT(0)), xm.LTE(line)).
    Sub(xm.Position(xm.LT(line).Mul(line.LT(0)), xm.GTE(line)))
  • Inputs: the line x, its first valid index f and the averaging length m (10); the result is +1 in a presell area, -1 in a prebuy area and 0 elsewhere.
  • First valid index: f + m - 1.
  • Anchors: the latch equals the memoryless vote xm.GT(line).Mul(line.GT(0)).Sub(xm.LT(line).Mul(line.LT(0))) wherever that vote is non-zero; they part only where the line changes sign inside an area. On x = NaN, -4, -4, -4, 8, -2, -1, 2 with f = 1 and m = 4 the line is -1, -0.5, 0.25, 1.75 from bar 4, and the latch reads -1, -1, 1 on bars 5 to 7 where the memoryless vote reads -1, 0, 1. A second hand case, x = 1, -1, -2, -3, -1, 1, 2, 3 against the line 2, 0.5, -0.5, -1, 0.5, 1.5, 2.5, 2.0, holds its prebuy area for three bars after the line turns positive. As a latch of two 0/1 flags that are not both 1 on a bar, each Position equals en.Sub(ex).Hysteresis(0, 0).GT(0) bit for bit.
  • Traps: the mask on x is redundant for the output (every flag also compares with the masked line, and a comparison with NaN fails), but it keeps a warm-up value from opening an area. The latch depends on where the history starts. A line that is 0 in exact arithmetic can be a rounding residual: on the test fixture’s 3-bar rate of change with m = 2 the line is -5.6e-15 where two percentages of 100/378 should cancel, and the output reads -1 on three bars.
  • Variant: the memoryless vote above is the shorter form; North’s Insync below uses it.
  • Evidence: North’s figure of Appel’s MACD presignal areas (AlliedSignal, 1994) is a chart, and its cases do not separate the latched and memoryless readings. Evidence, not a reference.
  • Pinned by TestRecipeT6Presignal.

Insync index (North)

North: “The values of the consensus indicator are simply the net sum of all its component indicators’ prebuy and presell signal areas”; for ten indicators, “take the number of presells minus prebuys, multiply by 5 and add 50”, and trade long only on “two threshold lines drawn at values of 10 and 90”. His tables give the components and their lines: CCI(14) at -100 and 100, Bollinger’s %B(20) at 5 and 95, RSI(14) at 35 and 65, %K(14) and %D(14) at 20 and 80, MFI(20) at 40 and 60, and four zero-crossing lines against 10-bar averages: ROC(10), Arms’s ease of movement EMV(10) (“EMV = Midpoint Move / Box Ratio”, Stocks & Commodities V8:5, May 1990), the price detrend oscillator PDO(18) (Bernstein’s “mathematical difference between price and a given length moving average”, Short-Term Futures Trading, 1993, appendix, p. 291; ch. 9 applies it) and MACD(7), “Appel’s 7/42/9” in his figure. t6Insync uses the memoryless vote for the four lines.

rsi := c.RSI(14)
macd := c.MACD(7, 42)
k := c.Stochastic(h, l, 14)
d := k.Average(3)
pctB := c.Standardise(20).Mul(25).Add(50)
cci := c.CCI(h, l, c, 14)
mfi := c.MFI(h, l, c, v, 20)
roc := c.RateOfChange(10)
emv := h.Add(l).Mul(0.5).Delta(1).Mul(h.Sub(l)).Div(v).Average(10)
pdo := c.Sub(c.Average(18))
rocL, emvL, pdoL, macdL := roc.Average(10), emv.Average(10), pdo.Average(10), macd.Average(10)
zones := cci.GT(100).Sub(cci.LT(-100)).Add(pctB.GT(95).Sub(pctB.LT(5))).Add(rsi.GT(65).Sub(rsi.LT(35))).
    Add(k.GT(80).Sub(k.LT(20))).Add(d.GT(80).Sub(d.LT(20))).Add(mfi.GT(60).Sub(mfi.LT(40)))
lines := roc.GT(rocL).Mul(rocL.GT(0)).Sub(roc.LT(rocL).Mul(rocL.LT(0))).
    Add(emv.GT(emvL).Mul(emvL.GT(0)).Sub(emv.LT(emvL).Mul(emvL.LT(0)))).
    Add(pdo.GT(pdoL).Mul(pdoL.GT(0)).Sub(pdo.LT(pdoL).Mul(pdoL.LT(0)))).
    Add(macd.GT(macdL).Mul(macdL.GT(0)).Sub(macd.LT(macdL).Mul(macdL.LT(0))))
votes := zones.Add(lines)
insync := votes.Mul(5).Add(50).SetN(50, math.NaN()).MaskNonFinite(rsi, macd)
pos := c.Position(insync.CrossesAboveS(10), insync.CrossesBelowS(90))
  • Inputs: the high h, low l, close c and volume v; insync runs from 0 to 100 in steps of 5, and pos is the long-only book at the signal bar’s close.
  • First valid index: 50, where the MACD’s line is first valid (41 + 10 - 1). The result is NaN wherever the RSI or the MACD is not finite (MaskNonFinite(rsi, macd)), so from a NaN close on: those recursions do not recover, and a NaN component would otherwise vote 0 for good while the index ran on nine.
  • Anchors: votes.Mul(5).Add(50) is exact, a multiple of 5 on every valid bar; votes.Div(10).Add(1).Mul(50) is not (-8 votes give 9.999999999999998, not 10). The thresholds are inclusive, as North’s 0 and 100 settings need: CrossesAboveS(10) tests the bar before with <=. %B as c.Standardise(20).Mul(25).Add(50) equals Bollinger’s 100 (c - lower)/(upper - lower) with 2-SD population bands to rounding off flat windows. Arms’s table checks the raw line h.Add(l).Mul(0.5).Delta(1).Mul(h.Sub(l)).Div(v): on bars of range 1 with his box ratio as the volume and his midpoint moves, it reproduces 42 of his 44 values at two decimals (day 12 used an unrounded box ratio, 1.89 for 1.88, and day 38 prints -1.00 for +1.00), and his “13-day” column is a running 13-day sum plus 1.01 (days 14 to 30) and 1.13 (days 31 to 45), not an average, which changes no vote.
  • Traps: Bollinger’s own %B divides by a zero width in its warm-up and on flat windows and votes prebuy there (0 is below 5), where the Standardise form reads 50. RSI and MFI read 0 in their warm-up and the stochastic reads 0 on a flat window, all prebuy votes, hence the mask to bar 50. A NaN volume makes MFI NaN for its 20-bar window and the ease of movement NaN for about 20 bars, and a NaN compares false, so those votes fall silent there and the index runs on fewer components without showing it; MaskNonFinite(rsi, macd, mfi, emv) would mark those bars too. A zero volume gives an ease of movement of 0 (Div’s rule) where Arms’s ratio is undefined. Latched presignal lines differ from the memoryless ones only where a line changes sign inside an area: on the 550 valid bars of the test fixture, on none.
  • Variant: the latched lines are t6Presignal(roc, 10, 10), t6Presignal(emv, 10, 10), t6Presignal(pdo, 17, 10) and t6Presignal(macd, 41, 10), summed with the zones as before.
  • Variant: a displaced detrend, the average shifted by n/2 + 1 as in TradeStation’s Detrend, is c.Sub(c.Average(18).Shift(10)), masked to bar 27.
  • Evidence: North’s 450-day simulations (DJ 30, S&P 100 and 39 futures, 1993 and 1994) rank the Insync first among its components at 5 and 95 or 0 and 100 (in the S&P 100 and futures tables; the DJ 30 table is a figure), by a net gain that leaves out each indicator’s best gain and worst loss and cannot be recomputed from his tables. Evidence, not a reference.
  • Pinned by TestRecipeT6Insync; runnable example ExampleSeries_GT_insync.

The seasonal indexes below average, for each month (or a caller’s period), the ratio of its price to a yearly or centred average (less 1 in Kaufman’s form) over past years, on one bar a period; the calendar windows that trade the same keys are on t6-calendar-windows.md above. Each section gives its first valid index; prices are positive and finite and lengths are in bars.

Yearly-average seasonal index (Kaufman; Mokrasch)

Perry Kaufman (“A Simple Way To Trade Seasonality”, Stocks & Commodities V37:10, September 2019, pp. 20-23) builds the index in four steps: put the monthly prices in a table, “Calculate the average price for the year”, “Divide the monthly price by its yearly average and subtract 1” and “Average the calculated values for each month”; his frequency counts the years with a positive value. In “A Strategy For Trading Seasonal And Non-Seasonal Markets” (V41:13, December 2023, pp. 18-23) he uses month-end prices, “dividing the end-of-month price by the yearly average of those month-end prices”. t6SeasonalIndex gives month k’s index and frequency over the last y completed years, held between Decembers.

x := c.Shift(12 - k).Div(c.Average(12)).Sub(1)
dec := month.Equal(12).SetN(11, 0)
si := x.AverageWhere(dec, y)
f := x.GT(0).MaskNonFinite(x).AverageWhere(dec, y)
  • Bars: gap-free monthly bars, one a calendar month, with c the month-end close and month its month. On a December bar from bar 11 on, c.Average(12) is that calendar year’s mean and c.Shift(12 - k) its month-k close; x on other bars is not read. From daily bars, the month-end close of month k is c.ValueWhen(month.Equal(k).Mul(tdr.Equal(0))).
  • Keys: month only. A December before bar 11 is a partial year and not an event: SetN(11, 0) clears it, since Average passes its input through there. f counts an exactly 0 ratio as not up, and is NaN while its window holds a non-finite ratio, where x.GT(0) alone would read NaN as down.
  • First valid index: the y-th unmasked December, bar 12y - 1 when the bars start in January; y = 0 averages every December so far, from the first.
  • Anchors: on Kaufman’s corn month-end prices (2023, Figure 8, March 2001 to August 2022), si with y = 1 at the December of each year from 2002 to 2021 is his Figure 9 cell at three decimals, all 240 of them; with y = 5 at December 2021 it is his “Average 2017-2021” row in 11 of the 12 months, and October’s -0.0024 prints -0.003 because the row averages the rounded cells (-0.0026). The 2019 prices average exactly 222.5, February’s close, so that ratio is exactly 0 and not up.
  • Traps: a zero mean gives a 0 ratio through Div’s zero rule, and back-adjusted futures can be negative or near zero, so use ratio-adjusted or unadjusted month-end prices (Kaufman’s own 2019 tables are back-adjusted). With y = 4 the frequency takes only five values, and an index of twelve months from a few years describes the sample rather than predicts.
  • Variant: Lewis Mokrasch (“Detecting Seasonality”, Stocks & Commodities V10:8, August 1992, pp. 339-343) expresses each monthly average price as a percentage of the year’s average: his column means are si.Add(1).Mul(100) for the same prices and years, to rounding, with c his monthly averages rather than month-end closes.
  • Missing piece: daily keyed statistics, such as Katz and McCormick’s seasonal change by calendar date (V15:4, 1997), Ruggiero and Barna’s index by trading day of the year (Cybernetic Trading Strategies, 1997), Williams’s seasonal trend and the per-day tables of Sherinian and of Connors and Alvarez, need a mean keyed by each value of a key; the keyed means planned with the Markov models (StateMean, StateUpProb, StateCount) are not shipped, and Ruggiero and Barna’s scaling over the whole year needs every key’s statistic at once, which no planned method gives.
  • Missing piece: Kaufman’s 2019 system buys at the end of the month of lowest frequency and sells at the end of the month of highest (75% or more against 25% or less, over four years), or sells short from the highest to the lowest when it comes first. Choosing those months needs an element-wise maximum and minimum of two series, the two-series Max and Min this page first asked for, which shipped after it as Maximum and Minimum (under “Math”): a chain of Maximum over the twelve months’ frequency series gives the highest, top, and a month’s f.EQ(top) says whether it reaches it, as a chain of Minimum gives the lowest. The system itself, its position between the two months chosen each December, is not written here.
  • Evidence: Kaufman’s 2019 system was tested on 18 futures markets from 1989 to 2019 and on nine stocks and SPY, and his 2023 rules on wheat, corn and soybeans; both are backtests without printed trades. Evidence, not references.
  • Pinned by TestRecipeT6SeasonalIndex; runnable example ExampleSeries_AverageWhere_seasonalIndex.

Ratio to a centred moving average (Arrington)

George Arrington (“Seasonal Adjustment Of Time Series Data”, Stocks & Commodities V17:1, January 1999, pp. 23-26) describes “the ratio to moving average method”: a trailing 12-month average; then, “Because we are working with an even number of periods”, the average of the two 12-month averages that centre on the month; the ratio of each month to it; the average ratio of each calendar month; and that average standardised so that the twelve sum to 12. t6RatioToMA posts each ratio p/2 bars late, when it is known, and averages period k’s ratios.

a := x.Average(p)
d := a.SAverage(a.Shift(1))
y := x.Shift(p / 2).Div(d)
ev := period.Shift(p/2).Equal(k).SetN(p, 0)
avg := y.AverageWhere(ev, w)
  • Causality: the published ratio needs p/2 later values. Posted at bar t, the ratio of bar t - p/2 reads nothing after t, and averaging only posted ratios makes the index causal and unchanged for every bar it covers. Arrington’s published averages pool every year, the last included, so they describe the sample.
  • Standardisation: with tot the sum of the p avg series, one call per period, the standardised index of period k is avg.Mul(float64(p)).Div(tot); the p values sum to p, to rounding.
  • Keys: period, the bar’s month (or quarter, with p = 4), shifted with the ratio; the Shift pad and SetN(p, 0) keep Average’s pass-through warm-up out of the events.
  • First valid index: p for y; avg from period k’s w-th event from bar p on (its first, for w = 0); the standardised index once every period has its window, bar 23 on Arrington’s data with w = 0.
  • Anchors: on Arrington’s AMEX average daily volumes (Figure 1, January 1993 to June 1998), the trailing average at December 1993 is his 18,196, the centred averages and ratios of July 1993 to February 1994 round to his Figure 2, and every one of Figure 3’s 54 ratios, 12 averages and 12 standardised averages reproduces at three decimals; the averages sum to 12.030.
  • Variant: Jack Karczewski (“Seasonal Variations In A Semiconductor Stock”, Stocks & Commodities V10:10, October 1992, pp. 439-443) averages each quarter’s ratios “after dropping the highest and lowest numbers”: with n := ev.CumSum(), the modified mean for w = 0 is avg.Mul(n).Sub(y.QuantileWhere(ev, 0, 1)).Sub(y.QuantileWhere(ev, 0, 0)).Div(n.Sub(2)) where n >= 3, to rounding.
  • Evidence: the AMEX volumes run above trend in February (1.051) and June (1.063) and below it in April (0.948) and August (0.945); a description of one exchange’s volume from 1993 to 1998, with no trading rule or costs. Evidence, not a reference.
  • Pinned by TestRecipeT6RatioToMA.

Conditional

If

func (s Series) If(cond any, yes any, no any) Series

If takes a function that acts on each element of a series and returns a boolean that is used to determine whether the “yes” or “no” values passed will be used in the corresponding positions in the returned series. The condition function can take either a single float64 (the series value at each position in the series) or a float64 and an int, which will be the series value and its position in the series. The “yes” and “no” values can be either a scalar, another series, or a function. If a function is passed as the “yes” or “no” value, it should accept either a single float64 (the series value) or a float64 and an int (the series value and its position in the series). A series passed as the “yes” or “no” value is right-aligned with the receiver at the most recent bar, each branch on its own: its last value is used at the receiver’s last position, its second-last at the second-last, and so on. A branch shorter than the receiver gives zero at the earlier positions it does not reach (where it is selected), and a longer branch contributes only its trailing len(s) values. For example, with a condition that is always true, Series{1, 2, 3}.If(cond, Series{8}, 0) gives [0, 0, 8]. Series are paired by position only; see “Time alignment” in the manual. An unsupported cond returns a zero-filled series of the same length; unsupported yes/no values yield zero at the positions where they are selected. Defined function types with these exact underlying signatures are accepted. Scalar branches accept built-in and defined real numeric types, including uintptr. Each operand is normalised once before iteration. A typed-nil function value of any of these forms is treated as unsupported and is not invoked. Only the selected branch (yes or no) is evaluated at each position.

MaskNonFinite

func (s Series) MaskNonFinite(refs ...Series) Series

MaskNonFinite returns a copy of the series with NaN on every bar where it, or any reference series paired with it at the most recent bar, is not finite, and on every bar that no reference covers.

Each reference is right-aligned with the receiver at the most recent bar: the last values are paired, then the second-last, and so on. With m the shortest of the lengths of the receiver and the references, the receiver’s last m bars are paired, and each reference contributes only its last m values. On a paired bar the result is NaN when the receiver’s value, or any reference’s paired value, is NaN, +Inf or -Inf; otherwise it is the receiver’s value with its bits kept, so -0 stays -0. With no reference, m is the receiver’s length, and the receiver’s own NaN, +Inf and -Inf become NaN. Every masked bar holds math.NaN(), not the payload of the NaN it masks, so the result has the same bits on every architecture.

Every receiver bar before the overlap reads NaN, not 0: the result is a mask for a state series, and a 0 there would read as the real state 0. So an empty reference, which leaves no overlap, gives NaN on every bar. A nil or empty receiver gives an empty Series. The first valid index is 0 within the overlap, the first paired bar, and the minimum overlap is 1. Series are paired by position only; see “Time alignment” in the manual.

The method is pointwise: each bar of the result reads only the same bar of each input, so it is causal, start-invariant and deterministic, and a missing value changes only its own bar. It makes one allocation and takes time proportional to len(s)*(1+len(refs)).

GT, LT and EQ read a comparison with NaN as false, so they turn a missing bar into the state 0, and UpDown and UpDownSum classify a NaN change as flat. Masking a comparison’s result with the series it compared keeps a missing bar missing. With no reference, MaskNonFinite equals If with a condition that is true for NaN, +Inf and -Inf, the yes value math.NaN() and the receiver as the no value, bit for bit. If can mask by another series only through a condition that reads it by position, without aligning it; MaskNonFinite pairs every reference at the most recent bar.

The name is not that of MaskedCorrelation, a correlation over an inclusion mask that the relative-indicators plan’s WP4.22 builds only on request: MaskNonFinite computes no statistic and only marks bars as missing. It is GLE-227’s NaN-preserving helper for flags and states and has no published source.

The compositions that use it are listed under “Compositions” in the “Consecutive”, “Comparison”, “Stochastic” and “Bid-Ask Spread Estimators” categories. The trend-strength compositions that use it are in the “Donchian Channels”, “Momentum” and “Average Directional Index” categories. Carver’s forecasts under “MACD” and Dukas’s phases under “Moving Averages” use it too. The breadth recipes under “Breadth” and the implied-volatility recipes under “Volatility Estimators” use it as well. Valcu’s psychological line under “N of M” also uses it. The essential-bar recipes under “Event Operators” use it in their masks.

Verification: for a reference x of the receiver’s length and a receiver with no -0, s.MaskNonFinite(x) equals s.Add(s.Sub(s)).Add(x.Sub(x)) bit for bit on every bar where it is not NaN, and is NaN wherever that is NaN, and each further reference adds its own Add(x.Sub(x)). With no reference it equals the If form above bit for bit, NaN payloads included, and with references of the receiver’s length it equals If with a condition that also tests each reference at the same position.

Function Application

Func

func (s Series) Func(fn any, args ...any) Series

Func applies a function to the receiver. Supported signatures are func() Series, func(Series) Series, func() []float64, and func([]float64) []float64, including defined function types with those exact underlying signatures. The callback is invoked once; its result is returned directly, preserving any aliasing chosen by the callback. Unsupported or typed-nil callbacks return receiver-length zeros. The optional args are unused.

Map

Map

func (s Series) Map(f func(x float64) float64) Series

Map applies an arbitrary func to each element of the series s and returns a series with the resulting computed values. A nil func returns a zero-filled series of the same length.

Slice Operations

Left

func (s Series) Left(n int) Series

Left returns a series of the n left-most values from a series.

Right

func (s Series) Right(n int) Series

Right returns a series of the n right-most values from a series.

Sorting

Reverse

func (s Series) Reverse() Series

Reverse returns a copy of the series with the element order reversed.

Sort

func (s Series) Sort(direction ...int) Series

Sort returns a copy of the series sorted in ascending order. An optional direction parameter can be provided; a negative value sorts in descending order.

Highest / Lowest

Highest

func (s Series) Highest(n int) Series

Highest returns the highest value from the last <n>{=html} values. For the first <n-1>{=html} values, the highest along the series to that point is returned. A value of n less than one returns a copy of the original Series.

Lowest

func (s Series) Lowest(n int) Series

Lowest returns the lowest value from the last <n>{=html} values. For the first <n-1>{=html} values, the lowest along the series to that point is returned. A value of n less than one returns a copy of the original Series.

Index Of

IndexOfHighest

func (s Series) IndexOfHighest(n int) int

IndexOfHighest returns the index (zero-based) of the highest value in last “n” values in a series. Where the highest value occurs more than once in the series, the index of the most recent (latest) one is returned. For example, the index returned for { 4, 6, 8, 10, 12, 10 } would be 4, since 12 is the fifth (i.e. index = 4) value in the series.

IndexOfHighestS

func (s Series) IndexOfHighestS(n int) Series

IndexOfHighestS returns a series of indices (zero-based) of the highest value in “n”-length windows in a series. Where the highest value occurs more than once in the series, the index of the most recent (latest) one is returned.

Bars 0 to n-2 are -1, a sentinel outside the valid range 0 to n-1. The sell setup risk level in the manual’s “DeMark Indicators” section reads it over the nine bars of a TD Setup. DeMarkSetupRiskLevel computes that level directly. The index counts from the window’s oldest bar, not back from the current bar: the bars since the latest highest high are (n-1) less it, which is how the cup’s handle length is read in the manual’s “Chart Patterns” section.

IndexOfLowest

func (s Series) IndexOfLowest(n int) int

IndexOfLowest returns the index (zero-based) of the lowest value in last “n” values in a series. Where the lowest value occurs more than once in the series, the index of the most recent (latest) one is returned. For example, the index returned for { 12, 4, 6, 8, 10, 12 } would be 1, since 4 is the second (i.e. index = 1) value in the series.

IndexOfLowestS

func (s Series) IndexOfLowestS(n int) Series

IndexOfLowestS returns a series of indices (zero-based) of the lowest value in “n”-length windows in a series. Where the lowest value occurs more than once in the series, the index of the most recent (latest) one is returned.

Bars 0 to n-2 are -1, a sentinel outside the valid range 0 to n-1. The buy setup risk level in the manual’s “DeMark Indicators” section reads it over the nine bars of a TD Setup. DeMarkSetupRiskLevel computes that level directly.

Variable-Length Windows

AverageVariable

func (s Series) AverageVariable(p Series, minPeriod, maxPeriod int) Series

AverageVariable computes Average’s arithmetic mean with a period from p at each bar.

p is right-aligned with s. Periods are clamped in float64 to [minPeriod, maxPeriod], then truncated; -Inf selects minPeriod and +Inf selects maxPeriod. Bars before a shorter p’s overlap, NaN periods and periods exceeding the history so far give NaN, so the start is data-dependent, not a zero warm-up; a longer p’s surplus oldest values are unused. Invalid bounds (minPeriod < 1 or minPeriod > maxPeriod) return len(s) aligned zeros. maxPeriod may exceed len(s). Series are paired by position only; see “Time alignment” in the manual.

For a constant period n within the bounds, bars from n-1 equal Average(n) bit for bit; earlier bars are NaN where Average copies the input. A window containing NaN gives NaN; infinities of one sign give that infinity, and both signs give NaN; a finite window whose sum overflows gives that infinity. Zero results are +0. Work is amortised O(1) per bar for a steady period, with O(q) rebuilds when the window grows. During a run of invalid bars the queue only grows, so working space is O(len(s)) besides the result.

Uses include Arrington’s variable-length moving average and the moving average of Jacinta Chan’s adjustable-bands z-test, ABZ’, whose recipe is under StdDevVariable. For finite data and finite periods, within TA-Lib’s period bounds (1 to 100000), TA-Lib MAVP with SMA agrees to rounding on overlapping output. MAVP starts at maxPeriod-1 regardless of the selected period and, from TA-Lib 0.8, treats NaN periods as minPeriod. XAverageAlphaS with per-bar alpha 2/(q+1) is the exponential analogue; TA-Lib MAVP(EMA) instead selects from a bank of fixed-period EMAs.

Sources: Arrington, “Building A Variable-Length Moving Average”, Stocks & Commodities (June 1991); TA-Lib MAVP.

HighestVariable

func (s Series) HighestVariable(p Series, minPeriod, maxPeriod int) Series

HighestVariable computes Highest’s window maximum with a period from p at each bar.

p is right-aligned with s. Periods are clamped in float64 to [minPeriod, maxPeriod], then truncated; -Inf selects minPeriod and +Inf selects maxPeriod. Bars before a shorter p’s overlap, NaN periods and periods exceeding the history so far give NaN, so the start is data-dependent, not a zero warm-up; a longer p’s surplus oldest values are unused. Invalid bounds (minPeriod < 1 or minPeriod > maxPeriod) return len(s) aligned zeros. maxPeriod may exceed len(s). Series are paired by position only; see “Time alignment” in the manual.

For a constant period n within the bounds, bars from n-1 equal Highest(n) bit for bit; earlier bars are NaN instead of its warm-up. A window containing NaN gives NaN; infinities are ordered normally. The builtin max rules apply: max(-0, +0) is +0, and a window of only -0 gives -0. Each window is re-read, so this is not a ratchet: growing the period brings older observations back. Work is O(q) per valid bar, with O(1) working space besides the result.

For a Castleman channel ending at the previous bar, use high.Shift(1).HighestVariable(p, minPeriod, maxPeriod), retaining bar t’s period. Shifting HighestVariable’s result instead uses bar t-1’s period. Shift(1) pads bar 0 with 0, which any window reaching bar 0 then reads. The adaptive channel breakouts under “Donchian Channels” in the manual use both forms, Castleman’s the first and Ruggiero’s the second. For the maximum since each event, see HighestSince; for a fixed window, Highest(n) or Quantile(n, 1).

Sources: Arrington, “Building A Variable-Length Moving Average”, Stocks & Commodities (June 1991); TA-Lib MAVP.

LowestVariable

func (s Series) LowestVariable(p Series, minPeriod, maxPeriod int) Series

LowestVariable computes Lowest’s window minimum with a period from p at each bar.

p is right-aligned with s. Periods are clamped in float64 to [minPeriod, maxPeriod], then truncated; -Inf selects minPeriod and +Inf selects maxPeriod. Bars before a shorter p’s overlap, NaN periods and periods exceeding the history so far give NaN, so the start is data-dependent, not a zero warm-up; a longer p’s surplus oldest values are unused. Invalid bounds (minPeriod < 1 or minPeriod > maxPeriod) return len(s) aligned zeros. maxPeriod may exceed len(s). Series are paired by position only; see “Time alignment” in the manual.

For a constant period n within the bounds, bars from n-1 equal Lowest(n) bit for bit; earlier bars are NaN instead of its warm-up. A window containing NaN gives NaN; infinities are ordered normally. The builtin min rules apply: min(-0, +0) is -0, and a window of only -0 gives -0. Each window is re-read, so this is not a ratchet: growing the period brings older observations back. Work is O(q) per valid bar, with O(1) working space besides the result.

For a Castleman channel ending at the previous bar, use low.Shift(1).LowestVariable(p, minPeriod, maxPeriod), retaining bar t’s period. Shifting LowestVariable’s result instead uses bar t-1’s period. Shift(1) pads bar 0 with 0, which any window reaching bar 0 then reads. The adaptive channel breakouts under “Donchian Channels” in the manual use both forms, Castleman’s the first and Ruggiero’s the second. For the minimum since each event, see LowestSince; for a fixed window, Lowest(n) or Quantile(n, 0).

Sources: Arrington, “Building A Variable-Length Moving Average”, Stocks & Commodities (June 1991); TA-Lib MAVP.

ShiftVariable

func (s Series) ShiftVariable(n Series) Series

ShiftVariable returns at each bar t the receiver’s value n[t] bars earlier, s[t-n[t]], with the lag n[t] read from the series n at that bar.

A lag is valid when it is a whole number from 0 to t: a lag that is NaN, infinite, negative, fractional or greater than t gives NaN at that bar. A lag of 0 gives the bar’s own value, and -0 is the lag 0. Lags are compared as float64 before any conversion, so 2^53 or 1e300 is simply too large, as is any lag beyond t. Unlike AverageVariable, which clamps and truncates its periods, ShiftVariable does not clamp and does not truncate: a lag of 0.5 reads NaN rather than the bar’s own value. The value s[t-n[t]] is copied verbatim, so a NaN, a +Inf, a -Inf or a -0 in the receiver reaches exactly the bars whose lag lands on it. The NaN written for an invalid lag is a per-bar footprint of n: it is not a warm-up and not the zero padding of Shift.

n is right-aligned with the receiver at the most recent bar, as AverageVariable aligns its period series and VWAPSince its volume: when n is shorter than the receiver, the bars before its start have no lag and read NaN, and when n is longer only its last len(s) values are used. The lag counts receiver bars back from t whatever the alignment, so a lag greater than t is invalid under a shorter n as well. An empty receiver gives an empty result and an empty n gives len(s) NaNs. Series are paired by position only; see “Time alignment” in the manual.

There is no warm-up: the first valid index is the first bar whose lag is valid, which depends on the data. With a constant whole lag k >= 0 in a series c of length len(s), s.ShiftVariable(c) equals Shift(k) bit for bit from bar k on and is NaN before bar k, where Shift pads with +0. For any event series e and any k >= 0, s.ShiftVariable(e.BarsSince(k)) equals s.ValueWhen(e, k) on every bar, NaN included.

With age = s.DirectionalChangeAge(theta), s.ShiftVariable(age) reads at each confirmation bar t of the directional-change state the value s[t-age[t]] of the extreme the event confirms, the highest value since the previous event bar for a -1 event and the lowest for a +1 event, so ValueWhen over the -1 and +1 events of s.DirectionalChangeEvent(theta) gives the last confirmed swing high and swing low. Before the first confirmation the age is 0 and the lag reads the bar itself. The threshold-swing recipes are in the manual’s “Swing State” section. The setup risk level of DeMark’s TD Setup, a recipe over ShiftVariable and IndexOfLowestS, is in the manual’s “DeMark Indicators” section. DeMarkSetupRiskLevel computes that level as a method, NaN when a true low or true high of the nine bars is NaN.

ShiftVariable is O(1) a bar, runs in one pass and allocates only the result.

StdDevVariable

func (s Series) StdDevVariable(p Series, minPeriod, maxPeriod int) Series

StdDevVariable computes the population standard deviation with a period from p at each bar, as in StdDev and Arrington’s printed figures; the Chan ABZ’ port uses this form too, though the published spreadsheets use Excel’s sample STDEV.

p is right-aligned with s. Periods are clamped in float64 to [minPeriod, maxPeriod], then truncated; -Inf selects minPeriod and +Inf selects maxPeriod. Bars before a shorter p’s overlap, NaN periods and periods exceeding the history so far give NaN, so the start is data-dependent, not a zero warm-up; a longer p’s surplus oldest values are unused. Invalid bounds (minPeriod < 1 or minPeriod > maxPeriod) return len(s) aligned zeros. maxPeriod may exceed len(s). Series are paired by position only; see “Time alignment” in the manual.

For a constant period n within the bounds, bars from n-1 agree with StdDev(n) to about 1e-12 relative on ordinary prices, and to about 1e-16 times sqrt(n) times |mean|/sd relative in general; earlier bars are NaN instead of its zero warm-up. The kernels differ: StdDev’s running moments lose digits when the mean dwarfs the spread, overflow once the standard deviation passes about 1.3e154/sqrt(n), and lose precision once the deviations from the window mean fall below about 1e-154 in magnitude, where this method’s scaled kernel does not. A window containing NaN or either infinity gives NaN. Finite q = 1 and flat windows give +0; all zero results are +0. Each finite window uses the scaled moments kernel. Work is O(q) per valid bar, with O(min(maxPeriod, len(s))) scratch allocated once besides the result.

Jacinta Chan’s adjustable-bands z-test, ABZ’, scores each close against a moving average and a standard deviation that share one period, set by her efficacy ratio: the 34-bar standard deviation of the closes over their 6-bar standard deviation. For closes c, bounds lo and hi, the threshold k = 0.8 and a function g of the caller’s own that turns the ratio into a period series, the recipe is

sd6 := c.StdDev(6)
er := c.StdDev(34).Div(sd6)
p := g(er).Add(sd6.EQ(0).Mul(float64(hi))).SetN(33, math.NaN())
z := c.Sub(c.AverageVariable(p, lo, hi)).Div(c.StdDevVariable(p, lo, hi))
state := z.GT(k).Sub(z.LT(-k))

with state 1 for long, -1 for short and 0 for flat. The ratio is taken on the closing prices themselves, as the 2011 article says, not on returns, and with the population standard deviation; Excel’s sample STDEV, which her spreadsheets use, would multiply it by sqrt((345)/(336)) = 0.926599. The sources fix neither the direction, the scale nor the rounding of g: the 2011 article says only that the ratio is the number of periods, which taken literally gives windows of a few bars, and the formula printed in the 2023 spreadsheet is not a ratio of the two deviations. On closing prices the ratio is sqrt(33), about 5.74, on a straight line and has a median of about 1.13 on Gaussian white noise, so it is larger in a trend, while Chan’s prose has a ranging market give the larger ratio; a g that gives a ranging market the longer window, as the prose intends, falls as the ratio rises.

Where the last six closes are equal, sd6 is 0 and Div’s zero rule makes the ratio 0, so a g that is 0 at 0 would select lo, the shortest window, in the quietest market, against Chan’s intent; adding sd6.EQ(0).Mul(float64(hi)) lifts those bars to at least hi, which the clamp reads as hi wherever g is neither negative nor NaN. SetN masks bars 0 to 32, before the 34-bar deviation exists, so the variable windows give NaN there and state reads 0, not NaN, on those bars, as comparisons with NaN give 0. Chan publishes no bounds; lo must be at least 2, as a one-bar window has a standard deviation of 0 and z then reads 0, and 6 and 34, the ratio’s own windows, are only an illustration. The threshold 0.8 comes from her prose and trading rules; the printed formula adds one plain standard deviation.

BBZ, the fixed-window test that ABZ’ extends, is z := c.Standardise(21) with k = 1, over 21 bars in the 2011 article and 20 in the 2006 one, and the same state line. With a constant period n in [lo, hi] and no mask, the moving average above is Average(n) bit for bit from bar n-1 (NaN before it, where Average passes the input through), and z agrees with c.Standardise(n) from bar n-1 to rounding, within about 1e-13 of each other on prices that move by 1% a bar and 1e-12 at 0.1%, as absolute differences in z (the relative difference is unbounded where z passes through 0), as the rounding of both grows when the moves shrink against the price. Chan’s entry on a close above the 21-bar, one-deviation Bollinger band is c.GT(c.Bollinger(21, 1)), which agrees with z.GT(1) except within rounding of the threshold. Her spreadsheets use Excel’s sample STDEV, so their threshold of 1 is a population threshold of sqrt(n/(n-1)): 1.024695 over 21 bars and 1.025978 over 20. The thresholds are strict, so a bar with z exactly at k or -k is flat. Chan’s rules neither enter nor exit a long position at k, nor a short one at -k, and the latch c.PositionSigned(z.GT(k), z.LT(k), z.LT(-k), z.GT(-k)) holds such a position where state goes flat; it differs from state only where z is exactly k or -k, or NaN.

Sources: Arrington, “Building A Variable-Length Moving Average”, Stocks & Commodities (June 1991); TA-Lib MAVP; Chan, “From BBZ To ABZ’: The Adjustable-Bands Z-Test”, Stocks & Commodities V29:13 (December 2011), “Trading Trends With The Bollinger Bands Z-Test”, Stocks & Commodities V24:3 (March 2006), and “The Time-Tested Adjustable Moving Average”, Stocks & Commodities V41:12 (November 2023).

SumVariable

func (s Series) SumVariable(p Series, minPeriod, maxPeriod int) Series

SumVariable computes Sum’s window total with a period from p at each bar.

p is right-aligned with s. Periods are clamped in float64 to [minPeriod, maxPeriod], then truncated; -Inf selects minPeriod and +Inf selects maxPeriod. Bars before a shorter p’s overlap, NaN periods and periods exceeding the history so far give NaN, so the start is data-dependent, not a zero warm-up; a longer p’s surplus oldest values are unused. Invalid bounds (minPeriod < 1 or minPeriod > maxPeriod) return len(s) aligned zeros. maxPeriod may exceed len(s). Series are paired by position only; see “Time alignment” in the manual.

For a constant period n within the bounds, bars from n-1 equal Sum(n) bit for bit; earlier bars are NaN where Sum gives expanding sums. A window containing NaN gives NaN; infinities of one sign give that infinity, and both signs give NaN; a finite window whose sum overflows gives that infinity. Zero results are +0. Work is amortised O(1) per bar for a steady period, with O(q) rebuilds when the window grows. During a run of invalid bars the queue only grows, so working space is O(len(s)) besides the result.

Sources: Arrington, “Building A Variable-Length Moving Average”, Stocks & Commodities (June 1991); TA-Lib MAVP.

VWMAVariable

func (s Series) VWMAVariable(v, n Series) Series

VWMAVariable returns at each bar t the volume-weighted average price of the receiver with the volume v over the last n[t] bars, with the window length n[t] read from the series n at that bar.

The window at bar t is the receiver bars t-k+1 .. t, with k = n[t], and the result is the sum of the products s*v over the window divided by the sum of v over the window. Both sums are formed in bar order with VWAPSince’s arithmetic: each product is rounded on its own before it is added, and each sum is compensated, so the result at every bar is the same, bit for bit, as summing the window afresh at that bar. A length is valid when it is a whole number from 1 to t+1: a length that is NaN, infinite, 0, -0, negative, fractional or greater than t+1 gives NaN at that bar. Lengths are compared as float64 before any conversion, so 2^53 or 1e300 is simply too large, as is any length beyond t+1. Unlike AverageVariable, which clamps and truncates its periods, VWMAVariable does not clamp and does not truncate.

The result is NaN where the window’s volume sums to exactly 0, of either sign, where a price or volume in the window is NaN or infinite, and where a product or sum overflows. A NaN comes from the window alone: a later window that does not hold the bad bar recovers, as a window of VWMA does and unlike a segment of VWAPSince, which stays NaN until its next anchor. A zero quotient is written as +0. A quotient that overflows is returned as +Inf or -Inf. Volumes are not validated: negative volumes are summed as given. With n[t] = 1 the result is (s*v)/v, the price to rounding, and NaN where the volume is 0, whereas VWMA(v, 1) copies the price whatever the volume.

n and v are right-aligned with the receiver at the most recent bar, as AverageVariable aligns its period series and VWAPSince its volume and anchor: when n is shorter than the receiver, the bars before its start have no length and read NaN; when v is shorter, the bars before its start add nothing to either sum and their prices are not read; a longer n or v contributes only its last len(s) values. The window counts receiver bars whatever the alignment, so a length greater than t+1 is invalid under a shorter n as well. An empty receiver gives an empty result; an empty v gives len(s) NaNs, every volume sum being +0, and so does an empty n. Series are paired by position only; see “Time alignment” in the manual.

There is no warm-up: the first valid index is the first bar whose length is valid and whose window is not NaN under the rules above, which depends on the data. For any anchor series of any length, s.VWMAVariable(v, anchor.BarsSince().Add(1)) equals s.VWAPSince(v, anchor) on every bar, NaN included: each window runs from the most recent anchor bar and its bars are added in the same order. With a launch series of the receiver’s length, s.VWMAVariable(v, launch.BarsSince()) equals s.VWAPSince(v, launch.Shift(1)), the MIDAS curve that leaves the launch bar out, on every bar except the launch bars, where the length is 0 and the result is NaN while VWAPSince shows the previous segment’s value or NaN. With a constant length n >= 2 and a v of the receiver’s length, the result equals VWMA(v, n) bit for bit at bar n-1 and to rounding after it, where VWMA evicts through its queue and associates its sums differently; before bar n-1 the result is NaN where VWMA reads 0. On quarter prices of magnitude below 2^17 and whole volumes below 2^16, in windows of at most 70 bars, every product and partial sum is exact and the result is the correctly rounded quotient of the exact sums.

Unlike the anchor of VWAPSince, which starts a segment at the anchor bar as it is seen, a length series can anchor a window on a bar already passed. With age = c.DirectionalChangeAge(theta) over the closes c and mp = h.Add(l).Div(2) the mid price, mp.VWMAVariable(v, age) is the zigzag-anchored VWAP in the MIDAS form of Levine, Reyna and Coles: its window runs from the bar after the extreme the age counts from, leaving the extreme bar out as they leave out the launch bar; it is NaN before the first confirmation, where the age is 0, and jumps at each confirmation bar to the window that starts after the extreme the event confirms. The inclusive form, with the extreme bar in the window, is mp.VWMAVariable(v, age.Add(1).Mul(age.GT(0))), where the mask age.GT(0) keeps the bars before the first confirmation NaN. The threshold-swing recipes are in the manual’s “Swing State” section.

The two sums are kept across bars. When the window of bar t starts on the same bar as the window of bar t-1, which is n[t] = n[t-1] + 1 with both lengths valid, bar t is added to the sums; on any other valid bar the sums are emptied in place, keeping their storage, and the window is summed afresh in bar order. The cost is amortised O(1) a bar while the window grows by one bar and O(n[t]) at a re-sum, so O(len(s)) in all over the ages of a directional-change state, whose re-sums barely overlap, and over an expanding window, and O(n) a bar for a constant length n.

WAverageVariable

func (s Series) WAverageVariable(p Series, minPeriod, maxPeriod int) Series

WAverageVariable computes WAverage’s linearly weighted mean with a period from p at each bar. Weights descend from q for the newest value to 1 for the oldest; products are rounded separately and summed newest first.

p is right-aligned with s. Periods are clamped in float64 to [minPeriod, maxPeriod], then truncated; -Inf selects minPeriod and +Inf selects maxPeriod. Bars before a shorter p’s overlap, NaN periods and periods exceeding the history so far give NaN, so the start is data-dependent, not a zero warm-up; a longer p’s surplus oldest values are unused. Invalid bounds (minPeriod < 1 or minPeriod > maxPeriod) return len(s) aligned zeros. maxPeriod may exceed len(s). Series are paired by position only; see “Time alignment” in the manual.

For a constant period n within the bounds, bars from n-1 equal WAverage(n) bit for bit with unfused multiply-adds and agree to rounding where the fixed method fuses them. Earlier bars are NaN instead of its warm-up. NaN propagates through the window; infinities follow the weighted sum. Zero results are +0. Work is O(q) per valid bar, with O(1) working space besides the result.

Sources: Arrington, “Building A Variable-Length Moving Average”, Stocks & Commodities (June 1991); TA-Lib MAVP.