feat: 持仓监控后台 SSE 推送与浏览器缓存,刷新不再阻塞读柜台。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+112
-77
@@ -11,8 +11,8 @@
|
||||
var tpInput = document.getElementById('trade-tp');
|
||||
var marketHint = document.getElementById('market-hint');
|
||||
var metricsHint = document.getElementById('trade-metrics-hint');
|
||||
var pollTimer = null;
|
||||
var recommendSource = null;
|
||||
var positionSource = null;
|
||||
var quoteTimer = null;
|
||||
var calcTimer = null;
|
||||
var lastQuotePrice = null;
|
||||
@@ -22,10 +22,11 @@
|
||||
var isTradingSession = false;
|
||||
var hasSlTpMonitoring = false;
|
||||
var ctpConnected = false;
|
||||
var pollIntervalMs = 0;
|
||||
var positionsRendered = false;
|
||||
var selectedMaxLots = null;
|
||||
var recommendMaxByProduct = {};
|
||||
var recommendMaxByCode = {};
|
||||
var POS_CACHE_KEY = 'qihuo_trading_live_v1';
|
||||
|
||||
function runWhenReady(fn) {
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -98,22 +99,86 @@
|
||||
}
|
||||
}
|
||||
|
||||
function loadPosCache() {
|
||||
try {
|
||||
var raw = sessionStorage.getItem(POS_CACHE_KEY);
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function savePosCache(data) {
|
||||
try {
|
||||
sessionStorage.setItem(POS_CACHE_KEY, JSON.stringify(data));
|
||||
} catch (e) { /* quota */ }
|
||||
}
|
||||
|
||||
function applyPositionsData(data) {
|
||||
if (!list || !data) return;
|
||||
var cap = document.getElementById('cap-display');
|
||||
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
|
||||
var connected = data.ctp_status && data.ctp_status.connected;
|
||||
var connecting = data.ctp_status && data.ctp_status.connecting;
|
||||
ctpConnected = !!connected;
|
||||
isTradingSession = !!data.trading_session;
|
||||
updateCtpBadge(!!connected, !!connecting);
|
||||
var riskBadge = document.getElementById('risk-badge');
|
||||
if (riskBadge && data.risk_status) {
|
||||
riskBadge.textContent = data.risk_status.status_label || '';
|
||||
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
|
||||
}
|
||||
var rows = data.rows || [];
|
||||
hasSlTpMonitoring = rows.some(function (row) {
|
||||
return row.stop_loss != null || row.take_profit != null;
|
||||
});
|
||||
updateSessionUi();
|
||||
savePosCache(data);
|
||||
positionsRendered = true;
|
||||
if (!connected) {
|
||||
if (connecting) {
|
||||
list.innerHTML = '<div class="empty-hint">CTP 连接中,请稍候…</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '<div class="empty-hint">CTP 未连接,正在尝试自动重连…</div>';
|
||||
tryAutoCtpReconnect();
|
||||
return;
|
||||
}
|
||||
if (!rows.length) {
|
||||
var pendingOnly = data.pending_orders || [];
|
||||
if (pendingOnly.length) {
|
||||
list.innerHTML = '<div class="empty-hint" style="margin-bottom:.75rem">柜台暂无持仓</div>' +
|
||||
pendingOnly.map(function (p) {
|
||||
var dismissBtn = p.monitor_id ?
|
||||
'<button type="button" class="pos-dismiss-btn" data-monitor-id="' + p.monitor_id + '">取消</button>' : '';
|
||||
return (
|
||||
'<div class="pos-pending-item ' +
|
||||
(p.order_kind === 'stop_loss' ? 'sl' : (p.order_kind === 'take_profit' ? 'tp' : 'ctp')) +
|
||||
'"><span>' + (p.label || '挂单') + ' · ' + (p.symbol || p.symbol_code) + '</span>' +
|
||||
'<span class="pos-pending-right"><strong>' + fmtNum(p.price) + '</strong> · ' +
|
||||
(p.lots || 1) + ' 手' + dismissBtn + '</span></div>'
|
||||
);
|
||||
}).join('');
|
||||
bindPendingDismiss(list);
|
||||
} else {
|
||||
list.innerHTML = '<div class="empty-hint">柜台暂无持仓。</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
list.innerHTML = rows.map(buildPosCard).join('');
|
||||
bindPendingDismiss(list);
|
||||
bindSlTpButtons(list);
|
||||
bindPlaceOrderButtons(list);
|
||||
list.querySelectorAll('[data-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
closePosition(JSON.parse(decodeURIComponent(btn.getAttribute('data-close'))), btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function schedulePositionPoll() {
|
||||
var nextMs = 0;
|
||||
if (hasSlTpMonitoring && isTradingSession) {
|
||||
nextMs = 1000;
|
||||
} else if (!ctpConnected) {
|
||||
nextMs = 5000;
|
||||
}
|
||||
if (nextMs === pollIntervalMs && pollTimer) return;
|
||||
pollIntervalMs = nextMs;
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
if (nextMs > 0) {
|
||||
pollTimer = setInterval(pollPositions, nextMs);
|
||||
}
|
||||
/* 持仓改由后台 SSE 推送,保留空函数兼容旧调用 */
|
||||
}
|
||||
|
||||
function updateSessionUi() {
|
||||
@@ -654,71 +719,35 @@
|
||||
return r.json();
|
||||
})
|
||||
.then(function (data) {
|
||||
var cap = document.getElementById('cap-display');
|
||||
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
|
||||
var connected = data.ctp_status && data.ctp_status.connected;
|
||||
var connecting = data.ctp_status && data.ctp_status.connecting;
|
||||
ctpConnected = !!connected;
|
||||
isTradingSession = !!data.trading_session;
|
||||
updateCtpBadge(!!connected, !!connecting);
|
||||
var riskBadge = document.getElementById('risk-badge');
|
||||
if (riskBadge && data.risk_status) {
|
||||
riskBadge.textContent = data.risk_status.status_label || '';
|
||||
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
|
||||
}
|
||||
var rows = data.rows || [];
|
||||
hasSlTpMonitoring = rows.some(function (row) {
|
||||
return row.stop_loss != null || row.take_profit != null;
|
||||
});
|
||||
schedulePositionPoll();
|
||||
updateSessionUi();
|
||||
if (!connected) {
|
||||
if (connecting) {
|
||||
list.innerHTML = '<div class="empty-hint">CTP 连接中,请稍候…</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '<div class="empty-hint">CTP 未连接,正在尝试自动重连…</div>';
|
||||
tryAutoCtpReconnect();
|
||||
return;
|
||||
}
|
||||
if (!rows.length) {
|
||||
var pendingOnly = data.pending_orders || [];
|
||||
if (pendingOnly.length) {
|
||||
list.innerHTML = '<div class="empty-hint" style="margin-bottom:.75rem">柜台暂无持仓</div>' +
|
||||
pendingOnly.map(function (p) {
|
||||
var dismissBtn = p.monitor_id ?
|
||||
'<button type="button" class="pos-dismiss-btn" data-monitor-id="' + p.monitor_id + '">取消</button>' : '';
|
||||
return (
|
||||
'<div class="pos-pending-item ' +
|
||||
(p.order_kind === 'stop_loss' ? 'sl' : (p.order_kind === 'take_profit' ? 'tp' : 'ctp')) +
|
||||
'"><span>' + (p.label || '挂单') + ' · ' + (p.symbol || p.symbol_code) + '</span>' +
|
||||
'<span class="pos-pending-right"><strong>' + fmtNum(p.price) + '</strong> · ' +
|
||||
(p.lots || 1) + ' 手' + dismissBtn + '</span></div>'
|
||||
);
|
||||
}).join('');
|
||||
bindPendingDismiss(list);
|
||||
} else {
|
||||
list.innerHTML = '<div class="empty-hint">柜台暂无持仓。</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
list.innerHTML = rows.map(buildPosCard).join('');
|
||||
bindPendingDismiss(list);
|
||||
bindSlTpButtons(list);
|
||||
bindPlaceOrderButtons(list);
|
||||
list.querySelectorAll('[data-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
closePosition(JSON.parse(decodeURIComponent(btn.getAttribute('data-close'))), btn);
|
||||
});
|
||||
});
|
||||
applyPositionsData(data);
|
||||
})
|
||||
.catch(function () {
|
||||
if (list.innerHTML.indexOf('pos-card') < 0) {
|
||||
if (!positionsRendered && list.innerHTML.indexOf('pos-card') < 0) {
|
||||
list.innerHTML = '<div class="empty-hint text-loss">持仓加载失败</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function connectPositionStream() {
|
||||
if (positionSource) {
|
||||
positionSource.close();
|
||||
positionSource = null;
|
||||
}
|
||||
positionSource = new EventSource('/api/trading/stream');
|
||||
positionSource.addEventListener('positions', function (ev) {
|
||||
try {
|
||||
applyPositionsData(JSON.parse(ev.data));
|
||||
} catch (e) { /* ignore */ }
|
||||
});
|
||||
positionSource.onerror = function () {
|
||||
if (positionSource) {
|
||||
positionSource.close();
|
||||
positionSource = null;
|
||||
}
|
||||
setTimeout(connectPositionStream, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
function renderRecommendations(data) {
|
||||
if (!recommendList || !data) return;
|
||||
updateRecommendMaxMaps(data);
|
||||
@@ -811,14 +840,20 @@
|
||||
|
||||
runWhenReady(function () {
|
||||
setPriceType('limit');
|
||||
pollPositions();
|
||||
var cached = loadPosCache();
|
||||
if (cached) {
|
||||
applyPositionsData(cached);
|
||||
}
|
||||
connectPositionStream();
|
||||
connectRecommendStream();
|
||||
fetch('/api/recommend/list')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) { if (data.ok) renderRecommendations(data); })
|
||||
.catch(function () {});
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') pollPositions();
|
||||
if (document.visibilityState === 'visible' && !positionSource) {
|
||||
connectPositionStream();
|
||||
}
|
||||
});
|
||||
updateSessionUi();
|
||||
scheduleQuote();
|
||||
|
||||
Reference in New Issue
Block a user