Apply 200k scope when CTP offline; trailing breakeven order UX.

When SimNow or live CTP is disconnected, default to the four-product whitelist regardless of reference capital. Trailing breakeven defaults off; when enabled hide take-profit and risk-reward, monitor exits via trailing stop only. Document both behaviors in TRADING.md and FEATURES.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-27 23:57:11 +08:00
parent 4f4c4bb9fc
commit e18d5feb72
12 changed files with 220 additions and 48 deletions
+45 -18
View File
@@ -313,6 +313,22 @@
return parseFloat(priceInput && priceInput.value) || 0;
}
function isTrailingBeOn() {
var el = document.getElementById('trailing-be');
return !!(el && el.checked);
}
function updateTrailingBeUi() {
var on = isTrailingBeOn();
var tpField = document.getElementById('field-tp');
var hint = document.getElementById('trailing-be-hint');
if (tpField) tpField.classList.toggle('is-hidden', on);
if (hint) hint.hidden = !on;
if (on && tpInput) tpInput.value = '';
updateRRDisplay();
scheduleAutoCalc();
}
function calcRR(direction, entry, sl, tp) {
entry = parseFloat(entry);
sl = parseFloat(sl);
@@ -335,19 +351,22 @@
function updateRRDisplay() {
var el = document.getElementById('trade-rr-hint');
if (!el) return;
var trailingOn = isTrailingBeOn();
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 tp = trailingOn ? 0 : (tpInput && tpInput.value ? parseFloat(tpInput.value) : 0);
var lots = effectiveLots();
var parts = [];
var rr = calcRR(dir, entry, sl, tp);
if (rr) parts.push('盈亏比 ' + rr + ':1');
if (!trailingOn) {
var rr = calcRR(dir, entry, sl, tp);
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) {
if (!trailingOn && lastPreviewMetrics.reward_amount != null && tp > 0) {
parts.push('止盈金额 ' + fmtNum(lastPreviewMetrics.reward_amount) + ' 元');
}
}
@@ -572,7 +591,7 @@
var sym = selectedSymbol();
var entry = entryPrice() || parseFloat(priceInput && priceInput.value) || 0;
var sl = parseFloat(slInput && slInput.value) || 0;
var tp = parseFloat(tpInput && tpInput.value) || 0;
var tp = isTrailingBeOn() ? 0 : (parseFloat(tpInput && tpInput.value) || 0);
if (isFixedMode()) {
var fixedLots = parseInt(window.TRADE_FIXED_LOTS, 10) || 1;
lotsCalc.value = String(fixedLots);
@@ -671,12 +690,12 @@
}
var lots = effectiveLots();
var trailingBeEl = document.getElementById('trailing-be');
var trailingOn = !!(trailingBeEl && trailingBeEl.checked);
if (offset === 'open') {
if (!isTradingSession) {
showOrderMsg('不在交易时间段', false);
return;
}
var trailingOn = !!(trailingBeEl && trailingBeEl.checked);
if (trailingOn && !(slInput && slInput.value)) {
showOrderMsg('开启移动保本须填写止损价', false);
return;
@@ -713,8 +732,8 @@
price: price,
order_type: priceType,
stop_loss: slInput && slInput.value ? parseFloat(slInput.value) : null,
take_profit: tpInput && tpInput.value ? parseFloat(tpInput.value) : null,
trailing_be: !!(trailingBeEl && trailingBeEl.checked)
take_profit: (trailingOn || !(tpInput && tpInput.value)) ? null : parseFloat(tpInput.value),
trailing_be: trailingOn
};
fetch('/api/trade/order', {
method: 'POST',
@@ -861,10 +880,12 @@
} else if (row.stop_loss != null) {
parts.push('<span class="text-muted">止损已设</span>');
}
if (row.tp_order_active || row.tp_monitoring) {
parts.push('<span class="text-profit">止盈监控中</span>');
} else if (row.take_profit != null) {
parts.push('<span class="text-muted">止盈已设</span>');
if (!row.trailing_be) {
if (row.tp_order_active || row.tp_monitoring) {
parts.push('<span class="text-profit">止盈监控中</span>');
} else if (row.take_profit != null) {
parts.push('<span class="text-muted">止盈已设</span>');
}
}
if (!parts.length) return '<span class="text-muted">未设置</span>';
return parts.join(' · ');
@@ -926,8 +947,8 @@
var metaLine =
'状态 <strong class="text-accent">' + pendingLabel + '</strong>' +
' · 委托价 <strong>' + fmtNum(orderPx) + '</strong>' +
(row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
(row.stop_loss != null || row.take_profit != null ? ' · ' + slTpStatusHtml(row) : '') +
(!row.trailing_be && row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
(row.stop_loss != null || (!row.trailing_be && row.take_profit != null) ? ' · ' + slTpStatusHtml(row) : '') +
(row.trailing_be ? ' · 移动保本 ' + trailingStatusHtml(row) : '') +
(!isCloseOrder ? ' · <span class="text-muted">约 ' + remainMin + ' 分钟内未成交自动撤单</span>' : '');
return (
@@ -988,13 +1009,14 @@
'<div class="pos-card-actions">' + entrustBtn + orderBtn + closeBtn + '</div>' : '';
var metaLine =
'来源 <strong>' + (row.source_label || 'CTP') + '</strong>' +
(row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
(!row.trailing_be && row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
' · 止损金额 <strong class="text-loss">' +
(row.risk_amount != null ? fmtNum(row.risk_amount) + ' 元' : '--') + '</strong>' +
' · 盈利金额 <strong class="text-profit">' +
(row.reward_amount != null ? fmtNum(row.reward_amount) + ' 元' : '--') + '</strong>' +
(!row.trailing_be ?
(' · 盈利金额 <strong class="text-profit">' +
(row.reward_amount != null ? fmtNum(row.reward_amount) + ' 元' : '--') + '</strong>') : '') +
' · ' + slTpStatusHtml(row) +
' · 移动保本 ' + trailingStatusHtml(row) +
(row.trailing_be ? ' · 移动保本 ' + trailingStatusHtml(row) : '') +
(slTpBtn ? ' · ' + slTpBtn : '') +
(function () {
if (row.order_state === 'pending' || !row.monitor_id) return '';
@@ -1565,6 +1587,11 @@
scheduleAutoCalc();
updateRRDisplay();
});
var trailingBeEl = document.getElementById('trailing-be');
if (trailingBeEl) {
trailingBeEl.addEventListener('change', updateTrailingBeUi);
updateTrailingBeUi();
}
if (priceInput) {
priceInput.addEventListener('input', function () {
if (priceType === 'limit') priceInput.dataset.manual = '1';