fix(execution): cancel SQL nullable type error, KIS child cancel detection, worker exception logging

Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-01 03:33:38 +00:00
committed by GitHub
co-authored by genichin
parent 2b412fd786
commit f0ac909958
6 changed files with 258 additions and 12 deletions
+19
View File
@@ -440,6 +440,25 @@ class KISBroker:
"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. 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":
# 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
)
+28 -11
View File
@@ -385,20 +385,37 @@ class PostgreSQLSlotRepository:
*,
retry_before: Optional[datetime] = None,
) -> bool:
"""Atomically reserve first cancel or an authoritative backoff-gated retry."""
"""Atomically reserve first cancel or an authoritative backoff-gated retry.
Two separate SQL paths avoid nullable-placeholder type-inference errors
(PostgreSQL IndeterminateDatatype when binding None to %s IS NOT NULL).
"""
now = datetime.now(tz=timezone.utc)
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
with conn.cursor() as cur:
cur.execute(
"UPDATE daily_slot SET cancel_state='unknown', cancel_requested_at=%s, "
"cancel_attempts=cancel_attempts+1, updated_at=%s "
"WHERE slot_id=%s AND broker_order_id=%s "
"AND status IN ('pending_buy','pending_sell') "
"AND (cancel_state IS NULL OR "
"(%s IS NOT NULL AND cancel_state IN ('rejected','unknown') "
"AND cancel_attempts < 4 AND cancel_requested_at <= %s))",
(now, now, slot_id, broker_order_id, retry_before, retry_before),
)
if retry_before is None:
# First reservation: only match when no cancel has been
# attempted yet (cancel_state IS NULL).
cur.execute(
"UPDATE daily_slot SET cancel_state='unknown', cancel_requested_at=%s, "
"cancel_attempts=cancel_attempts+1, updated_at=%s "
"WHERE slot_id=%s AND broker_order_id=%s "
"AND status IN ('pending_buy','pending_sell') "
"AND cancel_state IS NULL",
(now, now, slot_id, broker_order_id),
)
else:
# Retry: backoff-gated, only match retryable states.
cur.execute(
"UPDATE daily_slot SET cancel_state='unknown', cancel_requested_at=%s, "
"cancel_attempts=cancel_attempts+1, updated_at=%s "
"WHERE slot_id=%s AND broker_order_id=%s "
"AND status IN ('pending_buy','pending_sell') "
"AND cancel_state IN ('rejected','unknown') "
"AND cancel_attempts < 4 "
"AND cancel_requested_at <= %s",
(now, now, slot_id, broker_order_id, retry_before),
)
ok = cur.rowcount == 1
conn.commit()
return ok
+1 -1
View File
@@ -409,7 +409,7 @@ class Daemon:
try:
self.execution.run_once()
except Exception: # noqa: BLE001 - loop must survive transient errors
continue
_log.exception("execution loop error — worker continues")
def _ledger_loop(self) -> None:
while not self.stop.wait(self.config.ledger_interval_sec):