From ed669fab8028d93dba07417786498cf4ec0560df Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 4 Jun 2026 10:13:44 +0800 Subject: [PATCH] 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 --- manual_trading_hub/static/app.js | 170 ++++++++++++++++++++------- manual_trading_hub/static/index.html | 4 +- strategy_trend_register.py | 7 ++ 3 files changed, 139 insertions(+), 42 deletions(-) diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index c972298..a26339c 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -276,6 +276,75 @@ 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) { const n = Number(v); if (!Number.isFinite(n) || n === 0) return ""; @@ -1444,7 +1513,7 @@ return html; } - function renderTrendPlanCard(t, tickMap) { + function renderTrendPlanCard(t, tickMap, pos) { const sym = t.exchange_symbol || t.symbol || ""; const side = (t.direction || "long").toLowerCase(); 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) || "—"; const rr = resolveTrendPlanRr(t, side, t.avg_entry_price, t.stop_loss, t.take_profit); const rrTxt = rr != null ? `${fmt(rr, 2)}:1` : "—"; - const mark = - t.floating_mark != null && t.floating_mark !== "" - ? fmtSymbolPrice(t.floating_mark, sym, tickMap) - : "—"; + const mark = resolveTrendMarkPrice(pos, t, sym, tickMap); 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 legsTxt = @@ -1466,29 +1532,34 @@ : legsDone != null ? esc(legsDone) : "—"; - let pnlInner = "—"; - if (t.floating_pnl != null && t.floating_pnl !== "") { - let pnlText = `${fmt(t.floating_pnl, 2)}U`; - const margin = Number(t.plan_margin_capital); - if (Number.isFinite(margin) && margin > 0) { - const pct = (Number(t.floating_pnl) / margin) * 100; - pnlText += ` (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`; - } - pnlInner = `${esc(pnlText)}`; - } + const upnlTrend = resolveTrendFloatingPnl(pos, t); + const notional = + pos && pos.notional_usdt != null + ? pos.notional_usdt + : t.plan_margin_capital != null + ? Number(t.plan_margin_capital) * Number(t.leverage || 1) + : null; + const pnlFmt = formatFloatingPnlText(upnlTrend, notional); + const pnlInner = + pnlFmt.text === "—" + ? "—" + : `${esc(pnlFmt.text)}`; + 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 = t.risk_percent != null && t.risk_percent !== "" ? `${esc(t.risk_percent)}%` : "—"; - const foot = []; - if (t.snapshot_available_usdt != null) { - foot.push(`快照可用 ${fmt(t.snapshot_available_usdt, 2)}U`); - } - if (t.plan_margin_capital != null) { - foot.push(`计划保证金≈${fmt(t.plan_margin_capital, 2)}U`); - } - if (t.leverage != null) foot.push(`杠杆 ${esc(t.leverage)}x`); - const footHtml = foot.length - ? `
${foot.map((x) => esc(x)).join(" | ")}
` - : ""; + const footHtml = ``; return `
#${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)} @@ -1512,10 +1583,24 @@
`; } - function renderTrendSection(trends, tickMap) { + function renderTrendSection(trends, tickMap, positions) { if (!trends || !trends.length) return ""; + const posList = Array.isArray(positions) ? positions : []; return `
${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("")}
`; } @@ -1541,12 +1626,13 @@ const isTrend = isTrendContext(mo, trendPlan); const rr = resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored, trendPlan); const beSecured = isBreakevenSecured(side, entry, mo, cond, pos); - const upnl = pos.unrealized_pnl; - let pnlText = fmt(upnl, 2) + "U"; - if (pos.notional_usdt && upnl != null && Math.abs(Number(pos.notional_usdt)) > 1e-8) { - const pct = (Number(upnl) / Math.abs(Number(pos.notional_usdt))) * 100; - pnlText += ` (${pct >= 0 ? "" : ""}${pct.toFixed(2)}%)`; - } + const upnl = resolveTrendFloatingPnl(pos, trendPlan); + const pnlFmt = formatFloatingPnlText(upnl, pos.notional_usdt); + const pnlText = pnlFmt.text; + const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend); + const markDisplay = isTrend + ? resolveTrendMarkPrice(pos, trendPlan, symbol, tickMap) + : fmtMarkPrice(pos, tickMap); const meta = []; if (isTrend) { meta.push(monitorOrderSourceHtml(mo, trendPlan)); @@ -1601,17 +1687,17 @@
${meta.map((m) => `${m}`).join("")}
开仓价${fmtEntryPrice(pos, tickMap)}
-
标记价${fmtMarkPrice(pos, tickMap)}
+
标记价${markDisplay}
止损${sl != null && sl !== "" ? fmtSymbolPrice(sl, symbol, tickMap) : "—"}
止盈${formatTpCellValue(tp, tpMonitored, symbol, tickMap)}
盈亏比${rr != null ? fmt(rr, 2) + ":1" : "—"}
张数${fmt(pos.contracts, 4)}
-
浮盈亏${pnlText}
+
浮盈亏${pnlText}
交易所止盈止损
@@ -1927,7 +2013,11 @@ } html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders, tickMap), "暂无运行中的下单监控"); if ((row.capabilities || []).includes("trend")) { - html += renderHubSectionCard("趋势回调", renderTrendSection(trends, tickMap), "暂无运行中的趋势回调计划"); + html += renderHubSectionCard( + "趋势回调", + renderTrendSection(trends, tickMap, pos), + "暂无运行中的趋势回调计划" + ); } html += renderHubSectionCard("顺势加仓", renderRollSection(rolls, tickMap), "暂无运行中的顺势加仓组"); return html; diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 9dba87d..3fafae7 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -8,7 +8,7 @@ - + @@ -229,6 +229,6 @@
- + diff --git a/strategy_trend_register.py b/strategy_trend_register.py index 08c8f9f..1c6d221 100644 --- a/strategy_trend_register.py +++ b/strategy_trend_register.py @@ -352,6 +352,13 @@ def enrich_trend_plan_for_hub(cfg: dict, raw: dict) -> dict: d["planned_rr"] = float(rr) except (TypeError, ValueError, KeyError): 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