1use std::{cell::RefCell, fmt::Debug, rc::Rc};
17
18use nautilus_common::cache::Cache;
19use nautilus_core::{UUID4, UnixNanos};
20use nautilus_model::{
21 enums::OmsType,
22 identifiers::{PositionId, TradeId, Venue, VenueOrderId},
23 orders::{Order, OrderAny},
24};
25
26const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
28const FNV_PRIME: u64 = 0x0100_0000_01b3;
29
30pub struct IdsGenerator {
31 venue: Venue,
32 raw_id: u32,
33 oms_type: OmsType,
34 use_random_ids: bool,
35 use_position_ids: bool,
36 cache: Rc<RefCell<Cache>>,
37 position_count: usize,
38 order_count: usize,
39 execution_count: usize,
40}
41
42impl Debug for IdsGenerator {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct(stringify!(IdsGenerator))
45 .field("venue", &self.venue)
46 .field("raw_id", &self.raw_id)
47 .finish()
48 }
49}
50
51impl IdsGenerator {
52 pub const fn new(
53 venue: Venue,
54 oms_type: OmsType,
55 raw_id: u32,
56 use_random_ids: bool,
57 use_position_ids: bool,
58 cache: Rc<RefCell<Cache>>,
59 ) -> Self {
60 Self {
61 venue,
62 raw_id,
63 oms_type,
64 cache,
65 use_random_ids,
66 use_position_ids,
67 position_count: 0,
68 order_count: 0,
69 execution_count: 0,
70 }
71 }
72
73 pub const fn reset(&mut self) {
74 self.position_count = 0;
75 self.order_count = 0;
76 self.execution_count = 0;
77 }
78
79 pub fn get_venue_order_id(&mut self, order: &OrderAny) -> anyhow::Result<VenueOrderId> {
85 if let Some(venue_order_id) = order.venue_order_id() {
87 return Ok(venue_order_id);
88 }
89
90 if let Some(venue_order_id) = self.cache.borrow().venue_order_id(&order.client_order_id()) {
92 return Ok(venue_order_id.to_owned());
93 }
94
95 let venue_order_id = self.generate_venue_order_id();
96 self.cache.borrow_mut().add_venue_order_id(
97 &order.client_order_id(),
98 &venue_order_id,
99 false,
100 )?;
101 Ok(venue_order_id)
102 }
103
104 pub fn get_position_id(
110 &mut self,
111 order: &OrderAny,
112 generate: Option<bool>,
113 ) -> Option<PositionId> {
114 let generate = generate.unwrap_or(true);
115
116 if self.oms_type == OmsType::Hedging {
117 {
118 let cache = self.cache.as_ref().borrow();
119 let position_id_result = cache.position_id(&order.client_order_id());
120 if let Some(position_id) = position_id_result {
121 return Some(position_id.to_owned());
122 }
123 }
124
125 if generate {
126 self.generate_venue_position_id()
127 } else {
128 panic!(
129 "Position id should be generated. Hedging Oms type order matching engine doesn't exist in cache."
130 )
131 }
132 } else {
133 let cache = self.cache.as_ref().borrow();
135 let positions_open = cache.positions_open(
136 None,
137 Some(&order.instrument_id()),
138 Some(&order.strategy_id()),
139 None,
140 None,
141 );
142 positions_open.first().map(|position| position.id)
143 }
144 }
145
146 pub fn generate_trade_id(&mut self, ts_init: UnixNanos) -> TradeId {
147 self.execution_count += 1;
148 let hash = fnv1a_trade_id_hash(self.venue, self.raw_id, ts_init.as_u64());
155 let trade_id = format!("T-{hash:016x}-{:03}", self.execution_count);
156 TradeId::from(trade_id.as_str())
157 }
158
159 pub fn generate_venue_position_id(&mut self) -> Option<PositionId> {
160 if !self.use_position_ids {
161 return None;
162 }
163
164 self.position_count += 1;
165
166 if self.use_random_ids {
167 Some(PositionId::new(UUID4::new().to_string()))
168 } else {
169 Some(PositionId::new(
170 format!("{}-{}-{}", self.venue, self.raw_id, self.position_count).as_str(),
171 ))
172 }
173 }
174
175 pub fn generate_venue_order_id(&mut self) -> VenueOrderId {
176 self.order_count += 1;
177
178 if self.use_random_ids {
179 VenueOrderId::new(UUID4::new().to_string())
180 } else {
181 VenueOrderId::new(
182 format!("{}-{}-{}", self.venue, self.raw_id, self.order_count).as_str(),
183 )
184 }
185 }
186}
187
188fn fnv1a_trade_id_hash(venue: Venue, raw_id: u32, ts_init_ns: u64) -> u64 {
189 let mut hash: u64 = FNV_OFFSET_BASIS;
190
191 for bytes in [
192 venue.as_str().as_bytes(),
193 b"\x1f",
194 &raw_id.to_le_bytes(),
195 b"\x1f",
196 &ts_init_ns.to_le_bytes(),
197 ] {
198 for &byte in bytes {
199 hash ^= u64::from(byte);
200 hash = hash.wrapping_mul(FNV_PRIME);
201 }
202 }
203 hash
204}
205
206#[cfg(test)]
207mod tests {
208 use std::{cell::RefCell, rc::Rc};
209
210 use nautilus_common::cache::Cache;
211 use nautilus_core::UnixNanos;
212 use nautilus_model::{
213 enums::{OmsType, OrderSide, OrderType},
214 events::{OrderFilled, order::spec::OrderFilledSpec},
215 identifiers::{
216 AccountId, ClientOrderId, PositionId, StrategyId, Venue, VenueOrderId,
217 stubs::account_id,
218 },
219 instruments::{
220 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
221 },
222 orders::{Order, OrderAny, OrderTestBuilder},
223 position::Position,
224 types::{Price, Quantity},
225 };
226 use rstest::{fixture, rstest};
227
228 use crate::matching_engine::ids_generator::IdsGenerator;
229
230 #[fixture]
231 fn instrument_eth_usdt(crypto_perpetual_ethusdt: CryptoPerpetual) -> InstrumentAny {
232 InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt)
233 }
234
235 #[fixture]
236 fn market_order_buy(instrument_eth_usdt: InstrumentAny) -> OrderAny {
237 OrderTestBuilder::new(OrderType::Market)
238 .instrument_id(instrument_eth_usdt.id())
239 .side(OrderSide::Buy)
240 .quantity(Quantity::from("1.000"))
241 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
242 .submit(true)
243 .build()
244 }
245
246 #[fixture]
247 fn market_order_sell(instrument_eth_usdt: InstrumentAny) -> OrderAny {
248 OrderTestBuilder::new(OrderType::Market)
249 .instrument_id(instrument_eth_usdt.id())
250 .side(OrderSide::Sell)
251 .quantity(Quantity::from("1.000"))
252 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
253 .submit(true)
254 .build()
255 }
256
257 #[fixture]
258 fn market_order_fill(
259 instrument_eth_usdt: InstrumentAny,
260 account_id: AccountId,
261 market_order_buy: OrderAny,
262 ) -> OrderFilled {
263 OrderFilledSpec::builder()
264 .trader_id(market_order_buy.trader_id())
265 .strategy_id(market_order_buy.strategy_id())
266 .instrument_id(market_order_buy.instrument_id())
267 .client_order_id(market_order_buy.client_order_id())
268 .venue_order_id(VenueOrderId::new("BINANCE-1"))
269 .account_id(account_id)
270 .last_qty(Quantity::from("1"))
271 .last_px(Price::from("1000.000"))
272 .currency(instrument_eth_usdt.quote_currency())
273 .position_id(PositionId::new("P-1"))
274 .build()
275 }
276
277 fn get_ids_generator(
278 cache: Rc<RefCell<Cache>>,
279 use_position_ids: bool,
280 oms_type: OmsType,
281 ) -> IdsGenerator {
282 IdsGenerator::new(
283 Venue::from("BINANCE"),
284 oms_type,
285 1,
286 false,
287 use_position_ids,
288 cache,
289 )
290 }
291
292 #[rstest]
293 fn test_get_position_id_hedging_with_existing_position(
294 instrument_eth_usdt: InstrumentAny,
295 market_order_buy: OrderAny,
296 market_order_fill: OrderFilled,
297 ) {
298 let cache = Rc::new(RefCell::new(Cache::default()));
299 let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Hedging);
300
301 let position = Position::new(&instrument_eth_usdt, market_order_fill);
302
303 cache
305 .borrow_mut()
306 .add_position(&position, OmsType::Hedging)
307 .unwrap();
308
309 let position_id = ids_generator.get_position_id(&market_order_buy, None);
310 assert_eq!(position_id, Some(position.id));
311 }
312
313 #[rstest]
314 fn test_get_position_id_hedging_with_generated_position(market_order_buy: OrderAny) {
315 let cache = Rc::new(RefCell::new(Cache::default()));
316 let mut ids_generator = get_ids_generator(cache, true, OmsType::Hedging);
317
318 let position_id = ids_generator.get_position_id(&market_order_buy, None);
319 assert_eq!(position_id, Some(PositionId::new("BINANCE-1-1")));
320 }
321
322 #[rstest]
323 fn test_get_position_id_netting(
324 instrument_eth_usdt: InstrumentAny,
325 market_order_buy: OrderAny,
326 market_order_fill: OrderFilled,
327 ) {
328 let cache = Rc::new(RefCell::new(Cache::default()));
329 let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Netting);
330
331 let position_id = ids_generator.get_position_id(&market_order_buy, None);
333 assert_eq!(position_id, None);
334
335 let position = Position::new(&instrument_eth_usdt, market_order_fill);
337 cache
338 .as_ref()
339 .borrow_mut()
340 .add_position(&position, OmsType::Netting)
341 .unwrap();
342
343 let position_id = ids_generator.get_position_id(&market_order_buy, None);
345 assert_eq!(position_id, Some(position.id));
346 }
347
348 #[rstest]
349 fn test_get_position_id_netting_filters_by_strategy(
350 instrument_eth_usdt: InstrumentAny,
351 market_order_fill: OrderFilled,
352 ) {
353 let cache = Rc::new(RefCell::new(Cache::default()));
354 let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Netting);
355 let position = Position::new(&instrument_eth_usdt, market_order_fill);
356 cache
357 .as_ref()
358 .borrow_mut()
359 .add_position(&position, OmsType::Netting)
360 .unwrap();
361
362 let order_for_other_strategy = OrderTestBuilder::new(OrderType::Market)
363 .instrument_id(instrument_eth_usdt.id())
364 .strategy_id(StrategyId::from("S-002"))
365 .side(OrderSide::Buy)
366 .quantity(Quantity::from("1.000"))
367 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-9"))
368 .submit(true)
369 .build();
370
371 let position_id = ids_generator.get_position_id(&order_for_other_strategy, None);
372 assert_eq!(position_id, None);
373 }
374
375 #[rstest]
376 fn test_generate_venue_position_id() {
377 let cache = Rc::new(RefCell::new(Cache::default()));
378 let mut ids_generator_with_position_ids =
379 get_ids_generator(cache.clone(), true, OmsType::Netting);
380 let mut ids_generator_no_position_ids = get_ids_generator(cache, false, OmsType::Netting);
381
382 assert_eq!(
383 ids_generator_no_position_ids.generate_venue_position_id(),
384 None
385 );
386
387 let position_id_1 = ids_generator_with_position_ids.generate_venue_position_id();
388 let position_id_2 = ids_generator_with_position_ids.generate_venue_position_id();
389 assert_eq!(position_id_1, Some(PositionId::new("BINANCE-1-1")));
390 assert_eq!(position_id_2, Some(PositionId::new("BINANCE-1-2")));
391 }
392
393 #[rstest]
394 fn test_generate_venue_position_id_random_uses_uuid4_seam() {
395 let cache = Rc::new(RefCell::new(Cache::default()));
399 let mut generator = IdsGenerator::new(
400 Venue::from("BINANCE"),
401 OmsType::Netting,
402 1,
403 true,
404 true,
405 cache,
406 );
407
408 let id = generator.generate_venue_position_id().expect("position id");
409 let s = id.as_str();
410
411 assert_eq!(s.len(), 36, "expected canonical UUID4 length");
412 assert_eq!(s.as_bytes()[14], b'4', "expected UUID v4 version digit");
413 assert!(
414 matches!(s.as_bytes()[19], b'8' | b'9' | b'a' | b'b'),
415 "expected RFC 4122 variant byte",
416 );
417 }
418
419 #[rstest]
420 fn get_venue_position_id(market_order_buy: OrderAny, market_order_sell: OrderAny) {
421 let cache = Rc::new(RefCell::new(Cache::default()));
422 let mut ids_generator = get_ids_generator(cache, true, OmsType::Netting);
423
424 let venue_order_id1 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
425 let venue_order_id2 = ids_generator
426 .get_venue_order_id(&market_order_sell)
427 .unwrap();
428 assert_eq!(venue_order_id1, VenueOrderId::from("BINANCE-1-1"));
429 assert_eq!(venue_order_id2, VenueOrderId::from("BINANCE-1-2"));
430
431 let venue_order_id3 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
433 assert_eq!(venue_order_id3, VenueOrderId::from("BINANCE-1-1"));
434 }
435
436 fn build_ids_generator(venue: Venue, raw_id: u32) -> IdsGenerator {
437 let cache = Rc::new(RefCell::new(Cache::default()));
438 IdsGenerator::new(venue, OmsType::Netting, raw_id, false, true, cache)
439 }
440
441 #[rstest]
442 fn test_generate_trade_id_format_and_length_bound() {
443 let mut generator =
444 build_ids_generator(Venue::from("SOMETHING_VERY_LONG_FOR_SAFETY"), 4_294_967_295);
445 let ts = UnixNanos::from(u64::MAX);
446
447 let trade_id = generator.generate_trade_id(ts);
448 let value = trade_id.as_str();
449
450 assert!(value.len() <= 36);
451 assert!(value.starts_with("T-"));
452 assert_eq!(value.len(), "T-0123456789abcdef-001".len());
453 }
454
455 #[rstest]
456 fn test_generate_trade_id_is_deterministic_across_reset_for_same_ts() {
457 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
458 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
459
460 let first = generator.generate_trade_id(ts);
461 generator.reset();
462 let second = generator.generate_trade_id(ts);
463 assert_eq!(
464 first, second,
465 "same ts_init and reset execution_count must reproduce the same id"
466 );
467 }
468
469 #[rstest]
470 fn test_generate_trade_id_differs_when_ts_init_changes() {
471 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
472 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
473
474 let first = generator.generate_trade_id(ts);
475 generator.reset();
476 let second = generator.generate_trade_id(ts + UnixNanos::from(1));
477 assert_ne!(
478 first, second,
479 "distinct ts_init must produce distinct ids across a reset"
480 );
481 }
482
483 #[rstest]
484 fn test_generate_trade_id_counter_tiebreaker_for_same_ts() {
485 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
486 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
487
488 let first = generator.generate_trade_id(ts);
489 let second = generator.generate_trade_id(ts);
490 let third = generator.generate_trade_id(ts);
491 assert_ne!(first, second);
492 assert_ne!(second, third);
493 assert!(first.as_str().ends_with("-001"));
494 assert!(second.as_str().ends_with("-002"));
495 assert!(third.as_str().ends_with("-003"));
496 }
497
498 #[rstest]
499 fn test_generate_trade_id_differs_when_venue_or_raw_id_changes() {
500 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
501
502 let mut gen_a = build_ids_generator(Venue::from("BINANCE"), 1);
503 let mut gen_b = build_ids_generator(Venue::from("BYBIT"), 1);
504 let mut gen_c = build_ids_generator(Venue::from("BINANCE"), 2);
505
506 let a = gen_a.generate_trade_id(ts);
507 let b = gen_b.generate_trade_id(ts);
508 let c = gen_c.generate_trade_id(ts);
509 assert_ne!(a, b, "venue must distinguish ids");
510 assert_ne!(a, c, "raw_id must distinguish ids");
511 }
512
513 #[rstest]
517 #[case::zero("BINANCE", 1_u32, 0_u64, "T-59d6cf33c843f0cc-001")]
518 #[case::nanos(
519 "BINANCE",
520 1_u32,
521 1_700_000_000_000_000_000_u64,
522 "T-5c080ffb681dc0d4-001"
523 )]
524 #[case::long_venue(
525 "SOMETHING_VERY_LONG_FOR_SAFETY",
526 42_u32,
527 1_700_000_000_000_000_000_u64,
528 "T-2a2238c5cc0cbaf2-001"
529 )]
530 fn test_generate_trade_id_matches_python_parity_fixture(
531 #[case] venue: &str,
532 #[case] raw_id: u32,
533 #[case] ts_init: u64,
534 #[case] expected: &str,
535 ) {
536 let mut generator = build_ids_generator(Venue::from(venue), raw_id);
537 let trade_id = generator.generate_trade_id(UnixNanos::from(ts_init));
538 assert_eq!(trade_id.as_str(), expected);
539 }
540
541 #[rstest]
546 fn test_generate_trade_id_multi_tick_matches_python_parity_fixture() {
547 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
548 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
549 let sequence: Vec<String> = (0..4)
550 .map(|_| generator.generate_trade_id(ts).as_str().to_string())
551 .collect();
552
553 assert_eq!(
554 sequence,
555 vec![
556 "T-5c080ffb681dc0d4-001".to_string(),
557 "T-5c080ffb681dc0d4-002".to_string(),
558 "T-5c080ffb681dc0d4-003".to_string(),
559 "T-5c080ffb681dc0d4-004".to_string(),
560 ],
561 );
562 }
563}