中控期权浮盈只显示U;总浮盈亏按指数把币本位浮盈计入USDT

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-20 18:13:39 +08:00
parent 467f4f092f
commit 1ddfe3f72e
4 changed files with 136 additions and 14 deletions
+66 -9
View File
@@ -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:
+20 -4
View File
@@ -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))
? "—"
: `<span class="${pnlCls(upnl)}">${fmtOptPnlText(upnl, pnlCcy, spotPx)}</span>`;
: `<span class="${pnlCls(upnl)}">${fmtOptPnlUsdtOnly(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>
@@ -4655,10 +4671,10 @@
parts.push(`交易 ${fmt(bal.trading, 2)}U`);
}
if (optUpl != null && Number.isFinite(Number(optUpl))) {
parts.push(`浮盈 ${fmtOptPnlText(optUpl, optCcy, spotPxFromOptMeta(opt))}`);
parts.push(`浮盈 ${fmtOptPnlUsdtOnly(optUpl, optCcy, spotPxFromOptMeta(opt))}`);
}
if (optUpl != null && Number.isFinite(Number(optUpl)) && openCount === 0) {
// 永续空仓时主数字优先展示期权浮盈,避免一直显示 0U
// 永续空仓时主数字优先展示期权浮盈(只显示 U)
pnlShow = optUpl;
pnlSuffix = "期权";
pnlUnit = optCcy;
@@ -4673,7 +4689,7 @@
const strategyStats = renderCardStrategyStats(row, hm, flaskOk);
const tilePnlHtml =
pnlUnit === "ETH" || pnlUnit === "BTC"
? `${fmtOptPnlText(pnlShow, pnlUnit, pnlSpotPx)} <small>${pnlSuffix ? esc(pnlSuffix) : ""}</small>`
? `${fmtOptPnlUsdtOnly(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="点击进入全屏详情">
+28 -1
View File
@@ -66,6 +66,33 @@ class TestHubMonitorTotals(unittest.TestCase):
self.assertEqual(out["options_float_pnl_u"], 1.5)
self.assertEqual(out["float_pnl_u"], 1.5)
def test_aggregate_monitor_board_totals_coin_options_to_usdt(self):
rows = [
{
"capabilities": ["options"],
"options": {
"ok": True,
"enabled": True,
"options_margin_mode": "coin",
"options_index_px": 2000.0,
"positions": [
{
"inst_id": "ETH-USD-260822-2250-C",
"margin_mode": "coin",
"premium_ccy": "ETH",
"upl": 0.002,
"idx_px": 2000.0,
}
],
"upl_total_usdc": 0.002,
},
"agent": {"positions": [], "total_unrealized_pnl": 10.0},
}
]
out = aggregate_monitor_board_totals(rows, trading_day="2026-08-20", reset_hour=8)
self.assertEqual(out["options_float_pnl_u"], 4.0)
self.assertEqual(out["float_pnl_u"], 14.0)
def test_aggregate_excludes_option_like_agent_positions(self):
"""子代理误把期权当永续上报时:不算进持仓数,浮盈只用期权 snap."""
rows = [
@@ -74,7 +101,7 @@ class TestHubMonitorTotals(unittest.TestCase):
"options": {
"ok": True,
"enabled": True,
"positions": [{"inst_id": "ETH-USD-260806-1875-C"}],
"positions": [{"inst_id": "ETH-USD_UM-260806-1875-C"}],
"upl_total_usdc": -0.4,
},
"agent": {
+22
View File
@@ -63,6 +63,28 @@ class HubOptionsFundsLibTests(TestCase):
self.assertEqual(out["options_open_position_count"], 1)
self.assertEqual(out["options_float_pnl_u"], 0.5)
def test_options_float_pnl_usdt_coin_converts_by_index(self):
from lib.hub.hub_options_funds_lib import options_float_pnl_usdt
snap = {
"ok": True,
"enabled": True,
"options_margin_mode": "coin",
"options_index_px": 2280.0,
"upl_total_usdc": 0.0016,
"positions": [
{
"inst_id": "ETH-USD-260822-2250-C",
"margin_mode": "coin",
"premium_ccy": "ETH",
"upl": 0.0016,
"idx_px": 2280.0,
}
],
}
out = options_float_pnl_usdt(snap)
self.assertAlmostEqual(out, round(0.0016 * 2280.0, 4), places=4)
def test_repair_double_counted_fund_entry(self):
raw = {
"funding_usdt": 586.82,