"""Tests for OrderExecutor.""" from decimal import Decimal from unittest.mock import AsyncMock, MagicMock import pytest from shared.models import OrderSide, OrderStatus, Signal from order_executor.executor import OrderExecutor from order_executor.risk_manager import RiskCheckResult, RiskManager def make_signal(side: OrderSide = OrderSide.BUY, price: str = "100", quantity: str = "1") -> Signal: return Signal( strategy="test", symbol="BTC/USDT", side=side, price=Decimal(price), quantity=Decimal(quantity), reason="test", ) def make_mock_exchange(free_usdt: float = 10000.0) -> AsyncMock: exchange = AsyncMock() exchange.fetch_balance.return_value = {"free": {"USDT": free_usdt}} exchange.create_order = AsyncMock(return_value={"id": "exchange-order-123"}) return exchange def make_mock_risk_manager(allowed: bool = True, reason: str = "OK") -> MagicMock: rm = MagicMock(spec=RiskManager) rm.check.return_value = RiskCheckResult(allowed=allowed, reason=reason) return rm def make_mock_broker() -> AsyncMock: broker = AsyncMock() broker.publish = AsyncMock() return broker def make_mock_db() -> AsyncMock: db = AsyncMock() db.insert_order = AsyncMock() return db @pytest.mark.asyncio async def test_executor_places_order_when_risk_passes(): """When risk check passes, create_order is called and order status is FILLED.""" exchange = make_mock_exchange() risk_manager = make_mock_risk_manager(allowed=True) broker = make_mock_broker() db = make_mock_db() executor = OrderExecutor( exchange=exchange, risk_manager=risk_manager, broker=broker, db=db, notifier=AsyncMock(), dry_run=False, ) signal = make_signal() order = await executor.execute(signal) assert order is not None assert order.status == OrderStatus.FILLED exchange.create_order.assert_called_once() db.insert_order.assert_called_once_with(order) broker.publish.assert_called_once() @pytest.mark.asyncio async def test_executor_rejects_when_risk_fails(): """When risk check fails, create_order is not called and None is returned.""" exchange = make_mock_exchange() risk_manager = make_mock_risk_manager(allowed=False, reason="Position size exceeded") broker = make_mock_broker() db = make_mock_db() executor = OrderExecutor( exchange=exchange, risk_manager=risk_manager, broker=broker, db=db, notifier=AsyncMock(), dry_run=False, ) signal = make_signal() order = await executor.execute(signal) assert order is None exchange.create_order.assert_not_called() db.insert_order.assert_not_called() broker.publish.assert_not_called() @pytest.mark.asyncio async def test_executor_dry_run_does_not_call_exchange(): """In dry-run mode, risk passes, order is FILLED, but exchange.create_order is NOT called.""" exchange = make_mock_exchange() risk_manager = make_mock_risk_manager(allowed=True) broker = make_mock_broker() db = make_mock_db() executor = OrderExecutor( exchange=exchange, risk_manager=risk_manager, broker=broker, db=db, notifier=AsyncMock(), dry_run=True, ) signal = make_signal() order = await executor.execute(signal) assert order is not None assert order.status == OrderStatus.FILLED exchange.create_order.assert_not_called() db.insert_order.assert_called_once_with(order) broker.publish.assert_called_once()