feat(execution): reconcile timed-out orders safely

This commit is contained in:
Hermes Agent
2026-08-14 15:15:52 +09:00
parent 482b00e54d
commit 147873cb5d
21 changed files with 801 additions and 87 deletions
+48 -6
View File
@@ -14,7 +14,7 @@ from __future__ import annotations
import json
from contextlib import contextmanager
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from typing import Any, Iterator, Optional
from uuid import UUID, uuid4
@@ -38,9 +38,11 @@ _SLOT_COLUMNS = (
"sell_strategy_id, forced_close_at, stop_loss_price, take_profit_price, "
"created_at, updated_at, instance_key, retired_at, "
"timeout_sec, slippage_pct, occupancy_seq, venue, "
"cancel_state, cancel_requested_at"
"cancel_state, cancel_requested_at, order_submitted_at, order_date, cancel_attempts"
)
_KST = timezone(timedelta(hours=9))
def _require_psycopg(module: Any = None) -> Any:
if module is not None:
@@ -218,7 +220,8 @@ class PostgreSQLSlotRepository:
"forced_close_at=%s, stop_loss_price=%s, take_profit_price=%s, "
"instance_key=%s, retired_at=%s, timeout_sec=%s, slippage_pct=%s, "
"occupancy_seq=%s, venue=%s, cancel_state=%s, "
"cancel_requested_at=%s, updated_at=%s WHERE slot_id=%s",
"cancel_requested_at=%s, order_submitted_at=%s, order_date=%s, "
"cancel_attempts=%s, updated_at=%s WHERE slot_id=%s",
(
slot.status.value,
slot.symbol,
@@ -241,6 +244,9 @@ class PostgreSQLSlotRepository:
slot.venue,
slot.cancel_state,
slot.cancel_requested_at,
slot.order_submitted_at,
slot.order_date,
slot.cancel_attempts,
slot.updated_at or datetime.now(tz=timezone.utc),
slot.slot_id,
),
@@ -295,16 +301,22 @@ class PostgreSQLSlotRepository:
broker_order_id: str,
*,
venue: Optional[str] = None,
submitted_at: Optional[datetime] = None,
) -> bool:
submitted_at = submitted_at or datetime.now(tz=timezone.utc)
order_date = submitted_at.astimezone(_KST).strftime("%Y%m%d")
with self._psycopg.connect(self._dsn, autocommit=False) as conn:
with conn.cursor() as cur:
cur.execute(
"UPDATE daily_slot SET broker_order_id=%s, "
"venue=COALESCE(%s, venue), updated_at=%s "
"venue=COALESCE(%s, venue), order_submitted_at=%s, order_date=%s, "
"cancel_attempts=0, updated_at=%s "
"WHERE slot_id=%s AND broker_order_id IS NULL",
(
broker_order_id,
venue,
submitted_at,
order_date,
datetime.now(tz=timezone.utc),
slot_id,
),
@@ -366,6 +378,31 @@ class PostgreSQLSlotRepository:
conn.commit()
return ok
def reserve_cancel_attempt(
self,
slot_id: UUID,
broker_order_id: str,
*,
retry_before: Optional[datetime] = None,
) -> bool:
"""Atomically reserve first cancel or an authoritative backoff-gated retry."""
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),
)
ok = cur.rowcount == 1
conn.commit()
return ok
def list_dispatched_pending(self, instance_key: str = "default") -> list[Slot]:
with self._psycopg.connect(self._dsn) as conn:
with conn.cursor() as cur:
@@ -382,7 +419,8 @@ class PostgreSQLSlotRepository:
with conn.cursor() as cur:
cur.execute(
"UPDATE daily_slot SET broker_order_id=NULL, cancel_state=NULL, "
"cancel_requested_at=NULL, updated_at=%s WHERE slot_id=%s",
"cancel_requested_at=NULL, order_submitted_at=NULL, order_date=NULL, "
"cancel_attempts=0, updated_at=%s WHERE slot_id=%s",
(datetime.now(tz=timezone.utc), slot_id),
)
conn.commit()
@@ -410,7 +448,8 @@ class PostgreSQLSlotRepository:
# created_at(14) updated_at(15) instance_key(16) retired_at(17)
# timeout_sec(18) slippage_pct(19) [dcy-022/REQ-068]
# occupancy_seq(20) [dcy-028/REQ-081], venue(21),
# cancel_state(22), cancel_requested_at(23)
# cancel_state(22), cancel_requested_at(23), order_submitted_at(24),
# order_date(25), cancel_attempts(26)
return Slot(
slot_id=row[0] if isinstance(row[0], UUID) else UUID(str(row[0])),
status=SlotState(row[1]),
@@ -436,6 +475,9 @@ class PostgreSQLSlotRepository:
venue=str(row[21]) if len(row) > 21 and row[21] is not None else "KRX",
cancel_state=str(row[22]) if len(row) > 22 and row[22] is not None else None,
cancel_requested_at=row[23] if len(row) > 23 else None,
order_submitted_at=row[24] if len(row) > 24 else None,
order_date=str(row[25]) if len(row) > 25 and row[25] is not None else None,
cancel_attempts=int(row[26]) if len(row) > 26 and row[26] is not None else 0,
)