From 7569c88a67f74f4a794b3b61aadac7c7f14fdba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Fri, 20 Feb 2026 23:38:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(binance=5Fclient,=20position=5Fmanager,=20c?= =?UTF-8?q?onfig):=20=E5=A2=9E=E5=BC=BA=E6=AD=A2=E6=8D=9F=E4=B8=8E?= =?UTF-8?q?=E7=9B=88=E5=88=A9=E4=BF=9D=E6=8A=A4=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `binance_client.py` 中优化了错误处理,新增对特定错误信息的警告记录,确保在条件单被拒时能够清晰提示。同时,在 `position_manager.py` 中引入了保本止损逻辑,确保在盈利达到一定比例时自动将止损移至含手续费的保本价,提升了风险控制能力。此外,更新了 `config.py` 中的相关配置项,以支持移动止损与保本功能的灵活性。 --- ...保护对盈利率影响评估与建议.md | 86 +++++++++ trading_system/binance_client.py | 18 +- trading_system/config.py | 8 +- trading_system/main.py | 11 +- trading_system/position_manager.py | 175 +++++++++++------- trading_system/ws_trade_client.py | 6 +- 6 files changed, 231 insertions(+), 73 deletions(-) create mode 100644 docs/common/盈利保护对盈利率影响评估与建议.md diff --git a/docs/common/盈利保护对盈利率影响评估与建议.md b/docs/common/盈利保护对盈利率影响评估与建议.md new file mode 100644 index 0000000..fb1761d --- /dev/null +++ b/docs/common/盈利保护对盈利率影响评估与建议.md @@ -0,0 +1,86 @@ +# 盈利保护(移动止损+保本)对盈利率的影响评估与建议 + +## 一、当前逻辑简要回顾 + +- **PROFIT_PROTECTION_ENABLED**:总开关,关掉则不做移动止损与保本。 +- **尽早保本**:盈利达到 **LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT**(默认 2% 保证金)时,将止损移至「含手续费保本价」。 +- **移动止损**:盈利达到 **TRAILING_STOP_ACTIVATION**(默认 10%)后激活,按 **TRAILING_STOP_PROTECT**(默认 2%)保护利润并上移止损。 +- **部分止盈后保本**:第一目标(如 12%~20%)触发、平掉 50% 后,剩余仓位止损移至含手续费保本价。 + +--- + +## 二、对盈利率的预期影响 + +### 1. 正面(减少「先盈后亏」) + +- **2% 尽早保本**:一旦浮盈 ≥2%,最差也是平在保本附近(含手续费),能明显减少「曾经盈利最后亏着走」的笔数。 +- **保本价含手续费**:避免「名义保本、实际小亏」,提高真实保本比例。 +- **移动止损**:盈利拉大后锁住一部分利润,减少大幅回吐。 + +整体上会:**提高「不亏」的笔数、减轻回撤、可能提高夏普/稳定性**。 + +### 2. 负面(可能压低单笔盈利) + +- **2% 可能偏早**:山寨/高波动品种常出现 2% 来回波动。若刚触达 2% 就立刻移至保本,一次正常回撤就可能把单子平在保本,后面再涨到 15%~20% 就吃不到。 +- **结果**:部分本可到第一目标(如 12%~20%)的单子,会变成「保本出场」,**平均盈利可能被拉低**。 + +因此:**在减少「盈转亏」的同时,有可能把一部分「大赢」变成「小赢/保本」**。最终盈利率取决于: +- 被保本「救回来」的亏损单数量 vs +- 被保本「提前震出」、错过后续涨幅的笔数。 + +--- + +## 三、建议(在保留当前处理的前提下) + +### 1. 调高「尽早保本」阈值(优先建议) + +- 当前:**LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT = 0.02(2%)**。 +- 建议:先改为 **3%~5%**(如 `0.03`~`0.05`),再根据实盘/回测微调。 +- 理由: + - 2% 在波动大的标的上容易被「噪音」触发,增加被震出后再大涨的概率。 + - 3%~5% 仍能挡住大部分「先盈后亏」,同时给价格一点回撤空间,减少把潜在大赢单打成保本单。 + +若你观察到「很多单子刚到 2% 就回撤触发保本,之后又涨很多」,可再适当提高到 4%~5%。 + +### 2. 保持「保本含手续费」与总开关 + +- **FEE_BUFFER_PCT**、**PROFIT_PROTECTION_ENABLED** 的逻辑建议保留: + - 保本价含手续费能真实改善「盈转亏」比例。 + - 总开关便于在行情或策略变化时一键关闭保护,做对比或风控。 + +### 3. 可选:为「尽早保本」加「稳定时间」条件(进阶) + +- 思路:仅当「盈利在 ≥2%(或你设的阈值)以上维持一段时间」后,才执行「移至保本」。 + 例如:连续 2~3 次检查(或 1~2 分钟)都 ≥ 该阈值,再移止损。 +- 作用:减少因一根 K 线或一次 tick 冲高就立刻锁保本、随后被正常回撤打掉的情况。 +- 实现:在现有「盈利 ≥ LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT」判断上,增加「持续超过 N 秒或 N 次检查」再设 `breakevenStopSet`;若你愿意,可以后续再加这个开关和参数。 + +### 4. 用数据验证(强烈建议) + +- 在平仓原因(exit_reason)里区分: + - 因「尽早保本」触发的平仓(例如单独一个 reason 或标记)。 + - 因移动止损、第一目标、第二目标、原始止损等触发的平仓。 +- 定期看: + - 被「尽早保本」平掉的单子,若当时不平、按原止损/止盈规则,事后会怎样(可简单用「平仓后一段时间内的价格」做粗略回溯)。 + - 若多数是「保本出场后价格继续大涨」,说明阈值偏低,可再提高 LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT 或加「稳定时间」;若多数是「保本出场后价格大跌」,说明保护有价值,可维持或略降阈值。 + +--- + +## 四、参数建议汇总(可直接改配置) + +| 配置项 | 当前典型值 | 建议范围 | 说明 | +|--------|------------|----------|------| +| **PROFIT_PROTECTION_ENABLED** | True | 保持 True | 总开关,关掉则完全不保本/不移动止损。 | +| **LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT** | 0.02 (2%) | **0.03~0.05**(3%~5%) | 降低被短期波动震出、错过后续涨幅的概率。 | +| **TRAILING_STOP_ACTIVATION** | 0.10 (10%) | 0.08~0.12 | 可维持;若希望更早锁盈可略降到 8%。 | +| **TRAILING_STOP_PROTECT** | 0.02 (2%) | 0.02~0.03 | 可维持。 | +| **FEE_BUFFER_PCT** | 0.0015 | 保持 | 保本含手续费,不建议关。 | + +--- + +## 五、结论 + +- 加上「盈利保护(移动止损+保本)」后,**对盈利率的影响是双面的**: + - **减少「先盈后亏」**,有利于胜率和回撤。 + - **可能把一部分潜在大赢单变成保本/小赢单**,压低平均盈利。 +- **优先建议**:把 **LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT** 从 2% 提到 **3%~5%**,在「少亏」和「拿住趋势」之间折中;再通过 exit_reason 与简单回溯评估效果,按需微调或加「稳定时间」条件。 diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 62e4094..658ed69 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -2227,8 +2227,15 @@ class BinanceClient: logger.debug(f"{symbol} WS 条件单失败({e}),回退到 REST") except Exception as e: code = getattr(e, "code", None) + err_msg = str(e).strip() if code in (-4509, -4061): - raise BinanceAPIException(None, 400, json.dumps({"code": code, "msg": str(e)})) + raise BinanceAPIException(None, 400, json.dumps({"code": code, "msg": err_msg})) + # "Time in Force (TIF) GTE can only be used with open positions":持仓尚未可用或已平,不刷屏 + if "GTE" in err_msg and "open positions" in err_msg: + logger.warning( + f"{symbol} 条件单被拒(持仓未就绪或已平): {err_msg[:80]}…,将依赖 WebSocket 监控" + ) + return None logger.debug(f"{symbol} WS 条件单异常: {e},回退到 REST") # 回退到 REST(原有逻辑) @@ -2249,9 +2256,14 @@ class BinanceClient: return None except BinanceAPIException as e: error_code = e.code if hasattr(e, 'code') else None + error_msg = str(e).strip() if error_code in (-4509, -4061): raise # 让 place_trigger_close_position_order 统一打一条 warning,不在此处刷日志 - error_msg = str(e) + if "GTE" in error_msg and "open positions" in error_msg: + logger.warning( + f"{symbol} 条件单被拒(持仓未就绪或已平): {error_msg[:80]}…,将依赖 WebSocket 监控" + ) + return None trigger_type = params.get('type', 'UNKNOWN') logger.error(f"{symbol} ❌ 创建 Algo 条件单失败({trigger_type}): {error_msg}") logger.error(f" 错误代码: {error_code}") @@ -2508,6 +2520,7 @@ class BinanceClient: stop_price_str = self._format_price_str_with_rounding(sp, symbol_info, rounding_mode) # Algo 条件单接口使用 triggerPrice(不是 stopPrice) + # 显式传 timeInForce=GTC,避免交易所对 closePosition 单默认用 GTE 导致 "GTE can only be used with open positions" params: Dict[str, Any] = { "algoType": "CONDITIONAL", "symbol": symbol, @@ -2516,6 +2529,7 @@ class BinanceClient: "triggerPrice": stop_price_str, "workingType": working_type, "closePosition": True, + "timeInForce": "GTC", } # 单向持仓模式(ONE_WAY_POSITION_ONLY):不传 positionSide;否则按检测结果处理 diff --git a/trading_system/config.py b/trading_system/config.py index e573455..fbe1373 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -293,10 +293,14 @@ DEFAULT_TRADING_CONFIG = { 'USE_DYNAMIC_LEVERAGE': True, # 开启动态杠杆(基于止损宽度自动调整) 'MAX_SINGLE_TRADE_LOSS_PERCENT': 20.0, # 单笔交易最大本金亏损率(20%),用于限制杠杆 'MAX_LEVERAGE': 12, # 最大杠杆(回归盈利期上限) - # 移动止损:必须开启!山寨币利润要保护 - 'USE_TRAILING_STOP': True, + # 移动止损与保本:全局总开关(关闭后不执行移动止损、不执行盈利保本) + 'PROFIT_PROTECTION_ENABLED': True, # True=启用移动止损与保本,False=全部关闭 + 'USE_TRAILING_STOP': True, # 在 PROFIT_PROTECTION_ENABLED 为 True 时生效 'TRAILING_STOP_ACTIVATION': 0.10, # 盈利10%后激活(回归盈利期更早锁盈设置) 'TRAILING_STOP_PROTECT': 0.02, # 保护2%利润(回归盈利期紧凑保护) + # 盈利保护:保本含手续费,避免“保本”实为小亏(在 PROFIT_PROTECTION_ENABLED 为 True 时生效) + 'FEE_BUFFER_PCT': 0.0015, # 保本价相对入场价缓冲(0.15% 覆盖开平双向手续费) + 'LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT': 0.03, # 盈利达3%保证金时移至保本(建议3%~5%平衡「少亏」与「拿住趋势」,0=关闭) # 最小持仓时间锁:立即取消!山寨币30分钟可能暴涨暴跌50% 'MIN_HOLD_TIME_SEC': 0, # 取消持仓时间锁 diff --git a/trading_system/main.py b/trading_system/main.py index 399075e..63e7740 100644 --- a/trading_system/main.py +++ b/trading_system/main.py @@ -517,8 +517,9 @@ async def main(): logger.info(f" 盈亏比: {config.TRADING_CONFIG.get('RISK_REWARD_RATIO', 3.0)}:1") logger.info(f" 固定风险: {'开启' if config.TRADING_CONFIG.get('USE_FIXED_RISK_SIZING') else '关闭'}") logger.info(f" 每笔风险: {config.TRADING_CONFIG.get('FIXED_RISK_PERCENT', 0.02)*100:.1f}%") - logger.info(f" 移动止损: {'开启' if config.TRADING_CONFIG.get('USE_TRAILING_STOP') else '关闭'}") - if config.TRADING_CONFIG.get('USE_TRAILING_STOP'): + profit_protection = config.TRADING_CONFIG.get('PROFIT_PROTECTION_ENABLED', True) + logger.info(f" 盈利保护(移动止损+保本): {'开启' if profit_protection else '关闭'}") + if profit_protection and config.TRADING_CONFIG.get('USE_TRAILING_STOP'): # 修复:配置值已经是比例形式,直接乘以100显示 trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 0.1) trailing_protect = config.TRADING_CONFIG.get('TRAILING_STOP_PROTECT', 0.05) @@ -529,6 +530,12 @@ async def main(): trailing_protect = trailing_protect / 100.0 logger.info(f" 激活条件: 盈利{trailing_activation*100:.0f}%") logger.info(f" 保护利润: {trailing_protect*100:.0f}%") + if profit_protection: + fee_buf = config.TRADING_CONFIG.get('FEE_BUFFER_PCT', 0.0015) + lock_pct = config.TRADING_CONFIG.get('LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT', 0.02) + if lock_pct and lock_pct > 1: + lock_pct = lock_pct / 100.0 + logger.info(f" 保本含手续费缓冲 {float(fee_buf or 0)*100:.2f}% | 盈利达{float(lock_pct or 0)*100:.0f}%移至保本") logger.info(f" 最小持仓时间: {config.TRADING_CONFIG.get('MIN_HOLD_TIME_SEC', 0)} 秒") logger.info("") logger.info("【市场扫描】") diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 99a9496..52e24ae 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -939,6 +939,7 @@ class PositionManager: 'atr': atr, 'maxProfit': 0.0, # 记录最大盈利(用于移动止损) 'trailingStopActivated': False, # 移动止损是否已激活 + 'breakevenStopSet': False, # 是否已执行“盈利达阈值后移至保本” 'account_id': self.account_id } @@ -1344,6 +1345,22 @@ class PositionManager: pass return False + def _breakeven_stop_price(self, entry_price: float, side: str, fee_buffer_pct: Optional[float] = None) -> float: + """含手续费的保本止损价:做多=入场*(1+费率),做空=入场*(1-费率),平仓后不亏。""" + if fee_buffer_pct is None: + fee_buffer_pct = float(config.TRADING_CONFIG.get("FEE_BUFFER_PCT", 0.0015) or 0.0015) + if fee_buffer_pct > 0.01: + fee_buffer_pct = fee_buffer_pct / 100.0 + if side == "BUY": + return entry_price * (1 + fee_buffer_pct) + return entry_price * (1 - fee_buffer_pct) + + def _min_protect_amount_for_fees(self, margin: float, leverage: float) -> float: + """移动止损时至少保护的金额(覆盖开平双向手续费,避免“保护”后仍为负)。""" + lev = max(float(leverage or 10), 1) + # 双向约 0.05%*2=0.1% 名义 → 保证金上约 0.1%*leverage + return margin * (0.001 * lev) + async def _ensure_exchange_sltp_orders(self, symbol: str, position_info: Dict, current_price: Optional[float] = None) -> None: """ 在币安侧挂止损/止盈保护单(STOP_MARKET + TAKE_PROFIT_MARKET)。 @@ -1847,11 +1864,15 @@ class PositionManager: continue # 检查是否启用移动止损(默认False,需要显式启用) - use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', False) + profit_protection_enabled = bool(config.TRADING_CONFIG.get('PROFIT_PROTECTION_ENABLED', True)) + use_trailing = profit_protection_enabled and bool(config.TRADING_CONFIG.get('USE_TRAILING_STOP', False)) if use_trailing: logger.debug(f"{symbol} [移动止损] 已启用,将检查移动止损逻辑") else: - logger.debug(f"{symbol} [移动止损] 已禁用(USE_TRAILING_STOP=False),跳过移动止损检查") + if not profit_protection_enabled: + logger.debug(f"{symbol} [移动止损/保本] 已禁用(PROFIT_PROTECTION_ENABLED=False)") + else: + logger.debug(f"{symbol} [移动止损] 已禁用(USE_TRAILING_STOP=False),跳过移动止损检查") if use_trailing: trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 0.01) # 相对于保证金 trailing_protect = config.TRADING_CONFIG.get('TRAILING_STOP_PROTECT', 0.01) # 相对于保证金 @@ -1862,15 +1883,31 @@ class PositionManager: trailing_activation = trailing_activation / 100.0 if trailing_protect > 1: trailing_protect = trailing_protect / 100.0 + lock_pct = config.TRADING_CONFIG.get('LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT') or 0 + if lock_pct and lock_pct > 1: + lock_pct = lock_pct / 100.0 if not position_info.get('trailingStopActivated', False): + # 盈利达一定比例时尽早将止损移至含手续费保本(与实时监控一致) + if lock_pct > 0 and not position_info.get('breakevenStopSet', False) and pnl_percent_margin >= lock_pct * 100: + breakeven = self._breakeven_stop_price(entry_price, side) + current_sl = position_info.get('stopLoss') + set_be = (side == 'BUY' and (current_sl is None or current_sl < breakeven)) or (side == 'SELL' and (current_sl is None or current_sl > breakeven)) + if set_be: + position_info['stopLoss'] = breakeven + position_info['breakevenStopSet'] = True + logger.info(f"{symbol} [定时检查] 盈利{pnl_percent_margin:.2f}%≥{lock_pct*100:.0f}%,止损已移至含手续费保本价 {breakeven:.4f}") + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price) + except Exception as sync_e: + logger.warning(f"{symbol} 同步保本止损至交易所失败: {sync_e}") # 盈利超过阈值后(相对于保证金),激活移动止损 if pnl_percent_margin > trailing_activation * 100: position_info['trailingStopActivated'] = True - # 将止损移至成本价(保本) - position_info['stopLoss'] = entry_price + breakeven = self._breakeven_stop_price(entry_price, side) + position_info['stopLoss'] = breakeven logger.info( - f"{symbol} 移动止损激活: 止损移至成本价 {entry_price:.4f} " + f"{symbol} 移动止损激活: 止损移至含手续费保本价 {breakeven:.4f} (入场: {entry_price:.4f}) " f"(盈利: {pnl_percent_margin:.2f}% of margin)" ) try: @@ -1884,17 +1921,14 @@ class PositionManager: if position_info.get('partialProfitTaken', False): remaining_quantity = position_info.get('remainingQuantity', quantity) remaining_margin = (entry_price * remaining_quantity) / leverage if leverage > 0 else (entry_price * remaining_quantity) - protect_amount = remaining_margin * trailing_protect - - # 计算剩余仓位的盈亏 + protect_amount = max(remaining_margin * trailing_protect, self._min_protect_amount_for_fees(remaining_margin, leverage)) if position_info['side'] == 'BUY': remaining_pnl = (current_price - entry_price) * remaining_quantity else: remaining_pnl = (entry_price - current_price) * remaining_quantity - - # 计算新的止损价(基于剩余仓位) if position_info['side'] == 'BUY': new_stop_loss = entry_price + (remaining_pnl - protect_amount) / remaining_quantity + new_stop_loss = max(new_stop_loss, self._breakeven_stop_price(entry_price, 'BUY')) current_sl = float(position_info['stopLoss']) if position_info.get('stopLoss') is not None else None if current_sl is None or new_stop_loss > current_sl: position_info['stopLoss'] = new_stop_loss @@ -1909,12 +1943,8 @@ class PositionManager: except Exception as sync_e: logger.warning(f"{symbol} 同步移动止损至交易所失败: {sync_e}") else: - # 做空:止损价 = 开仓价 + (剩余盈亏 - 保护金额) / 剩余数量 - # 注意:对于做空,止损价应该高于开仓价,所以用加法 - # 移动止损只应该在盈利时激活 new_stop_loss = entry_price + (remaining_pnl - protect_amount) / remaining_quantity - # 对于做空,止损价应该越来越高(更宽松),所以检查 new_stop_loss > 当前止损 - # 同时,移动止损只应该在盈利时激活 + new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL')) current_sl = float(position_info['stopLoss']) if position_info.get('stopLoss') is not None else None if current_sl is not None and new_stop_loss > current_sl and remaining_pnl > 0: position_info['stopLoss'] = new_stop_loss @@ -1929,13 +1959,11 @@ class PositionManager: except Exception as sync_e: logger.warning(f"{symbol} 同步移动止损至交易所失败: {sync_e}") else: - # 未部分止盈,使用原始仓位计算 - protect_amount = margin * trailing_protect - # 计算对应的止损价 + # 未部分止盈,使用原始仓位计算;保护金额至少覆盖手续费 + protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage)) if position_info['side'] == 'BUY': - # 保护利润:当前盈亏 - 保护金额 = (止损价 - 开仓价) × 数量 - # 所以:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity + new_stop_loss = max(new_stop_loss, self._breakeven_stop_price(entry_price, 'BUY')) current_sl = float(position_info['stopLoss']) if position_info.get('stopLoss') is not None else None if current_sl is None or new_stop_loss > current_sl: position_info['stopLoss'] = new_stop_loss @@ -1954,8 +1982,7 @@ class PositionManager: # 当盈利时(pnl_amount > 0),止损价应该往上移(更宽松) # 当亏损时(pnl_amount < 0),不应该移动止损(保持初始止损) new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity - # 对于做空,止损价应该越来越高(更宽松),所以检查 new_stop_loss > 当前止损 - # 同时,移动止损只应该在盈利时激活,不应该在亏损时把止损往下移 + new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL')) current_sl = float(position_info['stopLoss']) if position_info.get('stopLoss') is not None else None if current_sl is not None and new_stop_loss > current_sl and pnl_amount > 0: position_info['stopLoss'] = new_stop_loss @@ -2131,16 +2158,14 @@ class PositionManager: logger.info( f"{symbol} 部分止盈成功: 平仓{partial_quantity:.4f},剩余{position_info['remainingQuantity']:.4f}" ) - # 分步止盈后的“保本”处理: - # - 若启用 USE_TRAILING_STOP:允许把剩余仓位止损移至成本价,并进入移动止损阶段 - # - 若关闭 USE_TRAILING_STOP:严格不自动移动止损(避免你说的“仍然保本/仍然移动止损”) - # 无论是否启用移动止损,分步止盈后都将剩余仓位止损移至成本价(保本) - # 这样既不错失后续行情,又彻底杜绝了该笔交易亏损的可能 - position_info['stopLoss'] = entry_price - logger.info( - f"{symbol} 部分止盈后:剩余仓位止损移至成本价 {entry_price:.4f}(保本)," - f"剩余50%仓位追求1.5:1止盈目标" - ) + # 分步止盈后的“保本”处理:仅在盈利保护总开关开启时移至含手续费保本价 + if profit_protection_enabled: + breakeven = self._breakeven_stop_price(entry_price, side) + position_info['stopLoss'] = breakeven + logger.info( + f"{symbol} 部分止盈后:剩余仓位止损移至含手续费保本价 {breakeven:.4f}(入场: {entry_price:.4f})," + f"剩余50%仓位追求1.5:1止盈目标" + ) else: # 兜底:可能遇到 -2022(reduceOnly rejected)等竞态,重新查一次持仓 try: @@ -2446,6 +2471,7 @@ class PositionManager: 'atr': trade.get('atr'), 'maxProfit': 0.0, 'trailingStopActivated': False, + 'breakevenStopSet': False, } self.active_positions[symbol] = position_info logger.debug(f"{symbol} 已从 DB 载入到 active_positions,便于监控") @@ -3375,7 +3401,8 @@ class PositionManager: 'entryTime': entry_time_ts if entry_time_ts is not None else get_beijing_time(), # 真实开仓时间(来自币安成交/订单) 'atr': None, 'maxProfit': 0.0, - 'trailingStopActivated': False + 'trailingStopActivated': False, + 'breakevenStopSet': False } self.active_positions[symbol] = position_info @@ -3532,7 +3559,8 @@ class PositionManager: 'entryReason': entry_reason, 'atr': None, 'maxProfit': 0.0, - 'trailingStopActivated': False + 'trailingStopActivated': False, + 'breakevenStopSet': False } # 订单统一由自动下单入 DB,此处仅做内存监控不创建 DB 记录 if not sync_create_manual and DB_AVAILABLE and Trade and not config.TRADING_CONFIG.get("ONLY_AUTO_TRADE_CREATES_RECORDS", True): @@ -3814,35 +3842,56 @@ class PositionManager: # 注意:如果需要防止秒级平仓,可以通过提高入场信号质量(MIN_SIGNAL_STRENGTH)来实现 # 检查是否启用移动止损(默认False,需要显式启用) - use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', False) + profit_protection_enabled = bool(config.TRADING_CONFIG.get('PROFIT_PROTECTION_ENABLED', True)) + use_trailing = profit_protection_enabled and bool(config.TRADING_CONFIG.get('USE_TRAILING_STOP', False)) if use_trailing: logger.debug(f"{symbol} [实时监控-移动止损] 已启用,将检查移动止损逻辑") else: - logger.debug(f"{symbol} [实时监控-移动止损] 已禁用(USE_TRAILING_STOP=False),跳过移动止损检查") + if not profit_protection_enabled: + logger.debug(f"{symbol} [实时监控-移动止损/保本] 已禁用(PROFIT_PROTECTION_ENABLED=False)") + else: + logger.debug(f"{symbol} [实时监控-移动止损] 已禁用(USE_TRAILING_STOP=False),跳过移动止损检查") if use_trailing: trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 0.01) # 相对于保证金 trailing_protect = config.TRADING_CONFIG.get('TRAILING_STOP_PROTECT', 0.01) # 相对于保证金 + lock_pct = config.TRADING_CONFIG.get('LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT') or 0 + if lock_pct and lock_pct > 1: + lock_pct = lock_pct / 100.0 if not position_info.get('trailingStopActivated', False): + # 盈利达一定比例时尽早将止损移至含手续费保本,避免先盈后亏 + if lock_pct > 0 and not position_info.get('breakevenStopSet', False) and pnl_percent_margin >= lock_pct * 100: + breakeven = self._breakeven_stop_price(entry_price, position_info['side']) + current_sl = position_info.get('stopLoss') + side_here = position_info['side'] + set_breakeven = False + if side_here == 'BUY' and (current_sl is None or current_sl < breakeven): + set_breakeven = True + elif side_here == 'SELL' and (current_sl is None or current_sl > breakeven): + set_breakeven = True + if set_breakeven: + position_info['stopLoss'] = breakeven + position_info['breakevenStopSet'] = True + logger.info( + f"{symbol} [实时监控] 盈利{pnl_percent_margin:.2f}%≥{lock_pct*100:.0f}%,止损已移至含手续费保本价 {breakeven:.4f}(留住盈利)" + ) + try: + await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) + except Exception as sync_e: + logger.warning(f"{symbol} 同步保本止损至交易所失败: {sync_e}") # 盈利超过阈值后(相对于保证金),激活移动止损 if pnl_percent_margin > trailing_activation * 100: position_info['trailingStopActivated'] = True - # ⚠️ 2026-01-27修复:移动止损激活时,不应该将止损移至成本价 - # 应该设置为"保护利润"的价格(如盈利5%后,保护2.5%利润) - # 计算需要保护的利润金额 - protect_amount = margin * trailing_protect - # 计算对应的止损价(保护利润) + # 保护利润金额至少覆盖双向手续费,避免“保护”后仍为负 + protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage)) if position_info['side'] == 'BUY': - # 保护利润:当前盈亏 - 保护金额 = (止损价 - 开仓价) × 数量 - # 所以:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity - # 确保止损价不低于成本价(保本) - new_stop_loss = max(new_stop_loss, entry_price) + breakeven = self._breakeven_stop_price(entry_price, 'BUY') + new_stop_loss = max(new_stop_loss, breakeven) else: # SELL - # 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity - # 确保止损价不高于成本价(保本) - new_stop_loss = min(new_stop_loss, entry_price) + breakeven = self._breakeven_stop_price(entry_price, 'SELL') + new_stop_loss = min(new_stop_loss, breakeven) position_info['stopLoss'] = new_stop_loss logger.info( @@ -3863,14 +3912,11 @@ class PositionManager: # 分步止盈第一目标已触发,移动止损不再更新 logger.debug(f"{symbol} [实时监控-移动止损] 分步止盈第一目标已触发,移动止损不再更新剩余仓位止损价") else: - # 盈利超过阈值后,止损移至保护利润位(基于保证金) - # 计算需要保护的利润金额 - protect_amount = margin * trailing_protect - # 计算对应的止损价 + # 盈利超过阈值后,止损移至保护利润位(基于保证金);保护金额至少覆盖手续费 + protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage)) if position_info['side'] == 'BUY': - # 保护利润:当前盈亏 - 保护金额 = (止损价 - 开仓价) × 数量 - # 所以:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity + new_stop_loss = max(new_stop_loss, self._breakeven_stop_price(entry_price, 'BUY')) if new_stop_loss > position_info['stopLoss']: position_info['stopLoss'] = new_stop_loss logger.info( @@ -3883,13 +3929,8 @@ class PositionManager: except Exception as sync_e: logger.warning(f"{symbol} 同步移动止损至交易所失败(不影响本地监控): {sync_e}") else: # SELL - # 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 - # 注意:对于做空,止损价应该高于开仓价,所以用加法 - # 当盈利时(pnl_amount > 0),止损价应该往上移(更宽松) - # 当亏损时(pnl_amount < 0),不应该移动止损(保持初始止损) new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity - # 对于做空,止损价应该越来越高(更宽松),所以检查 new_stop_loss > 当前止损 - # 同时,移动止损只应该在盈利时激活,不应该在亏损时把止损往下移 + new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL')) if new_stop_loss > position_info['stopLoss'] and pnl_amount > 0: position_info['stopLoss'] = new_stop_loss logger.info( @@ -4074,12 +4115,14 @@ class PositionManager: logger.info( f"{symbol} [实时监控] 部分止盈成功: 平仓{partial_quantity:.4f},剩余{position_info['remainingQuantity']:.4f}" ) - # 分步止盈后的"保本"处理:将剩余仓位止损移至成本价(保本) - position_info['stopLoss'] = entry_price - logger.info( - f"{symbol} [实时监控] 部分止盈后:剩余仓位止损移至成本价 {entry_price:.4f}(保本)," - f"剩余50%仓位追求更高收益(第二目标:4.0:1盈亏比或更高)" - ) + # 分步止盈后的"保本"处理:仅在盈利保护总开关开启时移至含手续费保本价 + if profit_protection_enabled: + breakeven = self._breakeven_stop_price(entry_price, position_info['side']) + position_info['stopLoss'] = breakeven + logger.info( + f"{symbol} [实时监控] 部分止盈后:剩余仓位止损移至含手续费保本价 {breakeven:.4f}(入场: {entry_price:.4f})," + f"剩余50%仓位追求更高收益(第二目标:4.0:1盈亏比或更高)" + ) except Exception as e: logger.error(f"{symbol} [实时监控] 部分止盈失败: {e}") diff --git a/trading_system/ws_trade_client.py b/trading_system/ws_trade_client.py index 3bf0b80..262bb4e 100644 --- a/trading_system/ws_trade_client.py +++ b/trading_system/ws_trade_client.py @@ -285,5 +285,9 @@ class WSTradeClient: logger.debug(f"WSTradeClient: algoOrder.place 失败: {e}") raise except Exception as e: - logger.error(f"WSTradeClient: algoOrder.place 异常: {e}") + err = str(e).strip() + if "GTE" in err and "open positions" in err: + logger.warning(f"WSTradeClient: algoOrder.place 被拒(持仓未就绪或已平): {err[:80]}…") + else: + logger.error(f"WSTradeClient: algoOrder.place 异常: {e}") raise