Preflight options position limit for dual-leg OO hedges.
Reject OO start when max active is under 2 free slots so limit=1 cannot open a half straddle. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -111,7 +111,7 @@ OKX_OPTIONS_API_PASSPHRASE=
|
||||
OKX_OPTIONS_ACCOUNT_LABEL=主账户·期权
|
||||
OKX_OPTIONS_TRADE_BUDGET_USDC=10
|
||||
OKX_OPTIONS_BUDGET_BUFFER=0.95
|
||||
# 期权同时持仓上限(笔,按交易所合约笔数);0=不限制;同合约加仓不占新笔数;热更
|
||||
# 期权同时持仓上限(笔,按交易所合约笔数);0=不限制;期期需≥2(或 0);同合约加仓不占新笔数;热更
|
||||
OKX_OPTIONS_MAX_ACTIVE_POSITIONS=0
|
||||
OKX_OPTIONS_DEFAULT_UNDERLY=ETH
|
||||
# 期权链仅显示卖一深度≥1张的合约(估算卖一/无深度不显示);false 则显示全部
|
||||
|
||||
Vendored
+1
-1
@@ -133,7 +133,7 @@ _OPTIONS_SECTION: dict[str, Any] = {
|
||||
(
|
||||
"OKX_OPTIONS_MAX_ACTIVE_POSITIONS",
|
||||
"期权持仓上限(笔)",
|
||||
"默认 0=不限制;按交易所当前期权合约笔数计数,达上限禁止新开买(同合约加仓仍允许)",
|
||||
"默认 0=不限制;按交易所期权合约笔数计数.期期对冲一次需 2 笔,上限设 1 时无法开期期(须≥2 或 0).同合约加仓不占新笔数.",
|
||||
),
|
||||
("OKX_OPTIONS_DEFAULT_UNDERLY", "默认标的", "如 ETH"),
|
||||
(
|
||||
|
||||
@@ -690,7 +690,24 @@ def execute_options_options_start(
|
||||
results: list[dict[str, Any]] = []
|
||||
leg_a = body.get("leg_a") or {}
|
||||
leg_b = body.get("leg_b") or {}
|
||||
a_res = _buy_option(cfg, inst_id=str(leg_a.get("inst_id") or ""), sheets=float(leg_a.get("sheets") or 1), dry_run=dry_run)
|
||||
inst_a = str(leg_a.get("inst_id") or "")
|
||||
inst_b = str(leg_b.get("inst_id") or "")
|
||||
from lib.options.options_position_limit_lib import option_position_limit_block_msg
|
||||
|
||||
pos_limit_msg = option_position_limit_block_msg(
|
||||
cfg.get("exchange_options"),
|
||||
opening_inst_ids=[inst_a, inst_b],
|
||||
fetch_positions=cfg.get("fetch_option_positions"),
|
||||
)
|
||||
if pos_limit_msg:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": pos_limit_msg,
|
||||
"path": path,
|
||||
"results": [],
|
||||
"refresh": refresh,
|
||||
}
|
||||
a_res = _buy_option(cfg, inst_id=inst_a, sheets=float(leg_a.get("sheets") or 1), dry_run=dry_run)
|
||||
results.append({"step": "options_buy_limit", "leg": "a", **a_res})
|
||||
if not a_res.get("ok"):
|
||||
return {
|
||||
@@ -700,7 +717,7 @@ def execute_options_options_start(
|
||||
"results": results,
|
||||
"refresh": refresh,
|
||||
}
|
||||
b_res = _buy_option(cfg, inst_id=str(leg_b.get("inst_id") or ""), sheets=float(leg_b.get("sheets") or 1), dry_run=dry_run)
|
||||
b_res = _buy_option(cfg, inst_id=inst_b, sheets=float(leg_b.get("sheets") or 1), dry_run=dry_run)
|
||||
results.append({"step": "options_buy_limit", "leg": "b", **b_res})
|
||||
if not b_res.get("ok"):
|
||||
if not dry_run and partial_auto_close_enabled():
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Sequence
|
||||
|
||||
|
||||
def options_max_active_positions() -> int:
|
||||
@@ -41,17 +41,36 @@ def _inst_already_open(rows: list[dict[str, Any]], inst_id: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _normalize_inst_ids(
|
||||
opening_inst_id: str = "",
|
||||
opening_inst_ids: Optional[Sequence[str]] = None,
|
||||
) -> list[str]:
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in list(opening_inst_ids or []) + ([opening_inst_id] if opening_inst_id else []):
|
||||
iid = str(raw or "").strip()
|
||||
if not iid or iid in seen:
|
||||
continue
|
||||
seen.add(iid)
|
||||
out.append(iid)
|
||||
return out
|
||||
|
||||
|
||||
def option_position_limit_block_msg(
|
||||
ex: Any,
|
||||
*,
|
||||
opening_inst_id: str = "",
|
||||
opening_inst_ids: Optional[Sequence[str]] = None,
|
||||
new_positions: Optional[int] = None,
|
||||
max_active: Optional[int] = None,
|
||||
fetch_positions=None,
|
||||
) -> Optional[str]:
|
||||
"""若禁止新开买期权则返回中文原因,否则 None.
|
||||
|
||||
- max_active<=0:不限制
|
||||
- 加仓已有合约(opening_inst_id 已在持仓中):不占新笔数,放行
|
||||
- opening_inst_ids:本次要开的合约;已在持仓中的不占新笔数
|
||||
- new_positions:显式指定还需新占几笔(默认按 opening_inst_ids 推算)
|
||||
- 期期两腿应一次传入两个 inst_id,在开仓前预检,避免上限=1 时开出半边仓
|
||||
- 拉持仓失败:拒绝开仓(避免绕过上限)
|
||||
"""
|
||||
mx = options_max_active_positions() if max_active is None else int(max_active)
|
||||
@@ -69,8 +88,26 @@ def option_position_limit_block_msg(
|
||||
if rows is None:
|
||||
return f"无法获取期权持仓,暂不可开仓(上限 {mx} 笔)"
|
||||
active = count_live_option_positions(rows)
|
||||
if _inst_already_open(rows, opening_inst_id):
|
||||
ids = _normalize_inst_ids(opening_inst_id, opening_inst_ids)
|
||||
|
||||
if new_positions is None:
|
||||
if ids:
|
||||
already = sum(1 for i in ids if _inst_already_open(rows, i))
|
||||
need = max(0, len(ids) - already)
|
||||
else:
|
||||
need = 1
|
||||
else:
|
||||
need = max(0, int(new_positions))
|
||||
if need <= 1 and len(ids) == 1 and _inst_already_open(rows, ids[0]):
|
||||
return None
|
||||
|
||||
if need <= 0:
|
||||
return None
|
||||
if active >= mx:
|
||||
return f"期权持仓已达上限({active}/{mx}),请先平仓后再开"
|
||||
return None
|
||||
if active + need <= mx:
|
||||
return None
|
||||
if need >= 2:
|
||||
return (
|
||||
f"期期对冲需新开 {need} 笔期权,当前已有 {active} 笔、上限 {mx};"
|
||||
f"请将 OKX_OPTIONS_MAX_ACTIVE_POSITIONS 设为 0(不限制)或不小于 {active + need},或先平仓"
|
||||
)
|
||||
return f"期权持仓已达上限({active}/{mx}),请先平仓后再开"
|
||||
|
||||
@@ -56,6 +56,36 @@ class OptionsPositionLimitTests(unittest.TestCase):
|
||||
)
|
||||
self.assertIsNone(msg)
|
||||
|
||||
def test_oo_needs_two_slots_when_max_one(self):
|
||||
msg = option_position_limit_block_msg(
|
||||
object(),
|
||||
opening_inst_ids=["ETH-C", "ETH-P"],
|
||||
max_active=1,
|
||||
fetch_positions=lambda _ex: [],
|
||||
)
|
||||
self.assertIsNotNone(msg)
|
||||
self.assertIn("期期", msg or "")
|
||||
|
||||
def test_oo_ok_when_max_two_empty(self):
|
||||
msg = option_position_limit_block_msg(
|
||||
object(),
|
||||
opening_inst_ids=["ETH-C", "ETH-P"],
|
||||
max_active=2,
|
||||
fetch_positions=lambda _ex: [],
|
||||
)
|
||||
self.assertIsNone(msg)
|
||||
|
||||
def test_oo_block_when_one_slot_left(self):
|
||||
rows = [{"instId": "OTHER", "pos": "1"}]
|
||||
msg = option_position_limit_block_msg(
|
||||
object(),
|
||||
opening_inst_ids=["ETH-C", "ETH-P"],
|
||||
max_active=2,
|
||||
fetch_positions=lambda _ex: rows,
|
||||
)
|
||||
self.assertIsNotNone(msg)
|
||||
self.assertIn("期期", msg or "")
|
||||
|
||||
def test_fail_closed_when_fetch_none(self):
|
||||
msg = option_position_limit_block_msg(
|
||||
object(),
|
||||
|
||||
Reference in New Issue
Block a user