Phase 3: Practical Application

Module 11: Strategy DevelopmentBuild your first trading strategy from the ground up

Strategy development in NautilusTrader follows a clear pattern: inherit from a base class → implement the event callbacks you need → add your trading logic. This module walks through that workflow step by step.

💡 Why Use the Strategy Base Class Instead of Building From Scratch?

❌ Building trading logic from scratch: You would need to manually handle data subscriptions, order management, position tracking, logging, and risk integration. It takes at least 2,000 lines of boilerplate code to replicate the functionality that the Strategy base class provides out of the box.

✅ Using the Strategy base class: You only need to focus on “when to buy and when to sell.” The rest of the infrastructure (Cache, Message Bus, Order Factory, Portfolio queries) is automatically injected.

1. The Strategy Base Class — Your Strategy Framework

Strategies inherit from the Strategy base class. It provides the framework hooks, state access, and order helpers; you supply the trading logic:

flowchart LR
                        subgraph lifecycle["🔄 Strategy Lifecycle"]
                        direction TB
                        init["__init__<br/>Initialize Config & Indicators"]
                        start["on_start()<br/>Subscribe Data & Init Logic"]
                        running["Running State"]
                        stop["on_stop()<br/>Cleanup Resources & Close Positions"]
                        init --> start --> running --> stop
                        end

                        subgraph callbacks["📡 Event Callbacks (Runtime)"]
                        direction TB
                        on_bar["on_bar(bar)"]
                        on_quote["on_quote_tick(tick)"]
                        on_trade["on_trade_tick(tick)"]
                        on_filled["on_order_filled(event)"]
                        end

                        running ==> callbacks

                        style lifecycle fill:#0f1419,stroke:#4fc3f7,stroke-width:1px
                        style callbacks fill:#0f1419,stroke:#ab47bc,stroke-width:1px
Figure 1: Strategy Lifecycle and Event Callbacks

2. Strategy Template — Minimal Viable Strategy

from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.config import StrategyConfig
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.data import Bar, BarType


class MyStrategyConfig(StrategyConfig, frozen=True):
    """Strategy Configuration — All tunable parameters go here"""
    instrument_id: InstrumentId
    bar_type: BarType
    fast_ema_period: int = 10
    slow_ema_period: int = 20


class MyStrategy(Strategy):
    """Your Strategy — Inject trading logic here"""

    def __init__(self, config: MyStrategyConfig):
        super().__init__(config)
        # Store configurations
        self.instrument_id = config.instrument_id
        self.bar_type = config.bar_type

        # Initialize Technical Indicators
        self.fast_ema = ExponentialMovingAverage(config.fast_ema_period)
        self.slow_ema = ExponentialMovingAverage(config.slow_ema_period)

    def on_start(self):
        """Invoked when the strategy starts — Subscribe to data"""
        # Subscribe to bar data
        self.subscribe_bars(self.bar_type)
        # Log entry
        self.log.info("Strategy started")

    def on_bar(self, bar: Bar):
        """Invoked on completed bar close — Core Execution Logic"""
        # Update tactical indicators
        self.fast_ema.update_raw(bar.close.as_double())
        self.slow_ema.update_raw(bar.close.as_double())

        # Logic Guard: Wait for indicator initialization
        if not self.fast_ema.initialized:
            return

        # ===== Algorithmic Signal Logic =====
        # Fast EMA crosses above Slow EMA → Bullish Entry
        if self.fast_ema.value > self.slow_ema.value:
            if self.portfolio.is_flat(self.instrument_id):
                self._buy()

        # Fast EMA crosses below Slow EMA → Bearish Exit
        elif self.fast_ema.value < self.slow_ema.value:
            if self.portfolio.is_net_long(self.instrument_id):
                self._sell()

    def _buy(self):
        """Construct Long Position"""
        order = self.order_factory.market(
            instrument_id=self.instrument_id,
            order_side=OrderSide.BUY,
            quantity=Quantity.from_str("0.01"),
        )
        self.submit_order(order)

    def _sell(self):
        """Liquidate Long Position"""
        order = self.order_factory.market(
            instrument_id=self.instrument_id,
            order_side=OrderSide.SELL,
            quantity=Quantity.from_str("0.01"),
        )
        self.submit_order(order)

    def on_order_filled(self, event):
        """Execution Callback"""
        self.log.info(f"Execution Event Received: {event}")

    def on_stop(self):
        """Clean-up Routine — Executed at teardown"""
        self.cancel_all_orders(self.instrument_id)
        self.close_all_positions(self.instrument_id)
        self.log.info("Strategy Gracefully Terminated")
