NautilusTrader SyntheticInstrument 合成工具完全指南:从公式语言到模拟订单触发
【免费下载链接】nautilus_traderProduction-grade Rust-native trading engine with deterministic event-driven architecture项目地址: https://gitcode.com/GitHub_Trending/na/nautilus_trader
本篇技术指南聚焦 NautilusTrader 模型层中的SyntheticInstrument(合成工具)——一种价格由其他工具公式推导而来的本地工具,广泛应用于价差(spread)、篮子(basket)、比值(ratio)等衍生价格场景。读完本文,你将掌握合成工具的字段语义、公式表达式引擎的完整语法与编译限制、Rust/Python 双端构建方式,以及如何在策略中订阅合成行情、动态更换公式,并用合成价格触发模拟订单。
核心概念:什么是 SyntheticInstrument
SyntheticInstrument表示一种本地工具(local instrument),其价格来自对若干其他工具(component instruments)价格的公式运算。它并不是交易所上真实可下单的市场,而是作为系统内一个标准工具形态出现,服务于价差、篮子、比值以及其他衍生价格的分析需求。
典型示例包括:
(BTC.BINANCE + LTC.BINANCE) / 2.0——两个币对价格的简单均值;- 由若干成分工具价格构成的 ratio 型交易对。
从源码看,SyntheticInstrument定义于 crates/model/src/instruments/synthetic.rs,其核心成员包括id、price_precision、price_increment、components、formula、ts_event、ts_init,以及两个内部私有字段component_names与compiled_formula(后者保存编译后的表达式,用于每次 tick 的快速求值)。
字段与默认值
构造一个合成工具需要以下字段,其中id与price_increment由系统自动推导:
| 字段 | Rust 类型 | Python 类型 | 必填/默认 | 说明 |
|---|---|---|---|---|
symbol | Symbol | Symbol | 必填 | 合成工具的符号,与SYNTH交易所(venue)组合使用 |
id | InstrumentId | InstrumentId | 派生 | 由symbol.SYNTH组成的工具 ID |
price_precision | u8 | int | 必填 | 合成价格允许的小数位数 |
price_increment | Price | Price | 派生 | 由精度推导出的最小价格步长 |
components | Vec<InstrumentId> | list[InstrumentId] | 必填 | 公式所使用的成分工具列表 |
formula | String | str | 必填 | 基于成分工具 ID 的数值表达式 |
ts_event | UnixNanos | int | 必填 | 事件时间戳(纳秒) |
ts_init | UnixNanos | int | 必填 | 初始化时间戳(纳秒) |
注意:Python 端由
symbol与SYNTHvenue 构造工具 ID;Rust 端在构造时即生成并存储相同的id值。从 crates/model/src/instruments/synthetic.rs 可以看到,new_checked内部通过InstrumentId::new(symbol, Venue::synthetic())生成{symbol}.SYNTH。
price_increment的推导逻辑在new_checked中通过Price::from_mantissa_exponent_checked(1, -price_precision, price_precision)完成,即精度为 2 时增量恰为0.01;源码测试test_new_checked_constructs_exact_price_increment验证了精度为 0、5 以及FIXED_PRECISION时的精确步长。
行为特征
SyntheticInstrument仅存在于 Nautilus 本地,不代表任何交易所可下单市场;- 它始终使用合成 venue
SYNTH; - 公式必须在构造时针对所给的成分工具 ID 成功编译,对象才有效——编译失败会直接返回错误;
- 它没有 venue 限制、保证金、手续费、订单簿或适配器专属元数据。
这些行为也直接体现在 Python API 存根 python/nautilus_trader/model/init.pyi 中:SyntheticInstrument的构造参数为symbol、price_precision、components、formula、ts_event、ts_init,并提供is_valid_formula、change_formula、calculate、calculate_from_map等方法。
公式语言详解
合成工具的价格由公式引擎求值。Nautilus 使用内置数值表达式引擎对公式进行编译(compile-once)与反复求值(eval-many),最终数值结果转换为合成工具的Price类型。公式语言的完整规范详见 docs/concepts/synthetics.md。
支持的语法
公式可以直接引用成分工具 ID,包括含/与-的 ID:
| 结构 | 示例 | 说明 |
|---|---|---|
| 成分引用 | BTCUSDT.BINANCE | 直接使用原始InstrumentId文本 |
| 成分引用 | AUD/USD.SIM | 含/的 ID 合法 |
| 成分引用 | ETH-USDT-SWAP.OKX | 含-的 ID 合法 |
| 数值字面量 | 1、0.5、1.2e-3 | 按f64语义求值 |
| 布尔字面量 | true、false | 用于条件与逻辑表达式 |
| 括号 | (a + b) / 2 | 用括号覆盖优先级 |
| 一元运算符 | -x、!flag | 一元-取负,一元!逻辑取反 |
| 二元运算符 | + - * / % ^、== !=、< <= > >=、&& \|\| | 算术运算符作用于数值,逻辑运算符作用于布尔值 |
| 局部赋值 | spread = a - b; spread / 2 | 语句从左到右执行,公式必须以一个值结尾 |
| 注释 | // line、/* block */ | 注释被忽略 |
提示:新公式应使用原始
InstrumentId值;出于向后兼容,公式中把成分 ID 里的-替换为_的写法仍然被接受。源码中build_bindings会为每个成分注册主绑定,并为含-的 ID 尽力添加_别名(别名与主绑定冲突时自动跳过),这正是 legacy 公式可用的底层原因。
运算符优先级
表达式引擎按以下顺序求值(从最高优先级到最低):
| 级别 | 运算符 | 说明 |
|---|---|---|
| 最高 | ^ | 幂运算,右结合 |
一元-、一元! | -2 ^ 2求值为-(2 ^ 2) | |
*、/、% | 乘法、除法、取模 | |
+、- | 加法、减法 | |
<、<=、>、>= | 数值比较 | |
==、!= | 相等/不等,两侧必须同类型 | |
| 最低 | &&、\|\| | 布尔逻辑运算符 |
赋值不是表达式运算符。用;分隔语句,并把最后一条语句作为合成工具要产出的值。
内置函数
| 函数 | 签名 | 说明 |
|---|---|---|
abs | abs(x) | 绝对值 |
ceil | ceil(x) | 向上取整 |
floor | floor(x) | 向下取整 |
round | round(x) | 按 Rustf64规则四舍五入到最近整数 |
min | min(x1, x2, ...) | 接受一个或多个数值参数 |
max | max(x1, x2, ...) | 接受一个或多个数值参数 |
if | if(condition, when_true, when_false) | 条件必须为布尔值,两个分支类型必须一致,仅求值被选中的分支 |
类型规则
- 成分输入为数值类型;
- 算术运算符要求数值操作数,返回数值;
<、<=、>、>=要求数值操作数,返回布尔值;==、!=接受任意匹配类型(同为数值或同为布尔),返回布尔值;&&、||与一元!要求布尔操作数;&&与||具有短路语义,右侧仅在需要时求值;- 局部变量必须先赋值后使用;
- 局部变量名必须以 ASCII 字母或
_开头,后续只能使用 ASCII 字母、数字或_; - 公式最终结果必须是数值:以赋值结尾或产出布尔结果的公式,对合成工具而言非法。
编译期限制
表达式引擎在编译期强制执行以下限制,超限的公式会在构造时直接报出明确的错误:
| 限制 | 值 | 说明 | | ---- | -- | ---- | | 栈深度 | 32 | 求值栈上中间值的最大数量 | | 局部变量 | 16 | 不同局部变量名的最大数量 | | 嵌套深度 | 128 | 最大语法嵌套与表达式树深度,顶层表达式计为 1 层 |
这些常量在 crates/model/src/expressions/eval.rs 中即为MAX_STACK = 32与MAX_LOCALS = 16。一个 8 成分的加权求和峰值栈深度仅 3、不占用局部变量;N 成分的加权求和会构建深度为 N + 1 的树,因此嵌套深度限制将加权求和的成分数上限封顶在 127 个。源码测试test_new_checked_rejects_excessive_expression_depth也验证了 129 层嵌套会得到 “Expression nesting depth 129 exceeds maximum 128” 的报错。
公式示例
# 简单价差 formula = "BTCUSDT.BINANCE - ETHUSDT.BINANCE" # 两个外汇对的均值 formula = "(AUD/USD.SIM + NZD/USD.SIM) / 2" # 复用中间值 formula = "spread = BTCUSDT.BINANCE - ETHUSDT.BINANCE; spread / 2" # 条件输出 formula = "if(BTCUSDT.BINANCE > ETHUSDT.BINANCE, BTCUSDT.BINANCE, ETHUSDT.BINANCE)"构建 SyntheticInstrument
Rust 示例
原文档给出的 Rust 构建方式使用 fluent builder:
use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::SyntheticInstrument, }; let synthetic = SyntheticInstrument::builder() .symbol(Symbol::from("BTC-LTC")) .price_precision(2) .components(vec![ InstrumentId::from("BTC.BINANCE"), InstrumentId::from("LTC.BINANCE"), ]) .formula("(BTC.BINANCE + LTC.BINANCE) / 2.0") .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap();Rust 端build的底层是build_checked→new_checked:在构造时就完成 ID 生成、price_increment推导、成分名提取与公式编译,任一环节失败都会返回SyntheticInstrumentError。源码测试test_builder_matches_new_checked确认了 builder 构造与位置参数构造new_checked在序列化结果与求值结果上完全一致。
Python 示例
from nautilus_trader.model import InstrumentId from nautilus_trader.model import Symbol from nautilus_trader.model import SyntheticInstrument synthetic = SyntheticInstrument( symbol=Symbol("BTC-LTC"), price_precision=2, components=[ InstrumentId.from_str("BTC.BINANCE"), InstrumentId.from_str("LTC.BINANCE"), ], formula="(BTC.BINANCE + LTC.BINANCE) / 2.0", ts_event=0, ts_init=0, )在策略/Actor 中创建并订阅合成行情
创建合成工具前,请确保所有成分工具已存在于缓存(cache)中,并且同时订阅每个成分的 quote 或 trade 流以及合成工具本身的流。合成 quote 仅由成分 quote 推导,合成 trade 仅由成分 trade 推导。
当某个成分 tick 到达时,引擎将其与其余成分的最新缓存价格组合,计算合成价格;在每个成分都至少产出一个 tick 之前,合成工具不会发布任何数据。
以下示例在 actor 或 strategy 中创建代表 Binance 上 BTC 与 ETH 现货价差的合成工具(假定BTCUSDT.BINANCE与ETHUSDT.BINANCE已存在于缓存):
from nautilus_trader.model import SyntheticInstrument btcusdt_binance_id = InstrumentId.from_str("BTCUSDT.BINANCE") ethusdt_binance_id = InstrumentId.from_str("ETHUSDT.BINANCE") synthetic = SyntheticInstrument( symbol=Symbol("BTC-ETH:BINANCE"), price_precision=8, components=[ btcusdt_binance_id, ethusdt_binance_id, ], formula=f"{btcusdt_binance_id} - {ethusdt_binance_id}", ts_event=self.clock.timestamp_ns(), ts_init=self.clock.timestamp_ns(), ) self._synthetic_id = synthetic.id self.add_synthetic(synthetic) self.subscribe_quotes(self._synthetic_id)注意:上例中合成工具的
instrument_id为{symbol}.SYNTH,即BTC-ETH:BINANCE.SYNTH。
动态更新公式
合成公式可以随时更新:
synthetic = self.cache.synthetic(self._synthetic_id) new_formula = "(BTCUSDT.BINANCE + ETHUSDT.BINANCE) / 2" synthetic.change_formula(new_formula) self.update_synthetic(synthetic)Rust 端对应方法为change_formula(见 crates/model/src/instruments/synthetic.rs),它基于现有成分重新编译公式,编译失败返回错误且不会破坏原公式状态——源码测试test_change_formula_rejects_invalid_formula_without_mutation验证了失败后formula与计算结果均保持原值。构建期之外,is_valid_formula/is_valid_formula_for_components可用于在修改前预检公式是否可编译。
用合成价格触发模拟订单
合成工具的一个典型用途是作为模拟订单(emulated order)的触发源:一旦合成价格达到触发条件,就释放一笔模拟订单。以下示例中,当合成价格触发条件满足时,在ETHUSDT.BINANCE上提交一笔限价买单:
order = self.order_factory.limit( instrument_id=InstrumentId.from_str("ETHUSDT.BINANCE"), order_side=OrderSide.BUY, quantity=Quantity.from_str("1.5"), price=Price.from_str("30000.00000000"), emulation_trigger=TriggerType.DEFAULT, trigger_instrument_id=self._synthetic_id, ) self.submit_order(order)trigger_instrument_id指向合成工具 ID,即可让订单价格与合成价格联动触发,这也是 docs/concepts/synthetics.md 中“从衍生价格触发模拟订单”的核心用法。
价格计算 API 与底层实现
除引擎自动求值外,SyntheticInstrument还暴露显式计算接口,便于测试与离线验证:
calculate(inputs: Sequence[float]) -> Price:按成分顺序传入f64输入数组;calculate_from_map(inputs: dict) -> Price:以{InstrumentId文本: 价格}字典传入输入。
从源码看,calculate会先校验输入数量与成分数一致(否则返回InputCountMismatch),再逐个检查输入是否为有限数(NaN/Infinity 直接返回NonFiniteInput),随后调用编译后表达式的eval_number,最后用Price::new_checked按price_precision量化结果。calculate_from_map对不超过 8 个成分(MAX_INLINE_COMPONENTS)的情况使用栈上定长缓冲区零分配求值,成分更多时回退到堆分配路径,再委托给calculate。
源码测试覆盖了这些路径:test_calculate与test_calculate_from_map验证(BTC.BINANCE + LTC.BINANCE) / 2.0在输入100.0、200.0时产出150.0;test_slashed_instrument_ids_calculate_from_map验证含/的 FX ID;test_hyphenated_instrument_ids_preserve_raw_formula与test_hyphenated_instrument_ids_support_legacy_sanitized_formula分别验证含-ID 的原始公式与 legacy 清洗公式均可求值;test_components_with_colliding_legacy_aliases_coexist则确认FOO-BAR.VENUE与FOO_BAR.VENUE共存时互不干扰。
错误处理与验证边界
Nautilus 在每个边界都校验合成工具:
- 编译期:公式编译拒绝未知符号(如
Unknown symbol \missing``)、类型错误与容量超限(栈深/局部变量/嵌套深度); - 求值期:拒绝输入数量不匹配与非有限价格(NaN、Infinity),避免其进入公式;
- 价格结果:公式结果无法构造成合法
Price(如产生inf)时报InvalidPriceResult。
上述错误统一封装为SyntheticInstrumentError枚举(Validation / Expression / MissingInput / InputCountMismatch / NonFiniteInput / InvalidPriceResult),测试test_builder_rejects_unknown_formula_symbol、test_calculate_rejects_wrong_input_count、test_calculate_rejects_non_finite_inputs、test_calculate_rejects_invalid_price_result分别对应验证了这些路径。此外,SyntheticInstrument实现了Serialize/Deserialize,反序列化时会重新编译公式,因此反序列化同样会拒绝未知符号(见test_deserialize_rejects_unknown_formula_symbol);其PartialEq/Hash仅基于id。
性能特性
公式在构造时编译一次,此后在每个成分价格 tick 上反复求值。表达式引擎采用 compile-once/eval-many 架构,配合零分配的f64栈,求值对 tick 处理路径的开销可忽略(相关说明见 docs/concepts/synthetics.md)。按官方文档在 Apple M4 Pro、rustc 1.94.1、release profile(opt-level 3)下的测量:
- 热路径求值:
(A + B) / 2.0约 12 ns;A * 0.4 + B * 0.3 + C * 0.2 + D * 0.1约 18 ns;if(A > B, A - B, B - A)约 12 ns;带局部变量的公式约 19 ns; - 加权求和扩展性:2 成分 14 ns、4 成分 18 ns、8 成分 28 ns;
- 冷路径编译:简单均值约 675 ns,4 输入加权约 1.4 us,条件公式约 1.0 us,带局部变量约 1.3 us,含连字符 ID 约 755 ns。
适配器与成分来源
SyntheticInstrument仅存在于本地,其价格从已加载到系统中的任意适配器(adapter)成分工具推导而来。它不绑定任何单一交易所适配器,也没有 venue 限制、保证金、手续费、订单簿或适配器专属元数据——这些特性使其成为跨交易所组合价差、篮子与比值的通用载体。
相关指南
- Synthetics 合成工具——公式推导工具与合成 K 线的完整规范;
- Instruments 工具——工具定义与各类 venue 专属工具类型;
- Data 数据——引用工具的市场数据类型;
- Orders 订单——订单可通过合成工具 ID 作为模拟触发源;
- 源码实现:crates/model/src/instruments/synthetic.rs 与表达式引擎 crates/model/src/expressions/eval.rs;
- Python API 存根:python/nautilus_trader/model/init.pyi。
【免费下载链接】nautilus_traderProduction-grade Rust-native trading engine with deterministic event-driven architecture项目地址: https://gitcode.com/GitHub_Trending/na/nautilus_trader
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考