Strategies
The heart of the NautilusTrader user experience is in writing and working with
trading strategies. Defining a trading strategy is achieved by inheriting the Strategy
class,
and implementing the methods required by the users trading strategy logic.
Strategies can be added to Nautilus systems with any environment context and will start sending commands and receiving events based on their logic as soon as the system starts.
Using the basic building blocks of data ingest, event handling, and order management (which we will discuss below), it's possible to implement any type of trading strategy including directional, momentum, re-balancing, pairs, market making etc.
See the Strategy
API Reference for a complete description
of all available methods.
There are two main parts of a Nautilus trading strategy:
- The strategy implementation itself, defined by inheriting the
Strategy
class - The optional strategy configuration, defined by inheriting the
StrategyConfig
class
Once a strategy is defined, the same source code can be used for backtesting and live trading.
The main capabilities of a strategy include:
- Historical data requests
- Live data feed subscriptions
- Setting time alerts or timers
- Cache access
- Portfolio access
- Creating and managing orders and positions
Strategy implementation
Since a trading strategy is a class which inherits from Strategy
, you must define
a constructor where you can handle initialization. Minimally the base/super class needs to be initialized:
from nautilus_trader.trading.strategy import Strategy
class MyStrategy(Strategy):
def __init__(self) -> None:
super().__init__() # <-- the super class must be called to initialize the strategy
From here, you can implement handlers as necessary to perform actions based on state transitions and events.
Do not call components such as clock
and logger
in the __init__
constructor (which is prior to registration).
This is because the systems clock and logging system have not yet been initialized.
Handlers
Handlers are methods within the Strategy
class which may perform actions based on different types of events or on state changes.
These methods are named with the prefix on_*
. You can choose to implement any or all of these handler
methods depending on the specific goals and needs of your strategy.
The purpose of having multiple handlers for similar types of events is to provide flexibility in handling granularity. This means that you can choose to respond to specific events with a dedicated handler, or use a more generic handler to react to a range of related events (using typical switch statement logic). The handlers are called in sequence from the most specific to the most general.
Stateful actions
These handlers are triggered by lifecycle state changes of the Strategy
. It's recommended to:
- Use the
on_start
method to initialize your strategy (e.g., fetch instruments, subscribe to data) - Use the
on_stop
method for cleanup tasks (e.g., cancel open orders, close open positions, unsubscribe from data)
def on_start(self) -> None:
def on_stop(self) -> None:
def on_resume(self) -> None:
def on_reset(self) -> None:
def on_dispose(self) -> None:
def on_degrade(self) -> None:
def on_fault(self) -> None:
def on_save(self) -> dict[str, bytes]: # Returns user-defined dictionary of state to be saved
def on_load(self, state: dict[str, bytes]) -> None:
Data handling
These handlers receive data updates, including built-in market data and custom user-defined data. You can use these handlers to define actions upon receiving data object instances.
from nautilus_trader.core.data import Data
from nautilus_trader.model.book import OrderBook
from nautilus_trader.model.data import Bar
from nautilus_trader.model.data import QuoteTick
from nautilus_trader.model.data import TradeTick
from nautilus_trader.model.data import OrderBookDeltas
from nautilus_trader.model.data import InstrumentClose
from nautilus_trader.model.data import InstrumentStatus
from nautilus_trader.model.instruments import Instrument
def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None:
def on_order_book(self, order_book: OrderBook) -> None:
def on_quote_tick(self, tick: QuoteTick) -> None:
def on_trade_tick(self, tick: TradeTick) -> None:
def on_bar(self, bar: Bar) -> None:
def on_instrument(self, instrument: Instrument) -> None:
def on_instrument_status(self, data: InstrumentStatus) -> None:
def on_instrument_close(self, data: InstrumentClose) -> None:
def on_historical_data(self, data: Data) -> None:
def on_data(self, data: Data) -> None: # Custom data passed to this handler
def on_signal(self, signal: Data) -> None: # Custom signals passed to this handler
Order management
These handlers receive events related to orders.
OrderEvent
type messages are passed to handlers in the following sequence:
- Specific handler (e.g.,
on_order_accepted
,on_order_rejected
, etc.) on_order_event(...)
on_event(...)
from nautilus_trader.model.events import OrderAccepted
from nautilus_trader.model.events import OrderCanceled
from nautilus_trader.model.events import OrderCancelRejected
from nautilus_trader.model.events import OrderDenied
from nautilus_trader.model.events import OrderEmulated
from nautilus_trader.model.events import OrderEvent
from nautilus_trader.model.events import OrderExpired
from nautilus_trader.model.events import OrderFilled
from nautilus_trader.model.events import OrderInitialized
from nautilus_trader.model.events import OrderModifyRejected
from nautilus_trader.model.events import OrderPendingCancel
from nautilus_trader.model.events import OrderPendingUpdate
from nautilus_trader.model.events import OrderRejected
from nautilus_trader.model.events import OrderReleased
from nautilus_trader.model.events import OrderSubmitted
from nautilus_trader.model.events import OrderTriggered
from nautilus_trader.model.events import OrderUpdated
def on_order_initialized(self, event: OrderInitialized) -> None:
def on_order_denied(self, event: OrderDenied) -> None:
def on_order_emulated(self, event: OrderEmulated) -> None:
def on_order_released(self, event: OrderReleased) -> None:
def on_order_submitted(self, event: OrderSubmitted) -> None:
def on_order_rejected(self, event: OrderRejected) -> None:
def on_order_accepted(self, event: OrderAccepted) -> None:
def on_order_canceled(self, event: OrderCanceled) -> None:
def on_order_expired(self, event: OrderExpired) -> None:
def on_order_triggered(self, event: OrderTriggered) -> None:
def on_order_pending_update(self, event: OrderPendingUpdate) -> None:
def on_order_pending_cancel(self, event: OrderPendingCancel) -> None:
def on_order_modify_rejected(self, event: OrderModifyRejected) -> None:
def on_order_cancel_rejected(self, event: OrderCancelRejected) -> None:
def on_order_updated(self, event: OrderUpdated) -> None:
def on_order_filled(self, event: OrderFilled) -> None:
def on_order_event(self, event: OrderEvent) -> None: # All order event messages are eventually passed to this handler
Position management
These handlers receive events related to positions.
PositionEvent
type messages are passed to handlers in the following sequence:
- Specific handler (e.g.,
on_position_opened
,on_position_changed
, etc.) on_position_event(...)
on_event(...)
from nautilus_trader.model.events import PositionChanged
from nautilus_trader.model.events import PositionClosed
from nautilus_trader.model.events import PositionEvent
from nautilus_trader.model.events import PositionOpened
def on_position_opened(self, event: PositionOpened) -> None:
def on_position_changed(self, event: PositionChanged) -> None:
def on_position_closed(self, event: PositionClosed) -> None:
def on_position_event(self, event: PositionEvent) -> None: # All position event messages are eventually passed to this handler
Generic event handling
This handler will eventually receive all event messages which arrive at the strategy, including those for which no other specific handler exists.
from nautilus_trader.core.message import Event
def on_event(self, event: Event) -> None:
Handler example
The following example shows a typical on_start
handler method implementation (taken from the example EMA cross strategy).
Here we can see the following:
- Indicators being registered to receive bar updates
- Historical data being requested (to hydrate the indicators)
- Live data being subscribed to
def on_start(self) -> None:
"""
Actions to be performed on strategy start.
"""
self.instrument = self.cache.instrument(self.instrument_id)
if self.instrument is None:
self.log.error(f"Could not find instrument for {self.instrument_id}")
self.stop()
return
# Register the indicators for updating
self.register_indicator_for_bars(self.bar_type, self.fast_ema)
self.register_indicator_for_bars(self.bar_type, self.slow_ema)
# Get historical data
self.request_bars(self.bar_type)
# Subscribe to live data
self.subscribe_bars(self.bar_type)
self.subscribe_quote_ticks(self.instrument_id)
Clock and timers
Strategies have access to a Clock
which provides a number of methods for creating
different timestamps, as well as setting time alerts or timers to trigger TimeEvent
s.
See the Clock
API reference for a complete list of available methods.
Current timestamps
While there are multiple ways to obtain current timestamps, here are two commonly used methods as examples:
To get the current UTC timestamp as a tz-aware pd.Timestamp
:
import pandas as pd
now: pd.Timestamp = self.clock.utc_now()
To get the current UTC timestamp as nanoseconds since the UNIX epoch:
unix_nanos: int = self.clock.timestamp_ns()