summaryrefslogtreecommitdiff
path: root/services/order-executor/tests/test_executor.py
diff options
context:
space:
mode:
Diffstat (limited to 'services/order-executor/tests/test_executor.py')
-rw-r--r--services/order-executor/tests/test_executor.py122
1 files changed, 122 insertions, 0 deletions
diff --git a/services/order-executor/tests/test_executor.py b/services/order-executor/tests/test_executor.py
new file mode 100644
index 0000000..5b18992
--- /dev/null
+++ b/services/order-executor/tests/test_executor.py
@@ -0,0 +1,122 @@
+"""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,
+ 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,
+ 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,
+ 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()