feat(hub): show floating PnL on market page and drag stop-loss to place TP/SL
Pass unrealized PnL from monitor jump context, refresh from board snapshot, and let users drag the SL price line to call the same place-tpsl API as the monitor entrust dialog. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2940,6 +2940,19 @@ body.login-page {
|
|||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.market-pos-pnl {
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-pos-pnl.pnl-up {
|
||||||
|
color: #3ddc84;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-pos-pnl.pnl-down {
|
||||||
|
color: #ff7070;
|
||||||
|
}
|
||||||
|
|
||||||
.market-pos-panel .ohlcv-item {
|
.market-pos-panel .ohlcv-item {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -3283,6 +3296,14 @@ html[data-theme="light"] .market-pos-order-price {
|
|||||||
color: #9a6b10;
|
color: #9a6b10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] .market-pos-pnl.pnl-up {
|
||||||
|
color: #0a7a3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="light"] .market-pos-pnl.pnl-down {
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
html[data-theme="light"] .market-pos-clear {
|
html[data-theme="light"] .market-pos-clear {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|||||||
@@ -1304,7 +1304,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPositionMarketContext(pos, monitorOrder, trendPlan) {
|
function buildPositionMarketContext(pos, monitorOrder, trendPlan, exchangeId) {
|
||||||
const tpsl = resolvePositionTpsl(pos, monitorOrder, trendPlan);
|
const tpsl = resolvePositionTpsl(pos, monitorOrder, trendPlan);
|
||||||
const cond = condOrdersFromPosition(pos);
|
const cond = condOrdersFromPosition(pos);
|
||||||
const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
|
const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
|
||||||
@@ -1330,7 +1330,10 @@
|
|||||||
amount: num(o.amount),
|
amount: num(o.amount),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const upnl = resolveTrendFloatingPnl(pos, trendPlan);
|
||||||
return {
|
return {
|
||||||
|
exchange_id: exchangeId || null,
|
||||||
|
symbol: (pos.symbol || "").trim(),
|
||||||
side: (pos.side || "long").toLowerCase(),
|
side: (pos.side || "long").toLowerCase(),
|
||||||
entry: num(tpsl.entry),
|
entry: num(tpsl.entry),
|
||||||
stop_loss: num(tpsl.sl),
|
stop_loss: num(tpsl.sl),
|
||||||
@@ -1338,6 +1341,8 @@
|
|||||||
tp_monitored: !!tpsl.tp_monitored,
|
tp_monitored: !!tpsl.tp_monitored,
|
||||||
is_trend: !!tpsl.is_trend,
|
is_trend: !!tpsl.is_trend,
|
||||||
contracts: num(pos.contracts),
|
contracts: num(pos.contracts),
|
||||||
|
unrealized_pnl: upnl != null ? Number(upnl) : null,
|
||||||
|
notional_usdt: num(pos.notional_usdt),
|
||||||
orders: orders,
|
orders: orders,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1364,7 +1369,9 @@
|
|||||||
function marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan) {
|
function marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan) {
|
||||||
const symAttr = esc(symbol || "").replace(/"/g, """);
|
const symAttr = esc(symbol || "").replace(/"/g, """);
|
||||||
const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """);
|
const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """);
|
||||||
const ctxEnc = esc(encodePosCtx(buildPositionMarketContext(pos, monitorOrder, trendPlan))).replace(
|
const ctxEnc = esc(
|
||||||
|
encodePosCtx(buildPositionMarketContext(pos, monitorOrder, trendPlan, exchangeId))
|
||||||
|
).replace(
|
||||||
/"/g,
|
/"/g,
|
||||||
"""
|
"""
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
const elPosSl = document.getElementById("mkt-pos-sl");
|
const elPosSl = document.getElementById("mkt-pos-sl");
|
||||||
const elPosTp = document.getElementById("mkt-pos-tp");
|
const elPosTp = document.getElementById("mkt-pos-tp");
|
||||||
const elPosSize = document.getElementById("mkt-pos-size");
|
const elPosSize = document.getElementById("mkt-pos-size");
|
||||||
|
const elPosPnl = document.getElementById("mkt-pos-pnl");
|
||||||
const elPosOrders = document.getElementById("market-pos-orders");
|
const elPosOrders = document.getElementById("market-pos-orders");
|
||||||
const elPosClear = document.getElementById("market-pos-clear");
|
const elPosClear = document.getElementById("market-pos-clear");
|
||||||
const elChartWrap = document.getElementById("market-chart-wrap");
|
const elChartWrap = document.getElementById("market-chart-wrap");
|
||||||
@@ -123,6 +124,9 @@
|
|||||||
let rangeMarkers = [];
|
let rangeMarkers = [];
|
||||||
let positionLines = [];
|
let positionLines = [];
|
||||||
let posContext = null;
|
let posContext = null;
|
||||||
|
let posPnlTimer = null;
|
||||||
|
const SL_DRAG_HIT_PX = 12;
|
||||||
|
let slDrag = null;
|
||||||
let currentPriceLine = null;
|
let currentPriceLine = null;
|
||||||
let lastCandles = [];
|
let lastCandles = [];
|
||||||
let candleByTime = {};
|
let candleByTime = {};
|
||||||
@@ -184,6 +188,10 @@
|
|||||||
const el = { entry: elPosEntry, sl: elPosSl, tp: elPosTp, size: elPosSize }[k];
|
const el = { entry: elPosEntry, sl: elPosSl, tp: elPosTp, size: elPosSize }[k];
|
||||||
if (el) el.textContent = "—";
|
if (el) el.textContent = "—";
|
||||||
});
|
});
|
||||||
|
if (elPosPnl) {
|
||||||
|
elPosPnl.textContent = "—";
|
||||||
|
elPosPnl.className = "market-pos-pnl";
|
||||||
|
}
|
||||||
if (elPosOrders) elPosOrders.innerHTML = "";
|
if (elPosOrders) elPosOrders.innerHTML = "";
|
||||||
syncChartWrapLayout();
|
syncChartWrapLayout();
|
||||||
}
|
}
|
||||||
@@ -882,6 +890,253 @@
|
|||||||
setChartFullscreen(!chartFullscreen);
|
setChartFullscreen(!chartFullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showHubToast(msg, isErr) {
|
||||||
|
const t = document.getElementById("toast");
|
||||||
|
if (!t) return;
|
||||||
|
t.textContent = msg;
|
||||||
|
t.classList.toggle("err", !!isErr);
|
||||||
|
t.classList.add("show");
|
||||||
|
clearTimeout(showHubToast._hideTimer);
|
||||||
|
showHubToast._hideTimer = setTimeout(function () {
|
||||||
|
t.classList.remove("show");
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPosPnlText(ctx) {
|
||||||
|
const upnl = ctx && ctx.unrealized_pnl;
|
||||||
|
if (upnl == null || !Number.isFinite(Number(upnl))) return { text: "—", cls: "" };
|
||||||
|
const n = Number(upnl);
|
||||||
|
let text = (n >= 0 ? "+" : "") + n.toFixed(2) + "U";
|
||||||
|
const notional = ctx.notional_usdt;
|
||||||
|
if (notional != null && Number(notional) > 1e-8) {
|
||||||
|
const pct = (n / Math.abs(Number(notional))) * 100;
|
||||||
|
text += " (" + (pct >= 0 ? "+" : "") + pct.toFixed(2) + "%)";
|
||||||
|
}
|
||||||
|
return { text: text, cls: n > 0 ? "pnl-up" : n < 0 ? "pnl-down" : "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintPosPnl(ctx) {
|
||||||
|
if (!elPosPnl) return;
|
||||||
|
const p = formatPosPnlText(ctx);
|
||||||
|
elPosPnl.textContent = p.text;
|
||||||
|
elPosPnl.className = "market-pos-pnl " + p.cls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPosPnlPoll() {
|
||||||
|
if (posPnlTimer) {
|
||||||
|
clearInterval(posPnlTimer);
|
||||||
|
posPnlTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPosPnlPoll() {
|
||||||
|
stopPosPnlPoll();
|
||||||
|
if (!posContext || !posContext.exchange_id) return;
|
||||||
|
refreshPosPnlFromBoard();
|
||||||
|
posPnlTimer = setInterval(refreshPosPnlFromBoard, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshPosPnlFromBoard() {
|
||||||
|
if (!posContext || !posContext.exchange_id) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch("/api/monitor/board/snapshot", { credentials: "same-origin" });
|
||||||
|
if (!r.ok) return;
|
||||||
|
const data = await r.json();
|
||||||
|
const rows = data.rows || [];
|
||||||
|
const sym = normalizeMarketSymbol(posContext.symbol || "");
|
||||||
|
const side = (posContext.side || "long").toLowerCase();
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const row = rows[i];
|
||||||
|
const ex = row.exchange || {};
|
||||||
|
if (ex.id !== posContext.exchange_id) continue;
|
||||||
|
const positions = (row.agent && row.agent.positions) || [];
|
||||||
|
for (let j = 0; j < positions.length; j++) {
|
||||||
|
const p = positions[j];
|
||||||
|
if ((p.side || "").toLowerCase() !== side) continue;
|
||||||
|
if (normalizeMarketSymbol(p.symbol || "") !== sym) continue;
|
||||||
|
if (p.unrealized_pnl != null && Number.isFinite(Number(p.unrealized_pnl))) {
|
||||||
|
posContext.unrealized_pnl = Number(p.unrealized_pnl);
|
||||||
|
if (p.notional_usdt != null && Number.isFinite(Number(p.notional_usdt))) {
|
||||||
|
posContext.notional_usdt = Number(p.notional_usdt);
|
||||||
|
}
|
||||||
|
paintPosPnl(posContext);
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(posContext));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTpForPlace(ctx) {
|
||||||
|
if (!ctx) return null;
|
||||||
|
const tp = ctx.take_profit;
|
||||||
|
if (tp != null && Number(tp) > 0) return Number(tp);
|
||||||
|
const orders = ctx.orders || [];
|
||||||
|
for (let i = 0; i < orders.length; i++) {
|
||||||
|
const o = orders[i];
|
||||||
|
const lbl = String(o.label || "");
|
||||||
|
if (/止盈/.test(lbl) && o.price != null && Number(o.price) > 0) return Number(o.price);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function placeTpslFromChart(newSl) {
|
||||||
|
if (!posContext || !posContext.exchange_id) {
|
||||||
|
showHubToast("缺少交易所信息,无法挂单", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sl = roundToTick(newSl);
|
||||||
|
if (sl == null || !Number.isFinite(sl) || sl <= 0) {
|
||||||
|
showHubToast("止损价无效", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tp = resolveTpForPlace(posContext);
|
||||||
|
if (tp == null || tp <= 0) {
|
||||||
|
showHubToast("未找到有效止盈价,请先在监控区用「委托」填写止盈", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sym = normalizeMarketSymbol(posContext.symbol || "");
|
||||||
|
const side = posContext.side || "long";
|
||||||
|
const contracts = posContext.contracts;
|
||||||
|
const oldSl = posContext.stop_loss;
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"确认 " +
|
||||||
|
sym +
|
||||||
|
" " +
|
||||||
|
side +
|
||||||
|
"\n先撤销全部条件单,再挂止损 " +
|
||||||
|
fmtPrice(sl) +
|
||||||
|
"、止盈 " +
|
||||||
|
fmtPrice(tp) +
|
||||||
|
(oldSl != null ? "\n(原止损 " + fmtPrice(oldSl) + ")" : "")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = await fetch(
|
||||||
|
"/api/orders/" + encodeURIComponent(posContext.exchange_id) + "/place-tpsl",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
symbol: sym,
|
||||||
|
side: side,
|
||||||
|
stop_loss: sl,
|
||||||
|
take_profit: tp,
|
||||||
|
contracts: contracts > 0 ? contracts : null,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const j = await r.json();
|
||||||
|
const pl = j.payload || {};
|
||||||
|
const ok = j.ok && pl.ok !== false;
|
||||||
|
showHubToast(
|
||||||
|
ok ? "止损已更新(已撤旧条件单并重新挂单)" : pl.error || JSON.stringify(j),
|
||||||
|
!ok
|
||||||
|
);
|
||||||
|
if (ok) {
|
||||||
|
posContext.stop_loss = sl;
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(posContext));
|
||||||
|
} catch (_) {}
|
||||||
|
if (elPosSl) elPosSl.textContent = fmtPrice(sl);
|
||||||
|
updatePositionLines();
|
||||||
|
fetch("/api/monitor/board/refresh", { method: "POST", credentials: "same-origin" });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showHubToast(String(e.message || e), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function slLineCoordinate() {
|
||||||
|
if (!candleSeries || !posContext) return null;
|
||||||
|
const px =
|
||||||
|
slDrag && slDrag.active && slDrag.previewSl != null
|
||||||
|
? slDrag.previewSl
|
||||||
|
: posContext.stop_loss;
|
||||||
|
if (px == null || !Number.isFinite(Number(px))) return null;
|
||||||
|
return candleSeries.priceToCoordinate(roundToTick(px));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientYToChartPrice(clientY) {
|
||||||
|
if (!candleSeries || !chartHost) return null;
|
||||||
|
const rect = chartHost.getBoundingClientRect();
|
||||||
|
const y = clientY - rect.top;
|
||||||
|
const p = candleSeries.coordinateToPrice(y);
|
||||||
|
if (p == null || !Number.isFinite(Number(p))) return null;
|
||||||
|
return roundToTick(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPointerNearSlLine(clientY) {
|
||||||
|
const coord = slLineCoordinate();
|
||||||
|
if (coord == null || !chartHost) return false;
|
||||||
|
const rect = chartHost.getBoundingClientRect();
|
||||||
|
return Math.abs(clientY - rect.top - coord) <= SL_DRAG_HIT_PX;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSlLineHover(e) {
|
||||||
|
if (!chartHost || (slDrag && slDrag.active)) return;
|
||||||
|
if (!posContext || posContext.stop_loss == null) {
|
||||||
|
chartHost.style.cursor = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chartHost.style.cursor = isPointerNearSlLine(e.clientY) ? "ns-resize" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSlDragStart(e) {
|
||||||
|
if (!posContext || posContext.stop_loss == null || !candleSeries) return;
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
if (!isPointerNearSlLine(e.clientY)) return;
|
||||||
|
e.preventDefault();
|
||||||
|
slDrag = {
|
||||||
|
active: true,
|
||||||
|
moved: false,
|
||||||
|
startSl: Number(posContext.stop_loss),
|
||||||
|
previewSl: Number(posContext.stop_loss),
|
||||||
|
};
|
||||||
|
if (chartHost) chartHost.style.cursor = "ns-resize";
|
||||||
|
updatePositionLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSlDragMove(e) {
|
||||||
|
if (!slDrag || !slDrag.active) return;
|
||||||
|
const p = clientYToChartPrice(e.clientY);
|
||||||
|
if (p == null || p <= 0) return;
|
||||||
|
slDrag.previewSl = p;
|
||||||
|
if (Math.abs(p - slDrag.startSl) > 1e-12) slDrag.moved = true;
|
||||||
|
if (elPosSl) elPosSl.textContent = fmtPrice(p);
|
||||||
|
updatePositionLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSlDragEnd() {
|
||||||
|
if (!slDrag || !slDrag.active) {
|
||||||
|
slDrag = null;
|
||||||
|
if (chartHost) chartHost.style.cursor = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const preview = slDrag.previewSl;
|
||||||
|
const moved = slDrag.moved;
|
||||||
|
slDrag = null;
|
||||||
|
if (chartHost) chartHost.style.cursor = "";
|
||||||
|
updatePositionLines();
|
||||||
|
if (!moved || preview == null) return;
|
||||||
|
placeTpslFromChart(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindSlDrag() {
|
||||||
|
if (!chartHost) return;
|
||||||
|
chartHost.addEventListener("mousedown", onSlDragStart);
|
||||||
|
chartHost.addEventListener("mousemove", onSlLineHover);
|
||||||
|
document.addEventListener("mousemove", onSlDragMove);
|
||||||
|
document.addEventListener("mouseup", onSlDragEnd);
|
||||||
|
}
|
||||||
|
|
||||||
function renderPosPanel(ctx) {
|
function renderPosPanel(ctx) {
|
||||||
if (!elPosPanel || !ctx) {
|
if (!elPosPanel || !ctx) {
|
||||||
clearPosPanel();
|
clearPosPanel();
|
||||||
@@ -908,6 +1163,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (elPosSize) elPosSize.textContent = ctx.contracts != null ? String(ctx.contracts) : "—";
|
if (elPosSize) elPosSize.textContent = ctx.contracts != null ? String(ctx.contracts) : "—";
|
||||||
|
paintPosPnl(ctx);
|
||||||
if (elPosOrders) {
|
if (elPosOrders) {
|
||||||
const orders = Array.isArray(ctx.orders) ? ctx.orders : [];
|
const orders = Array.isArray(ctx.orders) ? ctx.orders : [];
|
||||||
if (!orders.length) {
|
if (!orders.length) {
|
||||||
@@ -950,9 +1206,24 @@
|
|||||||
function updatePositionLines() {
|
function updatePositionLines() {
|
||||||
clearPositionLines();
|
clearPositionLines();
|
||||||
if (!candleSeries || !posContext) return;
|
if (!candleSeries || !posContext) return;
|
||||||
|
const slPrice =
|
||||||
|
slDrag && slDrag.active && slDrag.previewSl != null
|
||||||
|
? slDrag.previewSl
|
||||||
|
: posContext.stop_loss;
|
||||||
|
const slTitle =
|
||||||
|
slDrag && slDrag.active
|
||||||
|
? "止损 " + fmtPrice(slPrice)
|
||||||
|
: slPrice != null
|
||||||
|
? "止损 ⟷"
|
||||||
|
: "止损";
|
||||||
const specs = [
|
const specs = [
|
||||||
{ price: posContext.entry, color: "#5b9cf5", title: "入场" },
|
{ price: posContext.entry, color: "#5b9cf5", title: "入场", lineWidth: 1 },
|
||||||
{ price: posContext.stop_loss, color: "#ff4d6d", title: "止损" },
|
{
|
||||||
|
price: slPrice,
|
||||||
|
color: "#ff4d6d",
|
||||||
|
title: slTitle,
|
||||||
|
lineWidth: slPrice != null ? 2 : 1,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
if (posContext.take_profit != null) {
|
if (posContext.take_profit != null) {
|
||||||
specs.push({
|
specs.push({
|
||||||
@@ -969,7 +1240,7 @@
|
|||||||
candleSeries.createPriceLine({
|
candleSeries.createPriceLine({
|
||||||
price: Number(px),
|
price: Number(px),
|
||||||
color: s.color,
|
color: s.color,
|
||||||
lineWidth: 1,
|
lineWidth: s.lineWidth != null ? s.lineWidth : 1,
|
||||||
lineStyle: 2,
|
lineStyle: 2,
|
||||||
axisLabelVisible: true,
|
axisLabelVisible: true,
|
||||||
title: s.title,
|
title: s.title,
|
||||||
@@ -980,17 +1251,21 @@
|
|||||||
|
|
||||||
function clearPosContext() {
|
function clearPosContext() {
|
||||||
posContext = null;
|
posContext = null;
|
||||||
|
slDrag = null;
|
||||||
|
stopPosPnlPoll();
|
||||||
try {
|
try {
|
||||||
sessionStorage.removeItem(HUB_MARKET_POS_CTX_KEY);
|
sessionStorage.removeItem(HUB_MARKET_POS_CTX_KEY);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
clearPosPanel();
|
clearPosPanel();
|
||||||
clearPositionLines();
|
clearPositionLines();
|
||||||
|
if (chartHost) chartHost.style.cursor = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPosContext(ctx) {
|
function applyPosContext(ctx) {
|
||||||
posContext = ctx;
|
posContext = ctx;
|
||||||
renderPosPanel(ctx);
|
renderPosPanel(ctx);
|
||||||
updatePositionLines();
|
updatePositionLines();
|
||||||
|
startPosPnlPoll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncPosContextForView(exKey, sym) {
|
function syncPosContextForView(exKey, sym) {
|
||||||
@@ -1779,6 +2054,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bind() {
|
function bind() {
|
||||||
|
bindSlDrag();
|
||||||
if (elRefresh) {
|
if (elRefresh) {
|
||||||
elRefresh.addEventListener("click", function () {
|
elRefresh.addEventListener("click", function () {
|
||||||
loadChart(true);
|
loadChart(true);
|
||||||
|
|||||||
@@ -163,6 +163,7 @@
|
|||||||
<span class="ohlcv-item"><span class="k">止损</span><span id="mkt-pos-sl">—</span></span>
|
<span class="ohlcv-item"><span class="k">止损</span><span id="mkt-pos-sl">—</span></span>
|
||||||
<span class="ohlcv-item"><span class="k">止盈</span><span id="mkt-pos-tp">—</span></span>
|
<span class="ohlcv-item"><span class="k">止盈</span><span id="mkt-pos-tp">—</span></span>
|
||||||
<span class="ohlcv-item"><span class="k">张数</span><span id="mkt-pos-size">—</span></span>
|
<span class="ohlcv-item"><span class="k">张数</span><span id="mkt-pos-size">—</span></span>
|
||||||
|
<span class="ohlcv-item"><span class="k">浮盈亏</span><span id="mkt-pos-pnl" class="market-pos-pnl">—</span></span>
|
||||||
<button type="button" id="market-pos-clear" class="ghost market-pos-clear" title="清除 K 线上的持仓价格线">清除标记</button>
|
<button type="button" id="market-pos-clear" class="ghost market-pos-clear" title="清除 K 线上的持仓价格线">清除标记</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="market-pos-orders" class="market-pos-orders"></div>
|
<div id="market-pos-orders" class="market-pos-orders"></div>
|
||||||
@@ -248,7 +249,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=20260604-hub-inst-theme"></script>
|
<script src="/assets/chart.js?v=20260604-market-pnl-sl-drag"></script>
|
||||||
<script src="/assets/app.js?v=20260604-hub-inst-theme"></script>
|
<script src="/assets/app.js?v=20260604-hub-inst-theme"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -67,7 +67,8 @@
|
|||||||
- **主图**:K 线 + 成交量(Lightweight Charts)。
|
- **主图**:K 线 + 成交量(Lightweight Charts)。
|
||||||
- **价格轴**:「自动」切换是否跟随最新价缩放。
|
- **价格轴**:「自动」切换是否跟随最新价缩放。
|
||||||
- **技术指标**(可选勾选):EMA 21/55、MACD、RSI(含 30/70 参考线);副图自上而下为 MACD、RSI。
|
- **技术指标**(可选勾选):EMA 21/55、MACD、RSI(含 30/70 参考线);副图自上而下为 MACD、RSI。
|
||||||
- **持仓标记**(从监控跳转时):展示入场、止损、止盈、委托摘要;K 线上绘制对应价格线。趋势回调若止盈为程序监控,止盈栏显示「程序监控」且不与止损同价误显。
|
- **持仓标记**(从监控跳转时):展示入场、止损、止盈、张数、**浮盈亏**(约 5 秒随监控快照刷新)、委托摘要;K 线上绘制对应价格线。趋势回调若止盈为程序监控,止盈栏显示「程序监控」且不与止损同价误显。
|
||||||
|
- **拖动止损线**:鼠标靠近红色止损线(⟷)可上下拖动;松手确认后调用与监控区相同的 **挂止盈/止损** API(先撤全部条件单再挂新止损+止盈)。须已有有效止盈价(交易所条件单或计划止盈);仅改止损、不改止盈时止盈价沿用当前上下文。
|
||||||
- **背离**:MACD/RSI 与价格简易背离标注(箭头 + 图例说明)。
|
- **背离**:MACD/RSI 与价格简易背离标注(箭头 + 图例说明)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user