Cycles/cycles/IncStrategies/bbrs_incremental.py

532 lines
20 KiB
Python
Raw Normal View History

"""
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
}
}