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.
❌ 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
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")
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 Method | Activation Trigger | Standard Operational Routine |
|---|---|---|
on_start() | Strategy initialization | Subscribe to data, load historical indicator values. |
on_stop() | Strategy stopped | Cancel working orders, close out positions. |
on_bar(bar) | New Bar received | Update indicators, evaluate entry/exit logic. |
on_quote_tick(tick) | New Quote received | Monitor bid-ask spread. |
on_trade_tick(tick) | New Trade received | Observe trading volume and direction. |
on_order_filled(event) | Order filled | Log execution, adjust internal state. |
on_order_rejected(event) | Order rejected | Log error, attempt retry. |
on_order_canceled(event) | Order canceled | Clear working order records. |
on_position_opened(event) | New position opened | Set initial stop-loss. |
on_position_closed(event) | Position closed | Record realized PnL. |
on_event(event) | Any generic event | Generic event handling. |
4. Indicator System
NautilusTrader features a rich set of built-in technical indicators:
| Taxonomy | Indicator Primitives |
|---|---|
| Trend | EMA, SMA, WMA, DEMA, TEMA, KAMA, HMA |
| Momentum | RSI, MACD, Stochastic, CCI, ROC, Williams %R |
| Volatility | ATR, Bollinger Bands, Keltner Channel, Donchian |
| Volume | OBV, VWAP, MFI, AD |
| Statistical | Standard 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
pass4B. Actor vs Strategy — When to Use Which?
❌ 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.
| Capability | Actor Component | Strategy Component |
|---|---|---|
| Data Subscription | ✅ | ✅ |
| Order Submission | ❌ | ✅ |
| OrderFactory Access | ❌ | ✅ |
| Portfolio Access | ✅ (Read-Only) | ✅ |
| Typical Responsibility | Signal Generation, Data Processing, Monitoring | Trading 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 triggered5. 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
Configuration Separation
Isolate all tunable parameters into StrategyConfig rather than hardcoding them in the strategy. This facilitates parameter optimization during backtesting.
Wait for Indicator Initialization
Always verify indicator.initialized inside on_bar(); otherwise, the indicator values for the first few bars will be inaccurate.
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.
on_stop() Cleanup
On shutdown, cancel working orders and deliberately handle open positions. This prevents residual orders from executing without supervision.
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.
Use the OrderFactory
Strictly instantiate orders via self.order_factory. It automatically generates a unique ClientOrderId and correctly associates the StrategyId.
7. Interactive Exercises
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:
__init__: Create RSI indicator (period=14).on_start: Subscribe to bar data.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().
- Update RSI:
on_order_filled: Log the execution.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.
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 afloat.- ✅ Correct:
self.ema.update_raw(bar.close.as_double())(Note:as_doublerequires 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):
Determine whether you should use an Actor or a Strategy for the following scenarios:
- You want to continuously monitor the BTC/ETH spread and log an alert when it exceeds a threshold.
- You want to execute automated trades based on RSI signals.
- You want to scrape sentiment data from an external API and broadcast it into the system.
- You want to implement a statistical arbitrage strategy, placing orders on two instruments simultaneously.
- Actor ← Purely observation and logging; no execution required.
- Strategy ← Requires order submission.
- Actor ← Data processing and broadcasting; no trading involved.
- Strategy ← Requires submitting orders across multiple instruments.
Requires execution → Strategy
No execution required → Actor
8. Module Recap
- Strategy Base Class provides the framework hooks; you supply the trading logic.
- Lifecycle: init → on_start → Running (Callbacks) → on_stop.
- Event Callbacks: on_bar, on_quote_tick, on_order_filled, etc.
- Indicator System: 30+ built-in indicators, three-step workflow.
- Best Practices: Config separation, guard initializations, comprehensive logging, teardown cleanup.
- Examples: Start with
ema_cross.py, then compare it with the other reference strategies.