feat(execution): reconcile timed-out orders safely
This commit is contained in:
@@ -307,6 +307,7 @@ class KISBroker:
|
||||
reason=f"submit_outcome_unknown:{type(exc).__name__}",
|
||||
venue=actual_venue,
|
||||
raw={"error_class": type(exc).__name__},
|
||||
submitted_at=submitted_at,
|
||||
)
|
||||
if getattr(resp, "status_code", 200) >= 500:
|
||||
return OrderAck(
|
||||
@@ -315,6 +316,7 @@ class KISBroker:
|
||||
reason=f"submit_outcome_unknown:http_{resp.status_code}",
|
||||
venue=actual_venue,
|
||||
raw={"http_status": resp.status_code},
|
||||
submitted_at=submitted_at,
|
||||
)
|
||||
if str(payload.get("rt_cd")) == "0":
|
||||
out = payload.get("output") or {}
|
||||
@@ -326,6 +328,7 @@ class KISBroker:
|
||||
broker_order_id=bid,
|
||||
venue=actual_venue,
|
||||
raw=payload,
|
||||
submitted_at=submitted_at,
|
||||
)
|
||||
return OrderAck(
|
||||
accepted=False,
|
||||
@@ -362,7 +365,15 @@ class KISBroker:
|
||||
"EXCG_ID_DVSN_CD": "ALL",
|
||||
}
|
||||
|
||||
def poll_fill(self, broker_order_id: str, *, side: str, symbol: str) -> FillStatus:
|
||||
def poll_fill(
|
||||
self,
|
||||
broker_order_id: str,
|
||||
*,
|
||||
side: str,
|
||||
symbol: str,
|
||||
order_date: Optional[str] = None,
|
||||
submitted_at: Optional[datetime] = None,
|
||||
) -> FillStatus:
|
||||
if broker_order_id.startswith(_SUBMISSION_UNKNOWN_PREFIX):
|
||||
return self._poll_unknown_submission(broker_order_id, side=side, symbol=symbol)
|
||||
now = monotonic()
|
||||
@@ -375,8 +386,8 @@ class KISBroker:
|
||||
# open duplicate KIS inquiries for the same order.
|
||||
self._fill_poll_not_before[broker_order_id] = now + _FILL_POLL_MIN_INTERVAL_SECONDS
|
||||
odno = broker_order_id.split(":")[-1]
|
||||
today = datetime.now(tz=_KST).strftime("%Y%m%d")
|
||||
tr_id, params = self._daily_fill_request(today, order_number=odno)
|
||||
inquiry_date = order_date or self._clock().astimezone(_KST).strftime("%Y%m%d")
|
||||
tr_id, params = self._daily_fill_request(inquiry_date, order_number=odno)
|
||||
try:
|
||||
resp = httpx.get(
|
||||
f"{self._base_url}/uapi/domestic-stock/v1/trading/inquire-daily-ccld",
|
||||
@@ -410,11 +421,36 @@ class KISBroker:
|
||||
None,
|
||||
)
|
||||
if row is None:
|
||||
return FillStatus(
|
||||
state="accepted",
|
||||
raw={"reason": "order_not_visible_in_fill_inquiry"},
|
||||
visible_grace = (
|
||||
submitted_at is None
|
||||
or (self._clock().astimezone(timezone.utc) - submitted_at.astimezone(timezone.utc))
|
||||
.total_seconds()
|
||||
< _SUBMISSION_LOOKUP_GRACE_SECONDS
|
||||
)
|
||||
return self._fill_status_from_row(row, side=side, today=today, fallback_odno=odno)
|
||||
return FillStatus(
|
||||
state="accepted" if visible_grace else "unknown",
|
||||
raw={
|
||||
"reason": (
|
||||
"order_visibility_grace"
|
||||
if visible_grace
|
||||
else "order_not_visible_unresolved"
|
||||
),
|
||||
"order_visible": False,
|
||||
"inquiry_date": inquiry_date,
|
||||
},
|
||||
)
|
||||
status = self._fill_status_from_row(
|
||||
row, side=side, today=inquiry_date, fallback_odno=odno
|
||||
)
|
||||
return replace(
|
||||
status,
|
||||
raw={
|
||||
**(status.raw if isinstance(status.raw, dict) else {}),
|
||||
"order_visible": True,
|
||||
"order_open_confirmed": status.state in {"accepted", "partially_filled"},
|
||||
"inquiry_date": inquiry_date,
|
||||
},
|
||||
)
|
||||
|
||||
def _poll_unknown_submission(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user