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 `