feat(intents): emit warn alarm on first reconcile_warned rejection

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-08-26 04:19:14 +00:00
committed by GitHub
co-authored by genichin
parent 98c38327b2
commit a9e88f9770
4 changed files with 318 additions and 0 deletions
+63
View File
@@ -28,6 +28,7 @@ from decimal import Decimal
from typing import Any, Callable, Optional
from uuid import UUID
from trex_core.application.alarm_emitter import AlarmEmitter
from trex_core.application.ports import IntentStore
from trex_core.domain.slot import (
SlotAdmissionRejectedError,
@@ -115,6 +116,7 @@ class IntentService:
max_active_slots: Optional[int] = None,
default_order_timeout_sec: Optional[int] = None,
warn_store=None, # Optional[ReconcileWarningStore] — BAT-050 / REQ-104
alarm_emitter: Optional[AlarmEmitter] = None,
entry_allowed_fn: Optional[Callable[[], bool]] = None,
buy_guard_fn: Optional[Callable[[BuyIntent], Optional[str]]] = None,
) -> None:
@@ -141,6 +143,8 @@ class IntentService:
# BAT-050 / REQ-104: reconcile-warning gate store (Optional).
# When None, gate is skipped (backward compat, NFR-4).
self._warn_store = warn_store
# Optional best-effort alarm publisher for reconcile_warned rejections.
self._alarm_emitter = alarm_emitter
# dcy-036 / REQ-114: day-start entry gate (Optional callable → bool).
# When None or returns True, gate passes (backward compat / opt-in=False).
# When returns False, BuyIntent is rejected with reason 'day_start_load_failed'.
@@ -178,6 +182,22 @@ class IntentService:
self._intents.finalize(
intent.intent_id, decision.status, decision.slot_id, decision.reason
)
if decision.status == "rejected" and decision.reason == "reconcile_warned":
slot = (
self._repo.fetch_slot(intent.slot_id)
if intent.slot_id is not None
else (
self._repo.find_slot_by_portfolio(self._instance_key, intent.portfolio_id)
if intent.portfolio_id
else None
)
)
self._publish_reconcile_warned_rejection_alarm(
intent_id=intent.intent_id,
intent_type="buy",
strategy_id=intent.strategy_id,
slot=slot,
)
return decision
def submit_sell(self, intent: SellIntent) -> IntentDecision:
@@ -196,8 +216,51 @@ class IntentService:
self._intents.finalize(
intent.intent_id, decision.status, decision.slot_id, decision.reason
)
if decision.status == "rejected" and decision.reason == "reconcile_warned":
self._publish_reconcile_warned_rejection_alarm(
intent_id=intent.intent_id,
intent_type="sell",
strategy_id=intent.sell_strategy_id,
slot=self._repo.fetch_slot(intent.slot_id),
)
return decision
def _publish_reconcile_warned_rejection_alarm(
self,
*,
intent_id: str,
intent_type: str,
strategy_id: Optional[str],
slot: Any,
) -> None:
if self._alarm_emitter is None:
return
fields: dict[str, Any] = {
"instance_key": self._instance_key,
"intent_id": intent_id,
"intent_type": intent_type,
"reason": "reconcile_warned",
"broker_submitted": False,
}
if slot is not None:
if getattr(slot, "slot_id", None) is not None:
fields["slot_id"] = str(slot.slot_id)
if getattr(slot, "portfolio_id", None):
fields["portfolio_id"] = slot.portfolio_id
if getattr(slot, "symbol", None):
fields["symbol"] = slot.symbol
if strategy_id:
fields["strategy_id"] = strategy_id
try:
self._alarm_emitter.publish_alarm(
"strategy_intent_rejected",
"Strategy intent rejected by reconcile warning gate",
fields=fields,
severity="warn",
)
except Exception: # noqa: BLE001 - alarm path must not affect intent decision
pass
# ------------------------------------------------------------
def _dedup(self, intent_id: str, intent_type: str, payload: dict) -> Optional[IntentDecision]:
"""Return prior decision if intent_id already seen; else None (and reserve it)."""
+1
View File
@@ -299,6 +299,7 @@ class Daemon:
max_active_slots=config.max_active_slots, # dcy-020: asset-centric gate
default_order_timeout_sec=config.default_order_timeout_sec, # dcy-022
warn_store=self.warn_store, # BAT-050 / REQ-104: reconcile-warning gate
alarm_emitter=self.alarm_emitter, # req-0008 / ac-0025
entry_allowed_fn=entry_allowed_fn, # dcy-036 / REQ-114: day-start gate
buy_guard_fn=(
self._account_snapshot_buy_guard