Implement CTP-authoritative trading UI with event-driven state.

Add in-memory order/position books fed by CTP events, split active orders above positions in the UI, tick-triggered local SL/TP, and 30-second full calibration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 18:12:11 +08:00
parent 4ef33a367f
commit 3a150dd3d6
8 changed files with 1056 additions and 210 deletions
+53 -43
View File
@@ -6,6 +6,8 @@
var sizingMode = window.TRADE_SIZING_MODE || 'fixed';
if (sizingMode === 'risk') sizingMode = 'amount';
var list = document.getElementById('position-live-list');
var orderList = document.getElementById('order-live-list');
var syncBadge = document.getElementById('sync-badge');
var recommendList = document.getElementById('recommend-list');
var symInput = document.getElementById('trade-symbol');
var dirSelect = document.getElementById('trade-direction');
@@ -44,7 +46,7 @@
var REC_COLSPAN = 18;
var marketNavEnabled = !!window.MARKET_NAV_ENABLED;
var productCategories = window.PRODUCT_CATEGORIES || [];
var POS_CACHE_KEY = 'qihuo_trading_live_v3';
var POS_CACHE_KEY = 'qihuo_trading_live_v4';
function runWhenReady(fn) {
if (document.readyState === 'loading') {
@@ -156,8 +158,21 @@
return !!(msg && (msg.indexOf('不可达') >= 0 || msg.indexOf('Connection refused') >= 0 || msg.indexOf('timed out') >= 0));
}
function applyActiveOrders(orders) {
if (!orderList) return;
orders = orders || [];
if (!orders.length) {
orderList.innerHTML = '<div class="empty-hint">暂无委托。</div>';
return;
}
orderList.innerHTML = orders.map(buildPendingOrderCard).join('');
bindPendingDismiss(orderList);
bindCancelOpenButtons(orderList);
bindCancelOrderButtons(orderList);
}
function applyPositionsData(data) {
if (!list || !data) return;
if (!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;
@@ -168,6 +183,16 @@
ctpConnecting = !!connecting;
isTradingSession = !!data.trading_session;
syncCtpBadgeFromStatus(data.ctp_status || { connected: connected, connecting: connecting });
if (syncBadge) {
if (data.sync_label && connected) {
syncBadge.hidden = false;
syncBadge.textContent = data.sync_label;
syncBadge.className = 'sync-badge ' + (data.sync_state === 'syncing' ? 'text-accent' : 'text-muted');
} else {
syncBadge.hidden = true;
syncBadge.textContent = '';
}
}
if (!connected && !connecting && data.ctp_status && data.ctp_status.last_error) {
showCtpError(data.ctp_status.last_error);
if (isCtpLoginBanError(data.ctp_status.last_error)) {
@@ -181,10 +206,14 @@
riskBadge.textContent = data.risk_status.status_label || '';
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
}
var rows = data.rows || [];
applyActiveOrders(data.active_orders || []);
if (!list) return;
var rows = (data.rows || []).filter(function (row) {
return row.order_state !== 'pending';
});
var seenKeys = {};
rows = rows.filter(function (row) {
var k = row.key || ((row.symbol_code || '') + ':' + (row.direction || ''));
var k = row.key || row.position_key || ((row.symbol_code || '') + ':' + (row.direction || ''));
if (seenKeys[k]) return false;
seenKeys[k] = true;
return true;
@@ -210,36 +239,7 @@
tryAutoCtpReconnect();
return;
}
var pendingOnly = data.pending_orders || [];
if (pendingOnly.length) {
list.innerHTML = '<div class="empty-hint" style="margin-bottom:.75rem">暂无持仓</div>' +
pendingOnly.map(function (p) {
var cancelAllowed = p.cancel_allowed !== false && isTradingSession;
var actionBtn = '';
if (p.monitor_id) {
actionBtn = '<button type="button" class="pos-dismiss-btn' +
(cancelAllowed ? '' : ' is-session-off') + '"' +
(cancelAllowed ? '' : ' disabled title="不在交易时间段"') +
' data-monitor-id="' + p.monitor_id + '" data-pending-cancel="1">撤单</button>';
} else if (p.order_id && p.source === 'ctp') {
actionBtn = '<button type="button" class="pos-dismiss-btn' +
(cancelAllowed ? '' : ' is-session-off') + '"' +
(cancelAllowed ? '' : ' disabled title="不在交易时间段"') +
' data-cancel-order="' + encodeURIComponent(p.order_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) + ' 手' + actionBtn + '</span></div>'
);
}).join('');
bindPendingDismiss(list);
bindCancelOrderButtons(list);
} else {
list.innerHTML = '<div class="empty-hint">暂无持仓。</div>';
}
list.innerHTML = '<div class="empty-hint">暂无持仓。</div>';
return;
}
if (!connected) {
@@ -839,22 +839,32 @@
? row.pending_timeout_min
: (row.auto_cancel_sec != null ? Math.max(1, Math.ceil(row.auto_cancel_sec / 60)) : 5);
var cancelAllowed = row.cancel_allowed !== false && isTradingSession;
var cancelBtn = row.can_cancel_order ?
'<button type="button" class="pos-close-btn' + (cancelAllowed ? '' : ' is-session-off') + '"' +
(cancelAllowed ? '' : ' disabled title="不在交易时间段"') +
' data-cancel-open="' + row.monitor_id + '">撤单</button>' : '';
var cancelBtn = '';
if (row.can_cancel_order) {
if (row.monitor_id) {
cancelBtn = '<button type="button" class="pos-close-btn' + (cancelAllowed ? '' : ' is-session-off') + '"' +
(cancelAllowed ? '' : ' disabled title="不在交易时间段"') +
' data-cancel-open="' + row.monitor_id + '">撤单</button>';
} else if (row.order_id || row.vt_order_id) {
cancelBtn = '<button type="button" class="pos-close-btn' + (cancelAllowed ? '' : ' is-session-off') + '"' +
(cancelAllowed ? '' : ' disabled title="不在交易时间段"') +
' data-cancel-order="' + encodeURIComponent(row.vt_order_id || row.order_id) + '">撤单</button>';
}
}
var pendingLabel = row.source_label || '挂单中';
var isCloseOrder = pendingLabel.indexOf('平仓') >= 0;
var metaLine =
'状态 <strong class="text-accent">挂单中</strong>' +
'状态 <strong class="text-accent">' + pendingLabel + '</strong>' +
' · 委托价 <strong>' + fmtNum(orderPx) + '</strong>' +
(row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
' · ' + slTpStatusHtml(row) +
' · 移动保本 ' + trailingStatusHtml(row) +
' · <span class="text-muted">约 ' + remainMin + ' 分钟内未成交自动撤单</span>';
(row.stop_loss != null || row.take_profit != null ? ' · ' + slTpStatusHtml(row) : '') +
(row.trailing_be ? ' · 移动保本 ' + trailingStatusHtml(row) : '') +
(!isCloseOrder ? ' · <span class="text-muted">约 ' + remainMin + ' 分钟内未成交自动撤单</span>' : '');
return (
'<div class="pos-card is-pending">' +
'<div class="pos-card-head"><div><div class="title">' + posSymbolTitleHtml(row,
' <span class="badge dir">' + dirBadge + '</span>' +
' <span class="badge pending">挂单中</span>') + '</div>' +
' <span class="badge pending">' + (isCloseOrder ? '平仓委托' : '挂单中') + '</span>') + '</div>' +
'<div class="text-muted pos-symbol-sub">' + posSymbolSubHtml(row) + '</div></div>' +
'<div class="pos-card-actions">' + cancelBtn + '</div></div>' +
'<div class="pos-card-meta pos-card-meta-line">' + metaLine + '</div>' +