移动止损(Trailing Stop)设计方案
移动止损(Trailing Stop)设计方案
1. 概述
移动止损是风控体系中的止盈保护机制。当仓位盈利达到激活阈值后,系统开始追踪盈利峰值;当价格从峰值回撤超过回调阈值时,自动平仓锁定利润。
适用场景:短周期配对交易(持仓 < 30 分钟),捕获瞬间定价错误的套利机会。
2. 核心参数
| 参数 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
trailing_stop_activation_pct |
TRADING_TRAILING_STOP_ACTIVATION_PCT |
0.01 |
激活阈值,盈利达到 1% 后启用追踪 |
trailing_stop_callback_pct |
TRADING_TRAILING_STOP_CALLBACK_PCT |
0.003 |
回调阈值,从峰值回撤 0.3% 触发平仓 |
stop_loss_check_interval |
TRADING_STOP_LOSS_CHECK_INTERVAL |
3 |
监控轮询间隔(秒) |
注意:PnL 百分比基于名义价值(非保证金)。例如 1% 在 3x 杠杆下相当于保证金的 3%。
参数支持按交易对覆盖,通过 TradingConfig.get_strategy_params(symbol, base_symbol) 获取。
3. 判定算法
触发平仓 = (peak_pnl_pct >= activation_pct) AND (current_pnl_pct < peak_pnl_pct - callback_pct)
peak_pnl_pct:仓位生命周期内记录的最高盈利百分比(只升不降)current_pnl_pct:当前实时盈利百分比
两个条件缺一不可:
- 历史峰值盈利必须曾达到激活阈值(未达到则移动止损不生效,仅固定止损保护)
- 当前盈利必须从峰值回撤超过回调阈值
示例演示
配置:activation_pct=0.01, callback_pct=0.003
| 时间 | 当前盈利 | 峰值盈利 | 已激活 | 触发平仓 | 说明 |
|---|---|---|---|---|---|
| T1 | 0.5% | 0.5% | - | - | 峰值未达 1%,不激活 |
| T2 | 1.2% | 1.2% | Y | - | 达到激活阈值,开始追踪 |
| T3 | 2.0% | 2.0% | Y | - | 峰值更新,2.0% >= 2.0%-0.3% |
| T4 | 1.8% | 2.0% | Y | - | 小幅回撤,1.8% >= 1.7% |
| T5 | 1.6% | 2.0% | Y | 平仓 | 1.6% < 1.7%,触发 |
最终锁定约 1.6% 利润(名义价值维度)。
4. 架构与数据流
4.1 模块关系
orchestrator._stop_loss_monitor() # 监控线程(每 3s 轮询)
|
+-- position_manager.update_position_prices() # 从 WebSocket 缓存更新价格
|
+-- risk_manager.check_trailing_stop(pos) # 判定逻辑
|
+-- _calculate_pnl_pct(pos) # 计算当前 PnL%
+-- position.update_peak_pnl(pnl) # 更新峰值
+-- 比较 peak vs current # 判定是否触发
|
+-- orchestrator._close_with_retry(pos, reason) # 触发时市价平仓
4.2 涉及文件
| 文件 | 职责 |
|---|---|
src/trading/config.py |
参数定义、环境变量读取、按交易对覆盖 |
src/trading/models.py |
PairPosition.peak_pnl_pct 字段、update_peak_pnl() 线程安全更新 |
src/trading/risk_manager.py |
check_trailing_stop() 判定、_calculate_pnl_pct() PnL 计算 |
src/trading/orchestrator.py |
_stop_loss_monitor() 监控线程、平仓执行 |
src/trading/position_manager.py |
价格更新、peak_pnl_pct 初始化与持久化 |
src/trading/trade_repository.py |
update_position_runtime() DB 持久化 |
5. PnL 计算逻辑
5.1 Single 模式
# buy 方向
pnl_pct = (current_price - entry_price) / entry_price
# sell 方向
pnl_pct = (entry_price - current_price) / entry_price
5.2 Pair 模式(两腿加权平均)
alt_pnl_pct = ... # alt 腿 PnL%(同 single 逻辑)
base_pnl_pct = ... # base 腿 PnL%(同 single 逻辑)
alt_notional = alt_size * alt_entry_price
base_notional = base_size * base_entry_price
total_notional = alt_notional + base_notional
combined_pnl_pct = (alt_pnl_pct * alt_notional + base_pnl_pct * base_notional) / total_notional
按名义价值加权,避免两腿仓位大小不等时简单平均产生偏差。
6. 峰值追踪机制
6.1 运行时更新
PairPosition.update_peak_pnl() 使用 threading.Lock 保证线程安全,只允许峰值单调递增:
def update_peak_pnl(self, current_pnl_pct: float):
with self._pnl_lock:
if current_pnl_pct > self.peak_pnl_pct:
self.peak_pnl_pct = current_pnl_pct
6.2 持久化
- 写入 DB:仓位同步时(
position_sync_interval周期),若peak_pnl_pct > 0则调用trade_repository.update_position_runtime()写入pair_positions表 - 从 DB 恢复:服务重启时从
pair_positions.peak_pnl_pct字段恢复,不会丢失历史峰值
6.3 孤儿仓位初始化
服务重启发现交易所有仓位但 DB 无记录时,按以下方式初始化 peak_pnl_pct:
- Pair 模式:
(alt_unrealizedPnl + base_unrealizedPnl) / (alt_notional + base_notional),与_calculate_pnl_pct()两腿加权口径一致 - Single 模式:
unrealizedPnl / notional
7. 监控线程执行流程
orchestrator._stop_loss_monitor() 每 3 秒执行一轮:
1. KillSwitch 检查 → 激活则平仓所有仓位并跳过后续
2. 更新所有仓位当前价格(WebSocket L2Book 缓存,<10ms)
3. 遍历所有 OPEN 仓位,依次检查:
a. 固定止损(check_stop_loss)
b. 移动止损(check_trailing_stop)
c. 持仓超时(check_max_hold_duration)
4. 收集所有需平仓的仓位和原因
5. 逐个执行平仓(带重试机制)
- 止损/移动止损 → 市价单(紧急退场)
- 持仓超时 → 限价单(遵循 close_order_type 配置)
检查顺序有优先级:固定止损 > 移动止损 > 持仓超时。命中前者后不再检查后者(elif 链)。
8. 与其他退场机制的关系
| 退场机制 | 触发条件 | 优先级 | 订单类型 |
|---|---|---|---|
| KillSwitch | 外部文件信号 | 最高 | 市价 |
| 固定止损 | pnl < -stop_loss_pct |
高 | 市价 |
| 移动止损 | 峰值激活 + 回撤超阈值 | 中 | 市价 |
| 均值回归退场 | z-score 回归到阈值 | 中 | 限价/市价 |
| 持仓超时 | 持仓时间 > max_hold_hours |
低 | 限价 |
移动止损与固定止损互斥:亏损时固定止损保护,盈利时移动止损锁利。
9. 配置示例
# .env
TRADING_TRAILING_STOP_ACTIVATION_PCT=0.01 # 盈利 1% 后激活
TRADING_TRAILING_STOP_CALLBACK_PCT=0.003 # 从峰值回撤 0.3% 触发
TRADING_STOP_LOSS_CHECK_INTERVAL=3 # 3 秒轮询