nautilus_execution/matching_engine/
ids_generator.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 doesnt exists 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_model::{
182        enums::OmsType,
183        events::OrderFilled,
184        identifiers::{PositionId, Venue, VenueOrderId},
185        instruments::InstrumentAny,
186        orders::OrderAny,
187        position::Position,
188    };
189    use rstest::rstest;
190
191    use crate::matching_engine::{
192        ids_generator::IdsGenerator,
193        tests::{instrument_eth_usdt, market_order_buy, market_order_fill, market_order_sell},
194    };
195
196    fn get_ids_generator(
197        cache: Rc<RefCell<Cache>>,
198        use_position_ids: bool,
199        oms_type: OmsType,
200    ) -> IdsGenerator {
201        IdsGenerator::new(
202            Venue::from("BINANCE"),
203            oms_type,
204            1,
205            false,
206            use_position_ids,
207            cache,
208        )
209    }
210
211    #[rstest]
212    fn test_get_position_id_hedging_with_existing_position(
213        instrument_eth_usdt: InstrumentAny,
214        market_order_buy: OrderAny,
215        market_order_fill: OrderFilled,
216    ) {
217        let cache = Rc::new(RefCell::new(Cache::default()));
218        let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Hedging);
219
220        let position = Position::new(&instrument_eth_usdt, market_order_fill);
221
222        // Add position to cache
223        cache
224            .borrow_mut()
225            .add_position(position.clone(), OmsType::Hedging)
226            .unwrap();
227
228        let position_id = ids_generator.get_position_id(&market_order_buy, None);
229        assert_eq!(position_id, Some(position.id));
230    }
231
232    #[rstest]
233    fn test_get_position_id_hedging_with_generated_position(market_order_buy: OrderAny) {
234        let cache = Rc::new(RefCell::new(Cache::default()));
235        let mut ids_generator = get_ids_generator(cache, true, OmsType::Hedging);
236
237        let position_id = ids_generator.get_position_id(&market_order_buy, None);
238        assert_eq!(position_id, Some(PositionId::new("BINANCE-1-1")));
239    }
240
241    #[rstest]
242    fn test_get_position_id_netting(
243        instrument_eth_usdt: InstrumentAny,
244        market_order_buy: OrderAny,
245        market_order_fill: OrderFilled,
246    ) {
247        let cache = Rc::new(RefCell::new(Cache::default()));
248        let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Netting);
249
250        // position id should be none in non-initialized position id for this instrument
251        let position_id = ids_generator.get_position_id(&market_order_buy, None);
252        assert_eq!(position_id, None);
253
254        // create and add position in cache
255        let position = Position::new(&instrument_eth_usdt, market_order_fill);
256        cache
257            .as_ref()
258            .borrow_mut()
259            .add_position(position.clone(), OmsType::Netting)
260            .unwrap();
261
262        // position id should be returned for the existing position
263        let position_id = ids_generator.get_position_id(&market_order_buy, None);
264        assert_eq!(position_id, Some(position.id));
265    }
266
267    #[rstest]
268    fn test_generate_venue_position_id() {
269        let cache = Rc::new(RefCell::new(Cache::default()));
270        let mut ids_generator_with_position_ids =
271            get_ids_generator(cache.clone(), true, OmsType::Netting);
272        let mut ids_generator_no_position_ids = get_ids_generator(cache, false, OmsType::Netting);
273
274        assert_eq!(
275            ids_generator_no_position_ids.generate_venue_position_id(),
276            None
277        );
278
279        let position_id_1 = ids_generator_with_position_ids.generate_venue_position_id();
280        let position_id_2 = ids_generator_with_position_ids.generate_venue_position_id();
281        assert_eq!(position_id_1, Some(PositionId::new("BINANCE-1-1")));
282        assert_eq!(position_id_2, Some(PositionId::new("BINANCE-1-2")));
283    }
284
285    #[rstest]
286    fn get_venue_position_id(market_order_buy: OrderAny, market_order_sell: OrderAny) {
287        let cache = Rc::new(RefCell::new(Cache::default()));
288        let mut ids_generator = get_ids_generator(cache, true, OmsType::Netting);
289
290        let venue_order_id1 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
291        let venue_order_id2 = ids_generator
292            .get_venue_order_id(&market_order_sell)
293            .unwrap();
294        assert_eq!(venue_order_id1, VenueOrderId::from("BINANCE-1-1"));
295        assert_eq!(venue_order_id2, VenueOrderId::from("BINANCE-1-2"));
296
297        // check if venue order id is cached again
298        let venue_order_id3 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
299        assert_eq!(venue_order_id3, VenueOrderId::from("BINANCE-1-1"));
300    }
301}