404 lines
12 KiB
Markdown
404 lines
12 KiB
Markdown
# 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.* |