Hedge options open: require full fill (IOC + wait) before success.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -962,6 +962,119 @@ def cancel_option_order(ex: ccxt.okx, *, inst_id: str, ord_id: str) -> dict[str,
|
||||
return {"ok": False, "msg": _okx_trade_error_message(e)}
|
||||
|
||||
|
||||
def fetch_option_order(ex: ccxt.okx, *, inst_id: str, ord_id: str) -> dict[str, Any]:
|
||||
"""查询单笔期权订单状态."""
|
||||
inst_id = (inst_id or "").strip()
|
||||
ord_id = (ord_id or "").strip()
|
||||
if not inst_id or not ord_id:
|
||||
return {"ok": False, "msg": "缺少 inst_id 或 ord_id"}
|
||||
try:
|
||||
resp = ex.private_get_trade_order({"instId": inst_id, "ordId": ord_id})
|
||||
data = (resp or {}).get("data") or []
|
||||
if not data or not isinstance(data[0], dict):
|
||||
return {"ok": False, "msg": "订单不存在或暂不可查", "raw": resp}
|
||||
o = data[0]
|
||||
sz = _safe_float(o.get("sz"))
|
||||
acc = _safe_float(o.get("accFillSz"))
|
||||
if acc is None:
|
||||
acc = _safe_float(o.get("fillSz")) or 0.0
|
||||
avg = _safe_float(o.get("avgPx"))
|
||||
fill_px = _safe_float(o.get("fillPx"))
|
||||
if avg is None or avg <= 0:
|
||||
avg = fill_px
|
||||
state = str(o.get("state") or "").strip().lower()
|
||||
return {
|
||||
"ok": True,
|
||||
"ord_id": str(o.get("ordId") or ord_id),
|
||||
"inst_id": str(o.get("instId") or inst_id),
|
||||
"state": state,
|
||||
"sz": int(sz) if sz is not None else None,
|
||||
"acc_fill_sz": float(acc or 0),
|
||||
"avg_px": avg,
|
||||
"side": str(o.get("side") or "").lower(),
|
||||
"ord_type": str(o.get("ordType") or ""),
|
||||
"raw": o,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": _okx_trade_error_message(e)}
|
||||
|
||||
|
||||
def wait_option_order_full_fill(
|
||||
ex: ccxt.okx,
|
||||
*,
|
||||
inst_id: str,
|
||||
ord_id: str,
|
||||
need_sheets: int,
|
||||
timeout_sec: float = 12.0,
|
||||
poll_sec: float = 0.35,
|
||||
cancel_on_timeout: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""轮询至完全成交;超时则撤单.未完全成交返回 ok=False."""
|
||||
need = max(1, int(need_sheets))
|
||||
deadline = time.time() + max(0.5, float(timeout_sec))
|
||||
last: dict[str, Any] = {}
|
||||
while time.time() < deadline:
|
||||
last = fetch_option_order(ex, inst_id=inst_id, ord_id=ord_id)
|
||||
if not last.get("ok"):
|
||||
time.sleep(max(0.15, float(poll_sec)))
|
||||
continue
|
||||
acc = float(last.get("acc_fill_sz") or 0)
|
||||
state = str(last.get("state") or "")
|
||||
if acc + 1e-9 >= need or state == "filled":
|
||||
if acc + 1e-9 < need:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": f"订单已结束但成交不足 {need} 张(已成 {acc:g})",
|
||||
"filled_sheets": acc,
|
||||
"order": last,
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"filled_sheets": int(round(acc)),
|
||||
"avg_px": last.get("avg_px"),
|
||||
"state": state,
|
||||
"order": last,
|
||||
}
|
||||
if state in ("canceled", "cancelled", "mmp_canceled"):
|
||||
if acc + 1e-9 >= need:
|
||||
return {
|
||||
"ok": True,
|
||||
"filled_sheets": int(round(acc)),
|
||||
"avg_px": last.get("avg_px"),
|
||||
"state": state,
|
||||
"order": last,
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": f"订单已撤销且未完全成交(已成 {acc:g}/{need})",
|
||||
"filled_sheets": acc,
|
||||
"order": last,
|
||||
}
|
||||
time.sleep(max(0.15, float(poll_sec)))
|
||||
|
||||
if cancel_on_timeout:
|
||||
cancel_option_order(ex, inst_id=inst_id, ord_id=ord_id)
|
||||
time.sleep(0.25)
|
||||
last = fetch_option_order(ex, inst_id=inst_id, ord_id=ord_id)
|
||||
acc = float((last or {}).get("acc_fill_sz") or 0) if (last or {}).get("ok") else 0.0
|
||||
if acc + 1e-9 >= need:
|
||||
return {
|
||||
"ok": True,
|
||||
"filled_sheets": int(round(acc)),
|
||||
"avg_px": (last or {}).get("avg_px"),
|
||||
"state": (last or {}).get("state"),
|
||||
"order": last,
|
||||
"timed_out": True,
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": f"等待成交超时({float(timeout_sec):g}s),已撤未成交部分;已成 {acc:g}/{need}",
|
||||
"filled_sheets": acc,
|
||||
"order": last,
|
||||
"timed_out": True,
|
||||
}
|
||||
|
||||
|
||||
def place_option_limit_order(
|
||||
ex: ccxt.okx,
|
||||
*,
|
||||
@@ -973,12 +1086,16 @@ def place_option_limit_order(
|
||||
tick_sz: Any = None,
|
||||
reduce_only: bool = False,
|
||||
pos_side: str | None = None,
|
||||
ord_type: str = "limit",
|
||||
) -> dict[str, Any]:
|
||||
side_l = (side or "").lower()
|
||||
if side_l not in ("buy", "sell"):
|
||||
return {"ok": False, "msg": "side 必须为 buy 或 sell"}
|
||||
if sheets < 1:
|
||||
return {"ok": False, "msg": "张数至少为 1"}
|
||||
ot = (ord_type or "limit").strip().lower()
|
||||
if ot not in ("limit", "ioc", "fok", "post_only"):
|
||||
return {"ok": False, "msg": f"不支持的 ordType: {ord_type}"}
|
||||
px = round_option_px(float(price), tick_sz, side_l)
|
||||
if px <= 0:
|
||||
return {"ok": False, "msg": "价格无效"}
|
||||
@@ -986,7 +1103,7 @@ def place_option_limit_order(
|
||||
"instId": inst_id,
|
||||
"tdMode": td_mode,
|
||||
"side": side_l,
|
||||
"ordType": "limit",
|
||||
"ordType": ot,
|
||||
"px": format_option_px(px, tick_sz),
|
||||
"sz": str(int(sheets)),
|
||||
}
|
||||
@@ -998,7 +1115,7 @@ def place_option_limit_order(
|
||||
resp = ex.private_post_trade_order(body)
|
||||
data = (resp or {}).get("data") or []
|
||||
if data and str(data[0].get("sCode")) == "0":
|
||||
return {"ok": True, "data": data[0], "raw": resp, "px": px}
|
||||
return {"ok": True, "data": data[0], "raw": resp, "px": px, "ord_type": ot}
|
||||
return {"ok": False, "msg": _okx_trade_error_message(resp=resp), "raw": resp, "px": px}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": _okx_trade_error_message(e), "px": px}
|
||||
|
||||
Reference in New Issue
Block a user