diff options
Diffstat (limited to 'services/strategy-engine/strategies/rsi_strategy.py')
| -rw-r--r-- | services/strategy-engine/strategies/rsi_strategy.py | 86 |
1 files changed, 86 insertions, 0 deletions
diff --git a/services/strategy-engine/strategies/rsi_strategy.py b/services/strategy-engine/strategies/rsi_strategy.py index 0ec6780..0646d8c 100644 --- a/services/strategy-engine/strategies/rsi_strategy.py +++ b/services/strategy-engine/strategies/rsi_strategy.py @@ -34,6 +34,14 @@ class RsiStrategy(BaseStrategy): self._oversold: float = 30.0 self._overbought: float = 70.0 self._quantity: Decimal = Decimal("0.01") + # Divergence detection state + self._price_lows: deque[float] = deque(maxlen=5) + self._price_highs: deque[float] = deque(maxlen=5) + self._rsi_at_lows: deque[float] = deque(maxlen=5) + self._rsi_at_highs: deque[float] = deque(maxlen=5) + self._prev_close: float | None = None + self._prev_prev_close: float | None = None + self._prev_rsi: float | None = None @property def warmup_period(self) -> int: @@ -65,6 +73,13 @@ class RsiStrategy(BaseStrategy): def reset(self) -> None: self._closes.clear() + self._price_lows.clear() + self._price_highs.clear() + self._rsi_at_lows.clear() + self._rsi_at_highs.clear() + self._prev_close = None + self._prev_prev_close = None + self._prev_rsi = None def _rsi_conviction(self, rsi_value: float) -> float: """Map RSI value to conviction strength (0.0-1.0). @@ -86,14 +101,76 @@ class RsiStrategy(BaseStrategy): self._closes.append(float(candle.close)) if len(self._closes) < self._period + 1: + self._prev_prev_close = self._prev_close + self._prev_close = float(candle.close) return None series = pd.Series(list(self._closes)) rsi_value = _compute_rsi(series, self._period) if rsi_value is None: + self._prev_prev_close = self._prev_close + self._prev_close = float(candle.close) return None + close = float(candle.close) + + # Detect swing points for divergence + if self._prev_close is not None and self._prev_prev_close is not None: + # Swing low: prev_close < both neighbors + if self._prev_close < self._prev_prev_close and self._prev_close < close: + self._price_lows.append(self._prev_close) + self._rsi_at_lows.append( + self._prev_rsi if self._prev_rsi is not None else rsi_value + ) + # Swing high: prev_close > both neighbors + if self._prev_close > self._prev_prev_close and self._prev_close > close: + self._price_highs.append(self._prev_close) + self._rsi_at_highs.append( + self._prev_rsi if self._prev_rsi is not None else rsi_value + ) + + # Check bullish divergence: price lower low, RSI higher low + if len(self._price_lows) >= 2: + if ( + self._price_lows[-1] < self._price_lows[-2] + and self._rsi_at_lows[-1] > self._rsi_at_lows[-2] + ): + signal = Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.BUY, + price=candle.close, + quantity=self._quantity, + conviction=0.9, + reason="RSI bullish divergence", + ) + self._prev_rsi = rsi_value + self._prev_prev_close = self._prev_close + self._prev_close = close + return self._apply_filters(signal) + + # Check bearish divergence: price higher high, RSI lower high + if len(self._price_highs) >= 2: + if ( + self._price_highs[-1] > self._price_highs[-2] + and self._rsi_at_highs[-1] < self._rsi_at_highs[-2] + ): + signal = Signal( + strategy=self.name, + symbol=candle.symbol, + side=OrderSide.SELL, + price=candle.close, + quantity=self._quantity, + conviction=0.9, + reason="RSI bearish divergence", + ) + self._prev_rsi = rsi_value + self._prev_prev_close = self._prev_close + self._prev_close = close + return self._apply_filters(signal) + + # Existing oversold/overbought logic (secondary signals) if rsi_value < self._oversold: signal = Signal( strategy=self.name, @@ -104,6 +181,9 @@ class RsiStrategy(BaseStrategy): conviction=self._rsi_conviction(rsi_value), reason=f"RSI {rsi_value:.2f} below oversold threshold {self._oversold}", ) + self._prev_rsi = rsi_value + self._prev_prev_close = self._prev_close + self._prev_close = close return self._apply_filters(signal) elif rsi_value > self._overbought: signal = Signal( @@ -115,6 +195,12 @@ class RsiStrategy(BaseStrategy): conviction=self._rsi_conviction(rsi_value), reason=f"RSI {rsi_value:.2f} above overbought threshold {self._overbought}", ) + self._prev_rsi = rsi_value + self._prev_prev_close = self._prev_close + self._prev_close = close return self._apply_filters(signal) + self._prev_rsi = rsi_value + self._prev_prev_close = self._prev_close + self._prev_close = close return None |
