24 KiB
Bollinger Bands Indicators
Overview
Bollinger Bands are volatility indicators that consist of a moving average (middle band) and two standard deviation bands (upper and lower bands). They help identify overbought/oversold conditions and potential breakout opportunities. IncrementalTrader provides both simple price-based and OHLC-based implementations.
BollingerBandsState
Standard Bollinger Bands implementation using closing prices and simple moving average.
Features
- Three Bands: Upper, middle (SMA), and lower bands
- Volatility Measurement: Bands expand/contract with volatility
- Mean Reversion Signals: Price touching bands indicates potential reversal
- Breakout Detection: Price breaking through bands signals trend continuation
Mathematical Formula
Middle Band = Simple Moving Average (SMA)
Upper Band = SMA + (Standard Deviation × Multiplier)
Lower Band = SMA - (Standard Deviation × Multiplier)
Standard Deviation = √(Σ(Price - SMA)² / Period)
Class Definition
from IncrementalTrader.strategies.indicators import BollingerBandsState
class BollingerBandsState(IndicatorState):
def __init__(self, period: int, std_dev_multiplier: float = 2.0):
super().__init__(period)
self.std_dev_multiplier = std_dev_multiplier
self.values = []
self.sum = 0.0
self.sum_squares = 0.0
# Band values
self.middle_band = 0.0
self.upper_band = 0.0
self.lower_band = 0.0
def update(self, value: float):
self.values.append(value)
self.sum += value
self.sum_squares += value * value
if len(self.values) > self.period:
old_value = self.values.pop(0)
self.sum -= old_value
self.sum_squares -= old_value * old_value
self.data_count += 1
self._calculate_bands()
def _calculate_bands(self):
if not self.is_ready():
return
n = len(self.values)
# Calculate SMA (middle band)
self.middle_band = self.sum / n
# Calculate standard deviation
variance = (self.sum_squares / n) - (self.middle_band * self.middle_band)
std_dev = math.sqrt(max(variance, 0))
# Calculate upper and lower bands
band_width = std_dev * self.std_dev_multiplier
self.upper_band = self.middle_band + band_width
self.lower_band = self.middle_band - band_width
def get_value(self) -> float:
"""Returns middle band (SMA) value."""
return self.middle_band
def get_upper_band(self) -> float:
return self.upper_band
def get_lower_band(self) -> float:
return self.lower_band
def get_middle_band(self) -> float:
return self.middle_band
def get_band_width(self) -> float:
"""Get the width between upper and lower bands."""
return self.upper_band - self.lower_band
def get_percent_b(self, price: float) -> float:
"""Calculate %B: position of price within the bands."""
if self.get_band_width() == 0:
return 0.5
return (price - self.lower_band) / self.get_band_width()
Usage Examples
Basic Bollinger Bands Usage
# Create 20-period Bollinger Bands with 2.0 standard deviation
bb = BollingerBandsState(period=20, std_dev_multiplier=2.0)
# Price data
prices = [100, 101, 99, 102, 98, 103, 97, 104, 96, 105, 95, 106, 94, 107, 93]
for price in prices:
bb.update(price)
if bb.is_ready():
print(f"Price: {price:.2f}")
print(f" Upper: {bb.get_upper_band():.2f}")
print(f" Middle: {bb.get_middle_band():.2f}")
print(f" Lower: {bb.get_lower_band():.2f}")
print(f" %B: {bb.get_percent_b(price):.2f}")
print(f" Width: {bb.get_band_width():.2f}")
Bollinger Bands Trading Signals
class BollingerBandsSignals:
def __init__(self, period: int = 20, std_dev: float = 2.0):
self.bb = BollingerBandsState(period, std_dev)
self.previous_price = None
self.previous_percent_b = None
def update(self, price: float):
self.bb.update(price)
self.previous_price = price
def get_mean_reversion_signal(self, current_price: float) -> str:
"""Get mean reversion signals based on band touches."""
if not self.bb.is_ready():
return "HOLD"
percent_b = self.bb.get_percent_b(current_price)
# Oversold: price near or below lower band
if percent_b <= 0.1:
return "BUY"
# Overbought: price near or above upper band
elif percent_b >= 0.9:
return "SELL"
# Return to middle: exit positions
elif 0.4 <= percent_b <= 0.6:
return "EXIT"
return "HOLD"
def get_breakout_signal(self, current_price: float) -> str:
"""Get breakout signals based on band penetration."""
if not self.bb.is_ready() or self.previous_price is None:
return "HOLD"
upper_band = self.bb.get_upper_band()
lower_band = self.bb.get_lower_band()
# Bullish breakout: price breaks above upper band
if self.previous_price <= upper_band and current_price > upper_band:
return "BUY_BREAKOUT"
# Bearish breakout: price breaks below lower band
elif self.previous_price >= lower_band and current_price < lower_band:
return "SELL_BREAKOUT"
return "HOLD"
def get_squeeze_condition(self) -> bool:
"""Detect Bollinger Band squeeze (low volatility)."""
if not self.bb.is_ready():
return False
# Simple squeeze detection: band width below threshold
# You might want to compare with historical band width
band_width = self.bb.get_band_width()
middle_band = self.bb.get_middle_band()
# Squeeze when band width is less than 4% of middle band
return (band_width / middle_band) < 0.04
# Usage
bb_signals = BollingerBandsSignals(period=20, std_dev=2.0)
for price in prices:
bb_signals.update(price)
mean_reversion = bb_signals.get_mean_reversion_signal(price)
breakout = bb_signals.get_breakout_signal(price)
squeeze = bb_signals.get_squeeze_condition()
if mean_reversion != "HOLD":
print(f"Mean Reversion Signal: {mean_reversion} at {price:.2f}")
if breakout != "HOLD":
print(f"Breakout Signal: {breakout} at {price:.2f}")
if squeeze:
print(f"Bollinger Band Squeeze detected at {price:.2f}")
Performance Characteristics
- Time Complexity: O(1) per update (after initial period)
- Space Complexity: O(period)
- Memory Usage: ~8 bytes per period + constant overhead
BollingerBandsOHLCState
OHLC-based Bollinger Bands implementation using typical price (HLC/3) for more accurate volatility measurement.
Features
- OHLC Data Support: Uses high, low, close for typical price calculation
- Better Volatility Measurement: More accurate than close-only bands
- Intraday Analysis: Accounts for intraday price action
- Enhanced Signals: More reliable signals due to complete price information
Mathematical Formula
Typical Price = (High + Low + Close) / 3
Middle Band = SMA(Typical Price)
Upper Band = Middle Band + (Standard Deviation × Multiplier)
Lower Band = Middle Band - (Standard Deviation × Multiplier)
Class Definition
class BollingerBandsOHLCState(OHLCIndicatorState):
def __init__(self, period: int, std_dev_multiplier: float = 2.0):
super().__init__(period)
self.std_dev_multiplier = std_dev_multiplier
self.typical_prices = []
self.sum = 0.0
self.sum_squares = 0.0
# Band values
self.middle_band = 0.0
self.upper_band = 0.0
self.lower_band = 0.0
def _process_ohlc_data(self, high: float, low: float, close: float):
# Calculate typical price
typical_price = (high + low + close) / 3.0
self.typical_prices.append(typical_price)
self.sum += typical_price
self.sum_squares += typical_price * typical_price
if len(self.typical_prices) > self.period:
old_price = self.typical_prices.pop(0)
self.sum -= old_price
self.sum_squares -= old_price * old_price
self._calculate_bands()
def _calculate_bands(self):
if not self.is_ready():
return
n = len(self.typical_prices)
# Calculate SMA (middle band)
self.middle_band = self.sum / n
# Calculate standard deviation
variance = (self.sum_squares / n) - (self.middle_band * self.middle_band)
std_dev = math.sqrt(max(variance, 0))
# Calculate upper and lower bands
band_width = std_dev * self.std_dev_multiplier
self.upper_band = self.middle_band + band_width
self.lower_band = self.middle_band - band_width
def get_value(self) -> float:
"""Returns middle band (SMA) value."""
return self.middle_band
def get_upper_band(self) -> float:
return self.upper_band
def get_lower_band(self) -> float:
return self.lower_band
def get_middle_band(self) -> float:
return self.middle_band
def get_band_width(self) -> float:
return self.upper_band - self.lower_band
def get_percent_b_ohlc(self, high: float, low: float, close: float) -> float:
"""Calculate %B using OHLC data."""
typical_price = (high + low + close) / 3.0
if self.get_band_width() == 0:
return 0.5
return (typical_price - self.lower_band) / self.get_band_width()
Usage Examples
OHLC Bollinger Bands Analysis
# Create OHLC-based Bollinger Bands
bb_ohlc = BollingerBandsOHLCState(period=20, std_dev_multiplier=2.0)
# OHLC data: (high, low, close)
ohlc_data = [
(105.0, 102.0, 104.0),
(106.0, 103.0, 105.5),
(107.0, 104.0, 106.0),
(108.0, 105.0, 107.5),
(109.0, 106.0, 108.0)
]
for high, low, close in ohlc_data:
bb_ohlc.update_ohlc(high, low, close)
if bb_ohlc.is_ready():
typical_price = (high + low + close) / 3.0
percent_b = bb_ohlc.get_percent_b_ohlc(high, low, close)
print(f"OHLC: H={high:.2f}, L={low:.2f}, C={close:.2f}")
print(f" Typical Price: {typical_price:.2f}")
print(f" Upper: {bb_ohlc.get_upper_band():.2f}")
print(f" Middle: {bb_ohlc.get_middle_band():.2f}")
print(f" Lower: {bb_ohlc.get_lower_band():.2f}")
print(f" %B: {percent_b:.2f}")
Advanced OHLC Bollinger Bands Strategy
class OHLCBollingerStrategy:
def __init__(self, period: int = 20, std_dev: float = 2.0):
self.bb = BollingerBandsOHLCState(period, std_dev)
self.previous_ohlc = None
def update(self, high: float, low: float, close: float):
self.bb.update_ohlc(high, low, close)
self.previous_ohlc = (high, low, close)
def analyze_candle_position(self, high: float, low: float, close: float) -> dict:
"""Analyze candle position relative to Bollinger Bands."""
if not self.bb.is_ready():
return {"analysis": "NOT_READY"}
upper_band = self.bb.get_upper_band()
lower_band = self.bb.get_lower_band()
middle_band = self.bb.get_middle_band()
# Analyze different price levels
analysis = {
"high_above_upper": high > upper_band,
"low_below_lower": low < lower_band,
"close_above_middle": close > middle_band,
"body_outside_bands": high > upper_band and low < lower_band,
"squeeze_breakout": False,
"signal": "HOLD"
}
# Detect squeeze breakout
band_width = self.bb.get_band_width()
if band_width / middle_band < 0.03: # Very narrow bands
if high > upper_band:
analysis["squeeze_breakout"] = True
analysis["signal"] = "BUY_BREAKOUT"
elif low < lower_band:
analysis["squeeze_breakout"] = True
analysis["signal"] = "SELL_BREAKOUT"
# Mean reversion signals
percent_b = self.bb.get_percent_b_ohlc(high, low, close)
if percent_b <= 0.1 and close > low: # Bounce from lower band
analysis["signal"] = "BUY_BOUNCE"
elif percent_b >= 0.9 and close < high: # Rejection from upper band
analysis["signal"] = "SELL_REJECTION"
return analysis
def get_support_resistance_levels(self) -> dict:
"""Get dynamic support and resistance levels."""
if not self.bb.is_ready():
return {}
return {
"resistance": self.bb.get_upper_band(),
"support": self.bb.get_lower_band(),
"pivot": self.bb.get_middle_band(),
"band_width": self.bb.get_band_width()
}
# Usage
ohlc_strategy = OHLCBollingerStrategy(period=20, std_dev=2.0)
for high, low, close in ohlc_data:
ohlc_strategy.update(high, low, close)
analysis = ohlc_strategy.analyze_candle_position(high, low, close)
levels = ohlc_strategy.get_support_resistance_levels()
if analysis.get("signal") != "HOLD":
print(f"Signal: {analysis['signal']}")
print(f"Analysis: {analysis}")
print(f"S/R Levels: {levels}")
Performance Characteristics
- Time Complexity: O(1) per update (after initial period)
- Space Complexity: O(period)
- Memory Usage: ~8 bytes per period + constant overhead
Comparison: BollingerBandsState vs BollingerBandsOHLCState
| Aspect | BollingerBandsState | BollingerBandsOHLCState |
|---|---|---|
| Input Data | Close prices only | High, Low, Close |
| Calculation Base | Close price | Typical price (HLC/3) |
| Accuracy | Good for trends | Better for volatility |
| Signal Quality | Standard | Enhanced |
| Data Requirements | Minimal | Complete OHLC |
When to Use BollingerBandsState
- Simple Analysis: When only closing prices are available
- Trend Following: For basic trend and mean reversion analysis
- Memory Efficiency: When OHLC data is not necessary
- Quick Implementation: For rapid prototyping and testing
When to Use BollingerBandsOHLCState
- Complete Analysis: When full OHLC data is available
- Volatility Trading: For more accurate volatility measurement
- Intraday Trading: When intraday price action matters
- Professional Trading: For more sophisticated trading strategies
Advanced Usage Patterns
Multi-Timeframe Bollinger Bands
class MultiBollingerBands:
def __init__(self):
self.bb_short = BollingerBandsState(period=10, std_dev_multiplier=2.0)
self.bb_medium = BollingerBandsState(period=20, std_dev_multiplier=2.0)
self.bb_long = BollingerBandsState(period=50, std_dev_multiplier=2.0)
def update(self, price: float):
self.bb_short.update(price)
self.bb_medium.update(price)
self.bb_long.update(price)
def get_volatility_regime(self) -> str:
"""Determine volatility regime across timeframes."""
if not all([self.bb_short.is_ready(), self.bb_medium.is_ready(), self.bb_long.is_ready()]):
return "UNKNOWN"
# Compare band widths
short_width = self.bb_short.get_band_width() / self.bb_short.get_middle_band()
medium_width = self.bb_medium.get_band_width() / self.bb_medium.get_middle_band()
long_width = self.bb_long.get_band_width() / self.bb_long.get_middle_band()
avg_width = (short_width + medium_width + long_width) / 3
if avg_width > 0.08:
return "HIGH_VOLATILITY"
elif avg_width < 0.03:
return "LOW_VOLATILITY"
else:
return "NORMAL_VOLATILITY"
def get_trend_alignment(self, price: float) -> str:
"""Check trend alignment across timeframes."""
if not all([self.bb_short.is_ready(), self.bb_medium.is_ready(), self.bb_long.is_ready()]):
return "UNKNOWN"
# Check position relative to middle bands
above_short = price > self.bb_short.get_middle_band()
above_medium = price > self.bb_medium.get_middle_band()
above_long = price > self.bb_long.get_middle_band()
if all([above_short, above_medium, above_long]):
return "STRONG_BULLISH"
elif not any([above_short, above_medium, above_long]):
return "STRONG_BEARISH"
elif above_short and above_medium:
return "BULLISH"
elif not above_short and not above_medium:
return "BEARISH"
else:
return "MIXED"
# Usage
multi_bb = MultiBollingerBands()
for price in prices:
multi_bb.update(price)
volatility_regime = multi_bb.get_volatility_regime()
trend_alignment = multi_bb.get_trend_alignment(price)
print(f"Price: {price:.2f}, Volatility: {volatility_regime}, Trend: {trend_alignment}")
Bollinger Bands with RSI Confluence
class BollingerRSIStrategy:
def __init__(self, bb_period: int = 20, rsi_period: int = 14):
self.bb = BollingerBandsState(bb_period, 2.0)
self.rsi = SimpleRSIState(rsi_period)
def update(self, price: float):
self.bb.update(price)
self.rsi.update(price)
def get_confluence_signal(self, price: float) -> dict:
"""Get signals based on Bollinger Bands and RSI confluence."""
if not (self.bb.is_ready() and self.rsi.is_ready()):
return {"signal": "HOLD", "confidence": 0.0}
percent_b = self.bb.get_percent_b(price)
rsi_value = self.rsi.get_value()
# Bullish confluence: oversold RSI + lower band touch
if percent_b <= 0.1 and rsi_value <= 30:
confidence = min(0.9, (30 - rsi_value) / 20 + (0.1 - percent_b) * 5)
return {
"signal": "BUY",
"confidence": confidence,
"reason": "oversold_confluence",
"percent_b": percent_b,
"rsi": rsi_value
}
# Bearish confluence: overbought RSI + upper band touch
elif percent_b >= 0.9 and rsi_value >= 70:
confidence = min(0.9, (rsi_value - 70) / 20 + (percent_b - 0.9) * 5)
return {
"signal": "SELL",
"confidence": confidence,
"reason": "overbought_confluence",
"percent_b": percent_b,
"rsi": rsi_value
}
# Exit signals: return to middle
elif 0.4 <= percent_b <= 0.6 and 40 <= rsi_value <= 60:
return {
"signal": "EXIT",
"confidence": 0.5,
"reason": "return_to_neutral",
"percent_b": percent_b,
"rsi": rsi_value
}
return {"signal": "HOLD", "confidence": 0.0}
# Usage
bb_rsi_strategy = BollingerRSIStrategy(bb_period=20, rsi_period=14)
for price in prices:
bb_rsi_strategy.update(price)
signal_info = bb_rsi_strategy.get_confluence_signal(price)
if signal_info["signal"] != "HOLD":
print(f"Confluence Signal: {signal_info['signal']}")
print(f" Confidence: {signal_info['confidence']:.2f}")
print(f" Reason: {signal_info['reason']}")
print(f" %B: {signal_info.get('percent_b', 0):.2f}")
print(f" RSI: {signal_info.get('rsi', 0):.1f}")
Integration with Strategies
Bollinger Bands Mean Reversion Strategy
class BollingerMeanReversionStrategy(IncStrategyBase):
def __init__(self, name: str, params: dict = None):
super().__init__(name, params)
# Initialize Bollinger Bands
bb_period = self.params.get('bb_period', 20)
bb_std_dev = self.params.get('bb_std_dev', 2.0)
self.bb = BollingerBandsOHLCState(bb_period, bb_std_dev)
# Strategy parameters
self.entry_threshold = self.params.get('entry_threshold', 0.1) # %B threshold
self.exit_threshold = self.params.get('exit_threshold', 0.5) # Return to middle
# State tracking
self.position_type = None
def _process_aggregated_data(self, timestamp: int, ohlcv: tuple) -> IncStrategySignal:
open_price, high, low, close, volume = ohlcv
# Update Bollinger Bands
self.bb.update_ohlc(high, low, close)
# Wait for indicator to be ready
if not self.bb.is_ready():
return IncStrategySignal.HOLD()
# Calculate %B
percent_b = self.bb.get_percent_b_ohlc(high, low, close)
band_width = self.bb.get_band_width()
middle_band = self.bb.get_middle_band()
# Entry signals
if percent_b <= self.entry_threshold and self.position_type != "LONG":
# Oversold condition - buy signal
confidence = min(0.9, (self.entry_threshold - percent_b) * 5)
self.position_type = "LONG"
return IncStrategySignal.BUY(
confidence=confidence,
metadata={
'percent_b': percent_b,
'band_width': band_width,
'signal_type': 'mean_reversion_buy',
'upper_band': self.bb.get_upper_band(),
'lower_band': self.bb.get_lower_band()
}
)
elif percent_b >= (1.0 - self.entry_threshold) and self.position_type != "SHORT":
# Overbought condition - sell signal
confidence = min(0.9, (percent_b - (1.0 - self.entry_threshold)) * 5)
self.position_type = "SHORT"
return IncStrategySignal.SELL(
confidence=confidence,
metadata={
'percent_b': percent_b,
'band_width': band_width,
'signal_type': 'mean_reversion_sell',
'upper_band': self.bb.get_upper_band(),
'lower_band': self.bb.get_lower_band()
}
)
# Exit signals
elif abs(percent_b - 0.5) <= (0.5 - self.exit_threshold):
# Return to middle - exit position
if self.position_type is not None:
exit_signal = IncStrategySignal.SELL() if self.position_type == "LONG" else IncStrategySignal.BUY()
exit_signal.confidence = 0.6
exit_signal.metadata = {
'percent_b': percent_b,
'signal_type': 'mean_reversion_exit',
'previous_position': self.position_type
}
self.position_type = None
return exit_signal
return IncStrategySignal.HOLD()
Performance Optimization Tips
1. Choose the Right Implementation
# For simple price analysis
bb = BollingerBandsState(period=20, std_dev_multiplier=2.0)
# For comprehensive OHLC analysis
bb_ohlc = BollingerBandsOHLCState(period=20, std_dev_multiplier=2.0)
2. Optimize Standard Deviation Calculation
# Use incremental variance calculation for better performance
def incremental_variance(sum_val: float, sum_squares: float, count: int, mean: float) -> float:
"""Calculate variance incrementally."""
if count == 0:
return 0.0
return max(0.0, (sum_squares / count) - (mean * mean))
3. Cache Band Values for Multiple Calculations
class CachedBollingerBands:
def __init__(self, period: int, std_dev: float = 2.0):
self.bb = BollingerBandsState(period, std_dev)
self._cached_bands = None
self._cache_valid = False
def update(self, price: float):
self.bb.update(price)
self._cache_valid = False
def get_bands(self) -> tuple:
if not self._cache_valid:
self._cached_bands = (
self.bb.get_upper_band(),
self.bb.get_middle_band(),
self.bb.get_lower_band()
)
self._cache_valid = True
return self._cached_bands
Bollinger Bands are versatile indicators for volatility analysis and mean reversion trading. Use BollingerBandsState for simple price analysis or BollingerBandsOHLCState for comprehensive volatility measurement with complete OHLC data.