How BanTA Handles Sub-Series and Column Series: A Technical Deep Dive

BanTA handles sub-series and column series through two distinct mechanisms in its Series type: sub-series provide lazy, parameter-specific caching via the Subs map (accessed through the To method), while column series store multi-value indicator outputs in the Cols slice (created during slice-based Append operations).

The banbox/banta repository implements a high-performance technical analysis library in Go where efficient data stream management is critical. Understanding how BanTA handles sub-series and column series reveals the architecture behind its indicator caching system and multi-value return patterns.

The Series Type Foundation

At the core of BanTA’s data model is the Series struct defined in [main/types.go](https://github.com/banbox/banta/blob/main/main/types.go) (lines 48‑55). This struct manages time-ordered financial data through three key fields:

  • Data []float64 — Stores the primary time-ordered values (e.g., closing prices).
  • Cols []*Series — Contains column series, which hold additional values when an indicator returns multiple outputs (e.g., MACD line plus signal line).
  • Subs map[string]map[int]*Series — Stores sub-series, which are cached child series created for specific indicators and parameter combinations (e.g., RSI with period 14).

A fourth field, XLogs map[int]*CrossLog, stores crossing information but operates independently of the sub-series and column series mechanisms.

Sub-Series (Subs): Parameter-Specific Caching

Sub-series implement a lazy-loading cache system that prevents redundant indicator calculations across multiple bars. Each sub-series is uniquely identified by a string key (typically the indicator name) and an integer hash derived from parameters.

Creation via the To Method

The (*Series).To(k string, v int) method in [main/core.go](https://github.com/banbox/banta/blob/main/main/core.go) (lines 78‑90) handles sub-series creation and retrieval:

func (s *Series) To(k string, v int) *Series {
    if s.Subs[k] == nil {
        s.Subs[k] = make(map[int]*Series)
    }
    if s.Subs[k][v] == nil {
        s.Subs[k][v] = s.Env.NewSeries(nil)
    }
    return s.Subs[k][v]
}

The method checks s.Subs[k][v]; if the entry does not exist, it initializes a new empty Series via s.Env.NewSeries(nil) and stores it in the nested map before returning.

Usage Pattern in Indicators

Indicator functions call To with a unique key and parameter hash to retrieve dedicated cache slots. In [main/sta_inds.go](https://github.com/banbox/banta/blob/main/main/sta_inds.go) (lines 95‑100), the rsiBy function demonstrates this pattern:

func rsiBy(obj *Series, period int, subVal float64) *Series {
    res := obj.To("_rsi", period*100+int(subVal))
    // ... calculation logic only runs if !res.Cached()
    return res
}

The hash calculation period*100+int(subVal) ensures that RSI(14) and RSI(20) occupy distinct cache entries. On subsequent bars, calling RSI(close, 14) retrieves the cached sub-series instead of recomputing values.

Propagation on Clone

When environments clone series objects, sub-series must persist. In [main/core.go](https://github.com/banbox/banta/blob/main/main/core.go) (lines 503‑508), the Clone method deep-copies the Subs map using maps.Clone(s.Subs), ensuring that cached indicators remain attached to the new environment without shared mutable state between clones.

Column Series (Cols): Handling Multi-Value Returns

While sub-series cache different indicators, column series handle functions that return multiple values simultaneously (e.g., MACD’s main line and signal line).

Creation in the Append Method

The (*Series).Append method in [main/core.go](https://github.com/banbox/banta/blob/main/main/core.go) (lines 41‑55) detects when the input is a slice ([]float64) and automatically distributes values across column series:

} else if arr, ok := obj.([]float64); ok {
    s.Data = append(s.Data, arr[0])  // Primary value
    for i, v := range arr[1:] {      // Additional values → columns
        if i >= len(s.Cols) {
            col := s.To("_", i)       // Lazily create column slot
            s.Cols = append(s.Cols, col)
        }
        s.Cols[i].Append(v)
    }
}

The first element populates the main Data slice; each subsequent element appends to a corresponding entry in Cols.

Typical Usage in Multi-Output Indicators

The MACD implementation in [main/sta_inds.go](https://github.com/banbox/banta/blob/main/main/sta_inds.go) (lines 81‑93) demonstrates the column series pattern:

func MACDBy(obj *Series, fast, slow, smooth, initType int) (*Series, *Series) {
    res := obj.To("_macd", fast*1000+slow*100+smooth*10+initType)
    if !res.Cached() {
        short := EMABy(obj, fast, initType)
        longMA := EMABy(obj, slow, initType)
        macd := short.Sub(longMA)
        signal := EMABy(macd, smooth, initType)
        res.Append([]float64{macd.Get(0), signal.Get(0)})
    }
    return res, res.Cols[0]  // Signal line is res.Cols[0]
}

Here, res serves as both a sub-series (cached via To) and a container for column series. The primary MACD line resides in res.Data, while the signal line becomes res.Cols[0].

Accessing Column Series

After calling multi-value indicators, access the primary series through the return value’s Data field, and secondary values through the Cols slice or the additional return pointers:

macd, signal := banta.MACD(env.Close, 12, 26, 9)
// macd.Data[0] contains the MACD line
// signal.Data[0] or macd.Cols[0].Data[0] contains the signal line

Interaction Between Sub-Series and Column Series

Sub-series and column series operate orthogonally but can coexist:

  • Sub-series are identified by string key and integer hash, stored in the parent’s Subs map, and represent independent indicator caches. A sub-series itself can contain column series if the cached indicator returns multiple values.
  • Column series are indexed by position within a single parent series (Cols slice), created only when Append receives multiple values.

Both mechanisms share the underlying Series type, allowing higher-level code to treat cached indicators and multi-value outputs uniformly.

Practical Code Examples

Computing a Cached RSI (Sub-Series)

env, _ := banta.NewBarEnv("binance", "spot", "BTCUSDT", "1h")
close := env.Close

// First call creates the sub-series for period 14 via close.To("_rsi", 1400)
rsi14 := banta.RSI(close, 14)

// Subsequent bars reuse the cached sub-series automatically
rsi14_2 := banta.RSI(close, 14) // No recomputation; retrieved from Subs["_rsi"][1400]

MACD with Signal Line (Column Series)

macd, signal := banta.MACD(env.Close, 12, 26, 9)

// macd → main series (res.Data)
// signal → column series (res.Cols[0])
fmt.Println("MACD:", macd.Get(0))
fmt.Println("Signal:", signal.Get(0))

Accessing a Sub-Series Directly

// Inspect the cached RSI sub-series directly using the same hash algorithm
rsiSub := env.Close.To("_rsi", 14*100+0) // Matches RSI(..., 14) internal call
fmt.Println("Latest RSI:", rsiSub.Get(0))

Working with Multi-Value Custom Indicators

// Bollinger Bands returns three values
upper, middle, lower := banta.BBands(env.Close, 20, 2.0, 2.0)

// middle is the primary series (res.Data)
// upper and lower are column series at res.Cols[0] and res.Cols[1]
fmt.Println("Bands:", upper.Get(0), middle.Get(0), lower.Get(0))

Summary

  • Sub-series (Subs) provide lazy, parameter-specific caching for indicators via the To method, using a map[string]map[int]*Series structure to avoid redundant calculations across bars.
  • Column series (Cols) store additional outputs from multi-value indicators, automatically created when Append receives a slice with more than one element.
  • Both mechanisms rely on the same Series type defined in types.go, with core logic implemented in core.go and usage patterns demonstrated in sta_inds.go.
  • When cloning series objects, the Subs map is deep-copied to preserve cached indicator state in new environments.

Frequently Asked Questions

What is the difference between sub-series and column series in BanTA?

Sub-series are cached indicator instances stored in the Subs map, keyed by indicator name and parameter hash (e.g., RSI with period 14). They persist across calculations to avoid recomputation. Column series are additional value streams stored in the Cols slice, created when a single indicator call returns multiple values (e.g., MACD line and signal line). While sub-series cache different indicators, column series store multiple outputs from the same indicator call.

How does BanTA prevent recomputing indicators on every new bar?

The library implements lazy caching through the (*Series).To method in core.go. When an indicator function calls To with a specific key and hash, it either retrieves an existing sub-series from Subs or creates a new one. The indicator logic only executes if !res.Cached(), ensuring that previously calculated values for specific parameters are reused rather than recomputed.

Can a sub-series have its own column series?

Yes. Since both sub-series and column series use the same Series type, a sub-series cached in Subs can itself contain a Cols slice. For example, if a cached custom indicator returns multiple values via Append([]float64{val1, val2}), the primary value resides in the sub-series’ Data field, while the secondary value populates Cols[0] of that same sub-series.

How do I access the signal line from a MACD calculation?

The MACD function returns two *Series pointers. The first is the primary series containing the MACD line (res.Data), and the second is the signal line, which is actually res.Cols[0]. You can access it either through the second return value (signal.Get(0)) or directly via the first return value’s column slice (macd.Cols[0].Get(0)), as implemented in sta_inds.go lines 81‑93.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →