- 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.
228 lines
7.3 KiB
Python
228 lines
7.3 KiB
Python
"""
|
|
Moving Average Indicator State
|
|
|
|
This module implements incremental moving average calculation that maintains
|
|
constant memory usage and provides identical results to traditional batch calculations.
|
|
"""
|
|
|
|
from collections import deque
|
|
from typing import Union
|
|
from .base import SimpleIndicatorState
|
|
|
|
|
|
class MovingAverageState(SimpleIndicatorState):
|
|
"""
|
|
Incremental moving average calculation state.
|
|
|
|
This class maintains the state for calculating a simple moving average
|
|
incrementally. It uses a rolling window approach with constant memory usage.
|
|
|
|
Attributes:
|
|
period (int): The moving average period
|
|
values (deque): Rolling window of values (max length = period)
|
|
sum (float): Current sum of values in the window
|
|
|
|
Example:
|
|
ma = MovingAverageState(period=20)
|
|
|
|
# Add values incrementally
|
|
ma_value = ma.update(100.0) # Returns current MA value
|
|
ma_value = ma.update(105.0) # Updates and returns new MA value
|
|
|
|
# Check if warmed up (has enough values)
|
|
if ma.is_warmed_up():
|
|
current_ma = ma.get_current_value()
|
|
"""
|
|
|
|
def __init__(self, period: int):
|
|
"""
|
|
Initialize moving average state.
|
|
|
|
Args:
|
|
period: Number of periods for the moving average
|
|
|
|
Raises:
|
|
ValueError: If period is not a positive integer
|
|
"""
|
|
super().__init__(period)
|
|
self.values = deque(maxlen=period)
|
|
self.sum = 0.0
|
|
self.is_initialized = True
|
|
|
|
def update(self, new_value: Union[float, int]) -> float:
|
|
"""
|
|
Update moving average with new value.
|
|
|
|
Args:
|
|
new_value: New price/value to add to the moving average
|
|
|
|
Returns:
|
|
Current moving average value
|
|
|
|
Raises:
|
|
ValueError: If new_value is not finite
|
|
TypeError: If new_value is not numeric
|
|
"""
|
|
# Validate input
|
|
if not isinstance(new_value, (int, float)):
|
|
raise TypeError(f"new_value must be numeric, got {type(new_value)}")
|
|
|
|
self.validate_input(new_value)
|
|
|
|
# If deque is at max capacity, subtract the value being removed
|
|
if len(self.values) == self.period:
|
|
self.sum -= self.values[0] # Will be automatically removed by deque
|
|
|
|
# Add new value
|
|
self.values.append(float(new_value))
|
|
self.sum += float(new_value)
|
|
self.values_received += 1
|
|
|
|
# Calculate current moving average
|
|
current_count = len(self.values)
|
|
self._current_value = self.sum / current_count
|
|
|
|
return self._current_value
|
|
|
|
def is_warmed_up(self) -> bool:
|
|
"""
|
|
Check if moving average has enough data for reliable values.
|
|
|
|
Returns:
|
|
True if we have at least 'period' number of values
|
|
"""
|
|
return len(self.values) >= self.period
|
|
|
|
def reset(self) -> None:
|
|
"""Reset moving average state to initial conditions."""
|
|
self.values.clear()
|
|
self.sum = 0.0
|
|
self.values_received = 0
|
|
self._current_value = None
|
|
|
|
def get_current_value(self) -> Union[float, None]:
|
|
"""
|
|
Get current moving average value without updating.
|
|
|
|
Returns:
|
|
Current moving average value, or None if not enough data
|
|
"""
|
|
if len(self.values) == 0:
|
|
return None
|
|
return self.sum / len(self.values)
|
|
|
|
def get_state_summary(self) -> dict:
|
|
"""Get detailed state summary for debugging."""
|
|
base_summary = super().get_state_summary()
|
|
base_summary.update({
|
|
'window_size': len(self.values),
|
|
'sum': self.sum,
|
|
'values_in_window': list(self.values) if len(self.values) <= 10 else f"[{len(self.values)} values]"
|
|
})
|
|
return base_summary
|
|
|
|
|
|
class ExponentialMovingAverageState(SimpleIndicatorState):
|
|
"""
|
|
Incremental exponential moving average calculation state.
|
|
|
|
This class maintains the state for calculating an exponential moving average (EMA)
|
|
incrementally. EMA gives more weight to recent values and requires minimal memory.
|
|
|
|
Attributes:
|
|
period (int): The EMA period (used to calculate smoothing factor)
|
|
alpha (float): Smoothing factor (2 / (period + 1))
|
|
ema_value (float): Current EMA value
|
|
|
|
Example:
|
|
ema = ExponentialMovingAverageState(period=20)
|
|
|
|
# Add values incrementally
|
|
ema_value = ema.update(100.0) # Returns current EMA value
|
|
ema_value = ema.update(105.0) # Updates and returns new EMA value
|
|
"""
|
|
|
|
def __init__(self, period: int):
|
|
"""
|
|
Initialize exponential moving average state.
|
|
|
|
Args:
|
|
period: Number of periods for the EMA (used to calculate alpha)
|
|
|
|
Raises:
|
|
ValueError: If period is not a positive integer
|
|
"""
|
|
super().__init__(period)
|
|
self.alpha = 2.0 / (period + 1) # Smoothing factor
|
|
self.ema_value = None
|
|
self.is_initialized = True
|
|
|
|
def update(self, new_value: Union[float, int]) -> float:
|
|
"""
|
|
Update exponential moving average with new value.
|
|
|
|
Args:
|
|
new_value: New price/value to add to the EMA
|
|
|
|
Returns:
|
|
Current EMA value
|
|
|
|
Raises:
|
|
ValueError: If new_value is not finite
|
|
TypeError: If new_value is not numeric
|
|
"""
|
|
# Validate input
|
|
if not isinstance(new_value, (int, float)):
|
|
raise TypeError(f"new_value must be numeric, got {type(new_value)}")
|
|
|
|
self.validate_input(new_value)
|
|
|
|
new_value = float(new_value)
|
|
|
|
if self.ema_value is None:
|
|
# First value - initialize EMA
|
|
self.ema_value = new_value
|
|
else:
|
|
# EMA formula: EMA = alpha * new_value + (1 - alpha) * previous_EMA
|
|
self.ema_value = self.alpha * new_value + (1 - self.alpha) * self.ema_value
|
|
|
|
self.values_received += 1
|
|
self._current_value = self.ema_value
|
|
|
|
return self.ema_value
|
|
|
|
def is_warmed_up(self) -> bool:
|
|
"""
|
|
Check if EMA has enough data for reliable values.
|
|
|
|
For EMA, we consider it warmed up after receiving 'period' number of values,
|
|
though it starts producing values immediately.
|
|
|
|
Returns:
|
|
True if we have received at least 'period' number of values
|
|
"""
|
|
return self.values_received >= self.period
|
|
|
|
def reset(self) -> None:
|
|
"""Reset EMA state to initial conditions."""
|
|
self.ema_value = None
|
|
self.values_received = 0
|
|
self._current_value = None
|
|
|
|
def get_current_value(self) -> Union[float, None]:
|
|
"""
|
|
Get current EMA value without updating.
|
|
|
|
Returns:
|
|
Current EMA value, or None if no values received yet
|
|
"""
|
|
return self.ema_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,
|
|
'ema_value': self.ema_value
|
|
})
|
|
return base_summary |