Fix LIVE open/close double-book and security audit findings.
Prevent expiry dual-close from re-booking option cash, abandon ledger rejection, margin-mode mismatch, and manual close races; harden fill wait and refuse default AUTH_SECRET on LIVE. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -157,7 +157,26 @@ class OkxTradeClient:
|
||||
fill = self._wait_fill(inst_id, ord_id)
|
||||
return fill
|
||||
|
||||
def _wait_fill(self, inst_id: str, ord_id: str, *, tries: int = 20) -> LiveFill:
|
||||
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
|
||||
sz = safe_float(row.get("accFillSz")) or safe_float(row.get("sz")) or 0.0
|
||||
fee = abs(safe_float(row.get("fee")) or 0.0)
|
||||
fee_ccy = str(row.get("feeCcy") or "USDT")
|
||||
if fee <= 0 and ord_id:
|
||||
fee, fee_ccy = self.sum_fill_fees(inst_id, ord_id)
|
||||
from .money import abs_fee_usdt
|
||||
|
||||
return LiveFill(
|
||||
inst_id=inst_id,
|
||||
side=str(row.get("side") or ""),
|
||||
avg_px=float(avg),
|
||||
sz=float(sz),
|
||||
fee=abs_fee_usdt(fee, fee_ccy),
|
||||
ord_id=ord_id,
|
||||
raw=row,
|
||||
)
|
||||
|
||||
def _wait_fill(self, inst_id: str, ord_id: str, *, tries: int = 40) -> LiveFill:
|
||||
path = f"/api/v5/trade/order?instId={inst_id}&ordId={ord_id}"
|
||||
last: dict[str, Any] = {}
|
||||
for _ in range(tries):
|
||||
@@ -168,25 +187,21 @@ class OkxTradeClient:
|
||||
avg = safe_float(last.get("avgPx"))
|
||||
# 仅完全成交;部分成交继续等,避免账本张数与交易所不一致
|
||||
if state == "filled" and avg and avg > 0:
|
||||
sz = safe_float(last.get("accFillSz")) or safe_float(last.get("sz")) or 0.0
|
||||
fee = abs(safe_float(last.get("fee")) or 0.0)
|
||||
fee_ccy = str(last.get("feeCcy") or "USDT")
|
||||
if fee <= 0 and ord_id:
|
||||
fee, fee_ccy = self.sum_fill_fees(inst_id, ord_id)
|
||||
from .money import abs_fee_usdt
|
||||
|
||||
return LiveFill(
|
||||
inst_id=inst_id,
|
||||
side=str(last.get("side") or ""),
|
||||
avg_px=float(avg),
|
||||
sz=float(sz),
|
||||
fee=abs_fee_usdt(fee, fee_ccy),
|
||||
ord_id=ord_id,
|
||||
raw=last,
|
||||
)
|
||||
return self._fill_from_order_row(inst_id, ord_id, last)
|
||||
if state in ("canceled", "failed"):
|
||||
raise RuntimeError(f"OKX 订单失败 state={state} {last}")
|
||||
time.sleep(0.3)
|
||||
# 超时兜底:已 filled 或已有成交量+均价则回填,避免「有仓无账」
|
||||
state = str(last.get("state") or "")
|
||||
avg = safe_float(last.get("avgPx"))
|
||||
acc = safe_float(last.get("accFillSz")) or 0.0
|
||||
if state == "filled" or (acc > 0 and avg and avg > 0):
|
||||
logger.warning(
|
||||
"OKX fill wait timeout but using last fill data ordId=%s state=%s",
|
||||
ord_id,
|
||||
state,
|
||||
)
|
||||
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]:
|
||||
@@ -305,24 +320,29 @@ class OkxTradeClient:
|
||||
from .money import to_usdt
|
||||
|
||||
end = int(end_ms or int(time.time() * 1000))
|
||||
begin = int(begin_ms)
|
||||
# OKX:after=更早时间戳边界,before=更晚;再本地按 uTime 过滤兜底
|
||||
path = (
|
||||
f"/api/v5/account/positions-history?instType=SWAP&instId={inst_id}"
|
||||
f"&before={end}&after={int(begin_ms)}"
|
||||
f"&after={begin}&before={end}"
|
||||
)
|
||||
try:
|
||||
# positions-history 用 GET query;部分环境用 before/after 语义相反,失败则返回 None
|
||||
rows = self._request(
|
||||
"GET",
|
||||
f"/api/v5/account/positions-history?instType=SWAP&instId={inst_id}",
|
||||
)
|
||||
rows = self._request("GET", path)
|
||||
except Exception as e:
|
||||
logger.warning("okx positions-history failed: %s", e)
|
||||
return None
|
||||
try:
|
||||
rows = self._request(
|
||||
"GET",
|
||||
f"/api/v5/account/positions-history?instType=SWAP&instId={inst_id}",
|
||||
)
|
||||
except Exception as e2:
|
||||
logger.warning("okx positions-history fallback failed: %s", e2)
|
||||
return None
|
||||
total = 0.0
|
||||
hit = False
|
||||
for row in rows:
|
||||
u_time = int(safe_float(row.get("uTime")) or safe_float(row.get("cTime")) or 0)
|
||||
if u_time and (u_time < int(begin_ms) - 60_000 or u_time > end + 60_000):
|
||||
if u_time and (u_time < begin - 60_000 or u_time > end + 60_000):
|
||||
continue
|
||||
rpnl = safe_float(row.get("realizedPnl"))
|
||||
if rpnl is None:
|
||||
|
||||
Reference in New Issue
Block a user