"""Tests for the Grid strategy.""" from datetime import UTC, datetime from decimal import Decimal from strategies.grid_strategy import GridStrategy from shared.models import Candle, OrderSide def make_candle(close: float) -> Candle: return Candle( symbol="AAPL", timeframe="1m", open_time=datetime(2024, 1, 1, tzinfo=UTC), open=Decimal(str(close)), high=Decimal(str(close)), low=Decimal(str(close)), close=Decimal(str(close)), volume=Decimal("1.0"), ) def _configured_strategy() -> GridStrategy: strategy = GridStrategy() strategy.configure( { "lower_price": 48000, "upper_price": 52000, "grid_count": 5, "quantity": "0.01", } ) return strategy def test_grid_strategy_buy_at_lower_grid(): strategy = _configured_strategy() # First candle: establish zone at upper area strategy.on_candle(make_candle(51500)) # Second candle: price drops to lower zone → BUY signal = strategy.on_candle(make_candle(48100)) assert signal is not None assert signal.side == OrderSide.BUY def test_grid_strategy_sell_at_upper_grid(): strategy = _configured_strategy() # First candle: establish zone at lower area strategy.on_candle(make_candle(48100)) # Second candle: price rises to upper zone → SELL signal = strategy.on_candle(make_candle(51900)) assert signal is not None assert signal.side == OrderSide.SELL def test_grid_strategy_no_signal_in_same_zone(): strategy = _configured_strategy() # Both candles in approximately the same zone strategy.on_candle(make_candle(50000)) signal = strategy.on_candle(make_candle(50100)) assert signal is None def test_grid_exit_on_trend_break(): """Price drops well below grid range → SELL signal emitted.""" strategy = _configured_strategy() # Grid range is 48000-52000, exit_threshold_pct defaults to 5% # Lower bound = 48000 * 0.95 = 45600 # Establish a zone first strategy.on_candle(make_candle(50000)) # Price drops far below the grid range signal = strategy.on_candle(make_candle(45000)) assert signal is not None assert signal.side == OrderSide.SELL assert "broke out of range" in signal.reason def test_grid_no_signal_while_out_of_range(): """After exit signal, no more grid signals until price returns to range.""" strategy = _configured_strategy() # Establish a zone strategy.on_candle(make_candle(50000)) # First out-of-range candle → SELL exit signal signal = strategy.on_candle(make_candle(45000)) assert signal is not None assert signal.side == OrderSide.SELL # Subsequent out-of-range candles → no signals signal = strategy.on_candle(make_candle(44000)) assert signal is None signal = strategy.on_candle(make_candle(43000)) assert signal is None # Price returns to grid range → grid signals resume strategy.on_candle(make_candle(50000)) signal = strategy.on_candle(make_candle(48100)) assert signal is not None assert signal.side == OrderSide.BUY