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]]:
|
||||
|
||||
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user