Fix slow CTP position sync after restart and link positions to 15m K-line.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+21
-4
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
|
||||
+20
-5
@@ -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 ? ' <span class="badge planned pos-main-badge">主力</span>' : '';
|
||||
var title = name + mainBadge;
|
||||
var inner = name + mainBadge;
|
||||
if (code && String(name).toLowerCase() !== String(code).toLowerCase()) {
|
||||
title += ' <span class="text-accent">' + code + '</span>';
|
||||
inner += ' <span class="text-accent">' + code + '</span>';
|
||||
} else if (!name && code) {
|
||||
title = '<span class="text-accent">' + code + '</span>';
|
||||
inner = '<span class="text-accent">' + code + '</span>';
|
||||
}
|
||||
return title + extraBadges;
|
||||
if (marketNavEnabled && code) {
|
||||
var href = '/market?symbol=' + encodeURIComponent(code) + '&period=15m';
|
||||
inner = '<a href="' + href + '" class="pos-market-link" title="查看 15 分 K 线">' + inner + '</a>';
|
||||
}
|
||||
return inner + extraBadges;
|
||||
}
|
||||
|
||||
function posSymbolSubHtml(row) {
|
||||
@@ -910,7 +916,16 @@
|
||||
' · ' + slTpStatusHtml(row) +
|
||||
' · 移动保本 ' + trailingStatusHtml(row) +
|
||||
(slTpBtn ? ' · ' + slTpBtn : '') +
|
||||
(row.sync_pending ? ' · <span class="text-muted">同步柜台中…</span>' : '');
|
||||
(function () {
|
||||
if (row.order_state === 'pending' || !row.monitor_id) return '';
|
||||
if (ctpConnecting) {
|
||||
return ' · <span class="text-muted">CTP 连接中…</span>';
|
||||
}
|
||||
if (row.sync_pending) {
|
||||
return ' · <span class="text-muted">同步柜台中…</span>';
|
||||
}
|
||||
return '';
|
||||
}());
|
||||
var feeLabel = row.fee_source === 'ctp' ? '手续费(柜台)' : '手续费';
|
||||
var marginLabel = row.margin_source === 'ctp' ? '占用保证金(柜台)' : '占用保证金';
|
||||
var openLabel = '开仓';
|
||||
|
||||
+36
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user