news 2026/9/11 21:34:15

Nautilus Trader 复合做市策略(Composite Market Maker)完整指南:基于 book mid 报价、库存与外部信号双偏置的实现与调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nautilus Trader 复合做市策略(Composite Market Maker)完整指南:基于 book mid 报价、库存与外部信号双偏置的实现与调优

Nautilus Trader 复合做市策略(Composite Market Maker)完整指南:基于 book mid 报价、库存与外部信号双偏置的实现与调优

【免费下载链接】nautilus_traderProduction-grade Rust-native trading engine with deterministic event-driven architecture项目地址: https://gitcode.com/GitHub_Trending/na/nautilus_trader

导读

本文基于 Nautilus Trader 仓库内置的CompositeMarketMaker示例策略,深入讲解一种「单交易对做市、双信号源驱动」的做市策略实现:它围绕目标交易品种的盘口中间价(book mid)挂出单边买一与卖一,并通过**库存偏置(inventory skew)外部信号偏置(signal skew)**两套独立的位移机制来管理风险与捕捉方向性收益。读完本文,你将掌握该策略的报价循环、重新报价(requote)门控逻辑、最坏情况持仓上限计算,以及全部配置参数在 config.rs 与 strategy.rs 中的底层实现,并能在 Rust 与 Python(v2)两条路径上直接落地使用。

策略概述:一个目标盘 + 一个信号盘的组合

CompositeMarketMaker是一个「单一交易对」的做市策略,其定义位于 crates/trading/src/examples/strategies/composite_market_maker/mod.rs:

围绕目标交易品种的 book mid 报价单边买单与单边卖单,一个第二交易品种(典型为SyntheticInstrument)提供信号,其相对基线的残差(residual)将双边报价整体上移或下移;库存偏置则将双边报价朝当前持仓的反方向位移。

策略同时订阅两个交易品种的报价流:

  • 目标交易品种(target instrument):策略实际报价挂单的市场,例如OCPI-H100-PERP.AX
  • 信号交易品种(signal instrument):驱动信号偏置的价格来源。任何发布 quote 的品种都可以充当信号源,但SyntheticInstrument是典型选择——它可以让你把一个多腿的复合信号编码成公式(如多品种价差、指数合成),并在多个策略间复用同一信号。仓库文档中信号品种的示例 ID 为SEMI-COMPOSITE.SYNTH

每次目标品种 tick 到来时,策略评估是否需要重新报价;而信号品种的 tick 只被动更新内部last_signal值,永远不会独立触发 requote,它要等到下一个目标 tick 才会被读取并影响报价。

报价循环(Quoting Cycle):六步流程

原文档给出的报价循环如下,每次目标品种报价 tick 都会触发:

  1. 计算锚点(anchor):取最新 quote 的中间价作为锚点;
  2. requote 门控:如果锚点与信号残差的价格影响(signal_skew_factor * residual)自上次挂单以来都未移动超过requote_threshold_bps,则跳过本次重新报价;
  3. 撤销旧单:取消目标品种上的所有现存订单;
  4. 读取头寸与最坏情况暴露:从缓存(cache)中读取当前净头寸,以及每侧的最坏情况暴露(未平仓头寸 + 所有挂单中的买单或卖单);
  5. 计算双边价格:在锚点基础上叠加库存偏置与信号偏置,并在提交每一侧之前执行max_position上限检查;
  6. 提交剩余侧订单:以post_only限价单形式提交通过检查的那一侧。

锚点与残差门控的源码实现

在 strategy.rs 中,requote 门控被拆成两个独立的判断函数:

  • should_requote_on_anchor:比较当前锚点与last_quoted_anchor,相对位移|anchor - last| / last >= requote_threshold_bps / 10_000时返回true;没有上次锚点或上次锚点为 0 时直接短路返回true(保证首次能播种挂单);
  • should_requote_on_residual:当signal_skew_factor == 0.0时直接返回false(信号残差无法影响报价,也就不该触发 requote);否则计算残差变化带来的价格影响|residual - last_residual| * |signal_skew_factor|,再除以锚点与 bps 阈值比较;
  • should_requote = should_requote_on_anchor || should_requote_on_residual

