diff options
Diffstat (limited to 'services/strategy-engine/tests/test_bollinger_strategy.py')
| -rw-r--r-- | services/strategy-engine/tests/test_bollinger_strategy.py | 101 |
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 |
