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:
@@ -0,0 +1,12 @@
|
||||
.ai-page .card-body{display:flex;flex-direction:column;gap:0;min-height:0}
|
||||
.ai-usage{margin-bottom:.85rem;font-size:.84rem;color:var(--text-muted)}
|
||||
.ai-usage summary{cursor:pointer;color:var(--accent);font-weight:600;margin-bottom:.35rem}
|
||||
.ai-usage-body ul{margin:.25rem 0 0 1.1rem;padding:0;line-height:1.55}
|
||||
.ai-usage-body a{color:var(--accent)}
|
||||
.ai-section-label{font-size:.9rem;margin:0 0 .65rem;color:var(--text-title);font-weight:600}
|
||||
.ai-msg-list{max-height:min(70vh,720px);overflow:auto;padding-right:.25rem}
|
||||
.ai-msg{border:1px solid var(--border);border-radius:10px;padding:.85rem 1rem;margin-bottom:.75rem;background:rgba(255,255,255,.02)}
|
||||
.ai-msg-head{display:flex;justify-content:space-between;gap:.5rem;font-size:.75rem;color:var(--text-muted);margin-bottom:.35rem}
|
||||
.ai-msg-kind{text-transform:uppercase;letter-spacing:.04em;color:var(--accent)}
|
||||
.ai-msg-title{font-size:.95rem;margin:0 0 .5rem;color:var(--text-title)}
|
||||
.ai-msg-body{margin:0;white-space:pre-wrap;font-family:inherit;font-size:.84rem;line-height:1.55;color:var(--text-main)}
|
||||
@@ -0,0 +1,13 @@
|
||||
.key-rules{margin-bottom:.75rem;font-size:.82rem;color:var(--text-muted)}
|
||||
.key-rules summary{cursor:pointer;color:var(--accent);font-weight:600;margin-bottom:.35rem}
|
||||
.key-rules-body{padding:.35rem 0 .15rem}
|
||||
.key-rules-body ul{margin:.25rem 0 .5rem 1.1rem;padding:0}
|
||||
.key-rules-body li{margin:.15rem 0}
|
||||
.key-check{display:inline-flex;align-items:center;gap:.35rem;font-size:.82rem;flex:1;min-width:0;margin:0}
|
||||
.key-check-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.line-key-actions{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:nowrap}
|
||||
.line-key-actions.is-hidden{display:none!important}
|
||||
.line-key-actions .key-submit-btn{flex-shrink:0;min-width:5.5rem;padding:.55rem 1.1rem}
|
||||
.line-key-actions.key-actions-zone{justify-content:flex-end}
|
||||
.line-key-actions.key-actions-zone .key-check{display:none}
|
||||
#key-trade-mode-wrap.is-hidden,#key-rr-wrap.is-hidden{display:none!important}
|
||||
@@ -22,6 +22,8 @@
|
||||
.trade-top-bar-main{display:flex;flex-wrap:wrap;gap:.5rem .65rem;align-items:center;flex:1;min-width:0}
|
||||
.trade-top-bar-actions{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center}
|
||||
.trade-top-hint{font-size:.72rem;white-space:nowrap}
|
||||
.trade-session-clock{font-size:.78rem;line-height:1.45}
|
||||
.session-clock-detail strong{color:var(--accent);font-weight:600}
|
||||
.btn-ctp-sm{padding:.4rem .9rem;font-size:.8rem;width:auto;white-space:nowrap}
|
||||
.trade-card{margin-bottom:0;height:100%;display:flex;flex-direction:column}
|
||||
.trade-card h2{margin-bottom:.35rem;flex-shrink:0}
|
||||
@@ -85,7 +87,10 @@
|
||||
.pos-main-badge{font-size:.68rem;vertical-align:middle}
|
||||
.pos-change-up{color:var(--profit)}
|
||||
.rec-change-down{color:var(--loss)}
|
||||
#recommend .trade-table-wrap{max-height:min(70vh,520px)}
|
||||
#recommend .trade-table-wrap{max-height:none;overflow:visible}
|
||||
#recommend.card{height:auto}
|
||||
#recommend .card-body{display:flex;flex-direction:column}
|
||||
#recommend .trade-table-wrap{flex:0 0 auto}
|
||||
#positions .card-body.card-scroll{flex:1;max-height:none;overflow-y:auto}
|
||||
.pos-pending-orders{margin-top:.55rem;padding-top:.55rem;border-top:1px dashed var(--table-border)}
|
||||
.pos-pending-orders .pending-title{font-size:.68rem;color:var(--text-muted);margin-bottom:.35rem}
|
||||
|
||||
+41
-3
@@ -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);
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user