feat: CTP 断线重连、下单卡片优化、手数自动计算
后台每 30s 检测并重连;以损定仓填止损后自动算手数;开仓/平仓按钮并排对齐。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+8
-10
@@ -10,19 +10,17 @@
|
||||
.trade-order-status{display:grid;gap:.55rem;margin:.5rem 0 .75rem;padding:.65rem .85rem;background:var(--card-inner);border:1px solid var(--card-border);border-radius:8px;font-size:.82rem}
|
||||
.trade-order-status-compact{margin-top:0}
|
||||
.trade-order-status .status-row{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .65rem}
|
||||
.trade-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;margin-bottom:.75rem}
|
||||
.trade-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:.75rem .65rem;margin-bottom:.85rem}
|
||||
.trade-form-grid .span-2{grid-column:span 2}
|
||||
.trade-field label{display:block;font-size:.72rem;margin-bottom:.25rem;color:var(--text-label)}
|
||||
.trade-field select,.trade-field input{width:100%}
|
||||
.trade-field label{display:block;font-size:.72rem;margin-bottom:.28rem;color:var(--text-label)}
|
||||
.trade-field select,.trade-field input{width:100%;box-sizing:border-box}
|
||||
.trade-field .lots-auto{color:var(--accent);font-weight:600;background:var(--card-inner);cursor:default}
|
||||
.price-type-tabs{display:flex;gap:.35rem;margin-bottom:.35rem}
|
||||
.price-tab{border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);padding:.25rem .65rem;border-radius:6px;font-size:.75rem;cursor:pointer}
|
||||
.price-tab.active{border-color:var(--accent);color:var(--accent);font-weight:600}
|
||||
.price-tab{border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);padding:.28rem .7rem;border-radius:6px;font-size:.75rem;cursor:pointer;flex:1;text-align:center}
|
||||
.price-tab.active{border-color:var(--accent);color:var(--accent);font-weight:600;background:rgba(56,189,248,.08)}
|
||||
.market-hint{font-size:.7rem;margin-top:.25rem}
|
||||
.calc-lots-row{display:flex;gap:.4rem}
|
||||
.calc-lots-row input{flex:1}
|
||||
.calc-lots-row .btn-secondary{padding:.35rem .6rem;font-size:.75rem;white-space:nowrap}
|
||||
.trade-action-row{display:flex;gap:.65rem;margin:.75rem 0 .5rem}
|
||||
.trade-action-row .btn-open{flex:1;padding:.65rem}
|
||||
.trade-action-row{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;margin:.85rem 0 .55rem}
|
||||
.trade-action-row .btn-open,.trade-action-row .btn-secondary{padding:.6rem .75rem;font-size:.88rem;width:100%}
|
||||
.trade-footer{background:var(--card-inner);border-radius:8px;padding:.65rem .85rem;font-size:.78rem;line-height:1.5;border:1px solid var(--card-border);margin-top:.5rem}
|
||||
.trade-footer strong{color:var(--accent)}
|
||||
.rec-blocked td{opacity:.55}
|
||||
|
||||
+78
-21
@@ -14,8 +14,11 @@
|
||||
var pollTimer = null;
|
||||
var recommendSource = null;
|
||||
var quoteTimer = null;
|
||||
var calcTimer = null;
|
||||
var lastQuotePrice = null;
|
||||
var priceType = 'limit';
|
||||
var lastCtpReconnectAt = 0;
|
||||
var ctpReconnecting = false;
|
||||
|
||||
function runWhenReady(fn) {
|
||||
if (document.readyState === 'loading') {
|
||||
@@ -63,6 +66,18 @@
|
||||
if (marketHint) marketHint.hidden = priceType !== 'market';
|
||||
}
|
||||
|
||||
function updateCtpBadge(connected) {
|
||||
var ctpBadge = document.getElementById('ctp-badge');
|
||||
var btnConnect = document.getElementById('btn-ctp-connect');
|
||||
if (ctpBadge) {
|
||||
ctpBadge.textContent = connected ? 'CTP 已连接' : 'CTP 未连接';
|
||||
ctpBadge.className = 'badge ' + (connected ? 'profit' : 'planned');
|
||||
}
|
||||
if (btnConnect && connected) {
|
||||
btnConnect.textContent = '重连 CTP';
|
||||
}
|
||||
}
|
||||
|
||||
function refreshQuote() {
|
||||
var sym = selectedSymbol();
|
||||
var lots = isRiskMode() ? (effectiveLots() || 1) : (lotsInput ? lotsInput.value : '1');
|
||||
@@ -83,6 +98,7 @@
|
||||
'<strong>' + (data.name || sym) + '</strong> 精度 ' + m.price_precision +
|
||||
' 位 · 每跳 <strong class="text-accent">' + m.tick_value_total + '</strong> 元(' + lots + ' 手)';
|
||||
}
|
||||
scheduleAutoCalc();
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
@@ -91,14 +107,23 @@
|
||||
quoteTimer = setTimeout(refreshQuote, 400);
|
||||
}
|
||||
|
||||
function calcLotsPreview() {
|
||||
function scheduleAutoCalc() {
|
||||
if (!isRiskMode()) return;
|
||||
clearTimeout(calcTimer);
|
||||
calcTimer = setTimeout(autoCalcLots, 450);
|
||||
}
|
||||
|
||||
function autoCalcLots() {
|
||||
if (!isRiskMode() || !lotsCalc) return;
|
||||
var sym = selectedSymbol();
|
||||
var entry = entryPrice() || parseFloat(priceInput && priceInput.value) || 0;
|
||||
var sl = parseFloat(slInput && slInput.value) || 0;
|
||||
if (!sym || !entry || !sl) {
|
||||
alert('请填写品种、入场价与止损');
|
||||
lotsCalc.value = '';
|
||||
lotsCalc.placeholder = '填写止损后自动计算';
|
||||
return;
|
||||
}
|
||||
lotsCalc.placeholder = '计算中…';
|
||||
fetch('/api/trade/preview', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -111,12 +136,47 @@
|
||||
take_profit: parseFloat(tpInput && tpInput.value) || 0
|
||||
})
|
||||
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||
if (!data.ok) { alert(data.error || '计算失败'); return; }
|
||||
if (lotsCalc) lotsCalc.value = data.lots;
|
||||
if (!data.ok) {
|
||||
lotsCalc.value = '';
|
||||
lotsCalc.placeholder = data.error || '无法计算';
|
||||
return;
|
||||
}
|
||||
lotsCalc.value = data.lots;
|
||||
lotsCalc.placeholder = '填写止损后自动计算';
|
||||
scheduleQuote();
|
||||
}).catch(function () {
|
||||
lotsCalc.placeholder = '计算失败';
|
||||
});
|
||||
}
|
||||
|
||||
function tryAutoCtpReconnect() {
|
||||
if (ctpReconnecting) return;
|
||||
var now = Date.now();
|
||||
if (now - lastCtpReconnectAt < 30000) return;
|
||||
lastCtpReconnectAt = now;
|
||||
ctpReconnecting = true;
|
||||
fetch('/api/ctp/connect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ auto: true })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (d.ok && d.status && d.status.connected) {
|
||||
updateCtpBadge(true);
|
||||
var avail = document.getElementById('avail-display');
|
||||
if (avail && d.account && d.account.available != null) {
|
||||
avail.textContent = Number(d.account.available).toFixed(2);
|
||||
}
|
||||
pollPositions();
|
||||
}
|
||||
})
|
||||
.catch(function () { /* ignore */ })
|
||||
.finally(function () {
|
||||
ctpReconnecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
function postOrder(offset) {
|
||||
var sym = selectedSymbol();
|
||||
if (!sym) { alert('请选择品种'); return; }
|
||||
@@ -129,7 +189,7 @@
|
||||
var lots = effectiveLots();
|
||||
if (offset === 'open') {
|
||||
if (isRiskMode() && lots <= 0) {
|
||||
alert('请先点击「计算」得到手数');
|
||||
alert('请填写止损,系统将自动计算手数');
|
||||
return;
|
||||
}
|
||||
if (!isRiskMode() && lots <= 0) {
|
||||
@@ -216,14 +276,12 @@
|
||||
.then(function (data) {
|
||||
var cap = document.getElementById('cap-display');
|
||||
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
|
||||
var ctpBadge = document.getElementById('ctp-badge');
|
||||
if (ctpBadge && data.ctp_status) {
|
||||
ctpBadge.textContent = data.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
|
||||
ctpBadge.className = 'badge ' + (data.ctp_status.connected ? 'profit' : 'planned');
|
||||
}
|
||||
var connected = data.ctp_status && data.ctp_status.connected;
|
||||
updateCtpBadge(!!connected);
|
||||
var rows = data.rows || [];
|
||||
if (!data.ctp_status || !data.ctp_status.connected) {
|
||||
list.innerHTML = '<div class="empty-hint">请先连接 CTP,持仓将显示柜台实际数据。</div>';
|
||||
if (!connected) {
|
||||
list.innerHTML = '<div class="empty-hint">CTP 未连接,正在尝试自动重连…</div>';
|
||||
tryAutoCtpReconnect();
|
||||
return;
|
||||
}
|
||||
if (!rows.length) {
|
||||
@@ -286,20 +344,18 @@
|
||||
});
|
||||
});
|
||||
|
||||
if (symInput) symInput.addEventListener('input', scheduleQuote);
|
||||
if (symInput) symInput.addEventListener('input', function () { scheduleQuote(); scheduleAutoCalc(); });
|
||||
if (lotsInput) lotsInput.addEventListener('input', scheduleQuote);
|
||||
if (slInput) slInput.addEventListener('input', function () {
|
||||
if (isRiskMode() && lotsCalc) lotsCalc.value = '';
|
||||
});
|
||||
if (slInput) slInput.addEventListener('input', scheduleAutoCalc);
|
||||
if (tpInput) tpInput.addEventListener('input', scheduleAutoCalc);
|
||||
if (dirSelect) dirSelect.addEventListener('change', scheduleAutoCalc);
|
||||
if (priceInput) {
|
||||
priceInput.addEventListener('input', function () {
|
||||
if (priceType === 'limit') priceInput.dataset.manual = '1';
|
||||
scheduleAutoCalc();
|
||||
});
|
||||
}
|
||||
|
||||
var btnCalc = document.getElementById('btn-calc-lots');
|
||||
if (btnCalc) btnCalc.addEventListener('click', calcLotsPreview);
|
||||
|
||||
var btnOpen = document.getElementById('btn-open');
|
||||
var btnClose = document.getElementById('btn-close-pos');
|
||||
if (btnOpen) btnOpen.addEventListener('click', function () { postOrder('open'); });
|
||||
@@ -314,11 +370,12 @@
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (!d.ok) { alert(d.error || '连接失败'); return; }
|
||||
location.reload();
|
||||
updateCtpBadge(true);
|
||||
pollPositions();
|
||||
})
|
||||
.finally(function () {
|
||||
btnConnect.disabled = false;
|
||||
btnConnect.textContent = '连接 CTP';
|
||||
btnConnect.textContent = '重连 CTP';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user