From 01d26e9833f35c28982c3d5a5ef5e9a4f1aac087 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 2 Jun 2026 13:45:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B6=8B=E5=8A=BF=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E6=AD=A2=E7=9B=88=E8=AF=AF=E6=98=BE=E4=B8=8E=E8=A1=8C?= =?UTF-8?q?=E6=83=85=E5=8C=BA=E6=88=90=E4=BA=A4=E9=87=8F=E8=A2=AB=E8=A3=81?= =?UTF-8?q?=E5=88=87=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- manual_trading_hub/exchange_orders.py | 2 +- manual_trading_hub/static/app.css | 14 +- manual_trading_hub/static/app.js | 224 +++++++++++++++++++++----- manual_trading_hub/static/chart.js | 53 +++++- manual_trading_hub/static/index.html | 6 +- 5 files changed, 245 insertions(+), 54 deletions(-) diff --git a/manual_trading_hub/exchange_orders.py b/manual_trading_hub/exchange_orders.py index ca3b4f7..55cdbd0 100644 --- a/manual_trading_hub/exchange_orders.py +++ b/manual_trading_hub/exchange_orders.py @@ -166,7 +166,7 @@ def _okx_normalize_orders(raw: dict, channel: str) -> list[dict[str, Any]]: info = {} sl_trig = _coerce_float(info.get("slTriggerPx"), raw.get("stopLossPrice")) tp_trig = _coerce_float(info.get("tpTriggerPx"), raw.get("takeProfitPrice")) - if sl_trig is None or tp_trig is None: + if sl_trig is None or tp_trig is None or sl_trig == tp_trig: return [n] base_id = n["id"] rows: list[dict[str, Any]] = [] diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 485d6a0..a3dd809 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -1969,14 +1969,19 @@ body.login-page { .market-chart-wrap { display: flex; flex-direction: column; - height: min(72vh, 640px); - min-height: 360px; + height: min(76vh, 680px); + min-height: 380px; border: 1px solid var(--border-soft); border-radius: var(--radius); background: #0a1018; overflow: hidden; } +.market-chart-wrap.has-pos-panel { + height: min(80vh, 740px); + min-height: 440px; +} + .market-ohlcv-bar { flex: 0 0 auto; padding: 8px 12px; @@ -2147,6 +2152,11 @@ body.login-page { font-size: 0.68rem; } +.market-pos-tp-monitored { + color: #8fc8ff; + font-size: 0.72rem; +} + .sym-link { background: none; border: none; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index aea33a8..dae32ff 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -506,14 +506,146 @@ return (row && (row.key || row.id)) || exchangeId; } - function buildPositionMarketContext(pos, monitorOrder) { + function findTrendPlan(trends, symbol, side) { + const want = (side || "").toLowerCase(); + for (const t of trends || []) { + const sym = t.symbol || t.exchange_symbol || ""; + if (!symbolsMatchHub(sym, symbol)) continue; + const d = (t.direction || "").toLowerCase(); + if (!d || d === want) return t; + } + return null; + } + + function inferTpslFromCondOrders(side, cond, entry) { + const picked = pickExTpslOrders(cond); + let sl = picked.sl && picked.sl.trigger_price != null ? picked.sl.trigger_price : ""; + let tp = picked.tp && picked.tp.trigger_price != null ? picked.tp.trigger_price : ""; + if (sl !== "" && tp !== "" && Number(sl) !== Number(tp)) { + return { sl, tp }; + } + + const triggers = (cond || []) + .map(function (o) { + return { price: Number(o.trigger_price), label: o.label || "" }; + }) + .filter(function (o) { + return o.price != null && !Number.isNaN(o.price) && o.price > 0; + }); + if (!triggers.length) return { sl: sl || "", tp: tp || "" }; + + const s = (side || "long").toLowerCase(); + const e = entry != null && Number.isFinite(Number(entry)) ? Number(entry) : null; + + if (e != null) { + const below = triggers.filter(function (t) { + return t.price < e; + }); + const above = triggers.filter(function (t) { + return t.price > e; + }); + if (s === "long") { + if (sl === "" && below.length) { + sl = Math.max.apply( + null, + below.map(function (t) { + return t.price; + }) + ); + } + if (tp === "" && above.length) { + tp = Math.min.apply( + null, + above.map(function (t) { + return t.price; + }) + ); + } + } else { + if (sl === "" && above.length) { + sl = Math.min.apply( + null, + above.map(function (t) { + return t.price; + }) + ); + } + if (tp === "" && below.length) { + tp = Math.max.apply( + null, + below.map(function (t) { + return t.price; + }) + ); + } + } + } + + if (triggers.length === 1 && sl === "" && tp === "") { + const one = triggers[0]; + const p = one.price; + const lbl = one.label; + if (e != null) { + if (s === "long") { + if (p < e) sl = p; + else if (p > e) tp = p; + } else if (p > e) sl = p; + else if (p < e) tp = p; + } else if (/止损/.test(lbl)) sl = p; + else if (/止盈/.test(lbl) && !/止盈止损/.test(lbl)) tp = p; + } + + if (sl !== "" && tp !== "" && Number(sl) === Number(tp)) tp = ""; + return { sl: sl || "", tp: tp || "" }; + } + + function resolvePositionTpsl(pos, monitorOrder, trendPlan) { const mo = monitorOrder || {}; + const tp = trendPlan || {}; + const cond = condOrdersFromPosition(pos); + const entryRaw = + pos.entry_price != null + ? pos.entry_price + : mo.trigger_price != null + ? 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() === "趋势回调"; + + let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : ""; + let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : ""; + let tpMonitored = false; + + if (isTrend) { + tpMonitored = true; + takeProfit = ""; + if (trendPlan && trendPlan.stop_loss != null && trendPlan.stop_loss !== "") { + sl = trendPlan.stop_loss; + } + } + + const inferred = inferTpslFromCondOrders(pos.side, cond, entryN); + if (sl === "" || sl == null) sl = inferred.sl; + if (!tpMonitored && (takeProfit === "" || takeProfit == null)) takeProfit = inferred.tp; + + if (sl !== "" && takeProfit !== "" && Number(sl) === Number(takeProfit)) { + takeProfit = ""; + } + + return { + entry: entryRaw, + sl, + tp: takeProfit, + tp_monitored: tpMonitored, + is_trend: isTrend, + }; + } + + function buildPositionMarketContext(pos, monitorOrder, trendPlan) { + const tpsl = resolvePositionTpsl(pos, monitorOrder, trendPlan); 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); @@ -538,9 +670,11 @@ }); return { side: (pos.side || "long").toLowerCase(), - entry: num(entry), - stop_loss: num(sl), - take_profit: num(tp), + entry: num(tpsl.entry), + stop_loss: num(tpsl.sl), + take_profit: num(tpsl.tp), + tp_monitored: !!tpsl.tp_monitored, + is_trend: !!tpsl.is_trend, contracts: num(pos.contracts), orders: orders, }; @@ -565,10 +699,13 @@ } } - function marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder) { + function marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan) { const symAttr = esc(symbol || "").replace(/"/g, """); const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """); - const ctxEnc = esc(encodePosCtx(buildPositionMarketContext(pos, monitorOrder))).replace(/"/g, """); + const ctxEnc = esc(encodePosCtx(buildPositionMarketContext(pos, monitorOrder, trendPlan))).replace( + /"/g, + """ + ); return ( 'data-ex-id="' + esc(exchangeId) + @@ -706,18 +843,8 @@ return `${rows}
类型数量触发/价格操作
`; } - function guessTpslFromCondOrders(side, cond) { - const triggers = (cond || []) - .map((o) => o.trigger_price) - .filter((v) => v != null && !Number.isNaN(Number(v))) - .map(Number); - if (!triggers.length) return { sl: "", tp: "" }; - triggers.sort((a, b) => a - b); - const s = (side || "long").toLowerCase(); - if (s === "short") { - return { sl: triggers[triggers.length - 1], tp: triggers[0] }; - } - return { sl: triggers[0], tp: triggers[triggers.length - 1] }; + function guessTpslFromCondOrders(side, cond, entry) { + return inferTpslFromCondOrders(side, cond, entry); } function renderOrdersCollapse(exchangeId, symbol, cond, reg) { @@ -786,7 +913,7 @@ return row("止损", sl) + row("止盈", tp); } - function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder) { + function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan) { const symbol = pos.symbol || ""; const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """); const side = (pos.side || "long").toLowerCase(); @@ -795,16 +922,17 @@ const mo = monitorOrder || {}; const cond = condOrdersFromPosition(pos); const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : []; - const guess = guessTpslFromCondOrders(side, cond); + const tpsl = resolvePositionTpsl(pos, mo, trendPlan); const symAttr = esc(symbol).replace(/"/g, """); const sideAttr = esc(side).replace(/"/g, """); const contractsAttr = esc(String(pos.contracts != null ? pos.contracts : "")).replace(/"/g, """); - const slAttr = esc(String(mo.stop_loss != null ? mo.stop_loss : guess.sl)).replace(/"/g, """); - const tpAttr = esc(String(mo.take_profit != null ? mo.take_profit : guess.tp)).replace(/"/g, """); - 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 rr = calcRrRatio(side, entry, sl, tp); + const slAttr = esc(String(tpsl.sl)).replace(/"/g, """); + const tpAttr = esc(String(tpsl.tp)).replace(/"/g, """); + const entry = tpsl.entry; + const sl = tpsl.sl; + const tp = tpsl.tp; + const tpMonitored = tpsl.tp_monitored; + const rr = tpMonitored ? null : calcRrRatio(side, entry, sl, tp); const upnl = pos.unrealized_pnl; let pnlText = fmt(upnl, 2) + "U"; if (pos.notional_usdt && upnl != null && Math.abs(Number(pos.notional_usdt)) > 1e-8) { @@ -828,7 +956,7 @@ meta.push( `移动保本:${beOn ? "开" : "关"}` ); - const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder); + const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan); return `
@@ -844,8 +972,8 @@
成交价${entry != null ? fmt(entry, 4) : "—"}
止损${sl != null && sl !== "" ? fmt(sl, 4) : "—"}
-
止盈${tp != null && tp !== "" ? fmt(tp, 4) : "—"}
-
盈亏比${rr != null ? fmt(rr, 2) + ":1" : "-:1"}
+
止盈${tpMonitored ? "程序监控" : tp != null && tp !== "" ? fmt(tp, 4) : "—"}
+
盈亏比${tpMonitored ? "—" : rr != null ? fmt(rr, 2) + ":1" : "-:1"}
张数${fmt(pos.contracts, 4)}
浮盈亏${pnlText}
@@ -933,17 +1061,17 @@ .join(""); } - function renderPositionBlock(exchangeId, exchangeKey, x, monitorOrder) { + function renderPositionBlock(exchangeId, exchangeKey, x, monitorOrder, trendPlan) { const symAttr = esc(x.symbol || "").replace(/"/g, """); const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """); const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """); const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(/"/g, """); const cond = condOrdersFromPosition(x); const reg = Array.isArray(x.regular_orders) ? x.regular_orders : []; - 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); + const tpsl = resolvePositionTpsl(x, monitorOrder, trendPlan); + const slAttr = esc(String(tpsl.sl)).replace(/"/g, """); + const tpAttr = esc(String(tpsl.tp)).replace(/"/g, """); + const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, x.symbol, x, monitorOrder, trendPlan); return `
@@ -972,7 +1100,17 @@ `; inner += `
交易所持仓 · ${pos.length} 仓
`; if (pos.length) { - inner += pos.map((p) => renderPositionBlock(row.id, row.key || row.id, p, findMonitorOrder(orders, p.symbol, p.side))).join(""); + inner += pos + .map((p) => + renderPositionBlock( + row.id, + row.key || row.id, + p, + findMonitorOrder(orders, p.symbol, p.side), + findTrendPlan(trends, p.symbol, p.side) + ) + ) + .join(""); } else { inner += '
无持仓
'; } @@ -1065,7 +1203,13 @@ html += `
`; if (posCount) { pos.forEach((p) => { - html += renderLivePositionCard(row.id, row.key || row.id, p, findMonitorOrder(orders, p.symbol, p.side)); + html += renderLivePositionCard( + row.id, + row.key || row.id, + p, + findMonitorOrder(orders, p.symbol, p.side), + findTrendPlan(trends, p.symbol, p.side) + ); }); } else { html += '
暂无持仓
'; diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index b56c58f..ec0779f 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -5,6 +5,9 @@ const AUTO_REFRESH_MS = 5000; const DEFAULT_VISIBLE_BARS = 200; const RIGHT_OFFSET_BARS = 10; + const CANDLE_SCALE_BOTTOM = 0.26; + const VOLUME_SCALE_TOP = 0.73; + const VOLUME_SCALE_BOTTOM = 0.06; const TF_MS = { "1m": 60_000, "5m": 5 * 60_000, @@ -111,6 +114,30 @@ if (el) el.textContent = "—"; }); if (elPosOrders) elPosOrders.innerHTML = ""; + syncChartWrapLayout(); + } + + function resizeChart() { + if (!chart || !chartHost) return; + chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); + updatePriceTag(); + } + + let resizeChartRaf = 0; + function scheduleChartResize() { + if (resizeChartRaf) cancelAnimationFrame(resizeChartRaf); + resizeChartRaf = requestAnimationFrame(function () { + resizeChartRaf = 0; + syncChartWrapLayout(); + }); + } + + function syncChartWrapLayout() { + const wrap = chartHost && chartHost.closest(".market-chart-wrap"); + if (wrap && elPosPanel) { + wrap.classList.toggle("has-pos-panel", !elPosPanel.classList.contains("hidden")); + } + resizeChart(); } function renderPosPanel(ctx) { @@ -126,7 +153,15 @@ } 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 (elPosTp) { + if (ctx.tp_monitored) { + elPosTp.textContent = "程序监控"; + elPosTp.classList.add("market-pos-tp-monitored"); + } else { + elPosTp.textContent = ctx.take_profit != null ? fmtPrice(ctx.take_profit) : "—"; + elPosTp.classList.remove("market-pos-tp-monitored"); + } + } if (elPosSize) elPosSize.textContent = ctx.contracts != null ? String(ctx.contracts) : "—"; if (elPosOrders) { const orders = Array.isArray(ctx.orders) ? ctx.orders : []; @@ -155,6 +190,7 @@ .join(""); } } + scheduleChartResize(); } function clearPositionLines() { @@ -172,8 +208,10 @@ const specs = [ { price: posContext.entry, color: "#5b9cf5", title: "入场" }, { price: posContext.stop_loss, color: "#ff4d6d", title: "止损" }, - { price: posContext.take_profit, color: "#00ff9d", title: "止盈" }, ]; + if (!posContext.tp_monitored && posContext.take_profit != null) { + specs.push({ price: posContext.take_profit, color: "#00ff9d", title: "止盈" }); + } specs.forEach(function (s) { if (s.price == null || !Number.isFinite(Number(s.price))) return; positionLines.push( @@ -496,10 +534,10 @@ if (!volumeSeries) return false; chart.priceScale("right").applyOptions({ - scaleMargins: { top: 0.06, bottom: 0.28 }, + scaleMargins: { top: 0.06, bottom: CANDLE_SCALE_BOTTOM }, }); chart.priceScale("volume").applyOptions({ - scaleMargins: { top: 0.78, bottom: 0 }, + scaleMargins: { top: VOLUME_SCALE_TOP, bottom: VOLUME_SCALE_BOTTOM }, }); applyPriceAutoScale(); @@ -522,11 +560,9 @@ }); window.addEventListener("resize", function () { - if (!chart) return; - chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); - updatePriceTag(); + scheduleChartResize(); }); - chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight }); + scheduleChartResize(); return true; } @@ -712,6 +748,7 @@ updateVisibleRangeMarkers(); syncPosContextForView(exKey, sym); showLatestOhlcv(); + scheduleChartResize(); const limit = data.limit || lastCandles.length; let hint = diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 67ea3de..560e8a0 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -8,7 +8,7 @@ - + @@ -204,7 +204,7 @@
- - + +
合约方向张数浮盈操作