""" 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 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 data received """ 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