feat(hub): align trend pullback display with instance in fullscreen

Position cards show trend plan source, risk%, program TP price and RR; trend section uses plan grid; hub API enriches floating PnL and planned_rr.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 10:09:04 +08:00
parent 02bc3c14bc
commit 98c904c2d1
6 changed files with 263 additions and 52 deletions
+66
View File
@@ -1320,6 +1320,72 @@ body.market-chart-fs-open {
border-color: rgba(0, 255, 157, 0.38); border-color: rgba(0, 255, 157, 0.38);
} }
.hub-trend-plan-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.hub-trend-plan-card {
padding: 12px 14px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid var(--border-soft);
border-radius: 10px;
}
.hub-trend-plan-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.hub-trend-plan-title {
font-size: 14px;
font-weight: 600;
}
.hub-trend-plan-status {
font-size: 11px;
color: var(--muted);
}
.hub-trend-plan-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
font-size: 12px;
color: var(--muted);
margin-bottom: 10px;
}
.hub-trend-plan-meta .pos-meta-accent,
.hub-trend-plan-meta strong {
color: var(--accent);
}
.hub-trend-plan-foot {
margin-top: 10px;
font-size: 11px;
color: var(--muted);
}
.pos-value.pos-tp-program {
color: #8fc8ff;
}
.exchange-fullscreen .hub-trend-plan-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 14px;
align-items: stretch;
}
.exchange-fullscreen .hub-trend-plan-card {
height: 100%;
}
/* 顺势加仓 */ /* 顺势加仓 */
.card-stat-chip.card-stat-roll { .card-stat-chip.card-stat-roll {
color: #ffb020; color: #ffb020;
+166 -43
View File
@@ -303,19 +303,34 @@
return side || "—"; return side || "—";
} }
function monitorOrderSourceLabel(mo) { function isTrendContext(monitorOrder, trendPlan) {
const mo = monitorOrder || {};
const tp = trendPlan || {};
if (tp.id != null && Number(tp.id) > 0) return true;
const tid = Number(mo.trend_plan_id);
if (Number.isFinite(tid) && tid > 0) return true;
const mt = String(mo.monitor_type || "").trim();
if (mt === "趋势回调") return true;
const kst = String(mo.key_signal_type || "").trim();
return kst === "趋势回调" || kst === "趋势回调计划";
}
function trendAddZoneLabel(direction) {
return (direction || "long").toLowerCase() === "short" ? "补仓下沿" : "补仓上沿";
}
function monitorOrderSourceLabel(mo, trendPlan) {
if (isTrendContext(mo, trendPlan)) return "趋势回调计划";
const o = mo || {}; const o = mo || {};
const tid = Number(o.trend_plan_id);
if (Number.isFinite(tid) && tid > 0) return "趋势回调";
const mt = String(o.monitor_type || "").trim(); const mt = String(o.monitor_type || "").trim();
if (mt === "趋势回调") return "趋势回调";
const kst = String(o.key_signal_type || "").trim();
if (kst === "趋势回调" || kst === "趋势回调计划") return "趋势回调";
return mt || "下单监控"; return mt || "下单监控";
} }
function monitorOrderSourceHtml(mo) { function monitorOrderSourceHtml(mo, trendPlan) {
const src = monitorOrderSourceLabel(mo); if (isTrendContext(mo, trendPlan)) {
return `来源: ${esc(monitorOrderSourceLabel(mo, trendPlan))}`;
}
const src = monitorOrderSourceLabel(mo, trendPlan);
const kst = String((mo && mo.key_signal_type) || "").trim(); const kst = String((mo && mo.key_signal_type) || "").trim();
let text = src; let text = src;
if (kst && kst !== src && !text.includes(kst)) { if (kst && kst !== src && !text.includes(kst)) {
@@ -776,7 +791,23 @@
return reward / risk; return reward / risk;
} }
function resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored) { function resolveTrendPlanRr(trendPlan, side, entry, sl, tp) {
const t = trendPlan || {};
if (t.planned_rr != null && t.planned_rr !== "") {
const n = Number(t.planned_rr);
if (Number.isFinite(n) && n > 0) return n;
}
const e = t.avg_entry_price != null && t.avg_entry_price !== "" ? t.avg_entry_price : entry;
const s = t.stop_loss != null && t.stop_loss !== "" ? t.stop_loss : sl;
const p = t.take_profit != null && t.take_profit !== "" ? t.take_profit : tp;
return calcRrRatio(side, e, s, p);
}
function resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored, trendPlan) {
if (tpMonitored && isTrendContext(mo, trendPlan)) {
const rr = resolveTrendPlanRr(trendPlan, side, entry, sl, tp);
if (rr != null) return rr;
}
if (tpMonitored) return null; if (tpMonitored) return null;
const snap = mo && mo.rr_ratio; const snap = mo && mo.rr_ratio;
if (snap != null && snap !== "") { if (snap != null && snap !== "") {
@@ -787,6 +818,17 @@
return calcRrRatio(side, entry, initSl || sl, tp); return calcRrRatio(side, entry, initSl || sl, tp);
} }
function formatTpCellValue(tp, tpMonitored, symbol, tickMap) {
if (tpMonitored) {
if (tp != null && tp !== "") {
return `程序监控 · ${fmtSymbolPrice(tp, symbol, tickMap)}`;
}
return "程序监控";
}
if (tp != null && tp !== "") return fmtSymbolPrice(tp, symbol, tickMap);
return "—";
}
function isBreakevenSecured(side, entry, monitorOrder, cond, pos) { function isBreakevenSecured(side, entry, monitorOrder, cond, pos) {
const mo = monitorOrder || {}; const mo = monitorOrder || {};
const p = pos || {}; const p = pos || {};
@@ -1072,10 +1114,7 @@
? mo.trigger_price ? mo.trigger_price
: tp.avg_entry_price; : tp.avg_entry_price;
const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null; const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null;
const isTrend = const isTrend = isTrendContext(mo, trendPlan);
!!(trendPlan && trendPlan.id) ||
String(mo.monitor_type || "").trim() === "趋势回调" ||
(mo.trend_plan_id != null && Number(mo.trend_plan_id) > 0);
let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : ""; let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : "";
let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : ""; let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : "";
@@ -1083,10 +1122,14 @@
if (isTrend) { if (isTrend) {
tpMonitored = true; tpMonitored = true;
takeProfit = "";
if (trendPlan && trendPlan.stop_loss != null && trendPlan.stop_loss !== "") { if (trendPlan && trendPlan.stop_loss != null && trendPlan.stop_loss !== "") {
sl = trendPlan.stop_loss; sl = trendPlan.stop_loss;
} }
if (trendPlan && trendPlan.take_profit != null && trendPlan.take_profit !== "") {
takeProfit = trendPlan.take_profit;
} else {
takeProfit = "";
}
} }
const inferred = inferTpslFromCondOrders(pos.side, cond, entryN); const inferred = inferTpslFromCondOrders(pos.side, cond, entryN);
@@ -1401,20 +1444,79 @@
return html; return html;
} }
function renderTrendPlanCard(t, tickMap) {
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);
const tp = t.take_profit_display || fmtSymbolPrice(t.take_profit, sym, tickMap);
const avg = t.avg_entry_price_display || fmtSymbolPrice(t.avg_entry_price, sym, tickMap);
const addZone =
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 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 =
legsDone != null && legsTotal != null
? `${esc(legsDone)}/${esc(legsTotal)}`
: 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 = `<span class="pos-value ${pnlCls(t.floating_pnl)}">${esc(pnlText)}</span>`;
}
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
? `<div class="hub-trend-plan-foot">${foot.map((x) => esc(x)).join(" ")}</div>`
: "";
return `<div class="hub-trend-plan-card hub-pos-card">
<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-status">${esc(t.status || "active")}</span>
</div>
<div class="hub-trend-plan-meta">
<span>来源: 趋势回调计划</span>
<span>风险: ${riskTxt}</span>
<span>${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)}</span>
<span>已补仓 <strong>${legsTxt}</strong></span>
</div>
<div class="pos-grid hub-trend-plan-grid">
<div class="pos-cell"><span class="pos-label">均价</span><span class="pos-value">${esc(avg)}</span></div>
<div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${esc(sl)}</span></div>
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value pos-tp-program">程序监控 · ${esc(tp)}</span></div>
<div class="pos-cell"><span class="pos-label">盈亏比</span><span class="pos-value">${esc(rrTxt)}</span></div>
<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">${esc(mark)}</span></div>
<div class="pos-cell"><span class="pos-label">浮盈亏</span>${pnlInner}</div>
</div>
${footHtml}
</div>`;
}
function renderTrendSection(trends, tickMap) { function renderTrendSection(trends, tickMap) {
if (!trends || !trends.length) return ""; if (!trends || !trends.length) return "";
return trends return `<div class="hub-trend-plan-list">${trends
.map((t) => { .map((t) => renderTrendPlanCard(t, tickMap))
const sym = t.exchange_symbol || t.symbol || ""; .join("")}</div>`;
const sl = t.stop_loss_display || fmtSymbolPrice(t.stop_loss, sym, tickMap);
const tp = t.take_profit_display || fmtSymbolPrice(t.take_profit, sym, tickMap);
const avg = t.avg_entry_price_display || fmtSymbolPrice(t.avg_entry_price, sym, tickMap);
return `<div class="hub-mini-card">
<div class="hub-mini-title">#${esc(t.id)} · ${esc(t.symbol)} · ${renderDirectionHtml(t.direction)}</div>
<div class="hub-mini-line">均价 ${esc(avg)} · SL ${esc(sl)} · TP ${esc(tp)}${trendAddSummaryHtml(t, tickMap)} · 状态 ${esc(t.status || "active")}</div>
</div>`;
})
.join("");
} }
function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap) { function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap) {
@@ -1436,7 +1538,8 @@
const sl = tpsl.sl; const sl = tpsl.sl;
const tp = tpsl.tp; const tp = tpsl.tp;
const tpMonitored = tpsl.tp_monitored; const tpMonitored = tpsl.tp_monitored;
const rr = resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored); const isTrend = isTrendContext(mo, 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 = pos.unrealized_pnl;
let pnlText = fmt(upnl, 2) + "U"; let pnlText = fmt(upnl, 2) + "U";
@@ -1445,22 +1548,42 @@
pnlText += ` (${pct >= 0 ? "" : ""}${pct.toFixed(2)}%)`; pnlText += ` (${pct >= 0 ? "" : ""}${pct.toFixed(2)}%)`;
} }
const meta = []; const meta = [];
if (mo.monitor_type || mo.key_signal_type || mo.trend_plan_id) { if (isTrend) {
meta.push(monitorOrderSourceHtml(mo)); meta.push(monitorOrderSourceHtml(mo, trendPlan));
const riskPct =
trendPlan && trendPlan.risk_percent != null && trendPlan.risk_percent !== ""
? trendPlan.risk_percent
: mo.risk_percent;
if (riskPct != null && riskPct !== "") {
meta.push(`风险: ${esc(riskPct)}%`);
}
if (trendPlan && trendPlan.id) {
const zone =
trendPlan.add_upper_display ||
fmtSymbolPrice(trendPlan.add_upper, symbol, tickMap) ||
"—";
meta.push(
`<span class="pos-meta-accent">${esc(trendAddZoneLabel(trendPlan.direction))} ${esc(zone)}</span>`
);
const addSum = trendAddSummaryHtml(trendPlan, tickMap);
if (addSum) meta.push(addSum.replace(/^ · /, ""));
}
meta.push(`<span class="pos-meta-off">移动保本:关</span>`);
} else if (mo.monitor_type || mo.key_signal_type || mo.trend_plan_id) {
meta.push(monitorOrderSourceHtml(mo, trendPlan));
if (mo.trade_style) meta.push(`风格: ${esc(mo.trade_style)}`);
else meta.push("风格: —");
if (mo.risk_percent != null) {
meta.push(`风险: ${esc(mo.risk_percent)}%`);
}
const beOn = mo.breakeven_enabled === 1 || mo.breakeven_enabled === true;
meta.push(
`<span class="${beOn ? "pos-meta-on" : "pos-meta-off"}">移动保本:${beOn ? "开" : "关"}</span>`
);
} else { } else {
meta.push("来源: 交易所持仓"); meta.push("来源: 交易所持仓");
} meta.push("风格: —");
if (mo.trade_style) meta.push(`风格: ${esc(mo.trade_style)}`); meta.push(`<span class="pos-meta-off">移动保本:关</span>`);
else meta.push("风格: —");
if (mo.risk_percent != null) {
meta.push(`风险: ${esc(mo.risk_percent)}%`);
}
const beOn = mo.breakeven_enabled === 1 || mo.breakeven_enabled === true;
meta.push(
`<span class="${beOn ? "pos-meta-on" : "pos-meta-off"}">移动保本:${beOn ? "开" : "关"}</span>`
);
if (trendPlan && trendPlan.id) {
meta.push(`趋势回调${trendAddSummaryHtml(trendPlan, tickMap)}`);
} }
const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : ""; const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : "";
const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan); const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan);
@@ -1480,8 +1603,8 @@
<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">${fmtMarkPrice(pos, 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">${sl != null && sl !== "" ? fmtSymbolPrice(sl, symbol, tickMap) : "—"}</span></div>
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value">${tpMonitored ? "程序监控" : tp != null && tp !== "" ? fmtSymbolPrice(tp, 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 ? "—" : rr != null ? fmt(rr, 2) + ":1" : "-: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 ${pnlCls(upnl)}">${pnlText}</span></div>
</div> </div>
+10 -3
View File
@@ -890,7 +890,10 @@
if (elPosSl) elPosSl.textContent = ctx.stop_loss != null ? fmtPrice(ctx.stop_loss) : "—"; if (elPosSl) elPosSl.textContent = ctx.stop_loss != null ? fmtPrice(ctx.stop_loss) : "—";
if (elPosTp) { if (elPosTp) {
if (ctx.tp_monitored) { if (ctx.tp_monitored) {
elPosTp.textContent = "程序监控"; elPosTp.textContent =
ctx.take_profit != null
? "程序监控 · " + fmtPrice(ctx.take_profit)
: "程序监控";
elPosTp.classList.add("market-pos-tp-monitored"); elPosTp.classList.add("market-pos-tp-monitored");
} else { } else {
elPosTp.textContent = ctx.take_profit != null ? fmtPrice(ctx.take_profit) : "—"; elPosTp.textContent = ctx.take_profit != null ? fmtPrice(ctx.take_profit) : "—";
@@ -944,8 +947,12 @@
{ price: posContext.entry, color: "#5b9cf5", title: "入场" }, { price: posContext.entry, color: "#5b9cf5", title: "入场" },
{ price: posContext.stop_loss, color: "#ff4d6d", title: "止损" }, { price: posContext.stop_loss, color: "#ff4d6d", title: "止损" },
]; ];
if (!posContext.tp_monitored && posContext.take_profit != null) { if (posContext.take_profit != null) {
specs.push({ price: posContext.take_profit, color: "#00ff9d", title: "止盈" }); specs.push({
price: posContext.take_profit,
color: "#00ff9d",
title: posContext.tp_monitored ? "止盈(程序)" : "止盈",
});
} }
specs.forEach(function (s) { specs.forEach(function (s) {
if (s.price == null || !Number.isFinite(Number(s.price))) return; if (s.price == null || !Number.isFinite(Number(s.price))) return;
+3 -3
View File
@@ -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-stat-colors" /> <link rel="stylesheet" href="/assets/app.css?v=20260604-hub-trend-plan" />
</head> </head>
<body> <body>
<div class="app-bg" aria-hidden="true"></div> <div class="app-bg" aria-hidden="true"></div>
@@ -228,7 +228,7 @@
<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=20260603-hub-binance-tick"></script> <script src="/assets/chart.js?v=20260604-hub-trend-plan"></script>
<script src="/assets/app.js?v=20260604-trend-handoff-src"></script> <script src="/assets/app.js?v=20260604-hub-trend-plan"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -179,7 +179,7 @@ curl -s http://127.0.0.1:5100/api/ping
| 功能 | 说明 | | 功能 | 说明 |
|------|------| |------|------|
| **2×2 主界面** | 四所信息**完整展示**:余额、持仓表、委托/平仓、折叠委托单、下单监控、关键位、趋势/加仓摘要 | | **2×2 主界面** | 四所信息**完整展示**:余额、持仓表、委托/平仓、折叠委托单、下单监控、关键位、趋势/加仓摘要 |
| **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡;独立卡片:**关键位**、**下单监控**、**趋势回调**、**顺势加仓** | | **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡(趋势持仓显示**来源: 趋势回调计划**、**风险%**、**程序监控·止盈价**、**盈亏比**,与实例策略页一致);独立卡片:**关键位**、**下单监控**、**趋势回调**(计划卡含均价/止损/止盈/盈亏比/标记价/浮盈亏)、**顺势加仓** |
| **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** | | **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** |
| **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) | | **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) |
| **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel``cancel-symbol` | | **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel``cancel-symbol` |
+17 -2
View File
@@ -336,8 +336,23 @@ def _trend_add_leg_fields(cfg: dict, d: dict) -> dict:
def enrich_trend_plan_for_hub(cfg: dict, raw: dict) -> dict: def enrich_trend_plan_for_hub(cfg: dict, raw: dict) -> dict:
"""中控 /api/hub/monitor补仓次数、加仓价(交易所精度)。""" """中控 /api/hub/monitor与策略页运行中计划卡片同字段(浮盈亏、标记价、盈亏比等)。"""
return _trend_add_leg_fields(cfg, dict(raw or {})) d = enrich_trend_plan(cfg, dict(raw or {}))
d["monitor_source"] = "趋势回调计划"
m = _m(cfg)
direction = (d.get("direction") or "long").lower()
try:
avg_e = float(d["avg_entry_price"])
sl = float(d["stop_loss"])
tp = float(d["take_profit"])
rr_fn = getattr(m, "calc_rr_ratio", None)
if callable(rr_fn):
rr = rr_fn(direction, avg_e, sl, tp)
if rr is not None:
d["planned_rr"] = float(rr)
except (TypeError, ValueError, KeyError):
pass
return d
def _patch_hub_monitor_enrich(app: Flask, cfg: dict) -> None: def _patch_hub_monitor_enrich(app: Flask, cfg: dict) -> None: