12 KiB
12 KiB
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.