Block option auto-close on stub bids far below mark.
Reject target and depth closes when bid is a residual tick, and show invalid-bid UI instead of recycling at junk prices. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,7 +6,14 @@ import time
|
||||
from typing import Any, Callable
|
||||
|
||||
from lib.options.options_db import init_options_tables
|
||||
from lib.options.options_pricing_lib import estimate_close_by_bids, total_premium
|
||||
from lib.options.options_pricing_lib import (
|
||||
close_ref_prices,
|
||||
estimate_close_by_bids,
|
||||
fetch_option_mark_px,
|
||||
filter_bids_for_close,
|
||||
is_stub_bid_px,
|
||||
total_premium,
|
||||
)
|
||||
|
||||
|
||||
def _safe_float(v: Any) -> float | None:
|
||||
@@ -18,6 +25,24 @@ def _safe_float(v: Any) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = None) -> tuple[float | None, float | None]:
|
||||
from lib.exchange.okx_options_lib import option_fields_from_inst_id
|
||||
|
||||
inst_id = str(pos.get("instId") or pos.get("inst_id") or "")
|
||||
mark = _safe_float(pos.get("markPx")) or _safe_float((quote or {}).get("mark_px") or (quote or {}).get("mark"))
|
||||
if mark is None:
|
||||
mark = fetch_option_mark_px(ex, inst_id)
|
||||
opt_type = pos.get("optType") or (quote or {}).get("opt_type")
|
||||
strike = _safe_float(pos.get("stk")) or _safe_float((quote or {}).get("strike"))
|
||||
if not opt_type or strike is None:
|
||||
pt, ps = option_fields_from_inst_id(inst_id)
|
||||
opt_type = opt_type or pt
|
||||
if strike is None:
|
||||
strike = ps
|
||||
idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px"))
|
||||
return close_ref_prices(mark_px=mark, opt_type=str(opt_type or ""), strike=strike, index_px=idx)
|
||||
|
||||
|
||||
def ensure_target_tables(conn: sqlite3.Connection) -> None:
|
||||
init_options_tables(conn)
|
||||
conn.execute(
|
||||
@@ -290,6 +315,46 @@ def close_option_by_bid_depth(
|
||||
return {"ok": False, "msg": "可平张数不足", "already_flat": True}
|
||||
td_mode = str(pos.get("mgnMode") or cfg.get("td_mode") or "isolated")
|
||||
pos_side = _pos_side_from_position(pos) or "net"
|
||||
mark_px, intrinsic_px = _pos_close_refs(ex, pos, q)
|
||||
|
||||
def _cancel_sell_pending() -> None:
|
||||
try:
|
||||
pending = ex.private_get_trade_orders_pending({"instType": "OPTION", "instId": inst_id}) or {}
|
||||
for o in pending.get("data") or []:
|
||||
if str(o.get("side") or "").lower() != "sell":
|
||||
continue
|
||||
oid = o.get("ordId")
|
||||
if not oid:
|
||||
continue
|
||||
try:
|
||||
ex.private_post_trade_cancel_order({"instId": inst_id, "ordId": oid})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 残档买盘:禁止自动按买盘平仓(并撤掉可能已挂的异常低价卖单)
|
||||
try:
|
||||
book0 = cfg["fetch_option_book_depth"](ex, inst_id, 5)
|
||||
usable0, stub_only0, stub_reason0 = filter_bids_for_close(
|
||||
book0.get("bids") or [], mark_px=mark_px, intrinsic_px=intrinsic_px
|
||||
)
|
||||
raw_bid0 = None
|
||||
if book0.get("bids"):
|
||||
raw_bid0 = _safe_float((book0.get("bids") or [{}])[0].get("px"))
|
||||
if not usable0:
|
||||
bid_chk = raw_bid0 or _safe_float(q.get("bid"))
|
||||
stub, stub_reason = is_stub_bid_px(bid_chk, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||
if stub or stub_only0:
|
||||
_cancel_sell_pending()
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": stub_reason0 or stub_reason or "暂无有效买盘,禁止自动平仓",
|
||||
"stopped_reason": "stub_bid",
|
||||
"auto_close_blocked": True,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 已有未成交卖平单时先等成交,避免每轮撤单重挂反复推送/吃档
|
||||
try:
|
||||
@@ -378,12 +443,36 @@ def close_option_by_bid_depth(
|
||||
break
|
||||
remaining = min(remaining, current_avail)
|
||||
book = cfg["fetch_option_book_depth"](ex, inst_id, 5)
|
||||
preview = estimate_close_by_bids(book.get("bids") or [], remaining, ct_mult=ct_mult)
|
||||
preview = estimate_close_by_bids(
|
||||
book.get("bids") or [],
|
||||
remaining,
|
||||
ct_mult=ct_mult,
|
||||
mark_px=mark_px,
|
||||
intrinsic_px=intrinsic_px,
|
||||
)
|
||||
if preview.get("auto_close_blocked") or preview.get("bid_invalid"):
|
||||
stopped_reason = "stub_bid"
|
||||
_cancel_sell_pending()
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": preview.get("bid_invalid_reason") or "暂无有效买盘,禁止自动平仓",
|
||||
"stopped_reason": "stub_bid",
|
||||
"auto_close_blocked": True,
|
||||
}
|
||||
levels = preview.get("levels") or []
|
||||
if not levels:
|
||||
# 无买盘深度时仅允许真实买一价,不用标记价挂单
|
||||
q2 = cfg["quote_option_contract"](ex, inst_id)
|
||||
bid_px = _safe_float(q2.get("bid")) or _safe_float(q.get("bid"))
|
||||
stub, stub_reason = is_stub_bid_px(bid_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||
if stub:
|
||||
stopped_reason = "stub_bid"
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": stub_reason or "暂无有效买盘,禁止自动平仓",
|
||||
"stopped_reason": "stub_bid",
|
||||
"auto_close_blocked": True,
|
||||
}
|
||||
if bid_px is None or bid_px <= 0:
|
||||
stopped_reason = "no_bid"
|
||||
break
|
||||
@@ -394,6 +483,14 @@ def close_option_by_bid_depth(
|
||||
if level_sheets <= 0 or level_px <= 0:
|
||||
stopped_reason = "invalid_bid_depth"
|
||||
break
|
||||
stub_lv, stub_lv_reason = is_stub_bid_px(level_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||
if stub_lv:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": stub_lv_reason or "暂无有效买盘,禁止自动平仓",
|
||||
"stopped_reason": "stub_bid",
|
||||
"auto_close_blocked": True,
|
||||
}
|
||||
before_avail = current_avail
|
||||
order = cfg["place_option_limit_order"](
|
||||
ex,
|
||||
|
||||
Reference in New Issue
Block a user