
止损是量化策略里最容易被忽视、也最容易出问题的模块。很多新手策略回测看起来不错,实盘一跑就崩,往往根源在止损设计有问题:要么止损太宽导致单次亏损过大,要么止损太紧被市场噪音频繁触发。
这篇文章从 北极星策略 的实际设计出发,讲通用的止损设计思路。
为什么需要止损
止损本质上是承认错误的机制。趋势跟踪策略的逻辑是:大多数时候市场没有趋势,策略会小亏;少数时候有趋势,策略要大赚,用大赚覆盖小亏。
如果没有止损,那次"大亏"有可能把账户打穿,后面的大赚机会都没了。
数学上,止损控制的是单次最大亏损,进而控制最大回撤:
最大回撤 ≈ f(止损比例, 连续亏损次数, 胜率)
策略在某段时间内 5 连败是很正常的,如果每次亏 10%,5 连败后账户还剩 59%;如果每次亏 3%,5 连败后还剩 85%,差距非常大。
止损类型
1. 固定百分比止损
最简单的方式:入场后亏损超过固定比例就出场。
# 入场价 40000,止损 3%
entry_price = 40000
stop_loss_pct = 0.03
stop_price_long = entry_price * (1 - stop_loss_pct) # 做多止损:38800
stop_price_short = entry_price * (1 + stop_loss_pct) # 做空止损:41200
优点: 简单、可计算、风险固定 缺点: 忽略了市场的波动率,BTC 平时日内波动可能就有 3%,容易被噪音触发
适用场景: 高时间周期(日线以上),或者已经验证止损比例远大于平均波动
2. ATR 动态止损(推荐)
ATR(Average True Range,平均真实波幅)衡量的是市场最近的波动率。用 ATR 倍数设止损,波动大的时候止损宽,波动小的时候止损窄,更贴近市场实际。
import pandas as pd
def calculate_atr(df, period=14):
"""计算 ATR"""
high = df['high']
low = df['low']
close = df['close']
tr1 = high - low
tr2 = abs(high - close.shift(1))
tr3 = abs(low - close.shift(1))
true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
atr = true_range.rolling(period).mean()
return atr
def get_stop_price(entry_price, atr_value, direction, multiplier=2.0):
"""基于 ATR 的止损价"""
if direction == 'long':
return entry_price - atr_value * multiplier
else:
return entry_price + atr_value * multiplier
# 示例:BTC 1H,ATR=1200,入场 40000,做多
# 止损 = 40000 - 1200 * 2 = 37600(-6%)
ATR 倍数选择:
- 1.0 - 1.5:较紧,信噪比低的市场容易被触发
- 2.0 - 3.0:适中,大多数趋势策略的合理范围
- 4.0+:宽松,适合捕捉大趋势但单次亏损大
北极星策略在 1H 时间框架上,经过回测验证,固定百分比止损比 ATR 止损更稳定。原因是 BTC 的 ATR 在趋势行情里会快速扩大,导致止损越来越宽。
3. 追踪止损(Trailing Stop Loss)
入场后止损跟随价格移动,锁定浮盈,不让盈利全部回吐。
class TrailingStop:
def __init__(self, entry_price, direction, trail_pct=0.05):
self.direction = direction
self.trail_pct = trail_pct
if direction == 'long':
self.best_price = entry_price
self.stop_price = entry_price * (1 - trail_pct)
else:
self.best_price = entry_price
self.stop_price = entry_price * (1 + trail_pct)
def update(self, current_price):
"""每根 K 线更新一次"""
if self.direction == 'long':
if current_price > self.best_price:
self.best_price = current_price
self.stop_price = current_price * (1 - self.trail_pct)
else:
if current_price < self.best_price:
self.best_price = current_price
self.stop_price = current_price * (1 + self.trail_pct)
return self.stop_price
def is_triggered(self, current_price):
if self.direction == 'long':
return current_price <= self.stop_price
else:
return current_price >= self.stop_price
追踪止损适合的场景:
- 已经有一定浮盈,不想全部亏回去
- 趋势行情,想持仓跑行情同时保护利润
不适合的场景:
- 震荡行情,价格来回波动,追踪止损频繁触发又频繁进场
4. 时间止损
持仓超过一定时间没有达到预期方向就出场,无论是否亏损:
def check_time_stop(entry_time, current_time, max_bars=48):
"""持仓超过 48 根 K 线(1H = 2天)强制出场"""
bars_held = (current_time - entry_time).total_seconds() / 3600
return bars_held >= max_bars
时间止损的逻辑:如果入场后行情迟迟不启动,说明判断可能有误,解放资金去找更好的机会。
北极星策略的止损设计
不展开具体参数,但讲一下设计思路:
核心原则:固定止损比例 + 冷却期保护
用固定百分比止损而不是 ATR,原因是:
- 参数更稳定,不随市场波动率变化
- 回测中信号更清晰
- 配合分批止盈,整体期望值经过优化
止损触发后有冷却期,这段时间内不允许再开新仓。冷却期的作用:
- 避免在震荡行情里反复被打止损
- 让市场方向更清晰后再介入
- 减少手续费损耗
止盈和止损的配合:
好的策略,止盈设计和止损一样重要。北极星策略采用分批止盈:
- 第一笔在较小盈利时止盈,降低风险敞口
- 持有剩余仓位跟随趋势
- 最终全部止盈或被整体止损带走
这样的设计保证了:即使后续行情反转,前几笔止盈已经锁定了部分利润。
止损参数如何确定
回测是唯一可靠的方法,但要注意避坑:
1. 在样本外数据验证
用 2020-2023 年数据优化参数,然后用 2024-2025 年数据验证,不能用同一段数据。
2. 关注最大回撤而不是胜率
胜率高不代表好策略,最大回撤控制在多少以内才是关键:
- 20% 以内:比较舒适,心理承受范围内
- 30% 以上:大多数人在这里扛不住会手动干预
3. 别过拟合
止损如果精确到小数点后两位(比如 3.17%),大概率是过拟合。通用参数用整数或半整数(1%、2%、2.5%)即可。
4. 用蒙特卡洛模拟
随机打乱交易顺序,模拟 1000 种可能的序列,看最大回撤分布:
import numpy as np
def monte_carlo_max_drawdown(returns, n_simulations=1000):
"""蒙特卡洛模拟最大回撤"""
max_drawdowns = []
for _ in range(n_simulations):
shuffled = np.random.permutation(returns)
cumulative = np.cumprod(1 + shuffled)
peak = np.maximum.accumulate(cumulative)
drawdown = (peak - cumulative) / peak
max_drawdowns.append(drawdown.max())
return {
'median': np.median(max_drawdowns),
'p95': np.percentile(max_drawdowns, 95),
'worst': max(max_drawdowns)
}
如果 p95 的最大回撤在你承受范围内,这套止损参数才算稳健。
实盘中的止损执行
设计好止损只是第一步,实盘执行也有坑:
1. 用限价单还是市价单
- 市价单:确保成交,但滑点大(BTC 波动剧烈时可能滑点 0.1-0.5%)
- 限价单:控制成交价,但可能没成交(价格跳过止损价)
极端行情建议用市价单,宁可付滑点也要确保执行。
2. 止损单要放到交易所
不要只在程序里判断,要把止损单挂到交易所:
# Binance 创建止损单
order = client.create_order(
symbol='BTCUSDT',
side='SELL',
type='STOP_MARKET',
stopPrice=stop_price,
closePosition=True, # 全仓平仓
timeInForce='GTC'
)
即使策略程序宕机,交易所里的止损单仍然有效。
3. 定期验证止损单是否存在
策略程序启动时检查挂单:
def verify_stop_loss(client, symbol):
"""验证止损单是否存在"""
open_orders = client.get_open_orders(symbol=symbol)
stop_orders = [o for o in open_orders if o['type'] == 'STOP_MARKET']
if not stop_orders:
print("⚠️ 警告:止损单不存在!")
# 重新挂单逻辑
延伸阅读:
📷 封面图 · Flux Schnell AI 生成 · panshi.io 品牌风格