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, StrategyId},
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 ) -> Vec<(OrderSide, Price)> {
91 let instrument = self
92 .instrument
93 .as_ref()
94 .expect("instrument should be resolved in on_start");
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 trade_size = self
99 .trade_size
100 .expect("trade_size should be resolved in on_start")
101 .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 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
149impl Debug for GridMarketMaker {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct(stringify!(GridMarketMaker))
152 .field("config", &self.config)
153 .field("trade_size", &self.trade_size)
154 .finish()
155 }
156}
157
158impl DataActor for GridMarketMaker {
159 fn on_start(&mut self) -> anyhow::Result<()> {
160 let instrument_id = self.config.instrument_id;
161 let (instrument, size_precision, min_quantity) = {
162 let cache = self.cache();
163 let instrument = cache
164 .instrument(&instrument_id)
165 .ok_or_else(|| anyhow::anyhow!("Instrument {instrument_id} not found in cache"))?;
166 (
167 instrument.clone(),
168 instrument.size_precision(),
169 instrument.min_quantity(),
170 )
171 };
172 self.price_precision = Some(instrument.price_precision());
173 self.instrument = Some(instrument);
174
175 if self.trade_size.is_none() {
177 self.trade_size =
178 Some(min_quantity.unwrap_or_else(|| Quantity::new(1.0, size_precision)));
179 }
180
181 self.subscribe_quotes(instrument_id, None, None);
182 Ok(())
183 }
184
185 fn on_stop(&mut self) -> anyhow::Result<()> {
186 let instrument_id = self.config.instrument_id;
187 self.cancel_all_orders(instrument_id, None, None)?;
188 self.close_all_positions(instrument_id, None, None, None, None, None, None)?;
189 self.unsubscribe_quotes(instrument_id, None, None);
190 Ok(())
191 }
192
193 fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
194 let mid_f64 = (quote.bid_price.as_f64() + quote.ask_price.as_f64()) / 2.0;
196 let mid = Price::new(
197 mid_f64,
198 self.price_precision
199 .expect("price_precision should be resolved in on_start"),
200 );
201
202 let instrument_id = self.config.instrument_id;
203 let strategy_id = StrategyId::from(self.actor_id.inner().as_str());
204
205 let has_resting = {
207 let cache = self.cache();
208 let inst = Some(&instrument_id);
209 let sid = Some(&strategy_id);
210 !cache.orders_open(None, inst, sid, None, None).is_empty()
211 || !cache
212 .orders_inflight(None, inst, sid, None, None)
213 .is_empty()
214 };
215
216 if !self.should_requote(mid) && has_resting {
217 return Ok(());
218 }
219
220 log::info!(
221 "Requoting grid: mid={mid}, last_mid={:?}, instrument={instrument_id}",
222 self.last_quoted_mid,
223 );
224
225 if self.config.on_cancel_resubmit {
226 let inst = Some(&instrument_id);
227 let strategy = Some(&strategy_id);
228 let ids: Vec<ClientOrderId> = {
229 let cache = self.cache();
230 cache
231 .orders_open(None, inst, strategy, None, None)
232 .iter()
233 .chain(
234 cache
235 .orders_inflight(None, inst, strategy, None, None)
236 .iter(),
237 )
238 .map(|o| o.client_order_id())
239 .collect()
240 };
241 self.pending_self_cancels.extend(ids);
242 }
243
244 self.cancel_all_orders(instrument_id, None, None)?;
245
246 let (net_position, worst_long, worst_short) = {
249 let instrument_id = Some(&instrument_id);
250 let strategy = Some(&strategy_id);
251 let cache = self.cache();
252
253 let mut position_qty = 0.0_f64;
254 let mut position_dec = Decimal::ZERO;
255
256 for p in cache.positions_open(None, instrument_id, strategy, None, None) {
257 position_qty += p.signed_qty;
258 position_dec += p.quantity.as_decimal()
259 * if p.signed_qty < 0.0 {
260 Decimal::NEGATIVE_ONE
261 } else {
262 Decimal::ONE
263 };
264 }
265
266 let mut pending_buy_dec = Decimal::ZERO;
267 let mut pending_sell_dec = Decimal::ZERO;
268 let mut seen = AHashSet::new();
269
270 for order in cache
272 .orders_open(None, instrument_id, strategy, None, None)
273 .iter()
274 .chain(
275 cache
276 .orders_inflight(None, instrument_id, strategy, None, None)
277 .iter(),
278 )
279 {
280 if !seen.insert(order.client_order_id()) {
281 continue;
282 }
283 let qty = order.leaves_qty().as_decimal();
284 match order.order_side() {
285 OrderSide::Buy => pending_buy_dec += qty,
286 _ => pending_sell_dec += qty,
287 }
288 }
289
290 (
291 position_qty,
292 position_dec + pending_buy_dec,
293 position_dec - pending_sell_dec,
294 )
295 };
296
297 let grid = self.grid_orders(mid, net_position, worst_long, worst_short);
298
299 if grid.is_empty() {
302 return Ok(());
303 }
304
305 let trade_size = self
306 .trade_size
307 .expect("trade_size should be resolved in on_start");
308
309 let (tif, expire_time) = match self.config.expire_time_secs {
310 Some(secs) => {
311 let now_ns = self.core.clock().timestamp_ns();
312 let expire_ns = now_ns + secs * 1_000_000_000;
313 (Some(TimeInForce::Gtd), Some(expire_ns))
314 }
315 None => (None, None),
316 };
317
318 for (side, price) in grid {
319 let order = self.core.order_factory().limit(
320 instrument_id,
321 side,
322 trade_size,
323 price,
324 tif,
325 expire_time,
326 Some(true), None,
328 None,
329 None,
330 None,
331 None,
332 None,
333 None,
334 None,
335 None,
336 );
337 self.submit_order(order, None, None)?;
338 }
339
340 self.last_quoted_mid = Some(mid);
341 Ok(())
342 }
343
344 fn on_order_filled(&mut self, event: &OrderFilled) -> anyhow::Result<()> {
345 let closed = {
348 let cache = self.cache();
349 cache
350 .order(&event.client_order_id)
351 .is_some_and(|o| o.is_closed())
352 };
353
354 if closed {
355 self.pending_self_cancels.remove(&event.client_order_id);
356 }
357 Ok(())
358 }
359
360 fn on_order_canceled(&mut self, event: &OrderCanceled) -> anyhow::Result<()> {
361 if self.pending_self_cancels.remove(&event.client_order_id) {
362 return Ok(());
363 }
364
365 if self.config.on_cancel_resubmit {
366 self.last_quoted_mid = None;
368 }
369 Ok(())
370 }
371
372 fn on_reset(&mut self) -> anyhow::Result<()> {
373 self.instrument = None;
374 self.trade_size = self.config.trade_size;
375 self.price_precision = None;
376 self.last_quoted_mid = None;
377 self.pending_self_cancels.clear();
378 Ok(())
379 }
380}