Isolate CTP in worker process and improve strategy roll UX.

Split vn.py into qihuo-ctp worker with IPC client bridge, keep CTP connected during breaks with cached account fallback, speed up strategy page loads, and allow off-session breakout roll submissions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-01 12:35:47 +08:00
parent 08d55411aa
commit 9cd81a3ea7
17 changed files with 2214 additions and 227 deletions
+13
View File
@@ -109,6 +109,7 @@
var breakEl = document.getElementById('roll-break-price');
var execHint = document.getElementById('roll-exec-hint');
var btnExec = document.getElementById('btn-roll-exec');
var btnPreview = document.getElementById('btn-roll-preview');
if (!modeEl) return;
var mode = modeEl.value || 'market';
var isBreak = mode === 'breakout';
@@ -120,6 +121,12 @@
if (btnExec) {
btnExec.textContent = mode === 'market' ? '执行滚仓' : '提交监控';
}
if (btnPreview) {
btnPreview.disabled = !inTradingSession && !isBreak;
btnPreview.title = (!inTradingSession && !isBreak)
? '休盘期间请切换为突破加仓'
: '';
}
}
function syncRollRiskHint() {
@@ -196,6 +203,7 @@
var rollPayload = null;
var rollMonitorSel = document.getElementById('roll-monitor-select');
var rollModeSel = document.getElementById('roll-add-mode');
var inTradingSession = {{ 'true' if trading_session else 'false' }};
if (rollModeSel) rollModeSel.addEventListener('change', syncRollModeUi);
if (rollMonitorSel) rollMonitorSel.addEventListener('change', syncRollRiskHint);
@@ -214,6 +222,7 @@
}
showPreview(rollPrev, formatRoll(d.preview), true, false);
btnRollE.hidden = false;
syncRollModeUi();
}).finally(function () {
btnRollP.disabled = false;
});
@@ -224,6 +233,10 @@
var payload = rollPayload || formData(rollForm);
var mode = (payload.add_mode || 'market');
if (mode === 'market') {
if (!inTradingSession) {
alert('休盘期间请切换为「突破加仓」后提交监控');
return;
}
if (!confirm('确认执行市价滚仓?')) return;
startRollCountdown(btnRollE, payload);
return;
+61 -17
View File
@@ -34,6 +34,8 @@
var ctpConnecting = false;
var ctpAutoConnectEnabled = true;
var positionsRendered = false;
var posFastPollTimer = null;
var posFastPollCount = 0;
var lastPosRowCount = 0;
var selectedMaxLots = null;
var recommendMaxByProduct = {};
@@ -248,6 +250,24 @@
});
}
function stopPosFastPoll() {
if (posFastPollTimer) {
clearInterval(posFastPollTimer);
posFastPollTimer = null;
}
posFastPollCount = 0;
}
function startPosFastPoll() {
if (posFastPollTimer) return;
posFastPollCount = 0;
posFastPollTimer = setInterval(function () {
pollPositions();
posFastPollCount += 1;
if (posFastPollCount >= 90) stopPosFastPoll();
}, 1000);
}
function applyPositionsData(data) {
if (!data) return;
var cap = document.getElementById('cap-display');
@@ -312,6 +332,7 @@
if (!connected) {
if (connecting) {
list.innerHTML = '<div class="empty-hint">CTP 连接中,请稍候…</div>';
startPosFastPoll();
return;
}
if (cooldownSec > 0 || (data.ctp_status && data.ctp_status.last_error)) {
@@ -325,8 +346,8 @@
list.innerHTML = '<div class="empty-hint text-muted">' + offHint + '</div>';
return;
}
list.innerHTML = '<div class="empty-hint">CTP 未连接,正在尝试自动重连…</div>';
if (ctpAutoConnectEnabled) tryAutoCtpReconnect();
list.innerHTML = '<div class="empty-hint">CTP 未连接,后台自动连接中…</div>';
if (ctpAutoConnectEnabled) refreshCtpStatusPassive();
return;
}
var syncing = data.sync_state === 'syncing';
@@ -339,6 +360,7 @@
syncBadge.textContent = data.sync_label || '持仓同步中…';
syncBadge.className = 'sync-badge text-accent';
}
startPosFastPoll();
return;
}
list.innerHTML = '<div class="empty-hint">暂无持仓。</div>';
@@ -347,9 +369,7 @@
return;
}
lastPosRowCount = rows.length;
if (!connected && ctpAutoConnectEnabled) {
tryAutoCtpReconnect();
}
stopPosFastPoll();
list.innerHTML = rows.map(buildPosCard).join('');
syncPositionListScroll(rows.length);
bindPendingDismiss(list);
@@ -610,8 +630,9 @@
}
if (st.connecting && Date.now() < deadline) {
syncCtpBadgeFromStatus(st);
pollPositions();
return new Promise(function (resolve) {
setTimeout(function () { resolve(tick()); }, 2000);
setTimeout(function () { resolve(tick()); }, 800);
});
}
syncCtpBadgeFromStatus(st);
@@ -795,18 +816,31 @@
});
}
function tryAutoCtpReconnect() {
if (!ctpAutoConnectEnabled) return;
if (ctpReconnecting || ctpConnectInflight) return;
/** 只读 CTP 状态;连接由 qihuo-ctp 后台 worker 负责,前端不发起 connect。 */
function refreshCtpStatusPassive() {
if (ctpConnected || ctpConnecting) return;
var now = Date.now();
if (now - lastCtpReconnectAt < 60000) return;
if (lastCtpLoginBanAt && now - lastCtpLoginBanAt < 2700000) return;
if (lastCtpUnreachableAt && now - lastCtpUnreachableAt < 300000) return;
if (now - lastCtpReconnectAt < 8000) return;
lastCtpReconnectAt = now;
ctpReconnecting = true;
requestCtpConnect(false).finally(function () {
ctpReconnecting = false;
});
fetch('/api/ctp/status')
.then(function (r) { return r.json(); })
.then(function (d) {
var st = d.status || {};
syncCtpBadgeFromStatus(st);
if (st.connected) {
showCtpError('');
pollPositions();
startPosFastPoll();
} else if (st.connecting) {
updateCtpBadge(false, true);
startPosFastPoll();
} else if (st.last_error) {
showCtpError(st.last_error);
} else if (st.disabled_hint) {
showCtpError(st.disabled_hint);
}
})
.catch(function () {});
}
function showOrderMsg(text, ok) {
@@ -1911,12 +1945,22 @@
} else if (st.last_error) {
showCtpError(st.last_error);
}
if (st.connected) pollPositions();
if (st.connected) {
pollPositions();
startPosFastPoll();
} else if (st.connecting) {
startPosFastPoll();
waitForCtpConnected(90000);
} else if (ctpAutoConnectEnabled && !(st.login_cooldown_sec > 0)) {
refreshCtpStatusPassive();
startPosFastPoll();
}
})
.catch(function () {});
}
function cleanupTradePage() {
stopPosFastPoll();
if (sessionClockTickTimer) {
clearInterval(sessionClockTickTimer);
sessionClockTickTimer = null;