Fix dashboard TP profit display; add options ROI column.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1060,27 +1060,73 @@ def resolve_position_monitor_source(pos: dict, hub_mon: Optional[dict]) -> str:
|
||||
|
||||
|
||||
def resolve_position_reward_at_tp(pos: dict, hub_mon: Optional[dict]) -> Optional[float]:
|
||||
"""与监控区「盈利金额」一致:优先匹配下单监控 reward_at_tp_usdt,否则仓位自身字段."""
|
||||
"""与监控区「盈利金额」一致:有字段用字段,否则按止盈价×张数推算."""
|
||||
sym = str(pos.get("symbol") or "")
|
||||
side = str(pos.get("side") or "")
|
||||
side = str(pos.get("side") or "").lower()
|
||||
if side in ("buy",):
|
||||
side = "long"
|
||||
elif side in ("sell",):
|
||||
side = "short"
|
||||
matched: Optional[dict] = None
|
||||
if isinstance(hub_mon, dict) and hub_mon.get("ok") is not False and sym:
|
||||
# 与前端 findMonitorOrder 一致:先扫 orders
|
||||
for o in hub_mon.get("orders") or []:
|
||||
if isinstance(o, dict) and _monitor_item_matches_position(o, sym, side):
|
||||
for bucket in ("orders", "rolls", "trends"):
|
||||
for o in hub_mon.get(bucket) or []:
|
||||
if not isinstance(o, dict):
|
||||
continue
|
||||
o_sym = o.get("exchange_symbol") or o.get("symbol") or ""
|
||||
if not _symbols_match(sym, str(o_sym)):
|
||||
continue
|
||||
o_side = str(o.get("direction") or "").lower()
|
||||
# 与前端 findMonitorOrder 一致:方向为空也可匹配
|
||||
if o_side and o_side != side:
|
||||
continue
|
||||
matched = o
|
||||
v = _safe_float(o.get("reward_at_tp_usdt"))
|
||||
if v is not None:
|
||||
return v
|
||||
for r in hub_mon.get("rolls") or []:
|
||||
if isinstance(r, dict) and _monitor_item_matches_position(r, sym, side):
|
||||
v = _safe_float(r.get("reward_at_tp_usdt"))
|
||||
if v is not None:
|
||||
return v
|
||||
for t in hub_mon.get("trends") or []:
|
||||
if isinstance(t, dict) and _monitor_item_matches_position(t, sym, side):
|
||||
v = _safe_float(t.get("reward_at_tp_usdt"))
|
||||
if v is not None:
|
||||
return v
|
||||
return _safe_float(pos.get("reward_at_tp_usdt"))
|
||||
break
|
||||
if matched is not None:
|
||||
break
|
||||
|
||||
v = _safe_float(pos.get("reward_at_tp_usdt"))
|
||||
if v is not None:
|
||||
return v
|
||||
|
||||
entry = _safe_float(pos.get("entry_price"))
|
||||
if entry is None and matched is not None:
|
||||
entry = _safe_float(
|
||||
matched.get("avg_entry_price")
|
||||
or matched.get("entry_price")
|
||||
or matched.get("avg_px")
|
||||
)
|
||||
tp = None
|
||||
if matched is not None:
|
||||
tp = _safe_float(matched.get("take_profit"))
|
||||
if tp is None:
|
||||
tp = _safe_float(matched.get("take_profit_display"))
|
||||
if tp is None:
|
||||
tpsl = _resolve_position_tpsl(pos, hub_mon)
|
||||
tp = tpsl.get("tp")
|
||||
contracts = pos.get("contracts")
|
||||
if contracts is None:
|
||||
contracts = pos.get("size")
|
||||
if contracts is None and matched is not None:
|
||||
contracts = matched.get("contracts")
|
||||
try:
|
||||
qty = abs(float(contracts)) if contracts is not None else None
|
||||
except (TypeError, ValueError):
|
||||
qty = None
|
||||
cs = _safe_float(pos.get("contract_size"))
|
||||
if cs is None or cs <= 0:
|
||||
cs = 1.0
|
||||
if entry is None or tp is None or not qty:
|
||||
return None
|
||||
try:
|
||||
from lib.strategy.strategy_roll_ui_lib import reward_at_tp_usdt
|
||||
|
||||
return reward_at_tp_usdt(side or "long", float(entry), float(tp), float(qty), contract_size=float(cs))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _options_source_label(p: dict) -> str:
|
||||
|
||||
@@ -259,6 +259,20 @@
|
||||
return "solo:" + ex + ":" + String((p && p.inst_id) || Math.random());
|
||||
}
|
||||
|
||||
function optionsRoiPct(p) {
|
||||
if (!p || typeof p !== "object") return null;
|
||||
const preview = p.close_preview || {};
|
||||
if (preview.estimated_pnl_ratio_pct != null && Number.isFinite(Number(preview.estimated_pnl_ratio_pct))) {
|
||||
return Number(preview.estimated_pnl_ratio_pct);
|
||||
}
|
||||
const net = optionsNetPnl(p);
|
||||
const paid = Number(p.premium_paid);
|
||||
if (net != null && Number.isFinite(paid) && paid > 0) {
|
||||
return (net / paid) * 100;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderOptionsLegRow(ac, p, groupCls) {
|
||||
const optType =
|
||||
(p.opt_type || "").toUpperCase() === "C"
|
||||
@@ -270,6 +284,7 @@
|
||||
const target = String(p.target_monitor_text || "—");
|
||||
const targetCls = target && target !== "—" ? "dash-target-monitor is-on" : "dash-target-monitor";
|
||||
const net = optionsNetPnl(p);
|
||||
const roi = optionsRoiPct(p);
|
||||
return `<tr class="${groupCls || ""}">
|
||||
<td>${exchangeLinkCell(ac)}</td>
|
||||
<td>${sourceTypeCell(source)}</td>
|
||||
@@ -279,6 +294,7 @@
|
||||
<td>${p.idx_px != null ? fmt(p.idx_px, 0) : "—"}</td>
|
||||
<td><span class="${targetCls}">${esc(target)}</span></td>
|
||||
<td class="${pnlClass(net)}">${net != null ? pnlSigned(net, 2) : "—"}</td>
|
||||
<td class="${pnlClass(roi)}">${roi != null ? esc(Number(roi).toFixed(2)) + "%" : "—"}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
@@ -322,14 +338,22 @@
|
||||
groups.forEach((g) => {
|
||||
if (g.isHedge && g.items.length >= 1) {
|
||||
let sum = 0;
|
||||
let paidSum = 0;
|
||||
let hasSum = false;
|
||||
let hasPaid = false;
|
||||
g.items.forEach(({ p }) => {
|
||||
const n = optionsNetPnl(p);
|
||||
if (n != null) {
|
||||
sum += n;
|
||||
hasSum = true;
|
||||
}
|
||||
const paid = Number(p && p.premium_paid);
|
||||
if (Number.isFinite(paid) && paid > 0) {
|
||||
paidSum += paid;
|
||||
hasPaid = true;
|
||||
}
|
||||
});
|
||||
const groupRoi = hasSum && hasPaid && paidSum > 0 ? (sum / paidSum) * 100 : null;
|
||||
const title =
|
||||
(g.source && g.source !== "—" ? g.source : "对冲") +
|
||||
(g.planId ? " #" + g.planId : "") +
|
||||
@@ -339,6 +363,9 @@
|
||||
body += `<tr class="dash-opt-group-head">
|
||||
<td colspan="7"><span class="dash-opt-group-label">${esc(title)}</span></td>
|
||||
<td class="${hasSum ? pnlClass(sum) : ""}">${hasSum ? pnlSigned(sum, 2) : "—"}</td>
|
||||
<td class="${groupRoi != null ? pnlClass(groupRoi) : ""}">${
|
||||
groupRoi != null ? esc(Number(groupRoi).toFixed(2)) + "%" : "—"
|
||||
}</td>
|
||||
</tr>`;
|
||||
g.items.forEach(({ ac, p }, idx) => {
|
||||
const cls =
|
||||
@@ -359,7 +386,7 @@
|
||||
<div class="dash-table-wrap dash-options-table-wrap">
|
||||
<table class="dash-table dash-options-table">
|
||||
<thead><tr>
|
||||
<th>交易所</th><th>类型</th><th>合约</th><th>Call/Put</th><th>到期倒计时</th><th>指数</th><th>目标监控</th><th>净盈亏</th>
|
||||
<th>交易所</th><th>类型</th><th>合约</th><th>Call/Put</th><th>到期倒计时</th><th>指数</th><th>目标监控</th><th>净盈亏</th><th>收益率</th>
|
||||
</tr></thead>
|
||||
<tbody>${body}</tbody>
|
||||
</table>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260720-dash-hedge-group" />
|
||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260720-dash-tp-roi" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-bg" aria-hidden="true"></div>
|
||||
@@ -1385,7 +1385,7 @@
|
||||
<script src="/assets/archive.js?v=20260717-archive-cal-chart"></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=20260720-dash-hedge-group"></script>
|
||||
<script src="/assets/dashboard.js?v=20260720-dash-tp-roi"></script>
|
||||
<script src="/assets/strategy.js?v=8"></script>
|
||||
<script src="/assets/help.js?v=1"></script>
|
||||
<script src="/assets/logs.js?v=1"></script>
|
||||
|
||||
Reference in New Issue
Block a user