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
|