Nautilus Trader 网格做市策略(Grid Market Maker)深度指南:库存感知定价、几何网格与仓位风控
【免费下载链接】nautilus_traderProduction-grade Rust-native trading engine with deterministic event-driven architecture项目地址: https://gitcode.com/GitHub_Trending/na/nautilus_trader
网格做市(Grid Market Making)是加密货币与衍生品市场中最经典的流动性提供策略之一:围绕最新中间价对称挂出一组限价买单与卖单,通过价格波动反复成交赚取买卖价差。nautilus_trader 仓库在crates/trading中内置了生产级参考实现GridMarketMaker,它在传统网格基础上加入了库存感知偏斜(Avellaneda-Stoikov 启发)、最坏情况仓位投影与异步撤单保护等机制。读完本文,你将掌握该策略的完整下单流程、全部配置参数与调优方法,并能同时在 Rust 与 Python(v2)两条路径中将其接入LiveNode或BacktestEngine运行。
本文以 crates/trading/src/examples/strategies/grid_mm/README.md 为核心骨架,并结合同目录下的 strategy.rs、config.rs、tests.rs 源码与测试逐一展开印证。
策略概览
GridMarketMaker是一套库存感知(inventory-aware)的网格做市策略,核心设计目标有三:
- 围绕中间价对称挂单:在每个 requote 周期内,按几何间隔在中间价两侧各生成
num_levels档限价单(买、卖各num_levels档)。 - 降低撤换单频率:已挂出的订单不会逐 tick 重建,只有当中间价相对上次挂单位置移动超过
requote_threshold_bps阈值时才整体撤换,从而减少无谓的 cancel/replace 流量。 - 抑制库存堆积:整张网格会按当前净持仓方向施加一个与仓位成比例的偏斜(skew),鼓励市场消化已有库存,而非单向越积越多。
策略通过DataActor接口订阅单个合约的QuoteTick行情流,所有状态(instrument、price_precision、last_quoted_mid、pending_self_cancels)都保存在策略实例内部,采用纯事件驱动方式响应行情与订单事件。
下单流程:一次 requote 的完整生命周期
几何网格定价公式
GridMarketMaker每次 requote 的订单生成逻辑集中在 strategy.rs 的grid_orders方法中。网格采用**几何间隔(geometric spacing)**而非等差间隔,即价格按指数递减/递增:
- 买入第 N 档:
mid × (1 - grid_step_bps / 10000) ^ N - skew - 卖出第 N 档:
mid × (1 + grid_step_bps / 10000) ^ N - skew
其中grid_step_bps为网格间距(以中间价的基点计),10 bps = 0.1%。几何网格在高价位区间相邻档位间距更大、在低价位区间更密,天然契合价格百分比波动近似的市场特征,是网格做市的常见做法。
源码中对应实现为:
let pct = self.config.grid_step_bps as f64 / 10_000.0; for level in 1..=self.config.num_levels { let buy_f64 = mid_f64 * (1.0 - pct).powi(level as i32) - skew_f64; let sell_f64 = mid_f64 * (1.0 + pct).powi(level as i32) - skew_f64; let buy_price = instrument.next_bid_price(buy_f64, 0); let sell_price = instrument.next_ask_price(sell_f64, 0); // ... }值得注意的是,计算出的裸价格并不会直接下单,而是经过instrument.next_bid_price/next_ask_price做tick 取整:
- 买入价向下取整到最近的有效买盘 tick(保证
buy_price ≤ 理论价,对买方更有利、更易成交); - 卖出价向上取整到最近的有效卖盘 tick(保证
sell_price ≥ 理论价)。
源码注释明确指出这一步是为了防止在 tick 间隔较粗的合约上出现自成交(self-cross)——若两侧都向下或向上取整,买价与卖价可能在窄网格下互相穿越。测试 tests.rs 中给出精确例证:mid=1000、grid_step_bps=100(1% 网格)时,三档买价为 990.00 / 980.10 / 970.29,三档卖价为 1010.00 / 1020.10 / 1030.31(970.299→970.29 向下取整、1030.301→1030.31 向上取整)。
requote 触发条件:阈值 + 空仓兜底
每个QuoteTick到达后,on_quote 首先从 bid/ask 计算中间价:
let mid_f64 = f64::midpoint(quote.bid_price.as_f64(), quote.ask_price.as_f64());随后调用should_requote(strategy.rs)判断是否满足撤换条件——中间价相对last_quoted_mid的相对位移是否达到requote_threshold_bps:
let threshold = self.config.requote_threshold_bps as f64 / 10_000.0; (mid.as_f64() - last_f64).abs() / last_f64 >= threshold另外还有一层空网格兜底:即使中间价仍在阈值内,只要当前既没有 resting(open)订单也没有 inflight(在途)订单,策略也会强制 requote(strategy.rs),避免出现"零挂单干等价格漂移"的僵局。测试 tests.rs 验证了阈值判定边界:5 bps 阈值下 mid 从 1000.00 移到 1000.30(0.03%)不 requote,移到 1000.50(0.05%)或 999.40(-0.06%)则 requote。
requote 的完整执行顺序(README 与源码一致):
- 计算最新 quote 的中间价;
- 判断中间价自上次挂网以来是否移动超过
requote_threshold_bps; - 撤销全部既有订单(
cancel_all_orders); - 按几何间距公式计算各档买/卖价(并做 tick 取整);
- 逐档校验
max_position仓位上限(计入在途订单的最坏情况敞口)后才挂出。
库存偏斜(Inventory Skew):Avellaneda-Stoikov 启发
网格不是永远对称的。为了抑制库存堆积,策略会根据当前净持仓对整张网格施加平移偏斜,公式为:
skew = skew_factor * net_position当net_position > 0(持有净多头)时,skew > 0,此时:
- 卖出档价格 =
mid × (1+pct)^N - skew,整体下移→ 卖价更贴近市场,更激进地出掉多头库存; - 买入档价格 =
mid × (1-pct)^N - skew,同样下移→ 买价离市场更远,不再继续追买。
净空头时则完全对称地反向作用。这一"偏斜方向与持仓相反、幅度与仓位成正比"的思路正是 Avellaneda-Stoikov 做市模型中库存风险控制的核心思想(README 与 mod.rs 的文档注释均明确标注 "Avellaneda-Stoikov inspired")。
测试 tests.rs 给出精确数值例证:skew_factor=1.0、net_position=2.0时skew=2.0,mid=1000、5% 网格下单档买价 = 1000×0.95 − 2.0 =948.00,单档卖价 = 1000×1.05 − 2.0 =1048.00,整张网格整体下移 2 个价格单位。
仓位上限:基于最坏情况敞口的逐档校验
为什么必须用"最坏情况"
撤单是异步的:策略发起cancel_all_orders后,被撤订单在交易所确认之前仍可能成交。因此max_position的校验不能只看当前持仓,而必须把在途(pending)买单/卖单一并计入投影敞口。on_quote中通过缓存(cache)分别聚合:
net_position:所有 open 仓位的带符号数量(signed_qty)之和,用于计算 skew;worst_long= 当前仓位 + 所有在途买单剩余数量(leaves_qty);worst_short= 当前仓位 − 所有在途卖单剩余数量。
源码在聚合 open/inflight 订单时还对ClientOrderId做了去重(strategy.rs),因为订单在状态迁移过程中可能同时出现在 open 与 inflight 集合里,避免重复计仓。
逐档投影校验
grid_orders内逐档累加投影敞口(strategy.rs):
if let Some(buy_price) = buy_price && projected_long + trade_size <= max_pos { orders.push((OrderSide::Buy, buy_price)); projected_long += trade_size; } if let Some(sell_price) = sell_price && projected_short - trade_size >= -max_pos { orders.push((OrderSide::Sell, sell_price)); projected_short -= trade_size; }即:只有"当前投影多头 + 本档买单 ≤max_position"时才挂买档,只有"当前投影空头 − 本档卖单 ≥ −max_position"时才挂卖档,越界的档位直接跳过(skip)。max_position是双向硬顶(long 与 short 对称)。tests.rs 中用一组参数化测试完整覆盖了这些分支:
- 持仓 9.9、
trade_size=0.1、max=10.0时,买档只放得下 1 档,卖档 3 档全放(tests.rs); - 持仓恰好等于
max(10.0)时,买档全部被拦截(tests.rs); max_position=0.15、trade_size=0.1时两侧各只能放 1 档(投影敞口跨档累加校验,tests.rs);max_position=0.05小于单档trade_size=0.1时网格为空(tests.rs)。
还有一个容易被忽略的细节:当网格计算出来为空时,last_quoted_mid不会更新(strategy.rs),这样下一个 quote 仍会触发 requote 重试,策略不会因仓位限制而永久停滞。
订单生命周期与事件处理
策略不仅处理行情,还通过nautilus_strategy!宏注册了四个关键订单事件处理器(strategy.rs),核心逻辑围绕pending_self_cancels集合与last_quoted_mid锚点展开:
| 事件 | 行为 | 目的 |
|---|---|---|
on_order_rejected | 从pending_self_cancels移除该单;last_quoted_mid = None | 拒单后网格已不完整,下一个 quote 自动重挂整张网格 |
on_order_expired | 同上 | GTD 到期意味着网格消失,需解除 requote 抑制 |
on_order_filled | 仅当订单完全关闭(is_closed())时才从pending_self_cancels移除 | 部分成交必须保留 ID,避免后续自撤单被误判为外部撤单 |
on_order_canceled | 若命中pending_self_cancels则忽略(这是自己发起的撤单);否则当on_cancel_resubmit=true时重置last_quoted_mid | 区分"自撤"与"协议/外部撤单",外部撤单后允许重挂网格 |
pending_self_cancels会在每次 requote 撤单前由on_quote填充(当on_cancel_resubmit=true时,见 strategy.rs)。on_cancel_resubmit的典型应用场景是 dYdX 这类协议会主动撤销短时订单的交易所:外部撤单事件到达后策略能感知到网格残缺并在下一根 quote 上自动重挂。
tests.rs 针对这一区分逻辑设计了多组边界测试(tests.rs),例如"自撤单不重置 mid → 后续协议撤单正常重置"、"订单在撤单确认前先成交,不影响后续协议撤单触发重挂"、"on_cancel_resubmit=false时任何撤单都不重置"等。
启动、停止与重置
DataActor生命周期回调(strategy.rs):
- on_start:从缓存解析合约,缓存
price_precision与min_quantity;若配置未显式给出trade_size,则回退为合约min_quantity(再退化为Quantity::new(1.0, size_precision));随后subscribe_quotes订阅行情。 - on_stop:
cancel_all_orders撤掉全部订单、close_all_positions平掉全部仓位、unsubscribe_quotes退订行情,干净退出。 - on_reset:清空
instrument、price_precision、last_quoted_mid、pending_self_cancels,并把trade_size还原为配置值(tests.rs 验证了重置后状态完整性)。
配置参数全解
README 中的参数表是配置的核心依据,下表在保留全部字段的基础上,补充了 config.rs 中 builder 默认值、python/examples.rs Python 绑定签名及init.pyi 的类型信息:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
instrument_id | InstrumentId | 必填 | 交易合约,如BTC-USDT-SWAP.OKX |
max_position | Quantity | 必填 | 净敞口硬顶(多头/空头对称) |
trade_size | Option<Qty> | None | 每档下单数量;None时在on_start从合约min_quantity解析 |
num_levels | usize | 3 | 每侧(买/卖)档位数 |
grid_step_bps | u32 | 10 | 网格间距(中间价的基点),10 bps = 0.1% |
skew_factor | f64 | 0.0 | 库存偏斜乘数,越大越激进地逆持仓方向偏移网格 |
requote_threshold_bps | u32 | 5 | 触发重挂的最小中间价位移(bps),降低撤换单频率 |
expire_time_secs | Option<u64> | None | 订单到期秒数;设置后订单使用 GTD(Good-Till-Date)时间生效 |
on_cancel_resubmit | bool | false | 收到外部撤单事件后,在下一根 quote 重挂整张网格 |
此外,Rust builder 默认注入基础策略配置(config.rs):默认strategy_id = "GRID_MM-001"、order_id_tag = "001"(tests.rs 的test_config_defaults逐一断言了全部默认值)。
Python 构造器还额外暴露两个基础配置项(examples.rs):
strategy_id: StrategyId | None = None:自定义策略 ID;order_id_tag: str | None = None:订单 ID 标签;use_uuid_client_order_ids: bool = False/use_hyphens_in_client_order_ids: bool = True:客户端订单 ID 生成方式(部分交易所如 OKX 要求纯字母数字 clOrdId,需关闭连字符,见 examples/live/okx/exec_tester.py 的实际用法)。
下单时所有限价单均以post_only=true提交(strategy.rs),即只挂单不吃单,确保策略纯粹作为流动性提供者运行,同时避免与 taker 单竞争。
参数调优指南
README 给出了三组典型场景的调参建议,可归纳为以下决策框架:
- 窄价差、高成交量市场:
grid_step_bps=5、num_levels=5、requote_threshold_bps=2。网格更密、撤换更灵敏,能捕捉更多 tick,但会产生更高的订单流量(对成交与撤换速度要求高)。 - 宽价差、低成交量市场:
grid_step_bps=20、num_levels=3、requote_threshold_bps=10。放宽阈值减少对流动性差合约的无谓 requote,避免频繁撤换单带来的滑点与手续费损耗。 - 库存控制:从
skew_factor=0.5起步,若策略持续累积方向性库存再逐步提高;同时把max_position设为可承受的最大敞口,配合投影校验杜绝超限。
调参时需理解三组参数的耦合关系:requote_threshold_bps决定"多久撤换一次"(影响订单流量),grid_step_bps × num_levels决定"网格覆盖的价格范围"(影响成交概率与库存累积速度),skew_factor × max_position决定"库存如何被抑制与封顶"(影响风险敞口)。expire_time_secs适合设置后自动清掉残单的场景(配合on_cancel_resubmit=true实现"过期自动重挂"闭环)。
Rust 用法
策略暴露在nautilus_trading::examples::strategies模块下(见 strategies/mod.rs 的 re-export),通过 builder 构造配置、GridMarketMaker::new实例化后注册到节点:
use nautilus_trading::examples::strategies::{GridMarketMaker, GridMarketMakerConfig}; let config = GridMarketMakerConfig::builder() .instrument_id(InstrumentId::from("BTC-USDT-SWAP.OKX")) .max_position(Quantity::from("10.0")) .trade_size(Quantity::from("0.1")) .num_levels(5) .grid_step_bps(15) .skew_factor(0.5) .requote_threshold_bps(5) .build(); let strategy = GridMarketMaker::new(config); node.add_strategy(strategy)?;纯 Rust 路径不需要 Python 运行时:编写策略与main函数后用cargo build构建独立二进制,node.add_strategy(strategy)?注册后node.run().await?即可运行(参考 docs/concepts/rust.md 中的 Pure Rust 示例)。
Python 用法(v2)
Python 侧通过add_builtin_strategy把配置传给LiveNode或BacktestEngine(crates/live/src/python/node.rs 与 crates/backtest/src/python/engine.rs 均实现了同名方法)。Python 只负责提供配置,策略本体完全在 Rust 中运行——这是"single-source"设计:同一份 Rust 策略代码同时服务 Rust/Python 文档、示例与测试(见 docs/concepts/rust.md 的说明与内置策略注册表)。
from nautilus_trader.trading import GridMarketMakerConfig config = GridMarketMakerConfig( instrument_id=InstrumentId.from_str("BTC-USDT-SWAP.OKX"), max_position=Quantity.from_str("10.0"), trade_size=Quantity.from_str("0.1"), num_levels=5, grid_step_bps=15, skew_factor=0.5, requote_threshold_bps=5, ) node.add_builtin_strategy("GridMarketMaker", config)add_builtin_strategy("GridMarketMaker", ...)的字符串类型名与GridMarketMakerConfig的对应关系记录在 docs/concepts/rust.md 的内置策略表中。需要注意的是:add_builtin_strategy是内置示例策略的注册通道,并非自定义原生策略的一等扩展路径——如需编写自己的原生策略,请走纯 Rust 路径(add_strategy)。Python 调用方若使用BacktestEngine,同样调用engine.add_builtin_strategy("GridMarketMaker", config)(crates/backtest/src/python/engine.rs)。
源码验证与测试覆盖
策略的正确性由一组完备的单元测试背书(tests.rs,使用rstest参数化框架,测试合约为crypto_perpetual_ethusdt桩):
- 默认值:
test_config_defaults断言 strategy_id、order_id_tag、trade_size=None、num_levels=3、grid_step_bps=10、skew_factor=0.0、requote_threshold_bps=5、expire_time_secs=None、on_cancel_resubmit=false; - requote 阈值:无上次报价必 requote;阈值内不 requote;恰达阈值/越过阈值(正负方向)均 requote;
- 几何网格:平坦仓位下 1% 网格两侧各 3 档,价格精确到 tick(买档向下取整、卖档向上取整);
- 库存偏斜:skew=2.0 时买/卖单档价格精确平移 2.0;
- 仓位限制:多头/空头越界拦截、全拦截、投影敞口跨档累加、全约束时空网格;
- 错误路径:instrument 未解析、price_precision 未解析时的错误信息断言;
- 撤单语义:自撤/协议撤/成交后撤/关重挂开关 四种组合下
last_quoted_mid与pending_self_cancels的状态转移; - 重置:
on_reset后全部状态清空、trade_size 还原。
这些测试既验证了 README 描述的每一项行为,也明确了异常边界(如max_position < trade_size时返回空网格而非报错),可作为二次开发时理解语义的第一手材料。
小结
GridMarketMaker把网格做市从"简单对称挂单"推进到了"库存感知 + 最坏情况风控 + 异步撤单感知"的生产级形态:几何网格定价配合 tick 取整保证可成交性与无自成交,requote_threshold_bps抑制无谓撤换单流量,skew = skew_factor × net_position实现 Avellaneda-Stoikov 式库存管理,max_position投影校验覆盖在途订单。它同时是理解 nautilus_trader 事件驱动策略框架(DataActor、nautilus_strategy!宏、cache 查询)的优秀参考实现。上手路径清晰:Rust 侧 builder 构造 +add_strategy,Python 侧GridMarketMakerConfig+add_builtin_strategy("GridMarketMaker", config),配置与调优完全对齐本文参数表。
【免费下载链接】nautilus_traderProduction-grade Rust-native trading engine with deterministic event-driven architecture项目地址: https://gitcode.com/GitHub_Trending/na/nautilus_trader
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考