summaryrefslogtreecommitdiff
path: root/services/strategy-engine/tests
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 16:17:04 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 16:17:04 +0900
commitbdffabc630c0cc296fc164d5fa2ca8569626fd7e (patch)
tree563b6ee6960f1105a8aa0840f85f92805fd96a97 /services/strategy-engine/tests
parent49e5baaebf2f9ca1ba7b85a80c3451c5789edde4 (diff)
feat(strategy): add Bollinger Bands strategy
Diffstat (limited to 'services/strategy-engine/tests')
-rw-r--r--services/strategy-engine/tests/test_bollinger_strategy.py101
1 files changed, 101 insertions, 0 deletions
diff --git a/services/strategy-engine/tests/test_bollinger_strategy.py b/services/strategy-engine/tests/test_bollinger_strategy.py
new file mode 100644
index 0000000..b3d17ac
--- /dev/null
+++ b/services/strategy-engine/tests/test_bollinger_strategy.py
@@ -0,0 +1,101 @@
+"""Tests for the Bollinger Bands strategy."""
+from datetime import datetime, timezone
+from decimal import Decimal
+
+import pytest
+
+from shared.models import Candle, OrderSide
+from strategies.bollinger_strategy import BollingerStrategy
+
+
+def make_candle(close: float) -> Candle:
+ return Candle(
+ symbol="BTC/USDT",
+ timeframe="1m",
+ open_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
+ open=Decimal(str(close)),
+ high=Decimal(str(close)),
+ low=Decimal(str(close)),
+ close=Decimal(str(close)),
+ volume=Decimal("1.0"),
+ )
+
+
+def _make_strategy() -> BollingerStrategy:
+ s = BollingerStrategy()
+ s.configure({"period": 5, "num_std": 1.0, "min_bandwidth": 0.0})
+ return s
+
+
+def test_bollinger_warmup_period():
+ strategy = BollingerStrategy()
+ strategy.configure({"period": 20})
+ assert strategy.warmup_period == 20
+
+ strategy.configure({"period": 5})
+ assert strategy.warmup_period == 5
+
+
+def test_bollinger_no_signal_insufficient_data():
+ strategy = _make_strategy()
+ # Feed fewer candles than the period
+ for price in [100.0, 101.0, 102.0, 101.0]:
+ result = strategy.on_candle(make_candle(price))
+ assert result is None
+
+
+def test_bollinger_buy_on_lower_band_recovery():
+ strategy = _make_strategy()
+
+ # Feed stable prices to build up the window
+ for _ in range(5):
+ strategy.on_candle(make_candle(100.0))
+
+ # Drop well below the lower band
+ signal = strategy.on_candle(make_candle(50.0))
+ # No buy yet -- still below lower band
+ assert signal is None
+
+ # Recover back inside the bands
+ signal = strategy.on_candle(make_candle(100.0))
+ assert signal is not None
+ assert signal.side == OrderSide.BUY
+ assert "lower" in signal.reason.lower() or "bollinger" in signal.reason.lower()
+
+
+def test_bollinger_sell_on_upper_band_recovery():
+ strategy = _make_strategy()
+
+ # Feed stable prices to build up the window
+ for _ in range(5):
+ strategy.on_candle(make_candle(100.0))
+
+ # Spike well above the upper band
+ signal = strategy.on_candle(make_candle(150.0))
+ # No sell yet -- still above upper band
+ assert signal is None
+
+ # Recover back inside the bands
+ signal = strategy.on_candle(make_candle(100.0))
+ assert signal is not None
+ assert signal.side == OrderSide.SELL
+ assert "upper" in signal.reason.lower() or "bollinger" in signal.reason.lower()
+
+
+def test_bollinger_reset_clears_state():
+ strategy = _make_strategy()
+
+ # Build some state
+ for _ in range(5):
+ strategy.on_candle(make_candle(100.0))
+ strategy.on_candle(make_candle(50.0)) # penetrate lower band
+
+ strategy.reset()
+
+ # After reset, insufficient data again
+ result = strategy.on_candle(make_candle(100.0))
+ assert result is None
+ # Internal state should be cleared
+ assert len(strategy._closes) == 1
+ assert strategy._was_below_lower is False
+ assert strategy._was_above_upper is False