✅ This is a working strategy

In less than 80 lines of code, you have built a working EMA crossover strategy with configuration, indicators, execution logic, and platform-level risk checks via the RiskEngine. The same strategy class can be carried into backtest and live workflows with the right configuration.

3. Core Event Callbacks Overview

Callback MethodActivation TriggerStandard Operational Routine
on_start()Strategy initializationSubscribe to data, load historical indicator values.
on_stop()Strategy stoppedCancel working orders, close out positions.
on_bar(bar)New Bar receivedUpdate indicators, evaluate entry/exit logic.
on_quote_tick(tick)New Quote receivedMonitor bid-ask spread.
on_trade_tick(tick)New Trade receivedObserve trading volume and direction.
on_order_filled(event)Order filledLog execution, adjust internal state.
on_order_rejected(event)Order rejectedLog error, attempt retry.
on_order_canceled(event)Order canceledClear working order records.
on_position_opened(event)New position openedSet initial stop-loss.
on_position_closed(event)Position closedRecord realized PnL.
on_event(event)Any generic eventGeneric event handling.

4. Indicator System

NautilusTrader features a rich set of built-in technical indicators:

TaxonomyIndicator Primitives
TrendEMA, SMA, WMA, DEMA, TEMA, KAMA, HMA
MomentumRSI, MACD, Stochastic, CCI, ROC, Williams %R
VolatilityATR, Bollinger Bands, Keltner Channel, Donchian
VolumeOBV, VWAP, MFI, AD
StatisticalStandard Deviation, Variance, Linear Regression
# The Three-Step Indicator Workflow
# 1. Instantiate in __init__
self.rsi = RelativeStrengthIndex(period=14)

# 2. Update in on_bar
self.rsi.update_raw(bar.close.as_double())

# 3. Read the value
if self.rsi.initialized and self.rsi.value > 70:
    # RSI Overbought → Evaluate Bearish Signal
    pass

4B. Actor vs Strategy — When to Use Which?

💡 Why do we need Actors? Isn’t Strategy enough?

❌ Cramming all logic into a Strategy: Your strategy becomes a “God Object,” handling both data processing and trading decisions. It becomes difficult to test and reuse.

✅ Using Actor + Strategy: Actors handle “observation and computation” (e.g., signal generation), while Strategies handle “execution.” They communicate through the MessageBus, keeping signal generation and execution responsibilities separated.

CapabilityActor ComponentStrategy Component
Data Subscription✅✅
Order Submission❌✅
OrderFactory Access❌✅
Portfolio Access✅ (Read-Only)✅
Typical ResponsibilitySignal Generation, Data Processing, MonitoringTrading Logic, Order Management
"""Actor -> Strategy Collaboration Example: Signal Generator + Trade Executor"""
from nautilus_trader.common.actor import Actor
from nautilus_trader.common.signal import Signal

class SignalGenerator(Actor):
    """Actor: Responsible for signal generation only; zero execution authority"""
    def on_bar(self, bar: Bar):
        rsi = self._calculate_rsi(bar)
        if rsi < 30:
            # Broadcast "BUY" recommendation via the MessageBus
            self.publish_signal(name="BUY", value=rsi)
        elif rsi > 70:
            self.publish_signal(name="SELL", value=rsi)

