fix: OKX options buy/close orders with tick alignment and reduceOnly
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+125
-13
@@ -1,6 +1,7 @@
|
||||
"""OKX USDⓈ 期权 API 封装(主账户 exchange_options 专用)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from typing import Any, Callable
|
||||
|
||||
@@ -38,6 +39,57 @@ def _safe_float(v: Any) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def round_option_px(px: float, tick_sz: Any, side: str) -> float:
|
||||
"""按 OKX tickSz 对齐:买入向上取整,卖出向下取整。"""
|
||||
tick = _safe_float(tick_sz)
|
||||
if tick is None or tick <= 0 or px <= 0:
|
||||
return px
|
||||
steps = px / tick
|
||||
side_l = (side or "").lower()
|
||||
if side_l == "buy":
|
||||
return math.ceil(steps - 1e-12) * tick
|
||||
return math.floor(steps + 1e-12) * tick
|
||||
|
||||
|
||||
def format_option_px(px: float, tick_sz: Any) -> str:
|
||||
tick = _safe_float(tick_sz)
|
||||
if tick is None or tick <= 0:
|
||||
return str(px)
|
||||
decimals = max(0, -int(round(math.log10(tick)))) if tick < 1 else 0
|
||||
if tick >= 1:
|
||||
decimals = len(str(tick).split(".")[-1]) if "." in str(tick) else 0
|
||||
return f"{px:.{decimals}f}".rstrip("0").rstrip(".") or "0"
|
||||
|
||||
|
||||
def _fetch_book_bid_ask(ex: ccxt.okx, inst_id: str) -> tuple[float | None, float | None]:
|
||||
try:
|
||||
rows = ex.public_get_market_books({"instId": inst_id, "sz": "1"}).get("data") or []
|
||||
if not rows:
|
||||
return None, None
|
||||
row = rows[0]
|
||||
asks = row.get("asks") or []
|
||||
bids = row.get("bids") or []
|
||||
ask = _safe_float(asks[0][0]) if asks else None
|
||||
bid = _safe_float(bids[0][0]) if bids else None
|
||||
return bid, ask
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
|
||||
def _pos_side_from_position(pos: dict[str, Any] | None) -> str | None:
|
||||
if not pos:
|
||||
return None
|
||||
ps = str(pos.get("posSide") or "").strip().lower()
|
||||
if ps in ("long", "short", "net"):
|
||||
return ps
|
||||
sheets = _safe_float(pos.get("pos")) or 0.0
|
||||
if sheets > 0:
|
||||
return "long"
|
||||
if sheets < 0:
|
||||
return "short"
|
||||
return "net"
|
||||
|
||||
|
||||
def _extract_ccy_balance(balance: dict[str, Any], ccy: str) -> float | None:
|
||||
ccy = (ccy or "").upper()
|
||||
if not isinstance(balance, dict):
|
||||
@@ -200,15 +252,29 @@ def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]:
|
||||
meta = meta_rows[0]
|
||||
t_rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
|
||||
t = t_rows[0] if t_rows else {}
|
||||
ask = _safe_float(t.get("askPx"))
|
||||
bid = _safe_float(t.get("bidPx"))
|
||||
if ask is None or bid is None:
|
||||
book_bid, book_ask = _fetch_book_bid_ask(ex, inst_id)
|
||||
if ask is None:
|
||||
ask = book_ask
|
||||
if bid is None:
|
||||
bid = book_bid
|
||||
mark = _safe_float(t.get("markPx"))
|
||||
tick_sz = meta.get("tickSz")
|
||||
if ask is None and mark is not None:
|
||||
ask = round_option_px(mark, tick_sz, "buy")
|
||||
if bid is None and mark is not None:
|
||||
bid = round_option_px(mark, tick_sz, "sell")
|
||||
uly = str(meta.get("uly") or "")
|
||||
idx = fetch_index_price(ex, uly)
|
||||
return {
|
||||
"ok": True,
|
||||
"inst_id": inst_id,
|
||||
"meta": meta,
|
||||
"ask": _safe_float(t.get("askPx")),
|
||||
"bid": _safe_float(t.get("bidPx")),
|
||||
"mark": _safe_float(t.get("markPx")),
|
||||
"ask": ask,
|
||||
"bid": bid,
|
||||
"mark": mark,
|
||||
"index_px": idx,
|
||||
"ct_mult": _safe_float(meta.get("ctMult")) or 0.01,
|
||||
"min_sz": int(_safe_float(meta.get("minSz")) or 1),
|
||||
@@ -227,23 +293,69 @@ def place_option_limit_order(
|
||||
sheets: int,
|
||||
price: float,
|
||||
td_mode: str = "cross",
|
||||
tick_sz: Any = None,
|
||||
reduce_only: bool = False,
|
||||
pos_side: str | None = None,
|
||||
) -> 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"}
|
||||
px = round_option_px(float(price), tick_sz, side_l)
|
||||
if px <= 0:
|
||||
return {"ok": False, "msg": "价格无效"}
|
||||
body: dict[str, Any] = {
|
||||
"instId": inst_id,
|
||||
"tdMode": td_mode,
|
||||
"side": side_l,
|
||||
"ordType": "limit",
|
||||
"px": format_option_px(px, tick_sz),
|
||||
"sz": str(int(sheets)),
|
||||
}
|
||||
if pos_side:
|
||||
body["posSide"] = pos_side
|
||||
if reduce_only:
|
||||
body["reduceOnly"] = True
|
||||
try:
|
||||
resp = ex.private_post_trade_order(
|
||||
{
|
||||
"instId": inst_id,
|
||||
"tdMode": td_mode,
|
||||
"side": side_l,
|
||||
"ordType": "limit",
|
||||
"px": str(price),
|
||||
"sz": str(int(sheets)),
|
||||
}
|
||||
)
|
||||
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}
|
||||
msg = data[0].get("sMsg") if data else str(resp)
|
||||
return {"ok": False, "msg": msg or "下单失败", "raw": resp, "px": px}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e), "px": px}
|
||||
|
||||
|
||||
def place_option_market_order(
|
||||
ex: ccxt.okx,
|
||||
*,
|
||||
inst_id: str,
|
||||
side: str,
|
||||
sheets: int,
|
||||
td_mode: str = "cross",
|
||||
reduce_only: bool = False,
|
||||
pos_side: str | None = None,
|
||||
) -> 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"}
|
||||
body: dict[str, Any] = {
|
||||
"instId": inst_id,
|
||||
"tdMode": td_mode,
|
||||
"side": side_l,
|
||||
"ordType": "market",
|
||||
"sz": str(int(sheets)),
|
||||
}
|
||||
if pos_side:
|
||||
body["posSide"] = pos_side
|
||||
if reduce_only:
|
||||
body["reduceOnly"] = True
|
||||
try:
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user