Lab 03: Custom Indicators & StrategiesHands-on: build and test your own custom indicators
Objectives
- Learn how to inherit from the
Indicatorbase class. - Implement a Z-Score indicator (commonly used in mean-reversion models).
- Integrate custom indicators into a
Strategy. - Develop Bollinger Band-style mean-reversion execution logic based on the Z-Score.
Step 1: Implementing the Z-Score Indicator
The Z-score is defined as (current price - moving average) / standard deviation.
It measures how far price has moved away from its recent mean. Values above +2.0 or below -2.0 are often treated as overbought or oversold conditions.
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__() # Must call the parent class constructor
self.period = period
# Compose existing indicators
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. Update internal indicators
self.sma.update(price)
self.std.update(price)
# 2. Return immediately if internal indicators are not initialized
if not self.initialized:
return
# 3. Calculate Z-Score
# Prevent division by zero
if self.std.value == 0:
self._value = 0.0
else:
self._value = (price - self.sma.value) / self.std.value
# 4. Trigger update event (optional, if other components subscribe to this indicator)
self._set_value(self._value)This is a good example of composition. Instead of reimplementing the moving average and standard deviation yourself, you reuse the existing SMA and StdDev indicators. The result is cleaner, safer, and easier to maintain.
Step 2: Developing a Mean-Reversion Strategy
Strategy Logic:
- Z-Score > 2.0: Price is overextended on the upside, initiate a short position.
- Z-Score < -2.0: Price is overextended on the downside, initiate a long position.
- Z-Score reverts near 0 (e.g., -0.5 to 0.5): Close positions.
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)
# Instantiate our custom indicator
self.z_score = ZScore(config.period)
def on_bar(self, bar):
# Update the indicator
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}")
# Retrieve current position
pos = self.portfolio.position(self.config.instrument_id)
# --- Entry Logic ---
if not pos:
if z < -self.config.entry_z:
# Undervalued -> Go Long
self.buy()
elif z > self.config.entry_z:
# Overvalued -> Go Short
self.sell()
# --- Exit Logic ---
else:
if pos.is_long and z > -self.config.exit_z:
# Long position: Reverting near the moving average -> Close position
self.close_all_positions(self.config.instrument_id)
elif pos.is_short and z < self.config.exit_z:
# Short position: Reverting near the moving average -> Close position
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)Step 3: Backtest Validation
You can now deploy this strategy within the run_backtest.py script from Lab 01 to analyze the backtest performance of this mean-reversion strategy on the ETHUSDT instrument.
Exercises
1. Try modifying the strategy to include a momentum filter: Only emit long signals (Z < -2) when the RSI is concurrently in the oversold regime (< 30).
2. Deploy this strategy into the Lab 02 sandbox environment and observe its live execution performance.
3. Advanced Challenge: Implement a SuperTrend indicator (requires calculating the Average True Range, ATR).