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:
@@ -374,6 +374,9 @@
|
|||||||
|
|
||||||
/** 买盘深度:价格/流动性;仅展示平仓所需档位(买一不够才出买二…). */
|
/** 买盘深度:价格/流动性;仅展示平仓所需档位(买一不够才出买二…). */
|
||||||
function fmtCloseLevels(preview, tickSz) {
|
function fmtCloseLevels(preview, tickSz) {
|
||||||
|
if (preview && (preview.auto_close_blocked || preview.bid_invalid)) {
|
||||||
|
return "暂无有效买盘";
|
||||||
|
}
|
||||||
const levels = ((preview && preview.levels) || []).slice(0, 5);
|
const levels = ((preview && preview.levels) || []).slice(0, 5);
|
||||||
if (!levels.length) return "—";
|
if (!levels.length) return "—";
|
||||||
return levels.map(function (x, idx) {
|
return levels.map(function (x, idx) {
|
||||||
@@ -885,12 +888,18 @@
|
|||||||
'<div class="pos-cell"><span class="pos-label">到期平衡</span><span class="pos-value">' + fmt(p.expiry_be_px, 0) + "</span></div>" +
|
'<div class="pos-cell"><span class="pos-label">到期平衡</span><span class="pos-value">' + fmt(p.expiry_be_px, 0) + "</span></div>" +
|
||||||
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
|
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
|
||||||
'<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
|
'<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
|
||||||
(net == null ? "—" : fmt(net, 2)) + "</span></div>" +
|
(closePreview.auto_close_blocked || closePreview.bid_invalid || net == null ? "—" : fmt(net, 2)) + "</span></div>" +
|
||||||
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
||||||
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>" +
|
(closePreview.auto_close_blocked || closePreview.bid_invalid || roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>" +
|
||||||
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
|
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
|
||||||
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' + fmtClosePreview(closePreview, p.premium_paid) + "</span></div>" +
|
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' +
|
||||||
|
(closePreview.auto_close_blocked || closePreview.bid_invalid
|
||||||
|
? '<span class="muted">禁用以残档自动平</span>'
|
||||||
|
: fmtClosePreview(closePreview, p.premium_paid)) + "</span></div>" +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
|
(closePreview.auto_close_blocked || closePreview.bid_invalid
|
||||||
|
? '<div class="muted opt-bid-invalid-hint">' + (closePreview.bid_invalid_reason || "当前买一无效,禁止按买盘自动平仓") + "</div>"
|
||||||
|
: "") +
|
||||||
renderTargetDelegateRow(p)
|
renderTargetDelegateRow(p)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1184,6 +1193,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const preview = q.close_preview || {};
|
const preview = q.close_preview || {};
|
||||||
|
if (preview.auto_close_blocked || preview.bid_invalid) {
|
||||||
|
alert(preview.bid_invalid_reason || "当前买一为无效残档,禁止按买盘自动平仓。请到 OKX App 自行挂限价/市价。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!preview.covered_sheets || preview.covered_sheets <= 0) {
|
if (!preview.covered_sheets || preview.covered_sheets <= 0) {
|
||||||
alert("暂无可用买盘深度,请稍后在 OKX App 平仓或等盘口恢复");
|
alert("暂无可用买盘深度,请稍后在 OKX App 平仓或等盘口恢复");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -52,6 +52,9 @@
|
|||||||
|
|
||||||
/** 买盘深度:价格/流动性;仅展示平仓所需档位(买一不够才出买二…). */
|
/** 买盘深度:价格/流动性;仅展示平仓所需档位(买一不够才出买二…). */
|
||||||
function fmtCloseLevels(preview, tickSz) {
|
function fmtCloseLevels(preview, tickSz) {
|
||||||
|
if (preview && (preview.auto_close_blocked || preview.bid_invalid)) {
|
||||||
|
return "暂无有效买盘";
|
||||||
|
}
|
||||||
const levels = ((preview && preview.levels) || []).slice(0, 5);
|
const levels = ((preview && preview.levels) || []).slice(0, 5);
|
||||||
if (!levels.length) return "—";
|
if (!levels.length) return "—";
|
||||||
return levels.map(function (x, idx) {
|
return levels.map(function (x, idx) {
|
||||||
@@ -147,12 +150,18 @@
|
|||||||
'<div class="pos-cell"><span class="pos-label">到期平衡</span><span class="pos-value">' + fmt(p.expiry_be_px, 0) + "</span></div>" +
|
'<div class="pos-cell"><span class="pos-label">到期平衡</span><span class="pos-value">' + fmt(p.expiry_be_px, 0) + "</span></div>" +
|
||||||
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
|
'<div class="pos-cell"><span class="pos-label">平掉回本</span><span class="pos-value">' + fmt(p.close_be_px, 0) + "</span></div>" +
|
||||||
'<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
|
'<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
|
||||||
(net == null ? "—" : fmt(net, 2)) + "</span></div>" +
|
(closePreview.auto_close_blocked || closePreview.bid_invalid || net == null ? "—" : fmt(net, 2)) + "</span></div>" +
|
||||||
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
||||||
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>" +
|
(closePreview.auto_close_blocked || closePreview.bid_invalid || roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>" +
|
||||||
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
|
'<div class="pos-cell opt-pos-cell--depth"><span class="pos-label">买盘深度</span><span class="pos-value opt-bid-plain">' + fmtCloseLevels(closePreview, tickSz) + "</span></div>" +
|
||||||
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' + fmtClosePreview(closePreview, p.premium_paid, hub) + "</span></div>" +
|
'<div class="pos-cell opt-pos-cell--close"><span class="pos-label">按买盘回收</span><span class="pos-value">' +
|
||||||
|
(closePreview.auto_close_blocked || closePreview.bid_invalid
|
||||||
|
? '<span class="muted">禁用以残档自动平</span>'
|
||||||
|
: fmtClosePreview(closePreview, p.premium_paid, hub)) + "</span></div>" +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
|
(closePreview.auto_close_blocked || closePreview.bid_invalid
|
||||||
|
? '<div class="muted opt-bid-invalid-hint">' + (closePreview.bid_invalid_reason || "当前买一无效,禁止按买盘自动平仓") + "</div>"
|
||||||
|
: "") +
|
||||||
(p.target_index != null
|
(p.target_index != null
|
||||||
? (function () {
|
? (function () {
|
||||||
const eth = p.eth_amount != null ? Number(p.eth_amount)
|
const eth = p.eth_amount != null ? Number(p.eth_amount)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any
|
|||||||
|
|
||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
from lib.options.options_history_lib import enrich_position_row_display
|
from lib.options.options_history_lib import enrich_position_row_display
|
||||||
from lib.options.options_pricing_lib import estimate_close_by_bids
|
from lib.options.options_pricing_lib import estimate_close_by_bids, intrinsic_px_per_unit
|
||||||
|
|
||||||
|
|
||||||
def _safe_float(v: Any) -> float | None:
|
def _safe_float(v: Any) -> float | None:
|
||||||
@@ -34,11 +34,19 @@ def attach_close_preview(
|
|||||||
book = cfg["fetch_option_book_depth"](ex, inst_id, 5)
|
book = cfg["fetch_option_book_depth"](ex, inst_id, 5)
|
||||||
row["bid_depth"] = book.get("bids") or []
|
row["bid_depth"] = book.get("bids") or []
|
||||||
row["ask_depth"] = book.get("asks") or []
|
row["ask_depth"] = book.get("asks") or []
|
||||||
|
mark_px = _safe_float(row.get("mark_px") or row.get("markPx"))
|
||||||
|
intrinsic = intrinsic_px_per_unit(
|
||||||
|
row.get("opt_type") or row.get("optType"),
|
||||||
|
_safe_float(row.get("strike") or row.get("stk")),
|
||||||
|
_safe_float(row.get("idx_px") or row.get("idxPx")),
|
||||||
|
)
|
||||||
row["close_preview"] = estimate_close_by_bids(
|
row["close_preview"] = estimate_close_by_bids(
|
||||||
row["bid_depth"],
|
row["bid_depth"],
|
||||||
target_sheets,
|
target_sheets,
|
||||||
ct_mult=ct_mult,
|
ct_mult=ct_mult,
|
||||||
premium_paid=paid,
|
premium_paid=paid,
|
||||||
|
mark_px=mark_px,
|
||||||
|
intrinsic_px=intrinsic,
|
||||||
)
|
)
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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(
|
def estimate_close_by_bids(
|
||||||
bids: list[dict[str, Any]] | None,
|
bids: list[dict[str, Any]] | None,
|
||||||
sheets: int | float,
|
sheets: int | float,
|
||||||
*,
|
*,
|
||||||
ct_mult: float = 0.01,
|
ct_mult: float = 0.01,
|
||||||
premium_paid: float | None = None,
|
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]:
|
) -> dict[str, Any]:
|
||||||
"""按买一到买N逐档估算限价卖出可收回金额."""
|
"""按买一到买N逐档估算限价卖出可收回金额;残档买盘不参与估算与自动平仓."""
|
||||||
target = max(0, int(float(sheets or 0)))
|
target = max(0, int(float(sheets or 0)))
|
||||||
remaining = target
|
remaining = target
|
||||||
total_received = 0.0
|
total_received = 0.0
|
||||||
levels: list[dict[str, Any]] = []
|
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:
|
if target <= 0 or ct_mult <= 0:
|
||||||
return {
|
return empty
|
||||||
"levels": [],
|
usable, stub_only, stub_reason = filter_bids_for_close(
|
||||||
"covered_sheets": 0,
|
bids, mark_px=mark_px, intrinsic_px=intrinsic_px, min_ratio=min_bid_ratio
|
||||||
"uncovered_sheets": target,
|
)
|
||||||
"total_received": 0.0,
|
if stub_only:
|
||||||
"avg_px": None,
|
out = dict(empty)
|
||||||
"estimated_pnl": None,
|
out["bid_invalid"] = True
|
||||||
"estimated_pnl_ratio_pct": None,
|
out["bid_invalid_reason"] = stub_reason
|
||||||
}
|
out["auto_close_blocked"] = True
|
||||||
for i, level in enumerate(bids or [], start=1):
|
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:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
@@ -115,6 +239,9 @@ def estimate_close_by_bids(
|
|||||||
"avg_px": round(avg_px, 4) if avg_px is not None else None,
|
"avg_px": round(avg_px, 4) if avg_px is not None else None,
|
||||||
"estimated_pnl": estimated_pnl,
|
"estimated_pnl": estimated_pnl,
|
||||||
"estimated_pnl_ratio_pct": estimated_pnl_ratio_pct,
|
"estimated_pnl_ratio_pct": estimated_pnl_ratio_pct,
|
||||||
|
"bid_invalid": False,
|
||||||
|
"bid_invalid_reason": None,
|
||||||
|
"auto_close_blocked": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ from lib.options.options_db import init_options_tables
|
|||||||
from lib.options.options_monitor_lib import options_monitor_loop
|
from lib.options.options_monitor_lib import options_monitor_loop
|
||||||
from lib.options.options_pricing_lib import (
|
from lib.options.options_pricing_lib import (
|
||||||
calc_order_size,
|
calc_order_size,
|
||||||
|
close_ref_prices,
|
||||||
ct_mult_from_meta,
|
ct_mult_from_meta,
|
||||||
estimate_close_by_bids,
|
estimate_close_by_bids,
|
||||||
|
fetch_option_mark_px,
|
||||||
|
filter_bids_for_close,
|
||||||
|
is_stub_bid_px,
|
||||||
min_sz_from_meta,
|
min_sz_from_meta,
|
||||||
premium_per_sheet,
|
premium_per_sheet,
|
||||||
total_premium,
|
total_premium,
|
||||||
@@ -707,6 +711,41 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
return jsonify(order)
|
return jsonify(order)
|
||||||
elif depth_split:
|
elif depth_split:
|
||||||
ct_mult = float(q.get("ct_mult") or 0.01)
|
ct_mult = float(q.get("ct_mult") or 0.01)
|
||||||
|
from lib.exchange.okx_options_lib import option_fields_from_inst_id
|
||||||
|
|
||||||
|
mark_px = _safe_float(pos.get("markPx")) or _safe_float(q.get("mark_px") or q.get("mark"))
|
||||||
|
if mark_px is None:
|
||||||
|
mark_px = fetch_option_mark_px(ex, inst_id)
|
||||||
|
opt_type = pos.get("optType") or q.get("opt_type")
|
||||||
|
strike = _safe_float(pos.get("stk")) or _safe_float(q.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_px = _safe_float(pos.get("idxPx")) or _safe_float(q.get("index_px"))
|
||||||
|
mark_px, intrinsic_px = close_ref_prices(
|
||||||
|
mark_px=mark_px, opt_type=str(opt_type or ""), strike=strike, index_px=idx_px
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if stub_only0 or not usable0:
|
||||||
|
bid_chk = None
|
||||||
|
if book0.get("bids"):
|
||||||
|
bid_chk = _safe_float((book0.get("bids") or [{}])[0].get("px"))
|
||||||
|
bid_chk = bid_chk or _safe_float(bid)
|
||||||
|
stub, stub_reason = is_stub_bid_px(bid_chk, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||||
|
if stub or stub_only0:
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"msg": stub_reason0 or stub_reason or "暂无有效买盘,禁止按买盘自动平仓",
|
||||||
|
"stopped_reason": "stub_bid",
|
||||||
|
"auto_close_blocked": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
remaining = close_sheets
|
remaining = close_sheets
|
||||||
submitted_sheets = 0
|
submitted_sheets = 0
|
||||||
filled_or_reduced_sheets = 0
|
filled_or_reduced_sheets = 0
|
||||||
@@ -726,7 +765,22 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
break
|
break
|
||||||
remaining = min(remaining, current_avail)
|
remaining = min(remaining, current_avail)
|
||||||
book = cfg["fetch_option_book_depth"](ex, inst_id, 5)
|
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"):
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"msg": preview.get("bid_invalid_reason") or "暂无有效买盘,禁止按买盘自动平仓",
|
||||||
|
"stopped_reason": "stub_bid",
|
||||||
|
"auto_close_blocked": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
levels = preview.get("levels") or []
|
levels = preview.get("levels") or []
|
||||||
if not levels:
|
if not levels:
|
||||||
stopped_reason = "no_bid_depth"
|
stopped_reason = "no_bid_depth"
|
||||||
@@ -829,7 +883,33 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
from lib.exchange.okx_options_lib import option_fields_from_inst_id
|
||||||
|
|
||||||
|
mark_px = _safe_float(pos.get("markPx")) or _safe_float(q.get("mark_px") or q.get("mark"))
|
||||||
|
if mark_px is None:
|
||||||
|
mark_px = fetch_option_mark_px(ex, inst_id)
|
||||||
|
opt_type = pos.get("optType") or q.get("opt_type")
|
||||||
|
strike = _safe_float(pos.get("stk")) or _safe_float(q.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_px = _safe_float(pos.get("idxPx")) or _safe_float(q.get("index_px"))
|
||||||
|
mark_px, intrinsic_px = close_ref_prices(
|
||||||
|
mark_px=mark_px, opt_type=str(opt_type or ""), strike=strike, index_px=idx_px
|
||||||
|
)
|
||||||
close_px = float(bid)
|
close_px = float(bid)
|
||||||
|
stub, stub_reason = is_stub_bid_px(close_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||||
|
if stub:
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"msg": stub_reason or "暂无有效买盘,禁止按买盘自动平仓",
|
||||||
|
"stopped_reason": "stub_bid",
|
||||||
|
"auto_close_blocked": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
order = cfg["place_option_limit_order"](
|
order = cfg["place_option_limit_order"](
|
||||||
ex,
|
ex,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import time
|
|||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from lib.options.options_db import init_options_tables
|
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:
|
def _safe_float(v: Any) -> float | None:
|
||||||
@@ -18,6 +25,24 @@ def _safe_float(v: Any) -> float | None:
|
|||||||
return 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:
|
def ensure_target_tables(conn: sqlite3.Connection) -> None:
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -290,6 +315,46 @@ def close_option_by_bid_depth(
|
|||||||
return {"ok": False, "msg": "可平张数不足", "already_flat": True}
|
return {"ok": False, "msg": "可平张数不足", "already_flat": True}
|
||||||
td_mode = str(pos.get("mgnMode") or cfg.get("td_mode") or "isolated")
|
td_mode = str(pos.get("mgnMode") or cfg.get("td_mode") or "isolated")
|
||||||
pos_side = _pos_side_from_position(pos) or "net"
|
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:
|
try:
|
||||||
@@ -378,12 +443,36 @@ def close_option_by_bid_depth(
|
|||||||
break
|
break
|
||||||
remaining = min(remaining, current_avail)
|
remaining = min(remaining, current_avail)
|
||||||
book = cfg["fetch_option_book_depth"](ex, inst_id, 5)
|
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 []
|
levels = preview.get("levels") or []
|
||||||
if not levels:
|
if not levels:
|
||||||
# 无买盘深度时仅允许真实买一价,不用标记价挂单
|
# 无买盘深度时仅允许真实买一价,不用标记价挂单
|
||||||
q2 = cfg["quote_option_contract"](ex, inst_id)
|
q2 = cfg["quote_option_contract"](ex, inst_id)
|
||||||
bid_px = _safe_float(q2.get("bid")) or _safe_float(q.get("bid"))
|
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:
|
if bid_px is None or bid_px <= 0:
|
||||||
stopped_reason = "no_bid"
|
stopped_reason = "no_bid"
|
||||||
break
|
break
|
||||||
@@ -394,6 +483,14 @@ def close_option_by_bid_depth(
|
|||||||
if level_sheets <= 0 or level_px <= 0:
|
if level_sheets <= 0 or level_px <= 0:
|
||||||
stopped_reason = "invalid_bid_depth"
|
stopped_reason = "invalid_bid_depth"
|
||||||
break
|
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
|
before_avail = current_avail
|
||||||
order = cfg["place_option_limit_order"](
|
order = cfg["place_option_limit_order"](
|
||||||
ex,
|
ex,
|
||||||
|
|||||||
@@ -221,6 +221,38 @@ def test_estimate_close_by_bids_empty():
|
|||||||
assert out["avg_px"] is None
|
assert out["avg_px"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_stub_bid_blocks_auto_close_estimate():
|
||||||
|
from lib.options.options_pricing_lib import estimate_close_by_bids, is_stub_bid_px
|
||||||
|
|
||||||
|
stub, reason = is_stub_bid_px(0.2, mark_px=42.0)
|
||||||
|
assert stub is True
|
||||||
|
assert "残档" in reason or "无效" in reason or "远低于" in reason
|
||||||
|
|
||||||
|
out = estimate_close_by_bids(
|
||||||
|
[{"px": 0.2, "sz": 3500}],
|
||||||
|
66,
|
||||||
|
ct_mult=0.01,
|
||||||
|
premium_paid=9.37,
|
||||||
|
mark_px=42.0,
|
||||||
|
)
|
||||||
|
assert out["auto_close_blocked"] is True
|
||||||
|
assert out["bid_invalid"] is True
|
||||||
|
assert out["estimated_pnl"] is None
|
||||||
|
assert out["levels"] == []
|
||||||
|
|
||||||
|
ok, _ = is_stub_bid_px(30.0, mark_px=42.0)
|
||||||
|
assert ok is False
|
||||||
|
good = estimate_close_by_bids(
|
||||||
|
[{"px": 30.0, "sz": 100}],
|
||||||
|
10,
|
||||||
|
ct_mult=0.01,
|
||||||
|
premium_paid=1.0,
|
||||||
|
mark_px=42.0,
|
||||||
|
)
|
||||||
|
assert good["auto_close_blocked"] is False
|
||||||
|
assert good["covered_sheets"] == 10
|
||||||
|
|
||||||
|
|
||||||
def test_expiry_breakeven_from_ask():
|
def test_expiry_breakeven_from_ask():
|
||||||
from lib.options.options_pricing_lib import expiry_breakeven_from_ask
|
from lib.options.options_pricing_lib import expiry_breakeven_from_ask
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user