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