nautilus_execution/matching_engine/
ids_generator.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{cell::RefCell, fmt::Debug, rc::Rc};
17
18use nautilus_common::cache::Cache;
19use nautilus_model::{
20    enums::OmsType,
21    identifiers::{PositionId, TradeId, Venue, VenueOrderId},
22    orders::{Order, OrderAny},
23};
24use uuid::Uuid;
25
26pub struct IdsGenerator {
27    venue: Venue,
28    raw_id: u32,
29    oms_type: OmsType,
30    use_random_ids: bool,
31    use_position_ids: bool,
32    cache: Rc<RefCell<Cache>>,
33    position_count: usize,
34    order_count: usize,
35    execution_count: usize,
36}
37
38impl Debug for IdsGenerator {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct(stringify!(IdsGenerator))
41            .field("venue", &self.venue)
42            .field("raw_id", &self.raw_id)
43            .finish()
44    }
45}
46
47impl IdsGenerator {
48    pub const fn new(
49        venue: Venue,
50        oms_type: OmsType,
51        raw_id: u32,
52        use_random_ids: bool,
53        use_position_ids: bool,
54        cache: Rc<RefCell<Cache>>,
55    ) -> Self {
56        Self {
57            venue,
58            raw_id,
59            oms_type,
60            cache,
61            use_random_ids,
62            use_position_ids,
63            position_count: 0,
64            order_count: 0,
65            execution_count: 0,
66        }
67    }
68
69    pub const fn reset(&mut self) {
70        self.position_count = 0;
71        self.order_count = 0;
72        self.execution_count = 0;
73    }
74
75    /// Retrieves or generates a unique venue order ID for the given order.
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if ID generation fails.
80    pub fn get_venue_order_id(&mut self, order: &OrderAny) -> anyhow::Result<VenueOrderId> {
81        // check existing on order
82        if let Some(venue_order_id) = order.venue_order_id() {
83            return Ok(venue_order_id);
84        }
85
86        // check existing in cache
87        if let Some(venue_order_id) = self.cache.borrow().venue_order_id(&order.client_order_id()) {
88            return Ok(venue_order_id.to_owned());
89        }
90
91        let venue_order_id = self.generate_venue_order_id();
92        self.cache.borrow_mut().add_venue_order_id(
93            &order.client_order_id(),
94            &venue_order_id,
95            false,
96        )?;
97        Ok(venue_order_id)
98    }
99
100    /// Retrieves or generates a position ID for the given order.
101    ///
102    /// # Panics
103    ///
104    /// Panics if `generate` is `Some(true)` but no cached position ID is available.
105    pub fn get_position_id(
106        &mut self,
107        order: &OrderAny,
108        generate: Option<bool>,
109    ) -> Option<PositionId> {
110        let generate = generate.unwrap_or(true);
111        if self.oms_type == OmsType::Hedging {
112            {
113                let cache = self.cache.as_ref().borrow();
114                let position_id_result = cache.position_id(&order.client_order_id());
115                if let Some(position_id) = position_id_result {
116                    return Some(position_id.to_owned());
117                }
118            }
119            if generate {
120                self.generate_venue_position_id()
121            } else {
122                panic!(
123                    "Position id should be generated. Hedging Oms type order matching engine doesn't exist in cache."
124                )
125            }
126        } else {
127            // Netting OMS (position id will be derived from instrument and strategy)
128            let cache = self.cache.as_ref().borrow();
129            let positions_open =
130                cache.positions_open(None, Some(&order.instrument_id()), None, None);
131            if positions_open.is_empty() {
132                None
133            } else {
134                Some(positions_open[0].id)
135            }
136        }
137    }
138
139    pub fn generate_trade_id(&mut self) -> TradeId {
140        self.execution_count += 1;
141        let trade_id = if self.use_random_ids {
142            Uuid::new_v4().to_string()
143        } else {
144            format!("{}-{}-{}", self.venue, self.raw_id, self.execution_count)
145        };
146        TradeId::from(trade_id.as_str())
147    }
148
149    pub fn generate_venue_position_id(&mut self) -> Option<PositionId> {
150        if !self.use_position_ids {
151            return None;
152        }
153
154        self.position_count += 1;
155        if self.use_random_ids {
156            Some(PositionId::new(Uuid::new_v4().to_string()))
157        } else {
158            Some(PositionId::new(
159                format!("{}-{}-{}", self.venue, self.raw_id, self.position_count).as_str(),
160            ))
161        }
162    }
163
164    pub fn generate_venue_order_id(&mut self) -> VenueOrderId {
165        self.order_count += 1;
166        if self.use_random_ids {
167            VenueOrderId::new(Uuid::new_v4().to_string())
168        } else {
169            VenueOrderId::new(
170                format!("{}-{}-{}", self.venue, self.raw_id, self.order_count).as_str(),
171            )
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use std::{cell::RefCell, rc::Rc};
179
180    use nautilus_common::cache::Cache;
181    use nautilus_core::{UUID4, UnixNanos};
182    use nautilus_model::{
183        enums::{LiquiditySide, OmsType, OrderSide, OrderType},
184        events::OrderFilled,
185        identifiers::{
186            AccountId, ClientOrderId, PositionId, TradeId, Venue, VenueOrderId, stubs::account_id,
187        },
188        instruments::{
189            CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
190        },
191        orders::{Order, OrderAny, OrderTestBuilder},
192        position::Position,
193        types::{Price, Quantity},
194    };
195    use rstest::{fixture, rstest};
196
197    use crate::matching_engine::ids_generator::IdsGenerator;
198
199    #[fixture]
200    fn instrument_eth_usdt(crypto_perpetual_ethusdt: CryptoPerpetual) -> InstrumentAny {
201        InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt)
202    }
203
204    #[fixture]
205    fn market_order_buy(instrument_eth_usdt: InstrumentAny) -> OrderAny {
206        OrderTestBuilder::new(OrderType::Market)
207            .instrument_id(instrument_eth_usdt.id())
208            .side(OrderSide::Buy)
209            .quantity(Quantity::from("1.000"))
210            .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
211            .submit(true)
212            .build()
213    }
214
215    #[fixture]
216    fn market_order_sell(instrument_eth_usdt: InstrumentAny) -> OrderAny {
217        OrderTestBuilder::new(OrderType::Market)
218            .instrument_id(instrument_eth_usdt.id())
219            .side(OrderSide::Sell)
220            .quantity(Quantity::from("1.000"))
221            .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
222            .submit(true)
223            .build()
224    }
225
226    #[fixture]
227    fn market_order_fill(
228        instrument_eth_usdt: InstrumentAny,
229        account_id: AccountId,
230        market_order_buy: OrderAny,
231    ) -> OrderFilled {
232        OrderFilled::new(
233            market_order_buy.trader_id(),
234            market_order_buy.strategy_id(),
235            market_order_buy.instrument_id(),
236            market_order_buy.client_order_id(),
237            VenueOrderId::new("BINANCE-1"),
238            account_id,
239            TradeId::new("1"),
240            market_order_buy.order_side(),
241            market_order_buy.order_type(),
242            Quantity::from("1"),
243            Price::from("1000.000"),
244            instrument_eth_usdt.quote_currency(),
245            LiquiditySide::Taker,
246            UUID4::new(),
247            UnixNanos::default(),
248            UnixNanos::default(),
249            false,
250            Some(PositionId::new("P-1")),
251            None,
252        )
253    }
254
255    fn get_ids_generator(
256        cache: Rc<RefCell<Cache>>,
257        use_position_ids: bool,
258        oms_type: OmsType,
259    ) -> IdsGenerator {
260        IdsGenerator::new(
261            Venue::from("BINANCE"),
262            oms_type,
263            1,
264            false,
265            use_position_ids,
266            cache,
267        )
268    }
269
270    #[rstest]
271    fn test_get_position_id_hedging_with_existing_position(
272        instrument_eth_usdt: InstrumentAny,
273        market_order_buy: OrderAny,
274        market_order_fill: OrderFilled,
275    ) {
276        let cache = Rc::new(RefCell::new(Cache::default()));
277        let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Hedging);
278
279        let position = Position::new(&instrument_eth_usdt, market_order_fill);
280
281        // Add position to cache
282        cache
283            .borrow_mut()
284            .add_position(position.clone(), OmsType::Hedging)
285            .unwrap();
286
287        let position_id = ids_generator.get_position_id(&market_order_buy, None);
288        assert_eq!(position_id, Some(position.id));
289    }
290
291    #[rstest]
292    fn test_get_position_id_hedging_with_generated_position(market_order_buy: OrderAny) {
293        let cache = Rc::new(RefCell::new(Cache::default()));
294        let mut ids_generator = get_ids_generator(cache, true, OmsType::Hedging);
295
296        let position_id = ids_generator.get_position_id(&market_order_buy, None);
297        assert_eq!(position_id, Some(PositionId::new("BINANCE-1-1")));
298    }
299
300    #[rstest]
301    fn test_get_position_id_netting(
302        instrument_eth_usdt: InstrumentAny,
303        market_order_buy: OrderAny,
304        market_order_fill: OrderFilled,
305    ) {
306        let cache = Rc::new(RefCell::new(Cache::default()));
307        let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Netting);
308
309        // position id should be none in non-initialized position id for this instrument
310        let position_id = ids_generator.get_position_id(&market_order_buy, None);
311        assert_eq!(position_id, None);
312
313        // create and add position in cache
314        let position = Position::new(&instrument_eth_usdt, market_order_fill);
315        cache
316            .as_ref()
317            .borrow_mut()
318            .add_position(position.clone(), OmsType::Netting)
319            .unwrap();
320
321        // position id should be returned for the existing position
322        let position_id = ids_generator.get_position_id(&market_order_buy, None);
323        assert_eq!(position_id, Some(position.id));
324    }
325
326    #[rstest]
327    fn test_generate_venue_position_id() {
328        let cache = Rc::new(RefCell::new(Cache::default()));
329        let mut ids_generator_with_position_ids =
330            get_ids_generator(cache.clone(), true, OmsType::Netting);
331        let mut ids_generator_no_position_ids = get_ids_generator(cache, false, OmsType::Netting);
332
333        assert_eq!(
334            ids_generator_no_position_ids.generate_venue_position_id(),
335            None
336        );
337
338        let position_id_1 = ids_generator_with_position_ids.generate_venue_position_id();
339        let position_id_2 = ids_generator_with_position_ids.generate_venue_position_id();
340        assert_eq!(position_id_1, Some(PositionId::new("BINANCE-1-1")));
341        assert_eq!(position_id_2, Some(PositionId::new("BINANCE-1-2")));
342    }
343
344    #[rstest]
345    fn get_venue_position_id(market_order_buy: OrderAny, market_order_sell: OrderAny) {
346        let cache = Rc::new(RefCell::new(Cache::default()));
347        let mut ids_generator = get_ids_generator(cache, true, OmsType::Netting);
348
349        let venue_order_id1 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
350        let venue_order_id2 = ids_generator
351            .get_venue_order_id(&market_order_sell)
352            .unwrap();
353        assert_eq!(venue_order_id1, VenueOrderId::from("BINANCE-1-1"));
354        assert_eq!(venue_order_id2, VenueOrderId::from("BINANCE-1-2"));
355
356        // check if venue order id is cached again
357        let venue_order_id3 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
358        assert_eq!(venue_order_id3, VenueOrderId::from("BINANCE-1-1"));
359    }
360}