diff --git a/install_trading.py b/install_trading.py index aa9def8..115dd6c 100644 --- a/install_trading.py +++ b/install_trading.py @@ -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( diff --git a/static/css/trade.css b/static/css/trade.css index 710a09d..983a0ce 100644 --- a/static/css/trade.css +++ b/static/css/trade.css @@ -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} diff --git a/static/js/trade.js b/static/js/trade.js index fb894ff..4640803 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -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) ? '' : ''; 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 ? '' : ''; @@ -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(); diff --git a/templates/trade.html b/templates/trade.html index 1472c00..13baa72 100644 --- a/templates/trade.html +++ b/templates/trade.html @@ -269,6 +269,31 @@ + +
开启后不设固定止盈;达 1R 后止损移至开仓价 ± 缓冲跳
+