feat(hub): exchange price precision, entry price, and trend DCA display
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -181,6 +181,77 @@
|
||||
return Number(n).toLocaleString(undefined, { maximumFractionDigits: d });
|
||||
}
|
||||
|
||||
/** 交易所持仓开仓价(四所子代理 entry_price) */
|
||||
function positionEntryPrice(pos) {
|
||||
if (!pos) return null;
|
||||
const n = Number(pos.entry_price);
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
return n;
|
||||
}
|
||||
|
||||
function symbolPriceKey(sym) {
|
||||
return (sym || "").trim().toUpperCase();
|
||||
}
|
||||
|
||||
function buildPriceTickMap(row) {
|
||||
const map = Object.create(null);
|
||||
const put = (sym, tick) => {
|
||||
const k = symbolPriceKey(sym);
|
||||
if (!k || tick == null || !Number.isFinite(Number(tick))) return;
|
||||
if (map[k] == null) map[k] = Number(tick);
|
||||
};
|
||||
((row && row.agent && row.agent.positions) || []).forEach((p) => put(p.symbol, p.price_tick));
|
||||
const hm = (row && row.hub_monitor) || {};
|
||||
(hm.trends || []).forEach((t) => put(t.exchange_symbol || t.symbol, t.price_tick));
|
||||
(hm.orders || []).forEach((o) => put(o.exchange_symbol || o.symbol, o.price_tick));
|
||||
return map;
|
||||
}
|
||||
|
||||
function lookupPriceTick(symbol, tickMap) {
|
||||
if (!tickMap || !symbol) return null;
|
||||
const k = symbolPriceKey(symbol);
|
||||
if (tickMap[k] != null) return tickMap[k];
|
||||
const base = normSym(symbol);
|
||||
if (base && tickMap[base] != null) return tickMap[base];
|
||||
return null;
|
||||
}
|
||||
|
||||
function decimalsFromTick(tick) {
|
||||
if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) return null;
|
||||
const t = Number(tick);
|
||||
if (t >= 1) return 0;
|
||||
const s = t.toFixed(12).replace(/0+$/, "");
|
||||
const frac = s.split(".")[1];
|
||||
return frac ? Math.min(12, frac.length) : 0;
|
||||
}
|
||||
|
||||
function defaultPriceDecimals(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return 4;
|
||||
const av = Math.abs(n);
|
||||
if (av >= 10000) return 2;
|
||||
if (av >= 100) return 3;
|
||||
if (av >= 1) return 4;
|
||||
if (av >= 0.01) return 6;
|
||||
return 8;
|
||||
}
|
||||
|
||||
/** 按交易所 tick(子代理/Flask 下发)格式化价格 */
|
||||
function fmtSymbolPrice(value, symbol, tickMap, displayFallback) {
|
||||
if (displayFallback != null && displayFallback !== "") return String(displayFallback);
|
||||
if (value == null || value === "") return "—";
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return "—";
|
||||
const tick = lookupPriceTick(symbol, tickMap);
|
||||
const d = decimalsFromTick(tick);
|
||||
return fmt(n, d != null ? d : defaultPriceDecimals(n));
|
||||
}
|
||||
|
||||
function fmtEntryPrice(pos, tickMap) {
|
||||
if (pos && pos.entry_price_fmt) return String(pos.entry_price_fmt);
|
||||
return fmtSymbolPrice(positionEntryPrice(pos), pos && pos.symbol, tickMap);
|
||||
}
|
||||
|
||||
function pnlCls(v) {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n) || n === 0) return "";
|
||||
@@ -845,7 +916,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderOrderRows(exchangeId, symbol, orders, kind) {
|
||||
function renderOrderRows(exchangeId, symbol, orders, kind, tickMap) {
|
||||
if (!orders || !orders.length) {
|
||||
const hint =
|
||||
kind === "conditional"
|
||||
@@ -859,7 +930,11 @@
|
||||
const oidAttr = esc(o.id || "").replace(/"/g, """);
|
||||
const chAttr = esc(o.channel || "regular").replace(/"/g, """);
|
||||
const trig =
|
||||
o.trigger_price != null ? fmt(o.trigger_price, 4) : o.price != null ? fmt(o.price, 4) : "—";
|
||||
o.trigger_price != null
|
||||
? fmtSymbolPrice(o.trigger_price, symbol, tickMap)
|
||||
: o.price != null
|
||||
? fmtSymbolPrice(o.price, symbol, tickMap)
|
||||
: "—";
|
||||
return `<tr>
|
||||
<td>${esc(o.label || o.type || "委托")}</td>
|
||||
<td>${fmt(o.amount, 4)}</td>
|
||||
@@ -875,7 +950,7 @@
|
||||
return inferTpslFromCondOrders(side, cond, entry);
|
||||
}
|
||||
|
||||
function renderOrdersCollapse(exchangeId, symbol, cond, reg) {
|
||||
function renderOrdersCollapse(exchangeId, symbol, cond, reg, tickMap) {
|
||||
const symAttr = esc(symbol || "").replace(/"/g, """);
|
||||
const orderTotal = cond.length + reg.length;
|
||||
const collapseKey = ordersCollapseKey(exchangeId, symbol);
|
||||
@@ -884,8 +959,8 @@
|
||||
cond.length > 0
|
||||
? `<button type="button" class="btn-cancel-cond-all btn-sm ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}">撤销条件单</button>`
|
||||
: "";
|
||||
const condBody = renderOrderRows(exchangeId, symbol, cond, "conditional");
|
||||
const regBody = renderOrderRows(exchangeId, symbol, reg, "limit");
|
||||
const condBody = renderOrderRows(exchangeId, symbol, cond, "conditional", tickMap);
|
||||
const regBody = renderOrderRows(exchangeId, symbol, reg, "limit", tickMap);
|
||||
return `<details class="pos-orders-collapse"${openAttr} data-collapse-key="${esc(collapseKey)}">
|
||||
<summary class="pos-orders-collapse-summary">
|
||||
<span class="pos-orders-collapse-label">委托单 <em>${orderTotal}</em></span>
|
||||
@@ -923,7 +998,7 @@
|
||||
return { sl, tp };
|
||||
}
|
||||
|
||||
function renderExTpslRows(exchangeId, symbol, cond) {
|
||||
function renderExTpslRows(exchangeId, symbol, cond, tickMap) {
|
||||
const symAttr = esc(symbol || "").replace(/"/g, """);
|
||||
const { sl, tp } = pickExTpslOrders(cond);
|
||||
function row(label, o) {
|
||||
@@ -932,7 +1007,8 @@
|
||||
}
|
||||
const oid = esc(o.id || "").replace(/"/g, """);
|
||||
const ch = esc(o.channel || "regular").replace(/"/g, """);
|
||||
const trig = o.trigger_price != null ? fmt(o.trigger_price, 4) : "—";
|
||||
const trig =
|
||||
o.trigger_price != null ? fmtSymbolPrice(o.trigger_price, symbol, tickMap) : "—";
|
||||
return `<div class="pos-ex-order-row">
|
||||
<span class="pos-ex-order-main">${label}:触发 ${trig} · 数量 ${fmt(o.amount, 4)}</span>
|
||||
<button type="button" class="pos-ex-cancel-btn btn-cancel-order" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-order-id="${oid}" data-channel="${ch}">撤单</button>
|
||||
@@ -941,7 +1017,42 @@
|
||||
return row("止损", sl) + row("止盈", tp);
|
||||
}
|
||||
|
||||
function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan) {
|
||||
function trendAddSummaryHtml(t, tickMap) {
|
||||
const done = t.add_count != null ? t.add_count : t.legs_done;
|
||||
const total = t.add_count_total != null ? t.add_count_total : t.dca_legs;
|
||||
const sym = t.exchange_symbol || t.symbol || "";
|
||||
let html = "";
|
||||
if (done != null && Number(done) >= 0) {
|
||||
html += total != null ? ` · 补仓 <strong>${esc(done)}/${esc(total)}</strong>` : ` · 补仓 <strong>${esc(done)}</strong> 次`;
|
||||
const pxs = t.add_prices_display;
|
||||
if (Array.isArray(pxs) && pxs.length) {
|
||||
html += ` · 加仓价 ${pxs.map((p) => esc(p)).join(" / ")}`;
|
||||
} else if (Array.isArray(t.add_prices) && t.add_prices.length) {
|
||||
html += ` · 加仓价 ${t.add_prices.map((p) => esc(fmtSymbolPrice(p, sym, tickMap))).join(" / ")}`;
|
||||
} else if (Number(done) === 0) {
|
||||
html += " · 加仓价 —";
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
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("");
|
||||
}
|
||||
|
||||
function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap) {
|
||||
const symbol = pos.symbol || "";
|
||||
const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """);
|
||||
const side = (pos.side || "long").toLowerCase();
|
||||
@@ -985,6 +1096,9 @@
|
||||
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 mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan);
|
||||
return `<div class="pos-card hub-pos-card">
|
||||
@@ -1000,9 +1114,9 @@
|
||||
</div>
|
||||
<div class="pos-meta">${meta.map((m) => `<span class="pos-meta-item">${m}</span>`).join("")}</div>
|
||||
<div class="pos-grid">
|
||||
<div class="pos-cell"><span class="pos-label">成交价</span><span class="pos-value">${entry != null ? fmt(entry, 4) : "—"}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${sl != null && sl !== "" ? fmt(sl, 4) : "—"}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value">${tpMonitored ? "程序监控" : tp != null && tp !== "" ? fmt(tp, 4) : "—"}</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">${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">${fmt(pos.contracts, 4)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">浮盈亏</span><span class="pos-value ${pnlCls(upnl)}">${pnlText}</span></div>
|
||||
@@ -1014,9 +1128,9 @@
|
||||
</div>
|
||||
<div class="pos-ex-orders">
|
||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||
${renderExTpslRows(exchangeId, symbol, cond)}
|
||||
${renderExTpslRows(exchangeId, symbol, cond, tickMap)}
|
||||
</div>
|
||||
${renderOrdersCollapse(exchangeId, symbol, cond, reg)}
|
||||
${renderOrdersCollapse(exchangeId, symbol, cond, reg, tickMap)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -1055,43 +1169,32 @@
|
||||
return `<div class="hub-key-list">${cards}</div>`;
|
||||
}
|
||||
|
||||
function renderOrderMonitorSection(orders) {
|
||||
function renderOrderMonitorSection(orders, tickMap) {
|
||||
if (!orders || !orders.length) return "";
|
||||
return orders
|
||||
.map(
|
||||
(o) => `<div class="hub-mini-card">
|
||||
.map((o) => {
|
||||
const sym = o.exchange_symbol || o.symbol || "";
|
||||
return `<div class="hub-mini-card">
|
||||
<div class="hub-mini-title">#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} · ${renderDirectionHtml(o.direction)}</div>
|
||||
<div class="hub-mini-line">触发 ${fmt(o.trigger_price, 4)} · SL ${fmt(o.stop_loss, 4)} · TP ${fmt(o.take_profit, 4)} · ${esc(o.trade_style || o.monitor_type || "下单监控")}</div>
|
||||
</div>`
|
||||
)
|
||||
<div class="hub-mini-line">触发 ${fmtSymbolPrice(o.trigger_price, sym, tickMap)} · SL ${fmtSymbolPrice(o.stop_loss, sym, tickMap)} · TP ${fmtSymbolPrice(o.take_profit, sym, tickMap)} · ${esc(o.trade_style || o.monitor_type || "下单监控")}</div>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderTrendSection(trends) {
|
||||
if (!trends || !trends.length) return "";
|
||||
return trends
|
||||
.map(
|
||||
(t) => `<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">SL ${fmt(t.stop_loss, 4)} · TP ${fmt(t.take_profit, 4)} · 状态 ${esc(t.status || "active")}</div>
|
||||
</div>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderRollSection(rolls) {
|
||||
function renderRollSection(rolls, tickMap) {
|
||||
if (!rolls || !rolls.length) return "";
|
||||
return rolls
|
||||
.map(
|
||||
(g) => `<div class="hub-mini-card">
|
||||
<div class="hub-mini-title">组 #${esc(g.id)} · 监控单 #${esc(g.order_monitor_id || "—")}</div>
|
||||
<div class="hub-mini-line">腿数 ${esc(g.leg_count != null ? g.leg_count : "—")} · 止损 ${fmt(g.current_stop_loss, 4)} · ${esc(g.status || "active")}</div>
|
||||
<div class="hub-mini-line">腿数 ${esc(g.leg_count != null ? g.leg_count : "—")} · 止损 ${fmtSymbolPrice(g.current_stop_loss, g.symbol, tickMap)} · ${esc(g.status || "active")}</div>
|
||||
</div>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderPositionBlock(exchangeId, exchangeKey, x, monitorOrder, trendPlan) {
|
||||
function renderPositionBlock(exchangeId, exchangeKey, x, monitorOrder, trendPlan, tickMap) {
|
||||
const symAttr = esc(x.symbol || "").replace(/"/g, """);
|
||||
const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """);
|
||||
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
|
||||
@@ -1107,10 +1210,11 @@
|
||||
const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : "";
|
||||
return `<div class="pos-block">
|
||||
<div class="table-scroll">
|
||||
<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th><th>操作</th></tr></thead><tbody>
|
||||
<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>开仓价</th><th>张数</th><th>浮盈</th><th>操作</th></tr></thead><tbody>
|
||||
<tr>
|
||||
<td class="td-symbol"><button type="button" class="btn-open-market sym-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)">${esc(x.symbol)}</button>${symBeBadge}</td>
|
||||
<td class="${sideDirCls(x.side)}">${renderDirectionHtml(x.side)}</td>
|
||||
<td class="td-entry">${fmtEntryPrice(x, tickMap)}</td>
|
||||
<td>${fmt(x.contracts, 4)}</td>
|
||||
<td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 2)}</td>
|
||||
<td class="td-actions">
|
||||
@@ -1122,11 +1226,12 @@
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
${renderOrdersCollapse(exchangeId, x.symbol, cond, reg)}
|
||||
${renderOrdersCollapse(exchangeId, x.symbol, cond, reg, tickMap)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderGridBody(row, ag, pos, hm, flaskOk, keys, orders, trends, rolls, kmap) {
|
||||
const tickMap = buildPriceTickMap(row);
|
||||
let inner = `<div class="stat-row">
|
||||
<div class="stat-box"><div class="stat-label">余额</div><div class="stat-value">${fmt(ag.balance_usdt, 2)} <small style="font-size:12px;color:var(--muted)">U</small></div></div>
|
||||
<div class="stat-box"><div class="stat-label">浮盈合计</div><div class="stat-value ${pnlCls(ag.total_unrealized_pnl)}">${fmt(ag.total_unrealized_pnl, 2)}</div></div>
|
||||
@@ -1140,7 +1245,8 @@
|
||||
row.key || row.id,
|
||||
p,
|
||||
findMonitorOrder(orders, p.symbol, p.side),
|
||||
findTrendPlan(trends, p.symbol, p.side)
|
||||
findTrendPlan(trends, p.symbol, p.side),
|
||||
tickMap
|
||||
)
|
||||
)
|
||||
.join("");
|
||||
@@ -1150,7 +1256,8 @@
|
||||
if (orders.length) {
|
||||
inner += `<div class="section-title">下单监控 · ${orders.length}</div>`;
|
||||
orders.forEach((o) => {
|
||||
inner += `<div class="list-line">${esc(o.symbol || o.exchange_symbol)} · ${renderDirectionHtml(o.direction)} · 触发 ${fmt(o.trigger_price, 4)}</div>`;
|
||||
const sym = o.exchange_symbol || o.symbol || "";
|
||||
inner += `<div class="list-line">${esc(o.symbol || o.exchange_symbol)} · ${renderDirectionHtml(o.direction)} · 触发 ${fmtSymbolPrice(o.trigger_price, sym, tickMap)}</div>`;
|
||||
});
|
||||
}
|
||||
if ((row.capabilities || []).includes("key")) {
|
||||
@@ -1183,7 +1290,10 @@
|
||||
if ((row.capabilities || []).includes("trend") && trends.length) {
|
||||
inner += `<div class="section-title">趋势回调 · ${trends.length}</div>`;
|
||||
trends.forEach((t) => {
|
||||
inner += `<div class="list-line">#${t.id} ${esc(t.symbol)} ${renderDirectionHtml(t.direction)} · SL ${t.stop_loss} · TP ${t.take_profit}</div>`;
|
||||
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);
|
||||
inner += `<div class="list-line">#${t.id} ${esc(t.symbol)} ${renderDirectionHtml(t.direction)} · 均价 ${esc(t.avg_entry_price_display || fmtSymbolPrice(t.avg_entry_price, sym, tickMap))} · SL ${esc(sl)} · TP ${esc(tp)}${trendAddSummaryHtml(t, tickMap)}</div>`;
|
||||
});
|
||||
}
|
||||
if (rolls.length) {
|
||||
@@ -1197,6 +1307,7 @@
|
||||
}
|
||||
|
||||
function renderFullscreenExchange(row) {
|
||||
const tickMap = buildPriceTickMap(row);
|
||||
const ag = row.agent || {};
|
||||
const pos = Array.isArray(ag.positions) ? ag.positions : [];
|
||||
const hm = row.hub_monitor || {};
|
||||
@@ -1241,7 +1352,8 @@
|
||||
row.key || row.id,
|
||||
p,
|
||||
findMonitorOrder(orders, p.symbol, p.side),
|
||||
findTrendPlan(trends, p.symbol, p.side)
|
||||
findTrendPlan(trends, p.symbol, p.side),
|
||||
tickMap
|
||||
);
|
||||
});
|
||||
} else {
|
||||
@@ -1259,11 +1371,11 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders), "暂无运行中的下单监控");
|
||||
html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders, tickMap), "暂无运行中的下单监控");
|
||||
if ((row.capabilities || []).includes("trend")) {
|
||||
html += renderHubSectionCard("趋势回调", renderTrendSection(trends), "暂无运行中的趋势回调计划");
|
||||
html += renderHubSectionCard("趋势回调", renderTrendSection(trends, tickMap), "暂无运行中的趋势回调计划");
|
||||
}
|
||||
html += renderHubSectionCard("顺势加仓", renderRollSection(rolls), "暂无运行中的顺势加仓组");
|
||||
html += renderHubSectionCard("顺势加仓", renderRollSection(rolls, tickMap), "暂无运行中的顺势加仓组");
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user