Add OKX options max active positions env gate.
Enforce concurrent option contract count on standalone and hedge buys; editable in env UI with hot reload (0 = unlimited). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,7 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
if not ok:
|
||||
return {"ok": False, "enabled": True, "msg": reason or "期权 API 未配置"}
|
||||
try:
|
||||
from lib.options.options_position_limit_lib import options_max_active_positions
|
||||
from lib.options.options_positions_lib import build_display_option_positions
|
||||
|
||||
raw = cfg["fetch_option_positions"](ex)
|
||||
@@ -93,6 +94,7 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
"stats": {},
|
||||
"trade_budget": cfg.get("trade_budget"),
|
||||
"account_label": cfg.get("account_label") or "OKX期权",
|
||||
"max_active_positions": options_max_active_positions(),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"ok": False, "enabled": True, "msg": str(e)}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""OKX 期权持仓笔数上限(env: OKX_OPTIONS_MAX_ACTIVE_POSITIONS)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def options_max_active_positions() -> int:
|
||||
"""同时持有的期权合约笔数上限;0=不限制.热更读 env."""
|
||||
raw = os.getenv("OKX_OPTIONS_MAX_ACTIVE_POSITIONS", "0")
|
||||
try:
|
||||
v = int(float(str(raw).strip()))
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
return max(0, v)
|
||||
|
||||
|
||||
def count_live_option_positions(rows: Optional[list[dict[str, Any]]]) -> int:
|
||||
if not rows:
|
||||
return 0
|
||||
n = 0
|
||||
for r in rows:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
try:
|
||||
pos = float(r.get("pos") or 0)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if abs(pos) >= 1e-12:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def _inst_already_open(rows: list[dict[str, Any]], inst_id: str) -> bool:
|
||||
want = (inst_id or "").strip()
|
||||
if not want:
|
||||
return False
|
||||
for r in rows:
|
||||
if str(r.get("instId") or r.get("inst_id") or "").strip() == want:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def option_position_limit_block_msg(
|
||||
ex: Any,
|
||||
*,
|
||||
opening_inst_id: str = "",
|
||||
max_active: Optional[int] = None,
|
||||
fetch_positions=None,
|
||||
) -> Optional[str]:
|
||||
"""若禁止新开买期权则返回中文原因,否则 None.
|
||||
|
||||
- max_active<=0:不限制
|
||||
- 加仓已有合约(opening_inst_id 已在持仓中):不占新笔数,放行
|
||||
- 拉持仓失败:拒绝开仓(避免绕过上限)
|
||||
"""
|
||||
mx = options_max_active_positions() if max_active is None else int(max_active)
|
||||
if mx <= 0:
|
||||
return None
|
||||
fetch = fetch_positions
|
||||
if fetch is None:
|
||||
from lib.exchange.okx_options_lib import fetch_option_positions
|
||||
|
||||
fetch = fetch_option_positions
|
||||
try:
|
||||
rows = fetch(ex)
|
||||
except Exception:
|
||||
rows = None
|
||||
if rows is None:
|
||||
return f"无法获取期权持仓,暂不可开仓(上限 {mx} 笔)"
|
||||
active = count_live_option_positions(rows)
|
||||
if _inst_already_open(rows, opening_inst_id):
|
||||
return None
|
||||
if active >= mx:
|
||||
return f"期权持仓已达上限({active}/{mx}),请先平仓后再开"
|
||||
return None
|
||||
@@ -478,6 +478,33 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
"budget_full_usdc": budget if mode == "budget_full" else None,
|
||||
}
|
||||
)
|
||||
from lib.options.options_position_limit_lib import option_position_limit_block_msg
|
||||
|
||||
pos_limit_msg = option_position_limit_block_msg(
|
||||
ex,
|
||||
opening_inst_id=inst_id,
|
||||
fetch_positions=cfg.get("fetch_option_positions"),
|
||||
)
|
||||
if pos_limit_msg:
|
||||
return jsonify(
|
||||
{
|
||||
**q,
|
||||
"ok": True,
|
||||
"can_open": False,
|
||||
"msg": pos_limit_msg,
|
||||
"quote_per_unit": ask,
|
||||
"premium_per_sheet": None,
|
||||
"sizing": {
|
||||
"ok": False,
|
||||
"msg": pos_limit_msg,
|
||||
"sheets": 0,
|
||||
"eth_amount": 0.0,
|
||||
"total_premium": 0.0,
|
||||
},
|
||||
"available_usdc": available_usdc,
|
||||
"budget_full_usdc": budget if mode == "budget_full" else None,
|
||||
}
|
||||
)
|
||||
sizing = calc_order_size(
|
||||
quote_per_unit=float(ask),
|
||||
ct_mult=float(ct_mult),
|
||||
@@ -582,6 +609,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
"ref_ask": q.get("ref_ask"),
|
||||
}
|
||||
)
|
||||
from lib.options.options_position_limit_lib import option_position_limit_block_msg
|
||||
|
||||
pos_limit_msg = option_position_limit_block_msg(
|
||||
ex,
|
||||
opening_inst_id=inst_id,
|
||||
fetch_positions=cfg.get("fetch_option_positions"),
|
||||
)
|
||||
if pos_limit_msg:
|
||||
return jsonify({"ok": False, "msg": pos_limit_msg, "can_open": False})
|
||||
ct_mult = float(q.get("ct_mult") or 0.01)
|
||||
min_sz = int(q.get("min_sz") or 1)
|
||||
eth_amount = None
|
||||
|
||||
Reference in New Issue
Block a user