Add depth-based option close flow.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -185,6 +185,34 @@
|
||||
return price + "/" + size;
|
||||
}
|
||||
|
||||
function fmtBidDepth(levels) {
|
||||
const bids = (levels || []).slice(0, 5);
|
||||
if (!bids.length) return "—";
|
||||
return bids.map(function (x, idx) {
|
||||
return "买" + (idx + 1) + " " + fmtPxSz(x.px, x.sz);
|
||||
}).join(" · ");
|
||||
}
|
||||
|
||||
function fmtClosePreview(preview) {
|
||||
if (!preview || preview.total_received == null) return "—";
|
||||
let text = fmt(preview.total_received, 4) + " USDC";
|
||||
if (preview.covered_sheets != null) {
|
||||
text += " · 覆盖 " + preview.covered_sheets + "张";
|
||||
}
|
||||
if (preview.uncovered_sheets > 0) {
|
||||
text += " · 缺 " + preview.uncovered_sheets + "张";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function fmtPreviewLevels(preview) {
|
||||
const levels = (preview && preview.levels) || [];
|
||||
if (!levels.length) return "暂无可用买盘深度";
|
||||
return levels.map(function (x) {
|
||||
return "买" + x.level + " " + fmt(x.px, 4) + " × " + x.sheets + "张 ≈ " + fmt(x.received, 4) + " USDC";
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function pnlCls(v) {
|
||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "";
|
||||
const n = Number(v);
|
||||
@@ -456,12 +484,15 @@
|
||||
const sideCls = (p.opt_type || "").toUpperCase() === "P" ? "pos-side-short" : "pos-side-long";
|
||||
const expMs = p.exp_time_ms != null ? p.exp_time_ms : p.exp_time;
|
||||
const expAttr = expMs != null && expMs !== "" ? String(expMs) : "";
|
||||
const closePreview = p.close_preview || {};
|
||||
const closePreviewCls = pnlCls(closePreview.estimated_pnl);
|
||||
const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos;
|
||||
return (
|
||||
'<div class="pos-card-head">' +
|
||||
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + '</strong>' +
|
||||
'<span class="pos-side-badge ' + sideCls + '">' + optTypeLabel(p.opt_type) + "</span></div>" +
|
||||
'<div class="pos-head-actions">' +
|
||||
'<button type="button" class="btn-primary opt-close-btn" data-inst="' + p.inst_id + '">限价平仓</button>' +
|
||||
'<button type="button" class="btn-primary opt-close-btn" data-inst="' + p.inst_id + '" data-sheets="' + closeSheets + '">多档平仓</button>' +
|
||||
"</div></div>" +
|
||||
'<div class="pos-meta">' +
|
||||
'<span class="pos-meta-item">行权价: ' + fmt(p.strike, 0) + "</span>" +
|
||||
@@ -475,6 +506,8 @@
|
||||
'<div class="pos-cell"><span class="pos-label">开仓均价</span><span class="pos-value">' + fmt(p.avg_px, 4) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">' + fmt(p.mark_px, 4) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">指数价</span><span class="pos-value">' + fmt(p.idx_px, 0) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">买盘深度</span><span class="pos-value">' + fmtBidDepth(p.bid_depth) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">按买盘收回</span><span class="pos-value ' + closePreviewCls + '">' + fmtClosePreview(closePreview) + "</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 ' + uplCls + '">' + fmt(p.upl, 2) + "</span></div>" +
|
||||
@@ -562,26 +595,42 @@
|
||||
}
|
||||
|
||||
async function closePosition(inst, btn) {
|
||||
const q = await apiJson("/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=sheets&sheets=1");
|
||||
const sheets = btn && btn.getAttribute("data-sheets") ? parseInt(btn.getAttribute("data-sheets"), 10) : null;
|
||||
let url = "/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=close_preview";
|
||||
if (sheets && sheets > 0) url += "&sheets=" + encodeURIComponent(sheets);
|
||||
const q = await apiJson(url);
|
||||
if (!q.ok) {
|
||||
alert(q.msg || "获取买一价失败");
|
||||
return;
|
||||
}
|
||||
const bid = q.bid;
|
||||
if (bid == null || bid <= 0) {
|
||||
alert("暂无买一价,请稍后在 OKX App 平仓或等盘口恢复");
|
||||
const preview = q.close_preview || {};
|
||||
if (!preview.covered_sheets || preview.covered_sheets <= 0) {
|
||||
alert("暂无可用买盘深度,请稍后在 OKX App 平仓或等盘口恢复");
|
||||
return;
|
||||
}
|
||||
if (!confirm("限价卖出 @ 买一 " + fmt(bid, 4) + "(每 1 ETH/BTC)?\n合约:" + inst)) return;
|
||||
const msg = [
|
||||
"按最多5档买盘拆分限价卖出?",
|
||||
"合约: " + inst,
|
||||
"预计收回: " + fmtClosePreview(preview),
|
||||
preview.estimated_pnl != null ? "预估盈亏: " + fmt(preview.estimated_pnl, 4) + " USDC" : "",
|
||||
"",
|
||||
fmtPreviewLevels(preview),
|
||||
preview.uncovered_sheets > 0 ? "\n注意: 当前买盘不足,预计仍剩 " + preview.uncovered_sheets + " 张未覆盖。" : ""
|
||||
].filter(function (x) { return x !== ""; }).join("\n");
|
||||
if (!confirm(msg)) return;
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const r = await apiJson("/api/options/close", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ inst_id: inst }),
|
||||
body: JSON.stringify({ inst_id: inst, mode: "depth_split", sheets: sheets }),
|
||||
});
|
||||
if (r.ok) {
|
||||
alert("平仓单已提交" + (r.bid != null ? " @ " + fmt(r.bid, 4) : ""));
|
||||
let okMsg = "平仓单已提交 " + (r.submitted_sheets || 0) + " 张";
|
||||
if (r.premium_received != null) okMsg += "\n预估收回: " + fmt(r.premium_received, 4) + " USDC";
|
||||
if (r.remaining_sheets > 0) okMsg += "\n剩余: " + r.remaining_sheets + " 张";
|
||||
if (r.stopped_reason) okMsg += "\n停止原因: " + r.stopped_reason;
|
||||
alert(okMsg);
|
||||
} else {
|
||||
alert(r.msg || "平仓失败");
|
||||
}
|
||||
|
||||
@@ -176,6 +176,38 @@ def _fetch_book_bid_ask(ex: ccxt.okx, inst_id: str) -> tuple[float | None, float
|
||||
return bid, ask
|
||||
|
||||
|
||||
def _normalize_book_levels(rows: list[Any], depth: int) -> list[dict[str, float]]:
|
||||
levels: list[dict[str, float]] = []
|
||||
for row in rows[: max(0, int(depth))]:
|
||||
if not isinstance(row, (list, tuple)) or len(row) < 2:
|
||||
continue
|
||||
px = _safe_float(row[0])
|
||||
sz = _safe_float(row[1])
|
||||
if px is None or sz is None or px <= 0 or sz <= 0:
|
||||
continue
|
||||
levels.append({"px": px, "sz": sz})
|
||||
return levels
|
||||
|
||||
|
||||
def fetch_option_book_depth(ex: ccxt.okx, inst_id: str, depth: int = 5) -> dict[str, list[dict[str, float]]]:
|
||||
"""获取期权盘口深度,sz 为 OKX 返回的张数口径."""
|
||||
inst_id = (inst_id or "").strip()
|
||||
if not inst_id:
|
||||
return {"bids": [], "asks": []}
|
||||
try:
|
||||
sz = str(max(1, min(int(depth), 10)))
|
||||
rows = ex.public_get_market_books({"instId": inst_id, "sz": sz}).get("data") or []
|
||||
if not rows:
|
||||
return {"bids": [], "asks": []}
|
||||
row = rows[0]
|
||||
return {
|
||||
"bids": _normalize_book_levels(row.get("bids") or [], int(depth)),
|
||||
"asks": _normalize_book_levels(row.get("asks") or [], int(depth)),
|
||||
}
|
||||
except Exception:
|
||||
return {"bids": [], "asks": []}
|
||||
|
||||
|
||||
def _fetch_book_top(
|
||||
ex: ccxt.okx, inst_id: str
|
||||
) -> tuple[float | None, float | None, float | None, float | None]:
|
||||
|
||||
@@ -50,6 +50,68 @@ def total_premium(quote_per_unit: float, eth_amount: float, ct_mult: float = 0.0
|
||||
return float(quote_per_unit) * float(eth_amount)
|
||||
|
||||
|
||||
def estimate_close_by_bids(
|
||||
bids: list[dict[str, Any]] | None,
|
||||
sheets: int | float,
|
||||
*,
|
||||
ct_mult: float = 0.01,
|
||||
premium_paid: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""按买一到买N逐档估算限价卖出可收回金额."""
|
||||
target = max(0, int(float(sheets or 0)))
|
||||
remaining = target
|
||||
total_received = 0.0
|
||||
levels: list[dict[str, Any]] = []
|
||||
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,
|
||||
}
|
||||
for i, level in enumerate(bids or [], start=1):
|
||||
if remaining <= 0:
|
||||
break
|
||||
try:
|
||||
px = float(level.get("px"))
|
||||
sz = int(float(level.get("sz")))
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
continue
|
||||
if px <= 0 or sz <= 0:
|
||||
continue
|
||||
take = min(remaining, sz)
|
||||
eth_amount = eth_amount_from_sheets(take, ct_mult)
|
||||
received = total_premium(px, eth_amount)
|
||||
levels.append(
|
||||
{
|
||||
"level": i,
|
||||
"px": px,
|
||||
"available_sheets": sz,
|
||||
"sheets": take,
|
||||
"eth_amount": eth_amount,
|
||||
"received": round(received, 4),
|
||||
}
|
||||
)
|
||||
total_received += received
|
||||
remaining -= take
|
||||
covered = target - remaining
|
||||
avg_px = (total_received / eth_amount_from_sheets(covered, ct_mult)) if covered > 0 else None
|
||||
estimated_pnl = None
|
||||
if premium_paid is not None and covered > 0:
|
||||
paid_basis = float(premium_paid) * (covered / target)
|
||||
estimated_pnl = round(total_received - paid_basis, 4)
|
||||
return {
|
||||
"levels": levels,
|
||||
"covered_sheets": covered,
|
||||
"uncovered_sheets": remaining,
|
||||
"total_received": round(total_received, 4),
|
||||
"avg_px": round(avg_px, 4) if avg_px is not None else None,
|
||||
"estimated_pnl": estimated_pnl,
|
||||
}
|
||||
|
||||
|
||||
def sheets_from_eth_amount(eth_amount: float, ct_mult: float = 0.01) -> int:
|
||||
if eth_amount <= 0 or ct_mult <= 0:
|
||||
return 0
|
||||
|
||||
+208
-11
@@ -14,6 +14,7 @@ from lib.options.options_monitor_lib import options_monitor_loop
|
||||
from lib.options.options_pricing_lib import (
|
||||
calc_order_size,
|
||||
ct_mult_from_meta,
|
||||
estimate_close_by_bids,
|
||||
min_sz_from_meta,
|
||||
premium_per_sheet,
|
||||
total_premium,
|
||||
@@ -76,6 +77,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
build_option_chain,
|
||||
estimate_usdt_to_usdc,
|
||||
execute_convert,
|
||||
fetch_option_book_depth,
|
||||
fetch_option_positions,
|
||||
fetch_options_balances,
|
||||
format_position_row,
|
||||
@@ -109,6 +111,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
"account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "OKX期权").strip(),
|
||||
"build_option_chain": build_option_chain,
|
||||
"quote_option_contract": quote_option_contract,
|
||||
"fetch_option_book_depth": fetch_option_book_depth,
|
||||
"place_option_limit_order": place_option_limit_order,
|
||||
"place_option_market_order": place_option_market_order,
|
||||
"fetch_option_positions": fetch_option_positions,
|
||||
@@ -143,6 +146,75 @@ def _budget_full_usdc(cfg: dict[str, Any], ex: Any) -> tuple[float | None, str]:
|
||||
return float(raw), ""
|
||||
|
||||
|
||||
def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None:
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
init_options_tables(conn)
|
||||
rec = conn.execute(
|
||||
"""
|
||||
SELECT premium_paid FROM options_trades
|
||||
WHERE inst_id = ? AND status = 'open'
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""",
|
||||
(inst_id,),
|
||||
).fetchone()
|
||||
if rec and rec["premium_paid"] is not None:
|
||||
return round(float(rec["premium_paid"]), 4)
|
||||
finally:
|
||||
conn.close()
|
||||
return None
|
||||
|
||||
|
||||
def _position_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 _find_position(rows: list[dict[str, Any]] | None, inst_id: str) -> dict[str, Any] | None:
|
||||
return next((p for p in rows or [] if str(p.get("instId")) == inst_id), None)
|
||||
|
||||
|
||||
def _refresh_position_avail(cfg: dict[str, Any], ex: Any, inst_id: str) -> int | None:
|
||||
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
|
||||
|
||||
invalidate_option_positions_cache()
|
||||
raw = cfg["fetch_option_positions"](ex)
|
||||
if raw is None:
|
||||
return None
|
||||
pos = _find_position(raw, inst_id)
|
||||
if not pos:
|
||||
return 0
|
||||
return _position_avail_sheets(pos)
|
||||
|
||||
|
||||
def _attach_close_preview(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
row: dict[str, Any],
|
||||
*,
|
||||
sheets: int | None = None,
|
||||
premium_paid: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
inst_id = str(row.get("inst_id") or row.get("instId") or "").strip()
|
||||
if not inst_id:
|
||||
return row
|
||||
ct_mult = float(row.get("ct_mult") or 0.01)
|
||||
target_sheets = int(sheets) if sheets is not None else int(abs(_safe_float(row.get("pos")) or 0))
|
||||
paid = premium_paid if premium_paid is not None else _safe_float(row.get("premium_paid"))
|
||||
book = cfg["fetch_option_book_depth"](ex, inst_id, 5)
|
||||
row["bid_depth"] = book.get("bids") or []
|
||||
row["ask_depth"] = book.get("asks") or []
|
||||
row["close_preview"] = estimate_close_by_bids(
|
||||
row["bid_depth"],
|
||||
target_sheets,
|
||||
ct_mult=ct_mult,
|
||||
premium_paid=paid,
|
||||
)
|
||||
return row
|
||||
|
||||
|
||||
_OPTIONS_SYNC_LOCK = threading.Lock()
|
||||
_OPTIONS_SYNC_LAST_AT = 0.0
|
||||
_OPTIONS_SYNC_INTERVAL_SEC = 15.0
|
||||
@@ -231,6 +303,16 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
ct_mult = q.get("ct_mult") or 0.01
|
||||
min_sz = q.get("min_sz") or 1
|
||||
mode = (request.args.get("mode") or "budget_full").strip()
|
||||
sheet_count = None
|
||||
try:
|
||||
if request.args.get("sheets"):
|
||||
sheet_count = int(request.args.get("sheets"))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if mode == "close_preview":
|
||||
paid = _open_premium_paid(cfg, inst_id)
|
||||
target = sheet_count if sheet_count is not None else 0
|
||||
return jsonify(_attach_close_preview(cfg, ex, {**q, "pos": target, "premium_paid": paid}, sheets=target, premium_paid=paid))
|
||||
budget = cfg["trade_budget"]
|
||||
budget_cap = cfg["trade_budget"]
|
||||
available_usdc = None
|
||||
@@ -243,17 +325,11 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
|
||||
available_usdc = fetch_options_trading_usdc(ex)
|
||||
eth_amount = None
|
||||
sheet_count = None
|
||||
try:
|
||||
if request.args.get("eth_amount"):
|
||||
eth_amount = float(request.args.get("eth_amount"))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
if request.args.get("sheets"):
|
||||
sheet_count = int(request.args.get("sheets"))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if ask is None or ask <= 0:
|
||||
return jsonify({**q, "ok": False, "msg": "暂无卖一价"})
|
||||
sizing = calc_order_size(
|
||||
@@ -266,6 +342,13 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
sheets=sheet_count if mode == "sheets" else None,
|
||||
budget_cap=budget_cap if mode in ("budget_full", "sheets", "eth_amount") else None,
|
||||
)
|
||||
q = _attach_close_preview(
|
||||
cfg,
|
||||
ex,
|
||||
q,
|
||||
sheets=int(sizing.get("sheets") or sheet_count or 0),
|
||||
premium_paid=_open_premium_paid(cfg, inst_id),
|
||||
)
|
||||
return jsonify(
|
||||
{
|
||||
**q,
|
||||
@@ -403,6 +486,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
).fetchone()
|
||||
if rec and rec["premium_paid"] is not None:
|
||||
row["premium_paid"] = round(float(rec["premium_paid"]), 4)
|
||||
_attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid")))
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "positions": rows})
|
||||
@@ -416,23 +500,24 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
data = request.get_json(silent=True) or {}
|
||||
inst_id = (data.get("inst_id") or "").strip()
|
||||
use_market = bool(data.get("market")) and cfg["allow_market_close"]
|
||||
close_mode = (data.get("mode") or "").strip()
|
||||
depth_split = close_mode == "depth_split" and not use_market
|
||||
if not inst_id:
|
||||
return jsonify({"ok": False, "msg": "缺少 inst_id"})
|
||||
sheets = data.get("sheets")
|
||||
q = cfg["quote_option_contract"](ex, inst_id)
|
||||
bid = q.get("bid")
|
||||
if not use_market and (bid is None or bid <= 0):
|
||||
if not use_market and not depth_split and (bid is None or bid <= 0):
|
||||
return jsonify({"ok": False, "msg": "暂无买一价,无法限价平仓"})
|
||||
raw_positions = cfg["fetch_option_positions"](ex)
|
||||
if raw_positions is None:
|
||||
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
||||
pos = next((p for p in raw_positions if str(p.get("instId")) == inst_id), None)
|
||||
pos = _find_position(raw_positions, inst_id)
|
||||
if not pos:
|
||||
return jsonify({"ok": False, "msg": "未找到持仓"})
|
||||
avail = _safe_float(pos.get("availPos"))
|
||||
if avail is None or avail <= 0:
|
||||
avail = abs(_safe_float(pos.get("pos")) or 0)
|
||||
avail = _position_avail_sheets(pos)
|
||||
close_sheets = int(sheets) if sheets else int(avail)
|
||||
close_sheets = min(close_sheets, int(avail))
|
||||
if close_sheets < 1:
|
||||
return jsonify({"ok": False, "msg": "可平张数不足"})
|
||||
td_mode = str(pos.get("mgnMode") or cfg["td_mode"])
|
||||
@@ -450,6 +535,118 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
)
|
||||
if not order.get("ok"):
|
||||
return jsonify(order)
|
||||
elif depth_split:
|
||||
ct_mult = float(q.get("ct_mult") or 0.01)
|
||||
remaining = close_sheets
|
||||
submitted_sheets = 0
|
||||
filled_or_reduced_sheets = 0
|
||||
total_received = 0.0
|
||||
orders: list[dict[str, Any]] = []
|
||||
stopped_reason = None
|
||||
for _ in range(5):
|
||||
if remaining <= 0:
|
||||
break
|
||||
current_avail = _refresh_position_avail(cfg, ex, inst_id)
|
||||
if current_avail is None:
|
||||
stopped_reason = "refresh_position_failed"
|
||||
break
|
||||
if current_avail <= 0:
|
||||
filled_or_reduced_sheets = close_sheets
|
||||
remaining = 0
|
||||
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)
|
||||
levels = preview.get("levels") or []
|
||||
if not levels:
|
||||
stopped_reason = "no_bid_depth"
|
||||
break
|
||||
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:
|
||||
stopped_reason = "invalid_bid_depth"
|
||||
break
|
||||
before_avail = current_avail
|
||||
order = cfg["place_option_limit_order"](
|
||||
ex,
|
||||
inst_id=inst_id,
|
||||
side="sell",
|
||||
sheets=level_sheets,
|
||||
price=level_px,
|
||||
td_mode=td_mode,
|
||||
tick_sz=tick_sz,
|
||||
reduce_only=True,
|
||||
pos_side=pos_side,
|
||||
)
|
||||
if not order.get("ok"):
|
||||
stopped_reason = order.get("msg") or "order_failed"
|
||||
break
|
||||
px = float(order.get("px", level_px))
|
||||
orders.append({"order": order, "px": px, "sheets": level_sheets})
|
||||
submitted_sheets += level_sheets
|
||||
total_received += total_premium(px, level_sheets * ct_mult)
|
||||
time.sleep(0.6)
|
||||
after_avail = _refresh_position_avail(cfg, ex, inst_id)
|
||||
if after_avail is None:
|
||||
stopped_reason = "refresh_position_failed"
|
||||
break
|
||||
reduced = max(0, before_avail - after_avail)
|
||||
if reduced <= 0:
|
||||
stopped_reason = "order_not_filled"
|
||||
break
|
||||
filled_or_reduced_sheets += min(reduced, level_sheets)
|
||||
remaining = max(0, close_sheets - filled_or_reduced_sheets)
|
||||
if not orders:
|
||||
return jsonify({"ok": False, "msg": "暂无可用买盘深度,无法拆分平仓", "stopped_reason": stopped_reason})
|
||||
bid = (total_received / (submitted_sheets * ct_mult)) if submitted_sheets > 0 and ct_mult > 0 else 0
|
||||
prem_recv = round(total_received, 4)
|
||||
fully_submitted = submitted_sheets >= close_sheets and stopped_reason is None
|
||||
conn = cfg["get_db"]()
|
||||
try:
|
||||
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 and fully_submitted:
|
||||
paid = float(row["premium_paid"] or 0)
|
||||
pnl = prem_recv - paid
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_trades
|
||||
SET status = 'closed', close_quote = ?, premium_received = ?,
|
||||
realized_pnl = ?, close_ord_id = ?, closed_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
bid,
|
||||
prem_recv,
|
||||
pnl,
|
||||
",".join(str((o.get("order", {}).get("data") or {}).get("ordId") or "") for o in orders),
|
||||
int(row["id"]),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
|
||||
|
||||
invalidate_option_positions_cache()
|
||||
_sync_options_trades(cfg, force=True)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"mode": "depth_split",
|
||||
"orders": orders,
|
||||
"bid": bid,
|
||||
"submitted_sheets": submitted_sheets,
|
||||
"filled_or_reduced_sheets": filled_or_reduced_sheets,
|
||||
"remaining_sheets": max(0, close_sheets - filled_or_reduced_sheets),
|
||||
"premium_received": prem_recv,
|
||||
"stopped_reason": stopped_reason,
|
||||
}
|
||||
)
|
||||
else:
|
||||
close_px = float(bid)
|
||||
order = cfg["place_option_limit_order"](
|
||||
|
||||
@@ -200,4 +200,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=20"></script>
|
||||
<script src="/static/options_panel.js?v=21"></script>
|
||||
|
||||
@@ -129,6 +129,42 @@ def test_format_quote_liquidity():
|
||||
assert format_quote_liquidity(None, 10) is None
|
||||
|
||||
|
||||
def test_estimate_close_by_bids_full_depth():
|
||||
from lib.options.options_pricing_lib import estimate_close_by_bids
|
||||
|
||||
out = estimate_close_by_bids(
|
||||
[{"px": 12.3, "sz": 2}, {"px": 12.1, "sz": 3}],
|
||||
4,
|
||||
ct_mult=0.01,
|
||||
premium_paid=0.4,
|
||||
)
|
||||
assert out["covered_sheets"] == 4
|
||||
assert out["uncovered_sheets"] == 0
|
||||
assert out["total_received"] == 0.488
|
||||
assert out["avg_px"] == 12.2
|
||||
assert out["estimated_pnl"] == 0.088
|
||||
assert [x["sheets"] for x in out["levels"]] == [2, 2]
|
||||
|
||||
|
||||
def test_estimate_close_by_bids_partial_depth():
|
||||
from lib.options.options_pricing_lib import estimate_close_by_bids
|
||||
|
||||
out = estimate_close_by_bids([{"px": 10, "sz": 1}], 3, ct_mult=0.01, premium_paid=0.6)
|
||||
assert out["covered_sheets"] == 1
|
||||
assert out["uncovered_sheets"] == 2
|
||||
assert out["total_received"] == 0.1
|
||||
assert out["estimated_pnl"] == -0.1
|
||||
|
||||
|
||||
def test_estimate_close_by_bids_empty():
|
||||
from lib.options.options_pricing_lib import estimate_close_by_bids
|
||||
|
||||
out = estimate_close_by_bids([], 2)
|
||||
assert out["covered_sheets"] == 0
|
||||
assert out["uncovered_sheets"] == 2
|
||||
assert out["avg_px"] is None
|
||||
|
||||
|
||||
def test_expiry_breakeven_from_ask():
|
||||
from lib.options.options_pricing_lib import expiry_breakeven_from_ask
|
||||
|
||||
|
||||
Reference in New Issue
Block a user