Bollinger Bands
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 vector_ta::indicators::bollinger_bands::{
bollinger_bands, BollingerBandsInput, BollingerBandsParams
};
use vector_ta::utilities::data_loader::Candles;
let closes: Vec<f64> = vec![/* ... */]; // ensure closes.len() >= period
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 vector_ta::indicators::bollinger_bands::{bollinger_bands, BollingerBandsError};
match bollinger_bands(&input) {
Ok(output) => consume(output),
Err(BollingerBandsError::EmptyInputData) => {
eprintln!("No price data supplied");
}
Err(BollingerBandsError::InvalidPeriod { period, data_len }) => {
eprintln!("Window {period} exceeds available observations {data_len}");
}
Err(BollingerBandsError::AllValuesNaN) => {
eprintln!("All input values are NaN");
}
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 vector_ta import bollinger_bands
closes = np.asarray(close_series, dtype=np.float64) # length >= period
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 vector_ta 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 vector_ta 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 helpers are available when the Python package is built with CUDA support. Inputs must be float32; outputs are device arrays (DLPack / __cuda_array_interface__ compatible).
import numpy as np
from vector_ta import bollinger_bands_cuda_batch_dev, bollinger_bands_cuda_many_series_one_param_dev
# One series (float32)
data_f32 = np.asarray(load_data(), dtype=np.float32)
dev = bollinger_bands_cuda_batch_dev(
data_f32=data_f32,
period_range=(5, 30, 5),
devup_range=(0.5, 2.0, 0.5),
devdn_range=(0.5, 2.0, 0.5),
device_id=0,
)
# Many series (time-major)
prices_tm_f32 = np.asarray(load_prices_time_major_matrix(), dtype=np.float32)
rows, cols = prices_tm_f32.shape
prices_tm_f32 = prices_tm_f32.ravel()
dev_tm = bollinger_bands_cuda_many_series_one_param_dev(
prices_tm_f32=prices_tm_f32,
cols=cols,
rows=rows,
period=14,
devup=1.0,
devdn=1.0,
device_id=0,
) 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(loadPrices()); // length >= period
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]); CUDA Bindings (Rust)
use vector_ta::cuda::CudaBollingerBands;
use vector_ta::indicators::bollinger_bands::BollingerBandsBatchRange;
let cuda = CudaBollingerBands::new(0)?;
let data_f32: [f32] = /* ... */;
let sweep = BollingerBandsBatchRange::default();
let out = cuda.bollinger_bands_batch_dev(&data_f32, &sweep)?;
let _ = out; Performance Analysis
Across sizes, Rust CPU runs about 1.56× faster than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-02-28