feat: 止盈止损秒级监控市价平仓记交易记录,并加手数超限提醒。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
.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}
|
||||
.lots-warn{font-size:.7rem;margin-top:.25rem;margin-bottom:0}
|
||||
.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:.28rem .7rem;border-radius:6px;font-size:.75rem;cursor:pointer;flex:1;text-align:center;width:auto}
|
||||
.price-tab.active{border-color:var(--accent);color:var(--accent);font-weight:600;background:rgba(56,189,248,.08)}
|
||||
|
||||
+100
-4
@@ -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();
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user