Bulls v Bears

Parameters: period = 14 | ma_type = ema | calculation_method = normalized | normalized_bars_back = 120 | raw_rolling_period = 50 | raw_threshold_percentile = 95 | threshold_level = 80

Overview

Bulls v Bears measures how far each bar extends above and below a moving-average anchor, then combines those opposing pressures into a regime line. The raw bull component is the distance from the bar high to the anchor, while the raw bear component is the distance from the anchor to the bar low. That gives the indicator a more structured read than a single oscillator because the internal bull and bear legs remain available alongside the composite value.

VectorTA supports two interpretation modes. The normalized mode rescales recent bull and bear ranges into a bounded spread and uses symmetric trigger bands. The raw mode keeps the spread in native units and derives rolling trigger levels from the recent range. In both cases the output also includes threshold crossings and zero-line cross markers, which makes the indicator useful for both regime analysis and event-style signal logic.

Defaults: Bulls v Bears uses `period = 14`, `ma_type = "ema"`, `calculation_method = "normalized"`, `normalized_bars_back = 120`, `raw_rolling_period = 50`, `raw_threshold_percentile = 95.0`, and `threshold_level = 80.0`.

Implementation Examples

Compute the full bull-vs-bear regime surface from slices or from candle data.

use vector_ta::indicators::bulls_v_bears::{
    bulls_v_bears,
    BullsVBearsInput,
    BullsVBearsParams,
};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

let high = vec![101.0, 102.2, 103.0, 102.8, 104.1, 104.6];
let low = vec![99.4, 100.8, 101.2, 101.0, 102.5, 103.0];
let close = vec![100.5, 101.6, 102.4, 101.9, 103.6, 104.0];

let output = bulls_v_bears(&BullsVBearsInput::from_slices(
    &high,
    &low,
    &close,
    BullsVBearsParams::default(),
))?;

let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let candle_output = bulls_v_bears(&BullsVBearsInput::with_default_candles(&candles))?;

println!("value = {:?}", output.value.last());
println!("bullish signal = {:?}", output.bullish_signal.last());
println!("zero cross up = {:?}", candle_output.zero_cross_up.last());

API Reference

Input Methods
// From candles
BullsVBearsInput::from_candles(&Candles, BullsVBearsParams)
    -> BullsVBearsInput

// From high, low, close slices
BullsVBearsInput::from_slices(&[f64], &[f64], &[f64], BullsVBearsParams)
    -> BullsVBearsInput

// From candles with default parameters
BullsVBearsInput::with_default_candles(&Candles)
    -> BullsVBearsInput
Parameters Structure
pub struct BullsVBearsParams {
    pub period: Option<usize>,                       // default 14
    pub ma_type: Option<BullsVBearsMaType>,         // default Ema
    pub calculation_method: Option<BullsVBearsCalculationMethod>, // default Normalized
    pub normalized_bars_back: Option<usize>,        // default 120
    pub raw_rolling_period: Option<usize>,          // default 50
    pub raw_threshold_percentile: Option<f64>,      // default 95.0
    pub threshold_level: Option<f64>,               // default 80.0
}
Output Structure
pub struct BullsVBearsOutput {
    pub value: Vec<f64>,
    pub bull: Vec<f64>,
    pub bear: Vec<f64>,
    pub ma: Vec<f64>,
    pub upper: Vec<f64>,
    pub lower: Vec<f64>,
    pub bullish_signal: Vec<f64>,
    pub bearish_signal: Vec<f64>,
    pub zero_cross_up: Vec<f64>,
    pub zero_cross_down: Vec<f64>,
}
Validation, Warmup & NaNs
  • The high, low, and close slices must all be non-empty and have identical lengths.
  • The indicator requires at least one bar where high, low, and close are all finite; otherwise it returns AllValuesNaN.
  • period, normalized_bars_back, and raw_rolling_period must all be greater than zero.
  • raw_threshold_percentile must be finite and stay within 80.0..=99.0.
  • threshold_level must be finite and stay within 0.0..=100.0.
  • Warmup depends on the moving-average type: EMA starts immediately, while SMA and WMA warm up for period - 1 bars.
  • Streaming returns NaN placeholders when a bar or the anchor state is invalid rather than throwing.
  • Batch range expansion rejects invalid integer or float ranges, and non-batch kernels are rejected at the batch entry point.
Builder, Streaming & Batch APIs
// Builder
BullsVBearsBuilder::new()
    .period(usize)
    .ma_type(BullsVBearsMaType)
    .calculation_method(BullsVBearsCalculationMethod)
    .normalized_bars_back(usize)
    .raw_rolling_period(usize)
    .raw_threshold_percentile(f64)
    .threshold_level(f64)
    .kernel(Kernel)
    .apply_slices(&[f64], &[f64], &[f64])

