From e3b640b35c42783492ad499ade65cf949041e438 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 3 Jul 2026 22:58:14 +0800 Subject: [PATCH] Fix post-close UI: persist closing state in DB, defer trade log until CTP fill. Co-authored-by: Cursor --- modules/trading/install.py | 45 +++++--------- modules/trading/sl_tp_guard.py | 105 +++++++++++++++++++++++++++++---- 2 files changed, 109 insertions(+), 41 deletions(-) diff --git a/modules/trading/install.py b/modules/trading/install.py index 0954fdb..c919f9b 100644 --- a/modules/trading/install.py +++ b/modules/trading/install.py @@ -66,6 +66,7 @@ from modules.trading.sl_tp_guard import ( monitor_order_status, monitor_source_label, place_monitor_exit_orders, + position_close_in_progress, reconcile_monitors_without_position, should_skip_monitor_revive, start_sl_tp_guard_worker, @@ -1234,8 +1235,6 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se break if should_skip_monitor_revive(symbol, direction) and not ctp_still_open: return None - if ctp_still_open and close_pending_active(symbol, direction): - clear_close_pending(symbol, direction) for r in conn.execute( "SELECT * FROM trade_order_monitors WHERE status='closed' ORDER BY id DESC LIMIT 40" ).fetchall(): @@ -2447,12 +2446,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 and not close_pending_active(ths, direction): + if not mon and not position_close_in_progress(conn, mode, ths, direction): if fast: mon = _find_active_monitor(conn, ths, direction) else: mon = _find_or_revive_monitor(conn, ths, direction) - if mon and not close_pending_active(ths, direction): + if mon and not position_close_in_progress(conn, mode, ths, direction): if fast: mon = _overlay_sl_tp_readonly(conn, mon, ths, direction) or mon else: @@ -2466,9 +2465,9 @@ 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 and not close_pending_active(ths, direction): + elif fast and not position_close_in_progress(conn, mode, ths, direction): mon = _overlay_sl_tp_readonly(conn, None, ths, direction) - elif close_pending_active(ths, direction): + elif position_close_in_progress(conn, mode, ths, direction): mon = mon or {"symbol": ths, "direction": direction} try: row = _compose_position_row( @@ -2476,7 +2475,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se now_iso=now_iso, fast=fast, ) if row: - if close_pending_active(ths, direction): + if position_close_in_progress(conn, mode, ths, direction): row = _row_as_closing_state(row) rows.append(row) except Exception as exc: @@ -3404,14 +3403,9 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se if not sym or price <= 0: conn.close() return jsonify({"ok": False, "error": "品种或价格无效"}), 400 - if close_pending_active(sym, direction): + if close_pending_active(sym, direction, conn=conn): conn.close() return jsonify({"ok": False, "error": "平仓处理中,请稍候查看柜台委托"}), 400 - from modules.trading.sl_tp_guard import _has_pending_close_order - if _has_pending_close_order(mode, sym, direction): - mark_close_pending(sym, direction) - conn.close() - return jsonify({"ok": False, "error": "已有平仓委托在柜台排队,请勿重复提交"}), 400 offset = "close_long" if direction == "long" else "close_short" capital = _capital(conn) mon = None @@ -3434,6 +3428,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se mon = row mid = int(row["id"]) break + from modules.trading.sl_tp_guard import _has_pending_close_order + if _has_pending_close_order(mode, sym, direction): + mark_close_pending(sym, direction, conn=conn, monitor_id=mid) + conn.commit() + conn.close() + return jsonify({"ok": False, "error": "已有平仓委托在柜台排队,请勿重复提交"}), 400 entry = float(mon.get("entry_price") or 0) if mon else 0.0 if entry <= 0: for p in _ctp_positions(mode): @@ -3450,24 +3450,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se lots=lots, price=price, settings=_settings_dict(), order_type="market", urgency="stop_loss", ) - mark_close_pending(sym, direction) - # 始终写本地记录:CTP 同步依赖内存开平配对,重启后或成交回报延迟时会漏记 - write_manual_close_trade_log( - conn, - mon, - symbol=sym, - direction=direction, - lots=lots, - close_price=price, - entry_price=entry or price, - trading_mode=mode, - capital=capital, - stop_loss=float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None, - take_profit=float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None, - open_time=(mon.get("open_time") or "") if mon else "", - symbol_name=(mon.get("symbol_name") or "") if mon else "", - market_code=(mon.get("market_code") or "") if mon else "", - ) + mark_close_pending(sym, direction, conn=conn, monitor_id=mid) if mon: cancel_monitor_exit_orders(conn, mon, mode=mode) conn.commit() diff --git a/modules/trading/sl_tp_guard.py b/modules/trading/sl_tp_guard.py index a1fdfdc..3ab6377 100644 --- a/modules/trading/sl_tp_guard.py +++ b/modules/trading/sl_tp_guard.py @@ -60,6 +60,7 @@ MONITOR_ORDER_COLUMNS = ( "ALTER TABLE trade_order_monitors ADD COLUMN vt_order_id TEXT", "ALTER TABLE trade_order_monitors ADD COLUMN order_price REAL", "ALTER TABLE trade_order_monitors ADD COLUMN open_fee REAL", + "ALTER TABLE trade_order_monitors ADD COLUMN close_pending_until REAL", ) TRADE_RESULTS = ("止损", "止盈", "移动止盈", "保本止盈", "手动平仓") @@ -77,7 +78,7 @@ def _monitor_columns_exist(conn) -> bool: cols.add(r.get("name") or "") else: cols.add(r[1]) - return "open_fee" in cols + return "close_pending_until" in cols except Exception: return False @@ -181,29 +182,111 @@ def _position_key(sym: str, direction: str) -> str: return f"{(sym or '').strip().lower()}|{(direction or 'long').strip().lower()}" -def mark_close_pending(sym: str, direction: str, *, secs: int = CLOSE_PENDING_SEC) -> None: +def mark_close_pending( + sym: str, + direction: str, + *, + secs: int = CLOSE_PENDING_SEC, + conn=None, + monitor_id: int = 0, +) -> None: key = _position_key(sym, direction) + until = time.time() + max(30, int(secs)) with _closing_lock: - _close_pending_until[key] = time.time() + max(30, int(secs)) + _close_pending_until[key] = until + if conn is None: + return + try: + mid = int(monitor_id or 0) + if mid > 0: + conn.execute( + "UPDATE trade_order_monitors SET close_pending_until=? WHERE id=?", + (until, mid), + ) + return + direction = (direction or "long").strip().lower() + for r in conn.execute( + "SELECT id, symbol, direction FROM trade_order_monitors " + "WHERE status IN ('active', 'closed')" + ): + if (r["direction"] or "long").strip().lower() != direction: + continue + if _match_symbol(sym, r["symbol"] or ""): + conn.execute( + "UPDATE trade_order_monitors SET close_pending_until=? WHERE id=?", + (until, int(r["id"])), + ) + except Exception as exc: + logger.debug("persist close_pending: %s", exc) -def clear_close_pending(sym: str, direction: str) -> None: +def clear_close_pending(sym: str, direction: str, conn=None) -> None: key = _position_key(sym, direction) with _closing_lock: _close_pending_until.pop(key, None) + if conn is None: + return + try: + direction = (direction or "long").strip().lower() + for r in conn.execute( + "SELECT id, symbol, direction FROM trade_order_monitors " + "WHERE close_pending_until IS NOT NULL" + ): + if (r["direction"] or "long").strip().lower() != direction: + continue + if _match_symbol(sym, r["symbol"] or ""): + conn.execute( + "UPDATE trade_order_monitors SET close_pending_until=NULL WHERE id=?", + (int(r["id"]),), + ) + except Exception as exc: + logger.debug("clear close_pending db: %s", exc) -def close_pending_active(sym: str, direction: str) -> bool: +def close_pending_active(sym: str, direction: str, conn=None) -> bool: key = _position_key(sym, direction) + now = time.time() with _closing_lock: until = float(_close_pending_until.get(key) or 0) - if until > time.time(): + if until > now: return True if until: _close_pending_until.pop(key, None) + if conn is None: + return False + try: + direction = (direction or "long").strip().lower() + for r in conn.execute( + "SELECT id, symbol, direction, close_pending_until FROM trade_order_monitors " + "WHERE close_pending_until IS NOT NULL AND close_pending_until > ?", + (now,), + ): + if (r["direction"] or "long").strip().lower() != direction: + continue + if not _match_symbol(sym, r["symbol"] or ""): + continue + db_until = float(r["close_pending_until"] or 0) + if db_until > now: + with _closing_lock: + _close_pending_until[key] = db_until + return True + except Exception as exc: + logger.debug("read close_pending db: %s", exc) return False +def position_close_in_progress( + conn, + mode: str, + sym: str, + direction: str, +) -> bool: + """平仓委托已提交或柜台仍有平仓挂单(含进程重启后)。""" + if close_pending_active(sym, direction, conn=conn): + return True + return _has_pending_close_order(mode, sym, direction) + + def should_skip_monitor_revive(sym: str, direction: str) -> bool: return close_pending_active(sym, direction) @@ -804,7 +887,7 @@ def reconcile_monitors_without_position(conn, mode: str, *, grace_sec: int = 120 cancel_monitor_exit_orders(conn, mon, mode=mode) except Exception as exc: logger.warning("cancel exit orders monitor=%s: %s", mon.get("id"), exc) - clear_close_pending(ms, md) + clear_close_pending(ms, md, conn=conn) conn.execute("UPDATE trade_order_monitors SET status='closed' WHERE id=?", (mon["id"],)) closed += 1 if closed: @@ -843,12 +926,12 @@ def _execute_local_close( float(margin_raw), ) return - clear_close_pending(sym, direction) + clear_close_pending(sym, direction, conn=conn) _close_all_monitors_for_symbol(conn, sym, direction) reconcile_monitors_without_position(conn, mode) return if _has_pending_close_order(mode, sym, direction): - mark_close_pending(sym, direction) + mark_close_pending(sym, direction, conn=conn, monitor_id=int(mon.get("id") or 0)) cancel_monitor_exit_orders(conn, mon, mode=mode) return lots = int(pos.get("lots") or mon.get("lots") or 1) @@ -865,7 +948,9 @@ def _execute_local_close( order_type="market", urgency="stop_loss", ) - mark_close_pending(sym, direction) + mark_close_pending( + sym, direction, conn=conn, monitor_id=int(mon.get("id") or 0), + ) _close_all_monitors_for_symbol(conn, sym, direction) conn.commit() result_label = _result_for_close(mon, reason)