From ed0805538f4c44bb81ec187361bd3f1a484c7d1c Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 4 Jun 2026 19:39:11 +0800 Subject: [PATCH] fix(hub): sync TP/SL display after trend handoff to order monitor Use order monitor plan prices on handoff cards and fill exchange TP/SL rows when Gate shows reduce-only orders without algo labels. Co-authored-by: Cursor --- manual_trading_hub/hub.py | 51 +++++++++++++++++-- manual_trading_hub/static/app.js | 73 +++++++++++++++++++++++----- manual_trading_hub/static/index.html | 2 +- manual_trading_hub/行情区说明.md | 1 + 4 files changed, 109 insertions(+), 18 deletions(-) diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 4d80305..619a341 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -697,28 +697,59 @@ def _tpsl_slots_to_conditional_orders(exchange_tpsl: dict, symbol: str) -> list[ if not isinstance(slot, dict): continue trig = slot.get("trigger_price") - oid = slot.get("order_id") - if trig is None or oid is None: + if trig is None: continue try: trig_f = float(trig) except (TypeError, ValueError): continue + oid = slot.get("order_id") out.append( { - "id": str(oid), + "id": str(oid) if oid is not None else "", "symbol": symbol, "channel": "algo", "category": "conditional", "label": f"{label} {trig_f:g}", "trigger_price": trig_f, - "amount": None, + "amount": slot.get("amount"), "status": "open", } ) return out +def _exchange_tpsl_from_hub_order(hub_orders: list, symbol: str, side: str) -> dict | None: + """趋势保本移交后:用下单监控计划价补全 exchange_tpsl(与实例页一致)。""" + side_l = (side or "").lower() + 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(symbol, o_sym): + continue + if (o.get("direction") or "").lower() != side_l: + continue + sl = o.get("stop_loss") + tp = o.get("take_profit") + if sl in (None, "") and tp in (None, ""): + continue + slots: dict = {"sl": None, "tp": None} + if sl not in (None, ""): + try: + slots["sl"] = {"trigger_price": float(sl), "order_id": None} + except (TypeError, ValueError): + pass + if tp not in (None, ""): + try: + slots["tp"] = {"trigger_price": float(tp), "order_id": None} + except (TypeError, ValueError): + pass + if slots["sl"] or slots["tp"]: + return slots + return None + + def _find_exchange_tpsl_for_position( symbol: str, side: str, @@ -946,12 +977,22 @@ def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict sym = p.get("symbol") or "" side = p.get("side") or "" et = _find_exchange_tpsl_for_position(sym, side, order_prices, hub_orders) + if not et: + et = _exchange_tpsl_from_hub_order(hub_orders, sym, side) if not et: continue p["exchange_tpsl"] = et cond = p.get("conditional_orders") or [] + merged = _tpsl_slots_to_conditional_orders(et, sym) if not cond: - p["conditional_orders"] = _tpsl_slots_to_conditional_orders(et, sym) + p["conditional_orders"] = merged + elif merged: + labels = {str(c.get("label") or "") for c in cond if isinstance(c, dict)} + for row in merged: + lbl = str(row.get("label") or "") + if lbl and not any(lbl in x or x in lbl for x in labels): + cond.append(row) + p["conditional_orders"] = cond async def _fetch_exchange_flask_bundle( diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index eac01ae..bf261ec 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -455,6 +455,11 @@ return side || "—"; } + function isTrendHandoffOrder(monitorOrder) { + const mo = monitorOrder || {}; + return String(mo.trade_style || "").toLowerCase() === "trend_pullback_handoff"; + } + function isTrendContext(monitorOrder, trendPlan) { const mo = monitorOrder || {}; const tp = trendPlan || {}; @@ -1176,20 +1181,36 @@ return null; } + function orderTriggerOrPrice(o) { + if (!o) return null; + if (o.trigger_price != null && o.trigger_price !== "") { + const t = Number(o.trigger_price); + if (Number.isFinite(t) && t > 0) return t; + } + if (o.price != null && o.price !== "") { + const p = Number(o.price); + if (Number.isFinite(p) && p > 0) return p; + } + 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 : ""; + let sl = picked.sl ? orderTriggerOrPrice(picked.sl) : ""; + let tp = picked.tp ? orderTriggerOrPrice(picked.tp) : ""; + if (sl !== "" && sl != null) sl = Number(sl); + if (tp !== "" && tp != null) tp = Number(tp); 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 || "" }; + const px = orderTriggerOrPrice(o); + return px == null ? null : { price: px, label: o.label || "" }; }) .filter(function (o) { - return o.price != null && !Number.isNaN(o.price) && o.price > 0; + return o != null; }); if (!triggers.length) return { sl: sl || "", tp: tp || "" }; @@ -1270,12 +1291,15 @@ : tp.avg_entry_price; const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null; const isTrend = isTrendContext(mo, trendPlan); + const handoff = isTrendHandoffOrder(mo); 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) { + if (handoff) { + tpMonitored = false; + } else if (isTrend) { tpMonitored = true; if (trendPlan && trendPlan.stop_loss != null && trendPlan.stop_loss !== "") { sl = trendPlan.stop_loss; @@ -1301,6 +1325,7 @@ tp: takeProfit, tp_monitored: tpMonitored, is_trend: isTrend, + is_handoff: handoff, }; } @@ -1568,6 +1593,18 @@ `; } + function syntheticExTpslOrder(role, price, amount) { + if (price == null || price === "" || !Number.isFinite(Number(price))) return null; + return { + label: role === "sl" ? "止损" : "止盈", + trigger_price: Number(price), + price: Number(price), + amount: amount != null ? amount : null, + id: "", + channel: "plan", + }; + } + function pickExTpslOrders(cond) { let sl = cond.find((o) => /^止损\b/.test(o.label || "")); let tp = cond.find((o) => /^止盈\b/.test(o.label || "") && !(o.label || "").includes("止盈止损")); @@ -1586,20 +1623,32 @@ return { sl, tp }; } - function renderExTpslRows(exchangeId, symbol, cond, tickMap) { + function renderExTpslRows(exchangeId, symbol, cond, tickMap, resolvedTpsl, contracts) { const symAttr = esc(symbol || "").replace(/"/g, """); - const { sl, tp } = pickExTpslOrders(cond); + let { sl, tp } = pickExTpslOrders(cond); + const plan = resolvedTpsl || {}; + if (!sl && plan.sl != null && plan.sl !== "") { + sl = syntheticExTpslOrder("sl", plan.sl, contracts); + } + if (!tp && plan.tp != null && plan.tp !== "") { + tp = syntheticExTpslOrder("tp", plan.tp, contracts); + } function row(label, o) { if (!o) { return `
${label}:—
`; } const oid = esc(o.id || "").replace(/"/g, """); const ch = esc(o.channel || "regular").replace(/"/g, """); - const trig = - o.trigger_price != null ? fmtSymbolPrice(o.trigger_price, symbol, tickMap) : "—"; + const px = orderTriggerOrPrice(o); + const trig = px != null ? fmtSymbolPrice(px, symbol, tickMap) : "—"; + const cancelBtn = + oid && o.channel !== "plan" + ? `` + : ""; + const planHint = o.channel === "plan" ? '(下单监控)' : ""; return `
- ${label}:触发 ${trig} · 数量 ${fmt(o.amount, 4)} - + ${label}:触发 ${trig} · 数量 ${fmt(o.amount, 4)}${planHint} + ${cancelBtn}
`; } return row("止损", sl) + row("止盈", tp); @@ -1884,7 +1933,7 @@
交易所止盈止损
- ${renderExTpslRows(exchangeId, symbol, cond, tickMap)} + ${renderExTpslRows(exchangeId, symbol, cond, tickMap, tpsl, pos.contracts)}
${renderOrdersCollapse(exchangeId, symbol, cond, reg, tickMap)} `; diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 090750e..7f2bd6b 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -250,6 +250,6 @@
- + diff --git a/manual_trading_hub/行情区说明.md b/manual_trading_hub/行情区说明.md index 3cd402a..ab1ad58 100644 --- a/manual_trading_hub/行情区说明.md +++ b/manual_trading_hub/行情区说明.md @@ -68,6 +68,7 @@ - **价格轴**:「自动」切换是否跟随最新价缩放。 - **技术指标**(可选勾选):EMA 21/55、MACD、RSI(含 30/70 参考线);副图自上而下为 MACD、RSI。 - **持仓标记**(从监控跳转时):展示入场、止损、止盈、张数、**浮盈亏**(约 5 秒随监控快照刷新)、委托摘要;K 线上绘制对应价格线。趋势回调若止盈为程序监控,止盈栏显示「程序监控」且不与止损同价误显。 +- **趋势保本移交**:移交到下单监控后,持仓卡止盈/止损与「交易所止盈止损」与实例 **下单监控** 计划价一致(不再清空为程序监控占位);交易所仅市价只减仓单时也会按价格推断展示。 - **拖动止损线**:鼠标靠近红色止损线(⟷)可上下拖动;松手确认后调用与监控区相同的 **挂止盈/止损** API(先撤全部条件单再挂新止损+止盈)。须已有有效止盈价(交易所条件单或计划止盈);仅改止损、不改止盈时止盈价沿用当前上下文。 - **背离**:MACD/RSI 与价格简易背离标注(箭头 + 图例说明)。