6e423eebfb
新增 vnpy CTP 桥接、以损定仓/固定张数、趋势回调与滚仓策略、按资金推荐品种及交易风控;模拟盘走 SimNow,实盘预留期货公司配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
6.2 KiB
JavaScript
128 lines
6.2 KiB
JavaScript
(function () {
|
|
var symInput = document.getElementById('trade-symbol');
|
|
var lotsInput = document.getElementById('trade-lots');
|
|
var priceInput = document.getElementById('trade-price');
|
|
var footer = document.getElementById('trade-footer');
|
|
var slInput = document.getElementById('trade-sl');
|
|
var tpInput = document.getElementById('trade-tp');
|
|
var debounceTimer;
|
|
|
|
function selectedSymbol() {
|
|
return (symInput && symInput.value || '').trim();
|
|
}
|
|
|
|
function refreshQuote() {
|
|
var sym = selectedSymbol();
|
|
var lots = lotsInput ? lotsInput.value : '1';
|
|
if (!sym) return;
|
|
fetch('/api/trade/quote?symbol=' + encodeURIComponent(sym) + '&lots=' + encodeURIComponent(lots))
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
if (!data.ok) return;
|
|
if (priceInput && !priceInput.dataset.manual && data.price) {
|
|
priceInput.value = data.price;
|
|
}
|
|
var px = data.price != null ? data.price : '—';
|
|
['px-long', 'px-short'].forEach(function (id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.textContent = px;
|
|
});
|
|
var ml = document.getElementById('max-long');
|
|
var ms = document.getElementById('max-short');
|
|
if (ml) ml.textContent = '≤' + (data.max_open_long || '—');
|
|
if (ms) ms.textContent = '≤' + (data.max_open_short || '—');
|
|
document.getElementById('pos-long').textContent = '≤' + (data.pos_long || 0);
|
|
document.getElementById('pos-short').textContent = '≤' + (data.pos_short || 0);
|
|
if (footer && data.metrics) {
|
|
var m = data.metrics;
|
|
footer.innerHTML =
|
|
'<p><strong>' + (data.name || sym) + '</strong> ' + (data.footer_text || '') + '</p>' +
|
|
'<p>价格精度 <strong>' + m.price_precision + '</strong> 位 · ' +
|
|
'最小变动 <strong>' + m.tick_size + '</strong> · ' +
|
|
'每跳 <strong>' + m.tick_value_per_lot + '</strong> 元/手 · ' +
|
|
'当前 <strong>' + lots + '</strong> 手每跳合计 <strong class="text-accent">' + m.tick_value_total + '</strong> 元</p>' +
|
|
(m.margin_total ? '<p class="text-muted">预估保证金约 ' + m.margin_total + ' 元</p>' : '');
|
|
}
|
|
}).catch(function () {});
|
|
}
|
|
|
|
function scheduleRefresh() {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(refreshQuote, 400);
|
|
}
|
|
|
|
if (symInput) symInput.addEventListener('input', scheduleRefresh);
|
|
if (lotsInput) lotsInput.addEventListener('input', scheduleRefresh);
|
|
if (priceInput) {
|
|
priceInput.addEventListener('input', function () {
|
|
priceInput.dataset.manual = '1';
|
|
});
|
|
}
|
|
|
|
function postOrder(offset, direction) {
|
|
var sym = selectedSymbol();
|
|
if (!sym) { alert('请选择品种'); return; }
|
|
var body = {
|
|
symbol: sym,
|
|
offset: offset,
|
|
direction: direction,
|
|
lots: parseInt(lotsInput.value, 10) || 1,
|
|
price: parseFloat(priceInput.value) || 0,
|
|
stop_loss: slInput ? parseFloat(slInput.value) : null,
|
|
take_profit: tpInput ? parseFloat(tpInput.value) : null
|
|
};
|
|
fetch('/api/trade/order', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
|
if (!data.ok) { alert(data.error || '下单失败'); return; }
|
|
alert('已提交 ' + (data.lots || '') + ' 手');
|
|
location.reload();
|
|
});
|
|
}
|
|
|
|
var btnLong = document.getElementById('btn-open-long');
|
|
var btnShort = document.getElementById('btn-open-short');
|
|
var btnCloseL = document.getElementById('btn-close-long');
|
|
var btnCloseS = document.getElementById('btn-close-short');
|
|
if (btnLong) btnLong.addEventListener('click', function () { postOrder('open', 'long'); });
|
|
if (btnShort) btnShort.addEventListener('click', function () { postOrder('open', 'short'); });
|
|
if (btnCloseL) btnCloseL.addEventListener('click', function () { postOrder('close', 'long'); });
|
|
if (btnCloseS) btnCloseS.addEventListener('click', function () { postOrder('close', 'short'); });
|
|
|
|
var btnConnect = document.getElementById('btn-ctp-connect');
|
|
if (btnConnect) {
|
|
btnConnect.addEventListener('click', function () {
|
|
btnConnect.disabled = true;
|
|
btnConnect.textContent = '连接中…';
|
|
fetch('/api/ctp/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (d) {
|
|
if (!d.ok) { alert(d.error || '连接失败'); return; }
|
|
location.reload();
|
|
})
|
|
.finally(function () {
|
|
btnConnect.disabled = false;
|
|
btnConnect.textContent = '连接 CTP';
|
|
});
|
|
});
|
|
}
|
|
|
|
setInterval(function () {
|
|
fetch('/api/account_snapshot').then(function (r) { return r.json(); }).then(function (d) {
|
|
var cap = document.getElementById('cap-display');
|
|
if (cap && d.capital != null) cap.textContent = Number(d.capital).toFixed(2);
|
|
var badge = document.getElementById('risk-badge');
|
|
if (badge && d.risk_status) badge.textContent = d.risk_status.status_label;
|
|
var ctpBadge = document.getElementById('ctp-badge');
|
|
if (ctpBadge && d.ctp_status) {
|
|
ctpBadge.textContent = d.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
|
|
ctpBadge.className = 'badge ' + (d.ctp_status.connected ? 'profit' : 'planned');
|
|
}
|
|
}).catch(function () {});
|
|
}, 5000);
|
|
|
|
scheduleRefresh();
|
|
})();
|