Fix ghost positions: stop showing local monitor when CTP confirms flat account.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-03 23:25:26 +08:00
parent e3b640b35c
commit 59690a4037
2 changed files with 43 additions and 52 deletions
+5
View File
@@ -616,6 +616,11 @@ class CtpBridge:
trades = self.list_trades() trades = self.list_trades()
preserve_margin = 0.0 preserve_margin = 0.0
if self._connected_mode and not positions: if self._connected_mode and not positions:
try:
since_ok = time.time() - float(self._last_connect_ok_ts or 0)
except Exception:
since_ok = 9999.0
if since_ok < 180:
try: try:
preserve_margin = float( preserve_margin = float(
ctp_account_margin_used(self._connected_mode) or 0, ctp_account_margin_used(self._connected_mode) or 0,
+34 -48
View File
@@ -2416,8 +2416,16 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
ctp_list: list[dict] = [] ctp_list: list[dict] = []
if ctp_status(mode).get("connected"): if ctp_status(mode).get("connected"):
ctp_raw = list(_ctp_positions(mode, refresh_if_empty=False) or [])
merged: dict[str, dict] = {} 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) lots = int(p.get("lots") or 0)
if lots <= 0: if lots <= 0:
continue continue
@@ -2490,21 +2498,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
seen.add(rk) seen.add(rk)
deduped.append(row) 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_raw = ctp_account_margin_used(mode)
margin_used = float(margin_raw or 0) if margin_raw is not None else 0.0 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_margin_hint = margin_raw is not None and margin_used > 0
has_active_mon = any( since_connect = _ctp_since_connect_sec(mode)
int(m.get("lots") or 0) > 0 for m in monitor_by_pk.values() if has_margin_hint or since_connect < 120:
)
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:
if not monitor_by_pk and has_margin_hint: if not monitor_by_pk and has_margin_hint:
_ensure_monitors_from_sticky_state(conn, mode) _ensure_monitors_from_sticky_state(conn, mode)
monitor_by_pk = _monitors_by_position_key(conn) 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: except Exception as exc:
logger.warning("compose monitor fallback row failed: %s", 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 return deduped
def _build_trading_live_payload(conn, *, fast: bool = False) -> dict: 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: if not fast:
_ensure_monitors_from_ctp(conn, mode) _ensure_monitors_from_ctp(conn, mode)
_sync_trade_monitors_with_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: elif count_active_trade_monitors(conn) == 0:
margin_raw = ctp_account_margin_used(mode) margin_raw = ctp_account_margin_used(mode)
if margin_raw is not None and float(margin_raw) > 0: 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), (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: def _account_has_margin_in_use(mode: str) -> bool:
if not ctp_status(mode).get("connected"): if not ctp_status(mode).get("connected"):
return False return False