Lookback and series
OnBar gives you the current bar, but a crossover needs history. Declare how much by implementing the optional Lookbacker interface:
func (s *SmaCross) Lookback() int { return s.Slow + 1 }The SDK then keeps the last Slow+1 bars per series in a ring buffer you reach through ctx.Series(id):
series := ctx.Series(bar.Symbol) // see below for the SeriesID nuance
if series.Len() < s.Slow+1 {
return nil, nil // not enough history yet
}The Series view offers indexed access where 0 is the current bar, 1 the previous: series.Close(0), series.High(3), series.Bar(i) – plus bulk column views Opens() Highs() Lows() Closes() Volumes() (oldest first). The bulk slices are read-only windows into the ring buffer, valid only until the next OnBar; copy them if you keep them. series.Fresh() tells you whether this series’ newest bar closed at the current decision point – essential in multi-timeframe runs where a 1m bar arrives while the 5m series is mid-bar.
Without Lookbacker you get a one-bar buffer: just the current bar.
Warm-up. Before your first bar the engine fills the buffer with up to Lookback() bars from before the run’s start (warm-up bars do not call OnBar), so on a long enough archive series.Len() is already full on the first decision. A continuous series (fut:XCME:ES) warms up the same way (GLE-393): its warm-up bars come from the same served composite as its live bars, adjusted in their frame, and each carries in Bar.ActiveContract the dated contract it was served from. A warm-up may cross a roll; that roll calls no OnRoll and places no roll legs, because the position is still flat. When the archive holds fewer bars than the lookback, the series starts with what it has and the run records a warmup_shortfall degradation (see Continuous run conventions).
One nuance for multi-series strategies: bars carry a SeriesID, empty for single-series runs. Mirror the SDK’s own fallback when you look a series up:
id := bar.SeriesID
if id == "" {
id = bar.Symbol
}
series := ctx.Series(id)