"""Tests for the RSI strategy.""" from datetime import UTC, datetime from decimal import Decimal from strategies.rsi_strategy import RsiStrategy from shared.models import Candle, OrderSide def make_candle(close: float, idx: int = 0) -> Candle: return Candle( symbol="AAPL", timeframe="1m", open_time=datetime(2024, 1, 1, tzinfo=UTC), open=Decimal(str(close)), high=Decimal(str(close)), low=Decimal(str(close)), close=Decimal(str(close)), volume=Decimal("1.0"), ) def test_rsi_strategy_no_signal_insufficient_data(): strategy = RsiStrategy() strategy.configure({}) candle = make_candle(50000.0) result = strategy.on_candle(candle) assert result is None def test_rsi_strategy_buy_signal_on_oversold(): strategy = RsiStrategy() strategy.configure({"period": 14, "oversold": 30, "overbought": 70}) # Feed 20 steadily declining prices to force RSI into oversold territory prices = [50000 - i * 500 for i in range(20)] signal = None for i, price in enumerate(prices): signal = strategy.on_candle(make_candle(price, i)) # We may or may not get a signal depending on RSI calculation; # if a signal is returned, it must be a BUY if signal is not None: assert signal.side == OrderSide.BUY def test_rsi_detects_bullish_divergence(): """Bullish divergence: price makes lower low, RSI makes higher low.""" strategy = RsiStrategy() strategy.configure({"period": 5, "oversold": 20, "overbought": 80}) strategy._filter_enabled = False # Disable filters to test divergence logic only # Sharp consecutive drop to 50 drives RSI near 0 (first swing low). # Big recovery, then gradual decline to 48 (lower price, but RSI > 0 = higher low). prices = [100.0] * 7 prices += [85.0, 70.0, 55.0, 50.0] prices += [55.0, 65.0, 80.0, 95.0, 110.0, 120.0, 130.0, 135.0, 140.0, 142.0, 143.0, 144.0] prices += [142.0, 140.0, 138.0, 135.0, 130.0, 125.0, 120.0, 115.0, 110.0, 105.0] prices += [100.0, 95.0, 90.0, 85.0, 80.0, 75.0, 70.0, 65.0, 60.0, 55.0, 50.0, 48.0] prices += [52.0, 58.0] signals = [] for p in prices: result = strategy.on_candle(make_candle(p)) if result is not None: signals.append(result) divergence_signals = [s for s in signals if "divergence" in s.reason] assert len(divergence_signals) > 0, "Expected at least one bullish divergence signal" assert divergence_signals[0].side == OrderSide.BUY assert divergence_signals[0].conviction == 0.9 assert "bullish divergence" in divergence_signals[0].reason def test_rsi_detects_bearish_divergence(): """Bearish divergence: price makes higher high, RSI makes lower high.""" strategy = RsiStrategy() strategy.configure({"period": 5, "oversold": 20, "overbought": 80}) strategy._filter_enabled = False # Disable filters to test divergence logic only # Sharp consecutive rise to 160 drives RSI very high (first swing high). # Deep pullback, then rise to 162 (higher price) but with a dip right before # the peak to dampen RSI (lower high). prices = [100.0] * 7 prices += [110.0, 120.0, 130.0, 140.0, 150.0, 160.0] prices += [155.0, 145.0, 130.0, 115.0, 100.0, 90.0, 80.0] prices += [90.0, 100.0, 110.0, 120.0, 130.0, 140.0, 150.0] prices += [145.0, 162.0] prices += [155.0, 148.0] signals = [] for p in prices: result = strategy.on_candle(make_candle(p)) if result is not None: signals.append(result) divergence_signals = [s for s in signals if "divergence" in s.reason] assert len(divergence_signals) > 0, "Expected at least one bearish divergence signal" assert divergence_signals[0].side == OrderSide.SELL assert divergence_signals[0].conviction == 0.9 assert "bearish divergence" in divergence_signals[0].reason