2025-05-28 22:37:53 +08:00

24 KiB
Raw Blame History

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.