fix: reconcile KIS correction order chains
This commit is contained in:
@@ -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)."""
|
||||
|
||||
Reference in New Issue
Block a user