Implement Incremental BBRS Strategy for Real-time Data Processing

- Introduced `BBRSIncrementalState` for real-time processing of the Bollinger Bands + RSI strategy, allowing minute-level data input and internal timeframe aggregation.
- Added `TimeframeAggregator` class to handle real-time data aggregation to higher timeframes (15min, 1h, etc.).
- Updated `README_BBRS.md` to document the new incremental strategy, including key features and usage examples.
- Created comprehensive tests to validate the incremental strategy against the original implementation, ensuring signal accuracy and performance consistency.
- Enhanced error handling and logging for better monitoring during real-time processing.
- Updated `TODO.md` to reflect the completion of the incremental BBRS strategy implementation.
This commit is contained in:
Vasily.onl
2025-05-26 16:46:04 +08:00
parent ba78539cbb
commit bd6a0f05d7
10 changed files with 2239 additions and 62 deletions

View File

@@ -12,7 +12,7 @@ from .moving_average import ExponentialMovingAverageState
class RSIState(SimpleIndicatorState):
"""
Incremental RSI calculation state.
Incremental RSI calculation state using Wilder's smoothing.
RSI measures the speed and magnitude of price changes to evaluate overbought
or oversold conditions. It oscillates between 0 and 100.
@@ -20,13 +20,14 @@ class RSIState(SimpleIndicatorState):
RSI = 100 - (100 / (1 + RS))
where RS = Average Gain / Average Loss over the specified period
This implementation uses exponential moving averages for gain and loss smoothing,
which is more responsive and memory-efficient than simple moving averages.
This implementation uses Wilder's smoothing (alpha = 1/period) to match
the original pandas implementation exactly.
Attributes:
period (int): The RSI period (typically 14)
gain_ema (ExponentialMovingAverageState): EMA state for gains
loss_ema (ExponentialMovingAverageState): EMA state for losses
alpha (float): Wilder's smoothing factor (1/period)
avg_gain (float): Current average gain
avg_loss (float): Current average loss
previous_close (float): Previous period's close price
Example:
@@ -52,30 +53,32 @@ class RSIState(SimpleIndicatorState):
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.gain_ema = ExponentialMovingAverageState(period)
self.loss_ema = ExponentialMovingAverageState(period)
self.alpha = 1.0 / period # Wilder's smoothing factor
self.avg_gain = None
self.avg_loss = None
self.previous_close = None
self.is_initialized = True
def update(self, new_close: Union[float, int]) -> float:
"""
Update RSI with new close price.
Update RSI with new close price using Wilder's smoothing.
Args:
new_close: New closing price
Returns:
Current RSI value (0-100)
Current RSI value (0-100), or NaN if not warmed up
Raises:
ValueError: If new_close is not finite
TypeError: If new_close is not numeric
"""
# Validate input
if not isinstance(new_close, (int, float)):
# Validate input - accept numpy types as well
import numpy as np
if not isinstance(new_close, (int, float, np.integer, np.floating)):
raise TypeError(f"new_close must be numeric, got {type(new_close)}")
self.validate_input(new_close)
self.validate_input(float(new_close))
new_close = float(new_close)
@@ -83,8 +86,8 @@ class RSIState(SimpleIndicatorState):
# First value - no gain/loss to calculate
self.previous_close = new_close
self.values_received += 1
# Return neutral RSI for first value
self._current_value = 50.0
# Return NaN until warmed up (matches original behavior)
self._current_value = float('nan')
return self._current_value
# Calculate price change
@@ -94,17 +97,30 @@ class RSIState(SimpleIndicatorState):
gain = max(price_change, 0.0)
loss = max(-price_change, 0.0)
# Update EMAs for gains and losses
avg_gain = self.gain_ema.update(gain)
avg_loss = self.loss_ema.update(loss)
# Calculate RSI
if avg_loss == 0.0:
# Avoid division by zero - all gains, no losses
rsi_value = 100.0
if self.avg_gain is None:
# Initialize with first gain/loss
self.avg_gain = gain
self.avg_loss = loss
else:
rs = avg_gain / avg_loss
rsi_value = 100.0 - (100.0 / (1.0 + rs))
# Wilder's smoothing: avg = alpha * new_value + (1 - alpha) * previous_avg
self.avg_gain = self.alpha * gain + (1 - self.alpha) * self.avg_gain
self.avg_loss = self.alpha * loss + (1 - self.alpha) * self.avg_loss
# Calculate RSI only if warmed up
# RSI should start when we have 'period' price changes (not including the first value)
if self.values_received > self.period:
if self.avg_loss == 0.0:
# Avoid division by zero - all gains, no losses
if self.avg_gain > 0:
rsi_value = 100.0
else:
rsi_value = 50.0 # Neutral when both are zero
else:
rs = self.avg_gain / self.avg_loss
rsi_value = 100.0 - (100.0 / (1.0 + rs))
else:
# Not warmed up yet - return NaN
rsi_value = float('nan')
# Store state
self.previous_close = new_close
@@ -118,14 +134,15 @@ class RSIState(SimpleIndicatorState):
Check if RSI has enough data for reliable values.
Returns:
True if both gain and loss EMAs are warmed up
True if we have enough price changes for RSI calculation
"""
return self.gain_ema.is_warmed_up() and self.loss_ema.is_warmed_up()
return self.values_received > self.period
def reset(self) -> None:
"""Reset RSI state to initial conditions."""
self.gain_ema.reset()
self.loss_ema.reset()
self.alpha = 1.0 / self.period
self.avg_gain = None
self.avg_loss = None
self.previous_close = None
self.values_received = 0
self._current_value = None
@@ -137,22 +154,18 @@ class RSIState(SimpleIndicatorState):
Returns:
Current RSI value (0-100), or None if not enough data
"""
if self.values_received == 0:
if not self.is_warmed_up():
return None
elif self.values_received == 1:
return 50.0 # Neutral RSI for first value
elif not self.is_warmed_up():
return self._current_value # Return current calculation even if not fully warmed up
else:
return self._current_value
return self._current_value
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'alpha': self.alpha,
'previous_close': self.previous_close,
'gain_ema': self.gain_ema.get_state_summary(),
'loss_ema': self.loss_ema.get_state_summary(),
'avg_gain': self.avg_gain,
'avg_loss': self.avg_loss,
'current_rsi': self.get_current_value()
})
return base_summary