Bollinger Bands

Parameters: period = 20 (5–200) | devup = 2 (0.5–5) | devdn = 2 (0.5–5) | matype = sma | devtype = 0

Overview

Bollinger Bands create adaptive price envelopes by adding and subtracting standard deviations from a moving average, with bands expanding during volatile periods and contracting during consolidation. John Bollinger designed this system using a 20 period simple moving average as the middle band, then placing upper and lower bands at 2 standard deviations above and below. The bands automatically adjust to market conditions because standard deviation measures price dispersion from the average. When price touches the upper band during an uptrend, it signals strength rather than immediate reversal; similarly, lower band touches in downtrends confirm bearish momentum. Traders watch for the squeeze pattern where bands narrow to identify low volatility setups before explosive moves, then use band walks where price hugs one band to ride strong trends. The indicator provides multiple signals including breakouts when price closes outside bands, reversals when price moves from outside to inside bands, and volatility transitions when band width expands or contracts significantly.

Implementation Examples

Generate upper, middle, and lower bands in a few lines:

use vectorta::indicators::bollinger_bands::{
    bollinger_bands, BollingerBandsInput, BollingerBandsParams
};
use vectorta::utilities::data_loader::Candles;

let closes = vec![101.2, 102.5, 101.8, 103.6, 102.4, 104.2];
let params = BollingerBandsParams {
    period: Some(20),
    devup: Some(2.0),
    devdn: Some(2.0),
    matype: Some("sma".into()),
    devtype: Some(0),
};
let input = BollingerBandsInput::from_slice(&closes, params);
let bands = bollinger_bands(&input)?;

for ((u, m), l) in bands
    .upper_band
    .iter()
    .zip(bands.middle_band.iter())
    .zip(bands.lower_band.iter())
{
    println!("upper={u:.2}, middle={m:.2}, lower={l:.2}");
}

// Convenience helper with Candles (defaults to close / 20-period SMA)
let candles: Candles = load_price_history()?;
let input = BollingerBandsInput::with_default_candles(&candles);
let bands = bollinger_bands(&input)?;

API Reference

Input Methods
// From a price slice
BollingerBandsInput::from_slice(&closes, BollingerBandsParams) -> BollingerBandsInput

// From Candles with custom source column
BollingerBandsInput::from_candles(&candles, "hlc3", BollingerBandsParams) -> BollingerBandsInput

// Convenience helper (close column, 20 SMA, ±2 stddev)
BollingerBandsInput::with_default_candles(&candles) -> BollingerBandsInput
Parameters Structure
pub struct BollingerBandsParams {
    pub period: Option<usize>,   // Default: 20
    pub devup: Option<f64>,      // Default: 2.0
    pub devdn: Option<f64>,      // Default: 2.0
    pub matype: Option<String>,  // Default: "sma"
    pub devtype: Option<usize>,  // Default: 0 (stddev)
}
Output Structure
pub struct BollingerBandsOutput {
    pub upper_band: Vec<f64>,  // Baseline + devup * deviation
    pub middle_band: Vec<f64>, // Moving average values
    pub lower_band: Vec<f64>,  // Baseline - devdn * deviation
}
Error Handling
use vectorta::indicators::bollinger_bands::{bollinger_bands, BollingerBandsError};

match bollinger_bands(&input) {
    Ok(output) => consume(output),
    Err(BollingerBandsError::EmptyData) => {
        eprintln!("No price data supplied");
    }
    Err(BollingerBandsError::InvalidPeriod { period, data_len }) => {
        eprintln!("Window {period} exceeds available observations {data_len}");
    }
    Err(BollingerBandsError::NotEnoughValidData { needed, valid }) => {
        eprintln!("Need {needed} finite bars, only {valid} available");
    }
    Err(err) => eprintln!("Bollinger Bands failed: {err}"),
}

Python Bindings

Basic Usage

Compute bands from NumPy arrays and receive the three legs separately:

import numpy as np
from vectorta import bollinger_bands

closes = np.array([101.2, 102.5, 101.8, 103.6, 102.4, 104.2], dtype=np.float64)

upper, middle, lower = bollinger_bands(
    closes,
    period=20,
    devup=2.0,
    devdn=2.0,
    matype="sma",
    devtype=0,
)

print("Upper:", upper)
print("Middle:", middle)
print("Lower:", lower)

upper, middle, lower = bollinger_bands(
    closes,
    period=34,
    devup=2.5,
    devdn=2.0,
    matype="ema",
    devtype=1,
    kernel="avx2",
)
Streaming Real-time Updates

Track real-time closes and react once the rolling window is primed:

from vectorta import BollingerBandsStream

stream = BollingerBandsStream(period=20, devup=2.0, devdn=2.0, matype="sma", devtype=0)

for close in realtime_feed():
    bands = stream.update(close)
    if bands is None:
        continue  # warmup

    upper, middle, lower = bands
    if close > upper:
        handle_breakout(close)
    elif close < lower:
        handle_breakdown(close)
