diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 6f3c54f..87d0060 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -1846,32 +1846,24 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: except Exception: index_px = None stats = compute_options_stats_from_history(history, index_px=index_px) + # sum_options_net_pnl_usdc 已按指数折 U;勿再 × 指数(否则顶栏会变成上万假亏) open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live) - # 币本位浮盈为币数量,折算为 U 再与已平合计 - if open_float is not None and str(stats.get("pnl_unit") or "") == "U": - px = index_px - if px is None or px <= 0: - for h in history: - try: - px = float(h.get("idx_px") or 0) - except (TypeError, ValueError): - px = 0 - if px > 0: - break - if px and px > 0: - open_float = round(float(open_float) * float(px), 4) net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0 total_pnl = None if open_float is not None: total_pnl = round(net_realized + float(open_float), 4) elif stats.get("total_closed"): total_pnl = round(net_realized, 4) + from lib.options.options_margin_mode_lib import is_coin_margin_mode + + pnl_unit = "U" if is_coin_margin_mode() else str(stats.get("pnl_unit") or "USDC") return jsonify( { "ok": True, **stats, "open_float_pnl": open_float, "total_pnl": total_pnl, + "pnl_unit": pnl_unit, } ) diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 1f1420d..8a984d8 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -817,7 +817,19 @@ return `${sign}${fmt(n, 2)}U`; } - /** 中控期权浮盈汇总框:只显示换算后的 U(持仓表净盈亏仍用 fmtOptPnlText 双显). */ + /** 已是 USDT 的期权浮盈合计(upl_total_usdc),禁止再按币×指数. */ + function fmtOptFloatUsdt(v) { + if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + const uAbs = Math.abs(n).toFixed(2); + const uSign = n < 0 ? "-" : n > 0 ? "+" : ""; + return `${uSign}${uAbs}U`; + } + + /** + * 币数量 → U(仅逐仓净盈亏双显/兜底合计用). + * 汇总框勿用:后端 upl_total 已折 U,再乘会变成上万假亏. + */ function fmtOptPnlUsdtOnly(v, ccy, spotPx) { if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; const n = Number(v); @@ -3939,6 +3951,7 @@ const bal = opt.balances && typeof opt.balances === "object" ? opt.balances : {}; const pick = (a, b) => (a != null && a !== "" ? a : b); + // upl_total_usdc 实际为已折 USDT 的合计(币本位 × 指数后) let upl = opt.upl_total_usdc != null && Number.isFinite(Number(opt.upl_total_usdc)) ? Number(opt.upl_total_usdc) @@ -3949,10 +3962,16 @@ let found = false; pos.forEach((p) => { const n = optionsRowDisplayPnl(p); - if (n != null) { - sum += n; - found = true; + if (n == null || !Number.isFinite(Number(n))) return; + const ccy = String(optPremiumCcyOf(p) || "").toUpperCase(); + let uu = Number(n); + if (ccy === "ETH" || ccy === "BTC") { + const px = spotPxFromOptMeta(opt, p); + if (!Number.isFinite(px) || !(px > 0)) return; + uu = Number(n) * px; } + sum += uu; + found = true; }); if (found) upl = sum; } @@ -3987,11 +4006,10 @@ ? `${fmt(fundUsdt, 2)} U` : "—"; const tradeTxt = formatCoinTradingLabel(usdt, eth, btc); - const pnlCcy = optionsPanelCcy(optMeta); - const spotPx = spotPxFromOptMeta(optMeta); + // 汇总 upnl 已是 USDT,勿再按 ETH/BTC×指数(否则 -6U 会变成 -1.7万U) const pnlTxt = upnl == null || Number.isNaN(Number(upnl)) ? "—" - : `${fmtOptPnlUsdtOnly(upnl, pnlCcy, spotPx)}`; + : `${fmtOptFloatUsdt(upnl)}`; return `
资金账户
${fundTxt}
交易账户
${tradeTxt}
@@ -4671,14 +4689,14 @@ parts.push(`交易 ${fmt(bal.trading, 2)}U`); } if (optUpl != null && Number.isFinite(Number(optUpl))) { - parts.push(`浮盈 ${fmtOptPnlUsdtOnly(optUpl, optCcy, spotPxFromOptMeta(opt))}`); + parts.push(`浮盈 ${fmtOptFloatUsdt(optUpl)}`); } if (optUpl != null && Number.isFinite(Number(optUpl)) && openCount === 0) { - // 永续空仓时主数字优先展示期权浮盈(只显示 U) + // 永续空仓时主数字优先展示期权浮盈(已是 U,勿再按币折算) pnlShow = optUpl; pnlSuffix = "期权"; - pnlUnit = optCcy; - pnlSpotPx = spotPxFromOptMeta(opt); + pnlUnit = "USDC"; + pnlSpotPx = null; } } optLine = parts.join(" · ");