回测
我们目前正在编写这个概念指南。
使用NautilusTrader进行回测是一种系统化的模拟过程,它通过特定的系统实现来复现交易活动。该系统由多个组件构成,包括内置引擎、Cache、MessageBus、Portfolio、Actors、Strategies、Execution Algorithms以及其他用户自定义模块。整个交易模拟基于由BacktestEngine处理的历史数据流。当数据流处理完毕后,引擎会结束运行,并生成详细的结果和性能指标以供深入分析。
需要认识到,NautilusTrader提供了两个不同的API级别用于设置和执行回测:
- 高级API: 使用
BacktestNode和配置对象(内部使用BacktestEngine)。 - 底层API: 直接使用
BacktestEngine进行更"手动"的设置。
选择API级别
在以下情况下考虑使用底层 API:
- 您的整个数据流可以在可用的机器资源(如内存)范围内进行处理。
- 您不希望将数据存储为Nautilus特定的Parquet格式。
- 您有特定需求或偏好希望保留原始格式的原始数据(例如CSV、二进制等)。
- 您需要对
BacktestEngine进行细粒度控制,例如能够在相同数据集上重新运行回测,同时更换组件(如智能体或策略)或调整参数配置。
在以下情况下考虑使用高级API:
- 您的数据流超过了可用内存,需要分批处理流数据。
- 您希望利用
ParquetDataCatalog的性能和便利性来存储Nautilus特有的Parquet格式数据。 - 您重视通过传递配置对象来定义和管理跨多个引擎同时运行的多项回测的灵活性和功能性。
底层API
底层API围绕BacktestEngine展开,通过Python脚本手动初始化和添加输入。
实例化的BacktestEngine可以接受以下内容:
- 自动根据
ts_init排序为单调递增顺序的Data对象列表。 - 多交易场所,手动初始化。
- 多个智能体,手动初始化并添加。
- 多种执行算法,可手动初始化和添加。
这种方法提供了对回测过程的精细控制,允许您手动配置每个组件。
高级API
高层API围绕BacktestNode展开,它负责协调管理多个BacktestEngine实例,每个实例由BacktestRunConfig定义。多个配置可以打包成列表,由节点在一次运行中处理。
每个BacktestRunConfig对象包含以下内容:
- 一个包含
BacktestDataConfig对象的列表。 BacktestVenueConfig对象列表。- 一个
ImportableActorConfig对象的列表。 - 一个包含
ImportableStrategyConfig对象的列表。 ImportableExecAlgorithmConfig对象的列表。- 一个可选的
ImportableControllerConfig对象。 - 一个可选的
BacktestEngineConfig对象,如果未指定则使用默认配置。
数据
为回测提供的数据驱动执行流程。由于可以使用多种数据类型,确保您的交易场所配置与提供的回测数据相匹配至关重要。数据与配置不匹配可能导致执行过程中出现意外行为。
NautilusTrader主要针对订单簿数据进行了设计和优化,订单簿提供了市场中每个价格水平或订单的完整呈现,反映了交易场所的实时行为。这确保了最高水平的执行粒度和真实性。然而,如果无法获取或不需要细粒度的订单簿数据,该平台仍能按以下详细程度递减的顺序处理市场数据:
-
订单簿数据/增量(L3逐笔行情):
- 提供全面的市场深度和详细的订单流,可查看所有单个订单。
-
订单簿数据/增量(L2按价格市场数据):
- 提供所有价格层面的市场深度可视化。
-
报价Ticks (L1逐笔行情):
- 通过仅捕获最佳买入价、卖出价及其数量来表示"盘口顶部"。
-
交易Tick数据:
- 反映实际执行的交易,提供交易活动的精确视图。
-
行情数据:
- 聚合交易活动 - 通常基于固定时间间隔,例如1分钟、1小时或1天。
选择数据:成本与准确性
对于许多交易策略而言,条形数据(例如1分钟数据)足以满足回测和策略开发需求。这一点尤为重要,因为与tick数据或订单簿数据相比,条形数据通常更易获取且更具成本效益。
鉴于这一实际情况,Nautilus旨在支持基于K线的回测,并具备高级功能,即使在处理较低粒度数据时也能最大限度地提高模拟准确性。
对于某些交易策略而言,使用K线数据进行初步开发来验证核心交易理念是切实可行的。 如果策略表现良好但对精确执行时机更为敏感(例如需要在OHLC价格区间内特定价位成交,或采用严格的止盈/止损水平), 此时可以投入更高粒度的数据以进行更精确的验证。
交易场所
在初始化一个回测场所时,您必须从以下选项中指定其内部订单book_type以进行执行处理:
L1_MBP: 一级市场按价(默认)。仅维护订单簿的顶层。L2_MBP: Level 2 按价格市场。维护订单簿深度,每个价格级别聚合一个订单。L3_MBO: 三级按订单市场。维持订单簿深度,跟踪数据提供的所有单独订单。
数据的粒度必须与指定的book_type订单类型匹配。Nautilus无法从报价、交易或K线等较低层级数据生成更高粒度的数据(L2或L3)。
如果您将L2_MBP或L3_MBO指定为交易场所的book_type,则所有非订单簿数据(如报价、交易和K线)将在执行处理时被忽略。
这可能导致订单看起来永远不会成交。我们正在积极改进验证逻辑,以防止配置和数据不匹配的情况。
当提供L2或更高级别的订单簿数据时,请确保更新book_type以反映数据的粒度。
如果未能做到这一点,将导致数据聚合:L2数据将被简化为每个层级仅显示一个订单,而L1数据将仅反映订单簿顶部的层级。
执行
基于Bar的执行
Bar数据提供了每个时间段市场活动的摘要,包含四个关键价格(假设柱状图是通过交易聚合的):
- 开盘价: 开盘价格(首笔交易)
- 最高价: 交易中的最高价格
- 最低价: 交易的最低价格
- Close: 收盘价(最后一笔交易)
虽然这让我们对价格走势有了总体了解,但我们会丢失一些在更细粒度数据中才能看到的重要细节:
- 我们不知道市场达到最高价和最低价的具体顺序。
- 我们无法准确看到价格在时间段内的具体变化时间。
- 我们不知道实际发生的交易序列。
这就是为什么Nautilus通过一个复杂的系统来处理条形数据,尽管存在这些限制,该系统仍试图保持最真实且保守的市场行为。该平台的核心始终维护着一个订单簿模拟——即使您提供的是较不精细的数据,如报价、交易或条形数据(尽管模拟将只保留顶层订单簿)。
处理Bar数据
即使您提供了bar数据,Nautilus仍会为每个交易品种维护一个内部订单簿——就像真实的交易场所一样。
-
时间处理:
- Nautilus对于执行相关的K线数据时序处理有特定方法,这对精确模拟至关重要。
- Bar时间戳(
ts_event)应代表该Bar的收盘时间。这种方法最为合理,因为它代表了Bar完全形成且聚合完成的时刻。 - 初始化时间(
ts_init)可以通过BarDataWrangler中的ts_init_delta参数控制,通常应将其设置为以纳秒为单位的柱状图步长(时间框架)。 - 该平台确保所有事件根据这些时间戳以正确的顺序发生,防止回测中出现任何前瞻性偏差的可能性。
-
价格处理:
- 该平台将每个K线柱的OHLC价格转换为一连串的市场更新。
- 这些更新总是遵循相同的顺序:开盘价 → 最高价 → 最低价 → 收盘价。
- 如果您提供多个时间周期(例如同时提供1分钟和5分钟的K线数据),平台会使用更精细的数据以确保最高准确性。
-
执行记录:
- 当您下达订单时,它们会与模拟订单簿交互,就像在真实交易场所一样。
- 对于市价单(MARKET orders),执行价格是当前模拟市场价格加上任何已配置的延迟。
- 对于在市场中运作的限价单(LIMIT orders),如果任何一根K线的价格触及或突破您的限价,订单将会被执行(详见下文)。
- 匹配引擎会随着OHLC价格变动持续处理订单,而不是等待完整的K线形成。
OHLC价格模拟
在回测执行过程中,每个价格柱被转换为四个价格点的序列:
- 开盘价
- 高价 (高低价之间的订单是可配置的。请参阅下面的
bar_adaptive_high_low_ordering。) - 低价
- 收盘价
该柱状图的交易量平均分配在这四个点上(各占25%)。在边际情况下,如果原始柱状图的交易量除以4小于该工具的最小size_increment,我们仍会使用每个价格点的最小size_increment以确保有效的市场活动(例如,CME集团交易所的1份合约)。
这些价格点的排序方式可以通过在配置交易场所时设置bar_adaptive_high_low_ordering参数来控制。
Nautilus支持两种柱状处理模式:
-
固定排序 (
bar_adaptive_high_low_ordering=False, 默认)- 按照固定顺序处理每个K线柱:
Open → High → Low → Close。 - 简单且确定性的方法。
- 按照固定顺序处理每个K线柱:
-
自适应排序 (
bar_adaptive_high_low_ordering=True)- Uses bar structure to estimate likely price path:
- 如果开盘价接近最高价:处理顺序为
Open → High → Low → Close。 - 如果开盘价更接近最低价:处理顺序为
Open → Low → High → Close。
- 如果开盘价接近最高价:处理顺序为
- 研究显示这种方法在预测正确的高/低序列方面达到约75-85%的准确率(相比之下,固定排序的统计准确率约为50%)。
- 当止盈和止损水平出现在同一根K线内时,这一点尤为重要——因为执行顺序决定了哪个订单会先成交。
- Uses bar structure to estimate likely price path:
以下是配置自适应订单栏以适应交易场所的方法:
# Configure venue with adaptive bar ordering
engine.add_venue(
venue=venue,
oms_type=OmsType.NETTING,
bar_adaptive_high_low_ordering=True, # Enable adaptive ordering of High/Low bar prices
)
滑点与价差处理
在使用不同类型数据进行回测时,Nautilus针对滑点和价差模拟实现了特定处理:
对于L2(按价格市场)或L3(按订单市场)数据,nautilustrader通过以下方式高精度模拟滑点:
- 根据实际订单簿水平执行订单。
- 按价格层级顺序匹配可用数量。
- 保持真实的订单簿深度影响(每笔订单成交)。
对于L1数据类型(例如L1订单簿、交易、报价、K线),滑点处理通过:
初始成交滑点 (prob_slippage):
- 由
FillModel的prob_slippage参数控制。 - 判断初始成交是否会在当前市场价格的一个最小变动价位之外发生。
- 示例:当
prob_slippage=0.5时,市价买单有50%的概率会以比最佳卖价高一个最小变动单位的价格成交。
在使用K线数据进行回测时,请注意价格信息的粒度降低会影响滑点机制。 为了获得最真实的回测结果,建议在可用时考虑使用更高粒度的数据源,如L2或L3级别的订单簿数据。
填充模型
FillModel 帮助在回测过程中以简单的概率方式模拟订单队列位置和执行。
它解决了一个基本挑战:即使拥有完美的历史市场数据,我们也无法完全模拟订单在实时中如何与其他市场参与者互动。
FillModel模拟了真实市场中存在的两个关键交易方面,无论数据质量如何:
-
限价单的队列位置:
- 当多个交易者以相同价格水平下单时,订单在队列中的位置会影响其是否成交以及成交时间。
-
市场影响与竞争:
- 当使用市价单获取流动性时,您将与其他交易者竞争可用流动性,这可能会影响您的成交价格。
配置与参数
from nautilus_trader.backtest.models import FillModel
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.backtest.engine import BacktestEngineConfig
# Create a custom fill model with your desired probabilities
fill_model = FillModel(
prob_fill_on_limit=0.2, # Chance a limit order fills when price matches (applied to bars/trades/quotes + L1/L2/L3 orderbook)
prob_fill_on_stop=0.95, # [DEPRECATED] Will be removed in a future version, use `prob_slippage` instead
prob_slippage=0.5, # Chance of 1-tick slippage (applied to bars/trades/quotes + L1 orderbook only)
random_seed=None, # Optional: Set for reproducible results
)
# Add the fill model to your engine configuration
engine = BacktestEngine(
config=BacktestEngineConfig(
trader_id="TESTER-001",
fill_model=fill_model, # Inject your custom fill model here
)
)
prob_fill_on_limit (默认值: 1.0)
- Purpose:
- 模拟当限价单的价格水平在市场中达到时被成交的概率。
- Details:
- 模拟你在特定价格水平上的订单队列位置。
- 适用于所有数据类型(例如,L3/L2/L1订单簿、报价、交易、K线)。
- 每当市场价格触及您的订单价格(但未穿透)时,都会进行新的随机概率检查。
- 在概率检查成功后,填充剩余的全部订单数量。
示例:
- With
prob_fill_on_limit=0.0:- 限价买入订单在最佳卖价达到限价时永远不会成交。
- 限价卖出订单在最佳买价达到限价时不会成交。
- 这模拟了排在队列最后面永远无法到达前端的情况。
- With
prob_fill_on_limit=0.5:- 限价买单在最佳卖价达到限价时有50%的成交概率。
- 限价卖出订单在最佳买价达到限价时有50%的成交概率。
- 这模拟了处于队列中间位置的情况。
- With
prob_fill_on_limit=1.0(default):- 限价买入订单会在最佳卖价达到限价时成交。
- 限价卖出订单在最佳买价达到限价时总是会被成交。
- 这模拟了处于队列前端并保证成交的情况。
prob_slippage (默认值: 0.0)
- Purpose:
- 模拟执行市价单时遭遇价格滑点的概率。
- Details:
- 仅适用于L1数据类型(例如:报价、交易、柱状图)。
- 触发时,将成交价格向您的订单方向移动一个最小变动价位。
- 影响所有市价类型订单(
MARKET,MARKET_TO_LIMIT,MARKET_IF_TOUCHED,STOP_MARKET)。 - 不适用于L2/L3数据,其中订单簿深度可以决定滑点。
示例:
- With
prob_slippage=0.0(default):- 不应用人为滑点,代表一个理想化场景,您总是能以当前市场价格成交。
- With
prob_slippage=0.5:- 市价买入订单有50%的概率以高于最佳卖价一个最小变动单位的价格成交,50%的概率以最佳卖价成交。
- 市价卖出订单有50%的概率以低于最佳买价一个最小变动单位成交,50%的概率以最佳买价成交。
- With
prob_slippage=1.0:- 市价买入订单总是以比最佳卖价高一个最小变动价位成交。
- 市价卖出订单总是以低于最佳买价一个最小变动单位的价格成交。
- 这会模拟针对您订单的一致不利价格变动。
prob_fill_on_stop (默认值: 1.0)
- 止损单(Stop order)是止损市价单(stop-market order)的简称,当市场价格触及止损价时,该订单会转为市价单。
- 这意味着,止损订单的成交机制本质上就是市价单机制,由
prob_slippage参数控制。
prob_fill_on_stop 参数已弃用,将在未来版本中移除(请改用 prob_slippage)。
模拟如何因数据类型而异
FillModel的行为会根据所使用的订单簿类型进行调整:
L2/L3订单簿数据
通过完整的订单簿深度,FillModel专注于通过prob_fill_on_limit纯粹模拟限价单的队列位置。
订单簿本身会根据每个价格水平的可用流动性自然地处理滑点问题。
prob_fill_on_limit处于激活状态 - 模拟队列位置。prob_slippage未被使用 - 实际订单簿深度决定价格影响。
L1订单簿数据
在仅提供最优买卖价格的情况下,FillModel提供了额外的模拟功能:
prob_fill_on_limit处于激活状态 - 模拟队列位置。prob_slippage处于激活状态 - 由于缺乏真实的深度信息,模拟基本的价格影响。
Bar/Quote/Trade 数据
当使用较低粒度的数据时,其行为与L1级别数据相同:
prob_fill_on_limit处于激活状态 - 模拟队列位置。prob_slippage处于激活状态 - 模拟基本的价格影响。
重要注意事项
FillModel 存在一些需要注意的限制:
- 支持部分成交 使用L2/L3订单簿数据 - 当订单簿中不再有任何可用数量时,将不会生成更多成交,订单将保持部分成交状态。这准确模拟了真实市场条件下在期望价格水平没有足够流动性的情况。
- 使用L1数据时,滑点被限制在固定的1个最小价格变动单位,此时整个订单的数量都会被成交。
随着FillModel的持续发展,未来版本可能会引入更复杂的订单执行动态模拟,包括:
- 部分成交模拟
- 基于订单大小的可变滑点
- 更复杂的队列位置建模