feat(position_manager): Enhance stop-loss logic for short positions and implement automatic SL/TP order synchronization

Updated the stop-loss calculation for short positions to ensure it locks in profits effectively. Added logic to automatically synchronize stop-loss and take-profit orders for positions without existing SL/TP on the exchange. This improves risk management and ensures positions are adequately protected. Enhanced logging for better tracking of stop-loss updates and synchronization events.
This commit is contained in:
薇薇安 2026-02-28 19:01:59 +08:00
parent 5d5ead36ac
commit 4a5406c7e8
3 changed files with 268 additions and 21 deletions

View File

@ -0,0 +1,99 @@
# 持仓分析持仓记录_2026-02-28T10-43-05.json
## 一、20% 以上盈利的持仓(共 3 个)
| 交易对 | 方向 | 盈亏% | 入场价 | 标记价 | 止损 | 止盈 | 交易所 SL/TP |
|--------|------|-------|--------|--------|------|------|--------------|
| **WIFUSDT** | SELL | **40.3%** | 0.2051 | 0.1863 | **无** | **无** | **无** |
| **ZECUSDT** | SELL | **26.3%** | 221.44 | 207.78 | **无** | **无** | **无** |
| **BCHUSDT** | SELL | **23.7%** | 470.74 | 444.40 | 479.05 | 436.04 | 有 |
### 止损问题诊断
1. **WIF、ZEC完全没有止损/止盈**
- 两笔已浮盈 40%、26%,但交易所无任何 SL/TP 挂单,一旦反转会全部回吐甚至变亏。
- 可能原因:非本系统开仓(手动/别处)、或开仓时未成功挂出 SL/TP、或同步/补建时未补挂。
- **建议**:尽快在交易所或通过系统对这两笔**至少挂上保本/锁利止损**(例如锁住一半利润或 5%10% 利润)。
2. **BCH有 SL/TP但止损未上移锁利**
- 空头:入场 470.74,当前 444.40,止损仍在 **479.05**(开仓时的初始止损,高于入场价)。
- 正确做法:盈利 23.7% 后,**做空的移动止损应下移**(例如从 479 移到 455460 一带),锁住部分利润;否则价格反弹到 479 才触发,会几乎回吐全部盈利。
- 可能原因:
- 该持仓不在本系统 `active_positions` 内(例如由同步/补建得到),未参与定时检查的移动止损逻辑;
- 或做空方向的移动止损更新条件/方向在代码中有误(见下文「策略执行」)。
---
## 二、全部 13 笔持仓汇总
| 交易对 | 方向 | 盈亏% | 止损 | 止盈 | 交易所有 SL/TP |
|--------|------|-------|------|------|----------------|
| WIFUSDT | SELL | 40.3 | 无 | 无 | 无 |
| ZECUSDT | SELL | 26.3 | 无 | 无 | 无 |
| BCHUSDT | SELL | 23.7 | 479.05 | 436.04 | 有 |
| VVVUSDT | BUY | 15.1 | 无 | 无 | 无 |
| PUMPUSDT | SELL | 7.8 | 0.001813 | 0.001651 | 有 |
| ZROUSDT | SELL | 4.7 | 1.5763 | 1.4348 | 有 |
| SAHARAUSDT | BUY | 2.5 | 0.01829 | 0.02356 | 有 |
| ASTERUSDT | SELL | -0.0 | 0.7152 | 0.651 | 有 |
| MYXUSDT | SELL | -0.5 | 无 | 无 | 无 |
| FARTCOINUSDT | SELL | -0.8 | 无 | 无 | 无 |
| HYPEUSDT | SELL | -3.8 | 无 | 无 | 无 |
| VIRTUALUSDT | BUY | -9.5 | 无 | 无 | 无 |
| **ICPUSDT** | **BUY** | **-24.5** | **无** | **无** | **无** |
- **无任何止损/止盈的共 7 笔**VIRTUAL, ZEC, HYPE, FARTCOIN, VVV, ICP, WIF, MYX。其中 **ICP 已亏 24.5%**,若无止损会继续放大亏损。
---
## 三、策略执行问题分析
### 1. 为何大量持仓「无止损/无止盈」?
- 若这些仓位是**本系统开仓**:开仓后应调用 `_ensure_exchange_sltp_orders` 在交易所挂 SL/TP若仍无挂单可能是
- 开仓流程中未成功挂单(网络/权限/参数错误)且未重试或告警;
- 或仓位来自**同步/补建**币安有仓、DB 无记录),补建时若未同时挂 SL/TP就会一直处于「无保护」状态。
- **建议**
- 对「有持仓但无 SL/TP」的 symbol在同步/健康检查中**自动补挂**(至少止损,止盈可按配置或保守价);
- 开仓后若挂单失败,要有重试与日志/告警,便于排查。
### 2. 做空SELL移动止损方向是否正确
- 做空盈利时,应**下移止损价**(从高位往当前价方向移)才能锁利;当前 BCH 空头止损仍在 479未下移。
- 代码中做空分支(`position_manager` 定时检查)有:
- `new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity`
- `new_stop_loss = min(new_stop_loss, breakeven)`,再判断 `new_stop_loss > current_sl` 才更新。
- 做空时 `pnl_amount = (entry - current)*q > 0``entry + (pnl - protect)/q` 可能**大于**初始止损 479导致条件 `new_stop_loss > current_sl` 不成立,从而从不更新;即便更新,也是把止损往**更上方**移(更宽松),而不是往下锁利。这与「盈利后锁利」的目标相反。
- **建议**:复查做空移动止损公式与更新条件,确保「盈利后新止损比当前止损更靠近当前价」(空头即新止损 < 当前止损并只在锁利方向上更新
### 3. 亏损单 ICP -24.5% 且无止损
- 单笔已亏 24.5%,若无止损会继续暴露风险。
- **建议**:立即对该仓设止损(或减仓/平仓);并检查为何该仓从未挂上止损(是否补建、是否开仓失败挂单等)。
### 4. 多账号/多数据源一致性
- 持仓记录中 `id`、`entry_time`、`created_at` 多为 null说明可能来自**交易所快照 + 部分本地合并**,而非全部来自本系统 DB。要确保
- 凡在交易所存在的仓位,只要被本系统「认领」或展示,就应纳入监控并在缺失时补挂 SL/TP
- 移动止损/滞涨早止盈等逻辑应对**所有被监控仓位**生效,包括同步/补建进来的。
---
## 四、建议汇总
| 优先级 | 建议 |
|--------|------|
| **P0** | **WIF、ZEC**:立即在交易所或通过系统挂上止损(至少保本或锁 5%10% 利润),避免 40%/26% 盈利回吐。 |
| **P0** | **ICP**:已亏 24.5% 且无止损,立即设止损或减仓/平仓,并排查为何无 SL。 |
| **P1** | **BCH**:检查该仓是否在 `active_positions`、是否参与移动止损;若在,复查做空移动止损公式与更新条件,确保盈利后下移止损锁利。 |
| **P1** | 对所有「有仓但无 SL/TP」的持仓含同步/补建)在同步或健康检查中**自动补挂止损**(止盈可选)。 |
| **P2** | 开仓后 SL/TP 挂单失败时增加重试与告警;移动止损(尤其做空)逻辑做代码审查与单测。 |
| **P2** | 滞涨早止盈:若已开启,确认对 20%+ 盈利的仓位在「长时间不创新高」时能触发分批减仓+抬止损。 |
---
## 五、小结
- **3 笔 20%+ 盈利**2 笔WIF、ZEC**完全没有止损**1 笔BCH有止损但**未随盈利下移锁利**,整体「止损不对」的观感成立。
- **7 笔无任何 SL/TP**,其中 1 笔已亏 24.5%ICP风险突出。
- 策略执行上需:**补挂缺失 SL/TP**、**修正做空移动止损方向与条件**、**保证同步/补建仓位也纳入监控并补挂保护单**。

