From 98d63f38bf2c39e3ba8bf22cf2e38ad7cbe5a188 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 2 Jul 2026 14:53:04 +0800 Subject: [PATCH] Fix dashboard position limit flicker by unifying active count across APIs. Co-authored-by: Cursor --- modules/risk/account_risk_lib.py | 52 ++++++++++++++++++++++++++++++ modules/stats/dashboard_lib.py | 15 ++++++++- modules/trading/install.py | 25 +++----------- modules/web/static/js/dashboard.js | 34 +++++++++++++++++-- 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/modules/risk/account_risk_lib.py b/modules/risk/account_risk_lib.py index 86ced4a..150323b 100644 --- a/modules/risk/account_risk_lib.py +++ b/modules/risk/account_risk_lib.py @@ -274,6 +274,58 @@ def count_active_trade_monitors(conn) -> int: return 0 +def _position_keys_from_rows(rows: list) -> set[tuple[str, str]]: + keys: set[tuple[str, str]] = set() + for p in rows or []: + lots = int(p.get("lots") or 0) + if lots <= 0: + continue + sym = ( + p.get("symbol") + or p.get("symbol_code") + or p.get("ths_code") + or "" + ).strip().lower() + direction = (p.get("direction") or "long").strip().lower() + if sym: + keys.add((sym, direction)) + return keys + + +def effective_active_position_count( + conn, + mode: str, + *, + ctp_connected: Optional[bool] = None, +) -> int: + """风控持仓数:柜台/快照实际持仓优先,本地监控作兜底。""" + monitor_count = count_active_trade_monitors(conn) + if ctp_connected is None: + try: + from modules.ctp.vnpy_bridge import ctp_status + + ctp_connected = bool(ctp_status(mode).get("connected")) + except Exception: + ctp_connected = False + if not ctp_connected: + return monitor_count + keys: set[tuple[str, str]] = set() + try: + from modules.ctp.ctp_trading_state import trading_state + + keys |= _position_keys_from_rows(trading_state.get_positions()) + except Exception: + pass + try: + from modules.trading.position_stream import position_hub + + snap = position_hub.get_snapshot() or {} + keys |= _position_keys_from_rows(snap.get("rows")) + except Exception: + pass + return max(monitor_count, len(keys)) + + def parse_mood_issues(raw: Any) -> list[str]: if raw is None: return [] diff --git a/modules/stats/dashboard_lib.py b/modules/stats/dashboard_lib.py index 73baceb..1ed8140 100644 --- a/modules/stats/dashboard_lib.py +++ b/modules/stats/dashboard_lib.py @@ -62,6 +62,7 @@ def build_risk_overview( daily_position_limit, daily_trading_risk_pct_limit, daily_trading_risk_used_pct, + effective_active_position_count, ensure_account_risk_schema, get_risk_status, manual_close_daily_limit, @@ -76,10 +77,22 @@ def build_risk_overview( get_max_margin_pct, get_roll_max_margin_pct, get_sizing_mode, + get_trading_mode, ) ensure_account_risk_schema(conn) - risk = dict(get_risk_status(conn, equity=equity) or {}) + mode = get_trading_mode(get_setting) + ctp_connected = False + try: + from modules.ctp.vnpy_bridge import ctp_status + + ctp_connected = bool(ctp_status(mode).get("connected")) + except Exception: + pass + active_n = effective_active_position_count( + conn, mode, ctp_connected=ctp_connected, + ) + risk = dict(get_risk_status(conn, equity=equity, active_count=active_n) or {}) row = conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone() td = trading_day_label() stored_td = str(row["trading_day"] or "") if row else "" diff --git a/modules/trading/install.py b/modules/trading/install.py index ba072a0..35107ba 100644 --- a/modules/trading/install.py +++ b/modules/trading/install.py @@ -69,6 +69,7 @@ from modules.trading.sl_tp_guard import ( from risk.account_risk_lib import ( assert_can_open, count_active_trade_monitors, + effective_active_position_count, get_risk_status, on_mood_journal_freeze, on_user_initiated_close, @@ -972,27 +973,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se *, ctp_connected: Optional[bool] = None, ) -> int: - """风控持仓数以柜台/快照实际持仓优先,本地监控作兜底。""" - monitor_count = count_active_trade_monitors(conn) if ctp_connected is None: ctp_connected = bool(_cached_ctp_status(mode).get("connected")) - if not ctp_connected: - return monitor_count - keys: set[tuple[str, str]] = set() - for p in _positions_for_monitor_restore(mode, allow_ctp=False): - lots = int(p.get("lots") or 0) - if lots <= 0: - continue - sym = ( - p.get("symbol") - or p.get("symbol_code") - or p.get("ths_code") - or "" - ).strip().lower() - direction = (p.get("direction") or "long").strip().lower() - if sym: - keys.add((sym, direction)) - return max(monitor_count, len(keys)) + return effective_active_position_count( + conn, mode, ctp_connected=ctp_connected, + ) def _build_pending_orders(conn, mode: str) -> list[dict]: pending: list[dict] = [] @@ -2175,7 +2160,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se capital = _capital(conn) risk = get_risk_status( conn, - active_count=count_active_trade_monitors(conn), + active_count=_effective_active_position_count(conn, mode), equity=capital, ) syncing = bool(ctp_st.get("connected") or ctp_st.get("connecting")) diff --git a/modules/web/static/js/dashboard.js b/modules/web/static/js/dashboard.js index 34d4fa4..77bc1ad 100644 --- a/modules/web/static/js/dashboard.js +++ b/modules/web/static/js/dashboard.js @@ -352,16 +352,41 @@ function applyRisk(risk, account) { if (!riskGridEl || !risk) return; - if (risk.limits && Object.keys(risk.limits).length) { + var isFullRisk = risk.limits && Object.keys(risk.limits).length; + if (isFullRisk) { lastRiskPayload = risk; } else if (lastRiskPayload) { + var incomingSt = risk.status || {}; + var prevSt = lastRiskPayload.status || {}; risk = { enabled: lastRiskPayload.enabled, limits: lastRiskPayload.limits, manual_close_count_today: lastRiskPayload.manual_close_count_today, margin_pct_used: lastRiskPayload.margin_pct_used, - status: risk.status || lastRiskPayload.status, + daily_open_count: risk.daily_open_count != null + ? risk.daily_open_count + : lastRiskPayload.daily_open_count, + daily_risk_used_pct: risk.daily_risk_used_pct != null + ? risk.daily_risk_used_pct + : lastRiskPayload.daily_risk_used_pct, + status: Object.assign({}, prevSt, incomingSt), }; + var incActive = incomingSt.active_count; + var prevActive = prevSt.active_count; + if (incActive != null && prevActive != null) { + var incN = Number(incActive); + var prevN = Number(prevActive); + var marginUsed = account && account.margin_used != null + ? Number(account.margin_used) + : 0; + if ( + !isNaN(incN) && !isNaN(prevN) + && incN < prevN + && (marginUsed > 0 || prevN > 0) + ) { + risk.status.active_count = prevN; + } + } } if (account && account.equity > 0 && account.margin_used != null) { risk.margin_pct_used = Math.round(account.margin_used / account.equity * 10000) / 100; @@ -850,7 +875,10 @@ if (!data) return; if (data.ctp_status) updateCtpBadge(data.ctp_status); if (data.risk_status) { - applyRisk({ status: data.risk_status }); + applyRisk( + { status: data.risk_status }, + { equity: data.capital, margin_used: data.margin_used }, + ); } if (data.trading_mode_label && modeBadge) { modeBadge.textContent = data.trading_mode_label;