feat: 计仓改为固定手数/固定金额,推荐过滤与CTP保证金,下单与持仓UI优化

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 15:31:34 +08:00
parent c302e1f3ca
commit 9772f3d986
11 changed files with 387 additions and 119 deletions
+84 -36
View File
@@ -1,5 +1,6 @@
(function () {
var sizingMode = window.TRADE_SIZING_MODE || 'risk';
var sizingMode = window.TRADE_SIZING_MODE || 'fixed';
if (sizingMode === 'risk') sizingMode = 'amount';
var list = document.getElementById('position-live-list');
var recommendList = document.getElementById('recommend-list');
var symInput = document.getElementById('trade-symbol');
@@ -48,16 +49,17 @@
return (symInput && symInput.value || '').trim();
}
function isRiskMode() {
return sizingMode === 'risk';
function isFixedMode() {
return sizingMode === 'fixed';
}
function isAmountMode() {
return sizingMode === 'amount';
}
function effectiveLots() {
if (isRiskMode()) {
var v = parseInt(lotsCalc && lotsCalc.value, 10);
return v > 0 ? v : 0;
}
return parseInt(lotsInput && lotsInput.value, 10) || 1;
var v = parseInt(lotsCalc && lotsCalc.value, 10);
return v > 0 ? v : 0;
}
function updateRecommendMaxMaps(data) {
@@ -238,9 +240,20 @@
var entry = entryPrice();
var sl = slInput && slInput.value ? parseFloat(slInput.value) : 0;
var tp = tpInput && tpInput.value ? parseFloat(tpInput.value) : 0;
var lots = effectiveLots();
var parts = [];
var rr = calcRR(dir, entry, sl, tp);
if (rr) {
el.textContent = '盈亏比 ' + rr + ':1';
if (rr) parts.push('盈亏比 ' + rr + ':1');
if (sl > 0 && entry > 0 && lots > 0 && lastPreviewMetrics) {
if (lastPreviewMetrics.risk_amount != null) {
parts.push('止损金额 ' + fmtNum(lastPreviewMetrics.risk_amount) + ' 元');
}
if (lastPreviewMetrics.reward_amount != null && tp > 0) {
parts.push('止盈金额 ' + fmtNum(lastPreviewMetrics.reward_amount) + ' 元');
}
}
if (parts.length) {
el.textContent = parts.join(' · ');
el.hidden = false;
} else {
el.textContent = '';
@@ -248,6 +261,7 @@
}
}
var lastPreviewMetrics = null;
function setPriceType(type) {
priceType = type === 'market' ? 'market' : 'limit';
@@ -353,7 +367,7 @@
function refreshQuote() {
var sym = selectedSymbol();
var lots = isRiskMode() ? (effectiveLots() || 1) : (lotsInput ? lotsInput.value : '1');
var lots = effectiveLots() || (isFixedMode() ? (window.TRADE_FIXED_LOTS || 1) : 1);
if (!sym) return;
fetch('/api/trade/quote?symbol=' + encodeURIComponent(sym) + '&lots=' + encodeURIComponent(lots))
.then(function (r) { return r.json(); })
@@ -381,23 +395,39 @@
}
function scheduleAutoCalc() {
if (!isRiskMode()) return;
clearTimeout(calcTimer);
calcTimer = setTimeout(autoCalcLots, 450);
}
function autoCalcLots() {
if (!isRiskMode() || !lotsCalc) return;
if (!lotsCalc) return;
var sym = selectedSymbol();
var entry = entryPrice() || parseFloat(priceInput && priceInput.value) || 0;
var sl = parseFloat(slInput && slInput.value) || 0;
if (!sym || !entry || !sl) {
lotsCalc.value = '';
lotsCalc.placeholder = '填写止损后自动计算';
checkLotsLimit();
var tp = parseFloat(tpInput && tpInput.value) || 0;
if (isFixedMode()) {
var fixedLots = parseInt(window.TRADE_FIXED_LOTS, 10) || 1;
lotsCalc.value = String(fixedLots);
if (lotsInput) lotsInput.value = String(fixedLots);
if (!sym || !entry) {
lastPreviewMetrics = null;
updateRRDisplay();
checkLotsLimit();
return;
}
} else if (isAmountMode()) {
if (!sym || !entry || !sl) {
lotsCalc.value = '';
lotsCalc.placeholder = '填写止损后自动计算';
lastPreviewMetrics = null;
updateRRDisplay();
checkLotsLimit();
return;
}
lotsCalc.placeholder = '计算中…';
} else {
return;
}
lotsCalc.placeholder = '计算中…';
fetch('/api/trade/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -407,20 +437,30 @@
entry: entry,
price: entry,
stop_loss: sl,
take_profit: parseFloat(tpInput && tpInput.value) || 0
take_profit: tp
})
}).then(function (r) { return r.json(); }).then(function (data) {
if (!data.ok) {
lotsCalc.value = '';
lotsCalc.placeholder = data.error || '无法计算';
if (isAmountMode()) {
lotsCalc.value = '';
lotsCalc.placeholder = data.error || '无法计算';
}
lastPreviewMetrics = null;
updateRRDisplay();
checkLotsLimit();
return;
}
lotsCalc.value = data.lots;
lotsCalc.placeholder = '填写止损后自动计算';
lotsCalc.value = String(data.lots || '');
if (lotsInput) lotsInput.value = String(data.lots || '');
lotsCalc.placeholder = isAmountMode() ? '填写止损后自动计算' : '—';
lastPreviewMetrics = data.metrics || null;
updateRRDisplay();
checkLotsLimit();
scheduleQuote();
}).catch(function () {
lotsCalc.placeholder = '计算失败';
if (isAmountMode()) lotsCalc.placeholder = '计算失败';
lastPreviewMetrics = null;
updateRRDisplay();
});
}
@@ -470,12 +510,16 @@
showOrderMsg('开启移动保本须填写止损价', false);
return;
}
if (isRiskMode() && lots <= 0) {
showOrderMsg('请填写止损,系统将自动计算手数', false);
if (isAmountMode() && lots <= 0) {
showOrderMsg('请填写止损,系统将按固定金额自动计算手数', false);
return;
}
if (!isRiskMode() && lots <= 0) {
showOrderMsg('请填写手数', false);
if (isFixedMode() && lots <= 0) {
showOrderMsg('手数无效,请检查系统设置中的固定手数', false);
return;
}
if (lots <= 0) {
showOrderMsg('请填写有效手数', false);
return;
}
var maxLots = maxLotsForSymbol(sym);
@@ -627,10 +671,13 @@
' · 浮盈' +
(slTpBtn ? ' · ' + slTpBtn : '') +
(row.sl_order_active ? ' · <span class="text-profit">止损监控中</span>' : '') +
(row.tp_order_active ? ' · <span class="text-profit">止盈监控中</span>' : '') +
(row.trailing_be ? ' · <span class="text-accent">移动保本' +
(row.trailing_r_locked ? '(锁' + row.trailing_r_locked + 'R)' : '') + '</span>' : '') + '</div>' +
(row.tp_order_active ? ' · <span class="text-profit">止盈监控中</span>' : '') + '</div>' +
'<div class="pos-metrics">' +
'<div class="cell"><label>移动保本</label><div>' +
(row.trailing_be ?
'<span class="text-accent">已开启' + (row.trailing_r_locked ? '(锁' + row.trailing_r_locked + 'R' : '') + '</span>' :
'<span class="text-muted">未开启</span>') +
'</div></div>' +
'<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>' +
@@ -861,7 +908,7 @@
'<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.margin_one_lot != null ? r.margin_one_lot + (r.margin_source === 'ctp' ? ' <span class="text-muted">(柜台)</span>' : '') : '—') + '</td>' +
'<td>' + (r.open_fee_one_lot != null ? r.open_fee_one_lot : '—') + '</td>' +
'<td>' + (r.max_lots != null && r.max_lots > 0 ? r.max_lots : '—') + '</td>' +
'<td><span class="badge ' + (r.status === 'ok' ? 'profit' : 'planned') + '">' + (r.status_label || '') + '</span></td>' +
@@ -904,10 +951,6 @@
checkLotsLimit();
});
}
if (lotsInput) lotsInput.addEventListener('input', function () {
scheduleQuote();
checkLotsLimit();
});
if (lotsCalc) lotsCalc.addEventListener('input', checkLotsLimit);
if (slInput) {
slInput.addEventListener('input', function () {
@@ -945,6 +988,10 @@
runWhenReady(function () {
setPriceType('limit');
if (isFixedMode() && lotsCalc) {
lotsCalc.value = String(window.TRADE_FIXED_LOTS || 1);
if (lotsInput) lotsInput.value = lotsCalc.value;
}
var cached = loadPosCache();
if (cached) {
applyPositionsData(cached);
@@ -965,5 +1012,6 @@
updateSessionUi();
updateRRDisplay();
scheduleQuote();
scheduleAutoCalc();
});
})();