关键设计点:requote 阈值是「锚点位移」与「信号残差价格影响」二选一,哪个先越过阈值就触发重新报价。这意味着即使目标盘口纹丝不动,只要信号残差变化足够大,也会「穿透」requote 门控完成挂单刷新。这正对应原文档的说明:取消/重挂(cancel/replace)流量与真实价格变动成正比,同时仍能捕捉目标 tick 之间的信号更新。

有单才跳过,无单必重挂

on_quote中还有一处容易被忽略的逻辑:if !self.should_requote(...) && has_resting { return Ok(()); }。即只有当门控未通过且盘口上仍有挂单时才直接返回;如果当前没有挂单(例如全部被外部取消或成交),即使位移未达阈值也会重新报价补单。has_resting通过缓存统计orders_open_countorders_inflight_count得到。

库存偏置(Inventory Skew)

原文档给出公式:

inventory_shift = inventory_skew_factor * net_position

inventory_skew_factor为正且策略持有多头时,双边同时下移:买价离市场更远(更难成交、防止继续加仓),卖价离市场更近(更易成交、鼓励减仓)。位移是对称的,因此报价价差(spread)保持不变,改变的只是价差相对锚点的位置。

源码中对应的实现位于compute_quotes(strategy.rs):

let inventory_shift = self.config.inventory_skew_factor * net_position; let signal_shift = self.config.signal_skew_factor * signal_residual; // Positive signal residual lifts both sides; positive inventory lowers both sides. let total_shift = signal_shift - inventory_shift; let bid_f64 = anchor_f64 - half_spread + total_shift; let ask_f64 = anchor_f64 + half_spread + total_shift;

注意net_position的符号约定:持仓为正(多头)时inventory_shift为正,total_shift = signal_shift - inventory_shift会整体下移;持仓为负(空头)时整体上移。这一符号关系在 tests.rs 中有明确验证:

  • 多头 2 单位、inventory_skew_factor=1.0、锚点 1000、半价差 100bps(10 元)时:bid = 988.00、ask = 1008.00(整体下移 2 元);
  • 空头 -2 单位时:bid = 992.00、ask = 1012.00(整体上移 2 元)。

报价的 tick 化与防交叉处理

计算出原始 bid/ask 浮点价格后,代码使用instrument.next_bid_price(bid_f64, 0)向下取整到最近的合法买价 tick,用instrument.next_ask_price(ask_f64, 0)向上取整到最近的合法卖价 tick——这种取整方向保证了非交叉报价对在取整后仍然不交叉。但当偏置超过半价差(粗 tick 品种上)时取整仍可能让买价 ≥ 卖价,此时compute_quotes直接返回空订单列表(crossed检查),放弃本轮挂单而不是提交会吃单的订单。这是源码注释明确说明的防御逻辑。

信号偏置(Signal Skew)

原文档给出公式:

residual = (signal_mid - baseline) / baseline signal_shift = signal_skew_factor * residual

