fix: use isolated margin for OKX option buys (error 51019)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-07 10:24:33 +08:00
parent 2a3f36d34f
commit 5fdfc67438
3 changed files with 56 additions and 12 deletions
+52 -8
View File
@@ -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]]:
+3 -3
View File
@@ -17,7 +17,7 @@ from lib.options.options_pricing_lib import (
premium_per_sheet,
total_premium,
)
from lib.exchange.okx_options_lib import _pos_side_from_position, _safe_float
from lib.exchange.okx_options_lib import _pos_side_from_position, _safe_float, td_mode_for_option_buy
def _env_bool(key: str, default: bool = False) -> bool:
@@ -89,7 +89,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
"max_dte_days": _env_float("OKX_OPTIONS_MAX_DTE_DAYS", 2.0),
"chain_max_dte_days": _env_float("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS", 14.0),
"itm_max_dist": _env_float("OKX_OPTIONS_ITM_MAX_DIST_USD", 30.0),
"td_mode": (os.getenv("OKX_OPTIONS_TD_MODE") or "cross").strip(),
"td_mode": (os.getenv("OKX_OPTIONS_TD_MODE") or "isolated").strip(),
"allow_market_close": _env_bool("OKX_OPTIONS_ALLOW_MARKET_CLOSE", False),
"profit_ratio": _env_float("OKX_OPTIONS_PROFIT_ALERT_RATIO", 1.0),
"poll_seconds": _env_float("OKX_OPTIONS_POLL_SECONDS", 15.0),
@@ -250,7 +250,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
side="buy",
sheets=sheets,
price=float(ask),
td_mode=cfg["td_mode"],
td_mode=td_mode_for_option_buy(cfg["td_mode"]),
tick_sz=tick_sz,
)
if not order.get("ok"):