From 21ef97cbdb2fa36a13212794f9b8986f1d3b7d21 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 2 Jun 2026 13:38:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=91=E6=8E=A7=E8=B7=B3=E8=BD=AC=E8=A1=8C?= =?UTF-8?q?=E6=83=85=E5=8C=BA=EF=BC=9A=E5=B1=95=E7=A4=BA=E5=85=A5=E5=9C=BA?= =?UTF-8?q?=E4=BB=B7=E3=80=81=E6=AD=A2=E7=9B=88=E6=AD=A2=E6=8D=9F=E4=B8=8E?= =?UTF-8?q?=E5=A7=94=E6=89=98=E5=8D=95=E5=B9=B6=E5=9C=A8K=E7=BA=BF?= =?UTF-8?q?=E6=A0=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- manual_trading_hub/static/app.css | 87 ++++++++++++++ manual_trading_hub/static/app.js | 98 +++++++++++++++- manual_trading_hub/static/chart.js | 163 +++++++++++++++++++++++++++ manual_trading_hub/static/index.html | 17 ++- 4 files changed, 356 insertions(+), 9 deletions(-) diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 801e2d0..485d6a0 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -2060,6 +2060,93 @@ body.login-page { margin-right: 4px; } +.market-pos-panel { + flex: 0 0 auto; + padding: 6px 12px 8px; + border-bottom: 1px solid var(--border-soft); + background: rgba(12, 20, 32, 0.98); + font-size: 0.76rem; +} + +.market-pos-panel.hidden { + display: none; +} + +.market-pos-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 14px; +} + +.market-pos-side { + padding: 1px 8px; + border-radius: 4px; + font-size: 0.72rem; + font-weight: 600; +} + +.market-pos-side.side-long { + background: rgba(0, 255, 157, 0.12); + border: 1px solid rgba(0, 255, 157, 0.35); + color: #00ff9d; +} + +.market-pos-side.side-short { + background: rgba(255, 77, 109, 0.12); + border: 1px solid rgba(255, 77, 109, 0.35); + color: #ff4d6d; +} + +.market-pos-clear { + margin-left: auto; + font-size: 0.72rem; + padding: 2px 8px; +} + +.market-pos-orders { + display: flex; + flex-wrap: wrap; + gap: 4px 10px; + margin-top: 6px; + color: var(--muted); +} + +.market-pos-orders-empty { + font-size: 0.72rem; + opacity: 0.75; +} + +.market-pos-order { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border-soft); + white-space: nowrap; +} + +.market-pos-order-kind { + color: var(--accent); + font-size: 0.68rem; +} + +.market-pos-order-label { + color: var(--text); +} + +.market-pos-order-price { + color: #ffb84d; + font-family: var(--font-mono, monospace); +} + +.market-pos-order-amt { + color: var(--muted); + font-size: 0.68rem; +} + .sym-link { background: none; border: none; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index dfcc1ca..aea33a8 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -506,13 +506,97 @@ return (row && (row.key || row.id)) || exchangeId; } - function openMarketForPosition(exchangeId, symbol, exchangeKey) { + function buildPositionMarketContext(pos, monitorOrder) { + const mo = monitorOrder || {}; + const cond = condOrdersFromPosition(pos); + const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : []; + const guess = guessTpslFromCondOrders(pos.side, cond); + const entry = pos.entry_price != null ? pos.entry_price : mo.trigger_price; + const sl = mo.stop_loss != null ? mo.stop_loss : guess.sl; + const tp = mo.take_profit != null ? mo.take_profit : guess.tp; + const num = function (v) { + if (v == null || v === "") return null; + const n = Number(v); + return Number.isFinite(n) ? n : null; + }; + const orders = []; + cond.forEach(function (o) { + orders.push({ + kind: "条件", + label: o.label || "条件单", + price: num(o.trigger_price), + amount: num(o.amount), + }); + }); + reg.forEach(function (o) { + orders.push({ + kind: "普通", + label: o.label || o.type || "委托", + price: num(o.price != null ? o.price : o.trigger_price), + amount: num(o.amount), + }); + }); + return { + side: (pos.side || "long").toLowerCase(), + entry: num(entry), + stop_loss: num(sl), + take_profit: num(tp), + contracts: num(pos.contracts), + orders: orders, + }; + } + + const HUB_MARKET_POS_CTX_KEY = "hubMarketPosContext"; + + function encodePosCtx(ctx) { + try { + return btoa(unescape(encodeURIComponent(JSON.stringify(ctx)))); + } catch (e) { + return ""; + } + } + + function decodePosCtx(raw) { + if (!raw) return null; + try { + return JSON.parse(decodeURIComponent(escape(atob(raw)))); + } catch (e) { + return null; + } + } + + function marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder) { + const symAttr = esc(symbol || "").replace(/"/g, """); + const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """); + const ctxEnc = esc(encodePosCtx(buildPositionMarketContext(pos, monitorOrder))).replace(/"/g, """); + return ( + 'data-ex-id="' + + esc(exchangeId) + + '" data-ex-key="' + + exKeyAttr + + '" data-symbol="' + + symAttr + + '" data-pos-ctx="' + + ctxEnc + + '"' + ); + } + + function openMarketForPosition(exchangeId, symbol, exchangeKey, posCtxRaw) { const exKey = exchangeKey || resolveExchangeKey(exchangeId); const sym = normalizeMarketSymbol(symbol); if (!exKey || !sym) { showToast("无法打开行情:缺少交易所或合约", true); return; } + const ctx = decodePosCtx(posCtxRaw); + if (ctx) { + ctx.symbol = sym; + ctx.exchange_key = exKey; + sessionStorage.setItem(HUB_MARKET_POS_CTX_KEY, JSON.stringify(ctx)); + } else { + sessionStorage.removeItem(HUB_MARKET_POS_CTX_KEY); + } if (expandedExchangeId) { closeExchangeFullscreen(); } @@ -529,7 +613,7 @@ btn.onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); - openMarketForPosition(btn.dataset.exId, btn.dataset.symbol, btn.dataset.exKey); + openMarketForPosition(btn.dataset.exId, btn.dataset.symbol, btn.dataset.exKey, btn.dataset.posCtx); }; }); box.querySelectorAll(".btn-open-instance").forEach((btn) => { @@ -744,10 +828,11 @@ meta.push( `移动保本:${beOn ? "开" : "关"}` ); + const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder); return `
- + ${sideCn}
@@ -848,7 +933,7 @@ .join(""); } - function renderPositionBlock(exchangeId, exchangeKey, x) { + function renderPositionBlock(exchangeId, exchangeKey, x, monitorOrder) { const symAttr = esc(x.symbol || "").replace(/"/g, """); const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """); const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """); @@ -858,11 +943,12 @@ const guess = guessTpslFromCondOrders(x.side, cond); const slAttr = esc(String(guess.sl)).replace(/"/g, """); const tpAttr = esc(String(guess.tp)).replace(/"/g, """); + const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, x.symbol, x, monitorOrder); return `
- + @@ -886,7 +972,7 @@ `; inner += `
交易所持仓 · ${pos.length} 仓
`; if (pos.length) { - inner += pos.map((p) => renderPositionBlock(row.id, row.key || row.id, p)).join(""); + inner += pos.map((p) => renderPositionBlock(row.id, row.key || row.id, p, findMonitorOrder(orders, p.symbol, p.side))).join(""); } else { inner += '
无持仓
'; } diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index b43ffcf..b56c58f 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -38,6 +38,16 @@ const elSymLabel = document.getElementById("mkt-symbol-label"); const elTfLabel = document.getElementById("mkt-tf-label"); const elPriceAuto = document.getElementById("market-price-auto"); + const elPosPanel = document.getElementById("market-pos-panel"); + const elPosSide = document.getElementById("mkt-pos-side"); + const elPosEntry = document.getElementById("mkt-pos-entry"); + const elPosSl = document.getElementById("mkt-pos-sl"); + const elPosTp = document.getElementById("mkt-pos-tp"); + const elPosSize = document.getElementById("mkt-pos-size"); + const elPosOrders = document.getElementById("market-pos-orders"); + const elPosClear = document.getElementById("market-pos-clear"); + + const HUB_MARKET_POS_CTX_KEY = "hubMarketPosContext"; let chart = null; let candleSeries = null; @@ -45,6 +55,8 @@ let priceTick = null; let priceAutoScale = true; let rangeMarkers = []; + let positionLines = []; + let posContext = null; let currentPriceLine = null; let lastCandles = []; let candleByTime = {}; @@ -56,6 +68,151 @@ let currentTf = "1d"; let priceTagTimer = null; + function escHtml(s) { + return String(s || "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + + function normalizeMarketSymbol(sym) { + const s = String(sym || "").trim().toUpperCase(); + const m = s.match(/^([A-Z0-9]+)\/([A-Z0-9]+)(?::([A-Z0-9]+))?$/); + if (!m) return s; + return m[1] + "/" + m[2]; + } + + function loadPosContextFromStorage() { + try { + const raw = sessionStorage.getItem(HUB_MARKET_POS_CTX_KEY); + if (!raw) return null; + return JSON.parse(raw); + } catch (e) { + return null; + } + } + + function posContextMatches(ctx, exKey, sym) { + if (!ctx) return false; + const ctxSym = normalizeMarketSymbol(ctx.symbol || ""); + const ctxEx = String(ctx.exchange_key || "").trim(); + return ctxSym === normalizeMarketSymbol(sym) && ctxEx === String(exKey || "").trim(); + } + + function clearPosPanel() { + if (elPosPanel) elPosPanel.classList.add("hidden"); + if (elPosSide) { + elPosSide.textContent = ""; + elPosSide.className = "market-pos-side"; + } + ["entry", "sl", "tp", "size"].forEach(function (k) { + const el = { entry: elPosEntry, sl: elPosSl, tp: elPosTp, size: elPosSize }[k]; + if (el) el.textContent = "—"; + }); + if (elPosOrders) elPosOrders.innerHTML = ""; + } + + function renderPosPanel(ctx) { + if (!elPosPanel || !ctx) { + clearPosPanel(); + return; + } + elPosPanel.classList.remove("hidden"); + if (elPosSide) { + const isShort = (ctx.side || "").toLowerCase() === "short"; + elPosSide.textContent = isShort ? "空" : "多"; + elPosSide.className = "market-pos-side " + (isShort ? "side-short" : "side-long"); + } + if (elPosEntry) elPosEntry.textContent = ctx.entry != null ? fmtPrice(ctx.entry) : "—"; + if (elPosSl) elPosSl.textContent = ctx.stop_loss != null ? fmtPrice(ctx.stop_loss) : "—"; + if (elPosTp) elPosTp.textContent = ctx.take_profit != null ? fmtPrice(ctx.take_profit) : "—"; + if (elPosSize) elPosSize.textContent = ctx.contracts != null ? String(ctx.contracts) : "—"; + if (elPosOrders) { + const orders = Array.isArray(ctx.orders) ? ctx.orders : []; + if (!orders.length) { + elPosOrders.innerHTML = '暂无委托单'; + } else { + elPosOrders.innerHTML = orders + .map(function (o) { + const price = o.price != null ? fmtPrice(o.price) : "—"; + const amt = o.amount != null ? String(o.amount) : ""; + return ( + '' + + '' + + escHtml(o.kind || "") + + "" + + '' + + escHtml(o.label || "") + + "" + + '' + + price + + "" + + (amt ? '×' + escHtml(amt) + "" : "") + + "" + ); + }) + .join(""); + } + } + } + + function clearPositionLines() { + positionLines.forEach(function (m) { + try { + candleSeries.removePriceLine(m); + } catch (e) {} + }); + positionLines = []; + } + + function updatePositionLines() { + clearPositionLines(); + if (!candleSeries || !posContext) return; + const specs = [ + { price: posContext.entry, color: "#5b9cf5", title: "入场" }, + { price: posContext.stop_loss, color: "#ff4d6d", title: "止损" }, + { price: posContext.take_profit, color: "#00ff9d", title: "止盈" }, + ]; + specs.forEach(function (s) { + if (s.price == null || !Number.isFinite(Number(s.price))) return; + positionLines.push( + candleSeries.createPriceLine({ + price: Number(s.price), + color: s.color, + lineWidth: 1, + lineStyle: 2, + axisLabelVisible: true, + title: s.title, + }) + ); + }); + } + + function clearPosContext() { + posContext = null; + try { + sessionStorage.removeItem(HUB_MARKET_POS_CTX_KEY); + } catch (e) {} + clearPosPanel(); + clearPositionLines(); + } + + function applyPosContext(ctx) { + posContext = ctx; + renderPosPanel(ctx); + updatePositionLines(); + } + + function syncPosContextForView(exKey, sym) { + const stored = loadPosContextFromStorage(); + if (stored && posContextMatches(stored, exKey, sym)) { + applyPosContext(stored); + return; + } + clearPosContext(); + } + function fmtVol(v) { if (v == null || Number.isNaN(Number(v))) return "-"; const n = Number(v); @@ -553,6 +710,7 @@ } applyPriceAutoScale(); updateVisibleRangeMarkers(); + syncPosContextForView(exKey, sym); showLatestOhlcv(); const limit = data.limit || lastCandles.length; @@ -620,6 +778,11 @@ applyPriceAutoScale(); }); } + if (elPosClear) { + elPosClear.addEventListener("click", function () { + clearPosContext(); + }); + } } window.hubMarketChart = { diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 085f3a6..67ea3de 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -8,7 +8,7 @@ - + @@ -113,6 +113,17 @@ 振幅 +
@@ -193,7 +204,7 @@
- - + +
合约方向张数浮盈操作
${renderDirectionHtml(x.side)} ${fmt(x.contracts, 4)} ${fmt(x.unrealized_pnl, 2)}