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:
co-authored by
genichin
parent
98c38327b2
commit
a9e88f9770
@@ -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)."""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user