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