Use bid1-only option closes with hard-disabled market exits.
Manual close checks liquidity only; target auto still requires 2x recycle hold once, then reuses the shared bid1 executor. Add /options/guide doc and update hedge-plan refs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
"""期权平仓执行:只锁买一限价卖出;永不市价."""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from lib.options.options_close_gate_lib import (
|
||||
clear_close_gate,
|
||||
is_close_gate_passed,
|
||||
mark_close_gate_passed,
|
||||
update_close_gate,
|
||||
)
|
||||
from lib.options.options_pricing_lib import (
|
||||
estimate_close_by_bids,
|
||||
fetch_option_mark_px,
|
||||
is_stub_bid_px,
|
||||
total_premium,
|
||||
)
|
||||
|
||||
|
||||
def _safe_float(v: Any) -> float | None:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None:
|
||||
try:
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
from lib.options.options_db import init_options_tables
|
||||
|
||||
init_options_tables(conn)
|
||||
row = conn.execute(
|
||||
"SELECT premium_paid FROM options_trades WHERE inst_id = ? AND status = 'open' ORDER BY id DESC LIMIT 1",
|
||||
(inst_id,),
|
||||
).fetchone()
|
||||
if row and row["premium_paid"] is not None:
|
||||
return float(row["premium_paid"])
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
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
|
||||
from lib.options.options_pricing_lib import close_ref_prices
|
||||
|
||||
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 _avail_sheets(pos: dict[str, Any]) -> int:
|
||||
avail = _safe_float(pos.get("availPos"))
|
||||
if avail is None or avail <= 0:
|
||||
avail = abs(_safe_float(pos.get("pos")) or 0)
|
||||
return max(0, int(avail or 0))
|
||||
|
||||
|
||||
def _cancel_sell_pending(ex: Any, inst_id: str) -> 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
|
||||
|
||||
|
||||
def close_option_by_bid1(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
inst_id: str,
|
||||
*,
|
||||
sheets: int | None = None,
|
||||
require_recycle_gate: bool = False,
|
||||
signal_note: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
本轮只吃买一深度:
|
||||
- 本批张数 = min(请求张数, 持仓, 买一深度)
|
||||
- 限价 = 校验通过时锁定的买一价
|
||||
- 永不市价
|
||||
- 始终校验有效流动性(残档买一禁止)
|
||||
- require_recycle_gate=True 时:首次还需可回收≥2×权利金并持续 hold 秒;
|
||||
一旦通过后对同仓续批只验流动性
|
||||
"""
|
||||
from lib.exchange.okx_options_lib import (
|
||||
_pos_side_from_position,
|
||||
invalidate_option_positions_cache,
|
||||
)
|
||||
|
||||
inst_id = (inst_id or "").strip()
|
||||
if not inst_id:
|
||||
return {"ok": False, "msg": "缺少 inst_id"}
|
||||
|
||||
q = cfg["quote_option_contract"](ex, inst_id)
|
||||
if not q.get("ok"):
|
||||
return {"ok": False, "msg": q.get("msg") or "报价失败"}
|
||||
tick_sz = q.get("tick_sz")
|
||||
ct_mult = float(q.get("ct_mult") or 0.01)
|
||||
|
||||
raw_positions = cfg["fetch_option_positions"](ex)
|
||||
if raw_positions is None:
|
||||
return {"ok": False, "msg": "获取期权持仓失败"}
|
||||
pos = next((p for p in raw_positions if str(p.get("instId")) == inst_id), None)
|
||||
if not pos:
|
||||
clear_close_gate(inst_id)
|
||||
return {"ok": False, "msg": "未找到持仓", "already_flat": True}
|
||||
|
||||
avail = _avail_sheets(pos)
|
||||
want = int(sheets) if sheets else avail
|
||||
want = min(want, avail)
|
||||
if want < 1:
|
||||
clear_close_gate(inst_id)
|
||||
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)
|
||||
premium_paid = _open_premium_paid(cfg, inst_id)
|
||||
if premium_paid is None:
|
||||
premium_paid = _safe_float(pos.get("premium_paid"))
|
||||
|
||||
# 已有未成交卖平单:等成交,不撤不重挂
|
||||
try:
|
||||
pending = ex.private_get_trade_orders_pending({"instType": "OPTION", "instId": inst_id}) or {}
|
||||
sell_pending = [
|
||||
o
|
||||
for o in (pending.get("data") or [])
|
||||
if str(o.get("side") or "").lower() == "sell" and o.get("ordId")
|
||||
]
|
||||
if sell_pending:
|
||||
time.sleep(0.5)
|
||||
invalidate_option_positions_cache()
|
||||
raw_positions = cfg["fetch_option_positions"](ex)
|
||||
if raw_positions is None:
|
||||
return {"ok": False, "msg": "获取期权持仓失败"}
|
||||
pos = next((p for p in raw_positions if str(p.get("instId")) == inst_id), None)
|
||||
if not pos or _avail_sheets(pos) < 1:
|
||||
clear_close_gate(inst_id)
|
||||
return {
|
||||
"ok": True,
|
||||
"already_flat": True,
|
||||
"msg": "已有限价卖单成交",
|
||||
"close_ord_id": ",".join(str(o.get("ordId")) for o in sell_pending),
|
||||
"fully_closed": True,
|
||||
"submitted_sheets": want,
|
||||
"remaining_sheets": 0,
|
||||
"mode": "bid1",
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": "等待已有买一限价卖单成交",
|
||||
"stopped_reason": "pending_close_order",
|
||||
"close_ord_id": ",".join(str(o.get("ordId")) for o in sell_pending),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
book = cfg["fetch_option_book_depth"](ex, inst_id, 1)
|
||||
preview = estimate_close_by_bids(
|
||||
book.get("bids") or [],
|
||||
want,
|
||||
ct_mult=ct_mult,
|
||||
premium_paid=premium_paid,
|
||||
mark_px=mark_px,
|
||||
intrinsic_px=intrinsic_px,
|
||||
max_levels=1,
|
||||
)
|
||||
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
||||
_cancel_sell_pending(ex, inst_id)
|
||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": preview.get("bid_invalid_reason") or "暂无有效买盘,禁止平仓",
|
||||
"stopped_reason": "stub_bid",
|
||||
"auto_close_blocked": True,
|
||||
"liquidity_blocked": True,
|
||||
}
|
||||
|
||||
levels = preview.get("levels") or []
|
||||
if not levels:
|
||||
bid_px = _safe_float(q.get("bid"))
|
||||
stub, stub_reason = is_stub_bid_px(bid_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||
if stub or bid_px is None or bid_px <= 0:
|
||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": stub_reason or "暂无买一,无法限价平仓",
|
||||
"stopped_reason": "stub_bid" if stub else "no_bid",
|
||||
"auto_close_blocked": True,
|
||||
"liquidity_blocked": True,
|
||||
}
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": "暂无买一深度,无法平仓",
|
||||
"stopped_reason": "no_bid_depth",
|
||||
"liquidity_blocked": True,
|
||||
}
|
||||
|
||||
level = levels[0]
|
||||
level_sheets = int(level.get("sheets") or 0)
|
||||
level_px = float(level.get("px") or 0)
|
||||
if level_sheets <= 0 or level_px <= 0:
|
||||
return {"ok": False, "msg": "买一深度无效", "stopped_reason": "invalid_bid_depth"}
|
||||
|
||||
stub_lv, stub_lv_reason = is_stub_bid_px(level_px, mark_px=mark_px, intrinsic_px=intrinsic_px)
|
||||
if stub_lv:
|
||||
update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": stub_lv_reason or "暂无有效买盘,禁止平仓",
|
||||
"stopped_reason": "stub_bid",
|
||||
"auto_close_blocked": True,
|
||||
"liquidity_blocked": True,
|
||||
}
|
||||
|
||||
# 自动平仓:2×权利金门控(首次);通过后同仓续批只验流动性
|
||||
gate = update_close_gate(
|
||||
inst_id,
|
||||
recycle_usdc=_safe_float(preview.get("total_received")),
|
||||
premium_paid=premium_paid,
|
||||
)
|
||||
if require_recycle_gate and not is_close_gate_passed(inst_id) and not gate.get("ready"):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": gate.get("msg") or "平仓门控未就绪(需可回收≥2×权利金并持续一段时间)",
|
||||
"stopped_reason": "close_gate",
|
||||
"auto_close_blocked": True,
|
||||
"close_gate": gate,
|
||||
}
|
||||
if gate.get("ready"):
|
||||
mark_close_gate_passed(inst_id)
|
||||
|
||||
locked_bid_px = level_px
|
||||
before_avail = avail
|
||||
order = cfg["place_option_limit_order"](
|
||||
ex,
|
||||
inst_id=inst_id,
|
||||
side="sell",
|
||||
sheets=level_sheets,
|
||||
price=locked_bid_px,
|
||||
td_mode=td_mode,
|
||||
tick_sz=tick_sz,
|
||||
reduce_only=True,
|
||||
pos_side=pos_side,
|
||||
)
|
||||
if not order.get("ok"):
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": order.get("msg") or "买一限价平仓失败",
|
||||
"stopped_reason": "order_failed",
|
||||
"locked_bid_px": locked_bid_px,
|
||||
"batch_sheets": level_sheets,
|
||||
}
|
||||
|
||||
px = float(order.get("px", locked_bid_px))
|
||||
oid = str((order.get("data") or {}).get("ordId") or "")
|
||||
prem_recv = round(total_premium(px, level_sheets * ct_mult), 4)
|
||||
time.sleep(0.6)
|
||||
invalidate_option_positions_cache()
|
||||
raw2 = cfg["fetch_option_positions"](ex)
|
||||
after_avail = 0
|
||||
if raw2 is not None:
|
||||
after_pos = next((p for p in raw2 if str(p.get("instId")) == inst_id), None)
|
||||
after_avail = _avail_sheets(after_pos) if after_pos else 0
|
||||
reduced = max(0, before_avail - after_avail) if raw2 is not None else 0
|
||||
remaining_pos = after_avail if raw2 is not None else max(0, before_avail - level_sheets)
|
||||
fully_closed = remaining_pos < 1
|
||||
|
||||
if fully_closed:
|
||||
clear_close_gate(inst_id)
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
from lib.options.options_db import init_options_tables
|
||||
|
||||
init_options_tables(conn)
|
||||
row = conn.execute(
|
||||
"SELECT id, premium_paid FROM options_trades WHERE inst_id = ? AND status = 'open' ORDER BY id DESC LIMIT 1",
|
||||
(inst_id,),
|
||||
).fetchone()
|
||||
if row:
|
||||
paid = float(row["premium_paid"] or 0)
|
||||
pnl = prem_recv - paid
|
||||
note_sql = ""
|
||||
params: list[Any] = [px, prem_recv, pnl, oid or None]
|
||||
if signal_note:
|
||||
note_sql = """,
|
||||
signal_note = CASE
|
||||
WHEN signal_note IS NULL OR TRIM(signal_note) = '' THEN ?
|
||||
ELSE signal_note
|
||||
END"""
|
||||
params.append(signal_note)
|
||||
params.append(int(row["id"]))
|
||||
conn.execute(
|
||||
f"""
|
||||
UPDATE options_trades
|
||||
SET status = 'closed', close_quote = ?, premium_received = ?,
|
||||
realized_pnl = ?, close_ord_id = ?, closed_at = CURRENT_TIMESTAMP
|
||||
{note_sql}
|
||||
WHERE id = ?
|
||||
""",
|
||||
tuple(params),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
elif require_recycle_gate:
|
||||
# 自动平已挂过单:同仓续批只验流动性
|
||||
mark_close_gate_passed(inst_id)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"mode": "bid1",
|
||||
"orders": [{"order": order, "px": px, "sheets": level_sheets}],
|
||||
"bid": px,
|
||||
"locked_bid_px": locked_bid_px,
|
||||
"submitted_sheets": level_sheets,
|
||||
"filled_or_reduced_sheets": min(reduced, level_sheets) if reduced else 0,
|
||||
"remaining_sheets": remaining_pos,
|
||||
"premium_received": prem_recv,
|
||||
"stopped_reason": None if fully_closed else ("partial_bid1" if reduced > 0 else "order_not_filled"),
|
||||
"close_ord_id": oid or None,
|
||||
"fully_closed": fully_closed,
|
||||
"msg": (
|
||||
f"已按买一 {locked_bid_px:g} 提交 {level_sheets} 张"
|
||||
+ ("" if fully_closed else f",剩余 {remaining_pos} 张待下次平仓")
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# 兼容旧名
|
||||
def close_option_by_bid_depth(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
inst_id: str,
|
||||
*,
|
||||
sheets: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return close_option_by_bid1(
|
||||
cfg,
|
||||
ex,
|
||||
inst_id,
|
||||
sheets=sheets,
|
||||
require_recycle_gate=True,
|
||||
signal_note="目标位平仓",
|
||||
)
|
||||
@@ -40,6 +40,26 @@ def clear_close_gate(inst_id: str | None = None) -> None:
|
||||
_gates.clear()
|
||||
|
||||
|
||||
def mark_close_gate_passed(inst_id: str) -> None:
|
||||
"""标记同仓已通过 2× 门控,续批平仓只验流动性."""
|
||||
inst = (inst_id or "").strip()
|
||||
if not inst:
|
||||
return
|
||||
with _lock:
|
||||
st = _gates.get(inst) or {}
|
||||
st["passed"] = True
|
||||
st["updated"] = time.time()
|
||||
_gates[inst] = st
|
||||
|
||||
|
||||
def is_close_gate_passed(inst_id: str) -> bool:
|
||||
inst = (inst_id or "").strip()
|
||||
if not inst:
|
||||
return False
|
||||
with _lock:
|
||||
return bool((_gates.get(inst) or {}).get("passed"))
|
||||
|
||||
|
||||
def update_close_gate(
|
||||
inst_id: str,
|
||||
*,
|
||||
@@ -86,6 +106,8 @@ def update_close_gate(
|
||||
ok_since = None
|
||||
held = (ts - float(ok_since)) if ok_since is not None else 0.0
|
||||
ready = bool(recycle_ok and held + 1e-9 >= hold)
|
||||
prev_passed = bool(prev.get("passed"))
|
||||
passed = prev_passed or ready
|
||||
state = {
|
||||
"ok_since": ok_since,
|
||||
"recycle": recv,
|
||||
@@ -94,6 +116,7 @@ def update_close_gate(
|
||||
"updated": ts,
|
||||
"min_mult": mult,
|
||||
"hold_seconds": hold,
|
||||
"passed": passed,
|
||||
}
|
||||
_gates[inst] = state
|
||||
|
||||
@@ -103,18 +126,20 @@ def update_close_gate(
|
||||
elif recv is None:
|
||||
msg = "暂无有效买盘可回收金额"
|
||||
elif not recycle_ok:
|
||||
msg = f"可回收 {recv:.4f} USDC < 权利金×{mult:g}({need:.4f}),暂不可平仓"
|
||||
msg = f"可回收 {recv:.4f} USDC < 权利金×{mult:g}({need:.4f}),暂不可自动平仓"
|
||||
elif not ready:
|
||||
msg = (
|
||||
f"可回收已达×{mult:g}({recv:.4f}/{need:.4f}),"
|
||||
f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)"
|
||||
f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)后才可自动平仓"
|
||||
)
|
||||
else:
|
||||
msg = f"可回收已达×{mult:g}且持续≥{hold:.0f}s,允许按买盘平仓"
|
||||
msg = f"可回收已达×{mult:g}且持续≥{hold:.0f}s,允许自动按买一平仓"
|
||||
|
||||
auto_blocked = not (ready or passed)
|
||||
return {
|
||||
"ok": True,
|
||||
"ready": ready,
|
||||
"passed": passed,
|
||||
"recycle_ok": recycle_ok,
|
||||
"recycle_usdc": recv,
|
||||
"premium_paid": prem,
|
||||
@@ -125,8 +150,8 @@ def update_close_gate(
|
||||
"remain_seconds": round(remain, 1) if remain is not None else None,
|
||||
"ok_since": ok_since,
|
||||
"msg": msg,
|
||||
"auto_close_blocked": not ready,
|
||||
"close_gate_blocked": not ready,
|
||||
"auto_close_blocked": auto_blocked,
|
||||
"close_gate_blocked": auto_blocked,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any
|
||||
|
||||
from lib.options.options_db import init_options_tables
|
||||
from lib.options.options_history_lib import enrich_position_row_display
|
||||
from lib.options.options_close_gate_lib import clear_close_gate, update_close_gate
|
||||
from lib.options.options_close_gate_lib import clear_close_gate, is_close_gate_passed, update_close_gate
|
||||
from lib.options.options_pricing_lib import estimate_close_by_bids, intrinsic_px_per_unit
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ def attach_close_preview(
|
||||
_safe_float(row.get("strike") or row.get("stk")),
|
||||
_safe_float(row.get("idx_px") or row.get("idxPx")),
|
||||
)
|
||||
# 与实盘一致:只按买一估算本轮可平
|
||||
preview = estimate_close_by_bids(
|
||||
row["bid_depth"],
|
||||
target_sheets,
|
||||
@@ -48,23 +49,29 @@ def attach_close_preview(
|
||||
premium_paid=paid,
|
||||
mark_px=mark_px,
|
||||
intrinsic_px=intrinsic,
|
||||
max_levels=1,
|
||||
)
|
||||
# 残档时不累计 2×权利金门控;有效回收时刷新持续计时
|
||||
# 残档时不累计 2×门控;有效买一时刷新计时(仅自动平仓需要)
|
||||
if preview.get("bid_invalid") or preview.get("auto_close_blocked"):
|
||||
gate = update_close_gate(inst_id, recycle_usdc=None, premium_paid=paid)
|
||||
preview["close_gate"] = gate
|
||||
preview["close_gate_blocked"] = True
|
||||
preview["close_gate_msg"] = preview.get("bid_invalid_reason") or gate.get("msg")
|
||||
preview["manual_close_blocked"] = True
|
||||
preview["liquidity_ok"] = False
|
||||
else:
|
||||
gate = update_close_gate(
|
||||
inst_id,
|
||||
recycle_usdc=_safe_float(preview.get("total_received")),
|
||||
premium_paid=paid,
|
||||
)
|
||||
passed = bool(gate.get("passed") or is_close_gate_passed(inst_id) or gate.get("ready"))
|
||||
preview["close_gate"] = gate
|
||||
preview["close_gate_blocked"] = not gate.get("ready")
|
||||
preview["close_gate_blocked"] = not passed
|
||||
preview["close_gate_msg"] = gate.get("msg")
|
||||
if not gate.get("ready"):
|
||||
preview["manual_close_blocked"] = False
|
||||
preview["liquidity_ok"] = True
|
||||
if not passed:
|
||||
preview["auto_close_blocked"] = True
|
||||
row["close_preview"] = preview
|
||||
return row
|
||||
|
||||
@@ -166,12 +166,14 @@ def estimate_close_by_bids(
|
||||
mark_px: float | None = None,
|
||||
intrinsic_px: float | None = None,
|
||||
min_bid_ratio: float = BID_CLOSE_MIN_RATIO,
|
||||
max_levels: int = 1,
|
||||
) -> dict[str, Any]:
|
||||
"""按买一到买N逐档估算限价卖出可收回金额;残档买盘不参与估算与自动平仓."""
|
||||
"""按买盘估算限价卖出可收回金额;默认只估算买一(与实盘平仓一致);残档不参与."""
|
||||
target = max(0, int(float(sheets or 0)))
|
||||
remaining = target
|
||||
total_received = 0.0
|
||||
levels: list[dict[str, Any]] = []
|
||||
max_lv = max(1, int(max_levels or 1))
|
||||
empty = {
|
||||
"levels": [],
|
||||
"covered_sheets": 0,
|
||||
@@ -183,6 +185,7 @@ def estimate_close_by_bids(
|
||||
"bid_invalid": False,
|
||||
"bid_invalid_reason": None,
|
||||
"auto_close_blocked": False,
|
||||
"max_levels": max_lv,
|
||||
}
|
||||
if target <= 0 or ct_mult <= 0:
|
||||
return empty
|
||||
@@ -196,7 +199,7 @@ def estimate_close_by_bids(
|
||||
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):
|
||||
for i, level in enumerate(usable[:max_lv], start=1):
|
||||
if remaining <= 0:
|
||||
break
|
||||
try:
|
||||
@@ -223,7 +226,7 @@ def estimate_close_by_bids(
|
||||
remaining -= take
|
||||
covered = target - remaining
|
||||
avg_px = (total_received / eth_amount_from_sheets(covered, ct_mult)) if covered > 0 else None
|
||||
# 净盈亏 = 按买盘可回收 − 全部权利金(与「可落袋」口径一致;买一不够会展开更多档)
|
||||
# 净盈亏 = 本轮买盘可回收 − 全部权利金(买一不够时剩余张数计入 uncovered)
|
||||
estimated_pnl = None
|
||||
estimated_pnl_ratio_pct = None
|
||||
if premium_paid is not None and covered > 0:
|
||||
@@ -242,6 +245,7 @@ def estimate_close_by_bids(
|
||||
"bid_invalid": False,
|
||||
"bid_invalid_reason": None,
|
||||
"auto_close_blocked": False,
|
||||
"max_levels": max_lv,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1083
-1337
File diff suppressed because it is too large
Load Diff
+443
-813
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,8 @@
|
||||
|
||||
<div class="options-dual-grid">
|
||||
<div class="card options-order-card">
|
||||
<h2>期权下单</h2>
|
||||
<p class="muted options-hint">报价单位为每 1 ETH/BTC;1 张 = 0.01.<strong>列表</strong>含卖一/买一;<strong>T 型</strong>仅卖一(买方开仓),中间为跨式双买测算.卖一无挂单时以标记价估算并标 <strong>~</strong>.链展示近 <span id="opt-chain-dte">14</span> 日到期.<strong>T 型</strong>默认 ATM ±5 档,可展开全部.</p>
|
||||
<h2>期权下单 <a class="muted" href="/options/guide" target="_blank" rel="noopener" style="font-size:13px;font-weight:500;margin-left:8px">开平仓与监控说明</a></h2>
|
||||
<p class="muted options-hint">报价单位为每 1 ETH/BTC;1 张 = 0.01.<strong>列表</strong>含卖一/买一;<strong>T 型</strong>仅卖一(买方开仓),中间为跨式双买测算.卖一无挂单时以标记价估算并标 <strong>~</strong>.链展示近 <span id="opt-chain-dte">14</span> 日到期.<strong>T 型</strong>默认 ATM ±5 档,可展开全部.平仓仅买一限价,见说明.</p>
|
||||
<div class="form-row options-chain-toolbar">
|
||||
<button type="button" class="btn-secondary opt-uly-btn active" data-uly="ETH">ETH</button>
|
||||
<button type="button" class="btn-secondary opt-uly-btn" data-uly="BTC">BTC</button>
|
||||
@@ -137,15 +137,16 @@
|
||||
<div id="opt-pos-cards"></div>
|
||||
</div>
|
||||
<details class="opt-close-rule">
|
||||
<summary>多档平仓规则说明</summary>
|
||||
<summary>买一平仓规则说明</summary>
|
||||
<div class="opt-close-rule-body">
|
||||
<p>系统平仓前会重新读取最新买盘,不使用页面缓存。</p>
|
||||
<p>平仓前重新读盘口并校验有效流动性;市价平仓已禁用。</p>
|
||||
<ul>
|
||||
<li>买一数量足够覆盖持仓时,只按买一价提交一笔限价卖单。</li>
|
||||
<li>买一不够时,先卖买一可覆盖数量;成交后刷新持仓和盘口,再继续用新的最优买盘拆分。</li>
|
||||
<li>最多尝试 5 次,全程使用限价卖出,并带 <code>reduceOnly</code>,不会主动市价平仓。</li>
|
||||
<li>盘口不足或订单未成交时会停止后续拆单,并提示剩余张数。</li>
|
||||
<li>本轮只锁<strong>买一</strong>:张数 = min(持仓, 买一深度),限价 = 当场买一。</li>
|
||||
<li>买一不够时只平能吃掉的部分,剩余等下次再点「买一平仓」。</li>
|
||||
<li>手动平仓只验有效买一(非残档);目标位自动平额外需可回收≥2×权利金并持续约 2 分钟。</li>
|
||||
<li>全程 <code>reduceOnly</code> 限价卖,不吃买二及以下、不走市价。</li>
|
||||
</ul>
|
||||
<p><a href="/options/guide" target="_blank" rel="noopener">打开《期权开平仓与监控说明》</a></p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -257,4 +258,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=28"></script>
|
||||
<script src="/static/options_panel.js?v=29"></script>
|
||||
|
||||
Reference in New Issue
Block a user