feat: 盈亏比与亏损额度展示,市价FAK报单,修复止盈止损保存失败
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+78
-7
@@ -208,6 +208,43 @@
|
||||
return parseFloat(priceInput && priceInput.value) || 0;
|
||||
}
|
||||
|
||||
function calcRR(direction, entry, sl, tp) {
|
||||
entry = parseFloat(entry);
|
||||
sl = parseFloat(sl);
|
||||
tp = parseFloat(tp);
|
||||
if (!entry || !sl || !tp) return null;
|
||||
var risk, reward;
|
||||
if (direction === 'long') {
|
||||
risk = entry - sl;
|
||||
reward = tp - entry;
|
||||
} else if (direction === 'short') {
|
||||
risk = sl - entry;
|
||||
reward = entry - tp;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (risk <= 0 || reward <= 0) return null;
|
||||
return (reward / risk).toFixed(2);
|
||||
}
|
||||
|
||||
function updateRRDisplay() {
|
||||
var el = document.getElementById('trade-rr-hint');
|
||||
if (!el) return;
|
||||
var dir = dirSelect ? dirSelect.value : 'long';
|
||||
var entry = entryPrice();
|
||||
var sl = slInput && slInput.value ? parseFloat(slInput.value) : 0;
|
||||
var tp = tpInput && tpInput.value ? parseFloat(tpInput.value) : 0;
|
||||
var rr = calcRR(dir, entry, sl, tp);
|
||||
if (rr) {
|
||||
el.textContent = '盈亏比 ' + rr + ':1';
|
||||
el.hidden = false;
|
||||
} else {
|
||||
el.textContent = '';
|
||||
el.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setPriceType(type) {
|
||||
priceType = type === 'market' ? 'market' : 'limit';
|
||||
document.querySelectorAll('.price-tab').forEach(function (btn) {
|
||||
@@ -218,6 +255,7 @@
|
||||
if (priceType === 'market' && lastQuotePrice) priceInput.value = lastQuotePrice;
|
||||
}
|
||||
if (marketHint) marketHint.hidden = priceType !== 'market';
|
||||
updateRRDisplay();
|
||||
}
|
||||
|
||||
function updateCtpBadge(connected, connecting) {
|
||||
@@ -547,7 +585,7 @@
|
||||
'<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
|
||||
lots: row.lots, entry_price: row.entry_price, monitor_id: row.monitor_id || null
|
||||
})) + '">设置止盈止损</button>' : '';
|
||||
var orderBtn = '';
|
||||
if (row.monitor_id && (row.stop_loss != null || row.take_profit != null) && row.can_place_orders) {
|
||||
@@ -561,12 +599,19 @@
|
||||
'<button type="button" class="pos-close-btn" data-close="' + closePayload + '">平仓</button>' : '';
|
||||
var actionBtns = (orderBtn || closeBtn) ?
|
||||
'<div class="pos-card-actions">' + orderBtn + closeBtn + '</div>' : '';
|
||||
var riskMeta = '';
|
||||
if (row.rr_ratio != null) {
|
||||
riskMeta += ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>';
|
||||
}
|
||||
if (row.risk_amount != null) {
|
||||
riskMeta += ' · 亏损额度 <strong>' + fmtNum(row.risk_amount) + ' 元</strong>';
|
||||
}
|
||||
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>' +
|
||||
actionBtns + '</div>' +
|
||||
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong>' +
|
||||
'<div class="pos-card-meta">来源 <strong>' + (row.source_label || 'CTP') + '</strong>' + riskMeta +
|
||||
(row.sync_pending ? ' · <span class="text-muted">同步柜台中…</span>' : '') +
|
||||
' · 浮盈' +
|
||||
(slTpBtn ? ' · ' + slTpBtn : '') +
|
||||
@@ -652,22 +697,33 @@
|
||||
fetch('/api/trading/monitor/upsert', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
symbol_code: payload.symbol_code,
|
||||
direction: payload.direction,
|
||||
lots: payload.lots,
|
||||
entry_price: payload.entry_price,
|
||||
monitor_id: payload.monitor_id || null,
|
||||
stop_loss: sl,
|
||||
take_profit: tp
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (r) {
|
||||
if (!r.ok) {
|
||||
return r.json().catch(function () { return {}; }).then(function (d) {
|
||||
throw new Error(d.error || ('HTTP ' + r.status));
|
||||
});
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(function (d) {
|
||||
if (!d.ok) throw new Error(d.error || '保存失败');
|
||||
pollPositions();
|
||||
})
|
||||
.catch(function (e) {
|
||||
alert(e.message || '保存失败');
|
||||
var msg = e.message || '保存失败';
|
||||
if (msg === 'Failed to fetch') msg = '网络请求失败,请检查服务是否运行';
|
||||
alert(msg);
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '设置止盈止损';
|
||||
@@ -830,13 +886,27 @@
|
||||
checkLotsLimit();
|
||||
});
|
||||
if (lotsCalc) lotsCalc.addEventListener('input', checkLotsLimit);
|
||||
if (slInput) slInput.addEventListener('input', scheduleAutoCalc);
|
||||
if (tpInput) tpInput.addEventListener('input', scheduleAutoCalc);
|
||||
if (dirSelect) dirSelect.addEventListener('change', scheduleAutoCalc);
|
||||
if (slInput) {
|
||||
slInput.addEventListener('input', function () {
|
||||
scheduleAutoCalc();
|
||||
updateRRDisplay();
|
||||
});
|
||||
}
|
||||
if (tpInput) {
|
||||
tpInput.addEventListener('input', function () {
|
||||
scheduleAutoCalc();
|
||||
updateRRDisplay();
|
||||
});
|
||||
}
|
||||
if (dirSelect) dirSelect.addEventListener('change', function () {
|
||||
scheduleAutoCalc();
|
||||
updateRRDisplay();
|
||||
});
|
||||
if (priceInput) {
|
||||
priceInput.addEventListener('input', function () {
|
||||
if (priceType === 'limit') priceInput.dataset.manual = '1';
|
||||
scheduleAutoCalc();
|
||||
updateRRDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -869,6 +939,7 @@
|
||||
}
|
||||
});
|
||||
updateSessionUi();
|
||||
updateRRDisplay();
|
||||
scheduleQuote();
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user