diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index 82a90a2..a811dd2 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -118,7 +118,7 @@ OKX_SUB_ACCOUNT_NAME= OKX_OPTIONS_ITM_MAX_DIST_USD=30 OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0 OKX_OPTIONS_POLL_SECONDS=15 -OKX_OPTIONS_TD_MODE=cross +OKX_OPTIONS_TD_MODE=isolated OKX_OPTIONS_ALLOW_MARKET_CLOSE=false # ============================================================================= diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index e408756..1c6eb2d 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -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]]: diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 62011ed..b3408d0 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -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"):