View File

@ -0,0 +1,127 @@
# 交易复盘分析binance_trades_2_2026-02-28近一日
## 一、整体表现
| 指标 | 数值 |
|------|------|
| 总成交笔数 | 83开仓相关 47 + 平仓相关 36 |
| 盈利次数 / 亏损次数 | 7 / 29 |
| **胜率** | **19.4%** |
| 总已实现盈亏 | -2.10 USDT |
| 总手续费 | 0.60 USDT |
| **净盈亏** | **-2.70 USDT** |
| 总成交额(quote) | 1810.26 USDT |
| 手续费率 | ≈0.033% |
**结论**:胜率明显偏低,亏损单数量约为盈利单的 4 倍,单笔平均亏损虽被止损控制,但累积亏损 + 手续费导致净损。需要从**入场质量、止损止盈平衡、同一交易对重复交易、时段/币种过滤**等多环节优化。
---
## 二、按交易对表现
### 亏损集中(建议重点关注或加入黑名单/降权)
| 交易对 | 盈亏(USDT) | 胜/负 | 说明 |
|--------|------------|-------|------|
| PENGUUSDT | -1.75 | 0/2 | 连续 2 笔亏损 |
| AAVEUSDT | -1.73 | 0/1 | 单笔大亏 |
| HYPEUSDT | -1.43 | 0/1 | 单笔大亏 |
| WIFUSDT | -1.34 | 0/3 | **同一天 3 笔亏损**,重复交易严重 |
| ENAUSDT | -1.11 | 0/1 | 单笔大亏 |
| PUMPUSDT | -1.10 | 0/3 | **同一天 3 笔亏损** |
| SIRENUSDT | -0.71 | 0/3 | **同一天 3 笔亏损** |
| RIVERUSDT | -0.66 | 0/2 | 连续 2 笔亏损 |
| WLDUSDT | -0.51 | 0/3 | **同一天 3 笔亏损** |
### 盈利交易对(可作白名单或维持当前逻辑)
| 交易对 | 盈亏(USDT) | 胜/负 |
|--------|------------|-------|
| ARUSDT | +3.47 | 1/0 |
| ZECUSDT | +2.19 | 1/0 |
| TAOUSDT | +1.13 | 2/3有盈有亏整体小正 |
| VVVUSDT | +1.83 | 1/0 |
| 1000LUNCUSDT | +1.41 | 2/0 |
**问题**WIF、PUMP、SIREN、WLD 等同一天内多次亏损,说明要么**冷却时间不足**,要么**同一交易对当日开仓次数**未做上限,导致在弱势品种上反复试错。
---
## 三、按时段表现
- **亏损集中时段**0 时(-1.43)、1 时(-1.64)、7 时(-0.42)、8 时(-1.02)、17 时(-0.97)、19 时(-1.74)、20 时(-1.34)、21 时(-1.75)。
- **盈利集中时段**5 时(+1.41)、6 时(+3.41)、10 时(+0.73)、12 时(+0.85)、22 时(+2.19)。
- **按星期**:周五 48 笔、-2.42 USDT周六 35 笔、+0.31 USDT。
**结论**:凌晨 01 时、早 78 时、晚 1721 时表现较差,可与「按小时统计过滤/黑名单」结合,在这些时段对部分品种降权或减少开仓。
---
## 四、策略环节诊断与优化建议
### 1. 入场与信号质量
- **现象**:胜率 19.4%,多数单子被止损打掉,说明**假突破或逆势入场**较多。
- **建议**
- 将 **MIN_SIGNAL_STRENGTH** 从 7 提高到 **8**,减少弱信号开仓。
- 确认 **LOW_VOLATILITY_MIN_SIGNAL_STRENGTH**(当前 9在低波动期已生效。
- 对近期统计中**连续亏损或胜率极低的交易对**使用黑名单(硬/软)或降权,避免在 PENGU、WIF、PUMP、SIREN、WLD 等上重复开仓。
### 2. 同一交易对重复交易与冷却
- **现象**WIF、PUMP、SIREN、WLD 同一天内 3 笔亏损,说明冷却或「同一 symbol 当日次数」未充分限制。
- **建议**
- **SYMBOL_LOSS_COOLDOWN_SEC** 从 3600 提高到 **72002 小时)** 或更长,观察同一 symbol 亏损后是否仍频繁再开。
- 增加「**同一交易对每日最大开仓次数**」(如 2 次),超过则当日不再开该 symbol。
- 若已有基于 `trade_stats` 的黑名单/降权,确保这些近期大亏或连亏的 symbol 被纳入并生效。
### 3. 止损与止盈平衡
- **现象**:亏损单多、盈利单少,但单笔亏损被止损控制在约 -0.2-1.7 USDT说明止损在执行盈利单如 AR、ZEC、TAO、VVV 能拿住一部分利润。
- **建议**
- 当前 **STOP_LOSS_PERCENT** 5%、**TAKE_PROFIT_1_PERCENT** 12%、**TAKE_PROFIT_PERCENT** 25%:若多数单在未到第一目标就被止损,可考虑:
- **略放宽止损**(如 ATR 倍数从 2.0 调到 2.2)或 **MIN_STOP_LOSS_PRICE_PCT** 略增,减少「刚入场就被震荡扫损」;
- 或**提高入场门槛**(提高 MIN_SIGNAL_STRENGTH从源头减少劣质单而不是一味放宽止损。
- **滞涨早止盈**STAGNATION_EARLY_EXIT若已开启可观察是否在「曾涨到约 10% 后长时间不创新高」时锁利,减少由盈转亏。
### 4. 手续费与频率
- **现象**83 笔、手续费 0.60 USDT、费率约 0.033%,单笔平均约 0.007 USDT占比不高但**交易频率偏高**会放大亏损和手续费。
- **建议**
- 在提高信号门槛、冷却和 symbol 限制后,**自然降低开仓频率**,优先质量而非数量。
- 若使用市价单较多,可评估在能接受延迟的前提下部分改用限价单以降低 taker 费率。
### 5. 时段与品种过滤(统计过滤)
- **现象**01 时、78 时、1721 时亏损集中PENGU、WIF、PUMP、SIREN、WLD、AAVE、HYPE、ENA 等明显偏弱。
- **建议**
- 使用现有 **trade_stats 按 symbol / 按小时** 的统计:对**亏损集中时段**做降权或禁止开仓,对**近期连亏/大亏的 symbol** 加入黑名单或软黑名单(降权)。
- 白名单可考虑优先 AR、ZEC、TAO、VVV、1000LUNC 等近期有盈利记录的品种(需结合更长周期再确认)。
### 6. 风控与仓位
- **现象**:单笔亏损多在 -0.2-1.7 USDT未出现单笔爆仓式亏损说明**单笔风险**受控。
- **建议**
- 保持 **MAX_OPEN_POSITIONS**、**FIXED_RISK_PERCENT** 等不变,重点放在**减少开仓次数、提高单笔质量**上,而非加大仓位。
---
## 五、优化项汇总(按优先级)
| 优先级 | 优化项 | 说明 |
|--------|--------|------|
| P0 | 提高 MIN_SIGNAL_STRENGTH 到 8 | 减少弱信号开仓,提升胜率 |
| P0 | 延长 SYMBOL_LOSS_COOLDOWN_SEC 到 7200 | 降低同一 symbol 连续亏损次数 |
| P1 | 同一交易对每日最大开仓次数上限 | 避免 WIF/PUMP/SIREN/WLD 类重复试错 |
| P1 | 基于统计的黑名单/时段降权 | 对近期连亏 symbol、亏损集中时段过滤或降权 |
| P2 | 评估略放宽止损ATR 或 MIN_STOP | 在提高入场质量前提下,减少震荡扫损 |
| P2 | 滞涨早止盈观察与调参 | 确认 STAGNATION_* 是否在「涨后横盘」时锁利 |
| P3 | 盈利品种白名单/优先 | 结合更长周期后,对稳定盈利的 symbol 做优先或白名单 |
---
## 六、结论
- **主要问题**胜率低19.4%)、同一交易对同一天多次亏损、部分时段和品种持续贡献亏损。
- **策略环节**:入场信号偏松、同一 symbol 冷却与次数限制不足、缺少基于近期统计的 symbol/时段过滤,是当前最值得改动的环节;止损执行正常,可先通过「提高质量、减少次数」再考虑是否微调止损/止盈。
- **建议执行顺序**:先做 P0信号门槛 + 冷却),观察 12 天;再上 P1每日同 symbol 上限 + 统计黑名单/时段最后按需微调止损与滞涨早止盈P2/P3

