diff --git a/lib/common/static/options_panel.js b/lib/common/static/options_panel.js
index 0ae34d1..70b81f2 100644
--- a/lib/common/static/options_panel.js
+++ b/lib/common/static/options_panel.js
@@ -753,15 +753,27 @@
function netPnlFromPos(p) {
const preview = (p && p.close_preview) || {};
+ if (preview.bid_invalid) {
+ const upl = p && p.upl != null ? Number(p.upl) : NaN;
+ return Number.isFinite(upl) ? upl : null;
+ }
if (preview.estimated_pnl != null && !Number.isNaN(Number(preview.estimated_pnl))) {
return Number(preview.estimated_pnl);
}
+ const covered = Number(preview.covered_sheets);
const recv = Number(preview.total_received);
const prem = Number(p && p.premium_paid);
- if (preview.total_received != null && !Number.isNaN(recv) && !Number.isNaN(prem)) {
+ if (
+ preview.total_received != null &&
+ Number.isFinite(covered) &&
+ covered > 0 &&
+ !Number.isNaN(recv) &&
+ !Number.isNaN(prem)
+ ) {
return recv - prem;
}
- return null;
+ const upl = p && p.upl != null ? Number(p.upl) : NaN;
+ return Number.isFinite(upl) ? upl : null;
}
function netRoiFromPos(p, net) {
diff --git a/lib/common/static/options_position_cards.js b/lib/common/static/options_position_cards.js
index 8e1e726..d2010d2 100644
--- a/lib/common/static/options_position_cards.js
+++ b/lib/common/static/options_position_cards.js
@@ -103,15 +103,27 @@
function netPnlFromPos(p) {
const preview = (p && p.close_preview) || {};
+ if (preview.bid_invalid) {
+ const upl = p && p.upl != null ? Number(p.upl) : NaN;
+ return Number.isFinite(upl) ? upl : null;
+ }
if (preview.estimated_pnl != null && !Number.isNaN(Number(preview.estimated_pnl))) {
return Number(preview.estimated_pnl);
}
+ const covered = Number(preview.covered_sheets);
const recv = Number(preview.total_received);
const prem = Number(p && p.premium_paid);
- if (preview.total_received != null && !Number.isNaN(recv) && !Number.isNaN(prem)) {
+ if (
+ preview.total_received != null &&
+ Number.isFinite(covered) &&
+ covered > 0 &&
+ !Number.isNaN(recv) &&
+ !Number.isNaN(prem)
+ ) {
return recv - prem;
}
- return null;
+ const upl = p && p.upl != null ? Number(p.upl) : NaN;
+ return Number.isFinite(upl) ? upl : null;
}
function netRoiFromPos(p, net) {
@@ -170,9 +182,9 @@
const pnlCells = hidePnl
? ""
: '
' +
'
' + (p.inst_id || "") + "" +
diff --git a/lib/hub/hub_options_funds_lib.py b/lib/hub/hub_options_funds_lib.py
index 23448d9..602d4ef 100644
--- a/lib/hub/hub_options_funds_lib.py
+++ b/lib/hub/hub_options_funds_lib.py
@@ -54,11 +54,27 @@ def options_float_pnl_usdt(options_snap: dict[str, Any] | None) -> Optional[floa
if snap.get("enabled") is False or snap.get("ok") is False:
return None
upl = snap.get("upl_total_usdc")
- if upl is None:
- return None
+ if upl is not None:
+ try:
+ return round(float(upl), 4)
+ except (TypeError, ValueError):
+ pass
+ # 快照偶发缺合计时,按持仓行回退汇总(与卡片展示一致)
try:
- return round(float(upl), 4)
- except (TypeError, ValueError):
+ from lib.options.options_positions_lib import display_pnl_from_option_row
+
+ total = 0.0
+ found = False
+ for p in snap.get("positions") or []:
+ if not isinstance(p, dict):
+ continue
+ pnl = display_pnl_from_option_row(p)
+ if pnl is None:
+ continue
+ found = True
+ total += float(pnl)
+ return round(total, 4) if found else None
+ except Exception:
return None
diff --git a/lib/instance/instance_dashboard_lib.py b/lib/instance/instance_dashboard_lib.py
index 7d7ff1d..39c96c4 100644
--- a/lib/instance/instance_dashboard_lib.py
+++ b/lib/instance/instance_dashboard_lib.py
@@ -142,12 +142,12 @@ def _format_options_item(p: dict[str, Any], *, conn=None) -> dict[str, Any]:
inst = str(p.get("inst_id") or p.get("instId") or "-").strip() or "-"
opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else (opt_type or "OPT")
- # 看板期权列固定用净盈亏(买一回收−权利金);残档买一则空.
+ # 看板期权列:优先买一净盈亏,残档回退交易所 upl
pnl = None
try:
- from lib.options.options_positions_lib import net_pnl_from_display_row
+ from lib.options.options_positions_lib import display_pnl_from_option_row
- pnl = net_pnl_from_display_row(p)
+ pnl = display_pnl_from_option_row(p)
except Exception:
pnl = None
pos = _safe_float(p.get("pos"))
diff --git a/lib/options/options_hub_lib.py b/lib/options/options_hub_lib.py
index bc3c774..cbe0d9b 100644
--- a/lib/options/options_hub_lib.py
+++ b/lib/options/options_hub_lib.py
@@ -66,17 +66,17 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
conn.close()
except Exception:
target_monitors = []
- from lib.options.options_positions_lib import net_pnl_from_display_row
+ from lib.options.options_positions_lib import display_pnl_from_option_row
upl_total = 0.0
has_upl = False
for p in positions:
- # 与持仓卡「净盈亏」一致(买一回收−权利金);不用交易所标记价 upl
- net = net_pnl_from_display_row(p)
- if net is None:
+ # 与持仓卡展示一致:优先买一净盈亏,残档回退交易所 upl
+ pnl = display_pnl_from_option_row(p)
+ if pnl is None:
continue
has_upl = True
- upl_total += float(net)
+ upl_total += float(pnl)
bal = cfg["fetch_options_balances"](ex)
return {
"ok": True,
diff --git a/lib/options/options_positions_lib.py b/lib/options/options_positions_lib.py
index e94ef25..a917f2c 100644
--- a/lib/options/options_positions_lib.py
+++ b/lib/options/options_positions_lib.py
@@ -92,13 +92,26 @@ def net_pnl_from_display_row(row: dict[str, Any]) -> float | None:
return float(net)
except (TypeError, ValueError):
pass
+ # 仅当实际吃到买盘张数时,才用 total_received − 权利金(避免 bid 无效时 total_received=0 算出 −权利金假亏)
+ try:
+ covered = float(preview.get("covered_sheets") or 0)
+ except (TypeError, ValueError):
+ covered = 0.0
recv = _safe_float(preview.get("total_received"))
paid = _safe_float(row.get("premium_paid"))
- if recv is not None and paid is not None:
+ if covered > 0 and recv is not None and paid is not None:
return round(recv - paid, 4)
return None
+def display_pnl_from_option_row(row: dict[str, Any]) -> float | None:
+ """展示用盈亏:优先买一净盈亏;残档/无买一时回退交易所标记浮盈 upl."""
+ net = net_pnl_from_display_row(row)
+ if net is not None:
+ return net
+ return _safe_float(row.get("upl"))
+
+
def sum_options_net_pnl_usdc(
cfg: dict[str, Any],
ex: Any,
@@ -106,7 +119,8 @@ def sum_options_net_pnl_usdc(
) -> float | None:
"""
期权浮盈合计(USDC),与顶栏实时盈亏/中控口径对齐为「净盈亏」:
- 各仓买一可回收 − 权利金之和.获取失败返回 None;无持仓返回 0.
+ 各仓买一可回收 − 权利金之和;残档则回退该仓交易所 upl.
+ 获取失败返回 None;无持仓返回 0.
"""
raw = raw_positions
if raw is None:
@@ -119,11 +133,11 @@ def sum_options_net_pnl_usdc(
total = 0.0
found = False
for p in positions:
- net = net_pnl_from_display_row(p)
- if net is None:
+ pnl = display_pnl_from_option_row(p)
+ if pnl is None:
continue
found = True
- total += float(net)
+ total += float(pnl)
return round(total, 4) if found else (0.0 if not positions else None)
diff --git a/lib/options/templates/options_panel.html b/lib/options/templates/options_panel.html
index eb66f31..4707dcc 100644
--- a/lib/options/templates/options_panel.html
+++ b/lib/options/templates/options_panel.html
@@ -324,4 +324,4 @@
-
+
diff --git a/manual_trading_hub/hub_ai/context.py b/manual_trading_hub/hub_ai/context.py
index 4141778..95a8ef2 100644
--- a/manual_trading_hub/hub_ai/context.py
+++ b/manual_trading_hub/hub_ai/context.py
@@ -1307,9 +1307,9 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
row["source_label"] = _options_source_label(p)
row["target_monitor_text"] = _options_target_monitor_text(p)
try:
- from lib.options.options_positions_lib import net_pnl_from_display_row
+ from lib.options.options_positions_lib import display_pnl_from_option_row
- net = net_pnl_from_display_row(row)
+ net = display_pnl_from_option_row(row)
except Exception:
net = None
row["net_pnl"] = round(float(net), 4) if net is not None else None
@@ -1324,11 +1324,6 @@ def format_dashboard_account_detail(ac: dict) -> dict[str, Any]:
}
if row.get("net_pnl") is not None:
line["pnl"] = row["net_pnl"]
- elif row.get("upl") is not None:
- try:
- line["pnl"] = round(float(row["upl"]), 4)
- except (TypeError, ValueError):
- pass
position_lines.append(line)
issues = [str(x) for x in (ac.get("issues") or [])[:3]]
return {
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index 0e048f1..450dfba 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -3821,6 +3821,30 @@
return vals.reduce((s, v) => s + v, 0);
}
+ function optionsRowDisplayPnl(p) {
+ if (!p || typeof p !== "object") return null;
+ const preview = p.close_preview || {};
+ // bid 无效时 total_received 常为 0,不能用 0−权利金冒充净亏
+ if (preview.bid_invalid) {
+ const upl = p.upl != null ? Number(p.upl) : NaN;
+ return Number.isFinite(upl) ? upl : null;
+ }
+ let net = preview.estimated_pnl;
+ const covered = Number(preview.covered_sheets);
+ if (
+ net == null &&
+ preview.total_received != null &&
+ Number.isFinite(covered) &&
+ covered > 0 &&
+ p.premium_paid != null
+ ) {
+ net = Number(preview.total_received) - Number(p.premium_paid);
+ }
+ if (net != null && Number.isFinite(Number(net))) return Number(net);
+ const upl = p.upl != null ? Number(p.upl) : NaN;
+ return Number.isFinite(upl) ? upl : null;
+ }
+
function optionsBalanceFields(opt) {
if (!opt || typeof opt !== "object") {
return { funding: null, trading: null, upl: null };
@@ -3828,6 +3852,23 @@
const bal =
opt.balances && typeof opt.balances === "object" ? opt.balances : {};
const pick = (a, b) => (a != null && a !== "" ? a : b);
+ let upl =
+ opt.upl_total_usdc != null && Number.isFinite(Number(opt.upl_total_usdc))
+ ? Number(opt.upl_total_usdc)
+ : null;
+ if (upl == null) {
+ const pos = Array.isArray(opt.positions) ? opt.positions : [];
+ let sum = 0;
+ let found = false;
+ pos.forEach((p) => {
+ const n = optionsRowDisplayPnl(p);
+ if (n != null) {
+ sum += n;
+ found = true;
+ }
+ });
+ if (found) upl = sum;
+ }
return {
funding: sumUsdtEquiv(
pick(bal.funding_usdt, opt.funding_usdt),
@@ -3837,10 +3878,7 @@
pick(bal.trading_usdt, opt.trading_usdt),
pick(bal.trading_usdc, opt.trading_usdc)
),
- upl:
- opt.upl_total_usdc != null && Number.isFinite(Number(opt.upl_total_usdc))
- ? Number(opt.upl_total_usdc)
- : null,
+ upl,
};
}
@@ -3915,10 +3953,7 @@
? "Put"
: p.opt_type || "—";
const preview = p.close_preview || {};
- let net = preview.estimated_pnl;
- if (net == null && preview.total_received != null && p.premium_paid != null) {
- net = Number(preview.total_received) - Number(p.premium_paid);
- }
+ let net = optionsRowDisplayPnl(p);
let roi = preview.estimated_pnl_ratio_pct;
if (roi == null && net != null && Number(p.premium_paid) > 0) {
roi = (Number(net) / Number(p.premium_paid)) * 100;
diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js
index 450742e..c8d389a 100644
--- a/manual_trading_hub/static/dashboard.js
+++ b/manual_trading_hub/static/dashboard.js
@@ -249,14 +249,25 @@
if (!p || typeof p !== "object") return null;
if (p.net_pnl != null && Number.isFinite(Number(p.net_pnl))) return Number(p.net_pnl);
const preview = p.close_preview || {};
+ if (preview.bid_invalid) {
+ const upl = p.upl != null ? Number(p.upl) : NaN;
+ return Number.isFinite(upl) ? upl : null;
+ }
if (preview.estimated_pnl != null && Number.isFinite(Number(preview.estimated_pnl))) {
return Number(preview.estimated_pnl);
}
- if (preview.total_received != null && p.premium_paid != null) {
+ const covered = Number(preview.covered_sheets);
+ if (
+ preview.total_received != null &&
+ Number.isFinite(covered) &&
+ covered > 0 &&
+ p.premium_paid != null
+ ) {
const n = Number(preview.total_received) - Number(p.premium_paid);
return Number.isFinite(n) ? n : null;
}
- return null;
+ const upl = p.upl != null ? Number(p.upl) : NaN;
+ return Number.isFinite(upl) ? upl : null;
}
function optionsHedgePlanId(p) {
diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html
index c20b568..003ed4b 100644
--- a/manual_trading_hub/static/index.html
+++ b/manual_trading_hub/static/index.html
@@ -1757,7 +1757,7 @@
-
+
@@ -1765,8 +1765,8 @@
-
+
-
+