BullsVBearsBuilder::new()
    .apply(&Candles)

BullsVBearsBuilder::new()
    .into_stream()

// Stream
BullsVBearsStream::try_new(BullsVBearsParams)
BullsVBearsStream::update(high, low, close)
    -> (f64, f64, f64, f64, f64, f64, f64, f64, f64, f64)

// Batch
BullsVBearsBatchBuilder::new()
    .range(BullsVBearsBatchRange)
    .kernel(Kernel)
    .apply_slices(&[f64], &[f64], &[f64])
Error Handling
pub enum BullsVBearsError {
    EmptyInputData,
    AllValuesNaN,
    InconsistentSliceLengths { high_len: usize, low_len: usize, close_len: usize },
    InvalidPeriod { period: usize },
    InvalidNormalizedBarsBack { normalized_bars_back: usize },
    InvalidRawRollingPeriod { raw_rolling_period: usize },
    InvalidRawThresholdPercentile { raw_threshold_percentile: f64 },
    InvalidThresholdLevel { threshold_level: f64 },
    OutputLengthMismatch { expected: usize, got: usize },
    InvalidRange { start: String, end: String, step: String },
    InvalidKernelForBatch(Kernel),
}

Python Bindings

Python exposes a dictionary-returning single-run function, a streaming class, and a batch function. The single-run binding returns NumPy arrays for every output channel: value, bull, bear, ma, upper, lower, bullish_signal, bearish_signal, zero_cross_up, and zero_cross_down. Batch returns those same channels as two-dimensional matrices plus the tested parameter axes.

import numpy as np
from vector_ta import (
    bulls_v_bears,
    bulls_v_bears_batch,
    BullsVBearsStream,
)

high = np.asarray(high_values, dtype=np.float64)
low = np.asarray(low_values, dtype=np.float64)
close = np.asarray(close_values, dtype=np.float64)

result = bulls_v_bears(
    high,
    low,
    close,
    period=14,
    ma_type="ema",
    calculation_method="normalized",
    normalized_bars_back=120,
    raw_rolling_period=50,
    raw_threshold_percentile=95.0,
    threshold_level=80.0,
    kernel="auto",
)

print(result["value"], result["bullish_signal"])

stream = BullsVBearsStream(
    period=14,
    ma_type="ema",
    calculation_method="normalized",
    normalized_bars_back=120,
    raw_rolling_period=50,
    raw_threshold_percentile=95.0,
    threshold_level=80.0,
)

print(stream.update(high[-1], low[-1], close[-1]))

batch = bulls_v_bears_batch(
    high,
    low,
    close,
    period_range=(10, 20, 5),
    normalized_bars_back_range=(80, 120, 40),
    raw_rolling_period_range=(50, 50, 0),
    raw_threshold_percentile_range=(95.0, 95.0, 0.0),
    threshold_level_range=(70.0, 90.0, 10.0),
    ma_type="ema",
    calculation_method="normalized",
    kernel="auto",
)

print(batch["periods"], batch["rows"], batch["cols"])

JavaScript/WASM Bindings

The WASM layer exposes object-returning single-run and batch wrappers plus lower-level allocation and in-place exports. The normal JavaScript path returns plain objects containing the same ten output arrays as the Rust API. The batch wrapper adds the tested parameter lists together with the final rows and cols shape.

import init, {
  bulls_v_bears_js,
  bulls_v_bears_batch_js,
} from "/pkg/vector_ta.js";

await init();

const high = new Float64Array(highValues);
const low = new Float64Array(lowValues);
const close = new Float64Array(closeValues);

const result = bulls_v_bears_js(
  high,
  low,
  close,
  14,
  "ema",
  "normalized",
  120,
  50,
  95.0,
  80.0,
);

console.log(result.value, result.zero_cross_up);

const batch = bulls_v_bears_batch_js(high, low, close, {
  period_range: [10, 20, 5],
  normalized_bars_back_range: [80, 120, 40],
  raw_rolling_period_range: [50, 50, 0],
  raw_threshold_percentile_range: [95.0, 95.0, 0.0],
  threshold_level_range: [70.0, 90.0, 10.0],
  ma_type: "ema",
  calculation_method: "normalized",
});

console.log(batch.periods, batch.threshold_levels, batch.rows, batch.cols);

CUDA Bindings (Rust)

Additional details for the CUDA bindings can be found inside the VectorTA repository.

Performance Analysis

Comparison:
View:
Placeholder data (no recorded benchmarks for this indicator)

Across sizes, Rust CPU runs about 1.14× faster than Tulip C in this benchmark.

Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU)

Related Indicators