策略
NautilusTrader 用户体验的核心在于编写和使用交易策略。定义策略涉及继承 Strategy
类并实现策略逻辑所需的方法。
主要功能:
- 所有
Actor
功能 - 订单管理
与参与者的关系:
Strategy
类继承自 Actor
,这意味着策略可以访问所有参与者功能以及订单管理功能。
我们建议在深入策略开发之前先阅读参与者指南。
策略可以添加到任何环境上下文中的 Nautilus 系统,并将在系统启动后立即根据其逻辑开始发送命令和接收事件。
使用数据摄取、事件处理和订单管理的基本构建块(我们将在下面讨论),可以实现任何类型的策略,包括方向性、动量、再平衡、配对、做市等。
有关所有可用方法的完整描述,请参阅 Strategy
API 参考。
Nautilus 交易策略有两个主要部分:
- 策略实现本身,通过继承
Strategy
类定义 - 可选的策略配置,通过继承
StrategyConfig
类定义
一旦定义了策略,相同的源代码可以用于回测和实时交易。
策略的主要功能包括:
- 历史数据请求
- 实时数据订阅
- 设置时间警报或定时器
- 缓存访问
- 投资组合访问
- 创建和管理订单和头寸
策略实现
基本策略结构
所有策略都必须继承自 Strategy
基类并实现必要的方法:
from nautilus_trader.trading.strategy import Strategy
class MyStrategy(Strategy):
"""
自定义交易策略示例
"""
def __init__(self, config=None):
"""初始化策略"""
super().__init__(config)
# 在这里初始化策略特定的属性
self.my_indicator = None
def on_start(self):
"""策略启动时调用"""
# 订阅数据、设置定时器等
pass
def on_stop(self):
"""策略停止时调用"""
# 清理资源
pass
def on_data(self, data):
"""接收到数据时调用"""
# 处理市场数据
pass
def on_event(self, event):
"""接收到事件时调用"""
# 处理系统事件
pass
生命周期方法
策略有几个重要的生命周期方法:
on_start()
- 策略启动时调用
- 用于初始化订阅、指标等
- 只调用一次
on_stop()
- 策略停止时调用
- 用于清理资源
- 只调用一次
on_reset()
- 策略重置时调用
- 用于重置状态和指标
- 主要用于回测
on_save()
- 保存策略状态时调用
- 返回需要持久化的状态数据
on_load()
- 加载策略状态时调用
- 从持久化数据恢复状态
数据处理方法
策略可以处理各种类型的市场数据:
on_quote_tick(tick)
- 处理报价 tick 数据
- 包含买卖价格信息
on_trade_tick(tick)
- 处理成交 tick 数据
- 包含实际成交信息
on_bar(bar)
- 处理 K 线数据
- 包含开高低收量信息
on_order_book_deltas(deltas)
- 处理订单簿增量更新
- 用于高频策略
on_order_book(order_book)
- 处理完整订单簿快照
- 包含完整的买卖盘信息
事件处理方法
策略可以响应各种系统事件:
on_order_submitted(event)
- 订单提交事件
on_order_accepted(event)
- 订单接受事件
on_order_filled(event)
- 订单成交事件
on_position_opened(event)
- 头寸开仓事件
on_position_closed(event)
- 头寸平仓事件
订单管理
策略可以创建和管理各种类型的订单:
# 市价单
order = self.order_factory.market(
instrument_id=self.instrument_id,
order_side=OrderSide.BUY,
quantity=Quantity.from_int(100),
)
self.submit_order(order)
# 限价单
order = self.order_factory.limit(
instrument_id=self.instrument_id,
order_side=OrderSide.SELL,
quantity=Quantity.from_int(100),
price=Price.from_str("1.2000"),
)
self.submit_order(order)
# 止损单
order = self.order_factory.stop_market(
instrument_id=self.instrument_id,
order_side=OrderSide.SELL,
quantity=Quantity.from_int(100),
trigger_price=Price.from_str("1.1900"),
)
self.submit_order(order)
技术指标
策略可以使用内置的技术指标:
from nautilus_trader.indicators.average.sma import SimpleMovingAverage
from nautilus_trader.indicators.average.ema import ExponentialMovingAverage
from nautilus_trader.indicators.momentum.rsi import RelativeStrengthIndex
class IndicatorStrategy(Strategy):
def __init__(self, config=None):
super().__init__(config)
# 创建指标
self.sma = SimpleMovingAverage(20)
self.ema = ExponentialMovingAverage(12)
self.rsi = RelativeStrengthIndex(14)
def on_bar(self, bar):
# 更新指标
self.sma.update_raw(bar.close.as_double())
self.ema.update_raw(bar.close.as_double())
self.rsi.update_raw(bar.close.as_double())
# 检查指标是否已初始化
if not self.rsi.initialized:
return
# 使用指标值进行交易决策
if self.rsi.value < 30: # 超卖
self.buy()
elif self.rsi.value > 70: # 超买
self.sell()
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 superclass 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 日志记录 subsystem 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 金融工具, subscribe to 数据). - Use the
on_stop
method for cleanup tasks (e.g., cancel open 订单, close open 头寸, unsubscribe from 数据).
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:
数据 handling
These handlers receive 数据 updates, including built-in market 数据 and custom user-defined 数据. You can use these handlers to define actions upon receiving 数据 object instances.
from nautilus_trader.core import Data
from nautilus_trader.model import OrderBook
from nautilus_trader.model import Bar
from nautilus_trader.model import QuoteTick
from nautilus_trader.model import TradeTick
from nautilus_trader.model import OrderBookDeltas
from nautilus_trader.model import InstrumentClose
from nautilus_trader.model 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
订单管理
These handlers receive events related to 订单.
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 头寸.
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 实现 (taken from the example EMA cross strategy).
Here we can see the following:
- Indicators being registered to receive bar updates
- Historical 数据 being requested (to hydrate the indicators)
- Live 数据 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
策略 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 示例:
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()
Time alerts
Time alerts can be set which will result in a TimeEvent
being dispatched to the on_event
handler at the
specified alert time. In a live context, this might be slightly delayed by a few microseconds.
This example sets a time alert to trigger one minute from the current time:
import pandas as pd
# Fire a TimeEvent one minute from now
self.clock.set_time_alert(
name="MyTimeAlert1",
alert_time=self.clock.utc_now() + pd.Timedelta(minutes=1),
)
Timers
Continuous timers can be set up which will generate a TimeEvent
at regular intervals until the timer expires
or is canceled.
This example sets a timer to fire once per minute, starting immediately:
import pandas as pd
# Fire a TimeEvent every minute
self.clock.set_timer(
name="MyTimer1",
interval=pd.Timedelta(minutes=1),
)
缓存 access
The trader instances central 缓存
can be accessed to fetch 数据 and 执行 objects (订单, 头寸 etc).
There are many methods available often with filtering functionality, here we go through some basic use cases.
Fetching 数据
The following example shows how 数据 can be fetched from the 缓存 (assuming some instrument ID attribute is assigned):
last_quote = self.cache.quote_tick(self.instrument_id)
last_trade = self.cache.trade_tick(self.instrument_id)
last_bar = self.cache.bar(bar_type)
Fetching 执行 objects
The following example shows how individual order and position objects can be fetched from the 缓存:
order = self.cache.order(client_order_id)
position = self.cache.position(position_id)
See the Cache
API Reference for a complete description
of all available methods.
投资组合 access
The traders central 投资组合
can be accessed to fetch account and positional information.
The following shows a general outline of available methods.
Account and positional information
import decimal
from nautilus_trader.accounting.accounts.base import Account
from nautilus_trader.model import Venue
from nautilus_trader.model import Currency
from nautilus_trader.model import Money
from nautilus_trader.model import InstrumentId
def account(self, venue: Venue) -> Account
def balances_locked(self, venue: Venue) -> dict[Currency, Money]
def margins_init(self, venue: Venue) -> dict[Currency, Money]
def margins_maint(self, venue: Venue) -> dict[Currency, Money]
def unrealized_pnls(self, venue: Venue) -> dict[Currency, Money]
def realized_pnls(self, venue: Venue) -> dict[Currency, Money]
def net_exposures(self, venue: Venue) -> dict[Currency, Money]
def unrealized_pnl(self, instrument_id: InstrumentId) -> Money
def realized_pnl(self, instrument_id: InstrumentId) -> Money
def net_exposure(self, instrument_id: InstrumentId) -> Money
def net_position(self, instrument_id: InstrumentId) -> decimal.Decimal
def is_net_long(self, instrument_id: InstrumentId) -> bool
def is_net_short(self, instrument_id: InstrumentId) -> bool
def is_flat(self, instrument_id: InstrumentId) -> bool
def is_completely_flat(self) -> bool
See the Portfolio
API Reference for a complete description
of all available methods.
报告 and analysis
The 投资组合
also makes a PortfolioAnalyzer
available, which can be fed with a flexible amount of 数据
(to accommodate different lookback windows). The analyzer can provide tracking for and generating of 性能
metrics and statistics.
See the PortfolioAnalyzer
API Reference for a complete description
of all available methods.
See the Portfolio statistics guide.
Trading commands
NautilusTrader offers a comprehensive suite of trading commands, enabling granular 订单管理 tailored for 算法交易. These commands are essential for executing 策略, managing risk, and ensuring seamless interaction with various trading venues. In the following sections, we will delve into the specifics of each command and its use cases.
The Execution guide explains the flow through the system, and can be helpful to read in conjunction with the below.
Submitting 订单
An OrderFactory
is provided on the base class for every Strategy
as a convenience, reducing
the amount of boilerplate required to create different Order
objects (although these objects
can still be initialized directly with the Order.__init__(...)
constructor if the trader prefers).
The 组件 a SubmitOrder
or SubmitOrderList
command will flow to for 执行 depends on the following:
- If an
emulation_trigger
is specified, the command will firstly be sent to theOrderEmulator
. - If an
exec_algorithm_id
is specified (with noemulation_trigger
), the command will firstly be sent to the relevantExecAlgorithm
. - Otherwise, the command will firstly be sent to the
RiskEngine
.
This example submits a LIMIT
BUY order for emulation (see OrderEmulator):
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.enums import TriggerType
from nautilus_trader.model.orders import LimitOrder
def buy(self) -> None:
"""
Users simple buy method (example).
"""
order: LimitOrder = self.order_factory.limit(
instrument_id=self.instrument_id,
order_side=OrderSide.BUY,
quantity=self.instrument.make_qty(self.trade_size),
price=self.instrument.make_price(5000.00),
emulation_trigger=TriggerType.LAST_PRICE,
)
self.submit_order(order)
You can specify both order emulation and an 执行 algorithm.
This example submits a MARKET
BUY order to a TWAP 执行 algorithm:
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.enums import TimeInForce
from nautilus_trader.model import ExecAlgorithmId
def buy(self) -> None:
"""
Users simple buy method (example).
"""
order: MarketOrder = self.order_factory.market(
instrument_id=self.instrument_id,
order_side=OrderSide.BUY,
quantity=self.instrument.make_qty(self.trade_size),
time_in_force=TimeInForce.FOK,
exec_algorithm_id=ExecAlgorithmId("TWAP"),
exec_algorithm_params={"horizon_secs": 20, "interval_secs": 2.5},
)
self.submit_order(order)
Canceling 订单
订单 can be canceled individually, as a batch, or all 订单 for an instrument (with an optional side filter).
If the order is already closed or already pending cancel, then a warning will be logged.
If the order is currently open then the status will become PENDING_CANCEL
.
The 组件 a CancelOrder
, CancelAllOrders
or BatchCancelOrders
command will flow to for 执行 depends on the following:
- If the order is currently emulated, the command will firstly be sent to the
OrderEmulator
- If an
exec_algorithm_id
is specified (with noemulation_trigger
), and the order is still active within the local system, the command will firstly be sent to the relevantExecAlgorithm
- Otherwise, the order will firstly be sent to the
ExecutionEngine
Any managed GTD timer will also be canceled after the command has left the strategy.
The following shows how to cancel an individual order:
self.cancel_order(order)
The following shows how to cancel a batch of 订单:
from nautilus_trader.model import Order
my_order_list: list[Order] = [order1, order2, order3]
self.cancel_orders(my_order_list)
The following shows how to cancel all 订单:
self.cancel_all_orders()
Modifying 订单
订单 can be modified individually when emulated, or open on a venue (if supported).
If the order is already closed or already pending cancel, then a warning will be logged.
If the order is currently open then the status will become PENDING_UPDATE
.
At least one value must differ from the original order for the command to be valid.
The 组件 a ModifyOrder
command will flow to for 执行 depends on the following:
- If the order is currently emulated, the command will firstly be sent to the
OrderEmulator
- Otherwise, the order will firstly be sent to the
RiskEngine
Once an order is under the control of an 执行 algorithm, it cannot be directly modified by a strategy (only canceled).
The following shows how to modify the size of LIMIT
BUY order currently open on a venue:
from nautilus_trader.model import Quantity
new_quantity: Quantity = Quantity.from_int(5)
self.modify_order(order, new_quantity)
The price and trigger price can also be modified (when emulated or supported by a venue).
Strategy 配置
The main purpose of a separate 配置 class is to provide total flexibility over where and how a trading strategy can be instantiated. This includes being able to serialize 策略 and their configurations over the wire, making distributed 回测 and firing up remote 实时交易 possible.
This 配置 flexibility is actually opt-in, in that you can actually choose not to have any strategy 配置 beyond the parameters you choose to pass into your 策略' constructor. If you would like to run distributed backtests or launch 实时交易 servers remotely, then you will need to define a 配置.
Here is an example 配置:
from decimal import Decimal
from nautilus_trader.config import StrategyConfig
from nautilus_trader.model import Bar, BarType
from nautilus_trader.model import InstrumentId
from nautilus_trader.trading.strategy import Strategy
# Configuration definition
class MyStrategyConfig(StrategyConfig):
instrument_id: InstrumentId # example value: "ETHUSDT-PERP.BINANCE"
bar_type: BarType # example value: "ETHUSDT-PERP.BINANCE-15-MINUTE[LAST]-EXTERNAL"
fast_ema_period: int = 10
slow_ema_period: int = 20
trade_size: Decimal
order_id_tag: str
# Strategy definition
class MyStrategy(Strategy):
def __init__(self, config: MyStrategyConfig) -> None:
# Always initialize the parent Strategy class
# After this, configuration is stored and available via `self.config`
super().__init__(config)
# Custom state variables
self.time_started = None
self.count_of_processed_bars: int = 0
def on_start(self) -> None:
self.time_started = self.clock.utc_now() # Remember time, when strategy started
self.subscribe_bars(self.config.bar_type) # See how configuration data are exposed via `self.config`
def on_bar(self, bar: Bar):
self.count_of_processed_bars += 1 # Update count of processed bars
# Instantiate configuration with specific values. By setting:
# - InstrumentId - we parameterize the instrument the strategy will trade.
# - BarType - we parameterize bar-data, that strategy will trade.
config = MyStrategyConfig(
instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"),
bar_type=BarType.from_str("ETHUSDT-PERP.BINANCE-15-MINUTE[LAST]-EXTERNAL"),
trade_size=Decimal(1),
order_id_tag="001",
)
# Pass configuration to our trading strategy.
strategy = MyStrategy(config=config)
When implementing 策略, it's recommended to access 配置 values directly through self.config
.
This provides clear separation between:
-
配置 数据 (accessed via
self.config
):- Contains initial settings, that define how the strategy works
- Example:
self.config.trade_size
,self.config.instrument_id
-
Strategy state variables (as direct attributes):
- Track any custom state of the strategy
- Example:
self.time_started
,self.count_of_processed_bars
This separation makes code easier to understand and maintain.
Even though it often makes sense to define a strategy which will trade a single instrument. The number of 金融工具 a single strategy can work with is only limited by machine resources.
Managed GTD expiry
It's possible for the strategy to manage expiry for 订单 with a time in force of GTD (Good 'till Date). This may be desirable if the exchange/broker does not 支持 this time in force option, or for any reason you prefer the strategy to manage this.
To use this option, pass manage_gtd_expiry=True
to your StrategyConfig
. When an order is submitted with
a time in force of GTD, the strategy will automatically start an internal time alert.
Once the internal GTD time alert is reached, the order will be canceled (if not already closed).
Some venues (such as Binance Futures) 支持 the GTD time in force, so to avoid conflicts when using
managed_gtd_expiry
you should set use_gtd=False
for your 执行 客户端 config.
Multiple 策略
If you intend running multiple instances of the same strategy, with different
configurations (such as trading different 金融工具), then you will need to define
a unique order_id_tag
for each of these 策略 (as shown above).
The platform has built-in safety measures in the event that two 策略 share a duplicated strategy ID, then an 异常 will be raised 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 策略 order_id_tag
separated by a hyphen. For
example the above config would result in a strategy ID of MyStrategy-001
.
See the StrategyId
API Reference for further details.