Files
qihuo/static/js/trade.js
T
dekun 7b8a660309 合并期货下单与持仓监控为统一界面,移除手工录入。
策略与 CTP 自动同步持仓,新增 /api/trading/live 聚合展示与平仓接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 10:18:00 +08:00

164 lines
8.1 KiB
JavaScript

(function () {
var list = document.getElementById('position-live-list');
var pollTimer = null;
function fmtNum(v, digits) {
if (v === null || v === undefined) return '--';
return Number(v).toFixed(digits === undefined ? 2 : digits);
}
function buildPosCard(row) {
var pnlClass = '';
if (row.float_pnl > 0) pnlClass = 'pnl-pos';
if (row.float_pnl < 0) pnlClass = 'pnl-neg';
var pnlText = '--';
if (row.float_pnl != null) {
var sign = row.float_pnl >= 0 ? '+' : '';
pnlText = sign + fmtNum(row.float_pnl) + '元';
if (row.float_pct != null) {
pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)';
}
}
var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--';
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
var closeBtn = '';
if (row.close_url) {
closeBtn =
'<form method="post" action="' + row.close_url + '" style="display:inline" onsubmit="return confirm(\'确认平仓?\')">' +
'<button type="submit" class="btn-del pos-del">平仓</button></form>';
} else if (row.can_close) {
closeBtn =
'<button type="button" class="btn-del pos-del" data-close=\'' + JSON.stringify({
source: row.source,
symbol_code: row.symbol_code,
direction: row.direction,
lots: row.lots,
mark_price: row.mark_price,
monitor_id: row.monitor_id || null
}) + '\'>平仓</button>';
}
var metaParts = ['来源 <strong>' + (row.source_label || row.source) + '</strong>'];
if (row.risk_pct != null) {
metaParts.push('风险 <strong>' + fmtNum(row.risk_pct) + '%≈' + fmtNum(row.risk_amount) + '元</strong>');
}
if (row.tick_value_total != null) {
metaParts.push('每跳 <strong>' + fmtNum(row.tick_value_total) + '元</strong>');
}
var slTp =
'<div class="cell"><label>止损</label><div>' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '</div></div>' +
'<div class="cell"><label>止盈</label><div>' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '</div></div>';
var footerParts = ['张数 ' + row.lots];
if (row.margin != null) footerParts.push('保证金 ' + fmtNum(row.margin) + '元');
if (row.position_pct != null) footerParts.push('仓位占比 ' + fmtNum(row.position_pct) + '%');
if (openT) footerParts.push('开仓 ' + openT);
if (row.holding_duration) footerParts.push('持仓 ' + row.holding_duration);
if (row.est_fee != null) footerParts.push('手续费(估) ' + fmtNum(row.est_fee) + '元');
return (
'<div class="pos-card" data-key="' + (row.key || '') + '">' +
'<div class="pos-card-head">' +
'<div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div>' +
(row.symbol_code && row.symbol_code !== row.symbol ? '<div class="text-muted" style="font-size:.72rem;margin-top:.15rem">' + row.symbol_code + '</div>' : '') +
'</div>' + closeBtn + '</div>' +
'<div class="pos-card-meta">' + metaParts.join(' · ') + '</div>' +
'<div class="pos-metrics">' +
'<div class="cell"><label>成交价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
slTp +
'<div class="cell"><label>盈亏比</label><div>' + rr + '</div></div>' +
'<div class="cell"><label>标记价</label><div>' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '</div></div>' +
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
(row.est_fee != null ?
'<div class="cell"><label>预估手续费</label><div>' + fmtNum(row.est_fee) + '元</div></div>' +
'<div class="cell ' + (row.est_pnl_net > 0 ? 'pnl-pos' : (row.est_pnl_net < 0 ? 'pnl-neg' : '')) + '">' +
'<label>扣费后</label><div>' + (row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + '元' : '--') + '</div></div>'
: '') +
'</div>' +
'<div class="pos-footer">' + footerParts.map(function (s) { return '<span>' + s + '</span>'; }).join('') + '</div>' +
'</div>'
);
}
function closePosition(payload) {
var price = payload.mark_price;
if (!price || price <= 0) {
alert('无法获取现价,请稍后重试');
return;
}
if (!confirm('确认以 ' + price + ' 限价平仓 ' + payload.lots + ' 手?')) return;
fetch('/api/trading/close', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: payload.source,
symbol_code: payload.symbol_code,
direction: payload.direction,
lots: payload.lots,
price: price,
monitor_id: payload.monitor_id
})
}).then(function (r) { return r.json(); }).then(function (d) {
if (!d.ok) { alert(d.error || '平仓失败'); return; }
pollPositions();
}).catch(function () { alert('平仓请求失败'); });
}
function pollPositions() {
if (!list) return;
fetch('/api/trading/live')
.then(function (r) { return r.json(); })
.then(function (data) {
var cap = document.getElementById('cap-display');
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
var ctpBadge = document.getElementById('ctp-badge');
if (ctpBadge && data.ctp_status) {
ctpBadge.textContent = data.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接';
ctpBadge.className = 'badge ' + (data.ctp_status.connected ? 'profit' : 'planned');
}
var rows = data.rows || [];
if (!rows.length) {
list.innerHTML = '<div class="empty-hint">暂无持仓。请先在「策略交易」开仓,或连接 CTP 同步柜台持仓。</div>';
return;
}
list.innerHTML = rows.map(buildPosCard).join('');
list.querySelectorAll('[data-close]').forEach(function (btn) {
btn.addEventListener('click', function () {
closePosition(JSON.parse(btn.getAttribute('data-close')));
});
});
})
.catch(function () {
if (list.innerHTML.indexOf('pos-card') < 0) {
list.innerHTML = '<div class="empty-hint text-loss">加载失败,请刷新页面</div>';
}
});
}
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';
});
});
}
document.addEventListener('DOMContentLoaded', function () {
pollPositions();
pollTimer = setInterval(pollPositions, 2000);
});
})();