The heart of the NautilusTrader user experience is in writing and working with
trading strategies. Defining a trading strategy is achieved by inheriting the
and implementing the methods required by the strategy.
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.
Refer to the
for a complete description
of all the possible functionality.
There are two main parts of a Nautilus trading strategy:
The strategy implementation itself, defined by inheriting the
The optional strategy configuration, defined by inheriting the
Once a strategy is defined, the same source can be used for backtesting and live trading.
Since a trading strategy is a class which inherits from
, you must define
a constructor where you can handle initialization. Minimally the base/super class needs to be initialized:
class MyStrategy(Strategy): def __init__(self) -> None: super().__init__() # <-- the super class must be called to initialize the strategy
The main purpose of a separate configuration class is to provide total flexibility over where and how a trading strategy can be instantiated. This includes being able to serialize strategies and their configurations over the wire, making distributed backtesting and firing up remote live trading possible.
This configuration flexibility is actually opt-in, in that you can actually choose not to have any strategy configuration beyond the parameters you choose to pass into your strategies’ constructor. However, if you would like to run distributed backtests or launch live trading servers remotely, then you will need to define a configuration.
Here is an example configuration:
from decimal import Decimal from nautilus_trader.config import StrategyConfig from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.trading.strategy import Strategy class MyStrategyConfig(StrategyConfig): instrument_id: str bar_type: str fast_ema_period: int = 10 slow_ema_period: int = 20 trade_size: Decimal order_id_tag: str # Here we simply add an instrument ID as a string, to # parameterize the instrument the strategy will trade. class MyStrategy(Strategy): def __init__(self, config: MyStrategyConfig) -> None: super().__init__(config) # Configuration self.instrument_id = InstrumentId.from_str(config.instrument_id) # Once a configuration is defined and instantiated, we can pass this to our # trading strategy to initialize. config = MyStrategyConfig( instrument_id="ETHUSDT-PERP.BINANCE", bar_type="ETHUSDT-PERP.BINANCE-1000-TICK[LAST]-INTERNAL", trade_size=Decimal(1), order_id_tag="001", ) strategy = MyStrategy(config=config)
Even though it often makes sense to define a strategy which will trade a single instrument. There is actually no limit to the number of instruments a single strategy can work with.
Multiple strategies ¶
If you intend running multiple instances of the same strategy, with different
configurations (such as trading different instruments), then you will need to define
for each of these strategies (as shown above).
The platform has built-in safety measures in the event that two strategies share a duplicated strategy ID, then an exception will be thrown that the strategy ID has already been registered.
The reason for this is that the system must be able to identify which strategy
various commands and events belong to. A strategy ID is made up of the
strategy class name, and the strategies
separated by a hyphen. For
example the above config would result in a strategy ID of
for further details.
Managed GTD expiry ¶
It’s possible for the strategy to manage expiry for orders with a time in force of GTD ( Good ‘till Date ). This may be desirable if the exchange/broker does not support this time in force option, or for any reason you prefer the strategy to manage this.
Simply set the
boolean flag on the
. This will then start a timer, when the timer expires the order will be canceled (if not already closed).