"""Tests for the OrderSimulator.""" from datetime import UTC, datetime from decimal import Decimal from backtester.simulator import OrderSimulator from shared.models import OrderSide, Signal def make_signal( symbol: str, side: OrderSide, price: str, quantity: str, strategy: str = "test", ) -> Signal: return Signal( strategy=strategy, symbol=symbol, side=side, price=Decimal(price), quantity=Decimal(quantity), reason="test", ) # --------------------------------------------------------------------------- # Existing tests (backward compat: defaults slippage=0, fee=0) # --------------------------------------------------------------------------- def test_simulator_initial_balance(): sim = OrderSimulator(Decimal("10000")) assert sim.balance == Decimal("10000") def test_simulator_buy_reduces_balance(): sim = OrderSimulator(Decimal("10000")) signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") result = sim.execute(signal) assert result is True assert sim.balance == Decimal("5000") assert sim.positions["AAPL"] == Decimal("0.1") def test_simulator_sell_increases_balance(): sim = OrderSimulator(Decimal("10000")) buy_signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") sim.execute(buy_signal) balance_after_buy = sim.balance sell_signal = make_signal("AAPL", OrderSide.SELL, "55000", "0.1") result = sim.execute(sell_signal) assert result is True assert sim.balance > balance_after_buy # Profit: sold at 55000, bought at 50000 -> gain 500 assert sim.balance == Decimal("10000") - Decimal("5000") + Decimal("5500") def test_simulator_reject_buy_insufficient_balance(): sim = OrderSimulator(Decimal("100")) signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") result = sim.execute(signal) assert result is False assert sim.balance == Decimal("100") assert sim.positions.get("AAPL", Decimal("0")) == Decimal("0") def test_simulator_trade_history(): sim = OrderSimulator(Decimal("10000")) signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") sim.execute(signal) assert len(sim.trades) == 1 trade = sim.trades[0] assert trade.symbol == "AAPL" assert trade.side == OrderSide.BUY assert trade.price == Decimal("50000") assert trade.quantity == Decimal("0.1") # --------------------------------------------------------------------------- # Slippage tests # --------------------------------------------------------------------------- def test_slippage_on_buy(): """Buy price should increase by slippage_pct.""" sim = OrderSimulator(Decimal("100000"), slippage_pct=0.01) # 1% signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") sim.execute(signal) trade = sim.trades[0] expected_price = Decimal("50000") * Decimal("1.01") # 50500 assert trade.price == expected_price def test_slippage_on_sell(): """Sell price should decrease by slippage_pct.""" sim = OrderSimulator(Decimal("100000"), slippage_pct=0.01) # Buy first (no slippage check here, just need a position) buy = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") sim.execute(buy) # Sell sell = make_signal("AAPL", OrderSide.SELL, "50000", "0.1") sim.execute(sell) trade = sim.trades[1] expected_price = Decimal("50000") * Decimal("0.99") # 49500 assert trade.price == expected_price # --------------------------------------------------------------------------- # Fee tests # --------------------------------------------------------------------------- def test_fee_deducted_from_balance(): """Fees should reduce balance beyond the raw cost.""" fee_pct = 0.001 # 0.1% sim = OrderSimulator(Decimal("100000"), taker_fee_pct=fee_pct) signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") sim.execute(signal) # cost = 50000 * 0.1 = 5000, fee = 5000 * 0.001 = 5 expected_balance = Decimal("100000") - Decimal("5000") - Decimal("5") assert sim.balance == expected_balance assert sim.trades[0].fee == Decimal("5") # --------------------------------------------------------------------------- # Stop-loss / take-profit tests # --------------------------------------------------------------------------- def test_stop_loss_triggers(): """Long position auto-closed when candle_low <= stop_loss.""" sim = OrderSimulator(Decimal("100000")) signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") sim.execute(signal, stop_loss=Decimal("48000")) ts = datetime(2025, 1, 1, tzinfo=UTC) closed = sim.check_stops( candle_high=Decimal("50500"), candle_low=Decimal("47500"), # below stop_loss timestamp=ts, ) assert len(closed) == 1 assert closed[0].side == OrderSide.SELL assert closed[0].price == Decimal("48000") assert len(sim.open_positions) == 0 def test_take_profit_triggers(): """Long position auto-closed when candle_high >= take_profit.""" sim = OrderSimulator(Decimal("100000")) signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") sim.execute(signal, take_profit=Decimal("55000")) ts = datetime(2025, 1, 1, tzinfo=UTC) closed = sim.check_stops( candle_high=Decimal("56000"), # above take_profit candle_low=Decimal("50000"), timestamp=ts, ) assert len(closed) == 1 assert closed[0].side == OrderSide.SELL assert closed[0].price == Decimal("55000") assert len(sim.open_positions) == 0 def test_stop_not_triggered_within_range(): """No auto-close when price stays within stop/tp range.""" sim = OrderSimulator(Decimal("100000")) signal = make_signal("AAPL", OrderSide.BUY, "50000", "0.1") sim.execute(signal, stop_loss=Decimal("48000"), take_profit=Decimal("55000")) ts = datetime(2025, 1, 1, tzinfo=UTC) closed = sim.check_stops( candle_high=Decimal("52000"), candle_low=Decimal("49000"), timestamp=ts, ) assert len(closed) == 0 assert len(sim.open_positions) == 1 # --------------------------------------------------------------------------- # Short selling tests # --------------------------------------------------------------------------- def test_short_sell_allowed(): """Can open short position with allow_short=True.""" sim = OrderSimulator(Decimal("100000"), allow_short=True) signal = make_signal("AAPL", OrderSide.SELL, "50000", "0.1") result = sim.execute(signal) assert result is True assert sim.positions["AAPL"] == Decimal("-0.1") assert len(sim.open_positions) == 1 assert sim.open_positions[0].side == OrderSide.SELL def test_short_sell_rejected(): """Short rejected when allow_short=False (default).""" sim = OrderSimulator(Decimal("100000"), allow_short=False) signal = make_signal("AAPL", OrderSide.SELL, "50000", "0.1") result = sim.execute(signal) assert result is False assert sim.positions.get("AAPL", Decimal("0")) == Decimal("0") def test_short_stop_loss(): """Short position stop-loss triggers on candle high >= stop_loss.""" sim = OrderSimulator(Decimal("100000"), allow_short=True) signal = make_signal("AAPL", OrderSide.SELL, "50000", "0.1") sim.execute(signal, stop_loss=Decimal("52000")) ts = datetime(2025, 1, 1, tzinfo=UTC) closed = sim.check_stops( candle_high=Decimal("53000"), # above stop_loss candle_low=Decimal("49000"), timestamp=ts, ) assert len(closed) == 1 assert closed[0].side == OrderSide.BUY # closing a short = buy assert closed[0].price == Decimal("52000") assert len(sim.open_positions) == 0