Harden residual mid-close: IOC partial fills, exchange reconcile, atomic book.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 14:41:00 +08:00
parent 640ecc9530
commit 469e7a258a
8 changed files with 729 additions and 238 deletions
+26 -2
View File
@@ -185,7 +185,7 @@ class OkxTradeClient:
if not rows:
raise RuntimeError("OKX IOC 下单无返回")
ord_id = str(rows[0].get("ordId") or "")
return self._wait_fill(inst_id, ord_id)
return self._wait_fill(inst_id, ord_id, allow_partial=True)
def _fill_from_order_row(self, inst_id: str, ord_id: str, row: dict[str, Any]) -> LiveFill:
avg = safe_float(row.get("avgPx")) or 0.0
@@ -206,7 +206,14 @@ class OkxTradeClient:
raw=row,
)
def _wait_fill(self, inst_id: str, ord_id: str, *, tries: int = 40) -> LiveFill:
def _wait_fill(
self,
inst_id: str,
ord_id: str,
*,
tries: int = 40,
allow_partial: bool = False,
) -> LiveFill:
path = f"/api/v5/trade/order?instId={inst_id}&ordId={ord_id}"
last: dict[str, Any] = {}
for _ in range(tries):
@@ -215,21 +222,38 @@ class OkxTradeClient:
last = rows[0]
state = str(last.get("state") or "")
avg = safe_float(last.get("avgPx"))
acc = safe_float(last.get("accFillSz")) or 0.0
# 仅完全成交;部分成交继续等,避免账本张数与交易所不一致
if state == "filled" and avg and avg > 0:
return self._fill_from_order_row(inst_id, ord_id, last)
if state in ("canceled", "failed"):
# IOC:未成交部分取消;若已有成交量则按部分成交入账
if (
allow_partial
and acc > 1e-12
and avg
and avg > 0
):
return self._fill_from_order_row(inst_id, ord_id, last)
raise RuntimeError(f"OKX 订单失败 state={state} {last}")
time.sleep(0.3)
# 超时兜底:仅接受完全成交;部分成交不得当全成记账(会错张数/对冲)
state = str(last.get("state") or "")
avg = safe_float(last.get("avgPx"))
acc = safe_float(last.get("accFillSz")) or 0.0
if state == "filled" and avg and avg > 0:
logger.warning(
"OKX fill wait timeout but order filled ordId=%s",
ord_id,
)
return self._fill_from_order_row(inst_id, ord_id, last)
if allow_partial and acc > 1e-12 and avg and avg > 0:
logger.warning(
"OKX IOC partial fill on timeout ordId=%s acc=%s",
ord_id,
acc,
)
return self._fill_from_order_row(inst_id, ord_id, last)
raise RuntimeError(f"OKX 订单未完全成交 ordId={ord_id} last={last}")
def sum_fill_fees(self, inst_id: str, ord_id: str) -> tuple[float, str]: