Data Series and Multi-Timeframe Strategies

Data Series and Multi-Timeframe Strategies

Algolang strategies can operate on one or more data series simultaneously. This enables multi-timeframe analysis, inter-market strategies, and spread/pairs trading.

The primary series

The primary data series is always index 0. All trading functions (Buy, SellShort, etc.) operate on the primary series. The Strategy() function is called once per bar of the primary series.

Specifying multiple series on the command line

Use the slash separator to specify multiple series:

# Two symbols in one run (e.g., for spread or pairs strategies)
algo run --strategy pairs --symbols @ES/@NQ

In the strategy, access each series by index:

esClose := rs.Close(0)  // Primary series (@ES)
nqClose := rs.Close(1)  // Second series (@NQ)

Dynamic series loading

Strategies can load additional data series programmatically in the Begin() function using LoadSeries():

func (s *Strategy) Begin(rs *engine.RunState) error {
    // Load weekly data of the primary symbol
    weeklyIdx, err := rs.LoadSeries(engine.SeriesOptions{
        BarInterval: "weekly",
    })
    if err != nil {
        return err
    }
    s.weeklyIndex = weeklyIdx  // Store for use in Strategy()
    return nil
}

SeriesOptions

FieldTypeDefaultDescription
SymbolstringPrimary symbolSymbol to load (with prefix)
BarIntervalstring"daily"Bar interval
BarBuildingModestring"natural""natural" or "session"
SessionHoursstringSymbol defaultTrading session hours
SessionDaysstringSymbol defaultTrading session days
TimezonestringExchange TZIANA timezone
DetrendboolfalseApply logarithmic detrending
DetrendModestring"simple""simple" or "regression"
SeriesIndexintAuto-assignExplicit series index; values < 1 mean auto-assign next available

If Symbol is omitted, the primary symbol is used. This makes it easy to load additional timeframes of the same instrument.

If SeriesIndex is not specified (or is less than 1), the series is automatically assigned the next available index. The returned index should be stored for later use when accessing the series data.

Key constraints

  • LoadSeries() can only be called from Begin(). Calling it from Strategy() or End() returns an error.
  • Dynamically loaded series are automatically synchronised with the primary series.

Data series synchronisation

The engine iterates over bars in the primary series. For each primary bar, additional series are synchronised so that the strategy sees the most recent bar of each series that is not later than the current primary bar.

This means a strategy running on 60-minute bars with a daily secondary series will see the daily bar update once per day, while the primary series updates every hour.

Example: daily + weekly strategy

package lookaway

import "github.com/kmglennan/algolang/pkg/engine"

type Strategy struct {
    Len          int `algolang:"default=10,min=5,max=50,by=1"`
    weeklyIndex  int
}

func (s *Strategy) Begin(rs *engine.RunState) error {
    idx, err := rs.LoadSeries(engine.SeriesOptions{
        BarInterval: "weekly",
    })
    if err != nil {
        return err
    }
    s.weeklyIndex = idx
    return nil
}

func (s *Strategy) Strategy(rs *engine.RunState) error {
    weeklyClose := rs.Close(s.weeklyIndex)

    if weeklyClose.Value() >= weeklyClose.Lowest(s.Len).LookBack(1).Value() {
        rs.Buy()
    } else {
        rs.SellShort()
    }
    return nil
}