feat: 止盈止损秒级监控市价平仓记交易记录,并加手数超限提醒。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 13:16:19 +08:00
parent f05362ea74
commit 598a1407e1
5 changed files with 299 additions and 21 deletions
+100 -4
View File
@@ -19,6 +19,13 @@
var priceType = 'limit';
var lastCtpReconnectAt = 0;
var ctpReconnecting = false;
var isTradingSession = false;
var hasSlTpMonitoring = false;
var ctpConnected = false;
var pollIntervalMs = 0;
var selectedMaxLots = null;
var recommendMaxByProduct = {};
var recommendMaxByCode = {};
function runWhenReady(fn) {
if (document.readyState === 'loading') {
@@ -52,6 +59,63 @@
return parseInt(lotsInput && lotsInput.value, 10) || 1;
}
function updateRecommendMaxMaps(data) {
recommendMaxByProduct = {};
recommendMaxByCode = {};
(data && data.rows || []).forEach(function (r) {
if (!r || r.max_lots <= 0) return;
if (r.status !== 'ok' && r.status !== 'margin_ok') return;
if (r.ths) recommendMaxByProduct[String(r.ths).toLowerCase()] = r.max_lots;
if (r.main_code) recommendMaxByCode[String(r.main_code).toLowerCase()] = r.max_lots;
});
checkLotsLimit();
}
function maxLotsForSymbol(sym) {
if (selectedMaxLots > 0) return selectedMaxLots;
var code = (sym || '').trim().toLowerCase();
if (!code) return 0;
if (recommendMaxByCode[code]) return recommendMaxByCode[code];
var m = code.match(/^([a-z]+)/i);
if (m && recommendMaxByProduct[m[1].toLowerCase()]) {
return recommendMaxByProduct[m[1].toLowerCase()];
}
return 0;
}
function checkLotsLimit() {
var warn = document.getElementById('lots-warn');
if (!warn) return;
var sym = selectedSymbol();
var maxLots = maxLotsForSymbol(sym);
var lots = effectiveLots();
if (maxLots > 0 && lots > maxLots) {
warn.hidden = false;
warn.textContent = '已超过最大手数 ' + maxLots + ' 手,请调整手数';
} else {
warn.hidden = true;
warn.textContent = '';
}
}
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);
}
}
function entryPrice() {
if (priceType === 'market') return lastQuotePrice;
return parseFloat(priceInput && priceInput.value) || 0;
@@ -201,6 +265,7 @@
if (!sym || !entry || !sl) {
lotsCalc.value = '';
lotsCalc.placeholder = '填写止损后自动计算';
checkLotsLimit();
return;
}
lotsCalc.placeholder = '计算中…';
@@ -223,6 +288,7 @@
}
lotsCalc.value = data.lots;
lotsCalc.placeholder = '填写止损后自动计算';
checkLotsLimit();
scheduleQuote();
}).catch(function () {
lotsCalc.placeholder = '计算失败';
@@ -273,6 +339,11 @@
showOrderMsg('请填写手数', false);
return;
}
var maxLots = maxLotsForSymbol(sym);
if (maxLots > 0 && lots > maxLots) {
showOrderMsg('手数 ' + lots + ' 超过最大手数 ' + maxLots + ' 手', false);
return;
}
}
var btnOpen = document.getElementById('btn-open');
if (btnOpen) {
@@ -562,6 +633,8 @@
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) {
@@ -569,6 +642,10 @@
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();
if (!connected) {
if (connecting) {
list.innerHTML = '<div class="empty-hint">CTP 连接中,请稍候…</div>';
@@ -618,6 +695,7 @@
function renderRecommendations(data) {
if (!recommendList || !data) return;
updateRecommendMaxMaps(data);
var recCap = document.getElementById('rec-capital');
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
var recUpdated = document.getElementById('rec-updated');
@@ -666,13 +744,25 @@
});
if (symInput) {
symInput.addEventListener('input', function () { scheduleQuote(); scheduleAutoCalc(); });
symInput.addEventListener('symbol-selected', function () {
symInput.addEventListener('input', function () {
selectedMaxLots = null;
scheduleQuote();
scheduleAutoCalc();
checkLotsLimit();
});
symInput.addEventListener('symbol-selected', function (ev) {
var item = ev.detail || {};
selectedMaxLots = item.max_lots > 0 ? item.max_lots : null;
scheduleQuote();
scheduleAutoCalc();
checkLotsLimit();
});
}
if (lotsInput) lotsInput.addEventListener('input', scheduleQuote);
if (lotsInput) lotsInput.addEventListener('input', function () {
scheduleQuote();
checkLotsLimit();
});
if (lotsCalc) lotsCalc.addEventListener('input', checkLotsLimit);
if (slInput) slInput.addEventListener('input', scheduleAutoCalc);
if (tpInput) tpInput.addEventListener('input', scheduleAutoCalc);
if (dirSelect) dirSelect.addEventListener('change', scheduleAutoCalc);
@@ -697,7 +787,13 @@
setPriceType('limit');
pollPositions();
connectRecommendStream();
pollTimer = setInterval(pollPositions, 3000);
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();
});
scheduleQuote();
});
})();