币本位盈亏双显ETH/U(按指数换算);平仓卖币改为卖光交易户可用余额

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-20 17:45:32 +08:00
parent d4d2110412
commit f5c553844f
9 changed files with 235 additions and 48 deletions
+16 -1
View File
@@ -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,
+36 -2
View File
@@ -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 (
'<div class="pos-card-head">' +
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + '</strong>' +
@@ -1873,7 +1907,7 @@
? '<span class="opt-pos-bar-cd">到期 <span class="opt-expiry-cd" data-opt-exp-ms="' + expAttr + '">—</span></span>'
: "") +
'<span class="opt-pos-bar-pnl ' + uplCls + '">' +
(net == null ? "—" : (fmtPremiumAmt(net, posPremiumCcy(p)) + " " + posPremiumCcy(p))) + "</span>" +
(net == null ? "—" : fmtNetPnlDual(net, p)) + "</span>" +
'<span class="opt-pos-bar-roi ' + uplCls + '">' +
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span>" +
"</span>" +
+34 -1
View File
@@ -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
? ""
: '<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
(net == null ? "—" : (fmtPremiumAmt(net, premCcy) + (premCcy !== "USDC" ? (" " + premCcy) : ""))) + "</span></div>" +
(net == null ? "—" : fmtNetPnlDual(net, p)) + "</span></div>" +
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>";
return (
+29 -7
View File
@@ -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);
+29 -7
View File
@@ -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);
+20 -1
View File
@@ -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,
+21 -20
View File
@@ -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 "")
+33 -6
View File
@@ -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))
? "—"
: `<span class="${pnlCls(upnl)}">${fmtOptPnlText(upnl, pnlCcy)}</span>`;
: `<span class="${pnlCls(upnl)}">${fmtOptPnlText(upnl, pnlCcy, spotPx)}</span>`;
return `<div class="${rowCls}">
<div class="stat-box"><div class="stat-label">资金账户</div><div class="stat-value">${fundTxt}</div></div>
<div class="stat-box"><div class="stat-label">交易账户</div><div class="stat-value">${tradeTxt}</div></div>
@@ -4116,7 +4141,7 @@
${renderOptionsTargetCell(target, p)}`;
if (showPnl) {
const premCcy = optPremiumCcyOf(p);
html += `<td class="${pnlCls(net)}">${net == null ? "—" : fmtOptPnlText(net, premCcy)}</td>
html += `<td class="${pnlCls(net)}">${net == null ? "—" : fmtOptPnlText(net, premCcy, spotPxFromOptMeta(null, p))}</td>
<td class="${pnlCls(net)}">${roi == null ? "—" : esc(Number(roi).toFixed(2)) + "%"}</td>`;
}
html += "</tr>";
@@ -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)} <small>${esc(pnlUnit)}${pnlSuffix ? " · " + esc(pnlSuffix) : ""}</small>`
? `${fmtOptPnlText(pnlShow, pnlUnit, pnlSpotPx)} <small>${pnlSuffix ? esc(pnlSuffix) : ""}</small>`
: `${fmt(pnlShow, 2)} <small>U${pnlSuffix ? " · " + esc(pnlSuffix) : ""}</small>`;
return `<div class="card hub-tile ${tileCls}" data-ex-id="${esc(row.id)}">
<div class="hub-tile-body card-expand-zone" title="点击进入全屏详情">
+17 -3
View File
@@ -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 @@
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
<td><span class="${targetCls}">${esc(target)}</span></td>`;
if (showPnl) {
html += `<td class="${pnlClass(net)}">${net != null ? pnlSignedOpt(net, optPremiumCcyOf(p)) : "—"}</td>
html += `<td class="${pnlClass(net)}">${
net != null
? pnlSignedOpt(
net,
optPremiumCcyOf(p),
Number(p.idx_px != null ? p.idx_px : p.idxPx) || null
)
: "—"
}</td>
<td class="${pnlClass(roi)}">${roi != null ? esc(Number(roi).toFixed(2)) + "%" : "—"}</td>`;
}
html += "</tr>";