fix: reconcile KIS correction order chains

This commit is contained in:
Hermes Agent
2026-09-12 22:12:19 +09:00
parent 6017bde744
commit 06c3f06b5d
6 changed files with 864 additions and 149 deletions
+69
View File
@@ -62,6 +62,24 @@ SlotEventEmitter = Callable[
] # (event_type, slot, routing_strategy_id) -> None
_MAX_DISPATCH_PER_TICK = 64
_ORDER_POLL_WARNING_THRESHOLDS_SEC = (60, 5 * 60, 15 * 60, 60 * 60, 6 * 60 * 60)
_ORDER_POLL_REASON_ALLOWLIST = frozenset({
"correction_child_present_unresolved",
"fill_inquiry_rejected",
"fill_inquiry_transport",
"order_not_visible_unresolved",
"parent_identity_ambiguous",
"parent_identity_mismatch",
"replacement_chain_ambiguous",
"replacement_chain_fill_ambiguous",
"replacement_chain_stable_zero_terminal",
"replacement_chain_terminal_cancel",
"replacement_live_unresolved",
"submission_lookup_ambiguous",
"submission_lookup_pending",
"submission_lookup_rejected",
"submission_lookup_transport",
})
@dataclass
@@ -115,6 +133,7 @@ class ExecutionService:
# A restart intentionally emits once again, covering a crash between a
# durable cancel reservation and best-effort alarm publication.
self._order_alarm_keys: set[tuple[str, str, int]] = set()
self._order_poll_warning_state: dict[str, tuple[str, str, int]] = {}
self._clock = clock or (lambda: datetime.now(tz=UTC))
# dcy-041 / GitHub #45: pending-sell quantity may use a bounded
# account snapshot only through this injected reader. Reconciliation
@@ -282,6 +301,7 @@ class ExecutionService:
self._emit_order_metadata_unknown_alarm(slot)
continue
fs = self._poll_fill(slot, side)
self._observe_order_poll(slot, side, fs)
resolved_order_id = (
fs.raw.get("resolved_broker_order_id") if isinstance(fs.raw, dict) else None
)
@@ -344,6 +364,55 @@ class ExecutionService:
kwargs["submitted_at"] = slot.order_submitted_at
return poll(slot.broker_order_id, **kwargs)
def _observe_order_poll(self, slot, side: str, fs) -> None:
"""Emit sanitized poll telemetry and deduplicated unresolved warnings."""
raw = fs.raw if isinstance(fs.raw, dict) else {}
if raw.get("poll_performed") is not True:
return
def _metric_int(name: str) -> int:
try:
return max(0, int(raw.get(name, 0)))
except (TypeError, ValueError):
return 0
reason_value = raw.get("last_result_reason")
reason = (
str(reason_value)
if reason_value in _ORDER_POLL_REASON_ALLOWLIST
else "none"
)
poll_count = _metric_int("poll_count")
stuck_duration_sec = _metric_int("stuck_duration_sec")
try:
backoff_sec = max(0.0, float(raw.get("backoff_sec", 0.0)))
except (TypeError, ValueError):
backoff_sec = 0.0
fields = {
"event": "order_poll",
"side": side,
"result_state": fs.state,
"result_reason": reason,
"poll_count": poll_count,
"stuck_duration_sec": stuck_duration_sec,
"backoff_sec": backoff_sec,
}
_log.info("order poll observed", extra=fields)
slot_key = str(slot.slot_id)
if fs.state != "unknown":
self._order_poll_warning_state.pop(slot_key, None)
return
threshold_index = sum(
stuck_duration_sec >= threshold
for threshold in _ORDER_POLL_WARNING_THRESHOLDS_SEC
)
warning_key = (fs.state, reason, threshold_index)
if self._order_poll_warning_state.get(slot_key) == warning_key:
return
self._order_poll_warning_state[slot_key] = warning_key
_log.warning("order poll unresolved", extra={**fields, "event": "order_poll_unresolved"})
# ------------------------------------------------------------
def _balance_map(self) -> dict:
"""Current broker balance as {symbol: hldg_qty} (REQ-062 ground truth)."""