fix: use isolated margin for OKX option buys (error 51019)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -118,7 +118,7 @@ OKX_SUB_ACCOUNT_NAME=
|
|||||||
OKX_OPTIONS_ITM_MAX_DIST_USD=30
|
OKX_OPTIONS_ITM_MAX_DIST_USD=30
|
||||||
OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0
|
OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0
|
||||||
OKX_OPTIONS_POLL_SECONDS=15
|
OKX_OPTIONS_POLL_SECONDS=15
|
||||||
OKX_OPTIONS_TD_MODE=cross
|
OKX_OPTIONS_TD_MODE=isolated
|
||||||
OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
|
OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""OKX USDⓈ 期权 API 封装(主账户 exchange_options 专用)。"""
|
"""OKX USDⓈ 期权 API 封装(主账户 exchange_options 专用)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable
|
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
|
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(
|
def create_options_exchange(
|
||||||
api_key: str,
|
api_key: str,
|
||||||
@@ -327,7 +373,7 @@ def place_option_limit_order(
|
|||||||
side: str,
|
side: str,
|
||||||
sheets: int,
|
sheets: int,
|
||||||
price: float,
|
price: float,
|
||||||
td_mode: str = "cross",
|
td_mode: str = "isolated",
|
||||||
tick_sz: Any = None,
|
tick_sz: Any = None,
|
||||||
reduce_only: bool = False,
|
reduce_only: bool = False,
|
||||||
pos_side: str | None = None,
|
pos_side: str | None = None,
|
||||||
@@ -357,10 +403,9 @@ def place_option_limit_order(
|
|||||||
data = (resp or {}).get("data") or []
|
data = (resp or {}).get("data") or []
|
||||||
if data and str(data[0].get("sCode")) == "0":
|
if data and str(data[0].get("sCode")) == "0":
|
||||||
return {"ok": True, "data": data[0], "raw": resp, "px": px}
|
return {"ok": True, "data": data[0], "raw": resp, "px": px}
|
||||||
msg = data[0].get("sMsg") if data else str(resp)
|
return {"ok": False, "msg": _okx_trade_error_message(resp=resp), "raw": resp, "px": px}
|
||||||
return {"ok": False, "msg": msg or "下单失败", "raw": resp, "px": px}
|
|
||||||
except Exception as e:
|
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(
|
def place_option_market_order(
|
||||||
@@ -369,7 +414,7 @@ def place_option_market_order(
|
|||||||
inst_id: str,
|
inst_id: str,
|
||||||
side: str,
|
side: str,
|
||||||
sheets: int,
|
sheets: int,
|
||||||
td_mode: str = "cross",
|
td_mode: str = "isolated",
|
||||||
reduce_only: bool = False,
|
reduce_only: bool = False,
|
||||||
pos_side: str | None = None,
|
pos_side: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -394,10 +439,9 @@ def place_option_market_order(
|
|||||||
data = (resp or {}).get("data") or []
|
data = (resp or {}).get("data") or []
|
||||||
if data and str(data[0].get("sCode")) == "0":
|
if data and str(data[0].get("sCode")) == "0":
|
||||||
return {"ok": True, "data": data[0], "raw": resp}
|
return {"ok": True, "data": data[0], "raw": resp}
|
||||||
msg = data[0].get("sMsg") if data else str(resp)
|
return {"ok": False, "msg": _okx_trade_error_message(resp=resp), "raw": resp}
|
||||||
return {"ok": False, "msg": msg or "下单失败", "raw": resp}
|
|
||||||
except Exception as e:
|
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]]:
|
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,
|
premium_per_sheet,
|
||||||
total_premium,
|
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:
|
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),
|
"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),
|
"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),
|
"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),
|
"allow_market_close": _env_bool("OKX_OPTIONS_ALLOW_MARKET_CLOSE", False),
|
||||||
"profit_ratio": _env_float("OKX_OPTIONS_PROFIT_ALERT_RATIO", 1.0),
|
"profit_ratio": _env_float("OKX_OPTIONS_PROFIT_ALERT_RATIO", 1.0),
|
||||||
"poll_seconds": _env_float("OKX_OPTIONS_POLL_SECONDS", 15.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",
|
side="buy",
|
||||||
sheets=sheets,
|
sheets=sheets,
|
||||||
price=float(ask),
|
price=float(ask),
|
||||||
td_mode=cfg["td_mode"],
|
td_mode=td_mode_for_option_buy(cfg["td_mode"]),
|
||||||
tick_sz=tick_sz,
|
tick_sz=tick_sz,
|
||||||
)
|
)
|
||||||
if not order.get("ok"):
|
if not order.get("ok"):
|
||||||
|
|||||||
Reference in New Issue
Block a user