From 6c49c7d51ce53e734b910c66ea5fb56204a14177 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 20 Aug 2026 13:36:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B8=81=E6=9C=AC=E4=BD=8D=E6=8C=81=E4=BB=93?= =?UTF-8?q?=E5=8D=A1=E6=9D=83=E5=88=A9=E9=87=91/=E5=9B=9E=E6=94=B6/?= =?UTF-8?q?=E9=97=A8=E6=8E=A7=E6=94=B9=E4=B8=BA=E6=8C=89ETH/BTC=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=EF=BC=8C=E9=81=BF=E5=85=8D=E8=AF=AF=E6=A0=87USDC?= =?UTF-8?q?=E4=B8=8E=E4=B8=A4=E4=BD=8D=E5=B0=8F=E6=95=B0=E6=8A=B9=E9=9B=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- lib/common/static/options_panel.js | 117 ++++++++++++++++---- lib/common/static/options_position_cards.js | 55 +++++++-- lib/exchange/okx_options_lib.py | 30 ++++- lib/options/options_close_gate_lib.py | 47 +++++++- lib/options/options_db.py | 3 +- lib/options/options_history_lib.py | 11 +- lib/options/options_positions_lib.py | 6 +- lib/options/options_register.py | 16 ++- 8 files changed, 240 insertions(+), 45 deletions(-) diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js index 7eecf8f..e25df2c 100644 --- a/lib/common/static/options_panel.js +++ b/lib/common/static/options_panel.js @@ -868,9 +868,46 @@ return Number(v).toFixed(2); } - function fmtClosePreview(preview, premiumPaid) { + function posPremiumCcy(p) { + const ccy = String((p && p.premium_ccy) || "").trim().toUpperCase(); + if (ccy) return ccy; + const mode = String((p && p.margin_mode) || "").toLowerCase(); + const inst = String((p && p.inst_id) || ""); + if (mode === "coin" || (inst.indexOf("-USD-") >= 0 && inst.indexOf("_UM") < 0)) { + return (inst.split("-")[0] || "ETH").toUpperCase() || "ETH"; + } + if (isCoinMarginMode && isCoinMarginMode()) { + return ((inst.split("-")[0]) || "ETH").toUpperCase() || "ETH"; + } + return "USDC"; + } + + function isCoinPos(p) { + return posPremiumCcy(p) !== "USDC"; + } + + function fmtPremiumAmt(v, ccy) { + if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + const unit = String(ccy || "USDC").toUpperCase(); + if (unit === "ETH" || unit === "BTC") { + let s = n.toFixed(8).replace(/\.?0+$/, ""); + return s || "0"; + } + return fmtUsdc(n); + } + + function fmtPremiumAmtSigned(v, ccy) { + if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + const sign = n > 0 ? "+" : ""; + return sign + fmtPremiumAmt(n, ccy) + " " + (String(ccy || "USDC").toUpperCase()); + } + + function fmtClosePreview(preview, premiumPaid, p) { if (!preview || preview.total_received == null) return "—"; - const recvTxt = fmtUsdc(preview.total_received); + const ccy = posPremiumCcy(p); + const recvTxt = fmtPremiumAmt(preview.total_received, ccy); let cls = ""; const prem = Number(premiumPaid); const recv = Number(preview.total_received); @@ -878,12 +915,13 @@ if (recv > prem) cls = " pos-pnl-profit"; else if (recv < prem) cls = " pos-pnl-loss"; } - return '' + recvTxt + " USDC"; + return '' + recvTxt + " " + ccy + ""; } - function fmtClosePreviewText(preview) { + function fmtClosePreviewText(preview, p) { if (!preview || preview.total_received == null) return "—"; - let text = fmt(preview.total_received, 4) + " USDC"; + const ccy = posPremiumCcy(p); + let text = fmtPremiumAmt(preview.total_received, ccy) + " " + ccy; if (preview.covered_sheets != null) { text += " · 覆盖 " + preview.covered_sheets + "张"; } @@ -893,11 +931,13 @@ return text; } - function fmtPreviewLevels(preview) { + function fmtPreviewLevels(preview, p) { const levels = (preview && preview.levels) || []; if (!levels.length) return "暂无可用买盘深度"; + const ccy = posPremiumCcy(p); return levels.map(function (x) { - return "买" + x.level + " " + fmt(x.px, 4) + " × " + x.sheets + "张 ≈ " + fmt(x.received, 4) + " USDC"; + return "买" + x.level + " " + fmt(x.px, 4) + " × " + x.sheets + "张 ≈ " + + fmtPremiumAmt(x.received, ccy) + " " + ccy; }).join("\n"); } @@ -1555,10 +1595,18 @@ const closePreview = p.close_preview || {}; const closeSheets = p.avail_pos != null && Number(p.avail_pos) > 0 ? p.avail_pos : p.pos; const tickSz = p.tick_sz; - const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmtUsdc(p.premium_paid) : null); + const premCcy = posPremiumCcy(p); + const coinPos = isCoinPos(p); + const premTxt = fmtDisplay( + p.premium_paid_fmt, + p.premium_paid != null ? fmtPremiumAmt(p.premium_paid, premCcy) : null + ); // 优先用数值+tick 现算,避免接口侧 mark_px_fmt 带着浮点毛刺直出 const avgTxt = p.avg_px != null ? fmtOptionPx(p.avg_px, tickSz) : fmtDisplay(p.avg_px_fmt); const markTxt = p.mark_px != null ? fmtOptionPx(p.mark_px, tickSz) : fmtDisplay(p.mark_px_fmt); + const netTxt = closePreview.bid_invalid || net == null + ? "—" + : (fmtPremiumAmt(net, premCcy) + (coinPos ? (" " + premCcy) : "")); return ( '
' + '
' + (p.inst_id || "") + '' + @@ -1577,21 +1625,21 @@ : "") + "
" + '
' + - '
权利金' + premTxt + " USDC
" + + '
权利金' + premTxt + " " + premCcy + "
" + '
开仓均价' + avgTxt + "
" + '
标记价' + markTxt + "
" + '
指数价' + fmt(p.idx_px, 0) + "
" + '
到期平衡' + fmt(p.expiry_be_px, 0) + "
" + '
平掉回本' + fmt(p.close_be_px, 0) + "
" + '
净盈亏' + - (closePreview.bid_invalid || net == null ? "—" : fmt(net, 2)) + "
" + + netTxt + "
" + '
收益率' + (closePreview.bid_invalid || roi == null ? "—" : fmt(roi, 2) + "%") + "
" + '
买盘深度' + fmtCloseLevels(closePreview, tickSz) + "
" + '
按买盘回收' + (closePreview.bid_invalid ? '暂无有效买盘' - : fmtClosePreview(closePreview, p.premium_paid)) + "
" + + : fmtClosePreview(closePreview, p.premium_paid, p)) + "
" + "" + (function () { const hint = closeGateHint(closePreview); @@ -1628,6 +1676,7 @@ ); const statePe = String(p.profit_exit_state || (enabled ? "active" : "idle")); const req = p.profit_exit_required_recycle; + const premCcy = posPremiumCcy(p); let statusTxt = enabled ? ("监控中 · " + multLabel) : "未开启"; if (enabled && statePe === "closing") statusTxt = "平仓挂单中 · " + multLabel; return ( @@ -1644,7 +1693,7 @@ '' + statusTxt + "" + '' + (enabled - ? ("1倍=盈利=权利金" + (req != null ? (" · 需回收≥" + fmtUsdc(req)) : "")) + ? ("1倍=盈利=权利金" + (req != null ? (" · 需回收≥" + fmtPremiumAmt(req, premCcy) + " " + premCcy) : "")) : "开启后自选倍数;达标按买一限价平;可随时关闭") + "" + "" @@ -1659,16 +1708,23 @@ return null; } - function formatTargetEstimateHtml(optType, strike, targetIdx, ethAmount, premiumPaid) { + function formatTargetEstimateHtml(optType, strike, targetIdx, ethAmount, premiumPaid, indexPx, p) { const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount); - const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid); - const rr = estimateProfitRr(profit, premiumPaid); + const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid, indexPx); + const rr = estimateProfitRr(profit, premiumPaid, indexPx); if (value == null && profit == null && rr == null) return ""; + const coinPos = isCoinPos(p); + const valueUnit = coinPos ? " U(估)" : " USDC"; + const profitTxt = profit == null + ? "—" + : (coinPos + ? ((Number(profit) > 0 ? "+" : "") + fmtUsdc(profit) + " U(估)") + : fmtUsdcSigned(profit)); let html = ''; html += '价值' + - (value == null ? "—" : fmtUsdc(value) + " USDC") + ""; + (value == null ? "—" : fmtUsdc(value) + valueUnit) + ""; html += '预估盈利' + - (profit == null ? "—" : fmtUsdcSigned(profit)) + ""; + profitTxt + ""; html += '盈亏比' + (rr == null ? "—" : fmtProfitRr(rr)) + ""; html += ""; @@ -1715,7 +1771,7 @@ const ethAmt = posEthAmount(p); const prem = p.premium_paid; const estHtml = armed - ? formatTargetEstimateHtml(p.opt_type, p.strike, tgt, ethAmt, prem) + ? formatTargetEstimateHtml(p.opt_type, p.strike, tgt, ethAmt, prem, p.idx_px, p) : ''; return ( '
' + '委托' + '到期 ' : "") + - '' + (net == null ? "—" : fmt(net, 2) + " USDC") + "" + + '' + + (net == null ? "—" : (fmtPremiumAmt(net, posPremiumCcy(p)) + " " + posPremiumCcy(p))) + "" + '' + (roi == null ? "—" : fmt(roi, 2) + "%") + "" + "" + @@ -2060,12 +2121,20 @@ return; } const lv = (preview.levels && preview.levels[0]) || {}; + const posLike = { + inst_id: inst, + premium_ccy: q.premium_ccy || (preview.close_gate && preview.close_gate.premium_ccy) || null, + margin_mode: q.options_margin_mode || q.margin_mode || null, + }; + const premCcy = posPremiumCcy(posLike); const msg = [ "按买一限价卖出本轮可平张数?", "合约: " + inst, "锁定买一: " + (lv.px != null ? lv.px : "—") + " × " + (lv.sheets != null ? lv.sheets : preview.covered_sheets) + " 张", - "预计收回: " + fmtClosePreviewText(preview), - preview.estimated_pnl != null ? "预估盈亏: " + fmt(preview.estimated_pnl, 4) + " USDC" : "", + "预计收回: " + fmtClosePreviewText(preview, posLike), + preview.estimated_pnl != null + ? ("预估盈亏: " + fmtPremiumAmtSigned(preview.estimated_pnl, premCcy)) + : "", preview.uncovered_sheets > 0 ? "\n注意: 买一深度不足,预计仍剩 " + preview.uncovered_sheets + " 张,需下次再平。" : "" ].filter(function (x) { return x !== ""; }).join("\n"); if (!confirm(msg)) return; @@ -2079,7 +2148,9 @@ if (r.ok) { let okMsg = "买一平仓已提交 " + (r.submitted_sheets || 0) + " 张"; if (r.locked_bid_px != null) okMsg += "\n锁定买一: " + r.locked_bid_px; - if (r.premium_received != null) okMsg += "\n预估收回: " + fmt(r.premium_received, 4) + " USDC"; + if (r.premium_received != null) { + okMsg += "\n预估收回: " + fmtPremiumAmt(r.premium_received, premCcy) + " " + premCcy; + } if (r.remaining_sheets > 0) okMsg += "\n剩余: " + r.remaining_sheets + " 张(下次再平)"; if (r.stopped_reason) okMsg += "\n状态: " + r.stopped_reason; alert(okMsg); diff --git a/lib/common/static/options_position_cards.js b/lib/common/static/options_position_cards.js index d2010d2..e1079f4 100644 --- a/lib/common/static/options_position_cards.js +++ b/lib/common/static/options_position_cards.js @@ -34,6 +34,28 @@ return Number(v).toFixed(2); } + function posPremiumCcy(p) { + const ccy = String((p && p.premium_ccy) || "").trim().toUpperCase(); + if (ccy) return ccy; + const mode = String((p && p.margin_mode) || "").toLowerCase(); + const inst = String((p && p.inst_id) || ""); + if (mode === "coin" || (inst.indexOf("-USD-") >= 0 && inst.indexOf("_UM") < 0)) { + return (inst.split("-")[0] || "ETH").toUpperCase() || "ETH"; + } + return "USDC"; + } + + function fmtPremiumAmt(v, ccy) { + if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + const unit = String(ccy || "USDC").toUpperCase(); + if (unit === "ETH" || unit === "BTC") { + let s = n.toFixed(8).replace(/\.?0+$/, ""); + return s || "0"; + } + return fmtUsdc(n); + } + function optTypeLabel(t) { return (t || "").toUpperCase() === "P" ? "看跌 Put" : "看涨 Call"; } @@ -136,9 +158,10 @@ return (net / prem) * 100; } - function fmtClosePreview(preview, premiumPaid, hub) { + function fmtClosePreview(preview, premiumPaid, hub, p) { if (!preview || preview.total_received == null) return "—"; - const recvTxt = fmtUsdc(preview.total_received); + const ccy = posPremiumCcy(p); + const recvTxt = fmtPremiumAmt(preview.total_received, ccy); let cls = ""; const prem = Number(premiumPaid); const recv = Number(preview.total_received); @@ -146,7 +169,7 @@ if (recv > prem) cls = " " + pnlCls(1, hub); else if (recv < prem) cls = " " + pnlCls(-1, hub); } - return '' + recvTxt + " USDC"; + return '' + recvTxt + " " + ccy + ""; } function expiryCdHtml(expMs) { @@ -168,7 +191,11 @@ const expAttr = expMs != null && expMs !== "" ? String(expMs) : ""; const closePreview = p.close_preview || {}; const tickSz = p.tick_sz; - const premTxt = fmtDisplay(p.premium_paid_fmt, p.premium_paid != null ? fmtUsdc(p.premium_paid) : null); + const premCcy = posPremiumCcy(p); + const premTxt = fmtDisplay( + p.premium_paid_fmt, + p.premium_paid != null ? fmtPremiumAmt(p.premium_paid, premCcy) : null + ); const avgTxt = p.avg_px != null ? fmtOptionPx(p.avg_px, tickSz) : fmtDisplay(p.avg_px_fmt); const markTxt = p.mark_px != null ? fmtOptionPx(p.mark_px, tickSz) : fmtDisplay(p.mark_px_fmt); let headActions = ""; @@ -182,7 +209,7 @@ const pnlCells = hidePnl ? "" : '
净盈亏' + - (net == null ? "—" : fmt(net, 2)) + "
" + + (net == null ? "—" : (fmtPremiumAmt(net, premCcy) + (premCcy !== "USDC" ? (" " + premCcy) : ""))) + "
" + '
收益率' + (roi == null ? "—" : fmt(roi, 2) + "%") + "
"; return ( @@ -202,7 +229,7 @@ : "") + "" + '
' + - '
权利金' + premTxt + " USDC
" + + '
权利金' + premTxt + " " + premCcy + "
" + '
开仓均价' + avgTxt + "
" + '
标记价' + markTxt + "
" + '
指数价' + fmt(p.idx_px, 0) + "
" + @@ -213,7 +240,7 @@ '
按买盘回收' + (closePreview.bid_invalid ? '暂无有效买盘' - : fmtClosePreview(closePreview, hidePnl ? null : p.premium_paid, hub)) + "
" + + : fmtClosePreview(closePreview, hidePnl ? null : p.premium_paid, hub, p)) + "
" + "" + (function () { const hint = closeGateHint(closePreview); @@ -226,6 +253,7 @@ const strike = Number(p.strike); const tgt = Number(p.target_index); const prem = Number(p.premium_paid); + const idx = Number(p.idx_px); let profit = null; let value = null; if (Number.isFinite(tgt) && Number.isFinite(strike) && eth > 0) { @@ -233,10 +261,17 @@ const intrinsic = o === "C" ? Math.max(0, tgt - strike) : o === "P" ? Math.max(0, strike - tgt) : null; if (intrinsic != null) { value = Math.round(intrinsic * eth * 100) / 100; - if (!hidePnl && Number.isFinite(prem)) profit = Math.round((value - prem) * 100) / 100; + if (!hidePnl && Number.isFinite(prem)) { + let premUsd = prem; + if (premCcy !== "USDC" && Number.isFinite(idx) && idx > 0) premUsd = prem * idx; + profit = Math.round((value - premUsd) * 100) / 100; + } } } - const profitTxt = profit == null ? "—" : ((profit > 0 ? "+" : "") + fmtUsdc(profit) + " USDC"); + const valueUnit = premCcy !== "USDC" ? " U(估)" : " USDC"; + const profitTxt = profit == null + ? "—" + : ((profit > 0 ? "+" : "") + fmtUsdc(profit) + (premCcy !== "USDC" ? " U(估)" : " USDC")); const profitCls = profit > 0 ? " pnl-pos" : profit < 0 ? " pnl-neg" : ""; const hedgeTarget = p.hedge_plan_target || null; const managed = hedgeTarget && hedgeTarget.managed_by === "hedge_plan"; @@ -247,7 +282,7 @@ '
' + '' + (managed ? "对冲计划 #" + hedgeTarget.plan_id : "委托") + "" + '目标 ' + fmt(p.target_index, 1) + "" + - '价值 ' + (value == null ? "—" : fmtUsdc(value) + " USDC") + "" + + '价值 ' + (value == null ? "—" : fmtUsdc(value) + valueUnit) + "" + profitSpan + '' + (managed ? "进行中 · 由对冲计划监控,到位后仅平盈利腿" : "监控中 · 到位按买一限价平") + diff --git a/lib/exchange/okx_options_lib.py b/lib/exchange/okx_options_lib.py index 3633521..acd878d 100644 --- a/lib/exchange/okx_options_lib.py +++ b/lib/exchange/okx_options_lib.py @@ -171,6 +171,21 @@ def format_usdc_amount(v: float | None) -> str | None: return f"{float(v):.2f}" +def format_premium_amount(v: float | None, *, ccy: str | None = "USDC") -> str | None: + """权利金/回收金额文案:USDC 2 位;币本位 ETH/BTC 最多 8 位去尾零.""" + if v is None: + return None + try: + n = float(v) + except (TypeError, ValueError): + return None + unit = (ccy or "USDC").strip().upper() or "USDC" + if unit in ("ETH", "BTC"): + txt = f"{n:.8f}".rstrip("0").rstrip(".") + return txt or "0" + return f"{n:.2f}" + + def is_option_full_close_history(raw: dict[str, Any]) -> bool: """仅保留 OKX 历史仓位中的「全部平仓/强平/ADL 全平」记录,排除部分平仓.""" close_type = str(raw.get("type") or "").strip() @@ -1797,6 +1812,16 @@ def format_position_row( ct_mult=ct_mult, ) exp_time_ms = normalize_option_exp_ms(pos.get("expTime"), inst_id) + try: + from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode + + row_mode = margin_mode_from_inst_id(inst_id) if inst_id else "usdc" + underly = (inst_id.split("-")[0] if inst_id else "ETH") or "ETH" + premium_ccy = premium_ccy_for_mode(row_mode, underly) + except Exception: + row_mode = "usdc" + underly = (inst_id.split("-")[0] if inst_id else "ETH") or "ETH" + premium_ccy = "USDC" return { "inst_id": inst_id or pos.get("instId"), "pos": sheets, @@ -1805,11 +1830,14 @@ def format_position_row( "mark_px": mark, "avg_px_fmt": format_option_px(avg, tick_sz) if avg is not None else None, "mark_px_fmt": format_option_px(mark, tick_sz) if mark is not None else None, - "premium_paid_fmt": format_usdc_amount(premium_paid), + "premium_paid_fmt": format_premium_amount(premium_paid, ccy=premium_ccy), "tick_sz": tick_sz, "ct_mult": ct_mult, "idx_px": idx_px, "premium_paid": premium_paid, + "margin_mode": row_mode, + "premium_ccy": premium_ccy, + "underlying": underly, "upl": upl, "upl_ratio_pct": round(upl_ratio * 100, 2) if upl_ratio is not None else None, "exp_time": exp_time_ms, diff --git a/lib/options/options_close_gate_lib.py b/lib/options/options_close_gate_lib.py index d263db2..743eebe 100644 --- a/lib/options/options_close_gate_lib.py +++ b/lib/options/options_close_gate_lib.py @@ -60,6 +60,14 @@ def is_close_gate_passed(inst_id: str) -> bool: return bool((_gates.get(inst) or {}).get("passed")) +def _fmt_gate_amt(v: float, *, ccy: str) -> str: + unit = (ccy or "USDC").strip().upper() or "USDC" + if unit in ("ETH", "BTC"): + txt = f"{float(v):.8f}".rstrip("0").rstrip(".") + return txt or "0" + return f"{float(v):.4f}" + + def update_close_gate( inst_id: str, *, @@ -68,6 +76,7 @@ def update_close_gate( now: float | None = None, min_mult: float | None = None, hold_seconds: float | None = None, + premium_ccy: str | None = None, ) -> dict[str, Any]: """ 根据当前买盘可回收金额刷新门控. @@ -89,9 +98,16 @@ def update_close_gate( if hold < 0: hold = 0.0 + with _lock: + prev_ccy = (_gates.get(inst) or {}).get("premium_ccy") + ccy = (premium_ccy or prev_ccy or "USDC").strip().upper() or "USDC" + if ccy not in ("ETH", "BTC", "USDC"): + ccy = "USDC" + need_decimals = 8 if ccy in ("ETH", "BTC") else 4 + prem = _safe_float(premium_paid) recv = _safe_float(recycle_usdc) - need = round(prem * mult, 4) if prem is not None and prem > 0 else None + need = round(prem * mult, need_decimals) if prem is not None and prem > 0 else None recycle_ok = bool( prem is not None and prem > 0 and recv is not None and need is not None and recv + 1e-12 >= need ) @@ -117,6 +133,7 @@ def update_close_gate( "min_mult": mult, "hold_seconds": hold, "passed": passed, + "premium_ccy": ccy, } _gates[inst] = state @@ -126,10 +143,13 @@ def update_close_gate( elif recv is None: msg = "暂无有效买盘可回收金额" elif not recycle_ok: - msg = f"可回收 {recv:.4f} USDC < 权利金×{mult:g}({need:.4f}),目标平仓门控未过" + msg = ( + f"可回收 {_fmt_gate_amt(recv, ccy=ccy)} {ccy} < 权利金×{mult:g}" + f"({_fmt_gate_amt(need, ccy=ccy)}),目标平仓门控未过" + ) elif not ready: msg = ( - f"可回收已达×{mult:g}({recv:.4f}/{need:.4f})," + f"可回收已达×{mult:g}({_fmt_gate_amt(recv, ccy=ccy)}/{_fmt_gate_amt(need, ccy=ccy)} {ccy})," f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过" ) else: @@ -144,6 +164,7 @@ def update_close_gate( "recycle_usdc": recv, "premium_paid": prem, "need_recycle_usdc": need, + "premium_ccy": ccy, "min_mult": mult, "hold_seconds": hold, "held_seconds": round(held, 1) if recycle_ok else 0.0, @@ -160,25 +181,39 @@ def check_close_gate( *, recycle_usdc: float | None = None, premium_paid: float | None = None, + premium_ccy: str | None = None, refresh: bool = True, ) -> dict[str, Any]: """检查是否允许平仓;默认先用最新回收/权利金刷新.""" inst = (inst_id or "").strip() if refresh: - if recycle_usdc is None or premium_paid is None: + if recycle_usdc is None or premium_paid is None or premium_ccy is None: with _lock: prev = _gates.get(inst) or {} if recycle_usdc is None: recycle_usdc = prev.get("recycle") if premium_paid is None: premium_paid = prev.get("premium") - return update_close_gate(inst, recycle_usdc=recycle_usdc, premium_paid=premium_paid) + if premium_ccy is None: + premium_ccy = prev.get("premium_ccy") + return update_close_gate( + inst, + recycle_usdc=recycle_usdc, + premium_paid=premium_paid, + premium_ccy=premium_ccy, + ) with _lock: prev = _gates.get(inst) if not prev: - return update_close_gate(inst, recycle_usdc=recycle_usdc, premium_paid=premium_paid) + return update_close_gate( + inst, + recycle_usdc=recycle_usdc, + premium_paid=premium_paid, + premium_ccy=premium_ccy, + ) return update_close_gate( inst, recycle_usdc=recycle_usdc if recycle_usdc is not None else prev.get("recycle"), premium_paid=premium_paid if premium_paid is not None else prev.get("premium"), + premium_ccy=premium_ccy if premium_ccy is not None else prev.get("premium_ccy"), ) diff --git a/lib/options/options_db.py b/lib/options/options_db.py index 377ed08..d7fef4f 100644 --- a/lib/options/options_db.py +++ b/lib/options/options_db.py @@ -134,7 +134,8 @@ def sum_open_premium_paid(conn: sqlite3.Connection, inst_id: str) -> float | Non ).fetchone() if not row or int(row["n"] or 0) < 1: return None - return round(float(row["total"] or 0), 4) + # 币本位权利金常 <1e-4,保留 8 位避免被裁成 0 + return round(float(row["total"] or 0), 8) def sum_open_sheets(conn: sqlite3.Connection, inst_id: str) -> int | None: diff --git a/lib/options/options_history_lib.py b/lib/options/options_history_lib.py index e54d868..0dec61a 100644 --- a/lib/options/options_history_lib.py +++ b/lib/options/options_history_lib.py @@ -14,14 +14,21 @@ def enrich_position_row_display( meta_cache: dict[str, dict[str, Any] | None] | None = None, premium_override: float | None = None, ) -> dict[str, Any]: - from lib.exchange.okx_options_lib import format_position_row, format_usdc_amount, tick_sz_and_ct_mult + from lib.exchange.okx_options_lib import format_position_row, format_premium_amount, tick_sz_and_ct_mult + from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode inst_id = str(raw_pos.get("instId") or "").strip() tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache) row = format_position_row(raw_pos, ct_mult=ct_mult, tick_sz=tick_sz) + row_mode = margin_mode_from_inst_id(inst_id) if inst_id else "usdc" + underly = str(row.get("underlying") or (inst_id.split("-")[0] if inst_id else "ETH") or "ETH") + premium_ccy = premium_ccy_for_mode(row_mode, underly) + row["margin_mode"] = row_mode + row["premium_ccy"] = premium_ccy + row["margin_mode_label"] = "币本位" if row_mode == "coin" else "USDC" if premium_override is not None: row["premium_paid"] = premium_override - row["premium_paid_fmt"] = format_usdc_amount(premium_override) + row["premium_paid_fmt"] = format_premium_amount(row.get("premium_paid"), ccy=premium_ccy) return row diff --git a/lib/options/options_positions_lib.py b/lib/options/options_positions_lib.py index a917f2c..c442411 100644 --- a/lib/options/options_positions_lib.py +++ b/lib/options/options_positions_lib.py @@ -51,9 +51,12 @@ def attach_close_preview( intrinsic_px=intrinsic, max_levels=1, ) + premium_ccy = str(row.get("premium_ccy") or "USDC").strip().upper() or "USDC" # 残档时不累计 2×门控;有效买一时刷新计时(仅自动平仓需要) if preview.get("bid_invalid") or preview.get("auto_close_blocked"): - gate = update_close_gate(inst_id, recycle_usdc=None, premium_paid=paid) + gate = update_close_gate( + inst_id, recycle_usdc=None, premium_paid=paid, premium_ccy=premium_ccy + ) preview["close_gate"] = gate preview["close_gate_blocked"] = True preview["close_gate_msg"] = preview.get("bid_invalid_reason") or gate.get("msg") @@ -64,6 +67,7 @@ def attach_close_preview( inst_id, recycle_usdc=_safe_float(preview.get("total_received")), premium_paid=paid, + premium_ccy=premium_ccy, ) passed = bool(gate.get("passed") or is_close_gate_passed(inst_id) or gate.get("ready")) preview["close_gate"] = gate diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 4c63a5f..1cc100e 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -533,7 +533,21 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: 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)) + from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode + + row_mode = margin_mode_from_inst_id(inst_id) + prem_ccy = premium_ccy_for_mode(row_mode, (inst_id.split("-")[0] if inst_id else "ETH")) + preview_row = { + **q, + "pos": target, + "premium_paid": paid, + "margin_mode": row_mode, + "premium_ccy": prem_ccy, + } + out = _attach_close_preview(cfg, ex, preview_row, sheets=target, premium_paid=paid) + out["options_margin_mode"] = row_mode + out["premium_ccy"] = prem_ccy + return jsonify(out) mode, mode_note = _normalize_size_mode(mode) # 币本位:报价预览走 USDT 预算→估币→张数,禁止再查 USDC