View File

@ -2254,13 +2254,15 @@ class PositionManager:
exc_info=False,
)
else:
new_stop_loss = entry_price + (remaining_pnl - protect_amount) / remaining_quantity
new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL'))
# 做空锁利:止损下移,锁住 protect_amount 利润。(entry - stop)*q = protect → stop = entry - protect/q
new_stop_loss = entry_price - protect_amount / remaining_quantity
new_stop_loss = max(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:
# 仅当新止损低于当前止损(下移锁利)且高于市价(锁住的是利润)时更新
if (current_sl is None or new_stop_loss < current_sl) and new_stop_loss > current_price and remaining_pnl > 0:
position_info['stopLoss'] = new_stop_loss
logger.info(
f"{symbol} 移动止损更新(剩余仓位: {new_stop_loss:.4f} "
f"{symbol} 移动止损更新(剩余仓位-做空锁利: {new_stop_loss:.4f} "
f"(保护{trailing_protect*100:.1f}% of remaining margin = {protect_amount:.4f} USDT, "
f"剩余数量: {remaining_quantity:.4f})"
)
@ -2300,17 +2302,15 @@ class PositionManager:
exc_info=False,
)
else:
# 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量
# 注意:对于做空,止损价应该高于开仓价,所以用加法
# 当盈利时pnl_amount > 0止损价应该往上移更宽松
# 当亏损时pnl_amount < 0不应该移动止损保持初始止损
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity
new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL'))
# 做空锁利:止损下移(从初始高位往市价方向移),锁住 protect_amount。(entry - stop)*q = protect → stop = entry - protect/q
new_stop_loss = entry_price - protect_amount / quantity
new_stop_loss = max(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:
# 仅当新止损低于当前止损(下移锁利)且高于市价(锁住的是利润)时更新
if (current_sl is None or new_stop_loss < current_sl) and new_stop_loss > current_price and pnl_amount > 0:
position_info['stopLoss'] = new_stop_loss
logger.info(
f"{symbol} 移动止损更新: {new_stop_loss:.4f} "
f"{symbol} 移动止损更新(做空锁利): {new_stop_loss:.4f} "
f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)"
)
_log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="定时检查")
@ -2820,6 +2820,24 @@ class PositionManager:
except Exception as e:
logger.debug(f"{symbol} 载入 active_positions 失败: {e}")
# 2.6 对「有仓但交易所无 SL/TP」的持仓自动补挂止损止盈含仅在本系统 active 的仓)
for symbol in list(self.active_positions.keys()):
if symbol not in binance_symbols:
continue
position_info = self.active_positions[symbol]
if not position_info.get('stopLoss') and not position_info.get('takeProfit'):
continue
try:
sl_from_ex, tp_from_ex = await self._get_sltp_from_exchange(symbol, position_info['side'])
if sl_from_ex is not None and tp_from_ex is not None:
continue
ticker = await self.client.get_ticker_24h(symbol)
current_price = float(ticker.get('price', 0) or position_info.get('entryPrice', 0))
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price)
logger.info(f"[账号{self.account_id}] {symbol} [同步] 检测到交易所无止损/止盈,已补挂 SL/TP")
except Exception as e:
logger.warning(f"[账号{self.account_id}] {symbol} [同步] 补挂 SL/TP 失败: {e}", exc_info=False)
# 3. 找出在数据库中open但在币安已不存在的持仓
missing_in_binance = db_open_symbols - binance_symbols
@ -4412,10 +4430,10 @@ class PositionManager:
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity
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
else: # SELL 做空锁利:止损下移,锁住 protect_amount。(entry - stop)*q = protect → stop = entry - protect/q
new_stop_loss = entry_price - protect_amount / quantity
breakeven = self._breakeven_stop_price(entry_price, 'SELL')
new_stop_loss = min(new_stop_loss, breakeven)
new_stop_loss = max(new_stop_loss, breakeven)
position_info['stopLoss'] = new_stop_loss
logger.info(
@ -4448,7 +4466,8 @@ class PositionManager:
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']:
current_sl_buy = position_info.get('stopLoss')
if current_sl_buy is None or new_stop_loss > current_sl_buy:
position_info['stopLoss'] = new_stop_loss
logger.info(
f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} "
@ -4465,13 +4484,15 @@ class PositionManager:
f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
else: # SELL
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity
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:
else: # SELL 做空锁利:止损下移,锁住 protect_amount。(entry - stop)*q = protect → stop = entry - protect/q
new_stop_loss = entry_price - protect_amount / quantity
new_stop_loss = max(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL'))
current_sl = position_info.get('stopLoss')
# 仅当新止损低于当前止损(下移锁利)且高于市价时更新
if (current_sl is None or new_stop_loss < current_sl) and new_stop_loss > current_price_float and pnl_amount > 0:
position_info['stopLoss'] = new_stop_loss
logger.info(
f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} "
f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新(做空锁利): {new_stop_loss:.4f} "
f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)"
)
_log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="实时监控")