"""Tests for the VWAP strategy.""" from datetime import datetime, timezone, timedelta from decimal import Decimal from shared.models import Candle, OrderSide from strategies.vwap_strategy import VwapStrategy def make_candle( close: float, high: float | None = None, low: float | None = None, volume: float = 1.0, open_time: datetime | None = None, ) -> Candle: if high is None: high = close if low is None: low = close if open_time is None: open_time = datetime(2024, 1, 1, tzinfo=timezone.utc) return Candle( symbol="BTC/USDT", timeframe="1m", open_time=open_time, open=Decimal(str(close)), high=Decimal(str(high)), low=Decimal(str(low)), close=Decimal(str(close)), volume=Decimal(str(volume)), ) def _configured_strategy() -> VwapStrategy: strategy = VwapStrategy() strategy.configure({"deviation_threshold": 0.01, "quantity": "0.01"}) return strategy def test_vwap_warmup_period(): strategy = VwapStrategy() assert strategy.warmup_period == 30 def test_vwap_no_signal_insufficient_data(): strategy = _configured_strategy() # Feed fewer candles than warmup_period for _ in range(29): signal = strategy.on_candle(make_candle(100.0)) assert signal is None def test_vwap_buy_signal_below_vwap_recovery(): strategy = _configured_strategy() # Build VWAP around 100 with 30 candles (satisfy warmup) for _ in range(30): strategy.on_candle(make_candle(100.0, high=101.0, low=99.0)) # Drop price well below VWAP to trigger _was_below_vwap for _ in range(3): strategy.on_candle(make_candle(95.0, high=96.0, low=94.0)) # Recover back to VWAP (close ~100, deviation within threshold) signal = strategy.on_candle(make_candle(100.0, high=101.0, low=99.0)) assert signal is not None assert signal.side == OrderSide.BUY assert "VWAP" in signal.reason def test_vwap_sell_signal_above_vwap_recovery(): strategy = _configured_strategy() # Build VWAP around 100 with 30 candles (satisfy warmup) for _ in range(30): strategy.on_candle(make_candle(100.0, high=101.0, low=99.0)) # Rise price well above VWAP to trigger _was_above_vwap for _ in range(3): strategy.on_candle(make_candle(105.0, high=106.0, low=104.0)) # Recover back to VWAP (close ~100, deviation within threshold) signal = strategy.on_candle(make_candle(100.0, high=101.0, low=99.0)) assert signal is not None assert signal.side == OrderSide.SELL assert "VWAP" in signal.reason def test_vwap_reset_clears_state(): strategy = _configured_strategy() # Build some state for _ in range(35): strategy.on_candle(make_candle(100.0)) strategy.reset() assert strategy._cumulative_tp_vol == 0.0 assert strategy._cumulative_vol == 0.0 assert strategy._candle_count == 0 assert strategy._was_below_vwap is False assert strategy._was_above_vwap is False assert strategy._current_date is None assert len(strategy._tp_values) == 0 assert len(strategy._vwap_values) == 0 def test_vwap_daily_reset(): """Candles from two different dates cause VWAP to reset.""" strategy = _configured_strategy() day1 = datetime(2024, 1, 1, tzinfo=timezone.utc) day2 = datetime(2024, 1, 2, tzinfo=timezone.utc) # Feed 35 candles on day 1 to build VWAP state for i in range(35): strategy.on_candle(make_candle(100.0, high=101.0, low=99.0, open_time=day1)) # Verify state is built up assert strategy._candle_count == 35 assert strategy._cumulative_vol > 0 assert strategy._current_date == "2024-01-01" # Feed first candle of day 2 — should reset strategy.on_candle(make_candle(100.0, high=101.0, low=99.0, open_time=day2)) # After reset, candle_count should be 1 (the new candle) assert strategy._candle_count == 1 assert strategy._current_date == "2024-01-02" def test_vwap_reset_clears_date(): """Verify reset() clears _current_date and deviation band state.""" strategy = _configured_strategy() for _ in range(35): strategy.on_candle(make_candle(100.0)) assert strategy._current_date is not None strategy.reset() assert strategy._current_date is None assert len(strategy._tp_values) == 0 assert len(strategy._vwap_values) == 0