feat(alarm): trex-core 알람 emitter + trex.alerts exchange (dcy-033 BAT-049, Implemented)

AlarmEmitter(application, best-effort 비차단)+trex.alerts topic exchange 가산·격리+8 CRITICAL code 배선. 608 passed, opus QC PASS, 팀장 e2e(텔레그램 HTTP200) 통과. REQ-098~101 Implemented.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
2026-06-21 19:25:55 +09:00
co-authored by Claude Opus 4.8
parent b2165eac25
commit d4c5fcbba1
29 changed files with 1195 additions and 47 deletions
+19
View File
@@ -27,6 +27,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Callable, Optional
from trex_core.application.alarm_emitter import AlarmEmitter
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
@@ -67,6 +68,7 @@ class ExecutionService:
event_emitter: Optional[SlotEventEmitter] = None,
stale_pending_threshold_sec: Optional[int] = None,
instance_key: str = "default",
alarm_emitter: Optional[AlarmEmitter] = None,
) -> None:
self._svc = slot_service
self._repo = repo
@@ -85,6 +87,8 @@ class ExecutionService:
if stale_pending_threshold_sec and stale_pending_threshold_sec > 0
else None
)
# dcy-033 / REQ-100: best-effort alarm emitter (None = no alarms sent).
self._alarm_emitter = alarm_emitter
# ------------------------------------------------------------
def run_once(self) -> ExecutionResult:
@@ -130,6 +134,13 @@ class ExecutionService:
"(divergence — no order submitted; operator/reconcile attention)",
slot.slot_id, slot.symbol,
)
# dcy-033 / REQ-100: CRITICAL alarm — PENDING_SELL may stick.
if self._alarm_emitter is not None:
self._alarm_emitter.publish_alarm(
"sell_skipped_no_balance",
f"매도 dispatch skip: 브로커 잔고 0 (slot={slot.slot_id} symbol={slot.symbol})",
fields={"slot_id": str(slot.slot_id), "symbol": slot.symbol},
)
continue
else:
qty = int(slot.qty or 0)
@@ -265,6 +276,14 @@ class ExecutionService:
slot.slot_id, slot.symbol, slot.broker_order_id,
avg, limit,
)
# dcy-033 / REQ-100: CRITICAL alarm — slot stuck in PENDING.
if self._alarm_emitter is not None:
self._alarm_emitter.publish_alarm(
"fill_price_unresolvable",
f"체결가 미해결: 슬롯 PENDING 고착 위험 (slot={slot.slot_id} symbol={slot.symbol})",
fields={"slot_id": str(slot.slot_id), "symbol": slot.symbol,
"broker_order_id": slot.broker_order_id},
)
return None, False
def _on_filled(self, slot, fs, side: str, r: ExecutionResult) -> None: