"""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