# Moving Average Indicators ## Overview Moving averages are fundamental trend-following indicators that smooth price data by creating a constantly updated average price. IncrementalTrader provides both Simple Moving Average (SMA) and Exponential Moving Average (EMA) implementations with O(1) time complexity. ## MovingAverageState (SMA) Simple Moving Average that maintains a rolling window of prices. ### Features - **O(1) Updates**: Constant time complexity per update - **Memory Efficient**: Only stores necessary data points - **Real-time Ready**: Immediate calculation without historical data dependency ### Mathematical Formula ``` SMA = (P₁ + P₂ + ... + Pₙ) / n Where: - P₁, P₂, ..., Pₙ are the last n price values - n is the period ``` ### Class Definition ```python from IncrementalTrader.strategies.indicators import MovingAverageState class MovingAverageState(IndicatorState): def __init__(self, period: int): super().__init__(period) self.values = [] self.sum = 0.0 def update(self, value: float): self.values.append(value) self.sum += value if len(self.values) > self.period: old_value = self.values.pop(0) self.sum -= old_value self.data_count += 1 def get_value(self) -> float: if not self.is_ready(): return 0.0 return self.sum / len(self.values) ``` ### Usage Examples #### Basic Usage ```python # Create 20-period SMA sma_20 = MovingAverageState(period=20) # Update with price data prices = [100, 101, 99, 102, 98, 103, 97, 104] for price in prices: sma_20.update(price) if sma_20.is_ready(): print(f"SMA(20): {sma_20.get_value():.2f}") ``` #### Multiple Timeframes ```python # Different period SMAs sma_10 = MovingAverageState(period=10) sma_20 = MovingAverageState(period=20) sma_50 = MovingAverageState(period=50) for price in price_stream: # Update all SMAs sma_10.update(price) sma_20.update(price) sma_50.update(price) # Check for golden cross (SMA10 > SMA20) if all([sma_10.is_ready(), sma_20.is_ready()]): if sma_10.get_value() > sma_20.get_value(): print("Golden Cross detected!") ``` ### Performance Characteristics - **Time Complexity**: O(1) per update - **Space Complexity**: O(period) - **Memory Usage**: ~8 bytes per period (for float values) ## ExponentialMovingAverageState (EMA) Exponential Moving Average that gives more weight to recent prices. ### Features - **Exponential Weighting**: Recent prices have more influence - **O(1) Memory**: Only stores current EMA value and multiplier - **Responsive**: Reacts faster to price changes than SMA ### Mathematical Formula ``` EMA = (Price × α) + (Previous_EMA × (1 - α)) Where: - α = 2 / (period + 1) (smoothing factor) - Price is the current price - Previous_EMA is the previous EMA value ``` ### Class Definition ```python class ExponentialMovingAverageState(IndicatorState): def __init__(self, period: int): super().__init__(period) self.multiplier = 2.0 / (period + 1) self.ema_value = 0.0 self.is_first_value = True def update(self, value: float): if self.is_first_value: self.ema_value = value self.is_first_value = False else: self.ema_value = (value * self.multiplier) + (self.ema_value * (1 - self.multiplier)) self.data_count += 1 def get_value(self) -> float: return self.ema_value ``` ### Usage Examples #### Basic Usage ```python # Create 12-period EMA ema_12 = ExponentialMovingAverageState(period=12) # Update with price data for price in price_data: ema_12.update(price) print(f"EMA(12): {ema_12.get_value():.2f}") ``` #### MACD Calculation ```python # MACD uses EMA12 and EMA26 ema_12 = ExponentialMovingAverageState(period=12) ema_26 = ExponentialMovingAverageState(period=26) macd_values = [] for price in price_data: ema_12.update(price) ema_26.update(price) if ema_26.is_ready(): # EMA26 takes longer to be ready macd = ema_12.get_value() - ema_26.get_value() macd_values.append(macd) print(f"MACD: {macd:.4f}") ``` ### Performance Characteristics - **Time Complexity**: O(1) per update - **Space Complexity**: O(1) - **Memory Usage**: ~24 bytes (constant) ## Comparison: SMA vs EMA | Aspect | SMA | EMA | |--------|-----|-----| | **Responsiveness** | Slower | Faster | | **Memory Usage** | O(period) | O(1) | | **Smoothness** | Smoother | More volatile | | **Lag** | Higher lag | Lower lag | | **Noise Filtering** | Better | Moderate | ### When to Use SMA - **Trend Identification**: Better for identifying long-term trends - **Support/Resistance**: More reliable for support and resistance levels - **Noise Reduction**: Better at filtering out market noise - **Memory Constraints**: When memory usage is not a concern ### When to Use EMA - **Quick Signals**: When you need faster response to price changes - **Memory Efficiency**: When memory usage is critical - **Short-term Trading**: Better for short-term trading strategies - **Real-time Systems**: Ideal for high-frequency trading systems ## Advanced Usage Patterns ### Moving Average Crossover Strategy ```python class MovingAverageCrossover: def __init__(self, fast_period: int, slow_period: int): self.fast_ma = MovingAverageState(fast_period) self.slow_ma = MovingAverageState(slow_period) self.previous_fast = 0.0 self.previous_slow = 0.0 def update(self, price: float): self.previous_fast = self.fast_ma.get_value() if self.fast_ma.is_ready() else 0.0 self.previous_slow = self.slow_ma.get_value() if self.slow_ma.is_ready() else 0.0 self.fast_ma.update(price) self.slow_ma.update(price) def get_signal(self) -> str: if not (self.fast_ma.is_ready() and self.slow_ma.is_ready()): return "HOLD" current_fast = self.fast_ma.get_value() current_slow = self.slow_ma.get_value() # Golden Cross: Fast MA crosses above Slow MA if self.previous_fast <= self.previous_slow and current_fast > current_slow: return "BUY" # Death Cross: Fast MA crosses below Slow MA if self.previous_fast >= self.previous_slow and current_fast < current_slow: return "SELL" return "HOLD" # Usage crossover = MovingAverageCrossover(fast_period=10, slow_period=20) for price in price_stream: crossover.update(price) signal = crossover.get_signal() if signal != "HOLD": print(f"Signal: {signal} at price {price}") ``` ### Adaptive Moving Average ```python class AdaptiveMovingAverage: def __init__(self, min_period: int = 5, max_period: int = 50): self.min_period = min_period self.max_period = max_period self.sma_fast = MovingAverageState(min_period) self.sma_slow = MovingAverageState(max_period) self.current_ma = MovingAverageState(min_period) def update(self, price: float): self.sma_fast.update(price) self.sma_slow.update(price) if self.sma_slow.is_ready(): # Calculate volatility-based period volatility = abs(self.sma_fast.get_value() - self.sma_slow.get_value()) normalized_vol = min(volatility / price, 0.1) # Cap at 10% # Adjust period based on volatility adaptive_period = int(self.min_period + (normalized_vol * (self.max_period - self.min_period))) # Update current MA with adaptive period if adaptive_period != self.current_ma.period: self.current_ma = MovingAverageState(adaptive_period) self.current_ma.update(price) def get_value(self) -> float: return self.current_ma.get_value() def is_ready(self) -> bool: return self.current_ma.is_ready() ``` ## Error Handling and Edge Cases ### Robust Implementation ```python class RobustMovingAverage(MovingAverageState): def __init__(self, period: int): if period <= 0: raise ValueError("Period must be positive") super().__init__(period) def update(self, value: float): # Validate input if value is None: self.logger.warning("Received None value, skipping update") return if math.isnan(value) or math.isinf(value): self.logger.warning(f"Received invalid value: {value}, skipping update") return try: super().update(value) except Exception as e: self.logger.error(f"Error updating moving average: {e}") def get_value(self) -> float: try: return super().get_value() except Exception as e: self.logger.error(f"Error getting moving average value: {e}") return 0.0 ``` ### Handling Missing Data ```python def update_with_gap_handling(ma: MovingAverageState, value: float, timestamp: int, last_timestamp: int): """Update moving average with gap handling for missing data.""" # Define maximum acceptable gap (e.g., 5 minutes) max_gap = 5 * 60 * 1000 # 5 minutes in milliseconds if last_timestamp and (timestamp - last_timestamp) > max_gap: # Large gap detected - reset the moving average ma.reset() print(f"Gap detected, resetting moving average") ma.update(value) ``` ## Integration with Strategies ### Strategy Implementation Example ```python class MovingAverageStrategy(IncStrategyBase): def __init__(self, name: str, params: dict = None): super().__init__(name, params) # Initialize moving averages self.sma_short = MovingAverageState(self.params.get('short_period', 10)) self.sma_long = MovingAverageState(self.params.get('long_period', 20)) self.ema_signal = ExponentialMovingAverageState(self.params.get('signal_period', 5)) def _process_aggregated_data(self, timestamp: int, ohlcv: tuple) -> IncStrategySignal: open_price, high, low, close, volume = ohlcv # Update all moving averages self.sma_short.update(close) self.sma_long.update(close) self.ema_signal.update(close) # Wait for all indicators to be ready if not all([self.sma_short.is_ready(), self.sma_long.is_ready(), self.ema_signal.is_ready()]): return IncStrategySignal.HOLD() # Get current values sma_short_val = self.sma_short.get_value() sma_long_val = self.sma_long.get_value() ema_signal_val = self.ema_signal.get_value() # Generate signals if sma_short_val > sma_long_val and close > ema_signal_val: confidence = min(0.9, (sma_short_val - sma_long_val) / sma_long_val * 10) return IncStrategySignal.BUY(confidence=confidence) elif sma_short_val < sma_long_val and close < ema_signal_val: confidence = min(0.9, (sma_long_val - sma_short_val) / sma_long_val * 10) return IncStrategySignal.SELL(confidence=confidence) return IncStrategySignal.HOLD() ``` ## Performance Optimization Tips ### 1. Choose the Right Moving Average ```python # For memory-constrained environments ema = ExponentialMovingAverageState(period=20) # O(1) memory # For better smoothing and trend identification sma = MovingAverageState(period=20) # O(period) memory ``` ### 2. Batch Processing ```python # Process multiple prices efficiently def batch_update_moving_averages(mas: list, prices: list): for price in prices: for ma in mas: ma.update(price) # Return all values at once return [ma.get_value() for ma in mas if ma.is_ready()] ``` ### 3. Avoid Unnecessary Calculations ```python # Cache ready state to avoid repeated checks class CachedMovingAverage(MovingAverageState): def __init__(self, period: int): super().__init__(period) self._is_ready_cached = False def update(self, value: float): super().update(value) if not self._is_ready_cached: self._is_ready_cached = self.data_count >= self.period def is_ready(self) -> bool: return self._is_ready_cached ``` --- *Moving averages are the foundation of many trading strategies. Choose SMA for smoother, more reliable signals, or EMA for faster response to price changes.*