summaryrefslogtreecommitdiff
path: root/services/order-executor/tests
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 15:56:35 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 15:56:35 +0900
commit33b14aaa2344b0fd95d1629627c3d135b24ae102 (patch)
tree90b214758bc3b076baa7711226a1a1be6268e72e /services/order-executor/tests
parent9360f1a800aa29b40399a2f3bfbfcf215a04e279 (diff)
feat: initial trading platform implementation
Binance spot crypto trading platform with microservices architecture: - shared: Pydantic models, Redis Streams broker, asyncpg DB layer - data-collector: Binance WebSocket/REST market data collection - strategy-engine: Plugin-based strategy execution (RSI, Grid) - order-executor: Order execution with risk management - portfolio-manager: Position tracking and PnL calculation - backtester: Historical strategy testing with simulator - cli: Click-based CLI for all operations - Docker Compose orchestration with Redis and PostgreSQL - 24 test files covering all modules
Diffstat (limited to 'services/order-executor/tests')
-rw-r--r--services/order-executor/tests/__init__.py0
-rw-r--r--services/order-executor/tests/test_executor.py122
-rw-r--r--services/order-executor/tests/test_risk_manager.py72
3 files changed, 194 insertions, 0 deletions
diff --git a/services/order-executor/tests/__init__.py b/services/order-executor/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/services/order-executor/tests/__init__.py
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()
diff --git a/services/order-executor/tests/test_risk_manager.py b/services/order-executor/tests/test_risk_manager.py
new file mode 100644
index 0000000..f6b5545
--- /dev/null
+++ b/services/order-executor/tests/test_risk_manager.py
@@ -0,0 +1,72 @@
+"""Tests for RiskManager."""
+from decimal import Decimal
+
+import pytest
+
+from shared.models import OrderSide, Position, Signal
+from order_executor.risk_manager import RiskManager
+
+
+def make_signal(side: OrderSide, price: str, quantity: str, symbol: str = "BTC/USDT") -> Signal:
+ return Signal(
+ strategy="test",
+ symbol=symbol,
+ side=side,
+ price=Decimal(price),
+ quantity=Decimal(quantity),
+ reason="test signal",
+ )
+
+
+def make_risk_manager(
+ max_position_size: str = "0.1",
+ stop_loss_pct: str = "5.0",
+ daily_loss_limit_pct: str = "10.0",
+) -> RiskManager:
+ return RiskManager(
+ max_position_size=Decimal(max_position_size),
+ stop_loss_pct=Decimal(stop_loss_pct),
+ daily_loss_limit_pct=Decimal(daily_loss_limit_pct),
+ )
+
+
+def test_risk_check_passes_normal_order():
+ """Small BUY order with enough balance should be allowed."""
+ rm = make_risk_manager()
+ signal = make_signal(side=OrderSide.BUY, price="100", quantity="0.5")
+ # cost = 50, balance = 10000, position_value = 0 => (0+50)/10000 = 0.5% < 10%
+ result = rm.check(signal, balance=Decimal("10000"), positions={}, daily_pnl=Decimal("0"))
+ assert result.allowed is True
+ assert result.reason == "OK"
+
+
+def test_risk_check_rejects_exceeding_position_size():
+ """5 BTC at $50,000 = $250,000 order cost on $10,000,000 balance exceeds 10% limit."""
+ rm = make_risk_manager(max_position_size="0.1")
+ signal = make_signal(side=OrderSide.BUY, price="50000", quantity="5")
+ # cost = 250000, balance = 1000000 => 250000/1000000 = 25% > 10%
+ # balance is sufficient (250000 < 1000000) but position size is exceeded
+ result = rm.check(signal, balance=Decimal("1000000"), positions={}, daily_pnl=Decimal("0"))
+ assert result.allowed is False
+ assert result.reason == "Position size exceeded"
+
+
+def test_risk_check_rejects_daily_loss_exceeded():
+ """Daily PnL of -1100 on 10000 balance = -11%, exceeding -10% limit."""
+ rm = make_risk_manager(daily_loss_limit_pct="10.0")
+ signal = make_signal(side=OrderSide.BUY, price="100", quantity="0.1")
+ result = rm.check(
+ signal, balance=Decimal("10000"), positions={}, daily_pnl=Decimal("-1100")
+ )
+ assert result.allowed is False
+ assert result.reason == "Daily loss limit exceeded"
+
+
+def test_risk_check_rejects_insufficient_balance():
+ """Order cost of 500 exceeds available balance of 100."""
+ rm = make_risk_manager()
+ signal = make_signal(side=OrderSide.BUY, price="100", quantity="5")
+ # cost = 500, balance = 100
+ result = rm.check(signal, balance=Decimal("100"), positions={}, daily_pnl=Decimal("0"))
+ assert result.allowed is False
+ assert result.reason == "Insufficient balance"