From 1042f135ed0941d3bb57cc7ab5612f5337a55dbd Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 4 Jun 2026 16:23:52 +0800 Subject: [PATCH] fix(gate-bot): PnL colors, sync exchange TP/SL to plan display Color floating PnL on position cards, mirror exchange stop/take prices in the grid and DB, and purge false external-close records on monitor relink. --- crypto_monitor_gate_bot/app.py | 50 ++++++++++++++++++-- crypto_monitor_gate_bot/templates/index.html | 50 ++++++++++++++------ 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index fc34040..c6b8c44 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -54,6 +54,7 @@ from form_submit_lib import check_duplicate_submit, submit_scope_add_key, submit from order_monitor_display_lib import ( apply_order_price_display_fields, enrich_order_display_fields, + tpsl_slot_trigger_price, ) from journal_chart_lib import ( JOURNAL_CHART_DEFAULT_LIMIT, @@ -5799,7 +5800,6 @@ def api_price_snapshot(): order_rows = conn.execute( "SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'" ).fetchall() - conn.close() symbol_set = set() for r in key_rows: @@ -5927,17 +5927,51 @@ def api_price_snapshot(): except Exception: exchange_tpsl = {"sl": None, "tp": None} payload["exchange_tpsl"] = exchange_tpsl + live_sl = tpsl_slot_trigger_price(exchange_tpsl.get("sl")) + live_tp = tpsl_slot_trigger_price(exchange_tpsl.get("tp")) + disp_sl = live_sl if live_sl is not None else r["stop_loss"] + disp_tp = live_tp if live_tp is not None else r["take_profit"] + sym = r["symbol"] + payload["stop_loss_raw"] = disp_sl + payload["take_profit_raw"] = disp_tp + payload["stop_loss_display"] = ( + format_price_for_symbol(sym, disp_sl) if disp_sl not in (None, "") else "—" + ) + payload["take_profit_display"] = ( + format_price_for_symbol(sym, disp_tp) if disp_tp not in (None, "") else "—" + ) apply_order_price_display_fields( payload, direction=r["direction"], entry_price=entry, initial_stop_loss=r["initial_stop_loss"], - stop_loss=r["stop_loss"], - take_profit=r["take_profit"], + stop_loss=disp_sl, + take_profit=disp_tp, calc_rr_ratio_fn=calc_rr_ratio, exchange_tpsl=exchange_tpsl, ) order_prices.append(payload) + if live_sl is not None or live_tp is not None: + try: + cur_sl = float(r["stop_loss"] or 0) + cur_tp = float(r["take_profit"] or 0) + except (TypeError, ValueError): + cur_sl, cur_tp = 0.0, 0.0 + new_sl = live_sl if live_sl is not None else cur_sl + new_tp = live_tp if live_tp is not None else cur_tp + if (live_sl is not None and abs(new_sl - cur_sl) > 1e-12) or ( + live_tp is not None and abs(new_tp - cur_tp) > 1e-12 + ): + conn.execute( + "UPDATE order_monitors SET stop_loss=?, take_profit=? WHERE id=?", + (new_sl, new_tp, int(r["id"])), + ) + + try: + conn.commit() + except Exception: + pass + conn.close() from hub_position_metrics import build_position_marks_list @@ -6113,11 +6147,19 @@ def api_order_relink_orphan(): "msg": "未找到可恢复的历史监控记录,请在中控核对持仓或联系管理员", } ), 404 + opened_at = get_opened_at_value(row) + purged = conn.execute( + "DELETE FROM trade_records WHERE symbol=? AND direction=? AND opened_at=? AND result LIKE ?", + (symbol, direction, opened_at, "%外部平仓%"), + ).rowcount conn.execute("UPDATE order_monitors SET status='active' WHERE id=?", (int(row["id"]),)) conn.commit() oid = int(row["id"]) conn.close() - return jsonify({"ok": True, "msg": "已恢复本地监控", "order_id": oid}) + msg = "已恢复本地监控" + if purged: + msg += f"(已清除 {purged} 条误记的外部平仓记录)" + return jsonify({"ok": True, "msg": msg, "order_id": oid, "purged_trade_records": purged}) @app.route("/api/order_defaults") diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index ff676f0..02f6696 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -244,6 +244,9 @@ .pos-value.price-up{color:#4cd97f} .pos-value.price-down{color:#ff6666} .pos-value.price-flat{color:#e8ecf4} + .pos-value.pnl-profit{color:#4cd97f;font-weight:700} + .pos-value.pnl-loss{color:#ff6666;font-weight:700} + .pos-value.pnl-neutral{color:#cfd3ef;font-weight:600} .pos-footer{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:.75rem;color:#6d7689} .pos-empty{padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px} .pos-card-orphan{border-color:#6a5528;background:#1a1810} @@ -478,19 +481,11 @@
止损 - {% if o.stop_loss %} - {{ price_fmt(o.symbol, o.stop_loss) }} - {% else %} - - {% endif %} + {{ price_fmt(o.symbol, o.stop_loss) if o.stop_loss else '—' }}
止盈 - {% if o.take_profit %} - {{ price_fmt(o.symbol, o.take_profit) }} - {% else %} - - {% endif %} + {{ price_fmt(o.symbol, o.take_profit) if o.take_profit else '—' }}
盈亏比 @@ -1647,6 +1642,24 @@ function paintExchangeTpslRow(orderId, tpsl){ if(slBtn) slBtn.disabled = !(data.sl && data.sl.order_id); if(tpBtn) tpBtn.disabled = !(data.tp && data.tp.order_id); } +function paintPlanTpslDisplay(orderId, snap){ + if(!snap) return; + const card = document.getElementById(`order-row-${orderId}`); + const slEl = document.getElementById(`order-plan-sl-${orderId}`); + const tpEl = document.getElementById(`order-plan-tp-${orderId}`); + const rrEl = document.getElementById(`order-rr-${orderId}`); + const slDisp = snap.stop_loss_display; + const tpDisp = snap.take_profit_display; + if(slEl && slDisp) slEl.innerText = slDisp; + if(tpEl && tpDisp) tpEl.innerText = tpDisp; + if(card){ + if(snap.stop_loss_raw != null && snap.stop_loss_raw !== "") card.setAttribute('data-plan-sl', formatPriceForInput(snap.stop_loss_raw)); + else if(slDisp) card.setAttribute('data-plan-sl', slDisp); + if(snap.take_profit_raw != null && snap.take_profit_raw !== "") card.setAttribute('data-plan-tp', formatPriceForInput(snap.take_profit_raw)); + else if(tpDisp) card.setAttribute('data-plan-tp', tpDisp); + } + if(rrEl && typeof snap.rr_ratio !== "undefined") rrEl.innerText = formatRrRatio(snap.rr_ratio); +} function toggleTpslModalMode(){ const mode = (document.getElementById('tpsl-modal-mode')||{}).value || 'price'; const pct = mode === 'pct'; @@ -1696,6 +1709,13 @@ function submitTpslEntrust(){ alert(data.msg || '已提交'); closeTpslEntrustModal(); if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl); + paintPlanTpslDisplay(orderId, { + stop_loss_raw: data.stop_loss, + take_profit_raw: data.take_profit, + stop_loss_display: data.stop_loss != null ? formatPriceForInput(data.stop_loss) : null, + take_profit_display: data.take_profit != null ? formatPriceForInput(data.take_profit) : null, + rr_ratio: data.planned_rr, + }); refreshPriceSnapshotConditional(); }).catch(()=>alert('委托请求失败')); }; @@ -1839,14 +1859,14 @@ function refreshPriceSnapshotConditional(){ if(pnlEl){ pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`; pnlEl.classList.remove("price-up","price-down","price-flat","pnl-profit","pnl-loss","pnl-neutral"); - if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up"); - else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down"); - else pnlEl.classList.add("price-flat"); + const fp = Number(o.float_pnl); + if(fp > 0) pnlEl.classList.add("pnl-profit"); + else if(fp < 0) pnlEl.classList.add("pnl-loss"); + else pnlEl.classList.add("pnl-neutral"); } - const rrEl = document.getElementById(`order-rr-${o.id}`); - if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio); paintBreakevenBadge(o.id, o.sl_breakeven_secured); paintExchangeTpslRow(o.id, o.exchange_tpsl || {}); + paintPlanTpslDisplay(o.id, o); }); } }).catch(()=>{});