fix(binance_client, position_manager, config): 增强止损与盈利保护逻辑
在 `binance_client.py` 中优化了错误处理,新增对特定错误信息的警告记录,确保在条件单被拒时能够清晰提示。同时,在 `position_manager.py` 中引入了保本止损逻辑,确保在盈利达到一定比例时自动将止损移至含手续费的保本价,提升了风险控制能力。此外,更新了 `config.py` 中的相关配置项,以支持移动止损与保本功能的灵活性。
This commit is contained in:
parent
13a0e7d580
commit
7569c88a67
86
docs/common/盈利保护对盈利率影响评估与建议.md
Normal file
86
docs/common/盈利保护对盈利率影响评估与建议.md
Normal file
|
|
@ -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 与简单回溯评估效果,按需微调或加「稳定时间」条件。
|
||||
|
|
@ -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;否则按检测结果处理
|
||||
|
|
|
|||
|
|
@ -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, # 取消持仓时间锁
|
||||
|
|
|
|||
|
|
@ -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("【市场扫描】")
|
||||
|
|
|
|||
|
|
@ -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,9 +1864,13 @@ 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:
|
||||
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:
|
||||
|
|
@ -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,14 +2158,12 @@ class PositionManager:
|
|||
logger.info(
|
||||
f"{symbol} 部分止盈成功: 平仓{partial_quantity:.4f},剩余{position_info['remainingQuantity']:.4f}"
|
||||
)
|
||||
# 分步止盈后的“保本”处理:
|
||||
# - 若启用 USE_TRAILING_STOP:允许把剩余仓位止损移至成本价,并进入移动止损阶段
|
||||
# - 若关闭 USE_TRAILING_STOP:严格不自动移动止损(避免你说的“仍然保本/仍然移动止损”)
|
||||
# 无论是否启用移动止损,分步止盈后都将剩余仓位止损移至成本价(保本)
|
||||
# 这样既不错失后续行情,又彻底杜绝了该笔交易亏损的可能
|
||||
position_info['stopLoss'] = entry_price
|
||||
# 分步止盈后的“保本”处理:仅在盈利保护总开关开启时移至含手续费保本价
|
||||
if profit_protection_enabled:
|
||||
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"剩余50%仓位追求1.5:1止盈目标"
|
||||
)
|
||||
else:
|
||||
|
|
@ -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:
|
||||
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,10 +4115,12 @@ class PositionManager:
|
|||
logger.info(
|
||||
f"{symbol} [实时监控] 部分止盈成功: 平仓{partial_quantity:.4f},剩余{position_info['remainingQuantity']:.4f}"
|
||||
)
|
||||
# 分步止盈后的"保本"处理:将剩余仓位止损移至成本价(保本)
|
||||
position_info['stopLoss'] = entry_price
|
||||
# 分步止盈后的"保本"处理:仅在盈利保护总开关开启时移至含手续费保本价
|
||||
if profit_protection_enabled:
|
||||
breakeven = self._breakeven_stop_price(entry_price, position_info['side'])
|
||||
position_info['stopLoss'] = breakeven
|
||||
logger.info(
|
||||
f"{symbol} [实时监控] 部分止盈后:剩余仓位止损移至成本价 {entry_price:.4f}(保本),"
|
||||
f"{symbol} [实时监控] 部分止盈后:剩余仓位止损移至含手续费保本价 {breakeven:.4f}(入场: {entry_price:.4f}),"
|
||||
f"剩余50%仓位追求更高收益(第二目标:4.0:1盈亏比或更高)"
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -285,5 +285,9 @@ class WSTradeClient:
|
|||
logger.debug(f"WSTradeClient: algoOrder.place 失败: {e}")
|
||||
raise
|
||||
except Exception as 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user