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 品牌風格