Files
qihuo/static/js/strategy.js
T
dekun 44bec23296 Add futures roll strategy with breakout monitoring and fixed-amount sizing.
Replace percent-based risk with system fixed amount, support market/breakout add modes only, allow pending submission outside trading hours, and fix short breakout geometry plus route registration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 12:05:21 +08:00

266 lines
10 KiB
JavaScript

/* Copyright (c) 2025-2026 马建军. All rights reserved.
* 专有软件 — 未经授权禁止复制、传播、转售。
* 详见 LICENSE.zh-CN.txt
*/
(function () {
var trendPayload = null;
function jsonPost(url, body) {
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body || {})
}).then(function (r) { return r.json(); });
}
function formData(form) {
var fd = new FormData(form);
var o = {};
fd.forEach(function (v, k) { o[k] = v; });
return o;
}
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function showPreview(el, content, ok, isHtml) {
if (!el) return;
if (!content) {
el.hidden = true;
el.textContent = '';
el.innerHTML = '';
return;
}
el.hidden = false;
el.style.color = ok === false ? 'var(--loss)' : '';
if (isHtml) {
el.innerHTML = content;
} else {
el.innerHTML = '';
el.textContent = content;
}
}
function fmtNum(v) {
if (v == null || v === '') return '—';
return String(v);
}
function renderTrendPlanHtml(plan) {
if (!plan) return '';
var summary = plan.summary_line || (
(plan.symbol_name || plan.symbol || '') + ' ' +
(plan.direction_label || '') + ' ' + (plan.period_label || '')
);
var detail = plan.detail_line || '';
var rows = plan.preview_rows || [];
var html = '<div class="trend-summary">' + esc(summary) + '</div>';
if (detail) {
html += '<div class="trend-detail">' + esc(detail) + '</div>';
}
if (rows.length) {
html += '<table class="strategy-preview-table"><thead><tr>' +
'<th>档位</th><th>触发/参考价</th><th>手数</th><th>加仓后均价</th>' +
'<th>止盈盈利(元)</th><th>止损(元)</th><th>盈亏比</th>' +
'</tr></thead><tbody>';
rows.forEach(function (row) {
html += '<tr><td>' + esc(row.level) + '</td>' +
'<td>' + fmtNum(row.price) + '</td>' +
'<td>' + fmtNum(row.lots) + '</td>' +
'<td>' + fmtNum(row.avg_after) + '</td>' +
'<td>' + fmtNum(row.profit_at_tp) + '</td>' +
'<td>' + fmtNum(row.loss_at_sl) + '</td>' +
'<td>' + fmtNum(row.rr_ratio) + '</td></tr>';
});
html += '</tbody></table>';
} else {
html += '<div class="trend-detail">目标手数 ' + fmtNum(plan.target_lots) +
' · 首仓 ' + fmtNum(plan.first_lots) + ' 手</div>';
}
return html;
}
function formatRoll(preview) {
if (!preview) return '';
var lines = [];
if (preview.add_mode_label) lines.push('方式:' + preview.add_mode_label);
if (preview.add_lots != null) lines.push('加仓手数:' + preview.add_lots);
if (preview.qty_after != null) lines.push('合计手数:' + preview.qty_after);
if (preview.avg_entry_after != null) lines.push('加仓后均价:' + preview.avg_entry_after);
if (preview.new_stop_loss != null) lines.push('新止损:' + preview.new_stop_loss);
if (preview.trigger_price != null) lines.push('触发价:' + preview.trigger_price);
if (preview.risk_budget != null) lines.push('风险预算(固定金额):' + preview.risk_budget + ' 元');
if (preview.loss_at_sl != null) lines.push('打到止损亏损:' + preview.loss_at_sl + ' 元');
if (preview.reward_at_tp != null) lines.push('止盈盈利:' + preview.reward_at_tp + ' 元');
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.is_pending) lines.push('(提交后为程序监控,触价后自动市价加仓)');
return lines.length ? lines.join('\n') : JSON.stringify(preview, null, 2);
}
function syncRollModeUi() {
var modeEl = document.getElementById('roll-add-mode');
var breakEl = document.getElementById('roll-break-price');
var execHint = document.getElementById('roll-exec-hint');
var btnExec = document.getElementById('btn-roll-exec');
if (!modeEl) return;
var mode = modeEl.value || 'market';
var isBreak = mode === 'breakout';
if (breakEl) {
breakEl.hidden = !isBreak;
breakEl.required = isBreak;
}
if (execHint) execHint.hidden = false;
if (btnExec) {
btnExec.textContent = mode === 'market' ? '执行滚仓' : '提交监控';
}
}
function syncRollRiskHint() {
/* 固定金额由服务端渲染在 #roll-risk-budget,切换监控单无需变更 */
}
var rollCountdownTimer = null;
function startRollCountdown(btn, payload) {
var sec = 10;
btn.disabled = true;
function tick() {
btn.textContent = '确认执行 (' + sec + 's)';
if (sec <= 0) {
clearInterval(rollCountdownTimer);
rollCountdownTimer = null;
jsonPost('/api/strategy/roll/execute', payload).then(function (d) {
if (!d.ok) { alert(d.error || d.message || '失败'); btn.disabled = false; btn.textContent = '执行滚仓'; return; }
alert(d.message || '已提交');
location.reload();
}).catch(function () {
btn.disabled = false;
btn.textContent = '执行滚仓';
});
return;
}
sec -= 1;
}
tick();
rollCountdownTimer = setInterval(tick, 1000);
}
var trendForm = document.getElementById('trend-form');
var btnPreview = document.getElementById('btn-trend-preview');
var btnExec = document.getElementById('btn-trend-exec');
var previewEl = document.getElementById('trend-preview');
if (btnPreview && trendForm) {
btnPreview.addEventListener('click', function () {
btnPreview.disabled = true;
jsonPost('/api/strategy/trend/preview', formData(trendForm)).then(function (d) {
if (!d.ok) {
showPreview(previewEl, d.error || '预览失败', false, false);
btnExec.hidden = true;
return;
}
trendPayload = formData(trendForm);
showPreview(previewEl, renderTrendPlanHtml(d.plan), true, true);
btnExec.hidden = false;
}).finally(function () {
btnPreview.disabled = false;
});
});
}
if (btnExec) {
btnExec.addEventListener('click', function () {
if (!trendPayload) return;
btnExec.disabled = true;
btnExec.textContent = '执行中…';
jsonPost('/api/strategy/trend/execute', trendPayload).then(function (d) {
if (!d.ok) { alert(d.error); return; }
location.reload();
}).finally(function () {
btnExec.disabled = false;
btnExec.textContent = '确认执行首仓';
});
});
}
var rollForm = document.getElementById('roll-form');
var btnRollP = document.getElementById('btn-roll-preview');
var btnRollE = document.getElementById('btn-roll-exec');
var rollPrev = document.getElementById('roll-preview');
var rollPayload = null;
var rollMonitorSel = document.getElementById('roll-monitor-select');
var rollModeSel = document.getElementById('roll-add-mode');
if (rollModeSel) rollModeSel.addEventListener('change', syncRollModeUi);
if (rollMonitorSel) rollMonitorSel.addEventListener('change', syncRollRiskHint);
syncRollModeUi();
syncRollRiskHint();
if (btnRollP && rollForm) {
btnRollP.addEventListener('click', function () {
btnRollP.disabled = true;
rollPayload = formData(rollForm);
jsonPost('/api/strategy/roll/preview', rollPayload).then(function (d) {
if (!d.ok) {
showPreview(rollPrev, d.error, false, false);
btnRollE.hidden = true;
return;
}
showPreview(rollPrev, formatRoll(d.preview), true, false);
btnRollE.hidden = false;
}).finally(function () {
btnRollP.disabled = false;
});
});
}
if (btnRollE && rollForm) {
btnRollE.addEventListener('click', function () {
var payload = rollPayload || formData(rollForm);
var mode = (payload.add_mode || 'market');
if (mode === 'market') {
if (!confirm('确认执行市价滚仓?')) return;
startRollCountdown(btnRollE, payload);
return;
}
btnRollE.disabled = true;
btnRollE.textContent = '提交中…';
jsonPost('/api/strategy/roll/execute', payload).then(function (d) {
if (!d.ok) { alert(d.error || '失败'); return; }
alert(d.message || '已提交监控');
location.reload();
}).finally(function () {
btnRollE.disabled = false;
syncRollModeUi();
});
});
}
document.querySelectorAll('.roll-cancel-leg').forEach(function (btn) {
btn.addEventListener('click', function () {
var legId = btn.getAttribute('data-leg-id');
if (!legId || !confirm('删除该监控腿?')) return;
jsonPost('/api/strategy/roll/cancel/' + legId, {}).then(function (d) {
if (!d.ok) { alert(d.error); return; }
location.reload();
});
});
});
var btnStop = document.getElementById('btn-trend-stop');
if (btnStop) {
btnStop.addEventListener('click', function () {
var pid = document.querySelector('#trend-stop-form input[name=plan_id]');
jsonPost('/api/strategy/trend/stop', { plan_id: pid ? pid.value : 0 }).then(function (d) {
if (!d.ok) { alert(d.error); return; }
location.reload();
});
});
}
})();