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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 @@
|
||||
</details>`;
|
||||
}
|
||||
|
||||
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 `<div class="pos-ex-order-row"><span class="pos-ex-order-main">${label}:—</span></div>`;
|
||||
}
|
||||
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"
|
||||
? `<button type="button" class="pos-ex-cancel-btn btn-cancel-order" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-order-id="${oid}" data-channel="${ch}">撤单</button>`
|
||||
: "";
|
||||
const planHint = o.channel === "plan" ? '<span class="pos-ex-plan-hint">(下单监控)</span>' : "";
|
||||
return `<div class="pos-ex-order-row">
|
||||
<span class="pos-ex-order-main">${label}:触发 ${trig} · 数量 ${fmt(o.amount, 4)}</span>
|
||||
<button type="button" class="pos-ex-cancel-btn btn-cancel-order" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-order-id="${oid}" data-channel="${ch}">撤单</button>
|
||||
<span class="pos-ex-order-main">${label}:触发 ${trig} · 数量 ${fmt(o.amount, 4)}${planHint}</span>
|
||||
${cancelBtn}
|
||||
</div>`;
|
||||
}
|
||||
return row("止损", sl) + row("止盈", tp);
|
||||
@@ -1884,7 +1933,7 @@
|
||||
</div>
|
||||
<div class="pos-ex-orders">
|
||||
<div class="pos-ex-orders-title">交易所止盈止损</div>
|
||||
${renderExTpslRows(exchangeId, symbol, cond, tickMap)}
|
||||
${renderExTpslRows(exchangeId, symbol, cond, tickMap, tpsl, pos.contracts)}
|
||||
</div>
|
||||
${renderOrdersCollapse(exchangeId, symbol, cond, reg, tickMap)}
|
||||
</div>`;
|
||||
|
||||
@@ -250,6 +250,6 @@
|
||||
<div id="toast"></div>
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script src="/assets/chart.js?v=20260604-market-pnl-sl-drag"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-inst-theme"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-handoff-tpsl"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
- **价格轴**:「自动」切换是否跟随最新价缩放。
|
||||
- **技术指标**(可选勾选):EMA 21/55、MACD、RSI(含 30/70 参考线);副图自上而下为 MACD、RSI。
|
||||
- **持仓标记**(从监控跳转时):展示入场、止损、止盈、张数、**浮盈亏**(约 5 秒随监控快照刷新)、委托摘要;K 线上绘制对应价格线。趋势回调若止盈为程序监控,止盈栏显示「程序监控」且不与止损同价误显。
|
||||
- **趋势保本移交**:移交到下单监控后,持仓卡止盈/止损与「交易所止盈止损」与实例 **下单监控** 计划价一致(不再清空为程序监控占位);交易所仅市价只减仓单时也会按价格推断展示。
|
||||
- **拖动止损线**:鼠标靠近红色止损线(⟷)可上下拖动;松手确认后调用与监控区相同的 **挂止盈/止损** API(先撤全部条件单再挂新止损+止盈)。须已有有效止盈价(交易所条件单或计划止盈);仅改止损、不改止盈时止盈价沿用当前上下文。
|
||||
- **背离**:MACD/RSI 与价格简易背离标注(箭头 + 图例说明)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user