How to Detect Crossovers of Indicators with BanTA: Vector and Stateful Methods

BanTA provides two crossover detection modes—tav.Cross for bulk vector calculations on complete historical series and Series.Cross for incremental stateful tracking in live trading environments, both returning signed integers where the absolute value minus one indicates bars since the last cross.

The banbox/banta repository offers a robust technical analysis framework for Go and Python that includes reliable tools to detect crossovers of indicators with BanTA. Whether you are backtesting strategies on complete datasets or processing real-time market data streams, BanTA delivers optimized crossover detection through two distinct APIs. Both implementations monitor the sign change between two data series to identify when an indicator crosses above or below a reference value.

Understanding BanTA Crossover Detection Modes

BanTA implements crossover detection through complementary vector and stateful approaches. The vector-style tav.Cross function processes entire slices offline, making it ideal for strategy backtesting. The stateful Series.Cross method maintains internal history through CrossLog structs, designed for incremental updates in live trading bots. Both methods share a consistent return convention: positive values indicate up-crosses (series moves from below to above), negative values indicate down-crosses, and the distance from the crossing event equals abs(return_value) - 1.

Vector-Style Crossover Detection with tav.Cross

For bulk analysis of complete time series, BanTA exposes tav.Cross in tav/indicators.go (lines 2290-2318). This function accepts two []float64 slices and returns a parallel []int slice encoding crossover events.

The implementation iterates through both series simultaneously, calculating the difference at each index. When the sign of the difference changes between consecutive valid bars, the function records a crossover direction and updates the distance counter. The algorithm handles NaN values gracefully by propagating the previous state without resetting the distance calculation.

// Cross 计算两个序列在每个时间点的交叉状态。
// 返回值:正数表示上穿,负数表示下穿,0表示无交叉或未知。
// 返回值的绝对值减1 (abs(ret) - 1) 代表了最近一次交叉点到当前元素的距离。
func Cross(data1 []float64, data2 []float64) []int {
    n := len(data1)
    res := make([]int, n)

    // 维护交叉状态的局部变量
    var curSign int           // 最近一次交叉的方向
    var lastIndex = -1        // 最近一次交叉点的索引
    var prevDiff = math.NaN() // 上一个有效点的差值

    for i, v1 := range data1 {
        currentDiff := v1 - data2[i]
        if math.IsNaN(currentDiff) || currentDiff == 0 {
            // 仍然返回上一次的状态(距离递增)
            res[i] = curSign * (i - lastIndex + 1)
            continue
        }

        if !math.IsNaN(prevDiff) {
            // 只要符号相反就记录一次交叉
            if prevDiff*currentDiff < 0 {
                curSign = 1
                if currentDiff < 0 {
                    curSign = -1
                }
                lastIndex = i
            }
        }
        prevDiff = currentDiff
        // 若尚未发生交叉,返回 0;否则返回距离
        if lastIndex >= 0 {
            res[i] = curSign * (i - lastIndex + 1)
        }
    }
    return res
}

Key implementation details:

  • NaN resilience: Missing values continue the previous state rather than interrupting the distance count.
  • Direction encoding: curSign stores +1 for up-crosses and -1 for down-crosses.
  • Distance calculation: The value abs(res[i]) - 1 yields the number of bars elapsed since the last crossover event.

Stateful Crossover Detection with Series.Cross

For live streaming applications where bars arrive incrementally, BanTA provides Series.Cross in core.go (lines 540-595). This method operates on *Series objects and maintains persistent state through the CrossLog struct defined in types.go (lines 58-66).

The stateful implementation caches comparison history in Series.XLogs, a map keyed by the comparison target. For series-to-series comparisons, the key uses the negative ID of the opposing series; for constant values, the key derives from a scaled integer representation. On each new bar, the method compares the current difference against log.PrevVal; when the product is negative (indicating a sign change), it appends an XState record to log.Hist.

// Cross 计算最近一次交叉的距离。比较对象必须是常数或Series对象
// 返回值:正数上穿,负数下穿,0表示未知或重合;abs(ret) - 1表示交叉点与当前bar的距离
func (s *Series) Cross(obj2 interface{}) int {
    var env = s.Env
    var key int
    var v2 float64

    // Resolve the right‑hand operand (Series, int, float32, float64)
    switch v := obj2.(type) {
    case *Series:
        key = -v.ID
        v2 = v.Get(0)
    case int:
        key = v
        v2 = float64(v)
    case float32:
        key = int(v * 100)
        v2 = float64(v)
    case float64:
        key = int(v * 100)
        v2 = v
    default:
        panic(ErrInvalidSeriesVal)
    }

    // Retrieve or create the per‑pair log
    log, newData := s.XLogs[key], false
    if log == nil {
        log = &CrossLog{env.TimeStart, math.NaN(), []*XState{}}
        s.XLogs[key] = log
        newData = true
    } else if env.TimeStart > log.Time {
        newData = true
        log.Time = env.TimeStart
    }

    // If we have a fresh bar, update the log
    if newData {
        diffVal := s.Get(0) - v2
        if diffVal != 0 && !math.IsNaN(diffVal) {
            if math.IsNaN(log.PrevVal) {
                log.PrevVal = diffVal
            } else if log.PrevVal*diffVal < 0 {
                // Sign change → crossing
                log.PrevVal = diffVal
                log.Hist = append(log.Hist, &XState{numSign(diffVal), env.BarNum})
            }
        }
    }

    // Return the most recent crossing distance, if any
    if len(log.Hist) > 0 {
        state := log.Hist[len(log.Hist)-1]
        return state.Sign * (env.BarNum - state.BarNum + 1)
    }
    return 0
}