class TradeExecutor(Strategy):
    """Strategy: Consumes signals and orchestrates market execution"""
    def on_start(self):
        self.subscribe_signal(name="BUY")
        self.subscribe_signal(name="SELL")

    def on_signal(self, signal: Signal):
        if signal.name == "BUY" and self.portfolio.is_flat(self.instrument_id):
            self._buy()
        elif signal.name == "SELL" and self.portfolio.is_net_long(self.instrument_id):
            self._sell()

4C. OrderFactory — Advanced Order Patterns

self.order_factory is your exclusive tool for order submission within a strategy. It supports 9 distinct order types. Here are the most practical patterns:

Pattern 1: Bracket Orders (Entry + Stop-Loss + Take-Profit)

"""Submit three orders atomically: Market Entry + Stop-Loss + Take-Profit"""
from nautilus_trader.model.orders import OrderList

def _open_position_with_bracket(self):
    # OrderList bundles the three orders together
    bracket = self.order_factory.bracket(
        instrument_id=self.instrument_id,
        order_side=OrderSide.BUY,
        quantity=Quantity.from_str("0.01"),
        # Market order for entry
        entry_trigger_price=None,
        # Stop-loss set 2% below entry
        sl_trigger_price=Price.from_str("49000.00"),
        # Take-profit set 4% above entry
        tp_price=Price.from_str("52000.00"),
    )
    # Submit the entire bracket order list atomically
    self.submit_order_list(bracket)
    # Once the entry is filled, the SL and TP are automatically armed (OTO)
    # When either SL or TP is triggered, the other is automatically canceled (OCO)

Pattern 2: Trailing Stop

"""Dynamically calculate trailing stop distance using ATR"""
def _set_trailing_stop(self):
    atr_value = self.atr.value  # Current ATR volatility reading

    order = self.order_factory.trailing_stop_market(
        instrument_id=self.instrument_id,
        order_side=OrderSide.SELL,  # Protective Sell
        quantity=Quantity.from_str("0.01"),
        # Trailing offset = 2x ATR
        trailing_offset=Decimal(str(atr_value * 2)),
        trailing_offset_type=TrailingOffsetType.PRICE,
        trigger_type=TriggerType.LAST_TRADE,
    )
    self.submit_order(order)
    # As price rises, the stop price ratchets upward
    # If price drops by more than 2x ATR, the stop-loss is triggered

5. Decoding Official Strategy Examples

NautilusTrader ships with multiple example strategies across common trading styles. These five are the most useful starting points for learning the framework:

📐

ema_cross.py

EMA Crossover Strategy
The most fundamental trend-following strategy. Go long when fast EMA crosses above slow EMA, and short otherwise. The best starting point for learning the core strategy structure.

📊

ema_cross_bracket.py

EMA Crossover + Bracket Orders
Extends the EMA crossover with Stop-Loss and Take-Profit logic (Bracket Order), demonstrating the use of conditional order combinations.

📈

ema_cross_trailing_stop.py

Trailing Stop Strategy
Dynamically calculates the stop-loss distance using the ATR indicator, demonstrating how to lock in profits using a trailing stop.

📖

orderbook_imbalance.py

Order Book Imbalance Strategy
Executes trades based on the imbalance of buying and selling pressure in the order book. Demonstrates how to handle Level 2 market data.

🎯

market_maker.py

Market Maker Strategy
Quotes on both sides of the book to capture the bid-ask spread. Demonstrates the foundational logic of high-frequency trading and inventory management.

6. Strategy Development Best Practices

1️⃣

Configuration Separation

Isolate all tunable parameters into StrategyConfig rather than hardcoding them in the strategy. This facilitates parameter optimization during backtesting.

2️⃣

Wait for Indicator Initialization

Always verify indicator.initialized inside on_bar(); otherwise, the indicator values for the first few bars will be inaccurate.

3️⃣

Comprehensive Logging

Extensively use self.log.info() to record critical decision nodes. This allows you to audit the reasoning behind every execution during post-backtest analysis.

4️⃣

on_stop() Cleanup

On shutdown, cancel working orders and deliberately handle open positions. This prevents residual orders from executing without supervision.

