Add trailing BE to SL/TP dialog and speed up position refresh.
Use modal for monitor upsert with trailing checkbox, refresh CTP tick every second, and push full snapshot after orders. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+19
-7
@@ -1045,11 +1045,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
|
||||
holding = _holding_duration(open_time, now_iso) if open_time else ""
|
||||
|
||||
if (mark is None or float(mark or 0) <= 0) and not fast and ctp_status(mode).get("connected"):
|
||||
if ctp_status(mode).get("connected"):
|
||||
live_mark = ctp_get_tick_price(mode, sym)
|
||||
if live_mark and live_mark > 0:
|
||||
mark = live_mark
|
||||
if (mark is None or float(mark or 0) <= 0) and not fast and codes:
|
||||
elif (mark is None or float(mark or 0) <= 0) and not fast and codes:
|
||||
mark = fetch_price(
|
||||
sym,
|
||||
codes.get("market_code", ""),
|
||||
@@ -1780,6 +1780,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
return jsonify({"ok": False, "error": "缺少品种代码"}), 400
|
||||
if sl is None and tp is None:
|
||||
return jsonify({"ok": False, "error": "请至少填写止损或止盈"}), 400
|
||||
trailing_on = bool(d.get("trailing_be"))
|
||||
if trailing_on and sl is None:
|
||||
return jsonify({"ok": False, "error": "移动保本须填写止损价"}), 400
|
||||
if trailing_on:
|
||||
tp = None
|
||||
mode = get_trading_mode(get_setting)
|
||||
conn = get_db()
|
||||
try:
|
||||
@@ -1801,7 +1806,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
break
|
||||
if not has_pos:
|
||||
return jsonify({"ok": False, "error": "未找到对应持仓"}), 400
|
||||
trailing_be = 1 if d.get("trailing_be") else (
|
||||
trailing_be = 1 if trailing_on else (
|
||||
int(mon.get("trailing_be") or 0) if mon else 0
|
||||
)
|
||||
mid = _upsert_open_monitor(
|
||||
@@ -1814,8 +1819,15 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
tp=tp,
|
||||
trailing_be=trailing_be,
|
||||
)
|
||||
if trailing_on and sl is not None:
|
||||
conn.execute(
|
||||
"""UPDATE trade_order_monitors SET
|
||||
take_profit=NULL, initial_stop_loss=?, trailing_r_locked=0
|
||||
WHERE id=?""",
|
||||
(sl, mid),
|
||||
)
|
||||
conn.commit()
|
||||
_push_position_snapshot_async()
|
||||
_push_position_snapshot_async(fast=False)
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"monitor_id": mid,
|
||||
@@ -2419,7 +2431,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}"
|
||||
)
|
||||
conn.close()
|
||||
_push_position_snapshot_async()
|
||||
_push_position_snapshot_async(fast=False)
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"result": result,
|
||||
@@ -3196,8 +3208,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
|
||||
start_position_worker(
|
||||
refresh_fn=_position_worker_refresh,
|
||||
interval=2,
|
||||
idle_interval=5,
|
||||
interval=1,
|
||||
idle_interval=3,
|
||||
)
|
||||
_bootstrap_trading_runtime()
|
||||
start_ctp_fee_worker(
|
||||
|
||||
@@ -116,6 +116,13 @@
|
||||
.pos-order-btn:disabled,.pos-order-btn.pos-order-done{opacity:.55;cursor:default;border-color:var(--table-border);background:var(--card-inner);color:var(--text-muted)}
|
||||
.pos-order-btn:disabled:not(.pos-order-done){cursor:wait}
|
||||
|
||||
.sl-tp-modal{max-width:420px;width:100%}
|
||||
.sl-tp-modal-fields{display:flex;flex-direction:column;gap:.75rem;margin-bottom:1rem}
|
||||
.sl-tp-modal-fields .trade-field{margin:0}
|
||||
.sl-tp-modal-trailing{margin-top:.15rem}
|
||||
.sl-tp-modal-actions{display:flex;gap:.5rem;justify-content:flex-end}
|
||||
.sl-tp-modal-actions .btn-secondary,.sl-tp-modal-actions .btn-primary{width:auto;min-width:5rem;padding:.45rem 1rem;font-size:.85rem}
|
||||
|
||||
@media (min-width:768px) and (max-width:1100px){
|
||||
.trade-split .card{min-height:420px}
|
||||
.trade-form-line.line-3{grid-template-columns:1fr 1fr}
|
||||
|
||||
+93
-14
@@ -1061,16 +1061,18 @@
|
||||
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
||||
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
||||
var closeAllowed = row.close_allowed !== false && isTradingSession;
|
||||
var slTpBtn = (!row.stop_loss && !row.take_profit && row.can_close) ?
|
||||
var slTpBtn = (!row.stop_loss && !row.take_profit && !row.trailing_be && 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, monitor_id: row.monitor_id || null
|
||||
lots: row.lots, entry_price: row.entry_price, monitor_id: row.monitor_id || null,
|
||||
trailing_be: !!row.trailing_be
|
||||
})) + '">设置止盈止损</button>' : '';
|
||||
var editPayload = encodeURIComponent(JSON.stringify({
|
||||
symbol_code: row.symbol_code, direction: row.direction,
|
||||
lots: row.lots, entry_price: row.entry_price, monitor_id: row.monitor_id || null,
|
||||
stop_loss: row.stop_loss, take_profit: row.take_profit
|
||||
stop_loss: row.stop_loss, take_profit: row.take_profit,
|
||||
trailing_be: !!row.trailing_be
|
||||
}));
|
||||
var entrustBtn = row.can_close ?
|
||||
'<button type="button" class="pos-order-btn pos-entrust-btn" data-edit-sl-tp="' + editPayload + '">委托</button>' : '';
|
||||
@@ -1172,16 +1174,66 @@
|
||||
});
|
||||
}
|
||||
|
||||
function promptStopTakeProfit(payload, btn, btnLabel) {
|
||||
btnLabel = btnLabel || '设置止盈止损';
|
||||
var slDefault = payload.stop_loss != null && payload.stop_loss !== '' ? String(payload.stop_loss) : '';
|
||||
var tpDefault = payload.take_profit != null && payload.take_profit !== '' ? String(payload.take_profit) : '';
|
||||
var slRaw = prompt('止损价(可留空)', slDefault);
|
||||
if (slRaw === null) return;
|
||||
var tpRaw = prompt('止盈价(可留空)', tpDefault);
|
||||
if (tpRaw === null) return;
|
||||
var sl = slRaw.trim() ? parseFloat(slRaw) : null;
|
||||
var tp = tpRaw.trim() ? parseFloat(tpRaw) : null;
|
||||
var slTpModalState = { payload: null, btn: null, btnLabel: '设置止盈止损' };
|
||||
|
||||
function syncSlTpModalTrailingUi() {
|
||||
var trailingEl = document.getElementById('sl-tp-modal-trailing');
|
||||
var tpWrap = document.getElementById('sl-tp-modal-tp-wrap');
|
||||
var hint = document.getElementById('sl-tp-modal-trailing-hint');
|
||||
var on = !!(trailingEl && trailingEl.checked);
|
||||
if (tpWrap) tpWrap.hidden = on;
|
||||
if (hint) hint.hidden = !on;
|
||||
if (on) {
|
||||
var tpInput = document.getElementById('sl-tp-modal-tp');
|
||||
if (tpInput) tpInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function closeSlTpModal() {
|
||||
var mask = document.getElementById('sl-tp-modal');
|
||||
if (mask) mask.classList.remove('show');
|
||||
slTpModalState.payload = null;
|
||||
slTpModalState.btn = null;
|
||||
}
|
||||
|
||||
function openSlTpModal(payload, btn, btnLabel) {
|
||||
var mask = document.getElementById('sl-tp-modal');
|
||||
var title = document.getElementById('sl-tp-modal-title');
|
||||
var slInput = document.getElementById('sl-tp-modal-sl');
|
||||
var tpInput = document.getElementById('sl-tp-modal-tp');
|
||||
var trailingEl = document.getElementById('sl-tp-modal-trailing');
|
||||
if (!mask || !slInput) return;
|
||||
slTpModalState.payload = payload;
|
||||
slTpModalState.btn = btn || null;
|
||||
slTpModalState.btnLabel = btnLabel || '设置止盈止损';
|
||||
if (title) title.textContent = slTpModalState.btnLabel;
|
||||
slInput.value = payload.stop_loss != null && payload.stop_loss !== '' ? String(payload.stop_loss) : '';
|
||||
if (tpInput) {
|
||||
tpInput.value = payload.take_profit != null && payload.take_profit !== '' ? String(payload.take_profit) : '';
|
||||
}
|
||||
if (trailingEl) trailingEl.checked = !!payload.trailing_be;
|
||||
syncSlTpModalTrailingUi();
|
||||
mask.classList.add('show');
|
||||
slInput.focus();
|
||||
}
|
||||
|
||||
function saveSlTpModal() {
|
||||
var payload = slTpModalState.payload;
|
||||
if (!payload) return;
|
||||
var btn = slTpModalState.btn;
|
||||
var btnLabel = slTpModalState.btnLabel;
|
||||
var slInput = document.getElementById('sl-tp-modal-sl');
|
||||
var tpInput = document.getElementById('sl-tp-modal-tp');
|
||||
var trailingEl = document.getElementById('sl-tp-modal-trailing');
|
||||
var trailingOn = !!(trailingEl && trailingEl.checked);
|
||||
var slRaw = slInput && slInput.value ? slInput.value.trim() : '';
|
||||
var tpRaw = trailingOn ? '' : (tpInput && tpInput.value ? tpInput.value.trim() : '');
|
||||
var sl = slRaw ? parseFloat(slRaw) : null;
|
||||
var tp = tpRaw ? parseFloat(tpRaw) : null;
|
||||
if (trailingOn && (sl == null || isNaN(sl))) {
|
||||
alert('移动保本须填写止损价');
|
||||
return;
|
||||
}
|
||||
if (sl == null && tp == null) {
|
||||
alert('请至少填写止损或止盈');
|
||||
return;
|
||||
@@ -1190,6 +1242,8 @@
|
||||
btn.disabled = true;
|
||||
btn.textContent = '保存中…';
|
||||
}
|
||||
var saveBtn = document.getElementById('sl-tp-modal-save');
|
||||
if (saveBtn) saveBtn.disabled = true;
|
||||
fetch('/api/trading/monitor/upsert', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -1201,7 +1255,8 @@
|
||||
entry_price: payload.entry_price,
|
||||
monitor_id: payload.monitor_id || null,
|
||||
stop_loss: sl,
|
||||
take_profit: tp
|
||||
take_profit: tp,
|
||||
trailing_be: trailingOn
|
||||
})
|
||||
})
|
||||
.then(function (r) {
|
||||
@@ -1214,6 +1269,7 @@
|
||||
})
|
||||
.then(function (d) {
|
||||
if (!d.ok) throw new Error(d.error || '保存失败');
|
||||
closeSlTpModal();
|
||||
pollPositions();
|
||||
})
|
||||
.catch(function (e) {
|
||||
@@ -1224,9 +1280,31 @@
|
||||
btn.disabled = false;
|
||||
btn.textContent = btnLabel;
|
||||
}
|
||||
})
|
||||
.finally(function () {
|
||||
if (saveBtn) saveBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function bindSlTpModal() {
|
||||
var mask = document.getElementById('sl-tp-modal');
|
||||
var trailingEl = document.getElementById('sl-tp-modal-trailing');
|
||||
var cancelBtn = document.getElementById('sl-tp-modal-cancel');
|
||||
var saveBtn = document.getElementById('sl-tp-modal-save');
|
||||
if (trailingEl) trailingEl.addEventListener('change', syncSlTpModalTrailingUi);
|
||||
if (cancelBtn) cancelBtn.addEventListener('click', closeSlTpModal);
|
||||
if (saveBtn) saveBtn.addEventListener('click', saveSlTpModal);
|
||||
if (mask) {
|
||||
mask.addEventListener('click', function (e) {
|
||||
if (e.target === mask) closeSlTpModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function promptStopTakeProfit(payload, btn, btnLabel) {
|
||||
openSlTpModal(payload, btn, btnLabel || '设置止盈止损');
|
||||
}
|
||||
|
||||
function bindSlTpButtons(root) {
|
||||
if (!root) return;
|
||||
root.querySelectorAll('[data-sl-tp]').forEach(function (btn) {
|
||||
@@ -1754,6 +1832,7 @@
|
||||
}
|
||||
pollPositions();
|
||||
connectPositionStream();
|
||||
bindSlTpModal();
|
||||
initCtpOnLoad();
|
||||
connectRecommendStream();
|
||||
initRecommendSortControls();
|
||||
|
||||
@@ -269,6 +269,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sl-tp-modal" class="modal-mask" role="dialog" aria-labelledby="sl-tp-modal-title">
|
||||
<div class="modal-box sl-tp-modal">
|
||||
<h3 id="sl-tp-modal-title">设置止盈止损</h3>
|
||||
<div class="sl-tp-modal-fields">
|
||||
<div class="trade-field">
|
||||
<label class="text-label" for="sl-tp-modal-sl">止损</label>
|
||||
<input type="number" id="sl-tp-modal-sl" step="any" placeholder="必填(移动保本须填止损)">
|
||||
</div>
|
||||
<div class="trade-field" id="sl-tp-modal-tp-wrap">
|
||||
<label class="text-label" for="sl-tp-modal-tp">止盈</label>
|
||||
<input type="number" id="sl-tp-modal-tp" step="any" placeholder="可留空">
|
||||
</div>
|
||||
<label class="trailing-be-toggle sl-tp-modal-trailing">
|
||||
<input type="checkbox" id="sl-tp-modal-trailing">
|
||||
<span>移动保本</span>
|
||||
</label>
|
||||
<p class="hint" id="sl-tp-modal-trailing-hint" hidden>开启后不设固定止盈;达 1R 后止损移至开仓价 ± 缓冲跳</p>
|
||||
</div>
|
||||
<div class="sl-tp-modal-actions">
|
||||
<button type="button" class="btn-secondary" id="sl-tp-modal-cancel">取消</button>
|
||||
<button type="button" class="btn-primary" id="sl-tp-modal-save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script type="application/json" id="trade-page-data">{{ {
|
||||
|
||||
Reference in New Issue
Block a user