2025-05-28 22:37:53 +08:00

20 KiB
Raw Blame History

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.