diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py
index e19e424..831b079 100644
--- a/crypto_monitor_okx/app.py
+++ b/crypto_monitor_okx/app.py
@@ -7076,9 +7076,10 @@ def api_account_snapshot():
options_trading_btc = None
options_margin_mode = "coin"
options_underly = "ETH"
+ options_index_px = None
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
try:
- from lib.exchange.okx_options_lib import options_header_balance_pack
+ from lib.exchange.okx_options_lib import fetch_index_price, options_header_balance_pack
_op = options_header_balance_pack(exchange_options, force=force_refresh)
options_trading_usdc = _op.get("trading_usdc")
@@ -7090,6 +7091,7 @@ def api_account_snapshot():
options_trading_btc = _op.get("trading_btc")
options_margin_mode = _op.get("options_margin_mode") or "coin"
options_underly = _op.get("options_underly") or "ETH"
+ options_index_px = fetch_index_price(exchange_options, options_underly)
except Exception:
options_trading_usdc = None
options_funding_usdc = None
@@ -7100,6 +7102,7 @@ def api_account_snapshot():
options_trading_btc = None
options_margin_mode = "coin"
options_underly = "ETH"
+ options_index_px = None
recommended_capital = get_recommended_capital(current_capital)
from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors
@@ -7187,6 +7190,7 @@ def api_account_snapshot():
"options_trading_btc": options_trading_btc,
"options_margin_mode": options_margin_mode,
"options_underly": options_underly,
+ "options_index_px": options_index_px,
"total_funds": total_funds_usdt(
funding_usdt if _show_perp_funds else None,
current_capital if _show_perp_funds else None,
@@ -7586,9 +7590,14 @@ def api_price_snapshot():
)
options_unrealized_pnl = None
+ options_index_px = None
+ options_margin_mode = None
+ options_underly = None
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
try:
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
+ from lib.options.options_margin_mode_lib import normalize_options_margin_mode
+ from lib.exchange.okx_options_lib import fetch_index_price
opt_cfg = app.extensions.get("options_cfg")
if opt_cfg:
@@ -7597,6 +7606,9 @@ def api_price_snapshot():
from lib.exchange.okx_options_lib import fetch_options_unrealized_pnl_usdc
options_unrealized_pnl = fetch_options_unrealized_pnl_usdc(exchange_options)
+ options_margin_mode = normalize_options_margin_mode()
+ options_underly = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() or "ETH"
+ options_index_px = fetch_index_price(exchange_options, options_underly)
except Exception:
options_unrealized_pnl = None
@@ -7607,6 +7619,9 @@ def api_price_snapshot():
"position_marks": position_marks,
"positions_raw_count": len(all_swap_positions),
"options_unrealized_pnl": options_unrealized_pnl,
+ "options_index_px": options_index_px,
+ "options_margin_mode": options_margin_mode,
+ "options_underly": options_underly,
**force_close_template_context(
FORCE_CLOSE_ENABLED,
FORCE_CLOSE_BJ_HOUR,
diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index 8802fbb..aebcc4f 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -897,6 +897,31 @@
return fmtUsdc(n);
}
+ /** 币本位盈亏双显:0.0018 ETH / 4.09U(按指数/现货价换算). */
+ function spotPxOf(p) {
+ const n = Number(p && (p.idx_px != null ? p.idx_px : p.idxPx != null ? p.idxPx : p.index_px));
+ return Number.isFinite(n) && n > 0 ? n : null;
+ }
+
+ function fmtCoinUsdtDual(coinAmt, spotPx, ccy, signed) {
+ if (coinAmt === null || coinAmt === undefined || Number.isNaN(Number(coinAmt))) return "—";
+ const n = Number(coinAmt);
+ const unit = String(ccy || "ETH").toUpperCase();
+ if (unit !== "ETH" && unit !== "BTC") {
+ const sign = signed && n > 0 ? "+" : "";
+ return sign + fmtUsdc(n) + "U";
+ }
+ const absCoin = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
+ const coinSign = n < 0 ? "-" : signed && n > 0 ? "+" : "";
+ const coinTxt = coinSign + absCoin + " " + unit;
+ const px = Number(spotPx);
+ if (!Number.isFinite(px) || !(px > 0)) return coinTxt;
+ const u = n * px;
+ const absU = Math.abs(u).toFixed(2);
+ const uSign = u < 0 ? "-" : signed && u > 0 ? "+" : "";
+ return coinTxt + " / " + uSign + absU + "U";
+ }
+
function fmtPremiumAmtSigned(v, ccy) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
@@ -904,6 +929,15 @@
return sign + fmtPremiumAmt(n, ccy) + " " + (String(ccy || "USDC").toUpperCase());
}
+ function fmtNetPnlDual(net, p) {
+ const ccy = posPremiumCcy(p);
+ if (ccy === "USDC") {
+ if (net == null || Number.isNaN(Number(net))) return "—";
+ return fmtUsdc(Number(net)) + "U";
+ }
+ return fmtCoinUsdtDual(net, spotPxOf(p), ccy, true);
+ }
+
function fmtClosePreview(preview, premiumPaid, p) {
if (!preview || preview.total_received == null) return "—";
const ccy = posPremiumCcy(p);
@@ -1614,7 +1648,7 @@
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) : ""));
+ : fmtNetPnlDual(net, p);
return (
'
' +
'
' + (p.inst_id || "") + '' +
@@ -1873,7 +1907,7 @@
? '
到期 —'
: "") +
'
' +
- (net == null ? "—" : (fmtPremiumAmt(net, posPremiumCcy(p)) + " " + posPremiumCcy(p))) + "" +
+ (net == null ? "—" : fmtNetPnlDual(net, p)) + "" +
'
' +
(roi == null ? "—" : fmt(roi, 2) + "%") + "" +
"" +
diff --git a/lib/common/static/options_position_cards.js b/lib/common/static/options_position_cards.js
index e1079f4..3452cfe 100644
--- a/lib/common/static/options_position_cards.js
+++ b/lib/common/static/options_position_cards.js
@@ -56,6 +56,39 @@
return fmtUsdc(n);
}
+ function spotPxOf(p) {
+ const n = Number(p && (p.idx_px != null ? p.idx_px : p.idxPx != null ? p.idxPx : p.index_px));
+ return Number.isFinite(n) && n > 0 ? n : null;
+ }
+
+ function fmtCoinUsdtDual(coinAmt, spotPx, ccy, signed) {
+ if (coinAmt === null || coinAmt === undefined || Number.isNaN(Number(coinAmt))) return "—";
+ const n = Number(coinAmt);
+ const unit = String(ccy || "ETH").toUpperCase();
+ if (unit !== "ETH" && unit !== "BTC") {
+ const sign = signed && n > 0 ? "+" : "";
+ return sign + fmtUsdc(n) + "U";
+ }
+ const absCoin = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
+ const coinSign = n < 0 ? "-" : signed && n > 0 ? "+" : "";
+ const coinTxt = coinSign + absCoin + " " + unit;
+ const px = Number(spotPx);
+ if (!Number.isFinite(px) || !(px > 0)) return coinTxt;
+ const u = n * px;
+ const absU = Math.abs(u).toFixed(2);
+ const uSign = u < 0 ? "-" : signed && u > 0 ? "+" : "";
+ return coinTxt + " / " + uSign + absU + "U";
+ }
+
+ function fmtNetPnlDual(net, p) {
+ const ccy = posPremiumCcy(p);
+ if (ccy === "USDC") {
+ if (net == null || Number.isNaN(Number(net))) return "—";
+ return fmtUsdc(Number(net)) + "U";
+ }
+ return fmtCoinUsdtDual(net, spotPxOf(p), ccy, true);
+ }
+
function optTypeLabel(t) {
return (t || "").toUpperCase() === "P" ? "看跌 Put" : "看涨 Call";
}
@@ -209,7 +242,7 @@
const pnlCells = hidePnl
? ""
: '
净盈亏' +
- (net == null ? "—" : (fmtPremiumAmt(net, premCcy) + (premCcy !== "USDC" ? (" " + premCcy) : ""))) + "
" +
+ (net == null ? "—" : fmtNetPnlDual(net, p)) + "
" +
'
收益率' +
(roi == null ? "—" : fmt(roi, 2) + "%") + "
";
return (
diff --git a/lib/instance/templates/embed_boot_scripts.html b/lib/instance/templates/embed_boot_scripts.html
index c8de900..fe1b112 100644
--- a/lib/instance/templates/embed_boot_scripts.html
+++ b/lib/instance/templates/embed_boot_scripts.html
@@ -1075,7 +1075,7 @@ function refreshOrderDefaults(){
}).catch(()=>{});
}
-function paintRealtimePnl(v, unit){
+function paintRealtimePnl(v, unit, spotPx){
const nodes = document.querySelectorAll('[data-funds-field="realtime-pnl"]');
if(!nodes.length) return;
if(v === null || v === undefined || Number.isNaN(Number(v))){
@@ -1088,11 +1088,23 @@ function paintRealtimePnl(v, unit){
const n = Number(v);
const u = String(unit || lastRealtimePnlUnit || "U").toUpperCase() || "U";
lastRealtimePnlUnit = u;
+ if (spotPx != null && Number.isFinite(Number(spotPx)) && Number(spotPx) > 0) {
+ lastRealtimePnlSpotPx = Number(spotPx);
+ }
const sign = n > 0 ? "+" : "";
let text;
if (u === "ETH" || u === "BTC") {
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
- text = `${n < 0 ? "-" : sign}${abs}${u}`;
+ const coinTxt = `${n < 0 ? "-" : sign}${abs} ${u}`;
+ const px = Number(spotPx != null ? spotPx : lastRealtimePnlSpotPx);
+ if (Number.isFinite(px) && px > 0) {
+ const uu = n * px;
+ const uAbs = Math.abs(uu).toFixed(2);
+ const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
+ text = `${coinTxt} / ${uSign}${uAbs}U`;
+ } else {
+ text = coinTxt;
+ }
} else {
text = `${sign}${n.toFixed(2)}U`;
}
@@ -1104,15 +1116,16 @@ function paintRealtimePnl(v, unit){
}
let lastRealtimePnl = null;
let lastRealtimePnlUnit = "U";
-function updateRealtimePnl(v, unit){
+let lastRealtimePnlSpotPx = null;
+function updateRealtimePnl(v, unit, spotPx){
if(v != null && !Number.isNaN(Number(v))){
lastRealtimePnl = Number(v);
if (unit) lastRealtimePnlUnit = String(unit).toUpperCase();
- paintRealtimePnl(v, lastRealtimePnlUnit);
+ paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
return;
}
if(lastRealtimePnl != null) return;
- paintRealtimePnl(v, lastRealtimePnlUnit);
+ paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
}
function sumOrdersFloatPnl(orders){
if(!orders || !orders.length) return null;
@@ -1143,14 +1156,23 @@ function paintRealtimePnlFromSnapshot(data){
const opt = data.options_unrealized_pnl;
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
const underly = String(data.options_underly || "ETH").toUpperCase() || "ETH";
+ const spotPx = data.options_index_px != null ? Number(data.options_index_px) : null;
if (coinMode && opt != null && !Number.isNaN(Number(opt))) {
if (perp != null && Math.abs(Number(perp)) >= 0.005) {
const optAbs = Math.abs(Number(opt)).toFixed(8).replace(/\.?0+$/, "") || "0";
const optSign = Number(opt) > 0 ? "+" : (Number(opt) < 0 ? "-" : "");
const perpSign = Number(perp) > 0 ? "+" : "";
- const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optSign}${optAbs}${underly}`;
+ let optPart = `${optSign}${optAbs} ${underly}`;
+ if (Number.isFinite(spotPx) && spotPx > 0) {
+ const uu = Number(opt) * spotPx;
+ const uAbs = Math.abs(uu).toFixed(2);
+ const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
+ optPart += ` / ${uSign}${uAbs}U`;
+ }
+ const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optPart}`;
lastRealtimePnl = Number(opt);
lastRealtimePnlUnit = underly;
+ lastRealtimePnlSpotPx = spotPx;
document.querySelectorAll('[data-funds-field="realtime-pnl"]').forEach((pnlEl) => {
pnlEl.innerText = text;
const n = Number(opt) + Number(perp);
@@ -1159,7 +1181,7 @@ function paintRealtimePnlFromSnapshot(data){
});
return;
}
- paintRealtimePnl(opt, underly);
+ paintRealtimePnl(opt, underly, spotPx);
return;
}
const combined = combineRealtimeFloatPnl(perp, opt);
diff --git a/lib/instance/templates/index.html b/lib/instance/templates/index.html
index f572fb3..8467aee 100644
--- a/lib/instance/templates/index.html
+++ b/lib/instance/templates/index.html
@@ -1556,7 +1556,7 @@ function refreshOrderDefaults(){
}).catch(()=>{});
}
-function paintRealtimePnl(v, unit){
+function paintRealtimePnl(v, unit, spotPx){
const nodes = document.querySelectorAll('[data-funds-field="realtime-pnl"]');
if(!nodes.length) return;
if(v === null || v === undefined || Number.isNaN(Number(v))){
@@ -1569,11 +1569,23 @@ function paintRealtimePnl(v, unit){
const n = Number(v);
const u = String(unit || lastRealtimePnlUnit || "U").toUpperCase() || "U";
lastRealtimePnlUnit = u;
+ if (spotPx != null && Number.isFinite(Number(spotPx)) && Number(spotPx) > 0) {
+ lastRealtimePnlSpotPx = Number(spotPx);
+ }
const sign = n > 0 ? "+" : "";
let text;
if (u === "ETH" || u === "BTC") {
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
- text = `${n < 0 ? "-" : sign}${abs}${u}`;
+ const coinTxt = `${n < 0 ? "-" : sign}${abs} ${u}`;
+ const px = Number(spotPx != null ? spotPx : lastRealtimePnlSpotPx);
+ if (Number.isFinite(px) && px > 0) {
+ const uu = n * px;
+ const uAbs = Math.abs(uu).toFixed(2);
+ const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
+ text = `${coinTxt} / ${uSign}${uAbs}U`;
+ } else {
+ text = coinTxt;
+ }
} else {
text = `${sign}${n.toFixed(2)}U`;
}
@@ -1585,15 +1597,16 @@ function paintRealtimePnl(v, unit){
}
let lastRealtimePnl = null;
let lastRealtimePnlUnit = "U";
-function updateRealtimePnl(v, unit){
+let lastRealtimePnlSpotPx = null;
+function updateRealtimePnl(v, unit, spotPx){
if(v != null && !Number.isNaN(Number(v))){
lastRealtimePnl = Number(v);
if (unit) lastRealtimePnlUnit = String(unit).toUpperCase();
- paintRealtimePnl(v, lastRealtimePnlUnit);
+ paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
return;
}
if(lastRealtimePnl != null) return;
- paintRealtimePnl(v, lastRealtimePnlUnit);
+ paintRealtimePnl(v, lastRealtimePnlUnit, spotPx);
}
function sumOrdersFloatPnl(orders){
if(!orders || !orders.length) return null;
@@ -1624,15 +1637,24 @@ function paintRealtimePnlFromSnapshot(data){
const opt = data.options_unrealized_pnl;
const coinMode = String(data.options_margin_mode || "").toLowerCase() === "coin";
const underly = String(data.options_underly || "ETH").toUpperCase() || "ETH";
+ const spotPx = data.options_index_px != null ? Number(data.options_index_px) : null;
if (coinMode && opt != null && !Number.isNaN(Number(opt))) {
// 币本位期权盈亏单位为币,勿与永续 U 混加成「xxU」
if (perp != null && Math.abs(Number(perp)) >= 0.005) {
const optAbs = Math.abs(Number(opt)).toFixed(8).replace(/\.?0+$/, "") || "0";
const optSign = Number(opt) > 0 ? "+" : (Number(opt) < 0 ? "-" : "");
const perpSign = Number(perp) > 0 ? "+" : "";
- const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optSign}${optAbs}${underly}`;
+ let optPart = `${optSign}${optAbs} ${underly}`;
+ if (Number.isFinite(spotPx) && spotPx > 0) {
+ const uu = Number(opt) * spotPx;
+ const uAbs = Math.abs(uu).toFixed(2);
+ const uSign = uu < 0 ? "-" : uu > 0 ? "+" : "";
+ optPart += ` / ${uSign}${uAbs}U`;
+ }
+ const text = `${perpSign}${Number(perp).toFixed(2)}U / ${optPart}`;
lastRealtimePnl = Number(opt);
lastRealtimePnlUnit = underly;
+ lastRealtimePnlSpotPx = spotPx;
document.querySelectorAll('[data-funds-field="realtime-pnl"]').forEach((pnlEl) => {
pnlEl.innerText = text;
const n = Number(opt) + Number(perp);
@@ -1641,7 +1663,7 @@ function paintRealtimePnlFromSnapshot(data){
});
return;
}
- paintRealtimePnl(opt, underly);
+ paintRealtimePnl(opt, underly, spotPx);
return;
}
const combined = combineRealtimeFloatPnl(perp, opt);
diff --git a/lib/options/options_hub_lib.py b/lib/options/options_hub_lib.py
index 4e0cdd8..e3b151f 100644
--- a/lib/options/options_hub_lib.py
+++ b/lib/options/options_hub_lib.py
@@ -115,6 +115,24 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
)
p["margin_mode_label"] = "币本位" if row_mode == "coin" else "USDC"
+ underly = (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() or "ETH"
+ options_index_px = None
+ try:
+ from lib.exchange.okx_options_lib import fetch_index_price
+
+ options_index_px = fetch_index_price(ex, underly)
+ except Exception:
+ options_index_px = None
+ if options_index_px is None:
+ for p in positions:
+ try:
+ px = float(p.get("idx_px") or p.get("idxPx") or 0)
+ except (TypeError, ValueError):
+ px = 0
+ if px > 0:
+ options_index_px = px
+ break
+
coin_budget = None
bridge_status = None
open_bridges = []
@@ -157,7 +175,8 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
"max_active_positions": options_max_active_positions(),
"options_margin_mode": margin_mode,
"options_margin_mode_label": "币本位" if margin_mode == "coin" else "USDC",
- "options_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper() or "ETH",
+ "options_underly": underly,
+ "options_index_px": options_index_px,
"coin_budget": coin_budget,
"bridge_status": bridge_status,
"open_bridges": open_bridges,
diff --git a/lib/options/options_spot_bridge_lib.py b/lib/options/options_spot_bridge_lib.py
index 886947b..9f1e675 100644
--- a/lib/options/options_spot_bridge_lib.py
+++ b/lib/options/options_spot_bridge_lib.py
@@ -221,26 +221,32 @@ def spot_market_sell_coin_to_usdt(
underlying: str,
coin_amount: float | None = None,
) -> dict[str, Any]:
- """交易账户:市价卖出标的币换 USDT.coin_amount 空则尽量卖光可用."""
+ """交易账户:市价卖出标的币换 USDT.
+
+ 默认/推荐:coin_amount 为空 → 卖光交易账户全部可用币(全部卖出).
+ 传入 coin_amount 时仍不超过可用余额,且尽量按可用全额卖出(不故意留粉尘).
+ """
ccy = (underlying or "ETH").upper()
- amt = coin_amount
- if amt is None or float(amt) <= 0:
- avail = fetch_trading_coin_available(ex, ccy)
- if avail is None or float(avail) <= 0:
- return {"ok": False, "msg": f"交易账户无可用 {ccy}"}
- amt = float(avail)
- if float(amt) <= 0:
+ avail = fetch_trading_coin_available(ex, ccy)
+ if avail is None or float(avail) <= 0:
+ return {"ok": False, "msg": f"交易账户无可用 {ccy}"}
+ avail_f = float(avail)
+ # 全部卖出:以可用余额为准;若传入数量则不超过可用(开仓失败回滚用)
+ if coin_amount is None or float(coin_amount) <= 0:
+ sell_sz = avail_f
+ else:
+ sell_sz = min(float(coin_amount), avail_f)
+ if sell_sz <= 0:
return {"ok": False, "msg": f"{ccy} 数量须大于 0"}
- # 留一点粉尘避免精度拒单
- sell_sz = float(amt)
- if sell_sz > 1e-8:
- sell_sz = max(0.0, sell_sz * 0.999)
inst_id = spot_quote_inst_id(ccy)
try:
- # 现货卖出数量精度:截到 8 位
+ # 现货卖出:向下截到 8 位,避免超过可用被拒;不再 *0.999 故意留残
sz = f"{sell_sz:.8f}".rstrip("0").rstrip(".")
if not sz or float(sz) <= 0:
return {"ok": False, "msg": f"{ccy} 可卖数量过小"}
+ # 二次钳制:格式化后仍不得超过可用
+ if float(sz) > avail_f:
+ sz = f"{avail_f:.8f}".rstrip("0").rstrip(".")
body = {
"instId": inst_id,
"tdMode": "cash",
@@ -328,13 +334,8 @@ def sell_residual_after_option_flat(
if not underlying or str(b.get("underlying") or "").upper() == underlying.upper():
target = b
break
- coin_amt = None
- if target is not None:
- try:
- coin_amt = float(target.get("coin_bought") or 0) or None
- except (TypeError, ValueError):
- coin_amt = None
- sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying, coin_amount=coin_amt)
+ # 平仓后全部卖出交易账户可用标的币(含权利金盈亏留下的币),不按 bridge 记账量限卖
+ sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying, coin_amount=None)
if target is None:
if not sell.get("ok"):
msg = str(sell.get("msg") or "")
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index 3ba78cb..af90416 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -782,13 +782,37 @@
return fmt(n, 2);
}
- function fmtOptPnlText(v, ccy) {
+ function spotPxFromOptMeta(optMeta, p) {
+ if (p) {
+ const n = Number(p.idx_px != null ? p.idx_px : p.idxPx != null ? p.idxPx : p.index_px);
+ if (Number.isFinite(n) && n > 0) return n;
+ }
+ if (optMeta) {
+ const n = Number(optMeta.options_index_px != null ? optMeta.options_index_px : optMeta.index_px);
+ if (Number.isFinite(n) && n > 0) return n;
+ const pos = Array.isArray(optMeta.positions) ? optMeta.positions : [];
+ for (let i = 0; i < pos.length; i++) {
+ const px = Number(pos[i] && (pos[i].idx_px != null ? pos[i].idx_px : pos[i].idxPx));
+ if (Number.isFinite(px) && px > 0) return px;
+ }
+ }
+ return null;
+ }
+
+ function fmtOptPnlText(v, ccy, spotPx) {
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
const n = Number(v);
const unit = String(ccy || "USDC").toUpperCase();
const sign = n > 0 ? "+" : "";
if (unit === "ETH" || unit === "BTC") {
- return `${sign}${fmtOptPnlAmt(n, unit)} ${unit}`;
+ const coin = `${sign}${fmtOptPnlAmt(Math.abs(n), unit)}`;
+ const signedCoin = n < 0 ? `-${fmtOptPnlAmt(Math.abs(n), unit)}` : coin;
+ const px = Number(spotPx);
+ if (!Number.isFinite(px) || !(px > 0)) return `${signedCoin} ${unit}`;
+ const u = n * px;
+ const uAbs = Math.abs(u).toFixed(2);
+ const uTxt = u < 0 ? `-${uAbs}` : u > 0 ? `+${uAbs}` : uAbs;
+ return `${signedCoin} ${unit} / ${uTxt}U`;
}
return `${sign}${fmt(n, 2)}U`;
}
@@ -3948,9 +3972,10 @@
: "—";
const tradeTxt = formatCoinTradingLabel(usdt, eth, btc);
const pnlCcy = optionsPanelCcy(optMeta);
+ const spotPx = spotPxFromOptMeta(optMeta);
const pnlTxt = upnl == null || Number.isNaN(Number(upnl))
? "—"
- : `
${fmtOptPnlText(upnl, pnlCcy)}`;
+ : `
${fmtOptPnlText(upnl, pnlCcy, spotPx)}`;
return `
@@ -4116,7 +4141,7 @@
${renderOptionsTargetCell(target, p)}`;
if (showPnl) {
const premCcy = optPremiumCcyOf(p);
- html += `
${net == null ? "—" : fmtOptPnlText(net, premCcy)} |
+ html += `
${net == null ? "—" : fmtOptPnlText(net, premCcy, spotPxFromOptMeta(null, p))} |
${roi == null ? "—" : esc(Number(roi).toFixed(2)) + "%"} | `;
}
html += "";
@@ -4602,6 +4627,7 @@
let pnlShow = upnl;
let pnlSuffix = "";
let pnlUnit = "U";
+ let pnlSpotPx = null;
if (hasOptCap) {
if (opt.enabled === false) {
optLine = "期权未启用";
@@ -4629,13 +4655,14 @@
parts.push(`交易 ${fmt(bal.trading, 2)}U`);
}
if (optUpl != null && Number.isFinite(Number(optUpl))) {
- parts.push(`浮盈 ${fmtOptPnlText(optUpl, optCcy)}`);
+ parts.push(`浮盈 ${fmtOptPnlText(optUpl, optCcy, spotPxFromOptMeta(opt))}`);
}
if (optUpl != null && Number.isFinite(Number(optUpl)) && openCount === 0) {
// 永续空仓时主数字优先展示期权浮盈,避免一直显示 0U
pnlShow = optUpl;
pnlSuffix = "期权";
pnlUnit = optCcy;
+ pnlSpotPx = spotPxFromOptMeta(opt);
}
}
optLine = parts.join(" · ");
@@ -4646,7 +4673,7 @@
const strategyStats = renderCardStrategyStats(row, hm, flaskOk);
const tilePnlHtml =
pnlUnit === "ETH" || pnlUnit === "BTC"
- ? `${fmtOptPnlAmt(pnlShow, pnlUnit)}
${esc(pnlUnit)}${pnlSuffix ? " · " + esc(pnlSuffix) : ""}`
+ ? `${fmtOptPnlText(pnlShow, pnlUnit, pnlSpotPx)}
${pnlSuffix ? esc(pnlSuffix) : ""}`
: `${fmt(pnlShow, 2)}
U${pnlSuffix ? " · " + esc(pnlSuffix) : ""}`;
return `
diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js
index ce3757e..344cd16 100644
--- a/manual_trading_hub/static/dashboard.js
+++ b/manual_trading_hub/static/dashboard.js
@@ -51,14 +51,20 @@
return "USDC";
}
- function pnlSignedOpt(v, ccy) {
+ function pnlSignedOpt(v, ccy, spotPx) {
const n = Number(v);
if (!Number.isFinite(n)) return "—";
const unit = String(ccy || "USDC").toUpperCase();
if (unit === "ETH" || unit === "BTC") {
const abs = Math.abs(n).toFixed(8).replace(/\.?0+$/, "") || "0";
const sign = n > 0 ? "+" : n < 0 ? "-" : "";
- return `${sign}${abs} ${unit}`;
+ const coinTxt = `${sign}${abs} ${unit}`;
+ const px = Number(spotPx);
+ if (!Number.isFinite(px) || !(px > 0)) return coinTxt;
+ const u = n * px;
+ const uAbs = Math.abs(u).toFixed(2);
+ const uSign = u < 0 ? "-" : u > 0 ? "+" : "";
+ return `${coinTxt} / ${uSign}${uAbs}U`;
}
return pnlSigned(n, 2);
}
@@ -361,7 +367,15 @@
${p.idx_px != null ? fmt(p.idx_px, 0) : "—"} |
${esc(target)} | `;
if (showPnl) {
- html += `${net != null ? pnlSignedOpt(net, optPremiumCcyOf(p)) : "—"} |
+ html += `${
+ net != null
+ ? pnlSignedOpt(
+ net,
+ optPremiumCcyOf(p),
+ Number(p.idx_px != null ? p.idx_px : p.idxPx) || null
+ )
+ : "—"
+ } |
${roi != null ? esc(Number(roi).toFixed(2)) + "%" : "—"} | `;
}
html += "";