第五階段:動手 Lab

Lab 03:自定義指標與策略動手實作:打造專屬於你的獨家指標

目標

  1. 學習如何繼承 Indicator 基類
  2. 實現一個 Z-Score 指標(均值回歸常用)
  3. 將指標整合進 Strategy
  4. 編寫基於 Z-Score 的 Bolinger Band 策略邏輯

步驟 1:實現 Z-Score 指標

Z-Score = (當前價格 – 平均值) / 標準差。
它反映價格偏離均值的程度,通常超過 +2 或低於 -2 視為超買/超賣。

from nautilus_trader.indicators.base.indicator import Indicator
from nautilus_trader.indicators.average.sma import SimpleMovingAverage
from nautilus_trader.indicators.volatility.std_dev import StandardDeviation

class ZScore(Indicator):
    def __init__(self, period: int):
        super().__init__()  # 必須調用父類構造函數
        self.period = period
        
        # 組合現有指標
        self.sma = SimpleMovingAverage(period)
        self.std = StandardDeviation(period)
        
        self._value = 0.0

    @property
    def value(self) -> float:
        return self._value

    @property
    def initialized(self) -> bool:
        return self.sma.initialized and self.std.initialized

    def update(self, price: float):
        # 1. 更新內部指標
        self.sma.update(price)
        self.std.update(price)

        # 2. 如果內部指標未初始化,直接返回
        if not self.initialized:
            return

        # 3. 計算 Z-Score
        # 防止除以零
        if self.std.value == 0:
            self._value = 0.0
        else:
            self._value = (price - self.sma.value) / self.std.value
            
        # 4. 觸發更新事件(可選,如果其他組件訂閱了此指標)
        self._set_value(self._value)
💡 組合 vs 繼承

這裡是組合 (Composition) 的最佳範例。我們沒有從零計算均值和標準差,而是直接使用了現有的 SMA 和 StdDev 指標。這樣代碼更簡潔、更不容易出錯。

步驟 2:編寫 Mean Reversion 策略

策略邏輯:

  • Z-Score > 2.0:價格過高,做空
  • Z-Score < -2.0:價格過低,做多
  • Z-Score 回歸到 0 附近(如 -0.5 ~ 0.5):平倉
from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.config import StrategyConfig
from nautilus_trader.model.enums import OrderSide

class MeanReversionConfig(StrategyConfig):
    instrument_id: str
    period: int = 20
    entry_z: float = 2.0
    exit_z: float = 0.5
    trade_size: str = "0.01"

class MeanReversionStrategy(Strategy):
    def __init__(self, config: MeanReversionConfig):
        super().__init__(config)
        
        # 實例化我們的自定義指標
        self.z_score = ZScore(config.period)

    def on_bar(self, bar):
        # 更新指標
        self.z_score.update(bar.close)

        if not self.z_score.initialized:
            return
            
        z = self.z_score.value
        self.log.info(f"Z-Score: {z:.2f}")

        # 獲取持倉
        pos = self.portfolio.position(self.config.instrument_id)
        
        # --- 進場邏輯 ---
        if not pos:
            if z < -self.config.entry_z:
                # 價格過低 -> 買入
                self.buy()
            elif z > self.config.entry_z:
                # 價格過高 -> 賣出
                self.sell()
                
        # --- 出場邏輯 ---
        else:
            if pos.is_long and z > -self.config.exit_z:
                # 多單:回歸到均線下方附近 -> 平倉
                self.close_all_positions(self.config.instrument_id)
            elif pos.is_short and z < self.config.exit_z:
                # 空單:回歸到均線上方附近 -> 平倉
                self.close_all_positions(self.config.instrument_id)

    def buy(self):
        order = self.order_factory.market(
            instrument_id=self.config.instrument_id,
            order_side=OrderSide.BUY,
            quantity=self.config.trade_size,
        )
        self.submit_order(order)

    def sell(self):
        order = self.order_factory.market(
            instrument_id=self.config.instrument_id,
            order_side=OrderSide.SELL,
            quantity=self.config.trade_size,
        )
        self.submit_order(order)

步驟 3:回測驗證

現在你可以把這個策略放到 Lab 01 的 run_backtest.py 中運行,看看均值回歸策略在 ETHUSDT 上的表現如何。

作業

📝 挑戰任務

1. 嘗試修改 ZCheck 指標,加入「動量過濾」:只有當 RSI 同時處於超賣區(<30)時,才發出做多信號(Z < -2)。

2. 嘗試將此策略部署到 Lab 02 的沙箱環境中,觀察實時運行的效果。

3. 進階挑戰:實現一個 SuperTrend 指標(需要計算 ATR)。