diff --git a/install_trading.py b/install_trading.py
index a7ed4fe..bf56ca2 100644
--- a/install_trading.py
+++ b/install_trading.py
@@ -771,11 +771,17 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"monitor_id": mon["id"] if mon else None,
})
row_key = _canonical_position_key(sym, direction)
+ ctp_st = ctp_status(mode)
+ sync_pending = (
+ mon is not None
+ and ctp is None
+ and bool(ctp_st.get("connected"))
+ )
return {
"key": row_key,
"source": "ctp" if ctp else "local",
"source_label": source_label,
- "sync_pending": ctp is None and mon is not None,
+ "sync_pending": sync_pending,
"monitor_id": mon["id"] if mon else None,
"symbol_code": sym,
**_symbol_display_fields(sym),
@@ -1024,7 +1030,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
capital = _capital(conn)
if not fast and ctp_st.get("connected"):
_reconcile_pending(conn, mode, capital=capital)
- if not fast:
+ if ctp_st.get("connected"):
_ensure_monitors_from_ctp(conn, mode)
rows = _build_trading_live_rows(conn, fast=fast)
pending_orders = _build_pending_orders(conn, mode)
@@ -2262,8 +2268,19 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
def _position_worker_refresh() -> dict:
_pos_refresh_tick["n"] += 1
- # 每秒轻量刷新;每 5 秒做一次 CTP 持仓/挂单对账,避免频繁 query 导致 vnctptd 崩溃
- return _refresh_trading_live_snapshot(fast=(_pos_refresh_tick["n"] % 5 != 0))
+ mode = get_trading_mode(get_setting)
+ connected = bool(ctp_status(mode).get("connected"))
+ # 已连接时每 2 秒完整对账;未连接时每 5 秒轻量刷新(禁止 query_position)
+ if connected:
+ use_fast = _pos_refresh_tick["n"] % 2 != 0
+ else:
+ use_fast = _pos_refresh_tick["n"] % 5 != 0
+ payload = _refresh_trading_live_snapshot(fast=use_fast)
+ if connected and use_fast and any(
+ r.get("sync_pending") for r in (payload.get("rows") or [])
+ ):
+ payload = _refresh_trading_live_snapshot(fast=False)
+ return payload
start_position_worker(
refresh_fn=_position_worker_refresh,
diff --git a/static/css/trade.css b/static/css/trade.css
index 40300a6..c0c7067 100644
--- a/static/css/trade.css
+++ b/static/css/trade.css
@@ -65,6 +65,9 @@
.gap-badge{font-size:.72rem}
.rec-market-link{color:inherit;text-decoration:none;display:inline-flex;flex-wrap:wrap;align-items:baseline;gap:.2rem .35rem}
.rec-market-link:hover strong,.rec-market-link:hover .text-accent{color:var(--accent);text-decoration:underline}
+.pos-market-link{color:inherit;text-decoration:none}
+.pos-market-link:hover{color:var(--accent)}
+.pos-market-link:hover .text-accent{text-decoration:underline}
.pos-symbol-sub{font-size:.72rem;line-height:1.35}
.pos-main-badge{font-size:.68rem;vertical-align:middle}
.pos-change-up{color:var(--profit)}
diff --git a/static/js/trade.js b/static/js/trade.js
index 71ea5e2..d92b7e0 100644
--- a/static/js/trade.js
+++ b/static/js/trade.js
@@ -30,6 +30,7 @@
var isTradingSession = false;
var hasSlTpMonitoring = false;
var ctpConnected = false;
+ var ctpConnecting = false;
var positionsRendered = false;
var selectedMaxLots = null;
var recommendMaxByProduct = {};
@@ -164,6 +165,7 @@
var cooldownSec = (data.ctp_status && data.ctp_status.login_cooldown_sec) || 0;
if (cooldownSec > 0) connecting = false;
ctpConnected = !!connected;
+ ctpConnecting = !!connecting;
isTradingSession = !!data.trading_session;
syncCtpBadgeFromStatus(data.ctp_status || { connected: connected, connecting: connecting });
if (!connected && !connecting && data.ctp_status && data.ctp_status.last_error) {
@@ -811,13 +813,17 @@
var name = row.symbol_name || row.symbol || '';
var code = row.symbol_code || '';
var mainBadge = row.symbol_is_main ? ' 主力' : '';
- var title = name + mainBadge;
+ var inner = name + mainBadge;
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
- title += ' ' + code + '';
+ inner += ' ' + code + '';
} else if (!name && code) {
- title = '' + code + '';
+ inner = '' + code + '';
}
- return title + extraBadges;
+ if (marketNavEnabled && code) {
+ var href = '/market?symbol=' + encodeURIComponent(code) + '&period=15m';
+ inner = '' + inner + '';
+ }
+ return inner + extraBadges;
}
function posSymbolSubHtml(row) {
@@ -910,7 +916,16 @@
' · ' + slTpStatusHtml(row) +
' · 移动保本 ' + trailingStatusHtml(row) +
(slTpBtn ? ' · ' + slTpBtn : '') +
- (row.sync_pending ? ' · 同步柜台中…' : '');
+ (function () {
+ if (row.order_state === 'pending' || !row.monitor_id) return '';
+ if (ctpConnecting) {
+ return ' · CTP 连接中…';
+ }
+ if (row.sync_pending) {
+ return ' · 同步柜台中…';
+ }
+ return '';
+ }());
var feeLabel = row.fee_source === 'ctp' ? '手续费(柜台)' : '手续费';
var marginLabel = row.margin_source === 'ctp' ? '占用保证金(柜台)' : '占用保证金';
var openLabel = '开仓';
diff --git a/vnpy_bridge.py b/vnpy_bridge.py
index 8e5a179..0733e20 100644
--- a/vnpy_bridge.py
+++ b/vnpy_bridge.py
@@ -73,6 +73,8 @@ def _load_persisted_last_error() -> str:
return (get_setting(CTP_LAST_ERROR_KEY, "") or "").strip()
_position_refresh_callback: Optional[Callable[[], None]] = None
+_position_refresh_debounce_lock = threading.Lock()
+_position_refresh_debounce_ts: float = 0.0
def set_position_refresh_callback(fn: Optional[Callable[[], None]]) -> None:
@@ -89,6 +91,23 @@ def _fire_position_refresh_callback() -> None:
except Exception as exc:
logger.debug("position refresh callback: %s", exc)
+
+def _fire_position_refresh_callback_debounced(*, min_interval: float = 0.35) -> None:
+ global _position_refresh_debounce_ts
+ now = time.monotonic()
+ with _position_refresh_debounce_lock:
+ if now - _position_refresh_debounce_ts < min_interval:
+ return
+ _position_refresh_debounce_ts = now
+ _fire_position_refresh_callback()
+
+
+def _fire_position_refresh_burst() -> None:
+ """连接后持仓回报可能分批到达,分多次触发快照刷新。"""
+ _fire_position_refresh_callback()
+ for delay in (1.5, 4.0, 10.0, 25.0):
+ threading.Timer(delay, _fire_position_refresh_callback).start()
+
_bridge: Optional["CtpBridge"] = None
_bridge_lock = threading.Lock()
_ctp_td_lock = threading.RLock()
@@ -203,6 +222,7 @@ class CtpBridge:
self._last_trade_query_ts: float = 0.0
self._last_connect_ok_ts: float = 0.0
self._tick_hooked = False
+ self._position_hooked = False
self._bar_generators: dict[str, Any] = {}
self._bars_1m: dict[str, deque] = {}
self._init_engine()
@@ -217,11 +237,26 @@ class CtpBridge:
self._ee = EventEngine()
self._engine = MainEngine(self._ee)
self._engine.add_gateway(CtpGateway)
+ self._ensure_position_event_hook()
except ImportError:
self._last_error = "未安装 vnpy / vnpy_ctp,请 pip install vnpy vnpy_ctp"
except Exception as exc:
self._last_error = str(exc)
+ def _ensure_position_event_hook(self) -> None:
+ if self._position_hooked or not self._ee:
+ return
+ try:
+ from vnpy.trader.event import EVENT_POSITION
+ except ImportError:
+ return
+
+ def _on_position(_event) -> None:
+ _fire_position_refresh_callback_debounced()
+
+ self._ee.register(EVENT_POSITION, _on_position)
+ self._position_hooked = True
+
def available(self) -> bool:
return self._engine is not None
@@ -414,7 +449,7 @@ class CtpBridge:
mode, self._td_logged_in(),
len(self._engine.get_all_accounts() or []))
self._schedule_fee_sync(mode)
- _fire_position_refresh_callback()
+ _fire_position_refresh_burst()
return
finally:
self._ee.unregister(EVENT_LOG, _on_log)