- Introduced a comprehensive framework for incremental trading strategies, including modules for strategy execution, backtesting, and data processing. - Added key components such as `IncTrader`, `IncBacktester`, and various trading strategies (e.g., `MetaTrendStrategy`, `BBRSStrategy`, `RandomStrategy`) to facilitate real-time trading and backtesting. - Implemented a robust backtesting framework with configuration management, parallel execution, and result analysis capabilities. - Developed an incremental indicators framework to support real-time data processing with constant memory usage. - Enhanced documentation to provide clear usage examples and architecture overview, ensuring maintainability and ease of understanding for future development. - Ensured compatibility with existing strategies and maintained a focus on performance and scalability throughout the implementation.
325 lines
12 KiB
Python
325 lines
12 KiB
Python
"""
|
|
Bollinger Bands Indicator State
|
|
|
|
This module implements incremental Bollinger Bands calculation that maintains constant memory usage
|
|
and provides identical results to traditional batch calculations. Used by the BBRSStrategy.
|
|
"""
|
|
|
|
from typing import Dict, Union, Optional
|
|
from collections import deque
|
|
import math
|
|
from .base import OHLCIndicatorState
|
|
from .moving_average import MovingAverageState
|
|
|
|
|
|
class BollingerBandsState(OHLCIndicatorState):
|
|
"""
|
|
Incremental Bollinger Bands calculation state.
|
|
|
|
Bollinger Bands consist of:
|
|
- Middle Band: Simple Moving Average of close prices
|
|
- Upper Band: Middle Band + (Standard Deviation * multiplier)
|
|
- Lower Band: Middle Band - (Standard Deviation * multiplier)
|
|
|
|
This implementation maintains a rolling window for standard deviation calculation
|
|
while using the MovingAverageState for the middle band.
|
|
|
|
Attributes:
|
|
period (int): Period for moving average and standard deviation
|
|
std_dev_multiplier (float): Multiplier for standard deviation
|
|
ma_state (MovingAverageState): Moving average state for middle band
|
|
close_values (deque): Rolling window of close prices for std dev calculation
|
|
close_sum_sq (float): Sum of squared close values for variance calculation
|
|
|
|
Example:
|
|
bb = BollingerBandsState(period=20, std_dev_multiplier=2.0)
|
|
|
|
# Add price data incrementally
|
|
result = bb.update(103.5) # Close price
|
|
upper_band = result['upper_band']
|
|
middle_band = result['middle_band']
|
|
lower_band = result['lower_band']
|
|
bandwidth = result['bandwidth']
|
|
"""
|
|
|
|
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0):
|
|
"""
|
|
Initialize Bollinger Bands state.
|
|
|
|
Args:
|
|
period: Period for moving average and standard deviation (default: 20)
|
|
std_dev_multiplier: Multiplier for standard deviation (default: 2.0)
|
|
|
|
Raises:
|
|
ValueError: If period is not positive or multiplier is not positive
|
|
"""
|
|
super().__init__(period)
|
|
|
|
if std_dev_multiplier <= 0:
|
|
raise ValueError(f"Standard deviation multiplier must be positive, got {std_dev_multiplier}")
|
|
|
|
self.std_dev_multiplier = std_dev_multiplier
|
|
self.ma_state = MovingAverageState(period)
|
|
|
|
# For incremental standard deviation calculation
|
|
self.close_values = deque(maxlen=period)
|
|
self.close_sum_sq = 0.0 # Sum of squared values
|
|
|
|
self.is_initialized = True
|
|
|
|
def update(self, close_price: Union[float, int]) -> Dict[str, float]:
|
|
"""
|
|
Update Bollinger Bands with new close price.
|
|
|
|
Args:
|
|
close_price: New closing price
|
|
|
|
Returns:
|
|
Dictionary with 'upper_band', 'middle_band', 'lower_band', 'bandwidth', 'std_dev'
|
|
|
|
Raises:
|
|
ValueError: If close_price is not finite
|
|
TypeError: If close_price is not numeric
|
|
"""
|
|
# Validate input
|
|
if not isinstance(close_price, (int, float)):
|
|
raise TypeError(f"close_price must be numeric, got {type(close_price)}")
|
|
|
|
self.validate_input(close_price)
|
|
|
|
close_price = float(close_price)
|
|
|
|
# Update moving average (middle band)
|
|
middle_band = self.ma_state.update(close_price)
|
|
|
|
# Update rolling window for standard deviation
|
|
if len(self.close_values) == self.period:
|
|
# Remove oldest value from sum of squares
|
|
old_value = self.close_values[0]
|
|
self.close_sum_sq -= old_value * old_value
|
|
|
|
# Add new value
|
|
self.close_values.append(close_price)
|
|
self.close_sum_sq += close_price * close_price
|
|
|
|
# Calculate standard deviation
|
|
n = len(self.close_values)
|
|
if n < 2:
|
|
# Not enough data for standard deviation
|
|
std_dev = 0.0
|
|
else:
|
|
# Incremental variance calculation: Var = (sum_sq - n*mean^2) / (n-1)
|
|
mean = middle_band
|
|
variance = (self.close_sum_sq - n * mean * mean) / (n - 1)
|
|
std_dev = math.sqrt(max(variance, 0.0)) # Ensure non-negative
|
|
|
|
# Calculate bands
|
|
upper_band = middle_band + (self.std_dev_multiplier * std_dev)
|
|
lower_band = middle_band - (self.std_dev_multiplier * std_dev)
|
|
|
|
# Calculate bandwidth (normalized band width)
|
|
if middle_band != 0:
|
|
bandwidth = (upper_band - lower_band) / middle_band
|
|
else:
|
|
bandwidth = 0.0
|
|
|
|
self.values_received += 1
|
|
|
|
# Store current values
|
|
result = {
|
|
'upper_band': upper_band,
|
|
'middle_band': middle_band,
|
|
'lower_band': lower_band,
|
|
'bandwidth': bandwidth,
|
|
'std_dev': std_dev
|
|
}
|
|
|
|
self._current_values = result
|
|
return result
|
|
|
|
def is_warmed_up(self) -> bool:
|
|
"""
|
|
Check if Bollinger Bands has enough data for reliable values.
|
|
|
|
Returns:
|
|
True if we have at least 'period' number of values
|
|
"""
|
|
return self.ma_state.is_warmed_up()
|
|
|
|
def reset(self) -> None:
|
|
"""Reset Bollinger Bands state to initial conditions."""
|
|
self.ma_state.reset()
|
|
self.close_values.clear()
|
|
self.close_sum_sq = 0.0
|
|
self.values_received = 0
|
|
self._current_values = {}
|
|
|
|
def get_current_value(self) -> Optional[Dict[str, float]]:
|
|
"""
|
|
Get current Bollinger Bands values without updating.
|
|
|
|
Returns:
|
|
Dictionary with current BB values, or None if not warmed up
|
|
"""
|
|
if not self.is_warmed_up():
|
|
return None
|
|
return self._current_values.copy() if self._current_values else None
|
|
|
|
def get_squeeze_status(self, squeeze_threshold: float = 0.05) -> bool:
|
|
"""
|
|
Check if Bollinger Bands are in a squeeze condition.
|
|
|
|
Args:
|
|
squeeze_threshold: Bandwidth threshold for squeeze detection
|
|
|
|
Returns:
|
|
True if bandwidth is below threshold (squeeze condition)
|
|
"""
|
|
if not self.is_warmed_up() or not self._current_values:
|
|
return False
|
|
|
|
bandwidth = self._current_values.get('bandwidth', float('inf'))
|
|
return bandwidth < squeeze_threshold
|
|
|
|
def get_position_relative_to_bands(self, current_price: float) -> str:
|
|
"""
|
|
Get current price position relative to Bollinger Bands.
|
|
|
|
Args:
|
|
current_price: Current price to evaluate
|
|
|
|
Returns:
|
|
'above_upper', 'between_bands', 'below_lower', or 'unknown'
|
|
"""
|
|
if not self.is_warmed_up() or not self._current_values:
|
|
return 'unknown'
|
|
|
|
upper_band = self._current_values['upper_band']
|
|
lower_band = self._current_values['lower_band']
|
|
|
|
if current_price > upper_band:
|
|
return 'above_upper'
|
|
elif current_price < lower_band:
|
|
return 'below_lower'
|
|
else:
|
|
return 'between_bands'
|
|
|
|
def get_state_summary(self) -> dict:
|
|
"""Get detailed state summary for debugging."""
|
|
base_summary = super().get_state_summary()
|
|
base_summary.update({
|
|
'std_dev_multiplier': self.std_dev_multiplier,
|
|
'close_values_count': len(self.close_values),
|
|
'close_sum_sq': self.close_sum_sq,
|
|
'ma_state': self.ma_state.get_state_summary(),
|
|
'current_squeeze': self.get_squeeze_status() if self.is_warmed_up() else None
|
|
})
|
|
return base_summary
|
|
|
|
|
|
class BollingerBandsOHLCState(OHLCIndicatorState):
|
|
"""
|
|
Bollinger Bands implementation that works with OHLC data.
|
|
|
|
This version can calculate Bollinger Bands based on different price types
|
|
(close, typical price, etc.) and provides additional OHLC-based analysis.
|
|
"""
|
|
|
|
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0, price_type: str = 'close'):
|
|
"""
|
|
Initialize OHLC Bollinger Bands state.
|
|
|
|
Args:
|
|
period: Period for calculation
|
|
std_dev_multiplier: Standard deviation multiplier
|
|
price_type: Price type to use ('close', 'typical', 'median', 'weighted')
|
|
"""
|
|
super().__init__(period)
|
|
|
|
if price_type not in ['close', 'typical', 'median', 'weighted']:
|
|
raise ValueError(f"Invalid price_type: {price_type}")
|
|
|
|
self.std_dev_multiplier = std_dev_multiplier
|
|
self.price_type = price_type
|
|
self.bb_state = BollingerBandsState(period, std_dev_multiplier)
|
|
self.is_initialized = True
|
|
|
|
def _extract_price(self, ohlc_data: Dict[str, float]) -> float:
|
|
"""Extract price based on price_type setting."""
|
|
if self.price_type == 'close':
|
|
return ohlc_data['close']
|
|
elif self.price_type == 'typical':
|
|
return (ohlc_data['high'] + ohlc_data['low'] + ohlc_data['close']) / 3.0
|
|
elif self.price_type == 'median':
|
|
return (ohlc_data['high'] + ohlc_data['low']) / 2.0
|
|
elif self.price_type == 'weighted':
|
|
return (ohlc_data['high'] + ohlc_data['low'] + 2 * ohlc_data['close']) / 4.0
|
|
else:
|
|
return ohlc_data['close']
|
|
|
|
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, float]:
|
|
"""
|
|
Update Bollinger Bands with OHLC data.
|
|
|
|
Args:
|
|
ohlc_data: Dictionary with OHLC data
|
|
|
|
Returns:
|
|
Dictionary with Bollinger Bands values plus OHLC analysis
|
|
"""
|
|
# Validate input
|
|
if not isinstance(ohlc_data, dict):
|
|
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
|
|
|
|
self.validate_input(ohlc_data)
|
|
|
|
# Extract price based on type
|
|
price = self._extract_price(ohlc_data)
|
|
|
|
# Update underlying BB state
|
|
bb_result = self.bb_state.update(price)
|
|
|
|
# Add OHLC-specific analysis
|
|
high = ohlc_data['high']
|
|
low = ohlc_data['low']
|
|
close = ohlc_data['close']
|
|
|
|
# Check if high/low touched bands
|
|
upper_band = bb_result['upper_band']
|
|
lower_band = bb_result['lower_band']
|
|
|
|
bb_result.update({
|
|
'high_above_upper': high > upper_band,
|
|
'low_below_lower': low < lower_band,
|
|
'close_position': self.bb_state.get_position_relative_to_bands(close),
|
|
'price_type': self.price_type,
|
|
'extracted_price': price
|
|
})
|
|
|
|
self.values_received += 1
|
|
self._current_values = bb_result
|
|
|
|
return bb_result
|
|
|
|
def is_warmed_up(self) -> bool:
|
|
"""Check if OHLC Bollinger Bands is warmed up."""
|
|
return self.bb_state.is_warmed_up()
|
|
|
|
def reset(self) -> None:
|
|
"""Reset OHLC Bollinger Bands state."""
|
|
self.bb_state.reset()
|
|
self.values_received = 0
|
|
self._current_values = {}
|
|
|
|
def get_current_value(self) -> Optional[Dict[str, float]]:
|
|
"""Get current OHLC Bollinger Bands values."""
|
|
return self.bb_state.get_current_value()
|
|
|
|
def get_state_summary(self) -> dict:
|
|
"""Get detailed state summary."""
|
|
base_summary = super().get_state_summary()
|
|
base_summary.update({
|
|
'price_type': self.price_type,
|
|
'bb_state': self.bb_state.get_state_summary()
|
|
})
|
|
return base_summary |