summaryrefslogtreecommitdiff
path: root/services/strategy-engine/strategies/rsi_strategy.py
diff options
context:
space:
mode:
Diffstat (limited to 'services/strategy-engine/strategies/rsi_strategy.py')
-rw-r--r--services/strategy-engine/strategies/rsi_strategy.py86
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