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