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