""" Incremental BBRS Strategy This module implements an incremental version of the Bollinger Bands + RSI Strategy (BBRS) for real-time data processing. It maintains constant memory usage and provides identical results to the batch implementation after the warm-up period. Key Features: - Accepts minute-level data input for real-time compatibility - Internal timeframe aggregation (1min, 5min, 15min, 1h, etc.) - Incremental Bollinger Bands calculation - Incremental RSI calculation with Wilder's smoothing - Market regime detection (trending vs sideways) - Real-time signal generation - Constant memory usage """ from typing import Dict, Optional, Union, Tuple import numpy as np import pandas as pd from datetime import datetime, timedelta from .indicators.bollinger_bands import BollingerBandsState from .indicators.rsi import RSIState class TimeframeAggregator: """ Handles real-time aggregation of minute data to higher timeframes. This class accumulates minute-level OHLCV data and produces complete bars when a timeframe period is completed. """ def __init__(self, timeframe_minutes: int = 15): """ Initialize timeframe aggregator. Args: timeframe_minutes: Target timeframe in minutes (e.g., 60 for 1h, 15 for 15min) """ self.timeframe_minutes = timeframe_minutes self.current_bar = None self.current_bar_start = None self.last_completed_bar = None def update(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, float]]: """ Update with new minute data and return completed bar if timeframe is complete. Args: timestamp: Timestamp of the data ohlcv_data: OHLCV data dictionary Returns: Completed OHLCV bar if timeframe period ended, None otherwise """ # Calculate which timeframe bar this timestamp belongs to bar_start = self._get_bar_start_time(timestamp) # Check if we're starting a new bar if self.current_bar_start != bar_start: # Save the completed bar (if any) completed_bar = self.current_bar.copy() if self.current_bar is not None else None # Start new bar self.current_bar_start = bar_start self.current_bar = { 'timestamp': bar_start, 'open': ohlcv_data['close'], # Use current close as open for new bar 'high': ohlcv_data['close'], 'low': ohlcv_data['close'], 'close': ohlcv_data['close'], 'volume': ohlcv_data['volume'] } # Return the completed bar (if any) if completed_bar is not None: self.last_completed_bar = completed_bar return completed_bar else: # Update current bar with new data if self.current_bar is not None: self.current_bar['high'] = max(self.current_bar['high'], ohlcv_data['high']) self.current_bar['low'] = min(self.current_bar['low'], ohlcv_data['low']) self.current_bar['close'] = ohlcv_data['close'] self.current_bar['volume'] += ohlcv_data['volume'] return None # No completed bar yet def _get_bar_start_time(self, timestamp: pd.Timestamp) -> pd.Timestamp: """Calculate the start time of the timeframe bar for given timestamp.""" # Round down to the nearest timeframe boundary minutes_since_midnight = timestamp.hour * 60 + timestamp.minute bar_minutes = (minutes_since_midnight // self.timeframe_minutes) * self.timeframe_minutes return timestamp.replace( hour=bar_minutes // 60, minute=bar_minutes % 60, second=0, microsecond=0 ) def get_current_bar(self) -> Optional[Dict[str, float]]: """Get the current incomplete bar (for debugging).""" return self.current_bar.copy() if self.current_bar is not None else None def reset(self): """Reset aggregator state.""" self.current_bar = None self.current_bar_start = None self.last_completed_bar = None class BBRSIncrementalState: """ Incremental BBRS strategy state for real-time processing. This class maintains all the state needed for the BBRS strategy and can process new minute-level price data incrementally, internally aggregating to the configured timeframe before running indicators. Attributes: timeframe_minutes (int): Strategy timeframe in minutes (default: 60 for 1h) bb_period (int): Bollinger Bands period rsi_period (int): RSI period bb_width_threshold (float): BB width threshold for market regime detection trending_bb_multiplier (float): BB multiplier for trending markets sideways_bb_multiplier (float): BB multiplier for sideways markets trending_rsi_thresholds (tuple): RSI thresholds for trending markets (low, high) sideways_rsi_thresholds (tuple): RSI thresholds for sideways markets (low, high) squeeze_strategy (bool): Enable squeeze strategy Example: # Initialize strategy for 1-hour timeframe config = { "timeframe_minutes": 60, # 1 hour bars "bb_period": 20, "rsi_period": 14, "bb_width": 0.05, "trending": { "bb_std_dev_multiplier": 2.5, "rsi_threshold": [30, 70] }, "sideways": { "bb_std_dev_multiplier": 1.8, "rsi_threshold": [40, 60] }, "SqueezeStrategy": True } strategy = BBRSIncrementalState(config) # Process minute-level data in real-time for minute_data in live_data_stream: result = strategy.update_minute_data(minute_data['timestamp'], minute_data) if result is not None: # New timeframe bar completed if result['buy_signal']: print("Buy signal generated!") """ def __init__(self, config: Dict): """ Initialize incremental BBRS strategy. Args: config: Strategy configuration dictionary """ # Store configuration self.timeframe_minutes = config.get("timeframe_minutes", 60) # Default to 1 hour self.bb_period = config.get("bb_period", 20) self.rsi_period = config.get("rsi_period", 14) self.bb_width_threshold = config.get("bb_width", 0.05) # Market regime specific parameters trending_config = config.get("trending", {}) sideways_config = config.get("sideways", {}) self.trending_bb_multiplier = trending_config.get("bb_std_dev_multiplier", 2.5) self.sideways_bb_multiplier = sideways_config.get("bb_std_dev_multiplier", 1.8) self.trending_rsi_thresholds = tuple(trending_config.get("rsi_threshold", [30, 70])) self.sideways_rsi_thresholds = tuple(sideways_config.get("rsi_threshold", [40, 60])) self.squeeze_strategy = config.get("SqueezeStrategy", True) # Initialize timeframe aggregator self.aggregator = TimeframeAggregator(self.timeframe_minutes) # Initialize indicators with different multipliers for regime detection self.bb_trending = BollingerBandsState(self.bb_period, self.trending_bb_multiplier) self.bb_sideways = BollingerBandsState(self.bb_period, self.sideways_bb_multiplier) self.bb_reference = BollingerBandsState(self.bb_period, 2.0) # For regime detection self.rsi = RSIState(self.rsi_period) # State tracking self.bars_processed = 0 self.current_price = None self.current_volume = None self.volume_ma = None self.volume_sum = 0.0 self.volume_history = [] # For volume MA calculation # Signal state self.last_buy_signal = False self.last_sell_signal = False self.last_result = None def update_minute_data(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, Union[float, bool]]]: """ Update strategy with new minute-level OHLCV data. This method accepts minute-level data and internally aggregates to the configured timeframe. It only processes indicators and generates signals when a complete timeframe bar is formed. Args: timestamp: Timestamp of the minute data ohlcv_data: Dictionary with 'open', 'high', 'low', 'close', 'volume' Returns: Strategy result dictionary if a timeframe bar completed, None otherwise """ # Validate input required_keys = ['open', 'high', 'low', 'close', 'volume'] for key in required_keys: if key not in ohlcv_data: raise ValueError(f"Missing required key: {key}") # Update timeframe aggregator completed_bar = self.aggregator.update(timestamp, ohlcv_data) if completed_bar is not None: # Process the completed timeframe bar return self._process_timeframe_bar(completed_bar) return None # No completed bar yet def update(self, ohlcv_data: Dict[str, float]) -> Dict[str, Union[float, bool]]: """ Update strategy with pre-aggregated timeframe data (for testing/compatibility). This method is for backward compatibility and testing with pre-aggregated data. For real-time use, prefer update_minute_data(). Args: ohlcv_data: Dictionary with 'open', 'high', 'low', 'close', 'volume' Returns: Strategy result dictionary """ # Create a fake timestamp for compatibility fake_timestamp = pd.Timestamp.now() # Process directly as a completed bar completed_bar = { 'timestamp': fake_timestamp, 'open': ohlcv_data['open'], 'high': ohlcv_data['high'], 'low': ohlcv_data['low'], 'close': ohlcv_data['close'], 'volume': ohlcv_data['volume'] } return self._process_timeframe_bar(completed_bar) def _process_timeframe_bar(self, bar_data: Dict[str, float]) -> Dict[str, Union[float, bool]]: """ Process a completed timeframe bar and generate signals. Args: bar_data: Completed timeframe bar data Returns: Strategy result dictionary """ close_price = float(bar_data['close']) volume = float(bar_data['volume']) # Update indicators bb_trending_result = self.bb_trending.update(close_price) bb_sideways_result = self.bb_sideways.update(close_price) bb_reference_result = self.bb_reference.update(close_price) rsi_value = self.rsi.update(close_price) # Update volume tracking self._update_volume_tracking(volume) # Determine market regime market_regime = self._determine_market_regime(bb_reference_result) # Select appropriate BB values based on regime if market_regime == "sideways": bb_result = bb_sideways_result rsi_thresholds = self.sideways_rsi_thresholds else: # trending bb_result = bb_trending_result rsi_thresholds = self.trending_rsi_thresholds # Generate signals buy_signal, sell_signal = self._generate_signals( close_price, volume, bb_result, rsi_value, market_regime, rsi_thresholds ) # Update state self.current_price = close_price self.current_volume = volume self.bars_processed += 1 self.last_buy_signal = buy_signal self.last_sell_signal = sell_signal # Create comprehensive result result = { # Timeframe info 'timestamp': bar_data['timestamp'], 'timeframe_minutes': self.timeframe_minutes, # Price data 'open': bar_data['open'], 'high': bar_data['high'], 'low': bar_data['low'], 'close': close_price, 'volume': volume, # Bollinger Bands (regime-specific) 'upper_band': bb_result['upper_band'], 'middle_band': bb_result['middle_band'], 'lower_band': bb_result['lower_band'], 'bb_width': bb_result['bandwidth'], # RSI 'rsi': rsi_value, # Market regime 'market_regime': market_regime, 'bb_width_reference': bb_reference_result['bandwidth'], # Volume analysis 'volume_ma': self.volume_ma, 'volume_spike': self._check_volume_spike(volume), # Signals 'buy_signal': buy_signal, 'sell_signal': sell_signal, # Strategy metadata 'is_warmed_up': self.is_warmed_up(), 'bars_processed': self.bars_processed, 'rsi_thresholds': rsi_thresholds, 'bb_multiplier': bb_result.get('std_dev', self.trending_bb_multiplier) } self.last_result = result return result def _update_volume_tracking(self, volume: float) -> None: """Update volume moving average tracking.""" # Simple moving average for volume (20 periods) volume_period = 20 if len(self.volume_history) >= volume_period: # Remove oldest volume self.volume_sum -= self.volume_history[0] self.volume_history.pop(0) # Add new volume self.volume_history.append(volume) self.volume_sum += volume # Calculate moving average if len(self.volume_history) > 0: self.volume_ma = self.volume_sum / len(self.volume_history) else: self.volume_ma = volume def _determine_market_regime(self, bb_reference: Dict[str, float]) -> str: """ Determine market regime based on Bollinger Band width. Args: bb_reference: Reference BB result for regime detection Returns: "sideways" or "trending" """ if not self.bb_reference.is_warmed_up(): return "trending" # Default to trending during warm-up bb_width = bb_reference['bandwidth'] if bb_width < self.bb_width_threshold: return "sideways" else: return "trending" def _check_volume_spike(self, current_volume: float) -> bool: """Check if current volume represents a spike (≥1.5× average).""" if self.volume_ma is None or self.volume_ma == 0: return False return current_volume >= 1.5 * self.volume_ma def _generate_signals(self, price: float, volume: float, bb_result: Dict[str, float], rsi_value: float, market_regime: str, rsi_thresholds: Tuple[float, float]) -> Tuple[bool, bool]: """ Generate buy/sell signals based on strategy logic. Args: price: Current close price volume: Current volume bb_result: Bollinger Bands result rsi_value: Current RSI value market_regime: "sideways" or "trending" rsi_thresholds: (low_threshold, high_threshold) Returns: (buy_signal, sell_signal) """ # Don't generate signals during warm-up if not self.is_warmed_up(): return False, False # Don't generate signals if RSI is NaN if np.isnan(rsi_value): return False, False upper_band = bb_result['upper_band'] lower_band = bb_result['lower_band'] rsi_low, rsi_high = rsi_thresholds volume_spike = self._check_volume_spike(volume) buy_signal = False sell_signal = False if market_regime == "sideways": # Sideways market (Mean Reversion) buy_condition = (price <= lower_band) and (rsi_value <= rsi_low) sell_condition = (price >= upper_band) and (rsi_value >= rsi_high) if self.squeeze_strategy: # Add volume contraction filter for sideways markets volume_contraction = volume < 0.7 * (self.volume_ma or volume) buy_condition = buy_condition and volume_contraction sell_condition = sell_condition and volume_contraction buy_signal = buy_condition sell_signal = sell_condition else: # trending # Trending market (Breakout Mode) buy_condition = (price < lower_band) and (rsi_value < 50) and volume_spike sell_condition = (price > upper_band) and (rsi_value > 50) and volume_spike buy_signal = buy_condition sell_signal = sell_condition return buy_signal, sell_signal def is_warmed_up(self) -> bool: """ Check if strategy is warmed up and ready for reliable signals. Returns: True if all indicators are warmed up """ return (self.bb_trending.is_warmed_up() and self.bb_sideways.is_warmed_up() and self.bb_reference.is_warmed_up() and self.rsi.is_warmed_up() and len(self.volume_history) >= 20) def get_current_incomplete_bar(self) -> Optional[Dict[str, float]]: """ Get the current incomplete timeframe bar (for monitoring). Returns: Current incomplete bar data or None """ return self.aggregator.get_current_bar() def reset(self) -> None: """Reset strategy state to initial conditions.""" self.aggregator.reset() self.bb_trending.reset() self.bb_sideways.reset() self.bb_reference.reset() self.rsi.reset() self.bars_processed = 0 self.current_price = None self.current_volume = None self.volume_ma = None self.volume_sum = 0.0 self.volume_history.clear() self.last_buy_signal = False self.last_sell_signal = False self.last_result = None def get_state_summary(self) -> Dict: """Get comprehensive state summary for debugging.""" return { 'strategy_type': 'BBRS_Incremental', 'timeframe_minutes': self.timeframe_minutes, 'bars_processed': self.bars_processed, 'is_warmed_up': self.is_warmed_up(), 'current_price': self.current_price, 'current_volume': self.current_volume, 'volume_ma': self.volume_ma, 'current_incomplete_bar': self.get_current_incomplete_bar(), 'last_signals': { 'buy': self.last_buy_signal, 'sell': self.last_sell_signal }, 'indicators': { 'bb_trending': self.bb_trending.get_state_summary(), 'bb_sideways': self.bb_sideways.get_state_summary(), 'bb_reference': self.bb_reference.get_state_summary(), 'rsi': self.rsi.get_state_summary() }, 'config': { 'bb_period': self.bb_period, 'rsi_period': self.rsi_period, 'bb_width_threshold': self.bb_width_threshold, 'trending_bb_multiplier': self.trending_bb_multiplier, 'sideways_bb_multiplier': self.sideways_bb_multiplier, 'trending_rsi_thresholds': self.trending_rsi_thresholds, 'sideways_rsi_thresholds': self.sideways_rsi_thresholds, 'squeeze_strategy': self.squeeze_strategy } }