feat: add messaging transport rollout
This commit is contained in:
@@ -20,13 +20,19 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
from trex_core.application.broker_port import BrokerPort, OrderRequest
|
||||
from trex_core.application.ledger_outbox import LedgerOutbox
|
||||
from trex_core.application.ledger_port import LedgerTrade
|
||||
from trex_core.domain.slot import SlotService, SlotState, SlotStateMismatchError
|
||||
|
||||
# REQ-021/023: event emission hook. Runtime wires a callable that publishes
|
||||
# slot transition events through MessagingTransport. Application layer stays
|
||||
# transport-agnostic (NFR-1). Slot type is duck-typed to keep the application
|
||||
# layer free of domain re-imports (the slot is just used for attribute access).
|
||||
SlotEventEmitter = Callable[[str, object], None] # (event_type, slot) -> None
|
||||
|
||||
_MAX_DISPATCH_PER_TICK = 64
|
||||
|
||||
|
||||
@@ -46,11 +52,13 @@ class ExecutionService:
|
||||
repo,
|
||||
broker: BrokerPort,
|
||||
ledger_outbox: Optional[LedgerOutbox] = None,
|
||||
event_emitter: Optional[SlotEventEmitter] = None,
|
||||
) -> None:
|
||||
self._svc = slot_service
|
||||
self._repo = repo
|
||||
self._broker = broker
|
||||
self._ledger_outbox = ledger_outbox
|
||||
self._event_emitter = event_emitter
|
||||
|
||||
# ------------------------------------------------------------
|
||||
def run_once(self) -> ExecutionResult:
|
||||
@@ -112,23 +120,36 @@ class ExecutionService:
|
||||
broker_order_id = slot.broker_order_id # captured BEFORE clear_dispatch
|
||||
try:
|
||||
if slot.status is SlotState.PENDING_BUY:
|
||||
self._svc.transit_slot(
|
||||
updated = self._svc.transit_slot(
|
||||
slot.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING,
|
||||
reason="buy_filled", actor="execution_loop",
|
||||
avg_price=fs.avg_price, qty=float(fs.filled_qty) or slot.qty,
|
||||
)
|
||||
event_type = "slot_filled_buy"
|
||||
else: # PENDING_SELL
|
||||
self._svc.transit_slot(
|
||||
updated = self._svc.transit_slot(
|
||||
slot.slot_id, SlotState.PENDING_SELL, SlotState.CLOSED,
|
||||
reason="sell_filled", actor="execution_loop",
|
||||
)
|
||||
event_type = "slot_filled_sell"
|
||||
except SlotStateMismatchError:
|
||||
return # slot moved concurrently; next tick reconciles
|
||||
# REQ-018: enqueue ledger work item AFTER the slot transition is durable.
|
||||
# Slot transition is the critical path; ledger write is async (REQ-019).
|
||||
self._record_ledger(slot, fs, side, broker_order_id, r)
|
||||
# REQ-021/023: emit event AFTER transition + ledger enqueue (single
|
||||
# record subject). Best-effort: a publish failure must not roll back.
|
||||
self._emit_event(event_type, updated)
|
||||
self._repo.clear_dispatch(slot.slot_id) # done with this order
|
||||
|
||||
def _emit_event(self, event_type: str, slot) -> None:
|
||||
if self._event_emitter is None:
|
||||
return
|
||||
try:
|
||||
self._event_emitter(event_type, slot)
|
||||
except Exception: # noqa: BLE001 - event publish failure is non-critical
|
||||
return
|
||||
|
||||
def _record_ledger(self, slot, fs, side: str, broker_order_id: str, r: ExecutionResult) -> None:
|
||||
"""Append a LedgerWorkItem for the just-filled trade (REQ-018 + REQ-019)."""
|
||||
if self._ledger_outbox is None:
|
||||
|
||||
Reference in New Issue
Block a user