fix(hub): show trend plan leverage, base, ratio, mark and floating PnL
Position and trend plan cards read sizing from trend_pullback_plans; merge agent mark/PnL; compute position_ratio_pct in hub enrich. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -276,6 +276,75 @@
|
|||||||
return fmtSymbolPrice(positionMarkPrice(pos), pos && pos.symbol, tickMap);
|
return fmtSymbolPrice(positionMarkPrice(pos), pos && pos.symbol, tickMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveTrendPositionRatioPct(trendPlan) {
|
||||||
|
const t = trendPlan || {};
|
||||||
|
if (t.position_ratio_pct != null && t.position_ratio_pct !== "") {
|
||||||
|
const n = Number(t.position_ratio_pct);
|
||||||
|
if (Number.isFinite(n)) return n;
|
||||||
|
}
|
||||||
|
const snap = Number(t.snapshot_available_usdt);
|
||||||
|
const margin = Number(t.plan_margin_capital);
|
||||||
|
if (Number.isFinite(snap) && snap > 0 && Number.isFinite(margin) && margin > 0) {
|
||||||
|
return Math.round((margin / snap) * 10000) / 100;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTrendSizingFooter(mo, trendPlan, isTrend) {
|
||||||
|
if (!isTrend || !trendPlan || !trendPlan.id) {
|
||||||
|
return {
|
||||||
|
leverage: mo.leverage,
|
||||||
|
planBase: mo.margin_capital,
|
||||||
|
positionRatio: mo.position_ratio,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const base =
|
||||||
|
trendPlan.snapshot_available_usdt != null && trendPlan.snapshot_available_usdt !== ""
|
||||||
|
? trendPlan.snapshot_available_usdt
|
||||||
|
: trendPlan.plan_margin_capital;
|
||||||
|
return {
|
||||||
|
leverage: trendPlan.leverage,
|
||||||
|
planBase: base,
|
||||||
|
positionRatio: resolveTrendPositionRatioPct(trendPlan),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTrendMarkPrice(pos, trendPlan, symbol, tickMap) {
|
||||||
|
const fromPos = fmtMarkPrice(pos, tickMap);
|
||||||
|
if (fromPos && fromPos !== "—") return fromPos;
|
||||||
|
const t = trendPlan || {};
|
||||||
|
const sym = symbol || (pos && pos.symbol) || t.exchange_symbol || t.symbol || "";
|
||||||
|
if (t.floating_mark != null && t.floating_mark !== "") {
|
||||||
|
return fmtSymbolPrice(t.floating_mark, sym, tickMap);
|
||||||
|
}
|
||||||
|
if (t.last_mark_price != null && t.last_mark_price !== "") {
|
||||||
|
return fmtSymbolPrice(t.last_mark_price, sym, tickMap);
|
||||||
|
}
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTrendFloatingPnl(pos, trendPlan) {
|
||||||
|
let upnl = pos && pos.unrealized_pnl;
|
||||||
|
if (upnl != null && upnl !== "" && Number.isFinite(Number(upnl))) return Number(upnl);
|
||||||
|
const t = trendPlan || {};
|
||||||
|
if (t.floating_pnl != null && t.floating_pnl !== "") {
|
||||||
|
const n = Number(t.floating_pnl);
|
||||||
|
if (Number.isFinite(n)) return n;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFloatingPnlText(upnl, notionalUsdt) {
|
||||||
|
if (upnl == null || !Number.isFinite(Number(upnl))) return { text: "—", cls: "" };
|
||||||
|
let pnlText = fmt(upnl, 2) + "U";
|
||||||
|
const notional = Number(notionalUsdt);
|
||||||
|
if (Number.isFinite(notional) && Math.abs(notional) > 1e-8) {
|
||||||
|
const pct = (Number(upnl) / Math.abs(notional)) * 100;
|
||||||
|
pnlText += ` (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
|
||||||
|
}
|
||||||
|
return { text: pnlText, cls: pnlCls(upnl) };
|
||||||
|
}
|
||||||
|
|
||||||
function pnlCls(v) {
|
function pnlCls(v) {
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
if (!Number.isFinite(n) || n === 0) return "";
|
if (!Number.isFinite(n) || n === 0) return "";
|
||||||
@@ -1444,7 +1513,7 @@
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTrendPlanCard(t, tickMap) {
|
function renderTrendPlanCard(t, tickMap, pos) {
|
||||||
const sym = t.exchange_symbol || t.symbol || "";
|
const sym = t.exchange_symbol || t.symbol || "";
|
||||||
const side = (t.direction || "long").toLowerCase();
|
const side = (t.direction || "long").toLowerCase();
|
||||||
const sl = t.stop_loss_display || fmtSymbolPrice(t.stop_loss, sym, tickMap);
|
const sl = t.stop_loss_display || fmtSymbolPrice(t.stop_loss, sym, tickMap);
|
||||||
@@ -1454,10 +1523,7 @@
|
|||||||
t.add_upper_display || fmtSymbolPrice(t.add_upper, sym, tickMap) || "—";
|
t.add_upper_display || fmtSymbolPrice(t.add_upper, sym, tickMap) || "—";
|
||||||
const rr = resolveTrendPlanRr(t, side, t.avg_entry_price, t.stop_loss, t.take_profit);
|
const rr = resolveTrendPlanRr(t, side, t.avg_entry_price, t.stop_loss, t.take_profit);
|
||||||
const rrTxt = rr != null ? `${fmt(rr, 2)}:1` : "—";
|
const rrTxt = rr != null ? `${fmt(rr, 2)}:1` : "—";
|
||||||
const mark =
|
const mark = resolveTrendMarkPrice(pos, t, sym, tickMap);
|
||||||
t.floating_mark != null && t.floating_mark !== ""
|
|
||||||
? fmtSymbolPrice(t.floating_mark, sym, tickMap)
|
|
||||||
: "—";
|
|
||||||
const legsDone = t.add_count != null ? t.add_count : t.legs_done;
|
const legsDone = t.add_count != null ? t.add_count : t.legs_done;
|
||||||
const legsTotal = t.add_count_total != null ? t.add_count_total : t.dca_legs;
|
const legsTotal = t.add_count_total != null ? t.add_count_total : t.dca_legs;
|
||||||
const legsTxt =
|
const legsTxt =
|
||||||
@@ -1466,29 +1532,34 @@
|
|||||||
: legsDone != null
|
: legsDone != null
|
||||||
? esc(legsDone)
|
? esc(legsDone)
|
||||||
: "—";
|
: "—";
|
||||||
let pnlInner = "—";
|
const upnlTrend = resolveTrendFloatingPnl(pos, t);
|
||||||
if (t.floating_pnl != null && t.floating_pnl !== "") {
|
const notional =
|
||||||
let pnlText = `${fmt(t.floating_pnl, 2)}U`;
|
pos && pos.notional_usdt != null
|
||||||
const margin = Number(t.plan_margin_capital);
|
? pos.notional_usdt
|
||||||
if (Number.isFinite(margin) && margin > 0) {
|
: t.plan_margin_capital != null
|
||||||
const pct = (Number(t.floating_pnl) / margin) * 100;
|
? Number(t.plan_margin_capital) * Number(t.leverage || 1)
|
||||||
pnlText += ` (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
|
: null;
|
||||||
}
|
const pnlFmt = formatFloatingPnlText(upnlTrend, notional);
|
||||||
pnlInner = `<span class="pos-value ${pnlCls(t.floating_pnl)}">${esc(pnlText)}</span>`;
|
const pnlInner =
|
||||||
}
|
pnlFmt.text === "—"
|
||||||
|
? "—"
|
||||||
|
: `<span class="pos-value ${pnlFmt.cls}">${esc(pnlFmt.text)}</span>`;
|
||||||
|
const sizing = resolveTrendSizingFooter({}, t, true);
|
||||||
|
const levTxt =
|
||||||
|
sizing.leverage != null && sizing.leverage !== "" ? `${esc(sizing.leverage)}x` : "—";
|
||||||
|
const baseTxt =
|
||||||
|
sizing.planBase != null && sizing.planBase !== "" ? `${fmt(sizing.planBase, 2)}U` : "—";
|
||||||
|
const ratioTxt =
|
||||||
|
sizing.positionRatio != null && sizing.positionRatio !== ""
|
||||||
|
? `${fmt(sizing.positionRatio, 2)}%`
|
||||||
|
: "—";
|
||||||
const riskTxt =
|
const riskTxt =
|
||||||
t.risk_percent != null && t.risk_percent !== "" ? `${esc(t.risk_percent)}%` : "—";
|
t.risk_percent != null && t.risk_percent !== "" ? `${esc(t.risk_percent)}%` : "—";
|
||||||
const foot = [];
|
const footHtml = `<div class="hub-trend-plan-foot pos-footer">
|
||||||
if (t.snapshot_available_usdt != null) {
|
<span>杠杆: ${levTxt}</span>
|
||||||
foot.push(`快照可用 ${fmt(t.snapshot_available_usdt, 2)}U`);
|
<span>计划基数: ${baseTxt}</span>
|
||||||
}
|
<span>仓位占比: ${ratioTxt}</span>
|
||||||
if (t.plan_margin_capital != null) {
|
</div>`;
|
||||||
foot.push(`计划保证金≈${fmt(t.plan_margin_capital, 2)}U`);
|
|
||||||
}
|
|
||||||
if (t.leverage != null) foot.push(`杠杆 ${esc(t.leverage)}x`);
|
|
||||||
const footHtml = foot.length
|
|
||||||
? `<div class="hub-trend-plan-foot">${foot.map((x) => esc(x)).join(" | ")}</div>`
|
|
||||||
: "";
|
|
||||||
return `<div class="hub-trend-plan-card hub-pos-card">
|
return `<div class="hub-trend-plan-card hub-pos-card">
|
||||||
<div class="hub-trend-plan-head">
|
<div class="hub-trend-plan-head">
|
||||||
<span class="hub-trend-plan-title">#${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)}</span>
|
<span class="hub-trend-plan-title">#${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)}</span>
|
||||||
@@ -1512,10 +1583,24 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTrendSection(trends, tickMap) {
|
function renderTrendSection(trends, tickMap, positions) {
|
||||||
if (!trends || !trends.length) return "";
|
if (!trends || !trends.length) return "";
|
||||||
|
const posList = Array.isArray(positions) ? positions : [];
|
||||||
return `<div class="hub-trend-plan-list">${trends
|
return `<div class="hub-trend-plan-list">${trends
|
||||||
.map((t) => renderTrendPlanCard(t, tickMap))
|
.map((t) => {
|
||||||
|
const sym = t.exchange_symbol || t.symbol || "";
|
||||||
|
const side = (t.direction || "long").toLowerCase();
|
||||||
|
let matched = null;
|
||||||
|
for (const p of posList) {
|
||||||
|
if (!symbolsMatchHub(p.symbol, sym)) continue;
|
||||||
|
const ps = (p.side || "").toLowerCase();
|
||||||
|
if (!ps || ps === side) {
|
||||||
|
matched = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return renderTrendPlanCard(t, tickMap, matched);
|
||||||
|
})
|
||||||
.join("")}</div>`;
|
.join("")}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1541,12 +1626,13 @@
|
|||||||
const isTrend = isTrendContext(mo, trendPlan);
|
const isTrend = isTrendContext(mo, trendPlan);
|
||||||
const rr = resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored, trendPlan);
|
const rr = resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored, trendPlan);
|
||||||
const beSecured = isBreakevenSecured(side, entry, mo, cond, pos);
|
const beSecured = isBreakevenSecured(side, entry, mo, cond, pos);
|
||||||
const upnl = pos.unrealized_pnl;
|
const upnl = resolveTrendFloatingPnl(pos, trendPlan);
|
||||||
let pnlText = fmt(upnl, 2) + "U";
|
const pnlFmt = formatFloatingPnlText(upnl, pos.notional_usdt);
|
||||||
if (pos.notional_usdt && upnl != null && Math.abs(Number(pos.notional_usdt)) > 1e-8) {
|
const pnlText = pnlFmt.text;
|
||||||
const pct = (Number(upnl) / Math.abs(Number(pos.notional_usdt))) * 100;
|
const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend);
|
||||||
pnlText += ` (${pct >= 0 ? "" : ""}${pct.toFixed(2)}%)`;
|
const markDisplay = isTrend
|
||||||
}
|
? resolveTrendMarkPrice(pos, trendPlan, symbol, tickMap)
|
||||||
|
: fmtMarkPrice(pos, tickMap);
|
||||||
const meta = [];
|
const meta = [];
|
||||||
if (isTrend) {
|
if (isTrend) {
|
||||||
meta.push(monitorOrderSourceHtml(mo, trendPlan));
|
meta.push(monitorOrderSourceHtml(mo, trendPlan));
|
||||||
@@ -1601,17 +1687,17 @@
|
|||||||
<div class="pos-meta">${meta.map((m) => `<span class="pos-meta-item">${m}</span>`).join("")}</div>
|
<div class="pos-meta">${meta.map((m) => `<span class="pos-meta-item">${m}</span>`).join("")}</div>
|
||||||
<div class="pos-grid">
|
<div class="pos-grid">
|
||||||
<div class="pos-cell"><span class="pos-label">开仓价</span><span class="pos-value">${fmtEntryPrice(pos, tickMap)}</span></div>
|
<div class="pos-cell"><span class="pos-label">开仓价</span><span class="pos-value">${fmtEntryPrice(pos, tickMap)}</span></div>
|
||||||
<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">${fmtMarkPrice(pos, tickMap)}</span></div>
|
<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">${markDisplay}</span></div>
|
||||||
<div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${sl != null && sl !== "" ? fmtSymbolPrice(sl, symbol, tickMap) : "—"}</span></div>
|
<div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${sl != null && sl !== "" ? fmtSymbolPrice(sl, symbol, tickMap) : "—"}</span></div>
|
||||||
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value${tpMonitored ? " pos-tp-program" : ""}">${formatTpCellValue(tp, tpMonitored, symbol, tickMap)}</span></div>
|
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value${tpMonitored ? " pos-tp-program" : ""}">${formatTpCellValue(tp, tpMonitored, symbol, tickMap)}</span></div>
|
||||||
<div class="pos-cell"><span class="pos-label">盈亏比</span><span class="pos-value">${rr != null ? fmt(rr, 2) + ":1" : "—"}</span></div>
|
<div class="pos-cell"><span class="pos-label">盈亏比</span><span class="pos-value">${rr != null ? fmt(rr, 2) + ":1" : "—"}</span></div>
|
||||||
<div class="pos-cell"><span class="pos-label">张数</span><span class="pos-value">${fmt(pos.contracts, 4)}</span></div>
|
<div class="pos-cell"><span class="pos-label">张数</span><span class="pos-value">${fmt(pos.contracts, 4)}</span></div>
|
||||||
<div class="pos-cell"><span class="pos-label">浮盈亏</span><span class="pos-value ${pnlCls(upnl)}">${pnlText}</span></div>
|
<div class="pos-cell"><span class="pos-label">浮盈亏</span><span class="pos-value ${pnlFmt.cls}">${pnlText}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-footer">
|
<div class="pos-footer">
|
||||||
<span>杠杆: ${mo.leverage != null ? esc(mo.leverage) + "x" : "—"}</span>
|
<span>杠杆: ${sizingFoot.leverage != null && sizingFoot.leverage !== "" ? esc(sizingFoot.leverage) + "x" : "—"}</span>
|
||||||
<span>计划基数: ${mo.margin_capital != null ? fmt(mo.margin_capital, 2) + "U" : "—"}</span>
|
<span>计划基数: ${sizingFoot.planBase != null && sizingFoot.planBase !== "" ? fmt(sizingFoot.planBase, 2) + "U" : "—"}</span>
|
||||||
<span>仓位占比: ${mo.position_ratio != null ? esc(mo.position_ratio) + "%" : "—"}</span>
|
<span>仓位占比: ${sizingFoot.positionRatio != null && sizingFoot.positionRatio !== "" ? fmt(sizingFoot.positionRatio, 2) + "%" : "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-ex-orders">
|
<div class="pos-ex-orders">
|
||||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||||
@@ -1927,7 +2013,11 @@
|
|||||||
}
|
}
|
||||||
html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders, tickMap), "暂无运行中的下单监控");
|
html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders, tickMap), "暂无运行中的下单监控");
|
||||||
if ((row.capabilities || []).includes("trend")) {
|
if ((row.capabilities || []).includes("trend")) {
|
||||||
html += renderHubSectionCard("趋势回调", renderTrendSection(trends, tickMap), "暂无运行中的趋势回调计划");
|
html += renderHubSectionCard(
|
||||||
|
"趋势回调",
|
||||||
|
renderTrendSection(trends, tickMap, pos),
|
||||||
|
"暂无运行中的趋势回调计划"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
html += renderHubSectionCard("顺势加仓", renderRollSection(rolls, tickMap), "暂无运行中的顺势加仓组");
|
html += renderHubSectionCard("顺势加仓", renderRollSection(rolls, tickMap), "暂无运行中的顺势加仓组");
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||||
<link rel="stylesheet" href="/assets/app.css?v=20260604-hub-trend-plan" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260604-hub-trend-metrics" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -229,6 +229,6 @@
|
|||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart.js?v=20260604-hub-trend-plan"></script>
|
<script src="/assets/chart.js?v=20260604-hub-trend-plan"></script>
|
||||||
<script src="/assets/app.js?v=20260604-hub-trend-plan"></script>
|
<script src="/assets/app.js?v=20260604-hub-trend-metrics"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -352,6 +352,13 @@ def enrich_trend_plan_for_hub(cfg: dict, raw: dict) -> dict:
|
|||||||
d["planned_rr"] = float(rr)
|
d["planned_rr"] = float(rr)
|
||||||
except (TypeError, ValueError, KeyError):
|
except (TypeError, ValueError, KeyError):
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
snap = float(d.get("snapshot_available_usdt") or 0)
|
||||||
|
margin = float(d.get("plan_margin_capital") or 0)
|
||||||
|
if snap > 0 and margin > 0:
|
||||||
|
d["position_ratio_pct"] = round(margin / snap * 100.0, 2)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user