Futuristic abstract digital render depicting geometric shape

止损是量化策略里最容易被忽视、也最容易出问题的模块。很多新手策略回测看起来不错,实盘一跑就崩,往往根源在止损设计有问题:要么止损太宽导致单次亏损过大,要么止损太紧被市场噪音频繁触发。

这篇文章从 北极星策略 的实际设计出发,讲通用的止损设计思路。

为什么需要止损

止损本质上是承认错误的机制。趋势跟踪策略的逻辑是:大多数时候市场没有趋势,策略会小亏;少数时候有趋势,策略要大赚,用大赚覆盖小亏。

如果没有止损,那次"大亏"有可能把账户打穿,后面的大赚机会都没了。

数学上,止损控制的是单次最大亏损,进而控制最大回撤:

最大回撤 ≈ 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 品牌风格