fix(execution): bind parent row to request identity before child evaluation; add parent mismatch and race tests
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
65df10542b
commit
584e08eb36
@@ -413,6 +413,11 @@ class KISBroker:
|
||||
self._fill_poll_not_before[broker_order_id] = monotonic() + _FILL_POLL_MIN_INTERVAL_SECONDS
|
||||
rows = payload.get("output1") or []
|
||||
normalized_odno = odno.lstrip("0") or "0"
|
||||
# Canonical request identity from the caller — these are authoritative
|
||||
# and must not be overridden by whatever the response row contains.
|
||||
request_brno = broker_order_id.split(":")[0] if ":" in broker_order_id else ""
|
||||
request_date = inquiry_date # == order_date
|
||||
request_symbol = symbol
|
||||
row = next(
|
||||
(
|
||||
r
|
||||
@@ -440,40 +445,81 @@ class KISBroker:
|
||||
"inquiry_date": inquiry_date,
|
||||
},
|
||||
)
|
||||
# Validate that the matched parent row's identity agrees with the
|
||||
# authoritative request identity. KIS can return stale rows for
|
||||
# reused odno values; a branch, date, or symbol mismatch means this
|
||||
# row does not belong to our order → fail-closed as unknown.
|
||||
row_brno = str(row.get("ord_gno_brno") or "").strip()
|
||||
row_dt = str(row.get("ord_dt") or "").strip()
|
||||
row_pdno = str(row.get("pdno") or "").strip()
|
||||
if request_brno and row_brno and row_brno != request_brno:
|
||||
return FillStatus(
|
||||
state="unknown",
|
||||
raw={
|
||||
"reason": "parent_identity_mismatch_brno",
|
||||
"order_visible": True,
|
||||
"inquiry_date": inquiry_date,
|
||||
},
|
||||
)
|
||||
if row_dt and row_dt != request_date:
|
||||
return FillStatus(
|
||||
state="unknown",
|
||||
raw={
|
||||
"reason": "parent_identity_mismatch_date",
|
||||
"order_visible": True,
|
||||
"inquiry_date": inquiry_date,
|
||||
},
|
||||
)
|
||||
if row_pdno and row_pdno != request_symbol:
|
||||
return FillStatus(
|
||||
state="unknown",
|
||||
raw={
|
||||
"reason": "parent_identity_mismatch_symbol",
|
||||
"order_visible": True,
|
||||
"inquiry_date": inquiry_date,
|
||||
},
|
||||
)
|
||||
# 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.
|
||||
#
|
||||
# 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 must share the AUTHORITATIVE request identity (branch, date,
|
||||
# symbol from the caller), NOT the row values. Using row values as
|
||||
# ground truth would let a mismatched row pass its own wrong values
|
||||
# back as the filter reference.
|
||||
# - Ambiguous (> 1 matching cancel) → fail-closed "unknown".
|
||||
# - Correction (정정) children (cncl_yn != "Y") that match the same
|
||||
# request identity are also fail-closed → "unknown".
|
||||
# - Unrelated rows that only share orgn_odno but differ in branch/date/
|
||||
# symbol are excluded from both candidate lists.
|
||||
def _child_matches_request(r: dict) -> bool:
|
||||
"""True when a child row is bound to the same order identity as the request."""
|
||||
child_brno = str(r.get("ord_gno_brno") or "").strip()
|
||||
child_dt = str(r.get("ord_dt") or "").strip()
|
||||
child_pdno = str(r.get("pdno") or "").strip()
|
||||
if request_brno and child_brno and child_brno != request_brno:
|
||||
return False
|
||||
if child_dt and child_dt != request_date:
|
||||
return False
|
||||
return not (child_pdno and child_pdno != request_symbol)
|
||||
|
||||
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
|
||||
and _child_matches_request(r)
|
||||
]
|
||||
|
||||
# Correction children: same orgn_odno but cncl_yn != "Y". If present,
|
||||
# the situation is ambiguous — a replacement order may be live.
|
||||
# Correction children: same orgn_odno + matching identity but cncl_yn != "Y".
|
||||
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"
|
||||
and _child_matches_request(r)
|
||||
]
|
||||
|
||||
if len(child_cancel_candidates) > 1:
|
||||
|
||||
Reference in New Issue
Block a user