nautilus_trading/examples/strategies/grid_mm/
strategy.rs1use std::fmt::Debug;
19
20use ahash::AHashSet;
21use nautilus_common::actor::DataActor;
22use nautilus_model::{
23 data::QuoteTick,
24 enums::{OrderSide, TimeInForce},
25 events::{OrderCanceled, OrderExpired, OrderFilled, OrderRejected},
26 identifiers::ClientOrderId,
27 instruments::{Instrument, InstrumentAny},
28 orders::Order,
29 types::{Price, Quantity},
30};
31use rust_decimal::Decimal;
32
33use super::config::GridMarketMakerConfig;
34use crate::{
35 nautilus_strategy,
36 strategy::{Strategy, StrategyCore},
37};
38
39pub struct GridMarketMaker {
46 pub(super) core: StrategyCore,
47 pub(super) config: GridMarketMakerConfig,
48 pub(super) instrument: Option<InstrumentAny>,
49 pub(super) trade_size: Option<Quantity>,
50 pub(super) price_precision: Option<u8>,
51 pub(super) last_quoted_mid: Option<Price>,
52 pub(super) pending_self_cancels: AHashSet<ClientOrderId>,
53}
54
55impl GridMarketMaker {
56 #[must_use]
58 pub fn new(config: GridMarketMakerConfig) -> Self {
59 Self {
60 core: StrategyCore::new(config.base.clone()),
61 instrument: None,
62 trade_size: config.trade_size,
63 config,
64 price_precision: None,
65 last_quoted_mid: None,
66 pending_self_cancels: AHashSet::new(),
67 }
68 }
69
70 pub(super) fn should_requote(&self, mid: Price) -> bool {
71 match self.last_quoted_mid {
72 Some(last_mid) => {
73 let last_f64 = last_mid.as_f64();
74 if last_f64 == 0.0 {
75 return true;
76 }
77 let threshold = self.config.requote_threshold_bps as f64 / 10_000.0;
78 (mid.as_f64() - last_f64).abs() / last_f64 >= threshold
79 }
80 None => true,
81 }
82 }
83
84 pub(super) fn grid_orders(
85 &self,
86 mid: Price,
87 net_position: f64,
88 worst_long: Decimal,
89 worst_short: Decimal,
90 ) -> anyhow::Result<Vec<(OrderSide, Price)>> {
91 let Some(instrument) = self.instrument.as_ref() else {
92 anyhow::bail!("Cannot compute grid orders: instrument is not resolved");
93 };
94 let mid_f64 = mid.as_f64();
95 let skew_f64 = self.config.skew_factor * net_position;
96 let pct = self.config.grid_step_bps as f64 / 10_000.0;
97 let Some(trade_size) = self.trade_size else {
98 anyhow::bail!("Cannot compute grid orders: trade_size is not resolved");
99 };
100 let trade_size = trade_size.as_decimal();
101 let max_pos = self.config.max_position.as_decimal();
102 let mut projected_long = worst_long;
103 let mut projected_short = worst_short;
104 let mut orders = Vec::new();
105
106 for level in 1..=self.config.num_levels {
107 let buy_f64 = mid_f64 * (1.0 - pct).powi(level as i32) - skew_f64;
108 let sell_f64 = mid_f64 * (1.0 + pct).powi(level as i32) - skew_f64;
109 let buy_price = instrument.next_bid_price(buy_f64, 0);
113 let sell_price = instrument.next_ask_price(sell_f64, 0);
114
115 if let Some(buy_price) = buy_price
116 && projected_long + trade_size <= max_pos
117 {
118 orders.push((OrderSide::Buy, buy_price));
119 projected_long += trade_size;
120 }
121
122 if let Some(sell_price) = sell_price
123 && projected_short - trade_size >= -max_pos
124 {
125 orders.push((OrderSide::Sell, sell_price));
126 projected_short -= trade_size;
127 }
128 }
129
130 Ok(orders)
131 }
132}
133
134nautilus_strategy!(GridMarketMaker, {
135 fn on_order_rejected(&mut self, event: OrderRejected) {
136 self.pending_self_cancels.remove(&event.client_order_id);
137 self.last_quoted_mid = None;
139 }
140
141 fn on_order_expired(&mut self, event: OrderExpired) {
142 self.pending_self_cancels.remove(&event.client_order_id);
143 self.last_quoted_mid = None;
145 }
146});
147
148impl Debug for GridMarketMaker {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct(stringify!(GridMarketMaker))
151 .field("config", &self.config)
152 .field("trade_size", &self.trade_size)
153 .finish()
154 }
155}
156
157impl DataActor for GridMarketMaker {
158 fn on_start(&mut self) -> anyhow::Result<()> {
159 let instrument_id = self.config.instrument_id;
160 let (instrument, size_precision, min_quantity) = {
161 let cache = self.cache();
162 let instrument = cache.try_instrument(&instrument_id)?;
163 let size_precision = instrument.size_precision();
164 let min_quantity = instrument.min_quantity();
165 (instrument, size_precision, min_quantity)
166 };
167 self.price_precision = Some(instrument.price_precision());
168 self.instrument = Some(instrument);
169
170 if self.trade_size.is_none() {
172 self.trade_size =
173 Some(min_quantity.unwrap_or_else(|| Quantity::new(1.0, size_precision)));
174 }
175
176 self.subscribe_quotes(instrument_id, None, None);
177 Ok(())
178 }
179
180 fn on_stop(&mut self) -> anyhow::Result<()> {
181 let instrument_id = self.config.instrument_id;
182 self.cancel_all_orders(instrument_id, None, None, None)?;
183 self.close_all_positions(instrument_id, None, None, None, None, None, None)?;
184 self.unsubscribe_quotes(instrument_id, None, None);
185 Ok(())
186 }
187
188 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
189 let mid_f64 = f64::midpoint(quote.bid_price.as_f64(), quote.ask_price.as_f64());
190 let price_precision = self.price_precision.ok_or_else(|| {
191 anyhow::anyhow!("Cannot handle quote: price_precision is not resolved")
192 })?;
193 let mid = Price::new(mid_f64, price_precision);
194
195 let instrument_id = self.config.instrument_id;
196 let strategy_id = self.strategy_id().expect("Strategy must be registered");
197
198 let has_resting = {
200 let cache = self.cache();
201 let inst = Some(&instrument_id);
202 let sid = Some(&strategy_id);
203 cache.orders_open_count(None, inst, sid, None, None) > 0
204 || cache.orders_inflight_count(None, inst, sid, None, None) > 0
205 };
206
207 if !self.should_requote(mid) && has_resting {
208 return Ok(());
209 }
210
211 log::info!(
212 "Requoting grid: mid={mid}, last_mid={:?}, instrument={instrument_id}",
213 self.last_quoted_mid,
214 );
215
216 if self.config.on_cancel_resubmit {
217 let inst = Some(&instrument_id);
218 let strategy = Some(&strategy_id);
219 let ids: Vec<ClientOrderId> = {
220 let cache = self.cache();
221 let open = cache.orders_open(None, inst, strategy, None, None);
222 let inflight = cache.orders_inflight(None, inst, strategy, None, None);
223 open.iter()
224 .chain(inflight.iter())
225 .map(|o| o.client_order_id())
226 .collect()
227 };
228 self.pending_self_cancels.extend(ids);
229 }
230
231 self.cancel_all_orders(instrument_id, None, None, None)?;
232
233 let (net_position, worst_long, worst_short) = {
236 let instrument_id = Some(&instrument_id);
237 let strategy = Some(&strategy_id);
238 let cache = self.cache();
239
240 let mut position_qty = 0.0_f64;
241 let mut position_dec = Decimal::ZERO;
242
243 for p in cache.positions_open(None, instrument_id, strategy, None, None) {
244 position_qty += p.signed_qty;
245 position_dec += p.quantity.as_decimal()
246 * if p.signed_qty < 0.0 {
247 Decimal::NEGATIVE_ONE
248 } else {
249 Decimal::ONE
250 };
251 }
252
253 let mut pending_buy_dec = Decimal::ZERO;
254 let mut pending_sell_dec = Decimal::ZERO;
255 let mut seen = AHashSet::new();
256
257 let open = cache.orders_open(None, instrument_id, strategy, None, None);
259 let inflight = cache.orders_inflight(None, instrument_id, strategy, None, None);
260 for order in open.iter().chain(inflight.iter()) {
261 if !seen.insert(order.client_order_id()) {
262 continue;
263 }
264 let qty = order.leaves_qty().as_decimal();
265 match order.order_side() {
266 OrderSide::Buy => pending_buy_dec += qty,
267 _ => pending_sell_dec += qty,
268 }
269 }
270
271 (
272 position_qty,
273 position_dec + pending_buy_dec,
274 position_dec - pending_sell_dec,
275 )
276 };
277
278 let grid = self.grid_orders(mid, net_position, worst_long, worst_short)?;
279
280 if grid.is_empty() {
283 return Ok(());
284 }
285
286 let trade_size = self
287 .trade_size
288 .ok_or_else(|| anyhow::anyhow!("Cannot handle quote: trade_size is not resolved"))?;
289
290 let (tif, expire_time) = match self.config.expire_time_secs {
291 Some(secs) => {
292 let now_ns = self.clock().timestamp_ns();
293 let expire_ns = now_ns + secs * 1_000_000_000;
294 (Some(TimeInForce::Gtd), Some(expire_ns))
295 }
296 None => (None, None),
297 };
298
299 for (side, price) in grid {
300 let order = self.order().limit(
301 instrument_id,
302 side,
303 trade_size,
304 price,
305 tif,
306 expire_time,
307 Some(true), None,
309 None,
310 None,
311 None,
312 None,
313 None,
314 None,
315 None,
316 None,
317 );
318 self.submit_order(order, None, None, None)?;
319 }
320
321 self.last_quoted_mid = Some(mid);
322 Ok(())
323 }
324
325 fn on_order_filled(&mut self, event: &OrderFilled) -> anyhow::Result<()> {
326 let closed = {
329 let cache = self.cache();
330 cache
331 .order(&event.client_order_id)
332 .is_some_and(|o| o.is_closed())
333 };
334
335 if closed {
336 self.pending_self_cancels.remove(&event.client_order_id);
337 }
338 Ok(())
339 }
340
341 fn on_order_canceled(&mut self, event: &OrderCanceled) -> anyhow::Result<()> {
342 if self.pending_self_cancels.remove(&event.client_order_id) {
343 return Ok(());
344 }
345
346 if self.config.on_cancel_resubmit {
347 self.last_quoted_mid = None;
349 }
350 Ok(())
351 }
352
353 fn on_reset(&mut self) -> anyhow::Result<()> {
354 self.instrument = None;
355 self.trade_size = self.config.trade_size;
356 self.price_precision = None;
357 self.last_quoted_mid = None;
358 self.pending_self_cancels.clear();
359 Ok(())
360 }
361}