From aa92952b2d290feb73858534305332163e337faf Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 28 May 2026 12:43:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=B8=80=E9=94=AE=E4=BF=9D?= =?UTF-8?q?=E6=9C=ACbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_binance/app.py | 75 ------------ crypto_monitor_binance/templates/index.html | 113 +---------------- crypto_monitor_gate/.env.example | 2 + crypto_monitor_gate/app.py | 122 +++++++------------ crypto_monitor_gate/templates/index.html | 113 +---------------- crypto_monitor_gate_bot/app.py | 69 ----------- crypto_monitor_gate_bot/templates/index.html | 110 +---------------- crypto_monitor_okx/app.py | 75 ------------ crypto_monitor_okx/templates/index.html | 113 +---------------- manual_trading_hub/hub.py | 17 ++- order_manual_breakeven_lib.py | 63 ---------- 11 files changed, 68 insertions(+), 804 deletions(-) delete mode 100644 order_manual_breakeven_lib.py diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 9474a2d..46f499c 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -72,7 +72,6 @@ from key_sl_tp_lib import ( sl_tp_mode_label, sl_tp_plan_summary_text, ) -from order_manual_breakeven_lib import apply_order_manual_breakeven from key_monitor_lib import ( KEY_DIRECTION_WATCH, KEY_MONITOR_ALERT_ONLY_TYPES, @@ -6120,80 +6119,6 @@ def api_order_cancel_tpsl(order_id): return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400 -@app.route("/api/order//manual_breakeven", methods=["POST"]) -@login_required -def api_order_manual_breakeven(order_id): - data = request.get_json(silent=True) or {} - try: - offset_pct = float( - data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2")) - ) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400 - if offset_pct < 0 or offset_pct > 10: - return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400 - conn = get_db() - row = conn.execute( - "SELECT * FROM order_monitors WHERE id=? AND status='active'", - (order_id,), - ).fetchone() - if not row: - conn.close() - return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404 - ok, err, new_sl = apply_order_manual_breakeven( - row, - offset_pct, - calc_stop_fn=calc_trend_manual_breakeven_stop, - round_price_fn=round_price_to_exchange, - resolve_ex_sym_fn=resolve_monitor_exchange_symbol, - get_position_fn=get_live_position_contracts, - replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange, - ) - if not ok: - conn.close() - return jsonify({"ok": False, "msg": err or "保本失败"}), 400 - conn.execute( - "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", - (new_sl, new_sl, order_id), - ) - conn.commit() - sym = row["symbol"] - direction = row["direction"] - ex_sym = resolve_monitor_exchange_symbol(row) - take_profit = float(row["take_profit"] or 0) - slots = fetch_exchange_tpsl_slots( - ex_sym, direction, plan_sl=new_sl, plan_tp=take_profit - ) - conn.close() - try: - entry = float(row["trigger_price"] or 0) - send_wechat_msg( - "\n".join( - [ - f"# ✅ {sym} 一键保本", - f"**账户:{_wechat_account_label()}**", - f"- 方向:{'做多' if direction == 'long' else '做空'}", - f"- 成交价:{format_price_for_symbol(sym, entry)}", - f"- 偏移:{offset_pct}%(相对成交价)", - f"- 新止损:{format_price_for_symbol(sym, new_sl)}", - "- 交易所:已更新止盈止损", - ] - ) - ) - except Exception: - pass - return jsonify( - { - "ok": True, - "msg": "已一键保本", - "stop_loss": new_sl, - "stop_loss_display": format_price_for_symbol(sym, new_sl), - "breakeven_armed": True, - "exchange_tpsl": slots, - } - ) - - @app.route("/api/order//place_tpsl", methods=["POST"]) @login_required def api_order_place_tpsl(order_id): diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 1db2c12..ae3f5b5 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -194,13 +194,6 @@ .pos-ex-order-main{flex:1;min-width:0;line-height:1.35} .pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0} .pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed} - .pos-be-global{font-size:.76rem;font-weight:400;color:#8892b0;margin-left:10px;display:inline-flex;align-items:center;gap:4px} - .pos-be-global input{width:52px;padding:3px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem} - .pos-be-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:#1a2030;border-radius:8px;border:1px solid #2a3348} - .pos-be-label{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:#b8c4e8;cursor:pointer;user-select:none} - .pos-be-label input{width:15px;height:15px;accent-color:#6eb5ff} - .pos-be-offset{width:52px;padding:4px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem} - .pos-be-status{font-size:.74rem;color:#6ab88a} .tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px} .tpsl-modal-backdrop.open{display:flex} .tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto} @@ -471,11 +464,7 @@
-

实时持仓 - 一键保本默认偏移 - % - -

+

实时持仓

{% for o in order %}
-
- - % - {% if o.breakeven_armed %}已保本{% endif %} -
成交价 @@ -1753,96 +1733,6 @@ function submitTpslEntrust(){ post(); }).catch(()=>alert('无法校验盈亏比')); } -const MANUAL_BE_OFFSET_KEY = 'manualBreakevenOffsetPct'; -function getManualBeOffsetPct(){ - const g = document.getElementById('manual-be-offset-global'); - if(g && g.value !== ''){ - const n = parseFloat(g.value); - if(Number.isFinite(n)) return n; - } - const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY); - const n = saved ? parseFloat(saved) : 0.2; - return Number.isFinite(n) ? n : 0.2; -} -function initManualBreakevenUi(){ - const g = document.getElementById('manual-be-offset-global'); - if(g){ - const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY); - if(saved) g.value = saved; - g.addEventListener('change', ()=>{ - localStorage.setItem(MANUAL_BE_OFFSET_KEY, g.value); - document.querySelectorAll('.pos-be-offset').forEach(inp=>{ - if(!inp.dataset.touched) inp.value = g.value; - }); - }); - } - document.querySelectorAll('.pos-be-offset').forEach(inp=>{ - if(!inp.dataset.touched){ - inp.value = getManualBeOffsetPct(); - } - inp.addEventListener('input', ()=>{ inp.dataset.touched = '1'; }); - }); - document.querySelectorAll('.pos-be-switch').forEach(sw=>{ - if(sw.dataset.bound) return; - sw.dataset.bound = '1'; - sw.addEventListener('change', ()=>{ - const orderId = sw.getAttribute('data-order-id'); - const card = sw.closest('.pos-card'); - const offInp = card && card.querySelector('.pos-be-offset'); - if(!sw.checked){ - if(sw.dataset.armed === '1') sw.checked = true; - return; - } - applyManualBreakeven(orderId, sw, offInp, card); - }); - }); -} -function updateOrderCardSlDisplay(card, slDisplay){ - if(!card || !slDisplay) return; - card.setAttribute('data-plan-sl', slDisplay); - const cells = card.querySelectorAll('.pos-grid .pos-cell'); - if(cells[1]){ - const val = cells[1].querySelector('.pos-value'); - if(val) val.textContent = slDisplay; - } -} -function applyManualBreakeven(orderId, sw, offInp, card){ - const offset = offInp ? parseFloat(offInp.value) : getManualBeOffsetPct(); - if(!Number.isFinite(offset) || offset < 0 || offset > 10){ - alert('偏移%须在 0~10 之间'); - sw.checked = sw.dataset.armed === '1'; - return; - } - const dirHint = card && card.getAttribute('data-direction') === 'short' ? '下' : '上'; - if(!confirm(`确认一键保本?止损将移至成交价${dirHint}移 ${offset}%(相对成交价)`)){ - sw.checked = sw.dataset.armed === '1'; - return; - } - sw.disabled = true; - fetch(`/api/order/${orderId}/manual_breakeven`, { - method:'POST', - headers:{'Content-Type':'application/json'}, - body: JSON.stringify({ offset_pct: offset }) - }).then(r=>r.json()).then(data=>{ - if(!data.ok){ - alert(data.msg || '保本失败'); - sw.checked = sw.dataset.armed === '1'; - sw.disabled = false; - return; - } - sw.dataset.armed = '1'; - sw.checked = true; - const st = document.getElementById(`pos-be-status-${orderId}`); - if(st) st.textContent = '已保本'; - if(data.stop_loss_display) updateOrderCardSlDisplay(card, data.stop_loss_display); - if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl); - else refreshPriceSnapshotConditional(); - }).catch(()=>{ - alert('保本请求失败'); - sw.checked = sw.dataset.armed === '1'; - sw.disabled = false; - }); -} function cancelExchangeTpsl(orderId, role){ const label = role === 'sl' ? '止损' : '止盈'; if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return; @@ -2182,7 +2072,6 @@ function refreshPriceSnapshotConditional(){ } }).catch(()=>{}); } -initManualBreakevenUi(); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); diff --git a/crypto_monitor_gate/.env.example b/crypto_monitor_gate/.env.example index ec71ea4..f2aa45f 100644 --- a/crypto_monitor_gate/.env.example +++ b/crypto_monitor_gate/.env.example @@ -79,6 +79,8 @@ GATE_TPSL_USE_POSITION_ORDER=true GATE_TPSL_TRIGGER_EXPIRATION=604800 # 触发参考价:0=最新成交 1=标记价 2=指数价(非法值按 0) GATE_TPSL_PRICE_TYPE=0 +# 仓位类 TP/SL 相对现价的最小间距(%),避免 Gate 1026「触发价须高于/低于现价」 +GATE_TPSL_LAST_PRICE_GAP_PCT=0.05 # 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 Gate·模拟) # EXCHANGE_DISPLAY_NAME=Gate.io diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 1738034..5413984 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -73,7 +73,6 @@ from key_sl_tp_lib import ( sl_tp_mode_label, sl_tp_plan_summary_text, ) -from order_manual_breakeven_lib import apply_order_manual_breakeven from key_monitor_lib import ( KEY_DIRECTION_WATCH, KEY_MONITOR_ALERT_ONLY_TYPES, @@ -190,6 +189,8 @@ GATE_TPSL_PRICE_TYPE = int(os.getenv("GATE_TPSL_PRICE_TYPE", "0")) if GATE_TPSL_PRICE_TYPE < 0 or GATE_TPSL_PRICE_TYPE > 2: GATE_TPSL_PRICE_TYPE = 0 GATE_TPSL_USE_POSITION_ORDER = os.getenv("GATE_TPSL_USE_POSITION_ORDER", "true").lower() in ("1", "true", "yes") +# 仓位类触发单相对 mark/last 的最小间距(%),避免 Gate 1026 AUTO_TRIGGER_PRICE_*_LAST +GATE_TPSL_LAST_PRICE_GAP_PCT = float(os.getenv("GATE_TPSL_LAST_PRICE_GAP_PCT", "0.05")) # 页面展示的交易所名称(多实例/多环境时可按需区分) EXCHANGE_DISPLAY_NAME = (os.getenv("EXCHANGE_DISPLAY_NAME") or "Gate.io").strip() or "Gate.io" _GATE_DEFAULT_MARGIN_MODE = "cross" if GATE_TD_MODE in ("cross", "cross_margin") else "isolated" @@ -2871,6 +2872,40 @@ def _gate_contracts_amount_for_tpsl(order, fallback_amount): return float(fallback_amount) +def _gate_clamp_tpsl_to_last_price(exchange_symbol, direction, stop_loss, take_profit, *, sl_only=False): + """ + Gate price_orders 规则:空仓止损/多仓止盈 trigger>last;空仓止盈/多仓止损 trigger= last: + tp = float(exchange.price_to_precision(exchange_symbol, last * (1 - gap))) + notes.append(f"止盈触发价须低于现价 {last},已调整为 {tp}") + else: + if sl >= last: + sl = float(exchange.price_to_precision(exchange_symbol, last * (1 - gap))) + notes.append(f"止损触发价须低于现价 {last},已调整为 {sl}") + if not sl_only and tp <= last: + tp = float(exchange.price_to_precision(exchange_symbol, last * (1 + gap))) + notes.append(f"止盈触发价须高于现价 {last},已调整为 {tp}") + return sl, tp, (";".join(notes) if notes else None) + + def _gate_place_tp_sl_orders_legacy_conditional(exchange_symbol, direction, contracts_amount, stop_loss, take_profit): """ccxt 市价减仓条件单(两张单分别带 stopLossPrice / takeProfitPrice),与官方仓位类触发单等价逻辑不同路径。""" ensure_markets_loaded() @@ -2900,6 +2935,9 @@ def _gate_place_tp_sl_orders_position_price_orders(exchange_symbol, direction, s order_type=close-long-position / close-short-position,单向全平 close+size=0;双向需 auto_size。 与 App 内展示的「条件委托」一致,平仓后仍需 cancel_gate_swap_trigger_orders 避免残留。 """ + stop_loss, take_profit, _ = _gate_clamp_tpsl_to_last_price( + exchange_symbol, direction, stop_loss, take_profit + ) ensure_markets_loaded() market = exchange.market(exchange_symbol) if not market.get("swap"): @@ -2972,6 +3010,9 @@ def _gate_place_tp_sl_orders(exchange_symbol, direction, contracts_amount, stop_ def _gate_place_stop_loss_only_position(exchange_symbol, direction, stop_loss): """Gate 永续:仅挂仓位类止损触发单(趋势回调用)。""" + stop_loss, _, _ = _gate_clamp_tpsl_to_last_price( + exchange_symbol, direction, stop_loss, stop_loss, sl_only=True + ) ensure_markets_loaded() market = exchange.market(exchange_symbol) if not market.get("swap"): @@ -3298,6 +3339,9 @@ def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit): raise RuntimeError(reason or "实盘未就绪") ex_sym = resolve_monitor_exchange_symbol(order_row) direction = order_row["direction"] + sl, tp, adjust_note = _gate_clamp_tpsl_to_last_price( + ex_sym, direction, float(stop_loss), float(take_profit) + ) cancel_gate_swap_trigger_orders(ex_sym) contracts = get_live_position_contracts(ex_sym, direction) if contracts is None or float(contracts) <= 0: @@ -3310,7 +3354,7 @@ def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit): amt = 0 if amt <= 0: raise ValueError("无法确定平仓数量") - _gate_place_tp_sl_orders(ex_sym, direction, amt, float(stop_loss), float(take_profit)) + _gate_place_tp_sl_orders(ex_sym, direction, amt, sl, tp) def extract_trade_price_from_order(order): @@ -6208,80 +6252,6 @@ def api_order_cancel_tpsl(order_id): return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400 -@app.route("/api/order//manual_breakeven", methods=["POST"]) -@login_required -def api_order_manual_breakeven(order_id): - data = request.get_json(silent=True) or {} - try: - offset_pct = float( - data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2")) - ) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400 - if offset_pct < 0 or offset_pct > 10: - return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400 - conn = get_db() - row = conn.execute( - "SELECT * FROM order_monitors WHERE id=? AND status='active'", - (order_id,), - ).fetchone() - if not row: - conn.close() - return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404 - ok, err, new_sl = apply_order_manual_breakeven( - row, - offset_pct, - calc_stop_fn=calc_trend_manual_breakeven_stop, - round_price_fn=round_price_to_exchange, - resolve_ex_sym_fn=resolve_monitor_exchange_symbol, - get_position_fn=get_live_position_contracts, - replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange, - ) - if not ok: - conn.close() - return jsonify({"ok": False, "msg": err or "保本失败"}), 400 - conn.execute( - "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", - (new_sl, new_sl, order_id), - ) - conn.commit() - sym = row["symbol"] - direction = row["direction"] - ex_sym = resolve_monitor_exchange_symbol(row) - take_profit = float(row["take_profit"] or 0) - slots = fetch_exchange_tpsl_slots( - ex_sym, direction, plan_sl=new_sl, plan_tp=take_profit - ) - conn.close() - try: - entry = float(row["trigger_price"] or 0) - send_wechat_msg( - "\n".join( - [ - f"# ✅ {sym} 一键保本", - f"**账户:{_wechat_account_label()}**", - f"- 方向:{'做多' if direction == 'long' else '做空'}", - f"- 成交价:{format_price_for_symbol(sym, entry)}", - f"- 偏移:{offset_pct}%(相对成交价)", - f"- 新止损:{format_price_for_symbol(sym, new_sl)}", - "- 交易所:已更新止盈止损", - ] - ) - ) - except Exception: - pass - return jsonify( - { - "ok": True, - "msg": "已一键保本", - "stop_loss": new_sl, - "stop_loss_display": format_price_for_symbol(sym, new_sl), - "breakeven_armed": True, - "exchange_tpsl": slots, - } - ) - - @app.route("/api/order//place_tpsl", methods=["POST"]) @login_required def api_order_place_tpsl(order_id): diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 1db2c12..ae3f5b5 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -194,13 +194,6 @@ .pos-ex-order-main{flex:1;min-width:0;line-height:1.35} .pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0} .pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed} - .pos-be-global{font-size:.76rem;font-weight:400;color:#8892b0;margin-left:10px;display:inline-flex;align-items:center;gap:4px} - .pos-be-global input{width:52px;padding:3px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem} - .pos-be-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:#1a2030;border-radius:8px;border:1px solid #2a3348} - .pos-be-label{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:#b8c4e8;cursor:pointer;user-select:none} - .pos-be-label input{width:15px;height:15px;accent-color:#6eb5ff} - .pos-be-offset{width:52px;padding:4px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem} - .pos-be-status{font-size:.74rem;color:#6ab88a} .tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px} .tpsl-modal-backdrop.open{display:flex} .tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto} @@ -471,11 +464,7 @@
-

