From b7b1a7e7e0d5571f7b48f5bfa2a14ddb6eaaa198 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 14 Oct 2026 16:45:05 +0800 Subject: [PATCH] Wide order dialog (calc|form); enrich live cards with lev/bid/margin/time. Co-authored-by: Cursor --- lib/common/static/hedge_plan.js | 80 +++++++++++++++--- lib/common/static/instance_theme.css | 59 +++++++++++++- .../hedge_plan_live_positions_lib.py | 81 +++++++++++++++++-- .../templates/hedge_plan_panel.html | 29 ++++--- 4 files changed, 213 insertions(+), 36 deletions(-) diff --git a/lib/common/static/hedge_plan.js b/lib/common/static/hedge_plan.js index 8298cf7..2f79580 100644 --- a/lib/common/static/hedge_plan.js +++ b/lib/common/static/hedge_plan.js @@ -631,14 +631,26 @@ const profitBox = $("hp-po-exit-profit"); const targetBox = $("hp-po-exit-target"); if (profitBox) { - profitBox.classList.toggle("hidden", mode !== "profit"); - if (mode === "profit") profitBox.removeAttribute("hidden"); - else profitBox.setAttribute("hidden", "hidden"); + const show = mode === "profit"; + profitBox.classList.toggle("hidden", !show); + if (show) { + profitBox.removeAttribute("hidden"); + profitBox.style.display = ""; + } else { + profitBox.setAttribute("hidden", "hidden"); + profitBox.style.display = "none"; + } } if (targetBox) { - targetBox.classList.toggle("hidden", mode !== "target"); - if (mode === "target") targetBox.removeAttribute("hidden"); - else targetBox.setAttribute("hidden", "hidden"); + const show = mode === "target"; + targetBox.classList.toggle("hidden", !show); + if (show) { + targetBox.removeAttribute("hidden"); + targetBox.style.display = ""; + } else { + targetBox.setAttribute("hidden", "hidden"); + targetBox.style.display = "none"; + } } } @@ -904,6 +916,19 @@ return n > 0 ? "+" + s : s; } + function fmtDuration(sec) { + if (sec == null || Number.isNaN(Number(sec))) return "—"; + let s = Math.max(0, Math.floor(Number(sec))); + const d = Math.floor(s / 86400); + s -= d * 86400; + const h = Math.floor(s / 3600); + s -= h * 3600; + const m = Math.floor(s / 60); + if (d > 0) return d + "天 " + h + "时" + m + "分"; + if (h > 0) return h + "时" + m + "分"; + return m + "分"; + } + function renderLivePosCard(card) { const opt = card.option || {}; const perp = card.perp || {}; @@ -911,6 +936,11 @@ const optType = String(opt.opt_type || "").toUpperCase() === "P" ? "Put" : "Call"; const perpSide = String(perp.side || "").toLowerCase() === "short" ? "空" : "多"; const comboCls = pnlClsNum(combo.net_usdt); + const liqWarn = opt.liq_ok === false; + const bidTxt = + opt.bid != null + ? fmt(opt.bid, 4) + (opt.bid_sz != null ? "/" + fmt(opt.bid_sz, 0) : "") + : "—"; return ( '
' + - (card.opened_at || "") + - "" + + "" + + '
' + + "开仓 " + + (card.opened_at || "—") + + "" + + "持仓时长 " + + fmtDuration(card.hold_sec) + + "" + + "到期剩余 " + + fmtDuration(card.remain_sec) + + "" + + "
" + '
' + '
' + '
期权 · ' + @@ -935,12 +974,23 @@ "张数 " + fmt(opt.sheets, 0) + "" + + "杠杆 " + + (opt.leverage != null ? fmt(opt.leverage, 1) + "×" : "—") + + "" + "开仓 " + fmt(opt.avg_open, 4) + "" + "标记 " + fmt(opt.mark_px, 4) + "" + + '买一/流动性 ' + + bidTxt + + (liqWarn ? " · 不足" : "") + + "" + '净利 ' + @@ -958,12 +1008,18 @@ "张数 " + fmt(perp.contracts, 4) + "" + + "杠杆 " + + (perp.leverage != null ? fmt(perp.leverage, 0) + "×" : "—") + + "" + "开仓 " + fmt(perp.avg_open, 2) + "" + "标记 " + fmt(perp.mark_px, 2) + "" + + "保证金 " + + (perp.margin != null ? fmt(perp.margin, 2) + " U" : "—") + + "" + '净利 ' + @@ -985,7 +1041,7 @@ '">成交细节 ' + '' + + '">平仓' + "
" ); } @@ -2864,9 +2920,9 @@ async function endActivePlan(planId) { if ( !window.confirm( - "确认结束计划 #" + + "确认平仓并结束计划 #" + planId + - "?\n不会自动平仓;未成交/待补腿将标为未成交取消。\n已有持仓请自行平掉。" + "?\n未成交/待补腿将取消。\n说明:当前为结束计划标记,交易所持仓请自行确认已平。" ) ) { return; diff --git a/lib/common/static/instance_theme.css b/lib/common/static/instance_theme.css index 6f0c60f..ef3c783 100644 --- a/lib/common/static/instance_theme.css +++ b/lib/common/static/instance_theme.css @@ -3507,6 +3507,44 @@ html[data-theme="light"] .opt-be-dist-down { padding: 20px 22px 22px; max-height: 90vh; } +.opt-order-dialog.hp-po-order-dialog--wide { + width: min(96vw, 920px); +} +.hp-po-order-dialog .hp-po-order-split { + display: grid; + grid-template-columns: 1fr; + gap: 18px; +} +@media (min-width: 760px) { + .hp-po-order-dialog .hp-po-order-split { + grid-template-columns: minmax(280px, 1fr) minmax(320px, 1.15fr); + gap: 22px; + align-items: start; + } +} +.hp-po-order-dialog .hp-po-order-calc-pane { + display: flex; + flex-direction: column; + gap: 12px; + padding: 14px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.07); + min-height: 0; +} +.hp-po-order-dialog .hp-po-calc-box { + display: flex; + flex-direction: column; + gap: 10px; + font-size: 0.8rem; + line-height: 1.5; +} +.hp-po-order-dialog .hp-po-calc-box p { + margin: 0; +} +.hp-po-order-dialog .hp-po-order-form-pane { + gap: 16px; +} .hp-po-order-dialog .opt-order-main { gap: 18px; } @@ -4134,9 +4172,28 @@ html[data-theme="light"] .opt-be-dist-down { justify-content: space-between; gap: 8px; align-items: baseline; - margin-bottom: 10px; + margin-bottom: 8px; font-size: 0.88rem; } +.hedge-plan-page-wrap .hp-live-time-row { + display: flex; + flex-wrap: wrap; + gap: 6px 14px; + margin: 0 0 10px; + padding: 8px 10px; + border-radius: 8px; + background: rgba(0, 0, 0, 0.16); + font-size: 0.76rem; + color: #9aa4b2; +} +.hedge-plan-page-wrap .hp-live-time-row strong { + color: #e8ecf4; + font-weight: 600; +} +.hedge-plan-page-wrap .hp-liq-warn, +.hedge-plan-page-wrap .hp-liq-warn strong { + color: #f5c542 !important; +} .hedge-plan-page-wrap .hp-live-legs { display: grid; grid-template-columns: 1fr; diff --git a/lib/hedge_plan/hedge_plan_live_positions_lib.py b/lib/hedge_plan/hedge_plan_live_positions_lib.py index 99113e7..8455c2c 100644 --- a/lib/hedge_plan/hedge_plan_live_positions_lib.py +++ b/lib/hedge_plan/hedge_plan_live_positions_lib.py @@ -1,15 +1,19 @@ """永期对冲 · 当前持仓:期权+永续实时卡(组合净利扣开平双边手续费).""" from __future__ import annotations +from datetime import datetime, timedelta, timezone from typing import Any, Optional from lib.hedge_plan.hedge_plan_option_primary_lib import ( estimate_combo_net_pnl, fee_rate, opt_type_for_view, + option_bid_liquidity_ok, perp_direction_for_view, ) +_BJ = timezone(timedelta(hours=8)) + def _sf(v: Any) -> Optional[float]: try: @@ -33,19 +37,52 @@ def _pick_option_row(rows: list[dict[str, Any]] | None, inst_id: str) -> Optiona return None -def _option_bid(ex: Any, inst_id: str, fallback: Optional[float]) -> Optional[float]: +def _option_bid_quote( + ex: Any, inst_id: str, fallback: Optional[float] +) -> tuple[Optional[float], Optional[float]]: + """返回 (买一价, 买一张数).""" try: from lib.exchange.okx_options_lib import fetch_option_book_depth book = fetch_option_book_depth(ex, inst_id) or {} bids = book.get("bids") or [] if bids and isinstance(bids[0], dict): - bid = _sf(bids[0].get("px") or bids[0].get("price")) + bid = _sf(bids[0].get("px")) + sz = _sf(bids[0].get("sz")) if bid is not None and bid > 0: - return bid + return bid, sz except Exception: pass - return fallback + return fallback, None + + +def _parse_opt_exp_ms(inst_id: str) -> Optional[int]: + """BTC-USD-261017-84000-P → 到期日 16:00 北京时间.""" + parts = str(inst_id or "").upper().split("-") + if len(parts) < 4: + return None + raw = parts[2] + if len(raw) != 6 or not raw.isdigit(): + return None + try: + yy, mm, dd = int(raw[0:2]), int(raw[2:4]), int(raw[4:6]) + year = 2000 + yy + dt = datetime(year, mm, dd, 16, 0, 0, tzinfo=_BJ) + return int(dt.timestamp() * 1000) + except Exception: + return None + + +def _parse_opened_ms(opened_at: Any) -> Optional[int]: + s = str(opened_at or "").strip() + if not s: + return None + try: + # "2026-10-14 16:22:21" 按北京时间 + dt = datetime.strptime(s[:19], "%Y-%m-%d %H:%M:%S").replace(tzinfo=_BJ) + return int(dt.timestamp() * 1000) + except Exception: + return None def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]: @@ -79,6 +116,7 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any get_cs = cfg.get("get_contract_size") fetch_idx = cfg.get("fetch_index_price") fr = fee_rate() + now_ms = int(datetime.now(tz=_BJ).timestamp() * 1000) out: list[dict[str, Any]] = [] for plan in plans: @@ -97,14 +135,21 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any ct = _sf((opt_leg or {}).get("ct_mult")) or 0.01 avg_open = _sf((opt_leg or {}).get("avg_open")) or _sf((opt_raw or {}).get("avgPx")) or 0.0 mark_opt = _sf((opt_raw or {}).get("markPx")) or _sf((opt_raw or {}).get("last")) or avg_open - bid = _option_bid(ex_opt, inst_id, mark_opt) if inst_id else mark_opt + bid, bid_sz = _option_bid_quote(ex_opt, inst_id, mark_opt) if inst_id else (mark_opt, None) strike = _sf((opt_leg or {}).get("strike")) or 0.0 opt_type = str((opt_leg or {}).get("opt_type") or opt_type_for_view(view)).upper() + # 币本位杠杆≈1÷卖一/标记 + lev_ref = _sf(avg_open) or _sf(mark_opt) or 0.0 + opt_leverage = round(1.0 / lev_ref, 1) if lev_ref and lev_ref > 0 else None + liq_ok, liq_msg = option_bid_liquidity_ok(bid, bid_sz, need_sheets=float(sheets or 0)) sym = str((perp_leg or {}).get("symbol") or plan.get("exchange_symbol") or "") - perp_dir = str((perp_leg or {}).get("side") or plan.get("perp_direction") or perp_direction_for_view(view)).lower() + perp_dir = str( + (perp_leg or {}).get("side") or plan.get("perp_direction") or perp_direction_for_view(view) + ).lower() contracts = _sf((perp_leg or {}).get("size")) or _sf(plan.get("perp_size")) or 0.0 perp_entry = _sf((perp_leg or {}).get("avg_open")) or _sf(plan.get("entry_mark")) or 0.0 + perp_lev = _sf(plan.get("leverage")) or 0.0 cs = 0.01 if callable(get_cs) and sym: try: @@ -115,6 +160,7 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any perp_mark = None perp_upl = None live_contracts = None + margin = None if callable(get_perp_metrics) and sym: try: m = get_perp_metrics(sym, perp_dir, order_leverage=plan.get("leverage")) @@ -122,12 +168,15 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any perp_mark = _sf(m.get("mark_price") or m.get("mark")) perp_upl = _sf(m.get("unrealized_pnl")) live_contracts = _sf(m.get("contracts") or m.get("size")) + margin = _sf(m.get("initial_margin") or m.get("margin")) except Exception: pass if perp_mark is None or perp_mark <= 0: perp_mark = perp_entry if live_contracts and live_contracts > 0: contracts = live_contracts + if (margin is None or margin <= 0) and contracts and perp_mark and perp_lev and perp_lev > 0: + margin = round(float(contracts) * float(cs) * float(perp_mark) / float(perp_lev), 2) idx = None if callable(fetch_idx): @@ -153,19 +202,29 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any contract_size=float(cs or 0.01), fee=fr, ) - # 币本位期权净利(币) → USDT;永续净利已是 U opt_net_coin = float(net.get("opt_net") or 0) opt_net_usdt = round(opt_net_coin * float(idx or 0), 4) if idx else None perp_net = float(net.get("perp_net") or 0) combo_usdt = round((opt_net_usdt or 0) + perp_net, 4) if opt_net_usdt is not None else round(perp_net, 4) + opened_at = plan.get("opened_at") + opened_ms = _parse_opened_ms(opened_at) + # 两腿开仓时间统一取计划 opened_at + exp_ms = _parse_opt_exp_ms(inst_id) + hold_sec = int((now_ms - opened_ms) / 1000) if opened_ms else None + remain_sec = int((exp_ms - now_ms) / 1000) if exp_ms else None + out.append( { "plan_id": plan.get("id"), "status": plan.get("status"), "underlying": uly, "direction": view, - "opened_at": plan.get("opened_at"), + "opened_at": opened_at, + "opened_ms": opened_ms, + "exp_ms": exp_ms, + "hold_sec": hold_sec, + "remain_sec": remain_sec, "index_px": idx, "fee_rate": fr, "option": { @@ -177,6 +236,10 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any "avg_open": avg_open, "mark_px": mark_opt, "bid": bid, + "bid_sz": bid_sz, + "leverage": opt_leverage, + "liq_ok": bool(liq_ok), + "liq_msg": liq_msg or "", "net_coin": opt_net_coin, "net_usdt": opt_net_usdt, "live": bool(opt_raw), @@ -188,6 +251,8 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any "contract_size": cs, "avg_open": perp_entry, "mark_px": perp_mark, + "leverage": perp_lev or None, + "margin": margin, "upl": perp_upl, "net_usdt": perp_net, "live": perp_upl is not None or (perp_mark is not None and perp_mark > 0), diff --git a/lib/hedge_plan/templates/hedge_plan_panel.html b/lib/hedge_plan/templates/hedge_plan_panel.html index ca77389..cf459c7 100644 --- a/lib/hedge_plan/templates/hedge_plan_panel.html +++ b/lib/hedge_plan/templates/hedge_plan_panel.html @@ -102,15 +102,16 @@
- +