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

> Discover how BanTA handles sub-series and column series. Learn about lazy caching with Subs and multi-value storage in Cols for efficient data management.

- Repository: [banbox/banta](https://github.com/banbox/banta)
- Tags: deep-dive
- Published: 2026-02-26

---

**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)](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)](https://github.com/banbox/banta/blob/main/main/core.go) (lines 78‑90) handles sub-series creation and retrieval:

```go
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)](https://github.com/banbox/banta/blob/main/main/sta_inds.go) (lines 95‑100), the `rsiBy` function demonstrates this pattern:

```go
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)](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)](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:

```go
} 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)](https://github.com/banbox/banta/blob/main/main/sta_inds.go) (lines 81‑93) demonstrates the column series pattern:

```go
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:

```go
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)

```go
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)

```go
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

```go
// 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

```go
// 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`](https://github.com/banbox/banta/blob/main/types.go), with core logic implemented in [`core.go`](https://github.com/banbox/banta/blob/main/core.go) and usage patterns demonstrated in [`sta_inds.go`](https://github.com/banbox/banta/blob/main/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`](https://github.com/banbox/banta/blob/main/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`](https://github.com/banbox/banta/blob/main/sta_inds.go) lines 81‑93.