summaryrefslogtreecommitdiff
path: root/services/backtester/tests
diff options
context:
space:
mode:
Diffstat (limited to 'services/backtester/tests')
-rw-r--r--services/backtester/tests/test_engine.py8
-rw-r--r--services/backtester/tests/test_simulator.py157
2 files changed, 160 insertions, 5 deletions
diff --git a/services/backtester/tests/test_engine.py b/services/backtester/tests/test_engine.py
index 6962477..003e951 100644
--- a/services/backtester/tests/test_engine.py
+++ b/services/backtester/tests/test_engine.py
@@ -44,7 +44,9 @@ def test_backtest_engine_runs_strategy_over_candles():
strategy.on_candle.return_value = None
candles = make_candles([50000.0, 51000.0, 52000.0])
- engine = BacktestEngine(strategy, Decimal("10000"))
+ engine = BacktestEngine(
+ strategy, Decimal("10000"), slippage_pct=0.0, taker_fee_pct=0.0
+ )
result = engine.run(candles)
assert strategy.on_candle.call_count == 3
@@ -62,7 +64,9 @@ def test_backtest_engine_executes_signals():
strategy.on_candle.side_effect = [buy_signal, None, sell_signal]
candles = make_candles([50000.0, 52000.0, 55000.0])
- engine = BacktestEngine(strategy, Decimal("10000"))
+ engine = BacktestEngine(
+ strategy, Decimal("10000"), slippage_pct=0.0, taker_fee_pct=0.0
+ )
result = engine.run(candles)
assert result.total_trades == 2
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