fix(execution): KIS child cancel identity binding, sanitized logging, PG cancel SQL tests

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-01 12:06:45 +00:00
committed by GitHub
co-authored by genichin
parent f0ac909958
commit 65df10542b
5 changed files with 331 additions and 31 deletions
+60 -12
View File
@@ -443,22 +443,70 @@ class KISBroker:
# dcy-013 ext: KIS sometimes records an external cancel as a separate
# child row with orgn_odno=<original> and cncl_yn="Y" rather than
# setting cncl_yn on the original row. Scan sibling rows to detect
# this case. Correction (정정) children will NOT have cncl_yn="Y" so
# they are not misclassified as cancels here.
child_cancel_row = next(
(
r
for r in rows
if (str(r.get("orgn_odno") or "").strip().lstrip("0") or "0") == normalized_odno
and str(r.get("cncl_yn") or "").strip().upper() == "Y"
),
None,
)
if child_cancel_row is not None and str(row.get("cncl_yn") or "").strip().upper() != "Y":
# this case.
#
# Safety requirements:
# - Child must share the parent's canonical identity:
# ord_gno_brno (branch), ord_dt (date), pdno (symbol).
# Mismatched or ambiguous children → fail-closed as "unknown".
# - Correction (정정) children have cncl_yn != "Y"; they are not
# treated as cancels here. If a correction child is present with
# a new odno the evidence is ambiguous → "unknown" so the slot is
# not incorrectly unlocked.
parent_brno = str(row.get("ord_gno_brno") or "").strip()
parent_dt = str(row.get("ord_dt") or "").strip()
parent_pdno = str(row.get("pdno") or "").strip()
child_cancel_candidates = [
r
for r in rows
if (str(r.get("orgn_odno") or "").strip().lstrip("0") or "0") == normalized_odno
and str(r.get("cncl_yn") or "").strip().upper() == "Y"
and str(r.get("ord_gno_brno") or "").strip() == parent_brno
and str(r.get("ord_dt") or "").strip() == parent_dt
and str(r.get("pdno") or "").strip() == parent_pdno
]
# Correction children: same orgn_odno but cncl_yn != "Y". If present,
# the situation is ambiguous — a replacement order may be live.
correction_candidates = [
r
for r in rows
if (str(r.get("orgn_odno") or "").strip().lstrip("0") or "0") == normalized_odno
and str(r.get("cncl_yn") or "").strip().upper() != "Y"
]
if len(child_cancel_candidates) > 1:
# More than one matching cancel child — identity is ambiguous.
return FillStatus(
state="unknown",
raw={
"reason": "child_cancel_ambiguous",
"order_visible": True,
"inquiry_date": inquiry_date,
},
)
if correction_candidates:
# A correction/replacement child is present; the original order is
# superseded but we cannot safely infer terminal status without
# explicit broker confirmation of the replacement identity.
return FillStatus(
state="unknown",
raw={
"reason": "correction_child_present_unresolved",
"order_visible": True,
"inquiry_date": inquiry_date,
},
)
if child_cancel_candidates and str(row.get("cncl_yn") or "").strip().upper() != "Y":
child_cancel_row = child_cancel_candidates[0]
# Inject the cancel signal from the child row into the parent row so
# _fill_status_from_row can treat it as cancelled, while keeping
# the original row's fill quantities authoritative.
row = {**row, "cncl_yn": "Y", "_child_cancel_odno": str(child_cancel_row.get("odno") or "")}
status = self._fill_status_from_row(
row, side=side, today=inquiry_date, fallback_odno=odno
)