"""Tests for the Volume Profile strategy.""" from datetime import datetime, timezone from decimal import Decimal from shared.models import Candle, OrderSide from strategies.volume_profile_strategy import VolumeProfileStrategy def make_candle(close: float, volume: float = 1.0) -> Candle: return Candle( symbol="AAPL", 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(str(volume)), ) def test_volume_profile_warmup_period(): strategy = VolumeProfileStrategy() strategy.configure({"lookback_period": 10, "num_bins": 5}) assert strategy.warmup_period == 10 def test_volume_profile_no_signal_insufficient_data(): strategy = VolumeProfileStrategy() strategy.configure({"lookback_period": 10, "num_bins": 5}) # Feed fewer candles than lookback_period for _ in range(5): result = strategy.on_candle(make_candle(100.0, 10.0)) assert result is None def test_volume_profile_buy_at_value_area_low(): """Concentrate volume around 95-105, price drops to 88, bounces back to 99.""" strategy = VolumeProfileStrategy() strategy.configure( { "lookback_period": 10, "num_bins": 5, "value_area_pct": 0.7, "quantity": "0.01", } ) # Build profile: 10 candles with volume concentrated around 95-105 profile_data = [ (95, 50), (97, 50), (99, 100), (100, 100), (101, 100), (103, 50), (105, 50), (100, 100), (99, 100), (101, 50), ] for price, vol in profile_data: strategy.on_candle(make_candle(price, vol)) # Price drops below value area low strategy.on_candle(make_candle(88.0, 1.0)) # Price bounces back into value area (between va_low and poc) signal = strategy.on_candle(make_candle(99.0, 1.0)) assert signal is not None assert signal.side == OrderSide.BUY def test_volume_profile_sell_at_value_area_high(): """Concentrate volume around 95-105, price rises to 112, pulls back to 101.""" strategy = VolumeProfileStrategy() strategy.configure( { "lookback_period": 10, "num_bins": 5, "value_area_pct": 0.7, "quantity": "0.01", } ) # Build profile: 10 candles with volume concentrated around 95-105 profile_data = [ (95, 50), (97, 50), (99, 100), (100, 100), (101, 100), (103, 50), (105, 50), (100, 100), (99, 100), (101, 50), ] for price, vol in profile_data: strategy.on_candle(make_candle(price, vol)) # Price rises above value area high strategy.on_candle(make_candle(112.0, 1.0)) # Price pulls back into value area (between poc and va_high) signal = strategy.on_candle(make_candle(101.0, 1.0)) assert signal is not None assert signal.side == OrderSide.SELL def test_volume_profile_reset_clears_state(): strategy = VolumeProfileStrategy() strategy.configure({"lookback_period": 10, "num_bins": 5}) # Feed enough candles to establish profile for _ in range(10): strategy.on_candle(make_candle(100.0, 10.0)) strategy.reset() # After reset, should not have enough data result = strategy.on_candle(make_candle(100.0, 10.0)) assert result is None def test_volume_profile_hvn_detection(): """Feed clustered volume at specific price levels to produce HVN nodes.""" strategy = VolumeProfileStrategy() strategy.configure({"lookback_period": 20, "num_bins": 10, "value_area_pct": 0.7}) # Create a profile with very high volume at price ~100 and low volume elsewhere # Prices range from 90 to 110, heavy volume concentrated at 100 candles_data = [] # Low volume at extremes for p in [90, 91, 92, 109, 110]: candles_data.append((p, 1.0)) # Very high volume around 100 for _ in range(15): candles_data.append((100, 100.0)) for price, vol in candles_data: strategy.on_candle(make_candle(price, vol)) # Access the internal method to verify HVN detection result = strategy._compute_value_area() assert result is not None poc, va_low, va_high, hvn_levels, lvn_levels = result # The bin containing price ~100 should have very high volume -> HVN assert len(hvn_levels) > 0 # At least one HVN should be near 100 assert any(abs(h - 100) < 5 for h in hvn_levels) def test_volume_profile_reset_thorough(): """Verify all state is cleared on reset.""" strategy = VolumeProfileStrategy() strategy.configure({"lookback_period": 10, "num_bins": 5}) # Build up state for _ in range(10): strategy.on_candle(make_candle(100.0, 10.0)) # Set below/above VA flags strategy.on_candle(make_candle(50.0, 1.0)) # below VA strategy.on_candle(make_candle(200.0, 1.0)) # above VA strategy.reset() # Verify all state cleared assert len(strategy._candles) == 0 assert strategy._was_below_va is False assert strategy._was_above_va is False # Should not produce signal since no data result = strategy.on_candle(make_candle(100.0, 10.0)) assert result is None