615 lines
20 KiB
Markdown
615 lines
20 KiB
Markdown
# Oscillator Indicators
|
||
|
||
## Overview
|
||
|
||
Oscillator indicators help identify overbought and oversold conditions in the market. IncrementalTrader provides RSI (Relative Strength Index) implementations that measure the speed and magnitude of price changes.
|
||
|
||
## RSIState
|
||
|
||
Full RSI implementation using Wilder's smoothing method for accurate calculation.
|
||
|
||
### Features
|
||
- **Wilder's Smoothing**: Uses the traditional RSI calculation method
|
||
- **Overbought/Oversold**: Clear signals for market extremes
|
||
- **Momentum Measurement**: Indicates price momentum strength
|
||
- **Divergence Detection**: Helps identify potential trend reversals
|
||
|
||
### Mathematical Formula
|
||
|
||
```
|
||
RS = Average Gain / Average Loss
|
||
RSI = 100 - (100 / (1 + RS))
|
||
|
||
Where:
|
||
- Average Gain = Wilder's smoothing of positive price changes
|
||
- Average Loss = Wilder's smoothing of negative price changes
|
||
- Wilder's smoothing: ((previous_average × (period - 1)) + current_value) / period
|
||
```
|
||
|
||
### Class Definition
|
||
|
||
```python
|
||
from IncrementalTrader.strategies.indicators import RSIState
|
||
|
||
class RSIState(IndicatorState):
|
||
def __init__(self, period: int):
|
||
super().__init__(period)
|
||
self.gains = []
|
||
self.losses = []
|
||
self.avg_gain = 0.0
|
||
self.avg_loss = 0.0
|
||
self.previous_close = None
|
||
self.is_first_calculation = True
|
||
|
||
def update(self, value: float):
|
||
if self.previous_close is not None:
|
||
change = value - self.previous_close
|
||
gain = max(change, 0.0)
|
||
loss = max(-change, 0.0)
|
||
|
||
if self.is_first_calculation and len(self.gains) >= self.period:
|
||
# Initial calculation using simple average
|
||
self.avg_gain = sum(self.gains[-self.period:]) / self.period
|
||
self.avg_loss = sum(self.losses[-self.period:]) / self.period
|
||
self.is_first_calculation = False
|
||
elif not self.is_first_calculation:
|
||
# Wilder's smoothing
|
||
self.avg_gain = ((self.avg_gain * (self.period - 1)) + gain) / self.period
|
||
self.avg_loss = ((self.avg_loss * (self.period - 1)) + loss) / self.period
|
||
|
||
self.gains.append(gain)
|
||
self.losses.append(loss)
|
||
|
||
# Keep only necessary history
|
||
if len(self.gains) > self.period:
|
||
self.gains.pop(0)
|
||
self.losses.pop(0)
|
||
|
||
self.previous_close = value
|
||
self.data_count += 1
|
||
|
||
def get_value(self) -> float:
|
||
if not self.is_ready() or self.avg_loss == 0:
|
||
return 50.0 # Neutral RSI
|
||
|
||
rs = self.avg_gain / self.avg_loss
|
||
rsi = 100.0 - (100.0 / (1.0 + rs))
|
||
return rsi
|
||
|
||
def is_ready(self) -> bool:
|
||
return self.data_count > self.period and not self.is_first_calculation
|
||
```
|
||
|
||
### Usage Examples
|
||
|
||
#### Basic RSI Usage
|
||
```python
|
||
# Create 14-period RSI
|
||
rsi_14 = RSIState(period=14)
|
||
|
||
# Price data
|
||
prices = [44, 44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.85, 47.25, 47.92, 46.23, 44.18, 46.57, 46.61, 46.5]
|
||
|
||
for price in prices:
|
||
rsi_14.update(price)
|
||
if rsi_14.is_ready():
|
||
rsi_value = rsi_14.get_value()
|
||
print(f"Price: {price:.2f}, RSI(14): {rsi_value:.2f}")
|
||
```
|
||
|
||
#### RSI Trading Signals
|
||
```python
|
||
class RSISignals:
|
||
def __init__(self, period: int = 14, overbought: float = 70.0, oversold: float = 30.0):
|
||
self.rsi = RSIState(period)
|
||
self.overbought = overbought
|
||
self.oversold = oversold
|
||
self.previous_rsi = None
|
||
|
||
def update(self, price: float):
|
||
self.rsi.update(price)
|
||
|
||
def get_signal(self) -> str:
|
||
if not self.rsi.is_ready():
|
||
return "HOLD"
|
||
|
||
current_rsi = self.rsi.get_value()
|
||
|
||
# Oversold bounce signal
|
||
if (self.previous_rsi is not None and
|
||
self.previous_rsi <= self.oversold and
|
||
current_rsi > self.oversold):
|
||
signal = "BUY"
|
||
|
||
# Overbought pullback signal
|
||
elif (self.previous_rsi is not None and
|
||
self.previous_rsi >= self.overbought and
|
||
current_rsi < self.overbought):
|
||
signal = "SELL"
|
||
|
||
else:
|
||
signal = "HOLD"
|
||
|
||
self.previous_rsi = current_rsi
|
||
return signal
|
||
|
||
def get_condition(self) -> str:
|
||
"""Get current market condition based on RSI."""
|
||
if not self.rsi.is_ready():
|
||
return "UNKNOWN"
|
||
|
||
rsi_value = self.rsi.get_value()
|
||
|
||
if rsi_value >= self.overbought:
|
||
return "OVERBOUGHT"
|
||
elif rsi_value <= self.oversold:
|
||
return "OVERSOLD"
|
||
else:
|
||
return "NEUTRAL"
|
||
|
||
# Usage
|
||
rsi_signals = RSISignals(period=14, overbought=70, oversold=30)
|
||
|
||
for price in prices:
|
||
rsi_signals.update(price)
|
||
signal = rsi_signals.get_signal()
|
||
condition = rsi_signals.get_condition()
|
||
|
||
if signal != "HOLD":
|
||
print(f"RSI Signal: {signal}, Condition: {condition}, Price: {price:.2f}")
|
||
```
|
||
|
||
### Performance Characteristics
|
||
- **Time Complexity**: O(1) per update (after initial period)
|
||
- **Space Complexity**: O(period)
|
||
- **Memory Usage**: ~16 bytes per period + constant overhead
|
||
|
||
## SimpleRSIState
|
||
|
||
Simplified RSI implementation using exponential smoothing for memory efficiency.
|
||
|
||
### Features
|
||
- **O(1) Memory**: Constant memory usage regardless of period
|
||
- **Exponential Smoothing**: Uses EMA-based calculation
|
||
- **Fast Computation**: No need to maintain gain/loss history
|
||
- **Approximate RSI**: Close approximation to traditional RSI
|
||
|
||
### Mathematical Formula
|
||
|
||
```
|
||
Gain = max(price_change, 0)
|
||
Loss = max(-price_change, 0)
|
||
|
||
EMA_Gain = EMA(Gain, period)
|
||
EMA_Loss = EMA(Loss, period)
|
||
|
||
RSI = 100 - (100 / (1 + EMA_Gain / EMA_Loss))
|
||
```
|
||
|
||
### Class Definition
|
||
|
||
```python
|
||
class SimpleRSIState(IndicatorState):
|
||
def __init__(self, period: int):
|
||
super().__init__(period)
|
||
self.alpha = 2.0 / (period + 1)
|
||
self.ema_gain = 0.0
|
||
self.ema_loss = 0.0
|
||
self.previous_close = None
|
||
self.is_first_value = True
|
||
|
||
def update(self, value: float):
|
||
if self.previous_close is not None:
|
||
change = value - self.previous_close
|
||
gain = max(change, 0.0)
|
||
loss = max(-change, 0.0)
|
||
|
||
if self.is_first_value:
|
||
self.ema_gain = gain
|
||
self.ema_loss = loss
|
||
self.is_first_value = False
|
||
else:
|
||
self.ema_gain = (gain * self.alpha) + (self.ema_gain * (1 - self.alpha))
|
||
self.ema_loss = (loss * self.alpha) + (self.ema_loss * (1 - self.alpha))
|
||
|
||
self.previous_close = value
|
||
self.data_count += 1
|
||
|
||
def get_value(self) -> float:
|
||
if not self.is_ready() or self.ema_loss == 0:
|
||
return 50.0 # Neutral RSI
|
||
|
||
rs = self.ema_gain / self.ema_loss
|
||
rsi = 100.0 - (100.0 / (1.0 + rs))
|
||
return rsi
|
||
|
||
def is_ready(self) -> bool:
|
||
return self.data_count > 1 and not self.is_first_value
|
||
```
|
||
|
||
### Usage Examples
|
||
|
||
#### Memory-Efficient RSI
|
||
```python
|
||
# Create memory-efficient RSI
|
||
simple_rsi = SimpleRSIState(period=14)
|
||
|
||
# Process large amounts of data with constant memory
|
||
for i, price in enumerate(large_price_dataset):
|
||
simple_rsi.update(price)
|
||
|
||
if i % 1000 == 0 and simple_rsi.is_ready(): # Print every 1000 updates
|
||
print(f"RSI after {i} updates: {simple_rsi.get_value():.2f}")
|
||
```
|
||
|
||
#### RSI Divergence Detection
|
||
```python
|
||
class RSIDivergence:
|
||
def __init__(self, period: int = 14, lookback: int = 20):
|
||
self.rsi = SimpleRSIState(period)
|
||
self.lookback = lookback
|
||
self.price_history = []
|
||
self.rsi_history = []
|
||
|
||
def update(self, price: float):
|
||
self.rsi.update(price)
|
||
|
||
if self.rsi.is_ready():
|
||
self.price_history.append(price)
|
||
self.rsi_history.append(self.rsi.get_value())
|
||
|
||
# Keep only recent history
|
||
if len(self.price_history) > self.lookback:
|
||
self.price_history.pop(0)
|
||
self.rsi_history.pop(0)
|
||
|
||
def detect_bullish_divergence(self) -> bool:
|
||
"""Detect bullish divergence: price makes lower low, RSI makes higher low."""
|
||
if len(self.price_history) < self.lookback:
|
||
return False
|
||
|
||
# Find recent lows
|
||
price_low_idx = self.price_history.index(min(self.price_history[-10:]))
|
||
rsi_low_idx = self.rsi_history.index(min(self.rsi_history[-10:]))
|
||
|
||
# Check for divergence pattern
|
||
if (price_low_idx < len(self.price_history) - 3 and
|
||
rsi_low_idx < len(self.rsi_history) - 3):
|
||
|
||
recent_price_low = min(self.price_history[-3:])
|
||
recent_rsi_low = min(self.rsi_history[-3:])
|
||
|
||
# Bullish divergence: price lower low, RSI higher low
|
||
if (recent_price_low < self.price_history[price_low_idx] and
|
||
recent_rsi_low > self.rsi_history[rsi_low_idx]):
|
||
return True
|
||
|
||
return False
|
||
|
||
def detect_bearish_divergence(self) -> bool:
|
||
"""Detect bearish divergence: price makes higher high, RSI makes lower high."""
|
||
if len(self.price_history) < self.lookback:
|
||
return False
|
||
|
||
# Find recent highs
|
||
price_high_idx = self.price_history.index(max(self.price_history[-10:]))
|
||
rsi_high_idx = self.rsi_history.index(max(self.rsi_history[-10:]))
|
||
|
||
# Check for divergence pattern
|
||
if (price_high_idx < len(self.price_history) - 3 and
|
||
rsi_high_idx < len(self.rsi_history) - 3):
|
||
|
||
recent_price_high = max(self.price_history[-3:])
|
||
recent_rsi_high = max(self.rsi_history[-3:])
|
||
|
||
# Bearish divergence: price higher high, RSI lower high
|
||
if (recent_price_high > self.price_history[price_high_idx] and
|
||
recent_rsi_high < self.rsi_history[rsi_high_idx]):
|
||
return True
|
||
|
||
return False
|
||
|
||
# Usage
|
||
divergence_detector = RSIDivergence(period=14, lookback=20)
|
||
|
||
for price in price_data:
|
||
divergence_detector.update(price)
|
||
|
||
if divergence_detector.detect_bullish_divergence():
|
||
print(f"Bullish RSI divergence detected at price {price:.2f}")
|
||
|
||
if divergence_detector.detect_bearish_divergence():
|
||
print(f"Bearish RSI divergence detected at price {price:.2f}")
|
||
```
|
||
|
||
### Performance Characteristics
|
||
- **Time Complexity**: O(1) per update
|
||
- **Space Complexity**: O(1)
|
||
- **Memory Usage**: ~32 bytes (constant)
|
||
|
||
## Comparison: RSIState vs SimpleRSIState
|
||
|
||
| Aspect | RSIState | SimpleRSIState |
|
||
|--------|----------|----------------|
|
||
| **Memory Usage** | O(period) | O(1) |
|
||
| **Calculation Method** | Wilder's Smoothing | Exponential Smoothing |
|
||
| **Accuracy** | Higher (traditional) | Good (approximation) |
|
||
| **Responsiveness** | Standard | Slightly more responsive |
|
||
| **Historical Compatibility** | Traditional RSI | Modern approximation |
|
||
|
||
### When to Use RSIState
|
||
- **Precise Calculations**: When you need exact traditional RSI values
|
||
- **Backtesting**: For historical analysis and strategy validation
|
||
- **Research**: When studying exact RSI behavior and patterns
|
||
- **Small Periods**: When period is small (< 20) and memory isn't an issue
|
||
|
||
### When to Use SimpleRSIState
|
||
- **Memory Efficiency**: When processing large amounts of data
|
||
- **Real-time Systems**: For high-frequency trading applications
|
||
- **Approximate Analysis**: When close approximation is sufficient
|
||
- **Large Periods**: When using large RSI periods (> 50)
|
||
|
||
## Advanced Usage Patterns
|
||
|
||
### Multi-Timeframe RSI Analysis
|
||
```python
|
||
class MultiTimeframeRSI:
|
||
def __init__(self):
|
||
self.rsi_short = SimpleRSIState(period=7) # Short-term momentum
|
||
self.rsi_medium = SimpleRSIState(period=14) # Standard RSI
|
||
self.rsi_long = SimpleRSIState(period=21) # Long-term momentum
|
||
|
||
def update(self, price: float):
|
||
self.rsi_short.update(price)
|
||
self.rsi_medium.update(price)
|
||
self.rsi_long.update(price)
|
||
|
||
def get_momentum_regime(self) -> str:
|
||
"""Determine current momentum regime."""
|
||
if not all([self.rsi_short.is_ready(), self.rsi_medium.is_ready(), self.rsi_long.is_ready()]):
|
||
return "UNKNOWN"
|
||
|
||
short_rsi = self.rsi_short.get_value()
|
||
medium_rsi = self.rsi_medium.get_value()
|
||
long_rsi = self.rsi_long.get_value()
|
||
|
||
# All timeframes bullish
|
||
if all(rsi > 50 for rsi in [short_rsi, medium_rsi, long_rsi]):
|
||
return "STRONG_BULLISH"
|
||
|
||
# All timeframes bearish
|
||
elif all(rsi < 50 for rsi in [short_rsi, medium_rsi, long_rsi]):
|
||
return "STRONG_BEARISH"
|
||
|
||
# Mixed signals
|
||
elif short_rsi > 50 and medium_rsi > 50:
|
||
return "BULLISH"
|
||
elif short_rsi < 50 and medium_rsi < 50:
|
||
return "BEARISH"
|
||
else:
|
||
return "MIXED"
|
||
|
||
def get_overbought_oversold_consensus(self) -> str:
|
||
"""Get consensus on overbought/oversold conditions."""
|
||
if not all([self.rsi_short.is_ready(), self.rsi_medium.is_ready(), self.rsi_long.is_ready()]):
|
||
return "UNKNOWN"
|
||
|
||
rsi_values = [self.rsi_short.get_value(), self.rsi_medium.get_value(), self.rsi_long.get_value()]
|
||
|
||
overbought_count = sum(1 for rsi in rsi_values if rsi >= 70)
|
||
oversold_count = sum(1 for rsi in rsi_values if rsi <= 30)
|
||
|
||
if overbought_count >= 2:
|
||
return "OVERBOUGHT"
|
||
elif oversold_count >= 2:
|
||
return "OVERSOLD"
|
||
else:
|
||
return "NEUTRAL"
|
||
|
||
# Usage
|
||
multi_rsi = MultiTimeframeRSI()
|
||
|
||
for price in price_data:
|
||
multi_rsi.update(price)
|
||
|
||
regime = multi_rsi.get_momentum_regime()
|
||
consensus = multi_rsi.get_overbought_oversold_consensus()
|
||
|
||
print(f"Price: {price:.2f}, Momentum: {regime}, Condition: {consensus}")
|
||
```
|
||
|
||
### RSI with Dynamic Thresholds
|
||
```python
|
||
class AdaptiveRSI:
|
||
def __init__(self, period: int = 14, lookback: int = 50):
|
||
self.rsi = SimpleRSIState(period)
|
||
self.lookback = lookback
|
||
self.rsi_history = []
|
||
|
||
def update(self, price: float):
|
||
self.rsi.update(price)
|
||
|
||
if self.rsi.is_ready():
|
||
self.rsi_history.append(self.rsi.get_value())
|
||
|
||
# Keep only recent history
|
||
if len(self.rsi_history) > self.lookback:
|
||
self.rsi_history.pop(0)
|
||
|
||
def get_adaptive_thresholds(self) -> tuple:
|
||
"""Calculate adaptive overbought/oversold thresholds."""
|
||
if len(self.rsi_history) < 20:
|
||
return 70.0, 30.0 # Default thresholds
|
||
|
||
# Calculate percentiles for adaptive thresholds
|
||
sorted_rsi = sorted(self.rsi_history)
|
||
|
||
# Use 80th and 20th percentiles as adaptive thresholds
|
||
overbought_threshold = sorted_rsi[int(len(sorted_rsi) * 0.8)]
|
||
oversold_threshold = sorted_rsi[int(len(sorted_rsi) * 0.2)]
|
||
|
||
# Ensure minimum separation
|
||
if overbought_threshold - oversold_threshold < 20:
|
||
mid = (overbought_threshold + oversold_threshold) / 2
|
||
overbought_threshold = mid + 10
|
||
oversold_threshold = mid - 10
|
||
|
||
return overbought_threshold, oversold_threshold
|
||
|
||
def get_adaptive_signal(self) -> str:
|
||
"""Get signal using adaptive thresholds."""
|
||
if not self.rsi.is_ready() or len(self.rsi_history) < 2:
|
||
return "HOLD"
|
||
|
||
current_rsi = self.rsi.get_value()
|
||
previous_rsi = self.rsi_history[-2]
|
||
|
||
overbought, oversold = self.get_adaptive_thresholds()
|
||
|
||
# Adaptive oversold bounce
|
||
if previous_rsi <= oversold and current_rsi > oversold:
|
||
return "BUY"
|
||
|
||
# Adaptive overbought pullback
|
||
elif previous_rsi >= overbought and current_rsi < overbought:
|
||
return "SELL"
|
||
|
||
return "HOLD"
|
||
|
||
# Usage
|
||
adaptive_rsi = AdaptiveRSI(period=14, lookback=50)
|
||
|
||
for price in price_data:
|
||
adaptive_rsi.update(price)
|
||
|
||
signal = adaptive_rsi.get_adaptive_signal()
|
||
overbought, oversold = adaptive_rsi.get_adaptive_thresholds()
|
||
|
||
if signal != "HOLD":
|
||
print(f"Adaptive RSI Signal: {signal}, Thresholds: OB={overbought:.1f}, OS={oversold:.1f}")
|
||
```
|
||
|
||
## Integration with Strategies
|
||
|
||
### RSI Mean Reversion Strategy
|
||
```python
|
||
class RSIMeanReversionStrategy(IncStrategyBase):
|
||
def __init__(self, name: str, params: dict = None):
|
||
super().__init__(name, params)
|
||
|
||
# Initialize RSI
|
||
self.rsi = RSIState(self.params.get('rsi_period', 14))
|
||
|
||
# RSI parameters
|
||
self.overbought = self.params.get('overbought', 70.0)
|
||
self.oversold = self.params.get('oversold', 30.0)
|
||
self.exit_neutral = self.params.get('exit_neutral', 50.0)
|
||
|
||
# State tracking
|
||
self.previous_rsi = None
|
||
self.position_type = None
|
||
|
||
def _process_aggregated_data(self, timestamp: int, ohlcv: tuple) -> IncStrategySignal:
|
||
open_price, high, low, close, volume = ohlcv
|
||
|
||
# Update RSI
|
||
self.rsi.update(close)
|
||
|
||
# Wait for RSI to be ready
|
||
if not self.rsi.is_ready():
|
||
return IncStrategySignal.HOLD()
|
||
|
||
current_rsi = self.rsi.get_value()
|
||
|
||
# Entry signals
|
||
if self.previous_rsi is not None:
|
||
# Oversold bounce (mean reversion up)
|
||
if (self.previous_rsi <= self.oversold and
|
||
current_rsi > self.oversold and
|
||
self.position_type != "LONG"):
|
||
|
||
confidence = min(0.9, (self.oversold - self.previous_rsi) / 20.0)
|
||
self.position_type = "LONG"
|
||
|
||
return IncStrategySignal.BUY(
|
||
confidence=confidence,
|
||
metadata={
|
||
'rsi': current_rsi,
|
||
'previous_rsi': self.previous_rsi,
|
||
'signal_type': 'oversold_bounce'
|
||
}
|
||
)
|
||
|
||
# Overbought pullback (mean reversion down)
|
||
elif (self.previous_rsi >= self.overbought and
|
||
current_rsi < self.overbought and
|
||
self.position_type != "SHORT"):
|
||
|
||
confidence = min(0.9, (self.previous_rsi - self.overbought) / 20.0)
|
||
self.position_type = "SHORT"
|
||
|
||
return IncStrategySignal.SELL(
|
||
confidence=confidence,
|
||
metadata={
|
||
'rsi': current_rsi,
|
||
'previous_rsi': self.previous_rsi,
|
||
'signal_type': 'overbought_pullback'
|
||
}
|
||
)
|
||
|
||
# Exit signals (return to neutral)
|
||
elif (self.position_type == "LONG" and current_rsi >= self.exit_neutral):
|
||
self.position_type = None
|
||
return IncStrategySignal.SELL(confidence=0.5, metadata={'signal_type': 'exit_long'})
|
||
|
||
elif (self.position_type == "SHORT" and current_rsi <= self.exit_neutral):
|
||
self.position_type = None
|
||
return IncStrategySignal.BUY(confidence=0.5, metadata={'signal_type': 'exit_short'})
|
||
|
||
self.previous_rsi = current_rsi
|
||
return IncStrategySignal.HOLD()
|
||
```
|
||
|
||
## Performance Optimization Tips
|
||
|
||
### 1. Choose the Right RSI Implementation
|
||
```python
|
||
# For memory-constrained environments
|
||
rsi = SimpleRSIState(period=14) # O(1) memory
|
||
|
||
# For precise traditional RSI
|
||
rsi = RSIState(period=14) # O(period) memory
|
||
```
|
||
|
||
### 2. Batch Processing for Multiple RSIs
|
||
```python
|
||
def update_multiple_rsis(rsis: list, price: float):
|
||
"""Efficiently update multiple RSI indicators."""
|
||
for rsi in rsis:
|
||
rsi.update(price)
|
||
|
||
return [rsi.get_value() for rsi in rsis if rsi.is_ready()]
|
||
```
|
||
|
||
### 3. Cache RSI Values for Complex Calculations
|
||
```python
|
||
class CachedRSI:
|
||
def __init__(self, period: int):
|
||
self.rsi = SimpleRSIState(period)
|
||
self._cached_value = 50.0
|
||
self._cache_valid = False
|
||
|
||
def update(self, price: float):
|
||
self.rsi.update(price)
|
||
self._cache_valid = False
|
||
|
||
def get_value(self) -> float:
|
||
if not self._cache_valid:
|
||
self._cached_value = self.rsi.get_value()
|
||
self._cache_valid = True
|
||
return self._cached_value
|
||
```
|
||
|
||
---
|
||
|
||
*RSI indicators are essential for identifying momentum and overbought/oversold conditions. Use RSIState for traditional analysis or SimpleRSIState for memory efficiency in high-frequency applications.* |