BollingerBands#
Description#
BollingerBands computes the classic Bollinger bands of a single stream over a sliding window: the middle band is the rolling mean, and the lower and upper bands sit num_std standard deviations away on either side. All three are returned per step.
Equation:
where n = window_size and k = num_std. The standard deviation uses the unbiased sample estimator (ddof=1), matching pandas.Series.rolling(w).std().
Parameters:
window_size(int, ≥ 2): size of the rolling window.num_std(float, ≥ 0, default2.0): width of the bands in standard deviations.start_policy(str, default"strict"): controls warmup behavior. SeeRollingMeanfor the full definition.
Input shape: a single stream (scalar, 1-D array, 2-D array, iterator, …). Same matrix as RollingMean.
Output shape: an extra trailing axis of size 3 is appended to the input shape. A 1-D input of shape (T,) returns shape (T, 3); index 0 is the lower band, index 1 is the middle band, index 2 is the upper band. A scalar call returns a Python tuple of three floats.
NaN handling#
Policy: ignore. A NaN in any input at index t causes the function to skip that step: output at t is NaN and internal state is unchanged. Subsequent finite samples are processed as if step t had not occurred.
Examples#
Usage example#
import numpy as np
import plotly.graph_objects as go
from screamer import BollingerBands
np.random.seed(0)
N = 250
prices = 100.0 + np.cumsum(np.random.normal(scale=0.6, size=N))
bands = BollingerBands(window_size=20, num_std=2.0)(prices)
lower, mid, upper = bands[:, 0], bands[:, 1], bands[:, 2]
fig = go.Figure()
fig.add_trace(go.Scatter(y=upper, mode="lines", name="upper",
line=dict(color="green", dash="dot")))
fig.add_trace(go.Scatter(y=mid, mode="lines", name="mid (mean)",
line=dict(color="black")))
fig.add_trace(go.Scatter(y=lower, mode="lines", name="lower",
line=dict(color="red", dash="dot")))
fig.add_trace(go.Scatter(y=prices, mode="lines", name="price",
line=dict(color="steelblue")))
fig.update_layout(
title="BollingerBands(window=20, num_std=2)",
xaxis_title="time",
yaxis_title="price",
margin=dict(l=20, r=20, t=60, b=20),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)
fig.show()
Implementation Details#
Two detail::RollingSum buffers maintain Σx and Σx² over the window. Each step updates both sums in O(1), then the mean and (unbiased sample) variance are computed in closed form, the standard deviation is sqrt(max(var, 0)) to guard against tiny negative values from floating-point cancellation, and the three band values are returned as a tuple.
Time:
O(1)per new element.Space:
O(window_size)(two circular buffers of lengthwindow_size).Reference: parity with the pandas expression
mean = s.rolling(w).mean(); std = s.rolling(w).std(); (mean - k·std, mean, mean + k·std)to floating-point tolerance, parametrised over windows5, 10, 20, 30andnum_std ∈ {0.5, 1.0, 2.0, 2.5}intests/test_one_input_multi_output.py.
Edge cases#
Returns
(NaN, NaN, NaN)until at least 2 samples have arrived (variance is undefined for a single sample underddof=1).Under
"strict"policy, returns(NaN, NaN, NaN)until the window is full.num_std = 0is permitted; the upper, middle, and lower bands all equal the mean.