feat: 持仓保证金占比与止盈止损自动委托守护
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+133
-4
@@ -371,28 +371,148 @@
|
||||
function buildPosCard(row) {
|
||||
var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : '');
|
||||
var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--';
|
||||
var netClass = row.est_pnl_net > 0 ? 'pnl-pos' : (row.est_pnl_net < 0 ? 'pnl-neg' : '');
|
||||
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
||||
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
||||
var slTpBtn = (!row.stop_loss && !row.take_profit && row.can_close) ?
|
||||
'<button type="button" class="pos-dismiss-btn pos-sl-btn" data-sl-tp="' +
|
||||
encodeURIComponent(JSON.stringify({
|
||||
symbol_code: row.symbol_code, direction: row.direction,
|
||||
lots: row.lots, entry_price: row.entry_price
|
||||
})) + '">设置止盈止损</button>' : '';
|
||||
var orderBtn = '';
|
||||
if (row.monitor_id && (row.stop_loss != null || row.take_profit != null)) {
|
||||
if (row.can_place_orders) {
|
||||
orderBtn = '<button type="button" class="pos-order-btn" data-place-orders="' + row.monitor_id + '">委托</button>';
|
||||
} else {
|
||||
orderBtn = '<button type="button" class="pos-order-btn pos-order-done" disabled title="止盈止损委托已在柜台">委托</button>';
|
||||
}
|
||||
}
|
||||
var closePayload = encodeURIComponent(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
|
||||
}));
|
||||
var closeBtn = row.can_close ?
|
||||
'<button type="button" class="pos-close-btn" data-close="' + closePayload + '">平仓</button>' : '';
|
||||
var actionBtns = (orderBtn || closeBtn) ?
|
||||
'<div class="pos-card-actions">' + orderBtn + closeBtn + '</div>' : '';
|
||||
return (
|
||||
'<div class="pos-card">' +
|
||||
'<div class="pos-card-head"><div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div>' +
|
||||
'<div class="text-muted" style="font-size:.72rem">' + (row.symbol_code || '') + '</div></div>' + closeBtn + '</div>' +
|
||||
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong> · 柜台浮盈</div>' +
|
||||
'<div class="text-muted" style="font-size:.72rem">' + (row.symbol_code || '') + '</div></div>' +
|
||||
actionBtns + '</div>' +
|
||||
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong> · 柜台浮盈' +
|
||||
(slTpBtn ? ' · ' + slTpBtn : '') +
|
||||
(row.sl_order_active ? ' · <span class="text-profit">止损已挂</span>' : '') +
|
||||
(row.tp_order_active ? ' · <span class="text-profit">止盈已挂</span>' : '') + '</div>' +
|
||||
'<div class="pos-metrics">' +
|
||||
'<div class="cell"><label>持仓均价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
||||
'<div class="cell"><label>当前价格</label><div>' + (row.current_price != null ? fmtNum(row.current_price) : '--') + '</div></div>' +
|
||||
'<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>' +
|
||||
'<div class="cell"><label>占用保证金</label><div>' + (row.margin != null ? fmtNum(row.margin) + ' 元' : '--') + '</div></div>' +
|
||||
'<div class="cell"><label>仓位占比</label><div>' + (row.position_pct != null ? fmtNum(row.position_pct) + '%' : '--') + '</div></div>' +
|
||||
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
||||
'<div class="cell"><label>预估手续费</label><div>' + (row.est_fee != null ? fmtNum(row.est_fee) + ' 元' : '--') + '</div></div>' +
|
||||
'<div class="cell ' + netClass + '"><label>扣费后</label><div>' +
|
||||
(row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + ' 元' : '--') + '</div></div>' +
|
||||
'</div>' + buildPendingHtml(row.pending_orders) +
|
||||
'<div class="pos-footer"><span>' + row.lots + ' 手</span></div></div>'
|
||||
'<div class="pos-footer">' +
|
||||
'<span>' + row.lots + ' 手</span>' +
|
||||
'<span>开仓 ' + (openT || '--') + '</span>' +
|
||||
'<span>持仓 ' + (row.holding_duration || '--') + '</span>' +
|
||||
'<span>保证金 ' + (row.margin != null ? fmtNum(row.margin) + ' 元' : '--') + '</span>' +
|
||||
'<span>仓位 ' + (row.position_pct != null ? fmtNum(row.position_pct) + '%' : '--') + '</span>' +
|
||||
'</div></div>'
|
||||
);
|
||||
}
|
||||
|
||||
function placeMonitorOrders(monitorId, btn) {
|
||||
if (!monitorId) return;
|
||||
if (!confirm('按开仓快照向柜台挂止盈/止损平仓委托?')) return;
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '委托中…';
|
||||
}
|
||||
fetch('/api/trading/monitor/place-orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ monitor_id: monitorId })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (!d.ok) throw new Error(d.error || d.message || '委托失败');
|
||||
alert(d.message || '委托已提交');
|
||||
pollPositions();
|
||||
})
|
||||
.catch(function (e) {
|
||||
alert(e.message || '委托失败');
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '委托';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindPlaceOrderButtons(root) {
|
||||
if (!root) return;
|
||||
root.querySelectorAll('[data-place-orders]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
placeMonitorOrders(parseInt(btn.getAttribute('data-place-orders'), 10), btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promptStopTakeProfit(payload, btn) {
|
||||
var slRaw = prompt('止损价(可留空)', '');
|
||||
if (slRaw === null) return;
|
||||
var tpRaw = prompt('止盈价(可留空)', '');
|
||||
if (tpRaw === null) return;
|
||||
var sl = slRaw.trim() ? parseFloat(slRaw) : null;
|
||||
var tp = tpRaw.trim() ? parseFloat(tpRaw) : null;
|
||||
if (sl == null && tp == null) {
|
||||
alert('请至少填写止损或止盈');
|
||||
return;
|
||||
}
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '保存中…';
|
||||
}
|
||||
fetch('/api/trading/monitor/upsert', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
symbol_code: payload.symbol_code,
|
||||
direction: payload.direction,
|
||||
lots: payload.lots,
|
||||
entry_price: payload.entry_price,
|
||||
stop_loss: sl,
|
||||
take_profit: tp
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (!d.ok) throw new Error(d.error || '保存失败');
|
||||
pollPositions();
|
||||
})
|
||||
.catch(function (e) {
|
||||
alert(e.message || '保存失败');
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '设置止盈止损';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindSlTpButtons(root) {
|
||||
if (!root) return;
|
||||
root.querySelectorAll('[data-sl-tp]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
promptStopTakeProfit(JSON.parse(decodeURIComponent(btn.getAttribute('data-sl-tp'))), btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closePosition(payload, btn) {
|
||||
function doClose(price) {
|
||||
if (!price || price <= 0) { alert('无法获取现价'); return; }
|
||||
@@ -483,6 +603,8 @@
|
||||
}
|
||||
list.innerHTML = rows.map(buildPosCard).join('');
|
||||
bindPendingDismiss(list);
|
||||
bindSlTpButtons(list);
|
||||
bindPlaceOrderButtons(list);
|
||||
list.querySelectorAll('[data-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
closePosition(JSON.parse(decodeURIComponent(btn.getAttribute('data-close'))), btn);
|
||||
@@ -500,9 +622,13 @@
|
||||
if (!recommendList || !data) return;
|
||||
var recCap = document.getElementById('rec-capital');
|
||||
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
||||
var recUpdated = document.getElementById('rec-updated');
|
||||
if (recUpdated && data.updated_at) {
|
||||
recUpdated.textContent = '每日后台更新 · 最近 ' + data.updated_at;
|
||||
}
|
||||
var rows = data.rows || [];
|
||||
if (!rows.length) {
|
||||
recommendList.innerHTML = '<tr><td colspan="6" class="empty-hint">当前资金下暂无推荐品种</td></tr>';
|
||||
recommendList.innerHTML = '<tr><td colspan="9" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
||||
return;
|
||||
}
|
||||
recommendList.innerHTML = rows.map(function (r) {
|
||||
@@ -511,7 +637,10 @@
|
||||
'<td><strong>' + (r.name || '') + '</strong> <span class="text-accent">' + (r.main_code || r.ths || '') + '</span></td>' +
|
||||
'<td>' + (r.exchange || '') + '</td>' +
|
||||
'<td>' + (r.price != null ? r.price : '—') + '</td>' +
|
||||
'<td>' + (r.ref_stop_loss != null ? r.ref_stop_loss : '—') + '</td>' +
|
||||
'<td>' + (r.ref_take_profit != null ? r.ref_take_profit : '—') + '</td>' +
|
||||
'<td>' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '</td>' +
|
||||
'<td>' + (r.open_fee_one_lot != null ? r.open_fee_one_lot : '—') + '</td>' +
|
||||
'<td>' + (r.min_capital_one_lot != null ? r.min_capital_one_lot : '—') + '</td>' +
|
||||
'<td><span class="badge ' + (r.status === 'ok' ? 'profit' : 'planned') + '">' + (r.status_label || '') + '</span></td>' +
|
||||
'</tr>'
|
||||
|
||||
Reference in New Issue
Block a user