From d9b1b324f922223d519f321e454f2112ed5bc802 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 3 Jun 2026 16:41:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=AD=E6=8E=A7=E4=BF=9D?= =?UTF-8?q?=E6=9C=AC=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manual_trading_hub/hub.py | 46 +++++++++++++++++++++++++++++++ manual_trading_hub/static/app.css | 18 ++++++++++++ manual_trading_hub/static/app.js | 21 +++++++++----- 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 1fcd960..1418f6e 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -626,6 +626,51 @@ def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) -> o["sl_breakeven_secured"] = bool(op["sl_breakeven_secured"]) +def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None: + """将 price_snapshot 的已保本状态同步到 agent 持仓,供中控首页表格展示。""" + ag = agent_row.get("agent") + if not isinstance(ag, dict) or not isinstance(snap, dict): + return + positions = ag.get("positions") + if not isinstance(positions, list) or not positions: + return + order_prices = snap.get("order_prices") or [] + hub_orders = [] + if isinstance(hub_mon, dict): + hub_orders = hub_mon.get("orders") or [] + op_by_id = { + op.get("id"): op + for op in order_prices + if isinstance(op, dict) and op.get("id") is not None + } + for p in positions: + if not isinstance(p, dict): + continue + sym = p.get("symbol") or "" + side = (p.get("side") or "").lower() + matched = None + for o in hub_orders: + if not isinstance(o, dict): + continue + o_sym = o.get("exchange_symbol") or o.get("symbol") or "" + if not _symbols_match(sym, o_sym): + continue + if (o.get("direction") or "").lower() != side: + continue + matched = op_by_id.get(o.get("id")) + break + if matched is None: + for op in order_prices: + if not isinstance(op, dict): + continue + if not _symbols_match(sym, op.get("symbol") or ""): + continue + matched = op + break + if isinstance(matched, dict) and "sl_breakeven_secured" in matched: + p["sl_breakeven_secured"] = bool(matched["sl_breakeven_secured"]) + + def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None: """子代理挂单为空时,用实例 Flask 已算好的 exchange_tpsl 补全展示。""" ag = agent_row.get("agent") @@ -684,6 +729,7 @@ async def _assemble_board_row( if isinstance(hub_mon, dict): _merge_flask_order_price_fields(hub_mon, snap) _merge_flask_exchange_tpsl(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None) + _merge_flask_position_breakeven(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None) flask_ok = isinstance(hub_mon, dict) and hub_mon.get("ok") is not False raw_review = (ex.get("review_url") or "").strip() review_link = browser_url(raw_review) if raw_review else default_review_url( diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 89d8f9c..cb243f5 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -858,6 +858,24 @@ body.market-chart-fs-open { color: #4cd97f; } +.pos-breakeven-badge { + display: inline-flex; + align-items: center; + margin-left: 6px; + padding: 2px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + background: #1a3d2e; + color: #4cd97f; + vertical-align: middle; + white-space: nowrap; +} + +.data-table .td-symbol { + white-space: nowrap; +} + .hub-pos-card .pos-grid { display: grid; grid-template-columns: repeat(3, 1fr); diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index e5ad274..b3461fb 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -414,9 +414,11 @@ return calcRrRatio(side, entry, initSl || sl, tp); } - function isBreakevenSecured(side, entry, monitorOrder, cond) { + function isBreakevenSecured(side, entry, monitorOrder, cond, pos) { const mo = monitorOrder || {}; + const p = pos || {}; if (mo.sl_breakeven_secured === true || mo.sl_breakeven_secured === 1) return true; + if (p.sl_breakeven_secured === true || p.sl_breakeven_secured === 1) return true; const { sl } = pickExTpslOrders(cond); const trig = sl && sl.trigger_price != null ? Number(sl.trigger_price) : NaN; const e = Number(entry); @@ -425,6 +427,10 @@ return trig >= e; } + function breakevenBadgeHtml() { + return `已保本`; + } + async function loadMonitorBoard() { const box = document.getElementById("monitor-grid"); const showLoading = !lastMonitorRows.length; @@ -955,7 +961,7 @@ const tp = tpsl.tp; const tpMonitored = tpsl.tp_monitored; const rr = resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored); - const beSecured = isBreakevenSecured(side, entry, mo, cond); + const beSecured = isBreakevenSecured(side, entry, mo, cond, pos); 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) { @@ -979,14 +985,12 @@ meta.push( `移动保本:${beOn ? "开" : "关"}` ); - if (beSecured) { - meta.push(`已保本`); - } + const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : ""; const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan); return `
- + ${symBeBadge} ${sideCn}
@@ -1091,18 +1095,21 @@ const symAttr = esc(x.symbol || "").replace(/"/g, """); const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """); const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """); + const side = sideAttr || "long"; 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 tpsl = resolvePositionTpsl(x, monitorOrder, trendPlan); + const beSecured = isBreakevenSecured(side, tpsl.entry, monitorOrder, cond, x); 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); + const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : ""; return `
- +
合约方向张数浮盈操作
${symBeBadge} ${renderDirectionHtml(x.side)} ${fmt(x.contracts, 4)} ${fmt(x.unrealized_pnl, 2)}