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 `
| 合约 | 方向 | 张数 | 浮盈 | 操作 |
|---|---|---|---|---|
| + | ${renderDirectionHtml(x.side)} | ${fmt(x.contracts, 4)} | ${fmt(x.unrealized_pnl, 2)} | @@ -886,7 +972,7 @@ `; inner += `