实时持仓 - 一键保本默认偏移 - % - -

+

实时持仓

{% for o in order %}
-
- - % - {% if o.breakeven_armed %}已保本{% endif %} -
成交价 @@ -1753,96 +1733,6 @@ function submitTpslEntrust(){ post(); }).catch(()=>alert('无法校验盈亏比')); } -const MANUAL_BE_OFFSET_KEY = 'manualBreakevenOffsetPct'; -function getManualBeOffsetPct(){ - const g = document.getElementById('manual-be-offset-global'); - if(g && g.value !== ''){ - const n = parseFloat(g.value); - if(Number.isFinite(n)) return n; - } - const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY); - const n = saved ? parseFloat(saved) : 0.2; - return Number.isFinite(n) ? n : 0.2; -} -function initManualBreakevenUi(){ - const g = document.getElementById('manual-be-offset-global'); - if(g){ - const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY); - if(saved) g.value = saved; - g.addEventListener('change', ()=>{ - localStorage.setItem(MANUAL_BE_OFFSET_KEY, g.value); - document.querySelectorAll('.pos-be-offset').forEach(inp=>{ - if(!inp.dataset.touched) inp.value = g.value; - }); - }); - } - document.querySelectorAll('.pos-be-offset').forEach(inp=>{ - if(!inp.dataset.touched){ - inp.value = getManualBeOffsetPct(); - } - inp.addEventListener('input', ()=>{ inp.dataset.touched = '1'; }); - }); - document.querySelectorAll('.pos-be-switch').forEach(sw=>{ - if(sw.dataset.bound) return; - sw.dataset.bound = '1'; - sw.addEventListener('change', ()=>{ - const orderId = sw.getAttribute('data-order-id'); - const card = sw.closest('.pos-card'); - const offInp = card && card.querySelector('.pos-be-offset'); - if(!sw.checked){ - if(sw.dataset.armed === '1') sw.checked = true; - return; - } - applyManualBreakeven(orderId, sw, offInp, card); - }); - }); -} -function updateOrderCardSlDisplay(card, slDisplay){ - if(!card || !slDisplay) return; - card.setAttribute('data-plan-sl', slDisplay); - const cells = card.querySelectorAll('.pos-grid .pos-cell'); - if(cells[1]){ - const val = cells[1].querySelector('.pos-value'); - if(val) val.textContent = slDisplay; - } -} -function applyManualBreakeven(orderId, sw, offInp, card){ - const offset = offInp ? parseFloat(offInp.value) : getManualBeOffsetPct(); - if(!Number.isFinite(offset) || offset < 0 || offset > 10){ - alert('偏移%须在 0~10 之间'); - sw.checked = sw.dataset.armed === '1'; - return; - } - const dirHint = card && card.getAttribute('data-direction') === 'short' ? '下' : '上'; - if(!confirm(`确认一键保本?止损将移至成交价${dirHint}移 ${offset}%(相对成交价)`)){ - sw.checked = sw.dataset.armed === '1'; - return; - } - sw.disabled = true; - fetch(`/api/order/${orderId}/manual_breakeven`, { - method:'POST', - headers:{'Content-Type':'application/json'}, - body: JSON.stringify({ offset_pct: offset }) - }).then(r=>r.json()).then(data=>{ - if(!data.ok){ - alert(data.msg || '保本失败'); - sw.checked = sw.dataset.armed === '1'; - sw.disabled = false; - return; - } - sw.dataset.armed = '1'; - sw.checked = true; - const st = document.getElementById(`pos-be-status-${orderId}`); - if(st) st.textContent = '已保本'; - if(data.stop_loss_display) updateOrderCardSlDisplay(card, data.stop_loss_display); - if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl); - else refreshPriceSnapshotConditional(); - }).catch(()=>{ - alert('保本请求失败'); - sw.checked = sw.dataset.armed === '1'; - sw.disabled = false; - }); -} function cancelExchangeTpsl(orderId, role){ const label = role === 'sl' ? '止损' : '止盈'; if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return; @@ -2182,7 +2072,6 @@ function refreshPriceSnapshotConditional(){ } }).catch(()=>{}); } -initManualBreakevenUi(); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 61df640..b37824a 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -53,7 +53,6 @@ from journal_chart_lib import ( trade_review_fetch_window, trim_rows_for_trade_review, ) -from order_manual_breakeven_lib import apply_order_manual_breakeven from hub_auth import request_allowed as hub_request_allowed from history_window_lib import ( PRESET_CUSTOM, @@ -5417,74 +5416,6 @@ def api_account_snapshot(): }) -@app.route("/api/order//manual_breakeven", methods=["POST"]) -@login_required -def api_order_manual_breakeven(order_id): - data = request.get_json(silent=True) or {} - try: - offset_pct = float( - data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2")) - ) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400 - if offset_pct < 0 or offset_pct > 10: - return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400 - conn = get_db() - row = conn.execute( - "SELECT * FROM order_monitors WHERE id=? AND status='active'", - (order_id,), - ).fetchone() - if not row: - conn.close() - return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404 - ok, err, new_sl = apply_order_manual_breakeven( - row, - offset_pct, - calc_stop_fn=calc_trend_manual_breakeven_stop, - round_price_fn=round_price_to_exchange, - resolve_ex_sym_fn=resolve_monitor_exchange_symbol, - get_position_fn=get_live_position_contracts, - replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange, - ) - if not ok: - conn.close() - return jsonify({"ok": False, "msg": err or "保本失败"}), 400 - conn.execute( - "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", - (new_sl, new_sl, order_id), - ) - conn.commit() - sym = row["symbol"] - direction = row["direction"] - conn.close() - try: - entry = float(row["trigger_price"] or 0) - send_wechat_msg( - "\n".join( - [ - f"# ✅ {sym} 一键保本", - f"**账户:{_wechat_account_label()}**", - f"- 方向:{'做多' if direction == 'long' else '做空'}", - f"- 成交价:{format_price_for_symbol(sym, entry)}", - f"- 偏移:{offset_pct}%(相对成交价)", - f"- 新止损:{format_price_for_symbol(sym, new_sl)}", - "- 交易所:已更新止盈止损", - ] - ) - ) - except Exception: - pass - return jsonify( - { - "ok": True, - "msg": "已一键保本", - "stop_loss": new_sl, - "stop_loss_display": format_price_for_symbol(sym, new_sl), - "breakeven_armed": True, - } - ) - - @app.route("/api/price_snapshot") @login_required def api_price_snapshot(): diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index 49f2f34..6bf98ee 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -131,13 +131,6 @@ .plan-cell .val.pnl-neutral,.plan-cell .val .pnl-neutral{color:#cfd3ef} .btn-close-plan{padding:7px 14px;background:#5c1e2a;color:#ffb4b4;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;font-weight:600;text-decoration:none;white-space:nowrap} .btn-close-plan:hover{filter:brightness(1.08)} - .pos-be-global{font-size:.74rem;font-weight:400;color:#8892b0;margin-left:8px;display:inline-flex;align-items:center;gap:4px} - .pos-be-global input{width:52px;padding:3px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.76rem} - .pos-be-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:#1a2030;border-radius:8px;border:1px solid #2a3348} - .pos-be-label{display:inline-flex;align-items:center;gap:6px;font-size:.76rem;color:#b8c4e8;cursor:pointer} - .pos-be-label input{width:15px;height:15px} - .pos-be-offset{width:52px;padding:4px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.76rem} - .pos-be-status{font-size:.72rem;color:#6ab88a} .records-card{grid-column:1/-1} .review-card{grid-column:1/-1} .review-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap} @@ -355,15 +348,11 @@
-

实时持仓 - 一键保本默认偏移 - % - -

+

实时持仓

{% for o in order %} {% set osym = o.exchange_symbol or o.symbol %} -
+
{{ osym }} @@ -371,15 +360,6 @@
平仓
-
- - % - {% if o.breakeven_armed %}已保本{% endif %} -
来源: 下单监控 | 风格: {{ o.trade_style or 'trend' }} | 风险: {% if o.risk_percent is not none %}{{ o.risk_percent }}%{% else %}—{% endif %}≈{{ money_fmt(o.risk_amount) }}U | {% if o.breakeven_enabled %}移动保本: 开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(osym, o.breakeven_price) }}{% else %}移动保本: 关{% endif %} @@ -1653,92 +1633,6 @@ if(_journalFormEl){ syncJournalEntryReasonOtherUi(); } refreshOrderDefaults(); -const MANUAL_BE_OFFSET_KEY = 'manualBreakevenOffsetPct'; -function getManualBeOffsetPct(){ - const g = document.getElementById('manual-be-offset-global'); - if(g && g.value !== ''){ - const n = parseFloat(g.value); - if(Number.isFinite(n)) return n; - } - const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY); - const n = saved ? parseFloat(saved) : 0.2; - return Number.isFinite(n) ? n : 0.2; -} -function initManualBreakevenUi(){ - const g = document.getElementById('manual-be-offset-global'); - if(g){ - const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY); - if(saved) g.value = saved; - g.addEventListener('change', ()=>{ - localStorage.setItem(MANUAL_BE_OFFSET_KEY, g.value); - document.querySelectorAll('.pos-be-offset').forEach(inp=>{ - if(!inp.dataset.touched) inp.value = g.value; - }); - }); - } - document.querySelectorAll('.pos-be-offset').forEach(inp=>{ - if(!inp.dataset.touched) inp.value = getManualBeOffsetPct(); - inp.addEventListener('input', ()=>{ inp.dataset.touched = '1'; }); - }); - document.querySelectorAll('.pos-be-switch').forEach(sw=>{ - if(sw.dataset.bound) return; - sw.dataset.bound = '1'; - sw.addEventListener('change', ()=>{ - const orderId = sw.getAttribute('data-order-id'); - const card = document.getElementById(`order-row-${orderId}`); - const offInp = card && card.querySelector('.pos-be-offset'); - if(!sw.checked){ - if(sw.dataset.armed === '1') sw.checked = true; - return; - } - applyManualBreakeven(orderId, sw, offInp, card); - }); - }); -} -function updateOrderCardSlDisplay(card, slDisplay){ - if(!card || !slDisplay) return; - const cells = card.querySelectorAll('.plan-card-grid .plan-cell'); - if(cells[1]){ - const val = cells[1].querySelector('.val'); - if(val) val.textContent = slDisplay; - } -} -function applyManualBreakeven(orderId, sw, offInp, card){ - const offset = offInp ? parseFloat(offInp.value) : getManualBeOffsetPct(); - if(!Number.isFinite(offset) || offset < 0 || offset > 10){ - alert('偏移%须在 0~10 之间'); - sw.checked = sw.dataset.armed === '1'; - return; - } - const dirHint = card && card.getAttribute('data-direction') === 'short' ? '下' : '上'; - if(!confirm(`确认一键保本?止损将移至成交价${dirHint}移 ${offset}%`)){ - sw.checked = sw.dataset.armed === '1'; - return; - } - sw.disabled = true; - fetch(`/api/order/${orderId}/manual_breakeven`, { - method:'POST', - headers:{'Content-Type':'application/json'}, - body: JSON.stringify({ offset_pct: offset }) - }).then(r=>r.json()).then(data=>{ - if(!data.ok){ - alert(data.msg || '保本失败'); - sw.checked = sw.dataset.armed === '1'; - sw.disabled = false; - return; - } - sw.dataset.armed = '1'; - sw.checked = true; - const st = document.getElementById(`pos-be-status-${orderId}`); - if(st) st.textContent = '已保本'; - if(data.stop_loss_display) updateOrderCardSlDisplay(card, data.stop_loss_display); - }).catch(()=>{ - alert('保本请求失败'); - sw.checked = sw.dataset.armed === '1'; - sw.disabled = false; - }); -} -initManualBreakevenUi(); refreshPriceSnapshot(); setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }}); setInterval(refreshPriceSnapshot, {{ price_refresh_seconds * 1000 }}); diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 200e092..6807b43 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -73,7 +73,6 @@ from key_sl_tp_lib import ( sl_tp_mode_label, sl_tp_plan_summary_text, ) -from order_manual_breakeven_lib import apply_order_manual_breakeven from key_monitor_lib import ( KEY_DIRECTION_WATCH, KEY_MONITOR_ALERT_ONLY_TYPES, @@ -5847,80 +5846,6 @@ def api_order_cancel_tpsl(order_id): return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400 -@app.route("/api/order//manual_breakeven", methods=["POST"]) -@login_required -def api_order_manual_breakeven(order_id): - data = request.get_json(silent=True) or {} - try: - offset_pct = float( - data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2")) - ) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400 - if offset_pct < 0 or offset_pct > 10: - return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400 - conn = get_db() - row = conn.execute( - "SELECT * FROM order_monitors WHERE id=? AND status='active'", - (order_id,), - ).fetchone() - if not row: - conn.close() - return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404 - ok, err, new_sl = apply_order_manual_breakeven( - row, - offset_pct, - calc_stop_fn=calc_trend_manual_breakeven_stop, - round_price_fn=round_price_to_exchange, - resolve_ex_sym_fn=resolve_monitor_exchange_symbol, - get_position_fn=get_live_position_contracts, - replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange, - ) - if not ok: - conn.close() - return jsonify({"ok": False, "msg": err or "保本失败"}), 400 - conn.execute( - "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", - (new_sl, new_sl, order_id), - ) - conn.commit() - sym = row["symbol"] - direction = row["direction"] - ex_sym = resolve_monitor_exchange_symbol(row) - take_profit = float(row["take_profit"] or 0) - slots = fetch_exchange_tpsl_slots( - ex_sym, direction, plan_sl=new_sl, plan_tp=take_profit - ) - conn.close() - try: - entry = float(row["trigger_price"] or 0) - send_wechat_msg( - "\n".join( - [ - f"# ✅ {sym} 一键保本", - f"**账户:{_wechat_account_label()}**", - f"- 方向:{'做多' if direction == 'long' else '做空'}", - f"- 成交价:{format_price_for_symbol(sym, entry)}", - f"- 偏移:{offset_pct}%(相对成交价)", - f"- 新止损:{format_price_for_symbol(sym, new_sl)}", - "- 交易所:已更新止盈止损", - ] - ) - ) - except Exception: - pass - return jsonify( - { - "ok": True, - "msg": "已一键保本", - "stop_loss": new_sl, - "stop_loss_display": format_price_for_symbol(sym, new_sl), - "breakeven_armed": True, - "exchange_tpsl": slots, - } - ) - - @app.route("/api/order//place_tpsl", methods=["POST"]) @login_required def api_order_place_tpsl(order_id): diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index 9a02a14..00f7a2c 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -194,13 +194,6 @@ .pos-ex-order-main{flex:1;min-width:0;line-height:1.35} .pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0} .pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed} - .pos-be-global{font-size:.76rem;font-weight:400;color:#8892b0;margin-left:10px;display:inline-flex;align-items:center;gap:4px} - .pos-be-global input{width:52px;padding:3px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem} - .pos-be-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:#1a2030;border-radius:8px;border:1px solid #2a3348} - .pos-be-label{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:#b8c4e8;cursor:pointer;user-select:none} - .pos-be-label input{width:15px;height:15px;accent-color:#6eb5ff} - .pos-be-offset{width:52px;padding:4px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem} - .pos-be-status{font-size:.74rem;color:#6ab88a} .tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px} .tpsl-modal-backdrop.open{display:flex} .tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto} @@ -480,11 +473,7 @@
-

实时持仓 - 一键保本默认偏移 - % - -

+

实时持仓

{% for o in order %}
-
- - % - {% if o.breakeven_armed %}已保本{% endif %} -
成交价 @@ -1763,96 +1743,6 @@ function submitTpslEntrust(){ post(); }).catch(()=>alert('无法校验盈亏比')); } -const MANUAL_BE_OFFSET_KEY = 'manualBreakevenOffsetPct'; -function getManualBeOffsetPct(){ - const g = document.getElementById('manual-be-offset-global'); - if(g && g.value !== ''){ - const n = parseFloat(g.value); - if(Number.isFinite(n)) return n; - } - const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY); - const n = saved ? parseFloat(saved) : 0.2; - return Number.isFinite(n) ? n : 0.2; -} -function initManualBreakevenUi(){ - const g = document.getElementById('manual-be-offset-global'); - if(g){ - const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY); - if(saved) g.value = saved; - g.addEventListener('change', ()=>{ - localStorage.setItem(MANUAL_BE_OFFSET_KEY, g.value); - document.querySelectorAll('.pos-be-offset').forEach(inp=>{ - if(!inp.dataset.touched) inp.value = g.value; - }); - }); - } - document.querySelectorAll('.pos-be-offset').forEach(inp=>{ - if(!inp.dataset.touched){ - inp.value = getManualBeOffsetPct(); - } - inp.addEventListener('input', ()=>{ inp.dataset.touched = '1'; }); - }); - document.querySelectorAll('.pos-be-switch').forEach(sw=>{ - if(sw.dataset.bound) return; - sw.dataset.bound = '1'; - sw.addEventListener('change', ()=>{ - const orderId = sw.getAttribute('data-order-id'); - const card = sw.closest('.pos-card'); - const offInp = card && card.querySelector('.pos-be-offset'); - if(!sw.checked){ - if(sw.dataset.armed === '1') sw.checked = true; - return; - } - applyManualBreakeven(orderId, sw, offInp, card); - }); - }); -} -function updateOrderCardSlDisplay(card, slDisplay){ - if(!card || !slDisplay) return; - card.setAttribute('data-plan-sl', slDisplay); - const cells = card.querySelectorAll('.pos-grid .pos-cell'); - if(cells[1]){ - const val = cells[1].querySelector('.pos-value'); - if(val) val.textContent = slDisplay; - } -} -function applyManualBreakeven(orderId, sw, offInp, card){ - const offset = offInp ? parseFloat(offInp.value) : getManualBeOffsetPct(); - if(!Number.isFinite(offset) || offset < 0 || offset > 10){ - alert('偏移%须在 0~10 之间'); - sw.checked = sw.dataset.armed === '1'; - return; - } - const dirHint = card && card.getAttribute('data-direction') === 'short' ? '下' : '上'; - if(!confirm(`确认一键保本?止损将移至成交价${dirHint}移 ${offset}%(相对成交价)`)){ - sw.checked = sw.dataset.armed === '1'; - return; - } - sw.disabled = true; - fetch(`/api/order/${orderId}/manual_breakeven`, { - method:'POST', - headers:{'Content-Type':'application/json'}, - body: JSON.stringify({ offset_pct: offset }) - }).then(r=>r.json()).then(data=>{ - if(!data.ok){ - alert(data.msg || '保本失败'); - sw.checked = sw.dataset.armed === '1'; - sw.disabled = false; - return; - } - sw.dataset.armed = '1'; - sw.checked = true; - const st = document.getElementById(`pos-be-status-${orderId}`); - if(st) st.textContent = '已保本'; - if(data.stop_loss_display) updateOrderCardSlDisplay(card, data.stop_loss_display); - if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl); - else refreshPriceSnapshotConditional(); - }).catch(()=>{ - alert('保本请求失败'); - sw.checked = sw.dataset.armed === '1'; - sw.disabled = false; - }); -} function cancelExchangeTpsl(orderId, role){ const label = role === 'sl' ? '止损' : '止盈'; if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return; @@ -2224,7 +2114,6 @@ function refreshPriceSnapshotConditional(){ } }).catch(()=>{}); } -initManualBreakevenUi(); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 454b345..26f910e 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -336,7 +336,12 @@ def _parse_http_json_body(r: httpx.Response) -> dict: async def _fetch_flask_json( - client: httpx.AsyncClient, ex: dict, path: str, method: str = "GET", data=None + client: httpx.AsyncClient, + ex: dict, + path: str, + method: str = "GET", + data=None, + json_body: dict | None = None, ) -> dict | None: base = (ex.get("flask_url") or "").rstrip("/") if not base: @@ -345,7 +350,15 @@ async def _fetch_flask_json( if method == "GET": r = await client.get(f"{base}{path}", headers=_hub_headers(), timeout=HUB_FLASK_TIMEOUT) else: - r = await client.post(f"{base}{path}", headers=_hub_headers(), data=data, timeout=120.0) + headers = {**_hub_headers(), "Content-Type": "application/json"} + if json_body is not None: + r = await client.post( + f"{base}{path}", headers=headers, json=json_body, timeout=120.0 + ) + else: + r = await client.post( + f"{base}{path}", headers=headers, data=data, timeout=120.0 + ) if r.status_code >= 400: parsed = _parse_http_json_body(r) parsed.setdefault("ok", False) diff --git a/order_manual_breakeven_lib.py b/order_manual_breakeven_lib.py deleted file mode 100644 index 4e8962e..0000000 --- a/order_manual_breakeven_lib.py +++ /dev/null @@ -1,63 +0,0 @@ -"""中控台持仓:一键保本(成交价 ± 偏移%)。""" -from typing import Any, Callable, Optional, Tuple - - -def calc_manual_breakeven_stop(direction: str, entry_price: float, offset_pct: float) -> Optional[float]: - try: - e = float(entry_price) - pct = float(offset_pct) - except (TypeError, ValueError): - return None - if e <= 0 or pct < 0: - return None - direction = (direction or "long").strip().lower() - if direction == "short": - return e * (1.0 - pct / 100.0) - return e * (1.0 + pct / 100.0) - - -def apply_order_manual_breakeven( - row: Any, - offset_pct: float, - *, - calc_stop_fn: Callable[..., Optional[float]], - round_price_fn: Callable[[str, float], Any], - resolve_ex_sym_fn: Callable[[Any], str], - get_position_fn: Callable[[str, str], Any], - replace_tpsl_fn: Callable[[Any, float, float], None], - entry_price_key: str = "trigger_price", -) -> Tuple[bool, Optional[str], Optional[float]]: - if (row["status"] or "").strip() != "active": - return False, "持仓已结束", None - entry = float(row[entry_price_key] or 0) - if entry <= 0: - return False, "缺少有效成交价", None - direction = (row["direction"] or "long").lower() - take_profit = float(row["take_profit"] or 0) - if take_profit <= 0: - return False, "缺少有效止盈价,无法更新交易所委托", None - ex_sym = resolve_ex_sym_fn(row) - pos = get_position_fn(ex_sym, direction) - if pos is None or float(pos) <= 0: - return False, "交易所当前无该方向持仓", None - new_sl_raw = calc_stop_fn(direction, entry, offset_pct) - if new_sl_raw is None: - new_sl_raw = calc_manual_breakeven_stop(direction, entry, offset_pct) - if new_sl_raw is None: - return False, "保本价计算失败", None - new_sl = round_price_fn(ex_sym, new_sl_raw) - if new_sl is None: - return False, "保本价经交易所精度舍入后无效", None - new_sl = float(new_sl) - cur_sl = float(row["stop_loss"] or 0) - if direction == "long": - if cur_sl > 0 and new_sl <= cur_sl: - return False, f"新止损 {new_sl} 未高于当前止损 {cur_sl}(多仓需上移)", None - else: - if cur_sl > 0 and new_sl >= cur_sl: - return False, f"新止损 {new_sl} 未低于当前止损 {cur_sl}(空仓需下移)", None - try: - replace_tpsl_fn(row, new_sl, take_profit) - except Exception as e: - return False, str(e), None - return True, None, new_sl