Track open orders as pending until CTP fill, with cancel and timeout.

Add configurable pending timeout in settings and clearer CTP password save feedback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 00:05:45 +08:00
parent 7ea8fb6301
commit a23f2c80ca
10 changed files with 567 additions and 41 deletions
+63 -5
View File
@@ -225,6 +225,7 @@
}
list.innerHTML = rows.map(buildPosCard).join('');
bindPendingDismiss(list);
bindCancelOpenButtons(list);
bindSlTpButtons(list);
bindPlaceOrderButtons(list);
list.querySelectorAll('[data-close]').forEach(function (btn) {
@@ -632,7 +633,10 @@
showOrderMsg(data.error || '下单失败', false);
return;
}
var msg = data.message || ('开仓成功 · ' + (data.lots || lots) + ' 手');
var msg = data.message || (
data.filled ? ('开仓成功 · ' + (data.lots || lots) + ' 手') :
('委托已提交 · ' + (data.lots || lots) + ' 手挂单中')
);
showOrderMsg(msg, true);
pollPositions();
refreshQuote();
@@ -666,14 +670,20 @@
return '<div class="pos-pending-orders"><div class="pending-title">止盈止损监控</div>' + rows + '</div>';
}
function dismissMonitor(monitorId, btn) {
function dismissMonitor(monitorId, btn, opts) {
opts = opts || {};
if (!monitorId) return;
if (!confirm('取消该本地止盈止损监控?(不影响柜台委托)')) return;
var isPending = !!opts.pending;
var confirmMsg = isPending
? '撤销该开仓委托?(将向柜台发送撤单)'
: '取消该本地止盈止损监控?(不影响柜台委托)';
if (!confirm(confirmMsg)) return;
if (btn) {
btn.disabled = true;
btn.textContent = '取消中…';
}
fetch('/api/trading/monitor/dismiss', {
var url = isPending ? '/api/trading/monitor/cancel-open' : '/api/trading/monitor/dismiss';
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ monitor_id: monitorId })
@@ -687,11 +697,20 @@
alert(e.message || '取消失败');
if (btn) {
btn.disabled = false;
btn.textContent = '取消';
btn.textContent = isPending ? '撤单' : '取消';
}
});
}
function bindCancelOpenButtons(root) {
if (!root) return;
root.querySelectorAll('[data-cancel-open]').forEach(function (btn) {
btn.addEventListener('click', function () {
dismissMonitor(parseInt(btn.getAttribute('data-cancel-open'), 10), btn, { pending: true });
});
});
}
function bindPendingDismiss(root) {
if (!root) return;
root.querySelectorAll('[data-monitor-id]').forEach(function (btn) {
@@ -725,7 +744,46 @@
return '<span class="text-muted">未开启</span>';
}
function buildPendingOrderCard(row) {
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
var orderPx = row.order_price != null ? row.order_price : row.entry_price;
var remainMin = row.pending_timeout_min != null
? row.pending_timeout_min
: (row.auto_cancel_sec != null ? Math.max(1, Math.ceil(row.auto_cancel_sec / 60)) : 5);
var cancelBtn = row.can_cancel_order ?
'<button type="button" class="pos-close-btn" data-cancel-open="' + row.monitor_id + '">撤单</button>' : '';
var metaLine =
'状态 <strong class="text-accent">挂单中</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>';
return (
'<div class="pos-card is-pending">' +
'<div class="pos-card-head"><div><div class="title">' + row.symbol +
' <span class="badge dir">' + dirBadge + '</span>' +
' <span class="badge pending">挂单中</span></div>' +
'<div class="text-muted" style="font-size:.72rem">' + (row.symbol_code || '') + '</div></div>' +
'<div class="pos-card-actions">' + cancelBtn + '</div></div>' +
'<div class="pos-card-meta pos-card-meta-line">' + metaLine + '</div>' +
'<div class="pos-metrics">' +
'<div class="cell"><label>委托手数</label><div><strong>' + row.lots + ' 手</strong></div></div>' +
'<div class="cell"><label>委托价</label><div>' + fmtNum(orderPx) + '</div></div>' +
'<div class="cell"><label>当前价格</label><div>' + (row.current_price != null ? fmtNum(row.current_price) : '--') + '</div></div>' +
'<div class="cell pnl-pending"><label>状态</label><div class="text-accent">等待成交</div></div>' +
'<div class="cell"><label>提交时间</label><div>' + (openT || '--') + '</div></div>' +
'</div>' +
buildPendingHtml(row.pending_orders) +
'</div>'
);
}
function buildPosCard(row) {
if (row.order_state === 'pending') {
return buildPendingOrderCard(row);
}
var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : '');
var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--';
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');