Fix hub options float summary blank when bid book is invalid.
Stop treating total_received=0 as a real bid recycle, fall back to exchange upl for display totals, and keep row/summary aligned. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
? ""
|
||||
: '<div class="pos-cell"><span class="pos-label">净盈亏</span><span class="pos-value ' + uplCls + '">' +
|
||||
(closePreview.bid_invalid || net == null ? "—" : fmt(net, 2)) + "</span></div>" +
|
||||
(net == null ? "—" : fmt(net, 2)) + "</span></div>" +
|
||||
'<div class="pos-cell"><span class="pos-label">收益率</span><span class="pos-value ' + uplCls + '">' +
|
||||
(closePreview.bid_invalid || roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>";
|
||||
(roi == null ? "—" : fmt(roi, 2) + "%") + "</span></div>";
|
||||
return (
|
||||
'<div class="pos-card-head">' +
|
||||
'<div class="pos-card-symbol"><strong>' + (p.inst_id || "") + "</strong>" +
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -324,4 +324,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=52"></script>
|
||||
<script src="/static/options_panel.js?v=53"></script>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1757,7 +1757,7 @@
|
||||
<script src="/assets/archive.js?v=20260724-opt-archive"></script>
|
||||
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
|
||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
||||
<script src="/assets/dashboard.js?v=20260723-hide-pnl"></script>
|
||||
<script src="/assets/dashboard.js?v=20260807-opt-float"></script>
|
||||
<script src="/assets/strategy.js?v=11"></script>
|
||||
<script src="/assets/amp_stats.js?v=20260728-hedge"></script>
|
||||
<script src="/assets/help.js?v=1"></script>
|
||||
@@ -1765,8 +1765,8 @@
|
||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||
<script src="/assets/time_close_ui.js?v=3"></script>
|
||||
<script src="/assets/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/assets/options_position_cards.js?v=3"></script>
|
||||
<script src="/assets/options_position_cards.js?v=4"></script>
|
||||
<script src="/assets/backup.js?v=1"></script>
|
||||
<script src="/assets/app.js?v=20260805-intraday-close"></script>
|
||||
<script src="/assets/app.js?v=20260807-opt-float"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from lib.options.options_positions_lib import net_pnl_from_display_row, sum_options_net_pnl_usdc
|
||||
from lib.options.options_positions_lib import (
|
||||
display_pnl_from_option_row,
|
||||
net_pnl_from_display_row,
|
||||
sum_options_net_pnl_usdc,
|
||||
)
|
||||
|
||||
|
||||
class OptionsNetPnlSumTests(TestCase):
|
||||
@@ -16,17 +20,52 @@ class OptionsNetPnlSumTests(TestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
net_pnl_from_display_row(
|
||||
{"close_preview": {"total_received": 2.15}, "premium_paid": 4.95}
|
||||
{
|
||||
"close_preview": {"total_received": 2.15, "covered_sheets": 1},
|
||||
"premium_paid": 4.95,
|
||||
}
|
||||
),
|
||||
round(2.15 - 4.95, 4),
|
||||
)
|
||||
# bid 无效时 total_received=0 不得算出 −权利金
|
||||
self.assertIsNone(
|
||||
net_pnl_from_display_row(
|
||||
{
|
||||
"close_preview": {
|
||||
"bid_invalid": True,
|
||||
"total_received": 0.0,
|
||||
"covered_sheets": 0,
|
||||
},
|
||||
"premium_paid": 9.49,
|
||||
"upl": -4.2,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def test_display_pnl_falls_back_to_exchange_upl(self):
|
||||
self.assertEqual(
|
||||
display_pnl_from_option_row(
|
||||
{
|
||||
"close_preview": {"bid_invalid": True, "total_received": 0.0},
|
||||
"premium_paid": 9.49,
|
||||
"upl": -4.2,
|
||||
}
|
||||
),
|
||||
-4.2,
|
||||
)
|
||||
|
||||
@patch("lib.options.options_positions_lib.build_display_option_positions")
|
||||
def test_sum_options_net_pnl_usdc(self, mock_build):
|
||||
mock_build.return_value = [
|
||||
{"close_preview": {"estimated_pnl": -2.8}},
|
||||
{"close_preview": {"estimated_pnl": 1.0}},
|
||||
{"close_preview": {"bid_invalid": True, "estimated_pnl": 9}},
|
||||
{"close_preview": {"bid_invalid": True, "estimated_pnl": 9}, "upl": -0.5},
|
||||
]
|
||||
cfg = {"fetch_option_positions": lambda ex: [{"instId": "X"}]}
|
||||
self.assertEqual(sum_options_net_pnl_usdc(cfg, object()), -1.8)
|
||||
self.assertEqual(sum_options_net_pnl_usdc(cfg, object()), -2.3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from unittest import main
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user