87aef80594
程序报单状态与推荐表内嵌同一页面,/recommend 跳转至 #recommend。 Co-authored-by: Cursor <cursoragent@cursor.com>
171 lines
8.6 KiB
JavaScript
171 lines
8.6 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 recCap = document.getElementById('rec-capital');
|
|
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
|
var riskBadge = document.getElementById('risk-badge');
|
|
if (riskBadge && data.risk_status) {
|
|
riskBadge.textContent = data.risk_status.status_label;
|
|
riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss');
|
|
}
|
|
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);
|
|
});
|
|
})();
|