Add key-level auto trade, AI analysis, and trading UX improvements.

Key monitors use 5m close triggers with WeChat alerts and box/convergence auto orders; add pending-order worker, structured WeChat notify, AI settings/messages, session clock, CTP margin sizing, and dual-layer position limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-28 10:36:56 +08:00
parent 0109b59f27
commit 840e88daad
33 changed files with 2514 additions and 143 deletions
+41 -3
View File
@@ -4,6 +4,32 @@
*/
(function () {
var keyTimer = null;
var typeEl = document.getElementById('key-type');
var tradeModeWrap = document.getElementById('key-trade-mode-wrap');
var rrWrap = document.getElementById('key-rr-wrap');
var rrEl = document.getElementById('key-rr');
var trailingWrap = document.getElementById('key-trailing-wrap');
var trailingEl = document.getElementById('key-trailing');
var rowActions = document.getElementById('key-row-actions');
var rowPrices = document.getElementById('key-row-prices');
function isAutoType(typ) {
return typ === '箱体突破' || typ === '收敛突破';
}
function syncKeyForm() {
var typ = typeEl ? typeEl.value : '';
var auto = isAutoType(typ);
if (tradeModeWrap) tradeModeWrap.classList.toggle('is-hidden', !auto);
if (rrWrap) rrWrap.classList.toggle('is-hidden', !auto);
if (trailingWrap) trailingWrap.classList.toggle('is-hidden', !auto);
if (rowActions) rowActions.classList.toggle('key-actions-zone', !auto);
if (rowPrices) rowPrices.classList.toggle('key-zone-mode', !auto);
if (!auto && trailingEl) trailingEl.checked = false;
if (auto && trailingEl && trailingEl.checked && rrEl) {
if (parseFloat(rrEl.value) < 3) rrEl.value = '3';
}
}
function fmtDist(v) {
if (v === null || v === undefined) return '--';
@@ -44,8 +70,20 @@
keyTimer = setInterval(pollKeyPrices, 1000);
}
if (window.qihuoPageBoot) window.qihuoPageBoot(startPolling, '#key-monitor-list');
else if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(startPolling);
else document.addEventListener('DOMContentLoaded', startPolling);
function bindForm() {
if (typeEl) typeEl.addEventListener('change', syncKeyForm);
if (trailingEl) {
trailingEl.addEventListener('change', function () {
if (trailingEl.checked && rrEl && parseFloat(rrEl.value) < 3) {
rrEl.value = '3';
}
});
}
syncKeyForm();
}
if (window.qihuoPageBoot) window.qihuoPageBoot(function () { bindForm(); startPolling(); }, '#key-monitor-list');
else if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(function () { bindForm(); startPolling(); });
else document.addEventListener('DOMContentLoaded', function () { bindForm(); startPolling(); });
if (window.qihuoOnPageLeave) window.qihuoOnPageLeave(stopPolling);
})();
+17
View File
@@ -21,6 +21,23 @@
}
syncSizingFields();
var aiProviderSel = document.getElementById('ai-provider-select');
function syncAiProviderCards() {
if (!aiProviderSel) return;
var val = aiProviderSel.value;
document.querySelectorAll('.settings-ai-card[data-ai-provider]').forEach(function (card) {
var active = card.getAttribute('data-ai-provider') === val;
card.classList.toggle('is-active', active);
var badge = card.querySelector('.settings-ai-card-head .badge');
if (badge) badge.style.display = active ? '' : 'none';
});
}
if (aiProviderSel && !aiProviderSel.dataset.settingsBound) {
aiProviderSel.dataset.settingsBound = '1';
aiProviderSel.addEventListener('change', syncAiProviderCards);
}
syncAiProviderCards();
var SETTINGS_FOLD_KEY = 'qihuo_settings_fold';
function setSettingsFold(el, collapsed) {
if (!el) return;
+4
View File
@@ -91,6 +91,10 @@
if (preview.new_stop_loss != null) lines.push('新止损:' + preview.new_stop_loss);
if (preview.total_lots != null) lines.push('合计手数:' + preview.total_lots);
if (preview.worst_loss != null) lines.push('最坏亏损:' + preview.worst_loss + ' 元');
if (preview.margin_usage_pct != null) {
lines.push('滚仓后保证金占用:' + preview.margin_usage_pct + '%');
}
if (preview.margin_cap_note) lines.push(preview.margin_cap_note);
if (preview.message) lines.push(preview.message);
return lines.length ? lines.join('\n') : JSON.stringify(preview, null, 2);
}
+80
View File
@@ -61,6 +61,7 @@
window.TRADE_FIXED_LOTS = cfg.fixed_lots;
window.TRADE_FIXED_AMOUNT = cfg.fixed_amount;
window.__RECOMMEND_ROWS__ = cfg.recommend_rows || [];
if (cfg.session_clock) applySessionClock(cfg.session_clock);
} catch (e) { /* ignore */ }
}
@@ -119,6 +120,12 @@
var sym = selectedSymbol();
var maxLots = maxLotsForSymbol(sym);
var lots = effectiveLots();
if (lastSizingInfo && lastSizingInfo.capped_by === 'margin' && lastSizingInfo.lots_by_risk > lots) {
warn.hidden = false;
warn.textContent = '以损定仓 ' + lastSizingInfo.lots_by_risk + ' 手,保证金上限 ' +
(lastSizingInfo.max_margin_pct || '') + '% 收紧为 ' + lots + ' 手';
return;
}
if (maxLots > 0 && lots > maxLots) {
warn.hidden = false;
warn.textContent = '已超过最大手数 ' + maxLots + ' 手,请调整手数';
@@ -205,6 +212,7 @@
ctpConnected = !!connected;
ctpConnecting = !!connecting;
isTradingSession = !!data.trading_session;
if (data.session_clock) applySessionClock(data.session_clock);
syncCtpBadgeFromStatus(data.ctp_status || { connected: connected, connecting: connecting });
if (data.ctp_status && typeof data.ctp_status.auto_connect_enabled === 'boolean') {
ctpAutoConnectEnabled = data.ctp_status.auto_connect_enabled;
@@ -380,6 +388,71 @@
}
var lastPreviewMetrics = null;
var lastSizingInfo = null;
var sessionClockBase = null;
var sessionClockTickTimer = null;
function fmtCountdown(secs) {
secs = Math.max(0, parseInt(secs, 10) || 0);
var h = Math.floor(secs / 3600);
var m = Math.floor((secs % 3600) / 60);
var s = secs % 60;
if (h > 0) return h + '小时' + (m < 10 ? '0' : '') + m + '分' + (s < 10 ? '0' : '') + s + '秒';
if (m > 0) return m + '分' + (s < 10 ? '0' : '') + s + '秒';
return s + '秒';
}
function fmtClockNow(d) {
var mo = d.getMonth() + 1;
var da = d.getDate();
var pad = function (n) { return n < 10 ? '0' + n : String(n); };
return pad(mo) + '-' + pad(da) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
function tickSessionClock() {
var base = sessionClockBase;
if (!base || !base.clock) return;
var c = base.clock;
var elapsed = Math.floor((Date.now() - base.at) / 1000);
var nowEl = document.getElementById('clock-now');
if (nowEl && c.now) {
var t = new Date(String(c.now).replace(/-/g, '/'));
if (!isNaN(t.getTime())) {
t.setSeconds(t.getSeconds() + elapsed);
nowEl.textContent = fmtClockNow(t);
}
}
var statusEl = document.getElementById('clock-status');
if (statusEl) {
statusEl.textContent = c.status_label || (c.in_session ? '交易时间段' : '非交易时间段');
statusEl.className = c.in_session ? 'text-profit' : 'text-muted';
}
var detail = document.getElementById('clock-detail');
if (!detail) return;
var html = '';
if (!c.in_session && c.secs_to_open != null) {
html = ' · 下次' + (c.next_open_label || '开盘') + ' ' + (c.next_open_at || '') +
' · 距开盘 <strong>' + fmtCountdown(c.secs_to_open - elapsed) + '</strong>';
} else if (c.in_session) {
if (c.secs_to_break != null) {
html += ' · 距' + (c.break_label || '休盘') + ' <strong>' +
fmtCountdown(c.secs_to_break - elapsed) + '</strong>';
}
if (c.secs_to_close != null) {
html += ' · 距' + (c.close_label || '收盘') + ' <strong>' +
fmtCountdown(c.secs_to_close - elapsed) + '</strong>';
}
}
detail.innerHTML = html;
}
function applySessionClock(clock) {
if (!clock) return;
sessionClockBase = { clock: clock, at: Date.now() };
tickSessionClock();
if (sessionClockTickTimer) clearInterval(sessionClockTickTimer);
sessionClockTickTimer = setInterval(tickSessionClock, 1000);
}
function setPriceType(type) {
priceType = type === 'market' ? 'market' : 'limit';
@@ -633,6 +706,7 @@
lotsCalc.placeholder = data.error || '无法计算';
}
lastPreviewMetrics = null;
lastSizingInfo = null;
updateRRDisplay();
checkLotsLimit();
return;
@@ -641,12 +715,14 @@
if (lotsInput) lotsInput.value = String(data.lots || '');
lotsCalc.placeholder = isAmountMode() ? '填写止损后自动计算' : '—';
lastPreviewMetrics = data.metrics || null;
lastSizingInfo = data.sizing_info || null;
updateRRDisplay();
checkLotsLimit();
scheduleQuote();
}).catch(function () {
if (isAmountMode()) lotsCalc.placeholder = '计算失败';
lastPreviewMetrics = null;
lastSizingInfo = null;
updateRRDisplay();
});
}
@@ -1631,6 +1707,10 @@
}
function cleanupTradePage() {
if (sessionClockTickTimer) {
clearInterval(sessionClockTickTimer);
sessionClockTickTimer = null;
}
if (positionSource) {
positionSource.close();
positionSource = null;