fix: use isolated margin for OKX option buys (error 51019)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
"""OKX USDⓈ 期权 API 封装(主账户 exchange_options 专用)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Callable
|
||||
|
||||
@@ -9,6 +11,50 @@ import ccxt
|
||||
|
||||
from lib.options.options_pricing_lib import is_shallow_itm, option_moneyness, option_moneyness_label
|
||||
|
||||
_OKX_OPTION_ERR_ZH: dict[str, str] = {
|
||||
"51018": "期权账户不能持有净空头头寸",
|
||||
"51019": "期权买入须使用逐仓模式(全仓模式下不能持有多头净头寸)",
|
||||
}
|
||||
|
||||
|
||||
def _okx_trade_error_message(exc: BaseException | None = None, resp: Any = None) -> str:
|
||||
row: dict[str, Any] | None = None
|
||||
if isinstance(resp, dict):
|
||||
data = resp.get("data") or []
|
||||
if data and isinstance(data[0], dict):
|
||||
row = data[0]
|
||||
if row is None and exc is not None:
|
||||
text = str(exc)
|
||||
match = re.search(r"\{.*\}", text, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
payload = json.loads(match.group(0))
|
||||
data = payload.get("data") or []
|
||||
if data and isinstance(data[0], dict):
|
||||
row = data[0]
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if row:
|
||||
code = str(row.get("sCode") or "")
|
||||
zh = _OKX_OPTION_ERR_ZH.get(code)
|
||||
if zh:
|
||||
return zh
|
||||
msg = str(row.get("sMsg") or "").strip()
|
||||
if msg:
|
||||
return msg
|
||||
if exc is not None:
|
||||
text = str(exc).strip()
|
||||
if text.lower().startswith("okx "):
|
||||
text = text[4:].strip()
|
||||
return text or "下单失败"
|
||||
return "下单失败"
|
||||
|
||||
|
||||
def td_mode_for_option_buy(configured: str | None = None) -> str:
|
||||
"""OKX 买入期权(多头)必须使用逐仓。"""
|
||||
mode = (configured or "isolated").strip().lower()
|
||||
return "isolated" if mode == "cross" else mode or "isolated"
|
||||
|
||||
|
||||
def create_options_exchange(
|
||||
api_key: str,
|
||||
@@ -327,7 +373,7 @@ def place_option_limit_order(
|
||||
side: str,
|
||||
sheets: int,
|
||||
price: float,
|
||||
td_mode: str = "cross",
|
||||
td_mode: str = "isolated",
|
||||
tick_sz: Any = None,
|
||||
reduce_only: bool = False,
|
||||
pos_side: str | None = None,
|
||||
@@ -357,10 +403,9 @@ def place_option_limit_order(
|
||||
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}
|
||||
return {"ok": False, "msg": _okx_trade_error_message(resp=resp), "raw": resp, "px": px}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e), "px": px}
|
||||
return {"ok": False, "msg": _okx_trade_error_message(e), "px": px}
|
||||
|
||||
|
||||
def place_option_market_order(
|
||||
@@ -369,7 +414,7 @@ def place_option_market_order(
|
||||
inst_id: str,
|
||||
side: str,
|
||||
sheets: int,
|
||||
td_mode: str = "cross",
|
||||
td_mode: str = "isolated",
|
||||
reduce_only: bool = False,
|
||||
pos_side: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -394,10 +439,9 @@ def place_option_market_order(
|
||||
data = (resp or {}).get("data") or []
|
||||
if data and str(data[0].get("sCode")) == "0":
|
||||
return {"ok": True, "data": data[0], "raw": resp}
|
||||
msg = data[0].get("sMsg") if data else str(resp)
|
||||
return {"ok": False, "msg": msg or "下单失败", "raw": resp}
|
||||
return {"ok": False, "msg": _okx_trade_error_message(resp=resp), "raw": resp}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e)}
|
||||
return {"ok": False, "msg": _okx_trade_error_message(e)}
|
||||
|
||||
|
||||
def fetch_option_positions(ex: ccxt.okx) -> list[dict[str, Any]]:
|
||||
|
||||
Reference in New Issue
Block a user