2025-05-28 22:37:53 +08:00

12 KiB
Raw Blame History

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

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

# 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

# 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

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

# 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

# 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

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

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

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

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

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

# 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

# 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

# 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.