summaryrefslogtreecommitdiff
path: root/services/backtester/tests/test_engine.py
blob: 6962477b8b92cf9bddf080feec0f5a44366289a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"""Tests for the BacktestEngine."""

from datetime import datetime, timezone
from decimal import Decimal
from unittest.mock import MagicMock


from shared.models import Candle, Signal, OrderSide

from backtester.engine import BacktestEngine


def make_candle(symbol: str, price: float, timeframe: str = "1h") -> Candle:
    return Candle(
        symbol=symbol,
        timeframe=timeframe,
        open_time=datetime.now(timezone.utc),
        open=Decimal(str(price)),
        high=Decimal(str(price * 1.01)),
        low=Decimal(str(price * 0.99)),
        close=Decimal(str(price)),
        volume=Decimal("100"),
    )


def make_candles(prices: list[float], symbol: str = "BTCUSDT") -> list[Candle]:
    return [make_candle(symbol, p) for p in prices]


def make_signal(side: OrderSide, price: str, quantity: str = "0.1") -> Signal:
    return Signal(
        strategy="test",
        symbol="BTCUSDT",
        side=side,
        price=Decimal(price),
        quantity=Decimal(quantity),
        reason="test",
    )


def test_backtest_engine_runs_strategy_over_candles():
    strategy = MagicMock()
    strategy.name = "mock_strategy"
    strategy.on_candle.return_value = None

    candles = make_candles([50000.0, 51000.0, 52000.0])
    engine = BacktestEngine(strategy, Decimal("10000"))
    result = engine.run(candles)

    assert strategy.on_candle.call_count == 3
    assert result.total_trades == 0
    assert result.final_balance == Decimal("10000")
    assert result.strategy_name == "mock_strategy"


def test_backtest_engine_executes_signals():
    buy_signal = make_signal(OrderSide.BUY, "50000", "0.1")
    sell_signal = make_signal(OrderSide.SELL, "55000", "0.1")

    strategy = MagicMock()
    strategy.name = "mock_strategy"
    strategy.on_candle.side_effect = [buy_signal, None, sell_signal]

    candles = make_candles([50000.0, 52000.0, 55000.0])
    engine = BacktestEngine(strategy, Decimal("10000"))
    result = engine.run(candles)

    assert result.total_trades == 2
    # Initial: 10000, bought 0.1 BTC @ 50000 (cost 5000) → balance 5000
    # Sold 0.1 BTC @ 55000 (proceeds 5500) → balance 10500
    expected_final = Decimal("10500")
    assert result.final_balance == expected_final
    expected_profit = Decimal("500")
    assert result.profit == expected_profit