From a1b54d658fbe914d465af5a0d877a93920660063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Sun, 15 Feb 2026 10:06:25 +0800 Subject: [PATCH] 1 --- docs/止损止盈双通道说明.md | 65 +++++++++++++++++++++++++++++ trading_system/position_manager.py | 41 ++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 docs/止损止盈双通道说明.md diff --git a/docs/止损止盈双通道说明.md b/docs/止损止盈双通道说明.md new file mode 100644 index 0000000..fe00182 --- /dev/null +++ b/docs/止损止盈双通道说明.md @@ -0,0 +1,65 @@ +# 止损/止盈的两种实现方式(双通道) + +当前确实存在**两种**止损(以及止盈)执行方式,互为备份;逻辑如下。 + +--- + +## 一、两种方式分别是什么 + +### 1. 币安条件单(交易所侧,自动执行) + +- **做法**:开仓或补挂时,在币安挂 **STOP_MARKET**(止损)和 **TAKE_PROFIT_MARKET**(止盈)条件单,触发价为策略算出的 `stop_loss` / `take_profit` 价格。 +- **触发**:价格到达触发价时,**由币安自动撮合**,无需本机程序在线。 +- **代码位置**:`position_manager._ensure_exchange_sltp_orders` → `client.place_trigger_close_position_order(trigger_type="STOP_MARKET" / "TAKE_PROFIT_MARKET", stop_price=...)`。 +- **特点**:服务重启、断网、本机崩溃后,只要仓位和条件单还在,仍能按价止损/止盈。 + +### 2. 系统 WebSocket 监控(本机侧,到价后市价平仓) + +- **做法**:本机对每个持仓订阅该交易对价格(WebSocket),在 `_check_single_position` 里用**当前价**和**目标止损/止盈**比较(按保证金的亏损/盈利比例),到价则调用 `close_position(symbol, reason='stop_loss'/'take_profit'/...)`,即**市价平仓**。 +- **触发**:程序判定「当前盈亏已到止损或止盈目标」时,**本机主动发市价平仓单**。 +- **代码位置**:`position_manager._monitor_position_price` → `_check_single_position` → 比较 `pnl_percent_margin` 与 `stop_loss_pct_margin` / 止盈目标 → `close_position(...)`。 +- **特点**:可以做**移动止损、分步止盈(TP1 部分平仓)**等复杂逻辑,这些无法用交易所单一张单实现。 + +--- + +## 二、当前整体流程(开仓后) + +1. 开仓成交后(或补建/修复后)调用 **`_ensure_exchange_sltp_orders`**: + - 先取消该 symbol 上已有的 STOP_MARKET / TAKE_PROFIT_MARKET / TRAILING_STOP_MARKET; + - 再按当前 `position_info` 的止损价、止盈价挂**一张止损 + 一张止盈**(交易所条件单)。 +2. 同时对该 symbol 启动 **WebSocket 监控**(`_monitor_position_price`): + - 每次收到价格,用**保证金盈亏比例**判断是否到止损/到第一止盈/到第二止盈/到移动止损; + - 到则 **`close_position(..., reason=...)`**(市价平仓)。 + +因此:**同一笔仓位既有交易所条件单,又有本机监控**,两条路都能触发平仓,形成双通道。 + +--- + +## 三、设计意图与分工 + +- **交易所条件单**:保证在**本机不在线**时(重启、断网、崩溃)仍有止损/止盈保护。 +- **本机 WebSocket**: + - 实现**移动止损、TP1 部分平仓**等(交易所只挂固定止损/止盈,不会自动“移动”或“分步”); + - 在**挂单失败**时作为兜底(见下)。 + +--- + +## 四、可能的问题与现状 + +| 问题 | 说明 | 当前处理 | +|------|------|----------| +| **重复平仓** | 交易所先触发平仓后,本机再判“到价”可能再发平仓。 | `close_position` 会先查币安是否还有仓位;无仓位则不再下单,仅同步 DB,避免重复市价单。 | +| **挂单失败只剩 WS** | 若 STOP_MARKET 挂单失败(如 -2021、网络错误),则**只有** WebSocket 能止损。 | 代码已写:挂单失败时打日志「将依赖 WebSocket 监控」,并再次检查当前价是否已穿止损,若已穿则立即市价平仓;未穿则仅靠 WS,进程若挂则无交易所保护。 | +| **价格源不一致** | 交易所条件单多用 **MARK_PRICE**,本机 WS 多用 last/mark。 | 可能有毫秒级差异,一般先触发的一方完成平仓,另一方发现无仓位即不再操作。 | +| **移动止损仅本机** | 移动止损(盈利到 X% 上移止损)只能由本机逻辑实现。 | **已改**:在移动止损**激活**或**更新**时,会调用 `_ensure_exchange_sltp_orders`,取消原止损/止盈条件单并按新止损价重挂,使移动止损也有交易所保护。 | + +--- + +## 五、小结 + +- **是,止损(和止盈)有两种**: + 1)**币安条件单**:到价由交易所自动止损/止盈; + 2)**系统 WebSocket 监控**:到价由本机市价平仓。 +- **两者并存**:同一仓位既有交易所单,又有本机监控,互为备份;本机还负责移动止损、TP1 部分平仓等。 +- **主要风险点**:交易所止损/止盈**挂单失败**时,只剩 WebSocket,进程挂了就无交易所保护。 +- **移动止损**:已在「移动止损激活」与「移动止损更新」时同步至交易所(取消原条件单并按新止损价重挂),本机宕机后交易所仍能按最新移动止损价执行。 diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 67f9109..f49d1c4 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1719,6 +1719,11 @@ class PositionManager: f"{symbol} 移动止损激活: 止损移至成本价 {entry_price:.4f} " f"(盈利: {pnl_percent_margin:.2f}% of margin)" ) + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") + except Exception as sync_e: + logger.warning(f"{symbol} 同步移动止损至交易所失败: {sync_e}") else: # 盈利超过阈值后,止损移至保护利润位(基于保证金) # 如果已经部分止盈,使用剩余仓位计算 @@ -1744,6 +1749,11 @@ class PositionManager: f"(保护{trailing_protect*100:.1f}% of remaining margin = {protect_amount:.4f} USDT, " f"剩余数量: {remaining_quantity:.4f})" ) + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") + except Exception as sync_e: + logger.warning(f"{symbol} 同步移动止损至交易所失败: {sync_e}") else: # 做空:止损价 = 开仓价 + (剩余盈亏 - 保护金额) / 剩余数量 # 注意:对于做空,止损价应该高于开仓价,所以用加法 @@ -1759,6 +1769,11 @@ class PositionManager: f"(保护{trailing_protect*100:.1f}% of remaining margin = {protect_amount:.4f} USDT, " f"剩余数量: {remaining_quantity:.4f})" ) + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") + except Exception as sync_e: + logger.warning(f"{symbol} 同步移动止损至交易所失败: {sync_e}") else: # 未部分止盈,使用原始仓位计算 protect_amount = margin * trailing_protect @@ -1774,6 +1789,11 @@ class PositionManager: f"{symbol} 移动止损更新: {new_stop_loss:.4f} " f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" ) + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") + except Exception as sync_e: + logger.warning(f"{symbol} 同步移动止损至交易所失败: {sync_e}") else: # 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 # 注意:对于做空,止损价应该高于开仓价,所以用加法 @@ -1789,6 +1809,11 @@ class PositionManager: f"{symbol} 移动止损更新: {new_stop_loss:.4f} " f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" ) + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + logger.info(f"{symbol} [定时检查] 已同步移动止损至交易所") + except Exception as sync_e: + logger.warning(f"{symbol} 同步移动止损至交易所失败: {sync_e}") # 检查止损(使用更新后的止损价,基于保证金收益比) # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行 @@ -3507,6 +3532,12 @@ class PositionManager: f"{symbol} [实时监控] 移动止损激活: 止损移至保护利润位 {new_stop_loss:.4f} " f"(盈利: {pnl_percent_margin:.2f}% of margin, 保护: {trailing_protect*100:.1f}% of margin)" ) + # 同步至交易所:取消原止损单并按新止损价重挂,使移动止损也有交易所保护 + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) + logger.info(f"{symbol} [实时监控] 已同步移动止损至交易所") + except Exception as sync_e: + logger.warning(f"{symbol} 同步移动止损至交易所失败(不影响本地监控): {sync_e}") else: # ⚠️ 优化:如果分步止盈第一目标已触发,移动止损不再更新剩余仓位的止损价 # 原因:分步止盈第一目标触发后,剩余50%仓位止损已移至成本价(保本),等待第二目标 @@ -3529,6 +3560,11 @@ class PositionManager: f"{symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} " f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" ) + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) + logger.info(f"{symbol} [实时监控] 已同步移动止损至交易所") + except Exception as sync_e: + logger.warning(f"{symbol} 同步移动止损至交易所失败(不影响本地监控): {sync_e}") else: # SELL # 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 # 注意:对于做空,止损价应该高于开仓价,所以用加法 @@ -3543,6 +3579,11 @@ class PositionManager: f"{symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} " f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" ) + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) + logger.info(f"{symbol} [实时监控] 已同步移动止损至交易所") + except Exception as sync_e: + logger.warning(f"{symbol} 同步移动止损至交易所失败(不影响本地监控): {sync_e}") # 检查止损(基于保证金收益比) # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行