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:
@@ -303,19 +303,34 @@
|
||||
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 tid = Number(o.trend_plan_id);
|
||||
if (Number.isFinite(tid) && tid > 0) return "趋势回调";
|
||||
const mt = String(o.monitor_type || "").trim();
|
||||
if (mt === "趋势回调") return "趋势回调";
|
||||
const kst = String(o.key_signal_type || "").trim();
|
||||
if (kst === "趋势回调" || kst === "趋势回调计划") return "趋势回调";
|
||||
return mt || "下单监控";
|
||||
}
|
||||
|
||||
function monitorOrderSourceHtml(mo) {
|
||||
const src = monitorOrderSourceLabel(mo);
|
||||
function monitorOrderSourceHtml(mo, trendPlan) {
|
||||
if (isTrendContext(mo, trendPlan)) {
|
||||
return `来源: ${esc(monitorOrderSourceLabel(mo, trendPlan))}`;
|
||||
}
|
||||
const src = monitorOrderSourceLabel(mo, trendPlan);
|
||||
const kst = String((mo && mo.key_signal_type) || "").trim();
|
||||
let text = src;
|
||||
if (kst && kst !== src && !text.includes(kst)) {
|
||||
@@ -776,7 +791,23 @@
|
||||
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;
|
||||
const snap = mo && mo.rr_ratio;
|
||||
if (snap != null && snap !== "") {
|
||||
@@ -787,6 +818,17 @@
|
||||
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) {
|
||||
const mo = monitorOrder || {};
|
||||
const p = pos || {};
|
||||
@@ -1072,10 +1114,7 @@
|
||||
? mo.trigger_price
|
||||
: tp.avg_entry_price;
|
||||
const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null;
|
||||
const isTrend =
|
||||
!!(trendPlan && trendPlan.id) ||
|
||||
String(mo.monitor_type || "").trim() === "趋势回调" ||
|
||||
(mo.trend_plan_id != null && Number(mo.trend_plan_id) > 0);
|
||||
const isTrend = isTrendContext(mo, trendPlan);
|
||||
|
||||
let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : "";
|
||||
let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : "";
|
||||
@@ -1083,10 +1122,14 @@
|
||||
|
||||
if (isTrend) {
|
||||
tpMonitored = true;
|
||||
takeProfit = "";
|
||||
if (trendPlan && trendPlan.stop_loss != null && 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);
|
||||
@@ -1401,20 +1444,79 @@
|
||||
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) {
|
||||
if (!trends || !trends.length) return "";
|
||||
return trends
|
||||
.map((t) => {
|
||||
const sym = t.exchange_symbol || t.symbol || "";
|
||||
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("");
|
||||
return `<div class="hub-trend-plan-list">${trends
|
||||
.map((t) => renderTrendPlanCard(t, tickMap))
|
||||
.join("")}</div>`;
|
||||
}
|
||||
|
||||
function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap) {
|
||||
@@ -1436,7 +1538,8 @@
|
||||
const sl = tpsl.sl;
|
||||
const tp = tpsl.tp;
|
||||
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 upnl = pos.unrealized_pnl;
|
||||
let pnlText = fmt(upnl, 2) + "U";
|
||||
@@ -1445,22 +1548,42 @@
|
||||
pnlText += ` (${pct >= 0 ? "" : ""}${pct.toFixed(2)}%)`;
|
||||
}
|
||||
const meta = [];
|
||||
if (mo.monitor_type || mo.key_signal_type || mo.trend_plan_id) {
|
||||
meta.push(monitorOrderSourceHtml(mo));
|
||||
if (isTrend) {
|
||||
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 {
|
||||
meta.push("来源: 交易所持仓");
|
||||
}
|
||||
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>`
|
||||
);
|
||||
if (trendPlan && trendPlan.id) {
|
||||
meta.push(`趋势回调${trendAddSummaryHtml(trendPlan, tickMap)}`);
|
||||
meta.push("风格: —");
|
||||
meta.push(`<span class="pos-meta-off">移动保本:关</span>`);
|
||||
}
|
||||
const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : "";
|
||||
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">${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">${tpMonitored ? "程序监控" : tp != null && tp !== "" ? fmtSymbolPrice(tp, 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${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">${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>
|
||||
|
||||
Reference in New Issue
Block a user