summaryrefslogtreecommitdiff
path: root/services/strategy-engine/tests
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 09:19:31 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 09:19:31 +0900
commit3a256abb8c04ef07f125b0fb41f8f9090d97b136 (patch)
tree4ae95445bff10b2e74b589fd55a0015c44d66cb5 /services/strategy-engine/tests
parentda6c9598f92057e2fcbb206aa7466b6997a455f3 (diff)
feat(strategy): add RSI divergence detection and MACD signal-line crossover
Diffstat (limited to 'services/strategy-engine/tests')
-rw-r--r--services/strategy-engine/tests/test_macd_strategy.py58
-rw-r--r--services/strategy-engine/tests/test_rsi_strategy.py57
2 files changed, 115 insertions, 0 deletions
diff --git a/services/strategy-engine/tests/test_macd_strategy.py b/services/strategy-engine/tests/test_macd_strategy.py
index 9931b43..cd24ee0 100644
--- a/services/strategy-engine/tests/test_macd_strategy.py
+++ b/services/strategy-engine/tests/test_macd_strategy.py
@@ -78,3 +78,61 @@ def test_macd_reset_clears_state():
s.reset()
assert len(s._closes) == 0
assert s._prev_histogram is None
+ assert s._prev_macd is None
+ assert s._prev_signal is None
+
+
+def test_macd_signal_line_crossover():
+ """Test that MACD signal-line crossover generates signals."""
+ s = _make_strategy()
+ # Declining then rising prices should produce a signal-line bullish crossover
+ prices = [100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88]
+ prices += [89, 91, 94, 98, 103, 109, 116, 124, 133, 143]
+ signals = []
+ for p in prices:
+ result = s.on_candle(_candle(float(p)))
+ if result is not None:
+ signals.append(result)
+
+ buy_signals = [sig for sig in signals if sig.side == OrderSide.BUY]
+ assert len(buy_signals) > 0, "Expected at least one BUY signal"
+ # Check that at least one is a signal-line crossover or histogram crossover
+ all_reasons = [sig.reason for sig in buy_signals]
+ assert any("crossover" in r for r in all_reasons), f"Expected crossover signal, got: {all_reasons}"
+
+
+def test_macd_conviction_varies_with_distance():
+ """Test that conviction varies based on MACD distance from zero line."""
+ s1 = _make_strategy()
+ s2 = _make_strategy()
+
+ # Small price movements -> MACD near zero -> lower conviction
+ small_prices = [100, 99.5, 99, 98.5, 98, 97.5, 97, 96.5, 96, 95.5, 95, 94.5, 94]
+ small_prices += [94.5, 95, 95.5, 96, 96.5, 97, 97.5, 98, 98.5, 99]
+ small_signals = []
+ for p in small_prices:
+ result = s1.on_candle(_candle(float(p)))
+ if result is not None:
+ small_signals.append(result)
+
+ # Large price movements -> MACD far from zero -> higher conviction
+ large_prices = [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40]
+ large_prices += [45, 55, 70, 90, 115, 145, 180, 220, 265, 315]
+ large_signals = []
+ for p in large_prices:
+ result = s2.on_candle(_candle(float(p)))
+ if result is not None:
+ large_signals.append(result)
+
+ # Both should produce signals
+ assert len(small_signals) > 0, "Expected signals from small movements"
+ assert len(large_signals) > 0, "Expected signals from large movements"
+
+ # The large-movement signals should generally have higher conviction
+ # (or at least different conviction, since distance from zero affects it)
+ small_conv = small_signals[-1].conviction
+ large_conv = large_signals[-1].conviction
+ # Large movements should produce conviction >= small movements
+ assert large_conv >= small_conv, (
+ f"Expected large movement conviction ({large_conv}) >= small ({small_conv})"
+ )
diff --git a/services/strategy-engine/tests/test_rsi_strategy.py b/services/strategy-engine/tests/test_rsi_strategy.py
index 2a2f4e7..b2aecc9 100644
--- a/services/strategy-engine/tests/test_rsi_strategy.py
+++ b/services/strategy-engine/tests/test_rsi_strategy.py
@@ -43,3 +43,60 @@ def test_rsi_strategy_buy_signal_on_oversold():
# 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