From 59690a4037cc418162a8a5bdd53aa7012bbce304 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 3 Jul 2026 23:25:26 +0800 Subject: [PATCH] Fix ghost positions: stop showing local monitor when CTP confirms flat account. Co-authored-by: Cursor --- modules/ctp/vnpy_bridge.py | 13 ++++-- modules/trading/install.py | 82 ++++++++++++++++---------------------- 2 files changed, 43 insertions(+), 52 deletions(-) diff --git a/modules/ctp/vnpy_bridge.py b/modules/ctp/vnpy_bridge.py index 080af5a..7bbdb8e 100644 --- a/modules/ctp/vnpy_bridge.py +++ b/modules/ctp/vnpy_bridge.py @@ -617,11 +617,16 @@ class CtpBridge: preserve_margin = 0.0 if self._connected_mode and not positions: try: - preserve_margin = float( - ctp_account_margin_used(self._connected_mode) or 0, - ) + since_ok = time.time() - float(self._last_connect_ok_ts or 0) except Exception: - preserve_margin = 0.0 + since_ok = 9999.0 + if since_ok < 180: + try: + preserve_margin = float( + ctp_account_margin_used(self._connected_mode) or 0, + ) + except Exception: + preserve_margin = 0.0 trading_state.calibrate_from_lists( orders, positions, diff --git a/modules/trading/install.py b/modules/trading/install.py index c919f9b..161b4ff 100644 --- a/modules/trading/install.py +++ b/modules/trading/install.py @@ -2416,13 +2416,21 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se ctp_list: list[dict] = [] if ctp_status(mode).get("connected"): + ctp_raw = list(_ctp_positions(mode, refresh_if_empty=False) or []) merged: dict[str, dict] = {} - for p in list(_ctp_positions(mode) or []) + list(trading_state.get_positions() or []): + for p in ctp_raw: lots = int(p.get("lots") or 0) if lots <= 0: continue pk = p.get("position_key") or _position_key_from_ctp(p) merged[pk] = p + if not merged and _allow_monitor_position_fallback(mode): + for p in list(trading_state.get_positions() or []): + lots = int(p.get("lots") or 0) + if lots <= 0: + continue + pk = p.get("position_key") or _position_key_from_ctp(p) + merged[pk] = p ctp_list = list(merged.values()) ensure_monitor_order_columns(conn) @@ -2490,21 +2498,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se seen.add(rk) deduped.append(row) - if not deduped and ctp_status(mode).get("connected"): + if not deduped and ctp_status(mode).get("connected") and _allow_monitor_position_fallback(mode): margin_raw = ctp_account_margin_used(mode) margin_used = float(margin_raw or 0) if margin_raw is not None else 0.0 has_margin_hint = margin_raw is not None and margin_used > 0 - has_active_mon = any( - int(m.get("lots") or 0) > 0 for m in monitor_by_pk.values() - ) - since_connect = 9999.0 - try: - since_connect = time.time() - float( - getattr(get_bridge(), "_last_connect_ok_ts", 0) or 0, - ) - except Exception: - pass - if has_margin_hint or has_active_mon or since_connect < 300: + since_connect = _ctp_since_connect_sec(mode) + if has_margin_hint or since_connect < 120: if not monitor_by_pk and has_margin_hint: _ensure_monitors_from_sticky_state(conn, mode) monitor_by_pk = _monitors_by_position_key(conn) @@ -2542,41 +2541,6 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se except Exception as exc: logger.warning("compose monitor fallback row failed: %s", exc) - if not deduped and ctp_status(mode).get("connected"): - for r in conn.execute( - "SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC" - ).fetchall(): - mon = dict(r) - lots = int(mon.get("lots") or 0) - if lots <= 0: - continue - sym = (mon.get("symbol") or "").strip() - direction = (mon.get("direction") or "long").strip().lower() - rk = _monitor_position_key(mon) - if rk in seen: - continue - if fast: - mon = _overlay_sl_tp_readonly(conn, mon, sym, direction) or mon - try: - row = _compose_position_row( - conn, - mon=mon, - ctp=None, - mode=mode, - capital=capital, - now_iso=now_iso, - fast=fast, - ) - if not row: - continue - row_key = row.get("key") or row.get("position_key") or rk - if row_key in seen: - continue - seen.add(row_key) - deduped.append(row) - except Exception as exc: - logger.warning("compose active monitor row failed: %s", exc) - return deduped def _build_trading_live_payload(conn, *, fast: bool = False) -> dict: @@ -2593,6 +2557,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se if not fast: _ensure_monitors_from_ctp(conn, mode) _sync_trade_monitors_with_ctp(conn, mode) + elif ( + not _ctp_positions(mode, refresh_if_empty=False) + and not _account_has_margin_in_use(mode) + and _ctp_since_connect_sec(mode) >= 120 + ): + _sync_trade_monitors_with_ctp(conn, mode) elif count_active_trade_monitors(conn) == 0: margin_raw = ctp_account_margin_used(mode) if margin_raw is not None and float(margin_raw) > 0: @@ -3640,6 +3610,22 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se (now_s, gid), ) + def _ctp_since_connect_sec(mode: str) -> float: + try: + return time.time() - float( + getattr(get_bridge(), "_last_connect_ok_ts", 0) or 0, + ) + except Exception: + return 9999.0 + + def _allow_monitor_position_fallback(mode: str) -> bool: + """CTP 已连接且确认空仓后,禁止用本地监控拼「幽灵持仓」。""" + if not ctp_status(mode).get("connected"): + return False + if _ctp_since_connect_sec(mode) < 120: + return True + return _account_has_margin_in_use(mode) + def _account_has_margin_in_use(mode: str) -> bool: if not ctp_status(mode).get("connected"): return False