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:
+53
-43
@@ -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>' +
|
||||
|
||||
Reference in New Issue
Block a user