设计文档: 信号消退平仓 (Signal Exit on Profit)
设计文档: 信号消退平仓 (Signal Exit on Profit)
背景
当前平仓条件有4种:止损(PnL < -3%)、移动止盈(PnL >= 5% 后回撤0.5%)、持仓超时(72h)、Kill Switch。
存在一种场景:持仓盈利已达2%~5%之间,但尚未触发移动止盈的激活阈值(5%),此时如果开仓信号已经消退(当前市场状态不再满足开仓条件),继续持仓可能导致盈利回吐。
需求
新增平仓条件:当 PnL > 2% 且当前最新状态不满足本次持仓的开仓条件时,执行平仓。
开仓条件回顾
开仓需要同时满足三个条件(strategy.py:check_signal()):
- 条件1:
ema_beta < beta_ema_threshold— 负收益beta EMA低于阈值 - 条件2:
ema_beta < mean_beta— 负收益beta EMA低于均值(收敛趋势) - 条件3:
ema_beta < positive_ema_beta— 负收益EMA低于正收益EMA(上涨弹性 > 下跌弹性)
"不满足开仓条件" = 三个条件中任意一个不满足。
详细设计
1. src/trading/config.py — 新增配置参数
a) BetaArbConfig dataclass 新增字段
signal_exit_pnl_pct: float = 0.02 # 信号消退平仓的PnL阈值 (2%)
放在 stop_loss_pct 之后。
b) load_trading_config() 新增环境变量加载
signal_exit_pnl_pct=_env_float("BETA_ARB_SIGNAL_EXIT_PNL_PCT", "0.02"),
2. src/trading/strategy.py — 新增开仓条件检查方法
新增 check_entry_conditions_met() 方法
def check_entry_conditions_met(self) -> tuple[bool, str]:
"""检查当前市场状态是否满足开仓条件(纯信号判断,不检查持仓/冷却状态)
用于信号消退平仓:持仓期间定期检查,若条件不再满足则为平仓提供依据。
Returns:
(conditions_met, detail_msg)
"""
核心逻辑:
- 复用现有
_calculate_beta_series()和_calculate_positive_beta_series() - 计算三个条件的当前状态
- 返回
(True/False, "描述详情") - 若 beta 序列不足无法计算,返回
(True, "beta序列不足,保守持仓")— 保守策略,数据不足时不触发退出
为什么不直接用 get_signal_snapshot()?
get_signal_snapshot() 也能判断 signal_would_trigger,但它:
- 构造了一个完整的 dict snapshot,用于日志展示,数据冗余
- 调用方需要处理 key 可能不存在的情况
- 语义不清晰 — 它是"日志快照"不是"条件判断"
新增一个专用方法更清晰、更安全,且方法签名明确表达意图。
3. src/trading/risk_manager.py — 新增信号消退平仓检查
新增 check_signal_exit() 方法
def check_signal_exit(self, position: PairPosition, entry_conditions_met: bool) -> bool:
"""检查信号消退平仓 → True需要平仓
条件:PnL > signal_exit_pnl_pct 且开仓条件不再满足
"""
_, pnl_pct = self.calculate_pnl(position)
return (pnl_pct > self._config.beta_arb.signal_exit_pnl_pct
and not entry_conditions_met)
设计要点:
- 接收
entry_conditions_met参数而非直接依赖 strategy,保持 risk_manager 与 strategy 解耦 - 使用
>而非>=(严格大于阈值才触发) - 方法返回 bool,与现有的
check_stop_loss()、check_trailing_stop()风格一致
4. src/trading/orchestrator.py — 集成到监控循环
修改 _stop_loss_monitor() 方法
在现有的三个检查(止损 → 移动止盈 → 超时)之后,新增信号消退检查:
# 在 check_max_hold_duration 之后新增:
elif pnl_pct > 0:
# 仅当盈利时才检查信号消退(避免亏损时的无意义计算)
entry_met, detail = self._strategy.check_entry_conditions_met()
if self._risk_manager.check_signal_exit(pos, entry_met):
close_reason = (
f"📉🔔 信号消退平仓 | PnL={pnl_pct*100:+.2f}% > "
f"阈值{self._config.beta_arb.signal_exit_pnl_pct*100:.1f}% "
f"且开仓条件不满足 | {detail}"
)
检查优先级(保持现有顺序):
- 止损 — 最高优先级,亏损严重立即退出
- 移动止盈 — 高利润回撤退出
- 持仓超时 — 时间限制
- 信号消退 — 新增,盈利但信号消失时退出(最低优先级)
pnl_pct > 0 前置过滤:仅在盈利时才调用 check_entry_conditions_met(),避免每5秒一次的 beta 重算开销。这个优化是安全的,因为信号消退平仓本身就要求 PnL > 2%。
执行流程图
_stop_loss_monitor() 每5秒循环:
↓
update_position_prices()
↓
calculate_pnl() → pnl_pct
↓
check_stop_loss() → pnl_pct < -3%? → 平仓
↓ (未触发)
check_trailing_stop() → 峰值>=5% 且回撤? → 平仓
↓ (未触发)
check_max_hold_duration() → >72h? → 平仓
↓ (未触发)
pnl_pct > 0? → 否: 跳过
↓ (是)
strategy.check_entry_conditions_met() → (met, detail)
↓
check_signal_exit(pos, met) → PnL>2% && !met? → 平仓
不需要修改的文件
models.py— 无新数据结构position_manager.py— 平仓执行逻辑不变,close_position()已接受 reason 参数executor.py— 订单执行不变trade_repository.py— 已有通用平仓记录
配置默认值
| 参数 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
signal_exit_pnl_pct |
BETA_ARB_SIGNAL_EXIT_PNL_PCT |
0.02 |
信号消退平仓的PnL阈值,2% |
边界情况
- beta序列不足 —
check_entry_conditions_met()返回(True, ...)→ 不触发退出(保守) - PnL 恰好 = 2% — 不触发(使用严格
>) - 移动止盈已激活(PnL>=5%) — 移动止盈优先级更高,会先被检查并触发,信号消退不会被执行到
- 条件3(正收益beta)数据不足 — 与开仓时一致,条件3默认通过,即只看条件1和条件2
- K线缓冲区为空 — 返回
(True, ...)→ 不触发退出
验证
- 日志中应能看到
📉🔔 信号消退平仓触发时的详情 - PnL在0%~2%之间时,即使信号消退也不触发平仓
- PnL>2%但三个条件仍然满足时,不触发平仓
- PnL>2%且任一条件不满足时,触发平仓
- Lark通知中显示平仓原因为信号消退