From 0b8f410fbe34ef22a13c43c0b8549f67c4dbd733 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 2 Jul 2026 22:19:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=AD=E6=8E=A7=E6=8C=81=E4=BB=93?= =?UTF-8?q?=E5=8D=A1=E5=90=88=E5=B9=B6=E6=9C=80=E6=96=B0=E9=A3=8E=E9=99=A9?= =?UTF-8?q?=E4=B8=8E=E4=BF=9D=E8=AF=81=E9=87=91=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- manual_trading_hub/hub.py | 168 ++++++++++++++++++++----------- manual_trading_hub/static/app.js | 70 +++++++++++-- 2 files changed, 172 insertions(+), 66 deletions(-) diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index a42d4dc..47b5c9d 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -1658,6 +1658,97 @@ def _exchange_tpsl_from_hub_order(hub_orders: list, symbol: str, side: str) -> d return None +def _order_price_op_indexes(order_prices: list) -> tuple[dict, list]: + """price_snapshot order_prices:id 可能为 int/str,需双键索引。""" + by_id: dict = {} + flat: list = [] + for op in order_prices: + if not isinstance(op, dict): + continue + flat.append(op) + oid = op.get("id") + if oid is None: + continue + by_id[oid] = op + by_id[str(oid)] = op + try: + by_id[int(oid)] = op + except (TypeError, ValueError): + pass + return by_id, flat + + +def _match_order_price_op( + order_row: dict, + by_id: dict, + order_prices: list, +) -> dict | None: + if not isinstance(order_row, dict): + return None + oid = order_row.get("id") + if oid is not None: + for key in (oid, str(oid)): + op = by_id.get(key) + if isinstance(op, dict): + return op + try: + op = by_id.get(int(oid)) + if isinstance(op, dict): + return op + except (TypeError, ValueError): + pass + sym = order_row.get("exchange_symbol") or order_row.get("symbol") or "" + direction = (order_row.get("direction") or "").lower() + for op in order_prices: + if not isinstance(op, dict): + continue + if not _symbols_match(sym, op.get("symbol") or ""): + continue + op_dir = (op.get("direction") or "").lower() + if direction and op_dir and direction != op_dir: + continue + return op + return None + + +_ORDER_PRICE_MERGE_KEYS = ( + "stop_loss", + "take_profit", + "stop_loss_display", + "take_profit_display", + "display_rr_ratio", + "latest_risk_amount", + "contracts", + "exchange_initial_margin", + "plan_margin", + "time_close_enabled", + "time_close_hours", + "time_close_at_ms", + "time_close_label", + "time_close_countdown", + "time_close_remaining_sec", +) + + +def _apply_order_price_op_fields(target: dict, op: dict) -> None: + if not isinstance(target, dict) or not isinstance(op, dict): + return + if op.get("rr_ratio") is not None: + target["rr_ratio"] = op["rr_ratio"] + if "sl_breakeven_secured" in op: + target["sl_breakeven_secured"] = bool(op["sl_breakeven_secured"]) + for key in _ORDER_PRICE_MERGE_KEYS: + if key not in op: + continue + val = op[key] + if key == "latest_risk_amount": + if val is not None and val != "": + target[key] = val + continue + if val not in (None, ""): + target[key] = val + + def _find_exchange_tpsl_for_position( symbol: str, side: str, @@ -1665,11 +1756,7 @@ def _find_exchange_tpsl_for_position( hub_orders: list, ) -> dict | None: side_l = (side or "").lower() - op_by_id = { - op.get("id"): op - for op in order_prices - if isinstance(op, dict) and op.get("id") is not None - } + by_id, flat = _order_price_op_indexes(order_prices) for o in hub_orders: if not isinstance(o, dict): continue @@ -1678,13 +1765,13 @@ def _find_exchange_tpsl_for_position( continue if (o.get("direction") or "").lower() != side_l: continue - op = op_by_id.get(o.get("id")) + op = _match_order_price_op(o, by_id, flat) if not isinstance(op, dict): continue et = op.get("exchange_tpsl") if isinstance(et, dict) and (et.get("sl") or et.get("tp")): return et - for op in order_prices: + for op in flat: if not isinstance(op, dict): continue if not _symbols_match(symbol, op.get("symbol") or ""): @@ -1700,47 +1787,21 @@ def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) -> if not isinstance(hub_mon, dict) or not isinstance(snap, dict): return order_prices = snap.get("order_prices") or [] - op_by_id = { - op.get("id"): op - for op in order_prices - if isinstance(op, dict) and op.get("id") is not None - } + by_id, flat = _order_price_op_indexes(order_prices) orders = hub_mon.get("orders") or [] if not isinstance(orders, list): return for o in orders: if not isinstance(o, dict): continue - op = op_by_id.get(o.get("id")) + op = _match_order_price_op(o, by_id, flat) if not isinstance(op, dict): continue - if op.get("rr_ratio") is not None: - o["rr_ratio"] = op["rr_ratio"] - if "sl_breakeven_secured" in op: - o["sl_breakeven_secured"] = bool(op["sl_breakeven_secured"]) - for key in ( - "stop_loss", - "take_profit", - "stop_loss_display", - "take_profit_display", - "display_rr_ratio", - "latest_risk_amount", - "contracts", - "exchange_initial_margin", - "plan_margin", - "time_close_enabled", - "time_close_hours", - "time_close_at_ms", - "time_close_label", - "time_close_countdown", - "time_close_remaining_sec", - ): - if key in op and op[key] not in (None, ""): - o[key] = op[key] + _apply_order_price_op_fields(o, op) def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None: - """将 price_snapshot 的已保本状态同步到 agent 持仓,供中控首页表格展示。""" + """将 price_snapshot 的已保本、最新风险、保证金等同步到 agent 持仓。""" ag = agent_row.get("agent") if not isinstance(ag, dict) or not isinstance(snap, dict): return @@ -1748,14 +1809,10 @@ def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon: if not isinstance(positions, list) or not positions: return order_prices = snap.get("order_prices") or [] + by_id, flat = _order_price_op_indexes(order_prices) 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 @@ -1770,18 +1827,22 @@ def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon: continue if (o.get("direction") or "").lower() != side: continue - matched = op_by_id.get(o.get("id")) - break + matched = _match_order_price_op(o, by_id, flat) + if isinstance(matched, dict): + break + if o.get("latest_risk_amount") is not None or o.get("exchange_initial_margin") is not None: + matched = o + break if matched is None: - for op in order_prices: + for op in flat: 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"]) + if isinstance(matched, dict): + _apply_order_price_op_fields(p, matched) def _agent_position_has_mark(p: dict) -> bool: @@ -1809,10 +1870,10 @@ def _find_matched_order_price_op( p: dict, order_prices: list, hub_orders: list, - op_by_id: dict, ) -> dict | None: sym = p.get("symbol") or "" side = (p.get("side") or "").lower() + by_id, flat = _order_price_op_indexes(order_prices) for o in hub_orders: if not isinstance(o, dict): continue @@ -1821,11 +1882,11 @@ def _find_matched_order_price_op( continue if (o.get("direction") or "").lower() != side: continue - matched = op_by_id.get(o.get("id")) + matched = _match_order_price_op(o, by_id, flat) if isinstance(matched, dict): return matched break - for op in order_prices: + for op in flat: if not isinstance(op, dict): continue if not _symbols_match(sym, op.get("symbol") or ""): @@ -1848,15 +1909,10 @@ def _merge_flask_position_mark_price( 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) or _agent_position_has_mark(p): continue - matched = _find_matched_order_price_op(p, order_prices, hub_orders, op_by_id) + matched = _find_matched_order_price_op(p, order_prices, hub_orders) if isinstance(matched, dict): _apply_agent_mark_price( p, diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 40d190b..a8e6279 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -706,11 +706,17 @@ return null; } - function resolveTrendSizingFooter(mo, trendPlan, isTrend) { + function resolveTrendSizingFooter(mo, trendPlan, isTrend, pos) { const m = mo || {}; + const p = pos || {}; if (!isTrend || !trendPlan || !trendPlan.id) { return { - margin: m.exchange_initial_margin ?? m.plan_margin ?? null, + margin: + m.exchange_initial_margin ?? + p.exchange_initial_margin ?? + m.plan_margin ?? + p.plan_margin ?? + null, leverage: m.leverage, planBase: m.margin_capital, positionRatio: m.position_ratio, @@ -788,15 +794,59 @@ hubHoldDurationTimer = setInterval(tickHubHoldDurations, 1000); } - function formatLatestRiskMeta(mo, trendPlan) { + function estimateLatestRiskUsdt(side, entry, sl, pos, mo) { + const e = Number(entry); + const s = Number(sl); + if (!Number.isFinite(e) || !Number.isFinite(s) || e <= 0) return null; + const sd = (side || "long").toLowerCase(); + const rf = sd === "short" ? (s - e) / e : (e - s) / e; + if (!Number.isFinite(rf)) return null; + if (rf <= 0) return 0; + const m = mo || {}; + const p = pos || {}; + let notional = Number(p.notional_usdt); + if (!Number.isFinite(notional) || notional <= 0) { + notional = Number(m.exchange_notional); + } + if (!Number.isFinite(notional) || notional <= 0) { + const mc = Number(m.margin_capital); + const lev = Number(m.leverage); + if (Number.isFinite(mc) && mc > 0 && Number.isFinite(lev) && lev > 0) { + notional = mc * lev; + } + } + if (!Number.isFinite(notional) || notional <= 0) { + const c = Math.abs(Number(p.contracts)); + const cs = Number(p.contract_size); + const mult = Number.isFinite(cs) && cs > 0 ? cs : 1; + const px = Number(p.mark_price); + const mark = Number.isFinite(px) && px > 0 ? px : e; + if (Number.isFinite(c) && c > 0) notional = c * mult * mark; + } + if (!Number.isFinite(notional) || notional <= 0) return null; + return Math.round(notional * rf * 100) / 100; + } + + function formatLatestRiskMeta(mo, trendPlan, pos, tpsl) { const m = mo || {}; const t = trendPlan || {}; - const v = + let v = m.latest_risk_amount != null && m.latest_risk_amount !== "" ? Number(m.latest_risk_amount) - : t.latest_risk_amount != null && t.latest_risk_amount !== "" - ? Number(t.latest_risk_amount) - : null; + : pos && pos.latest_risk_amount != null && pos.latest_risk_amount !== "" + ? Number(pos.latest_risk_amount) + : t.latest_risk_amount != null && t.latest_risk_amount !== "" + ? Number(t.latest_risk_amount) + : null; + if ((v == null || !Number.isFinite(v)) && tpsl && pos) { + v = estimateLatestRiskUsdt( + pos.side || m.direction, + tpsl.entry, + tpsl.sl, + pos, + m + ); + } if (v != null && Number.isFinite(v)) { return `最新风险: ${fmt(v, 2)}U`; } @@ -2837,7 +2887,7 @@ const upnl = resolveTrendFloatingPnl(pos, trendPlan); const pnlFmt = formatFloatingPnlText(upnl, pos.notional_usdt); const pnlText = pnlFmt.text; - const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend); + const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend, pos); const openMeta = resolvePositionOpenMeta(mo, trendPlan, isTrend); const marginText = sizingFoot.margin != null && sizingFoot.margin !== "" && Number.isFinite(Number(sizingFoot.margin)) @@ -2855,7 +2905,7 @@ meta.push(monitorOrderSourceHtml(mo, trendPlan)); const riskLine = formatMonitorRiskMeta(mo, trendPlan); if (riskLine) meta.push(riskLine); - const latestRiskLine = formatLatestRiskMeta(mo, trendPlan); + const latestRiskLine = formatLatestRiskMeta(mo, trendPlan, pos, tpsl); if (latestRiskLine) meta.push(latestRiskLine); if (trendPlan && trendPlan.id) { const zone = @@ -2875,7 +2925,7 @@ else meta.push("风格: —"); const riskLine = formatMonitorRiskMeta(mo, trendPlan); if (riskLine) meta.push(riskLine); - const latestRiskLine = formatLatestRiskMeta(mo, trendPlan); + const latestRiskLine = formatLatestRiskMeta(mo, trendPlan, pos, tpsl); if (latestRiskLine) meta.push(latestRiskLine); const beOn = mo.breakeven_enabled === 1 || mo.breakeven_enabled === true; meta.push(