Batch Parameter Optimization

Evaluate many combinations without leaving Python:

import numpy as np
from vectorta import bollinger_bands_batch

closes = np.asarray(close_series, dtype=np.float64)

result = bollinger_bands_batch(
    closes,
    period_range=(10, 40, 5),
    devup_range=(1.5, 2.5, 0.5),
    devdn_range=(1.5, 2.5, 0.5),
    matype="sma",
    devtype_range=(0, 0, 0),
    kernel="auto",
)

upper = result["upper"].reshape(-1, closes.size)
middle = result["middle"].reshape(-1, closes.size)
lower = result["lower"].reshape(-1, closes.size)
lengths = result["periods"]

def score(bands):
    # custom evaluation for your strategy
    return some_metric(bands)

best_idx = max(range(len(lengths)), key=lambda i: score(upper[i]))
print("Best period:", lengths[best_idx])
CUDA Acceleration

CUDA support for Bollinger Bands is in development. The API will mirror other GPU-enabled indicators when released.

# Coming soon: CUDA-accelerated Bollinger Band calculations
#
# from vectorta import bollinger_bands_cuda_batch
# import numpy as np
#
# closes = np.asarray(close_series, dtype=np.float32)
# result = bollinger_bands_cuda_batch(
#     closes,
#    period_range=(10, 40, 1),
#     devup_range=(1.0, 3.0, 0.25),
#     devdn_range=(1.0, 3.0, 0.25),
#     matype="sma",
#     devtype=0,
#     device_id=0,
# )
#
# upper = result["upper"]
# middle = result["middle"]
# lower = result["lower"]
# combos = result["periods"]
#
# # Zero-copy variant would expose *_into helpers similar to the CPU bindings.

JavaScript/WASM Bindings

Basic Usage

Call the WASM helper to receive flattened band data and reshape as needed:

import { bollinger_bands_js } from 'vectorta-wasm';

const closes = new Float64Array([101.2, 102.5, 101.8, 103.6, 102.4, 104.2]);
const { values, rows, cols } = bollinger_bands_js(
  closes,
  20,
  2.0,
  2.0,
  'sma',
  0,
);

const upper = values.slice(0, cols);
const middle = values.slice(cols, 2 * cols);
const lower = values.slice(2 * cols);

console.log('Upper band', upper);
console.log('Middle band', middle);
console.log('Lower band', lower);
Memory-Efficient Operations

Operate directly on WASM memory to reuse buffers for large data sets:

import {
  bollinger_bands_alloc,
  bollinger_bands_free,
  bollinger_bands_into,
  memory,
} from 'vectorta-wasm';

const closes = new Float64Array(loadPrices());
const length = closes.length;

// Allocate WASM memory for input and outputs (three vectors)
const inputPtr = allocatePrices(closes); // project helper returning pointer
const upperPtr = bollinger_bands_alloc(length);
const middlePtr = bollinger_bands_alloc(length);
const lowerPtr = bollinger_bands_alloc(length);

bollinger_bands_into(
  inputPtr,
  upperPtr,
  middlePtr,
  lowerPtr,
  length,
  20,
  2.0,
  2.0,
  'sma',
  0,
);

const upper = new Float64Array(memory.buffer, upperPtr, length);
const middle = new Float64Array(memory.buffer, middlePtr, length);
const lower = new Float64Array(memory.buffer, lowerPtr, length);

console.log('Upper', upper);
console.log('Middle', middle);
console.log('Lower', lower);

// Clean up using your allocator wrappers
bollinger_bands_free(upperPtr, length);
bollinger_bands_free(middlePtr, length);
bollinger_bands_free(lowerPtr, length);
freePrices(inputPtr, length);
Batch Processing

Enumerate parameter grids and reshape flattened outputs for analysis:

import {
  bollinger_bands_batch_js,
  bollinger_bands_batch_metadata_js,
} from 'vectorta-wasm';

const closes = new Float64Array(loadPrices());
const metadata = bollinger_bands_batch_metadata_js(
  10, 40, 5,
  1.5, 2.5, 0.5,
  1.5, 2.5, 0.5,
  'sma',
  0,
);

const flat = bollinger_bands_batch_js(
  closes,
  10, 40, 5,
  1.5, 2.5, 0.5,
  1.5, 2.5, 0.5,
  'sma',
  0,
);

const combos = metadata.length / 4;
const cols = closes.length;
const block = combos * cols;
const upper: number[][] = [];
const middle: number[][] = [];
const lower: number[][] = [];

for (let i = 0; i < combos; i++) {
  const base = i * cols;
  const end = base + cols;
  upper.push(Array.from(flat.slice(base, end)));
  middle.push(Array.from(flat.slice(block + base, block + end)));
  lower.push(Array.from(flat.slice(2 * block + base, 2 * block + end)));
}

console.log('Metadata', metadata);
console.log('Upper[0]', upper[0]);

Performance Analysis

Comparison:
View:

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

Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05

Related Indicators