From 5af12516875fdc519bf8c46804076dfc6a173ee1 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 19 May 2026 14:45:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=A7=BB=E5=8A=A8=E6=AD=A2?= =?UTF-8?q?=E6=8D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_binance/app.py | 42 +++++++++++---- crypto_monitor_binance/更新文档.md | 1 + crypto_monitor_gate/app.py | 43 ++++++++++++---- crypto_monitor_gate/更新文档.md | 1 + crypto_monitor_gate_bot/app.py | 82 ++++++++++++++++++++++++++---- 5 files changed, 141 insertions(+), 28 deletions(-) diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 9ea64c9..0fe0a0d 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -4670,14 +4670,36 @@ def check_order_monitors(): direction == "long" and new_sl > float(stop_loss) ) if should_move: - conn.execute( - "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", - (new_sl, new_sl, pid), - ) - stop_loss = new_sl - arm_txt = "保本止盈" if not breakeven_armed else "移动止盈" - send_wechat_msg( - build_wechat_breakeven_message( + ex_sym = resolve_monitor_exchange_symbol(r) + new_sl = round_price_to_exchange(ex_sym, new_sl) + tp_ex = float(take_profit or 0) + ok_live, _live_reason = ensure_exchange_live_ready() + synced_ex = not ok_live + if ok_live and tp_ex > 0: + try: + replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex) + synced_ex = True + except Exception as e: + print( + f"[breakeven] exchange tpsl replace failed order={pid} {sym}: {e}", + flush=True, + ) + send_wechat_msg( + f"⚠️ {sym} 移动保本止损未同步交易所:{friendly_exchange_error(e)}" + ) + elif ok_live: + print( + f"[breakeven] skip exchange order={pid} {sym}: invalid take_profit", + flush=True, + ) + if synced_ex: + conn.execute( + "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", + (new_sl, new_sl, pid), + ) + stop_loss = new_sl + arm_txt = "保本止盈" if not breakeven_armed else "移动止盈" + be_msg = build_wechat_breakeven_message( sym, direction, arm_txt, @@ -4685,7 +4707,9 @@ def check_order_monitors(): locked_r, new_sl, ) - ) + if ok_live: + be_msg += "\n- 交易所:已先撤后挂止盈止损" + send_wechat_msg(be_msg) res = None # 做多 diff --git a/crypto_monitor_binance/更新文档.md b/crypto_monitor_binance/更新文档.md index 58cfee9..0d9ffe7 100644 --- a/crypto_monitor_binance/更新文档.md +++ b/crypto_monitor_binance/更新文档.md @@ -93,6 +93,7 @@ - 左列:实盘下单监控(表单、划转、规则)。 - 右列:实时持仓(独立模块)。 - **人工开仓门控**:计划盈亏比 < `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝。 +- **移动保本**(勾选启用):监控轮询达到触发 RR 后,止损阶梯上移时**同步交易所**——**先撤**该合约全部 TP/SL(含 Algo 条件单)**再挂**新止损 + 原止盈(`replace_active_monitor_tpsl_on_exchange`)。仅交易所成功后才写库;失败发企业微信告警。未配置实盘 API 时仍只更新本地。 ## 统计分析页(`/stats`) diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index a606bd4..b5bc027 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -4817,15 +4817,36 @@ def check_order_monitors(): direction == "long" and new_sl > float(stop_loss) ) if should_move: - new_sl = round_price_to_exchange(resolve_monitor_exchange_symbol(r), new_sl) - conn.execute( - "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", - (new_sl, new_sl, pid), - ) - stop_loss = new_sl - arm_txt = "保本止盈" if not breakeven_armed else "移动止盈" - send_wechat_msg( - build_wechat_breakeven_message( + ex_sym = resolve_monitor_exchange_symbol(r) + new_sl = round_price_to_exchange(ex_sym, new_sl) + tp_ex = float(take_profit or 0) + ok_live, _live_reason = ensure_exchange_live_ready() + synced_ex = not ok_live + if ok_live and tp_ex > 0: + try: + replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex) + synced_ex = True + except Exception as e: + print( + f"[breakeven] exchange tpsl replace failed order={pid} {sym}: {e}", + flush=True, + ) + send_wechat_msg( + f"⚠️ {sym} 移动保本止损未同步交易所:{friendly_exchange_error(e)}" + ) + elif ok_live: + print( + f"[breakeven] skip exchange order={pid} {sym}: invalid take_profit", + flush=True, + ) + if synced_ex: + conn.execute( + "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", + (new_sl, new_sl, pid), + ) + stop_loss = new_sl + arm_txt = "保本止盈" if not breakeven_armed else "移动止盈" + be_msg = build_wechat_breakeven_message( sym, direction, arm_txt, @@ -4833,7 +4854,9 @@ def check_order_monitors(): locked_r, new_sl, ) - ) + if ok_live: + be_msg += "\n- 交易所:已先撤后挂止盈止损" + send_wechat_msg(be_msg) res = None # 做多 diff --git a/crypto_monitor_gate/更新文档.md b/crypto_monitor_gate/更新文档.md index 392a59d..f29eb12 100644 --- a/crypto_monitor_gate/更新文档.md +++ b/crypto_monitor_gate/更新文档.md @@ -93,6 +93,7 @@ - 左列:实盘下单监控(表单、划转、规则)。 - 右列:实时持仓(独立模块)。 - **人工开仓门控**:计划盈亏比 < `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝。 +- **移动保本**(勾选启用):监控轮询达到触发 RR 后,止损阶梯上移时**同步交易所**——调用与页面「挂止盈止损」相同的 **先撤后挂**(`replace_active_monitor_tpsl_on_exchange`:撤该合约全部 TP/SL 条件单 → 按新止损 + 原止盈重挂)。仅交易所成功后才写库;失败发企业微信告警,本地止损不变。未配置实盘 API 时仍只更新本地(与旧行为一致)。 ## 统计分析页(`/stats`) diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index ef78a2b..e3fe4ec 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -1656,6 +1656,24 @@ def resolve_monitor_exchange_symbol(row): return normalize_exchange_symbol(raw) if raw else "" +def round_price_to_exchange(exchange_symbol, price): + """与交易所 tick 对齐后的 float,供入库与计算;失败时退回 float(price)。""" + if price in (None, ""): + return None + try: + v = float(price) + except (TypeError, ValueError): + return None + if not exchange_symbol: + return v + try: + ensure_markets_loaded() + s = exchange.price_to_precision(exchange_symbol, v) + return float(s) + except Exception: + return v + + def _position_contract_symbol_match(position_symbol, wanted_exchange_symbol): if not position_symbol or not wanted_exchange_symbol: return False @@ -3093,6 +3111,28 @@ def cancel_all_open_orders_for_symbol(exchange_symbol): pass +def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit): + """移动保本/手动改价:先撤该合约 TP/SL 条件单,再按新价重挂。""" + ok, reason = ensure_exchange_live_ready() + if not ok: + raise RuntimeError(reason or "实盘未就绪") + ex_sym = resolve_monitor_exchange_symbol(order_row) + direction = order_row["direction"] + cancel_gate_swap_trigger_orders(ex_sym) + contracts = get_live_position_contracts(ex_sym, direction) + if contracts is None or float(contracts) <= 0: + raise ValueError("交易所当前无该方向持仓,无法挂止盈止损") + amt = float(contracts) + if amt <= 0: + try: + amt = float(order_row["order_amount"] or 0) + except Exception: + amt = 0 + if amt <= 0: + raise ValueError("无法确定平仓数量") + _gate_place_tp_sl_orders(ex_sym, direction, amt, float(stop_loss), float(take_profit)) + + def extract_trade_price_from_order(order): if not order: return None @@ -4491,14 +4531,36 @@ def check_order_monitors(): direction == "long" and new_sl > float(stop_loss) ) if should_move: - conn.execute( - "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", - (new_sl, new_sl, pid), - ) - stop_loss = new_sl - arm_txt = "保本止盈" if not breakeven_armed else "移动止盈" - send_wechat_msg( - build_wechat_breakeven_message( + ex_sym = resolve_monitor_exchange_symbol(r) + new_sl = round_price_to_exchange(ex_sym, new_sl) + tp_ex = float(take_profit or 0) + ok_live, _live_reason = ensure_exchange_live_ready() + synced_ex = not ok_live + if ok_live and tp_ex > 0: + try: + replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex) + synced_ex = True + except Exception as e: + print( + f"[breakeven] exchange tpsl replace failed order={pid} {sym}: {e}", + flush=True, + ) + send_wechat_msg( + f"⚠️ {sym} 移动保本止损未同步交易所:{friendly_exchange_error(e)}" + ) + elif ok_live: + print( + f"[breakeven] skip exchange order={pid} {sym}: invalid take_profit", + flush=True, + ) + if synced_ex: + conn.execute( + "UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?", + (new_sl, new_sl, pid), + ) + stop_loss = new_sl + arm_txt = "保本止盈" if not breakeven_armed else "移动止盈" + be_msg = build_wechat_breakeven_message( sym, direction, arm_txt, @@ -4506,7 +4568,9 @@ def check_order_monitors(): locked_r, new_sl, ) - ) + if ok_live: + be_msg += "\n- 交易所:已先撤后挂止盈止损" + send_wechat_msg(be_msg) res = None # 做多