"""Tests for the BacktestEngine.""" from datetime import datetime, timezone from decimal import Decimal from unittest.mock import MagicMock import pytest from shared.models import Candle, Signal, OrderSide from backtester.engine import BacktestEngine, BacktestResult 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