5️⃣

Check Position State Before Execution

Always verify your position state using self.portfolio.is_flat() before submitting an order to avoid unintended pyramiding or double-leveraging.

6️⃣

Use the OrderFactory

Strictly instantiate orders via self.order_factory. It automatically generates a unique ClientOrderId and correctly associates the StrategyId.

7. Interactive Exercises

🧠 Exercise 1: Architect an RSI Strategy

Design an RSI-based strategy: Sell when RSI > 70 (Overbought), Buy when RSI < 30 (Oversold). Outline the required callback methods and logical workflow.

Required Architectural Workflow:

  1. __init__: Create RSI indicator (period=14).
  2. on_start: Subscribe to bar data.
  3. on_bar:
    • Update RSI: self.rsi.update_raw(bar.close.as_double()).
    • Check initialization: if not self.rsi.initialized: return.
    • RSI < 30 AND flat → self._buy().
    • RSI > 70 AND long → self._sell().
  4. on_order_filled: Log the execution.
  5. on_stop: Cancel all orders, close positions.

💡 Bonus Points:

  • Integrate ATR to calculate dynamic stop-loss distance.
  • Wait for RSI to cross back above 30 (confirmation) before buying.
  • Enforce a maximum position limit.
🧠 Exercise 2: Identify the Syntax Bugs

The following strategy snippet contains 3 critical errors. Can you spot them?

class BuggyStrategy(Strategy):
    def __init__(self, config):
        self.ema = ExponentialMovingAverage(20)  # 🤔 Error 1

    def on_start(self):
        self.subscribe_bars(self.config.bar_type)

    def on_bar(self, bar: Bar):
        self.ema.update_raw(bar.close)            # 🤔 Error 2
        if self.ema.value > bar.close.as_double():
            self._buy()

    def _buy(self):
        order = self.order_factory.market(
            instrument_id=self.instrument_id,
            order_side=OrderSide.BUY,
            quantity=Quantity.from_str("0.01"),
        )
        self.submit_order(order)                  # 🤔 Error 3 (Logic Error)

❌ Error 1: Missing `super().__init__(config)`

  • Without initializing the parent class, references like self.cache, self.log, etc., are entirely unavailable.
  • ✅ Correct: Add super().__init__(config) as the first line.

❌ Error 2: `bar.close` is a `Price` object, not a `float`

  • update_raw() requires a float.
  • ✅ Correct: self.ema.update_raw(bar.close.as_double()) (Note: as_double requires parentheses to be called).

❌ Error 3: Ordering without checking position state

  • A market buy order will fire on every bar, accumulating an exponential position size.
  • ✅ Correct: Implement a state check first: if self.portfolio.is_flat(self.instrument_id):
🧠 Exercise 3: Actor or Strategy?

Determine whether you should use an Actor or a Strategy for the following scenarios:

  1. You want to continuously monitor the BTC/ETH spread and log an alert when it exceeds a threshold.
  2. You want to execute automated trades based on RSI signals.
  3. You want to scrape sentiment data from an external API and broadcast it into the system.
  4. You want to implement a statistical arbitrage strategy, placing orders on two instruments simultaneously.
  1. Actor ← Purely observation and logging; no execution required.
  2. Strategy ← Requires order submission.
  3. Actor ← Data processing and broadcasting; no trading involved.
  4. Strategy ← Requires submitting orders across multiple instruments.

💡 The heuristic is simple:

Requires execution → Strategy

No execution required → Actor

8. Module Recap

📋 Architectural Constants to Internalize
  1. Strategy Base Class provides the framework hooks; you supply the trading logic.
  2. Lifecycle: init → on_start → Running (Callbacks) → on_stop.
  3. Event Callbacks: on_bar, on_quote_tick, on_order_filled, etc.
  4. Indicator System: 30+ built-in indicators, three-step workflow.
  5. Best Practices: Config separation, guard initializations, comprehensive logging, teardown cleanup.
  6. Examples: Start with ema_cross.py, then compare it with the other reference strategies.