From 7bfdf07dccb09a613f66f63d1513b80f167a3881 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:24:32 +0900 Subject: feat(backtester): add slippage, fees, stop-loss/take-profit, and short selling --- services/backtester/tests/test_simulator.py | 157 +++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 3 deletions(-) (limited to 'services/backtester/tests/test_simulator.py') diff --git a/services/backtester/tests/test_simulator.py b/services/backtester/tests/test_simulator.py index e8c80ec..a407c21 100644 --- a/services/backtester/tests/test_simulator.py +++ b/services/backtester/tests/test_simulator.py @@ -1,9 +1,9 @@ """Tests for the OrderSimulator.""" +from datetime import datetime, timezone from decimal import Decimal - -from shared.models import Signal, OrderSide +from shared.models import OrderSide, Signal from backtester.simulator import OrderSimulator @@ -24,6 +24,11 @@ def make_signal( ) +# --------------------------------------------------------------------------- +# Existing tests (backward compat: defaults slippage=0, fee=0) +# --------------------------------------------------------------------------- + + def test_simulator_initial_balance(): sim = OrderSimulator(Decimal("10000")) assert sim.balance == Decimal("10000") @@ -48,7 +53,7 @@ def test_simulator_sell_increases_balance(): result = sim.execute(sell_signal) assert result is True assert sim.balance > balance_after_buy - # Profit: sold at 55000, bought at 50000 → gain 500 + # Profit: sold at 55000, bought at 50000 -> gain 500 assert sim.balance == Decimal("10000") - Decimal("5000") + Decimal("5500") @@ -71,3 +76,149 @@ def test_simulator_trade_history(): 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("BTCUSDT", 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("BTCUSDT", OrderSide.BUY, "50000", "0.1") + sim.execute(buy) + # Sell + sell = make_signal("BTCUSDT", 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("BTCUSDT", 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("BTCUSDT", OrderSide.BUY, "50000", "0.1") + sim.execute(signal, stop_loss=Decimal("48000")) + + ts = datetime(2025, 1, 1, tzinfo=timezone.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("BTCUSDT", OrderSide.BUY, "50000", "0.1") + sim.execute(signal, take_profit=Decimal("55000")) + + ts = datetime(2025, 1, 1, tzinfo=timezone.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("BTCUSDT", OrderSide.BUY, "50000", "0.1") + sim.execute(signal, stop_loss=Decimal("48000"), take_profit=Decimal("55000")) + + ts = datetime(2025, 1, 1, tzinfo=timezone.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("BTCUSDT", OrderSide.SELL, "50000", "0.1") + result = sim.execute(signal) + assert result is True + assert sim.positions["BTCUSDT"] == 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("BTCUSDT", OrderSide.SELL, "50000", "0.1") + result = sim.execute(signal) + assert result is False + assert sim.positions.get("BTCUSDT", 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("BTCUSDT", OrderSide.SELL, "50000", "0.1") + sim.execute(signal, stop_loss=Decimal("52000")) + + ts = datetime(2025, 1, 1, tzinfo=timezone.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 -- cgit v1.2.3