signal_skew_factor为正且残差为正(信号价格高于基线)时,双边报价同幅上移——这正是信号预示目标品种即将上行漂移时想要的挂单位置。基线的取值有两种方式:

  • 配置signal_baseline:显式给出(对回测是确定性的);
  • 不配置(None:捕捉第一个观察到的信号 mid 作为基线。

源码中signal_residual()(strategy.rs)的实现为:

match (self.last_signal, self.signal_baseline) { (Some(signal), Some(baseline)) if baseline != 0.0 => signal / baseline - 1.0, _ => 0.0, }

signal / baseline - 1.0(signal - baseline) / baseline。测试 tests.rs 覆盖了三个边界情形:

  • 没有信号时残差恒为 0.0;
  • 基线 100、信号 110 时残差为 0.10;
  • 基线为 0 时守卫生效,返回 0.0 避免除零

信号 tick 处理(on_quote前半段)只做两件事:更新last_signal = f64::midpoint(bid, ask);若signal_baseline尚未设置则用首个信号 mid 捕获基线。测试test_signal_tick_updates_last_signal_and_captures_baseline_once验证了基线只捕获一次;test_signal_tick_does_not_overwrite_explicit_baseline验证显式配置的基线永远不会被信号 tick 覆盖。

持仓上限(Position Limits):最坏情况投影

在提交每一侧之前,策略会投影最坏情况的单侧暴露:

worst_long = 净持仓(带方向) + 所有挂单中的买单剩余量 worst_short = 净持仓(带方向) - 所有挂单中的卖单剩余量

源码中(strategy.rs)通过缓存同时遍历orders_openorders_inflight,用AHashSet去重后按方向累加leaves_qty。随后在compute_quotes中执行上限检查:

if let Some(price) = bid_price && worst_long + trade_size <= max_pos { orders.push((OrderSide::Buy, price)); } if let Some(price) = ask_price && worst_short - trade_size >= -max_pos { orders.push((OrderSide::Sell, price)); }

为什么连 in-flight 订单也算:异步撤单在途期间,挂单仍然可能成交并计入持仓,因此必须把未确认撤单的订单继续视为暴露,max_position才能在上限处真正「封顶」。这与仓库中同目录下的GridMarketMaker采用同一模式(见 mod.rs 的模块注释)。测试test_compute_quotes_max_position_blocks_buy/test_compute_quotes_max_position_blocks_sell验证了触顶后对应侧订单被丢弃、只保留另一侧。

需要强调的是:策略在触顶时停止加仓,但不会主动减仓。偏置会让某一侧变得更「划算」,那一侧成交把仓位打回来是唯一的退出路径——这是原文档明确描述的行为边界。

配置参数全表

原文档给出的完整配置表如下,字段定义可在 config.rs 中逐条核对:

ParameterTypeDefaultDescription
instrument_idInstrumentIdrequiredTarget instrument the strategy quotes on.
signal_instrument_idInstrumentIdrequiredSignal instrument (typically a synthetic) whose mid drives the signal residual.
max_positionQuantityrequiredHard cap on net exposure (long or short).
trade_sizeOption<Quantity>NoneSize per quote. WhenNone, resolves from the instrument'smin_quantity.
half_spread_bpsu325Half the desired quoted spread, in basis points of the anchor.
inventory_skew_factorf640.0Price units per unit of net position. Both sides shift down by this times position.
signal_skew_factorf640.0Price units per unit of normalized signal residual. Both sides shift up.
signal_baselineOption<f64>NoneBaseline price for the signal residual. WhenNone, captured from the first signal.
requote_threshold_bpsu325Minimum anchor or signal-residual price-impact move in bps before re-quoting.
expire_time_secsOption<u64>NoneOrder expiry in seconds. When set, orders use GTD time-in-force.
on_cancel_resubmitboolfalseResubmit on the next quote after an external cancel.

源码层的参数语义补充

  • base: StrategyConfig:每个示例策略配置都内嵌一个基础策略配置。CompositeMarketMakerConfig的默认basestrategy_id: "COMPOSITE_MM-001"order_id_tag: "001"(config.rs),测试test_config_defaults逐项断言了这些默认值;
  • trade_size = None的解析时机:在on_start中从缓存解析目标品种,取min_quantity;若品种未定义最小数量,则回退为Quantity::new(1.0, size_precision)(strategy.rs);
  • half_spread_bps的语义5表示半价差 5bps,即偏置前的完整报价价差为 10bps;
  • expire_time_secs:设置后订单使用TimeInForce::Gtdexpire_time = now + expire_time_secs(基于策略时钟);
  • on_cancel_resubmit:为true时,若发生非策略自发的外部撤单(如交易所主动撤掉短期订单),策略会重置last_quoted_anchor/last_quoted_residual,让下一个 tick 直接重新挂单。实现上策略用pending_self_cancels集合区分「自己发起的撤单」与「外部/协议撤单」(strategy.rs),测试test_on_order_canceled_self_cancel_preserves_anchortest_on_order_canceled_protocol_cancel_resets_anchor验证了这一区分;
  • 订单事件重置on_order_rejectedon_order_expired都会无条件重置锚点与残差状态并清理pending_self_cancels,保证被拒/过期后能立即重新报价。

生命周期行为

DataActortrait 的实现可以看到完整的生命周期管理(strategy.rs):

  • on_start:解析目标品种信息(price_precisionsize_precisionmin_quantity),解析trade_size,随后订阅目标与信号两个品种的 quote 流;
  • on_stop:取消目标品种全部订单、平掉全部仓位,并退订两个 quote 流;
  • on_reset:清空全部运行状态——instrumentprice_precisionlast_quoted_anchorlast_quoted_residuallast_signalpending_self_cancels,并把signal_baseline恢复为配置值(未配置则恢复为None),trade_size恢复为配置值。测试test_on_reset_reverts_signal_baseline_to_config_value验证了显式基线的恢复行为。

调优指南(Tuning Guidelines)

原文档给出的四组调优建议,是实战中最有价值的经验参数:

  • 流动性好的盘口(Liquid book):小half_spread_bps(5 到 10)搭配紧requote_threshold_bps(2 到 5)。策略能吃到价差,但必须跟上中间价的移动速度;
  • 流动性差的盘口(Illiquid book):更宽的half_spread_bps(20 到 50)与requote_threshold_bps(10 到 20)。报价能容忍陈旧盘口条件,不会在每一次微小抖动上都造成撤单风暴;
  • 库存控制(Inventory control):选择inventory_skew_factor,使factor * max_position落在半价差的 10% 到 50% 区间。超过这个区间,偏置后的报价会穿越活跃盘口,作为 post-only 订单被拒;低于这个区间,偏置太小不足以改变成交概率;
  • 信号权重(Signal weighting):选择signal_skew_factor,使factor * typical_residual是 tick size 的小倍数。例如信号偏离基线 5%、signal_skew_factor = 1.0时产生 0.05 个价格单位的位移——对低价品种有实际意义,对高价品种则几乎不可见。

结合源码可以进一步理解这些建议的机理:半价差、库存位移、信号位移最终在compute_quotes中合并为total_shift,再经过next_bid_price/next_ask_price的 tick 取整。|total_shift|超过半价差时,报价对会交叉并整轮被丢弃——这正是「偏置过大会导致挂不出单」的源码级原因;而requote_threshold_bps则通过should_requote控制撤单重挂的频率,直接决定 cancel/replace 流量与交易所费率成本。

Rust 用法

原文档给出的 Rust 用法如下(路径为 crates/trading/src/examples/strategies/composite_market_maker/mod.rs 导出的CompositeMarketMaker/CompositeMarketMakerConfig):

use nautilus_model::{identifiers::InstrumentId, types::Quantity}; use nautilus_trading::examples::strategies::{ CompositeMarketMaker, CompositeMarketMakerConfig, }; let config = CompositeMarketMakerConfig::builder() .instrument_id(InstrumentId::from("OCPI-H100-PERP.AX")) .signal_instrument_id(InstrumentId::from("SEMI-COMPOSITE.SYNTH")) .max_position(Quantity::from("100")) .trade_size(Quantity::from("100")) .half_spread_bps(25) .inventory_skew_factor(0.0005) .signal_skew_factor(0.5) .requote_threshold_bps(10) .build(); let strategy = CompositeMarketMaker::new(config); node.add_strategy(strategy)?;

配置采用bon::Builder派生构造器(见 config.rs),必填项为instrument_idsignal_instrument_idmax_position,其余参数均有默认值——test_config_defaults断言了half_spread_bps = 5requote_threshold_bps = 5、两个 skew factor 均为0.0trade_size = Nonesignal_baseline = Noneexpire_time_secs = Noneon_cancel_resubmit = false

Python 用法(v2)

在 Python v2 中,将配置传给LiveNodeBacktestEngineadd_builtin_strategy即可。Python 只负责提供配置,策略本身完全运行在 Rust 侧

from nautilus_trader.trading import CompositeMarketMakerConfig config = CompositeMarketMakerConfig( instrument_id=InstrumentId.from_str("OCPI-H100-PERP.AX"), signal_instrument_id=InstrumentId.from_str("SEMI-COMPOSITE.SYNTH"), max_position=Quantity.from_str("100"), trade_size=Quantity.from_str("100"), half_spread_bps=25, inventory_skew_factor=0.0005, signal_skew_factor=0.5, requote_threshold_bps=10, ) node.add_builtin_strategy("CompositeMarketMaker", config)

CompositeMarketMakerConfig通过pyo3暴露为nautilus_trader.trading模块下的 Python 类(config.rs),并登记在 python/nautilus_trader/trading/init.pyi 的类型存根中。Python 侧还可以额外传入strategy_idorder_id_tag覆盖默认策略 ID 与订单 ID 前缀,这一用法在仓库测试 python/tests/unit/trading/test_example_configs.py 中有直接示例:

CompositeMarketMakerConfig( instrument_id=INSTRUMENT_ID, signal_instrument_id=SIGNAL_INSTRUMENT_ID, max_position=Quantity.from_str("1"), strategy_id=STRATEGY_ID, order_id_tag=ORDER_ID_TAG, )

测试覆盖:行为契约的可验证依据

策略的完整行为契约由 tests.rs 中的 30 余个rstest测试固化,可作为理解与改造策略时的行为参考:

  • requote 门控:无上次报价时必重挂;阈值内不重挂;恰达阈值重挂;锚点不动但信号残差价格影响越过阈值时重挂;signal_skew_factor = 0时残差门控完全失效;
  • 除零防御:基线为 0 时残差返回 0;锚点为 0 时残差门控短路为false;上次锚点为 0 时锚点门控短路为true
  • 报价计算:无偏置时的对称报价(bid = anchor - half_spread,ask = anchor + half_spread);库存偏置多/空头方向的位移;信号偏置正残差的上移;组合偏置的叠加;偏置对称性保证价差不变;max_position阻断买单/卖单;
  • 事件处理:自撤单保留锚点状态、外部撤单(开启 resubmit 时)重置锚点、拒绝/过期无条件重置、on_reset全状态清理与基线回退。

适用边界与注意事项

  • 本策略是仓库内置的示例策略(位于crates/trading/src/examples/),定位是展示 Nautilus Trader 策略框架的完整写法(配置构造、DataActor生命周期、缓存读取、订单提交、事件处理),并非开箱即用的生产系统——上线前仍需结合具体交易所的费率结构、盘口深度与风控要求自行评估;
  • 策略提交的是post_only限价单(strategy.rs 中Some(true)参数),一旦偏置让报价穿越活跃盘口,订单会被交易所按 post-only 违约拒绝——这正是调优指南中「偏置过大会被拒」的出处;
  • 库存上限是「停止加仓」而非「主动减仓」,退出路径完全依赖偏置让某一侧更易成交,在极端单边行情下需要外部风控兜底。

如需继续深入,可对照阅读仓库中同目录的其他示例策略(如 grid_mm、delta_neutral_vol),以及add_builtin_strategy在 crates/backtest/src/python/engine.rs 与 crates/live/src/python/node.rs 中的注册实现,以了解示例策略在回测引擎与实盘节点上的完整接入链路。

【免费下载链接】nautilus_traderProduction-grade Rust-native trading engine with deterministic event-driven architecture项目地址: https://gitcode.com/GitHub_Trending/na/nautilus_trader

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 21:30:06

Agent记忆系统设计:从数据存储到语义建模的实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 21:29:08

Kilo Code CLI 安装指南:npm 全局安装、旧 CPU 兼容与安装验证

Kilo Code CLI 安装指南&#xff1a;npm 全局安装、旧 CPU 兼容与安装验证 【免费下载链接】kilocode Kilo is the all-in-one agentic engineering platform. Build, ship, and iterate faster with the most popular open source coding agent. 项目地址: https://gitcode.…

作者头像 李华