From f2bd76d970d627253dcad9c1bbff1d13b409456c Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 3 Jul 2026 21:54:35 +0800 Subject: [PATCH] Fix UI stuck after manual close: fast API return and closing state instead of SL/TP revive. Co-authored-by: Cursor --- modules/trading/install.py | 63 +++++++++++++++++++++++++++++----- modules/web/static/js/trade.js | 29 +++++++++++++++- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/modules/trading/install.py b/modules/trading/install.py index 0414944..80d4c05 100644 --- a/modules/trading/install.py +++ b/modules/trading/install.py @@ -59,6 +59,7 @@ from modules.trading.order_pending import ( from modules.core.db_conn import commit_retry, execute_retry from modules.trading.sl_tp_guard import ( cancel_monitor_exit_orders, + clear_close_pending, close_pending_active, ensure_monitor_order_columns, mark_close_pending, @@ -1170,6 +1171,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se direction: str, ) -> Optional[dict]: """只读:从已关闭监控补全止盈止损,不写库。""" + if should_skip_monitor_revive(sym, direction): + return {"symbol": sym, "direction": direction} if not mon: rsl, rtp, rtrail, rinitial = _restore_sl_tp_from_closed(conn, sym, direction) if rsl is None and rtp is None and not rtrail: @@ -1197,6 +1200,24 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se merged["initial_stop_loss"] = rinitial return merged + def _row_as_closing_state(row: dict) -> dict: + """手动/程序平仓已提交、柜台持仓未清零时的展示状态。""" + out = dict(row) + out["order_state"] = "closing" + out["source_label"] = "平仓处理中" + out["stop_loss"] = None + out["take_profit"] = None + out["sl_monitoring"] = False + out["tp_monitoring"] = False + out["sl_order_active"] = False + out["tp_order_active"] = False + out["pending_orders"] = [] + out["can_close"] = False + out["close_allowed"] = False + out["can_place_orders"] = False + out["trailing_be"] = False + return out + def _revive_closed_monitor(conn, symbol: str, direction: str) -> Optional[dict]: """柜台仍有持仓但本地监控被误关时,恢复最近一条同品种记录。""" if should_skip_monitor_revive(symbol, direction): @@ -2413,12 +2434,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se direction = p.get("direction") or "long" if not mon: mon = _find_pending_monitor(conn, ths, direction) - if not mon: + if not mon and not close_pending_active(ths, direction): if fast: mon = _find_active_monitor(conn, ths, direction) else: mon = _find_or_revive_monitor(conn, ths, direction) - if mon: + if mon and not close_pending_active(ths, direction): if fast: mon = _overlay_sl_tp_readonly(conn, mon, ths, direction) or mon else: @@ -2432,14 +2453,18 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se conn, mon.get("symbol") or ths, mon.get("direction") or direction, ) or mon mon = _restore_monitor_sl_tp_if_missing(conn, mon, ths, direction) or mon - elif fast: + elif fast and not close_pending_active(ths, direction): mon = _overlay_sl_tp_readonly(conn, None, ths, direction) + elif close_pending_active(ths, direction): + mon = mon or {"symbol": ths, "direction": direction} try: row = _compose_position_row( conn, mon=mon, ctp=p, mode=mode, capital=capital, now_iso=now_iso, fast=fast, ) if row: + if close_pending_active(ths, direction): + row = _row_as_closing_state(row) rows.append(row) except Exception as exc: logger.warning("compose ctp position row failed: %s", exc) @@ -3433,14 +3458,36 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se _close_all_monitors_for_sym_dir(conn, sym, direction) conn.commit() try: - from modules.ctp.ctp_trade_sync import sync_trade_logs_from_ctp - sync_trade_logs_from_ctp(conn, mode, capital=capital, trading_mode=mode) + on_user_initiated_close(conn, trading_day=trading_day_label()) conn.commit() except Exception as exc: - logger.debug("sync trades after close: %s", exc) + logger.debug("user initiated close hook: %s", exc) + cap_snapshot = capital conn.close() - _push_position_snapshot_async() - return jsonify({"ok": True, "message": "已平仓,交易记录已写入"}) + + def _after_close() -> None: + try: + bg = get_db() + try: + init_strategy_tables(bg) + from modules.ctp.ctp_trade_sync import sync_trade_logs_from_ctp + sync_trade_logs_from_ctp( + bg, mode, capital=cap_snapshot, trading_mode=mode, + ) + bg.commit() + finally: + bg.close() + except Exception as exc: + logger.debug("sync trades after close: %s", exc) + _push_position_snapshot_async(fast=True) + + threading.Thread(target=_after_close, daemon=True, name="close-finalize").start() + _push_position_snapshot_async(fast=True) + return jsonify({ + "ok": True, + "message": "平仓委托已提交", + "closing": True, + }) except ValueError as exc: conn.close() return jsonify({"ok": False, "error": str(exc)}), 400 diff --git a/modules/web/static/js/trade.js b/modules/web/static/js/trade.js index 82bf369..5dee4fe 100644 --- a/modules/web/static/js/trade.js +++ b/modules/web/static/js/trade.js @@ -1232,10 +1232,32 @@ ); } + function buildClosingCard(row) { + var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空'); + var openT = (row.open_time || '').replace('T', ' ').slice(0, 16); + return ( + '
' + + '
' + posSymbolTitleHtml(row, + ' ' + dirBadge + '') + '
' + + '
' + posSymbolSubHtml(row) + '
' + + '
来源 ' + + (row.source_label || '平仓处理中') + ' · 平仓委托已提交,等待柜台成交…
' + + '
' + + '
' + row.lots + ' 手
' + + '
' + fmtNum(row.entry_price) + '
' + + '
' + (row.current_price != null ? fmtNum(row.current_price) : '--') + '
' + + '
' + (openT || '--') + '
' + + '
' + ); + } + function buildPosCard(row) { if (row.order_state === 'pending') { return buildPendingOrderCard(row); } + if (row.order_state === 'closing') { + return buildClosingCard(row); + } var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : ''); var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--'; var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空'); @@ -1529,9 +1551,14 @@ } return; } - if (btn) btn.textContent = '已平仓'; + if (btn) { + btn.disabled = true; + btn.textContent = '平仓中…'; + } + startPosFastPoll(90); pollPositions(); }).catch(function () { + alert('平仓请求失败,请查看持仓是否已变化'); if (btn) { btn.disabled = false; btn.textContent = '平仓';