Source code for backtrader.indicators.bollinger
#!/usr/bin/env python
"""Bollinger Bands Indicator Module - Volatility bands.
This module provides the Bollinger Bands indicator developed by
John Bollinger in the 1980s for measuring market volatility.
Classes:
BollingerBands: Bollinger Bands indicator (alias: BBands).
BollingerBandsPct: Bollinger Bands with %B line.
Example:
class MyStrategy(bt.Strategy):
def __init__(self):
self.bbands = bt.indicators.BBands(self.data, period=20, devfactor=2.0)
def next(self):
if self.data.close[0] < self.bbands.bot[0]:
self.buy()
elif self.data.close[0] > self.bbands.top[0]:
self.sell()
"""
import math
from . import Indicator, MovAv
[docs]
class BollingerBands(Indicator):
"""
Defined by John Bollinger in the 80s. It measures volatility by defining
upper and lower bands at distance x standard deviations
Formula:
- midband = SimpleMovingAverage(close, period)
- topband = midband + devfactor * StandardDeviation(data, period)
- botband = midband - devfactor * StandardDeviation(data, period)
See:
- http://en.wikipedia.org/wiki/Bollinger_Bands
"""
alias = ("BBands",)
lines = (
"mid",
"top",
"bot",
)
params = (
("period", 20),
("devfactor", 2.0),
("movav", MovAv.Simple),
)
plotinfo = {"subplot": False}
plotlines = {
"mid": {"ls": "--"},
"top": {"_samecolor": True},
"bot": {"_samecolor": True},
}
def _plotlabel(self):
plabels = [self.p.period, self.p.devfactor]
plabels += [self.p.movav] * self.p.notdefault("movav")
return plabels
def __init__(self):
"""Initialize the Bollinger Bands indicator.
Sets minimum period to the configured period.
"""
super().__init__()
self.addminperiod(self.p.period)
[docs]
def next(self):
"""Calculate Bollinger Bands for the current bar.
Calculates mid (SMA), top (mid + devfactor*stddev), and
bot (mid - devfactor*stddev) bands.
"""
period = self.p.period
devfactor = self.p.devfactor
# Calculate SMA (mid)
data_sum = 0.0
data_sq_sum = 0.0
for i in range(period):
val = self.data[-i]
data_sum += val
data_sq_sum += val * val
mid = data_sum / period
# Calculate StdDev
meansq = data_sq_sum / period
sqmean = mid * mid
diff = abs(meansq - sqmean)
stddev = math.sqrt(max(0, diff))
# Set lines
self.lines.mid[0] = mid
self.lines.top[0] = mid + devfactor * stddev
self.lines.bot[0] = mid - devfactor * stddev
[docs]
def once(self, start, end):
"""Calculate Bollinger Bands in runonce mode."""
darray = self.data.array
mid_array = self.lines.mid.array
top_array = self.lines.top.array
bot_array = self.lines.bot.array
period = self.p.period
devfactor = self.p.devfactor
actual_end = min(end, len(darray))
darray_len = len(darray)
# Ensure arrays are sized
for arr in [mid_array, top_array, bot_array]:
while len(arr) < end:
arr.append(float("nan"))
# PERFORMANCE: Cache constants and functions
nan_val = float("nan")
sqrt = math.sqrt
# Pre-fill warmup with NaN
for i in range(min(period - 1, darray_len)):
mid_array[i] = nan_val
top_array[i] = nan_val
bot_array[i] = nan_val
for i in range(period - 1, actual_end):
data_sum = 0.0
data_sq_sum = 0.0
has_nan = False
# PERFORMANCE: Simplified loop with faster NaN check
for j in range(period):
idx = i - j
if 0 <= idx < darray_len:
val = darray[idx]
# PERFORMANCE: Use val != val for NaN check (faster)
if val != val:
has_nan = True
break
data_sum += val
data_sq_sum += val * val
if has_nan:
mid_array[i] = nan_val
top_array[i] = nan_val
bot_array[i] = nan_val
continue
mid = data_sum / period
meansq = data_sq_sum / period
sqmean = mid * mid
diff = abs(meansq - sqmean)
stddev = sqrt(max(0, diff))
mid_array[i] = mid
top_array[i] = mid + devfactor * stddev
bot_array[i] = mid - devfactor * stddev
# Bollinger Bands Percentage indicator
[docs]
class BollingerBandsPct(BollingerBands):
"""
Extends the Bollinger Bands with a Percentage line
"""
lines = ("pctb",)
plotlines = {"pctb": {"_name": "%B"}} # display the line as %B on chart
def __init__(self):
"""Initialize the Bollinger Bands %B indicator.
Extends Bollinger Bands with percentage calculation.
"""
super().__init__()
[docs]
def next(self):
"""Calculate %B line for the current bar.
Formula: %B = (price - bot) / (top - bot)
"""
super().next()
top = self.lines.top[0]
bot = self.lines.bot[0]
diff = top - bot
if diff != 0:
self.lines.pctb[0] = (self.data[0] - bot) / diff
else:
self.lines.pctb[0] = 0.0
[docs]
def once(self, start, end):
"""Calculate %B line in runonce mode."""
super().once(start, end)
darray = self.data.array
top_array = self.lines.top.array
bot_array = self.lines.bot.array
pctb_array = self.lines.pctb.array
while len(pctb_array) < end:
pctb_array.append(float("nan"))
for i in range(start, min(end, len(darray), len(top_array), len(bot_array))):
top = top_array[i] if i < len(top_array) else 0.0
bot = bot_array[i] if i < len(bot_array) else 0.0
data_val = darray[i] if i < len(darray) else 0.0
if (
isinstance(top, float)
and math.isnan(top)
or isinstance(bot, float)
and math.isnan(bot)
):
pctb_array[i] = float("nan")
else:
diff = top - bot
if diff != 0:
pctb_array[i] = (data_val - bot) / diff
else:
pctb_array[i] = 0.0