Key implementation details:

  • CrossLog persistence: Each unique comparison pair stores its previous difference value and crossing history in XLogs.
  • Incremental updates: The newData flag triggers recalculation only when the environment advances to a new timestamp.
  • Flexible operands: Accepts *Series, int, float32, or float64 as the comparison target.

Python Bindings for Crossover Detection

BanTA exposes the vector crossover function to Python through CGO bindings in python/tav/index.go (lines 332-338). The banta.tav.Cross function maintains identical semantics to its Go counterpart, accepting two float slices and returning a list of integers.

// Cross detects crossovers between two data series.
func Cross(data1 []float64, data2 []float64) []int {
    return banta_tav.Cross(data1, data2)
}

Python users can import banta.tav to perform vectorized crossover detection on NumPy arrays or standard lists without manual loop implementation.

Practical Code Examples

Go Vector Example (Offline Backtesting)

Use tav.Cross when analyzing complete historical series to generate entry signals for backtests.

package main

import (
	"fmt"

	"github.com/banbox/banta/main/tav"
)

func main() {
	close := []float64{101, 103, 102, 105, 107, 106}
	sma10 := []float64{100, 100, 101, 102, 103, 104}

	// Cross returns a slice; positive = up‑cross, negative = down‑cross
	cross := tav.Cross(close, sma10)
	fmt.Println("Cross slice:", cross)
	// Example output: [0 1 0 1 1 1] (meaning an up‑cross at index 1, etc.)
}

Go Stateful Example (Live Streaming)

Use Series.Cross within a BarEnv to detect real-time crosses against price levels or other indicators.

package main

import (
	"fmt"

	"github.com/banbox/banta/main"
)

func main() {
	env, _ := banta.NewBarEnv("binance", "spot", "", "1h")

	// Simulate receiving bars
	for i := 0; i < 5; i++ {
		env.OnBar(0, 0, 0, 0, float64(30000+i*10), 0, 0, 0, 0) // close rises
		// Detect crossing against the constant 30000 level
		distance := env.Close.Cross(30000)
		fmt.Printf("Bar %d – distance to last cross: %d\n", i, distance)
	}
	// Output shows a positive distance once the price moves above 30000.
}

Python Vector Example

Process historical data in Python using the banta.tav module.

import banta.tav as tav

close = [101, 103, 102, 105, 107, 106]
sma =  [100, 100, 101, 102, 103, 104]

cross = tav.Cross(close, sma)
print("cross =", cross)   # e.g. [0, 1, 0, 1, 1, 1]

Python Stateful Example

Implement live crossover detection in Python by interacting with the BarEnv wrapper.

from banta import BarEnv

env = BarEnv("binance", "spot", "", "1h")

for i in range(5):
    env.on_bar(0, 0, 0, 0, 30000 + i*10, 0, 0, 0, 0)   # close rises

    distance = env.close.cross(30000)   # method provided by Series wrapper

    print(f"Bar {i} → distance = {distance}")

Summary

  • BanTA offers two methods to detect crossovers of indicators with BanTA: the vector-based tav.Cross for batch processing and the stateful Series.Cross for incremental updates.
  • Vector detection (tav/indicators.go) returns a slice where each element encodes direction and distance since the last cross, ideal for backtesting complete datasets.
  • Stateful detection (core.go) maintains CrossLog entries in Series.XLogs to track crossing history across individual bars, optimized for live trading environments.
  • Both implementations use consistent sign conventions: positive values indicate up-crosses, negative values indicate down-crosses, and abs(value) - 1 calculates the bar distance.
  • Python bindings in python/tav/index.go provide direct access to vector crossover functionality without leaving the Python runtime.

Frequently Asked Questions

What is the difference between tav.Cross and Series.Cross?

tav.Cross performs bulk calculations on complete float slices, returning a full history of crossover events suitable for offline analysis. Series.Cross operates incrementally on streaming data, maintaining internal state through CrossLog structs to report only the most recent crossing distance, making it suitable for live trading bots.

How does BanTA handle missing data (NaN values) in crossover detection?

Both implementations gracefully handle NaN values by preserving the previous valid state. In tav.Cross, missing values propagate the existing curSign and continue the distance counter without resetting the crossing history. In Series.Cross, NaN differences are skipped during the newData update phase, preventing false crossover signals.

Can Series.Cross compare an indicator against a constant price level?

Yes. The Series.Cross method accepts constants (int, float32, float64) as the comparison operand. It generates a unique map key by scaling float values by 100 or using integer values directly, storing the crossing history separately from series-to-series comparisons.

How do I interpret the return values from BanTA crossover functions?

A return value of 0 indicates no crossover has occurred yet. Positive values signify an up-cross (the first series moved from below to above the second), while negative values indicate a down-cross. The magnitude follows the formula abs(return_value) - 1, which yields the number of bars elapsed since the crossover event occurred.

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 →