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:
dekun
2026-07-15 21:05:54 +08:00
parent e15eca76f3
commit c025c06fac
7 changed files with 387 additions and 21 deletions
+138 -11
View File
@@ -50,29 +50,153 @@ def total_premium(quote_per_unit: float, eth_amount: float, ct_mult: float = 0.0
return float(quote_per_unit) * float(eth_amount)
# 买一相对标记价/内在价值低于该比例 → 视为残档,禁止按买盘自动/多档平仓
BID_CLOSE_MIN_RATIO = 0.3
def _safe_px(v: Any) -> float | None:
if v is None or v == "":
return None
try:
x = float(v)
except (TypeError, ValueError):
return None
return x if x > 0 else None
def intrinsic_px_per_unit(opt_type: str | None, strike: float | None, index_px: float | None) -> float | None:
o = (opt_type or "").strip().upper()
if strike is None or index_px is None:
return None
try:
k = float(strike)
idx = float(index_px)
except (TypeError, ValueError):
return None
if o == "C" and idx > k:
return idx - k
if o == "P" and idx < k:
return k - idx
return None
def is_stub_bid_px(
bid_px: float | None,
*,
mark_px: float | None = None,
intrinsic_px: float | None = None,
min_ratio: float = BID_CLOSE_MIN_RATIO,
) -> tuple[bool, str]:
"""
判断买一是否为无效残档(如标记 42、买一 0.2).
返回 (is_stub, reason).
"""
bid = _safe_px(bid_px)
if bid is None:
return True, "无买一"
ref = _safe_px(mark_px)
ref_name = "标记价"
intrinsic = _safe_px(intrinsic_px)
if intrinsic is not None and (ref is None or intrinsic > ref):
ref = intrinsic
ref_name = "内在价值"
if ref is None:
return False, ""
ratio = float(min_ratio) if min_ratio and min_ratio > 0 else BID_CLOSE_MIN_RATIO
if bid < ref * ratio:
return True, f"买一{bid:g}远低于{ref_name}{ref:g},属无效残档,禁止按买盘自动平仓"
return False, ""
def fetch_option_mark_px(ex: Any, inst_id: str) -> float | None:
"""优先 mark-price 接口,失败则 None."""
inst_id = (inst_id or "").strip()
if not inst_id or ex is None:
return None
try:
rows = ex.public_get_public_mark_price({"instType": "OPTION", "instId": inst_id}).get("data") or []
if rows:
return _safe_px(rows[0].get("markPx"))
except Exception:
pass
return None
def close_ref_prices(
*,
mark_px: float | None = None,
opt_type: str | None = None,
strike: float | None = None,
index_px: float | None = None,
) -> tuple[float | None, float | None]:
"""返回 (mark_px, intrinsic_px) 供残档判断."""
return _safe_px(mark_px), intrinsic_px_per_unit(opt_type, strike, index_px)
def filter_bids_for_close(
bids: list[dict[str, Any]] | None,
*,
mark_px: float | None = None,
intrinsic_px: float | None = None,
min_ratio: float = BID_CLOSE_MIN_RATIO,
) -> tuple[list[dict[str, Any]], bool, str]:
"""过滤不可用于平仓的残档买盘.返回 (usable_bids, had_stub_only, reason)."""
raw = list(bids or [])
usable: list[dict[str, Any]] = []
stub_reason = ""
for level in raw:
px = _safe_px(level.get("px") if isinstance(level, dict) else None)
stub, reason = is_stub_bid_px(px, mark_px=mark_px, intrinsic_px=intrinsic_px, min_ratio=min_ratio)
if stub:
if not stub_reason:
stub_reason = reason or "买一无效"
continue
usable.append(level)
if raw and not usable:
return [], True, stub_reason or "暂无有效买盘"
return usable, False, ""
def estimate_close_by_bids(
bids: list[dict[str, Any]] | None,
sheets: int | float,
*,
ct_mult: float = 0.01,
premium_paid: float | None = None,
mark_px: float | None = None,
intrinsic_px: float | None = None,
min_bid_ratio: float = BID_CLOSE_MIN_RATIO,
) -> dict[str, Any]:
"""按买一到买N逐档估算限价卖出可收回金额."""
"""按买一到买N逐档估算限价卖出可收回金额;残档买盘不参与估算与自动平仓."""
target = max(0, int(float(sheets or 0)))
remaining = target
total_received = 0.0
levels: list[dict[str, Any]] = []
empty = {
"levels": [],
"covered_sheets": 0,
"uncovered_sheets": target,
"total_received": 0.0,
"avg_px": None,
"estimated_pnl": None,
"estimated_pnl_ratio_pct": None,
"bid_invalid": False,
"bid_invalid_reason": None,
"auto_close_blocked": False,
}
if target <= 0 or ct_mult <= 0:
return {
"levels": [],
"covered_sheets": 0,
"uncovered_sheets": target,
"total_received": 0.0,
"avg_px": None,
"estimated_pnl": None,
"estimated_pnl_ratio_pct": None,
}
for i, level in enumerate(bids or [], start=1):
return empty
usable, stub_only, stub_reason = filter_bids_for_close(
bids, mark_px=mark_px, intrinsic_px=intrinsic_px, min_ratio=min_bid_ratio
)
if stub_only:
out = dict(empty)
out["bid_invalid"] = True
out["bid_invalid_reason"] = stub_reason
out["auto_close_blocked"] = True
out["raw_bid_px"] = _safe_px((bids or [{}])[0].get("px")) if bids else None
return out
for i, level in enumerate(usable, start=1):
if remaining <= 0:
break
try:
@@ -115,6 +239,9 @@ def estimate_close_by_bids(
"avg_px": round(avg_px, 4) if avg_px is not None else None,
"estimated_pnl": estimated_pnl,
"estimated_pnl_ratio_pct": estimated_pnl_ratio_pct,
"bid_invalid": False,
"bid_invalid_reason": None,
"auto_close_blocked": False,
}