diff --git a/lib/hub/hub_options_funds_lib.py b/lib/hub/hub_options_funds_lib.py index e466747..6c030a3 100644 --- a/lib/hub/hub_options_funds_lib.py +++ b/lib/hub/hub_options_funds_lib.py @@ -74,21 +74,50 @@ def options_balances_usdt_equiv(options_snap: dict[str, Any] | None) -> dict[str def options_float_pnl_usdt(options_snap: dict[str, Any] | None) -> Optional[float]: + """期权浮盈合计(USDT).币本位按指数换算,勿把 ETH/BTC 数量当 U.""" snap = options_snap if isinstance(options_snap, dict) else {} if snap.get("enabled") is False or snap.get("ok") is False: return None - upl = snap.get("upl_total_usdc") - if upl is not None: + + def _safe(v: Any) -> float | None: + if v is None or v == "": + return None try: - return round(float(upl), 4) + return float(v) except (TypeError, ValueError): - pass - # 快照偶发缺合计时,按持仓行回退汇总(与卡片展示一致) + return None + + def _index_px() -> float | None: + px = _safe(snap.get("options_index_px") or snap.get("index_px")) + if px is not None and px > 0: + return px + for p in snap.get("positions") or []: + if not isinstance(p, dict): + continue + px = _safe(p.get("idx_px") or p.get("idxPx")) + if px is not None and px > 0: + return px + return None + + def _row_is_coin(p: dict[str, Any]) -> bool: + ccy = str(p.get("premium_ccy") or "").strip().upper() + if ccy in ("ETH", "BTC"): + return True + if str(p.get("margin_mode") or "").strip().lower() == "coin": + return True + mid = str(p.get("inst_id") or "") + return "-USD-" in mid.upper() and "_UM" not in mid.upper() + + mode = str(snap.get("options_margin_mode") or snap.get("margin_mode") or "").strip().lower() + snap_coin = mode == "coin" + idx = _index_px() + try: from lib.options.options_positions_lib import display_pnl_from_option_row - total = 0.0 + total_u = 0.0 found = False + missing_fx = False for p in snap.get("positions") or []: if not isinstance(p, dict): continue @@ -96,10 +125,38 @@ def options_float_pnl_usdt(options_snap: dict[str, Any] | None) -> Optional[floa if pnl is None: continue found = True - total += float(pnl) - return round(total, 4) if found else None + if snap_coin or _row_is_coin(p): + px = _safe(p.get("idx_px") or p.get("idxPx")) + if px is None or px <= 0: + px = idx + if px is None or px <= 0: + missing_fx = True + continue + total_u += float(pnl) * float(px) + else: + total_u += float(pnl) + if found and not missing_fx: + return round(total_u, 4) + if found and missing_fx and abs(total_u) > 1e-12: + # 部分腿已换算成功时仍返回可得合计 + return round(total_u, 4) except Exception: - return None + pass + + upl = snap.get("upl_total_usdc") + if upl is not None: + try: + raw = float(upl) + except (TypeError, ValueError): + return None + if snap_coin or any( + isinstance(p, dict) and _row_is_coin(p) for p in (snap.get("positions") or []) + ): + if idx is None or idx <= 0: + return None + return round(raw * float(idx), 4) + return round(raw, 4) + return None def options_open_position_count(options_snap: dict[str, Any] | None) -> int: diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index af90416..1f1420d 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -817,6 +817,22 @@ return `${sign}${fmt(n, 2)}U`; } + /** 中控期权浮盈汇总框:只显示换算后的 U(持仓表净盈亏仍用 fmtOptPnlText 双显). */ + function fmtOptPnlUsdtOnly(v, ccy, spotPx) { + if (v === null || v === undefined || Number.isNaN(Number(v))) return "—"; + const n = Number(v); + const unit = String(ccy || "USDC").toUpperCase(); + let uu = n; + if (unit === "ETH" || unit === "BTC") { + const px = Number(spotPx); + if (!Number.isFinite(px) || !(px > 0)) return "—"; + uu = n * px; + } + const uAbs = Math.abs(uu).toFixed(2); + const uSign = uu < 0 ? "-" : uu > 0 ? "+" : ""; + return `${uSign}${uAbs}U`; + } + /** 交易所持仓开仓价(三所子代理 entry_price) */ function positionEntryPrice(pos) { if (!pos) return null; @@ -3975,7 +3991,7 @@ const spotPx = spotPxFromOptMeta(optMeta); const pnlTxt = upnl == null || Number.isNaN(Number(upnl)) ? "—" - : `${fmtOptPnlText(upnl, pnlCcy, spotPx)}`; + : `${fmtOptPnlUsdtOnly(upnl, pnlCcy, spotPx)}`; return `