summaryrefslogtreecommitdiff
path: root/tests/integration/test_order_execution_flow.py
blob: 1ea0485132c42156ffec325b244dc5b9e7b02643 (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
75
76
77
78
79
"""Integration test: signal -> risk manager -> order executor -> order event."""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "services" / "order-executor" / "src"))

import pytest
from decimal import Decimal
from unittest.mock import AsyncMock, MagicMock

from shared.models import Signal, OrderSide, OrderStatus
from order_executor.executor import OrderExecutor
from order_executor.risk_manager import RiskManager


@pytest.mark.asyncio
async def test_signal_to_order_flow():
    """A valid signal passes risk checks and produces a filled order."""
    signal = Signal(
        strategy="rsi", symbol="BTC/USDT", side=OrderSide.BUY,
        price=Decimal("50000"), quantity=Decimal("0.01"), reason="RSI oversold",
    )

    exchange = AsyncMock()
    exchange.fetch_balance = AsyncMock(return_value={"free": {"USDT": 10000}})

    risk_manager = RiskManager(
        max_position_size=Decimal("0.5"),
        stop_loss_pct=Decimal("5"),
        daily_loss_limit_pct=Decimal("10"),
    )

    broker = AsyncMock()
    db = AsyncMock()
    notifier = AsyncMock()

    executor = OrderExecutor(
        exchange=exchange, risk_manager=risk_manager,
        broker=broker, db=db, notifier=notifier, dry_run=True,
    )

    order = await executor.execute(signal)

    assert order is not None
    assert order.status == OrderStatus.FILLED
    assert order.symbol == "BTC/USDT"
    assert order.side == OrderSide.BUY

    # Verify order was persisted and published
    db.insert_order.assert_called_once()
    broker.publish.assert_called_once()
    notifier.send_order.assert_called_once()


@pytest.mark.asyncio
async def test_signal_rejected_by_risk_manager():
    """A signal that exceeds position size is rejected."""
    signal = Signal(
        strategy="rsi", symbol="BTC/USDT", side=OrderSide.BUY,
        price=Decimal("50000"), quantity=Decimal("100"),  # Way too large
        reason="test",
    )

    exchange = AsyncMock()
    exchange.fetch_balance = AsyncMock(return_value={"free": {"USDT": 1000}})

    risk_manager = RiskManager(
        max_position_size=Decimal("0.1"),
        stop_loss_pct=Decimal("5"),
        daily_loss_limit_pct=Decimal("10"),
    )

    executor = OrderExecutor(
        exchange=exchange, risk_manager=risk_manager,
        broker=AsyncMock(), db=AsyncMock(), notifier=AsyncMock(), dry_run=True,
    )

    order = await executor.execute(signal)
    assert order is None  # Rejected