summaryrefslogtreecommitdiff
path: root/services/order-executor/src/order_executor/executor.py
blob: a71e762cde2d670893ebaeb9bda8a881207e3a7c (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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
"""Order execution logic."""

import structlog
from datetime import datetime, timezone
from decimal import Decimal
from typing import Any, Optional

from shared.broker import RedisBroker
from shared.db import Database
from shared.events import OrderEvent
from shared.models import Order, OrderStatus, OrderType, Signal
from shared.notifier import TelegramNotifier

from order_executor.risk_manager import RiskManager

logger = structlog.get_logger()


class OrderExecutor:
    """Executes orders on an exchange with risk gating."""

    def __init__(
        self,
        exchange: Any,
        risk_manager: RiskManager,
        broker: RedisBroker,
        db: Database,
        notifier: TelegramNotifier,
        dry_run: bool = True,
    ) -> None:
        self.exchange = exchange
        self.risk_manager = risk_manager
        self.broker = broker
        self.db = db
        self.notifier = notifier
        self.dry_run = dry_run

    async def execute(self, signal: Signal) -> Optional[Order]:
        """Run risk checks and place an order for the given signal."""
        # Fetch buying power from Alpaca
        balance = await self.exchange.get_buying_power()

        # Fetch current positions
        positions = {}

        # Compute daily PnL (not tracked at executor level — use 0 unless provided)
        daily_pnl = Decimal(0)

        # Run risk checks
        result = self.risk_manager.check(
            signal=signal,
            balance=balance,
            positions=positions,
            daily_pnl=daily_pnl,
        )

        if not result.allowed:
            logger.warning("risk_check_rejected", signal_id=str(signal.id), reason=result.reason)
            return None

        # Build the order model
        order = Order(
            signal_id=signal.id,
            symbol=signal.symbol,
            side=signal.side,
            type=OrderType.MARKET,
            price=signal.price,
            quantity=signal.quantity,
            status=OrderStatus.PENDING,
        )

        if self.dry_run:
            order.status = OrderStatus.FILLED
            order.filled_at = datetime.now(timezone.utc)
            logger.info(
                "order_filled_dry_run",
                side=str(order.side),
                quantity=str(order.quantity),
                symbol=order.symbol,
            )
        else:
            try:
                await self.exchange.submit_order(
                    symbol=signal.symbol,
                    qty=float(signal.quantity),
                    side=signal.side.value.lower(),
                    type="market",
                )
                order.status = OrderStatus.FILLED
                order.filled_at = datetime.now(timezone.utc)
                logger.info(
                    "order_filled",
                    side=str(order.side),
                    quantity=str(order.quantity),
                    symbol=order.symbol,
                )
            except Exception as exc:
                order.status = OrderStatus.FAILED
                logger.error("order_failed", signal_id=str(signal.id), error=str(exc))

        # Persist to DB
        await self.db.insert_order(order)

        # Publish order event
        event = OrderEvent(data=order)
        await self.broker.publish("orders", event.to_dict())

        # Notify via Telegram
        await self.notifier.send_order(order)

        return order