1use std::{cell::RefCell, fmt::Debug, rc::Rc};
19
20use nautilus_common::cache::{Cache, VenueOrderIdOwnershipError};
21use nautilus_core::{UUID4, UnixNanos};
22use nautilus_model::{
23 enums::OmsType,
24 identifiers::{PositionId, TradeId, Venue, VenueOrderId},
25 orders::{Order, OrderAny},
26};
27
28const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
30const FNV_PRIME: u64 = 0x0100_0000_01b3;
31const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
32const TRADE_ID_COUNTER_START: usize = 19;
33
34pub struct IdsGenerator {
36 venue: Venue,
37 raw_id: u32,
38 oms_type: OmsType,
39 use_random_ids: bool,
40 use_position_ids: bool,
41 cache: Rc<RefCell<Cache>>,
42 position_count: usize,
43 order_count: usize,
44 execution_count: usize,
45}
46
47impl Debug for IdsGenerator {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct(stringify!(IdsGenerator))
50 .field("venue", &self.venue)
51 .field("raw_id", &self.raw_id)
52 .finish()
53 }
54}
55
56impl IdsGenerator {
57 pub const fn new(
59 venue: Venue,
60 oms_type: OmsType,
61 raw_id: u32,
62 use_random_ids: bool,
63 use_position_ids: bool,
64 cache: Rc<RefCell<Cache>>,
65 ) -> Self {
66 Self {
67 venue,
68 raw_id,
69 oms_type,
70 cache,
71 use_random_ids,
72 use_position_ids,
73 position_count: 0,
74 order_count: 0,
75 execution_count: 0,
76 }
77 }
78
79 pub const fn reset(&mut self) {
81 self.position_count = 0;
82 self.order_count = 0;
83 self.execution_count = 0;
84 }
85
86 pub fn get_venue_order_id(&mut self, order: &OrderAny) -> anyhow::Result<VenueOrderId> {
92 if let Some(venue_order_id) = order.venue_order_id() {
94 return Ok(venue_order_id);
95 }
96
97 if let Some(venue_order_id) = self.cache.borrow().venue_order_id(&order.client_order_id()) {
99 return Ok(venue_order_id.to_owned());
100 }
101
102 let client_order_id = order.client_order_id();
103 let mut conflict_count = 0_usize;
104
105 loop {
106 let venue_order_id = self.try_generate_venue_order_id()?;
107 let claim_result = self.cache.borrow_mut().add_venue_order_id(
108 &client_order_id,
109 &venue_order_id,
110 false,
111 );
112
113 match claim_result {
114 Ok(()) => {
115 if conflict_count > 0 {
116 log::info!(
117 "Allocated venue order ID {venue_order_id} for {client_order_id} after \
118 probing past {conflict_count} ownership conflicts"
119 );
120 }
121 return Ok(venue_order_id);
122 }
123 Err(e) => {
124 let Some(conflict) = e.downcast_ref::<VenueOrderIdOwnershipError>() else {
125 return Err(e);
126 };
127
128 if conflict_count == 0 {
129 log::error!(
130 "Generated venue order ID conflict: candidate={}, existing_owner={}, \
131 claimant={}",
132 conflict.venue_order_id,
133 conflict.existing_client_order_id,
134 conflict.claimant_client_order_id,
135 );
136 }
137 conflict_count += 1;
138 }
139 }
140 }
141 }
142
143 pub fn get_position_id(
150 &mut self,
151 order: &OrderAny,
152 generate: Option<bool>,
153 ) -> Option<PositionId> {
154 let generate = generate.unwrap_or(true);
155
156 if self.oms_type == OmsType::Hedging {
157 {
158 let cache = self.cache.as_ref().borrow();
159 let position_id_result = cache.position_id(&order.client_order_id());
160 if let Some(position_id) = position_id_result {
161 return Some(position_id.to_owned());
162 }
163 }
164
165 if generate {
166 self.generate_venue_position_id()
167 } else {
168 panic!(
169 "Position id should be generated. Hedging Oms type order matching engine doesn't exist in cache."
170 )
171 }
172 } else {
173 let cache = self.cache.as_ref().borrow();
175 let positions_open = cache.positions_open(
176 None,
177 Some(&order.instrument_id()),
178 Some(&order.strategy_id()),
179 None,
180 None,
181 );
182 positions_open.first().map(|position| position.id)
183 }
184 }
185
186 pub fn generate_trade_id(&mut self, ts_init: UnixNanos) -> TradeId {
193 self.execution_count += 1;
194
195 let hash = fnv1a_trade_id_hash(self.venue, self.raw_id, ts_init.as_u64());
202 let mut value = [0_u8; TRADE_ID_COUNTER_START + usize::BITS as usize];
203 value[..2].copy_from_slice(b"T-");
204
205 for (index, byte) in value[2..18].iter_mut().enumerate() {
206 let shift = (15 - index) * 4;
207 *byte = HEX_DIGITS[((hash >> shift) & 0x0f) as usize];
208 }
209
210 value[18] = b'-';
211
212 let mut counter = self.execution_count;
213 let mut digit_start = value.len();
214
215 loop {
216 digit_start -= 1;
217 value[digit_start] = b'0' + (counter % 10) as u8;
218 counter /= 10;
219 if counter == 0 {
220 break;
221 }
222 }
223
224 let digit_count = value.len() - digit_start;
225 let padding = 3_usize.saturating_sub(digit_count);
226 let value_len = TRADE_ID_COUNTER_START + padding + digit_count;
227 value[TRADE_ID_COUNTER_START..TRADE_ID_COUNTER_START + padding].fill(b'0');
228 value.copy_within(digit_start.., TRADE_ID_COUNTER_START + padding);
229
230 let value =
231 std::str::from_utf8(&value[..value_len]).expect("trade ID bytes should be valid ASCII");
232 TradeId::from(value)
233 }
234
235 pub fn generate_venue_position_id(&mut self) -> Option<PositionId> {
237 if !self.use_position_ids {
238 return None;
239 }
240
241 self.position_count += 1;
242
243 if self.use_random_ids {
244 Some(PositionId::new(UUID4::new().to_string()))
245 } else {
246 Some(PositionId::new(
247 format!("{}-{}-{}", self.venue, self.raw_id, self.position_count).as_str(),
248 ))
249 }
250 }
251
252 pub fn generate_venue_order_id(&mut self) -> VenueOrderId {
258 self.try_generate_venue_order_id()
259 .expect("Venue order ID counter exhausted")
260 }
261
262 fn try_generate_venue_order_id(&mut self) -> anyhow::Result<VenueOrderId> {
263 self.order_count = self
264 .order_count
265 .checked_add(1)
266 .ok_or_else(|| anyhow::anyhow!("Venue order ID counter exhausted"))?;
267
268 if self.use_random_ids {
269 Ok(VenueOrderId::new(UUID4::new().to_string()))
270 } else {
271 Ok(VenueOrderId::new(
272 format!("{}-{}-{}", self.venue, self.raw_id, self.order_count).as_str(),
273 ))
274 }
275 }
276}
277
278fn fnv1a_trade_id_hash(venue: Venue, raw_id: u32, ts_init_ns: u64) -> u64 {
279 let mut hash: u64 = FNV_OFFSET_BASIS;
280
281 for bytes in [
282 venue.as_str().as_bytes(),
283 b"\x1f",
284 &raw_id.to_le_bytes(),
285 b"\x1f",
286 &ts_init_ns.to_le_bytes(),
287 ] {
288 for &byte in bytes {
289 hash ^= u64::from(byte);
290 hash = hash.wrapping_mul(FNV_PRIME);
291 }
292 }
293 hash
294}
295
296#[cfg(test)]
297mod tests {
298 use std::{cell::RefCell, rc::Rc};
299
300 use nautilus_common::cache::{Cache, VenueOrderIdOwnershipError};
301 use nautilus_core::{DurationNanos, UnixNanos};
302 use nautilus_model::{
303 enums::{OmsType, OrderSide, OrderType},
304 events::{OrderFilled, order::spec::OrderFilledSpec},
305 identifiers::{
306 AccountId, ClientOrderId, PositionId, StrategyId, Venue, VenueOrderId,
307 stubs::account_id,
308 },
309 instruments::{
310 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
311 },
312 orders::{Order, OrderAny, OrderTestBuilder, stubs::TestOrderEventStubs},
313 position::Position,
314 types::{Price, Quantity},
315 };
316 use rstest::{fixture, rstest};
317
318 use crate::matching_engine::ids_generator::IdsGenerator;
319
320 #[fixture]
321 fn instrument_eth_usdt(crypto_perpetual_ethusdt: CryptoPerpetual) -> InstrumentAny {
322 InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt)
323 }
324
325 #[fixture]
326 fn market_order_buy(instrument_eth_usdt: InstrumentAny) -> OrderAny {
327 OrderTestBuilder::new(OrderType::Market)
328 .instrument_id(instrument_eth_usdt.id())
329 .side(OrderSide::Buy)
330 .quantity(Quantity::from("1.000"))
331 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
332 .submit(true)
333 .build()
334 }
335
336 #[fixture]
337 fn market_order_sell(instrument_eth_usdt: InstrumentAny) -> OrderAny {
338 OrderTestBuilder::new(OrderType::Market)
339 .instrument_id(instrument_eth_usdt.id())
340 .side(OrderSide::Sell)
341 .quantity(Quantity::from("1.000"))
342 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
343 .submit(true)
344 .build()
345 }
346
347 #[fixture]
348 fn market_order_fill(
349 instrument_eth_usdt: InstrumentAny,
350 account_id: AccountId,
351 market_order_buy: OrderAny,
352 ) -> OrderFilled {
353 OrderFilledSpec::builder()
354 .trader_id(market_order_buy.trader_id())
355 .strategy_id(market_order_buy.strategy_id())
356 .instrument_id(market_order_buy.instrument_id())
357 .client_order_id(market_order_buy.client_order_id())
358 .venue_order_id(VenueOrderId::new("BINANCE-1"))
359 .account_id(account_id)
360 .last_qty(Quantity::from("1"))
361 .last_px(Price::from("1000.000"))
362 .currency(instrument_eth_usdt.quote_currency())
363 .position_id(PositionId::new("P-1"))
364 .build()
365 }
366
367 fn get_ids_generator(
368 cache: Rc<RefCell<Cache>>,
369 use_position_ids: bool,
370 oms_type: OmsType,
371 ) -> IdsGenerator {
372 IdsGenerator::new(
373 Venue::from("BINANCE"),
374 oms_type,
375 1,
376 false,
377 use_position_ids,
378 cache,
379 )
380 }
381
382 #[rstest]
383 fn test_get_position_id_hedging_with_existing_position(
384 instrument_eth_usdt: InstrumentAny,
385 market_order_buy: OrderAny,
386 market_order_fill: OrderFilled,
387 ) {
388 let cache = Rc::new(RefCell::new(Cache::default()));
389 let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Hedging);
390
391 let position = Position::new(&instrument_eth_usdt, market_order_fill);
392
393 cache
395 .borrow_mut()
396 .add_position(&position, OmsType::Hedging)
397 .unwrap();
398
399 let position_id = ids_generator.get_position_id(&market_order_buy, None);
400 assert_eq!(position_id, Some(position.id));
401 }
402
403 #[rstest]
404 fn test_get_position_id_hedging_with_generated_position(market_order_buy: OrderAny) {
405 let cache = Rc::new(RefCell::new(Cache::default()));
406 let mut ids_generator = get_ids_generator(cache, true, OmsType::Hedging);
407
408 let position_id = ids_generator.get_position_id(&market_order_buy, None);
409 assert_eq!(position_id, Some(PositionId::new("BINANCE-1-1")));
410 }
411
412 #[rstest]
413 fn test_get_position_id_netting(
414 instrument_eth_usdt: InstrumentAny,
415 market_order_buy: OrderAny,
416 market_order_fill: OrderFilled,
417 ) {
418 let cache = Rc::new(RefCell::new(Cache::default()));
419 let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Netting);
420
421 let position_id = ids_generator.get_position_id(&market_order_buy, None);
423 assert_eq!(position_id, None);
424
425 let position = Position::new(&instrument_eth_usdt, market_order_fill);
427 cache
428 .as_ref()
429 .borrow_mut()
430 .add_position(&position, OmsType::Netting)
431 .unwrap();
432
433 let position_id = ids_generator.get_position_id(&market_order_buy, None);
435 assert_eq!(position_id, Some(position.id));
436 }
437
438 #[rstest]
439 fn test_get_position_id_netting_filters_by_strategy(
440 instrument_eth_usdt: InstrumentAny,
441 market_order_fill: OrderFilled,
442 ) {
443 let cache = Rc::new(RefCell::new(Cache::default()));
444 let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Netting);
445 let position = Position::new(&instrument_eth_usdt, market_order_fill);
446 cache
447 .as_ref()
448 .borrow_mut()
449 .add_position(&position, OmsType::Netting)
450 .unwrap();
451
452 let order_for_other_strategy = OrderTestBuilder::new(OrderType::Market)
453 .instrument_id(instrument_eth_usdt.id())
454 .strategy_id(StrategyId::from("S-002"))
455 .side(OrderSide::Buy)
456 .quantity(Quantity::from("1.000"))
457 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-9"))
458 .submit(true)
459 .build();
460
461 let position_id = ids_generator.get_position_id(&order_for_other_strategy, None);
462 assert_eq!(position_id, None);
463 }
464
465 #[rstest]
466 fn test_generate_venue_position_id() {
467 let cache = Rc::new(RefCell::new(Cache::default()));
468 let mut ids_generator_with_position_ids =
469 get_ids_generator(cache.clone(), true, OmsType::Netting);
470 let mut ids_generator_no_position_ids = get_ids_generator(cache, false, OmsType::Netting);
471
472 assert_eq!(
473 ids_generator_no_position_ids.generate_venue_position_id(),
474 None
475 );
476
477 let position_id_1 = ids_generator_with_position_ids.generate_venue_position_id();
478 let position_id_2 = ids_generator_with_position_ids.generate_venue_position_id();
479 assert_eq!(position_id_1, Some(PositionId::new("BINANCE-1-1")));
480 assert_eq!(position_id_2, Some(PositionId::new("BINANCE-1-2")));
481 }
482
483 #[rstest]
484 fn test_generate_venue_position_id_random_uses_uuid4_seam() {
485 let cache = Rc::new(RefCell::new(Cache::default()));
489 let mut generator = IdsGenerator::new(
490 Venue::from("BINANCE"),
491 OmsType::Netting,
492 1,
493 true,
494 true,
495 cache,
496 );
497
498 let id = generator.generate_venue_position_id().expect("position id");
499 let s = id.as_str();
500
501 assert_eq!(s.len(), 36, "expected canonical UUID4 length");
502 assert_eq!(s.as_bytes()[14], b'4', "expected UUID v4 version digit");
503 assert!(
504 matches!(s.as_bytes()[19], b'8' | b'9' | b'a' | b'b'),
505 "expected RFC 4122 variant byte",
506 );
507 }
508
509 #[rstest]
510 fn get_venue_position_id(market_order_buy: OrderAny, market_order_sell: OrderAny) {
511 let cache = Rc::new(RefCell::new(Cache::default()));
512 let mut ids_generator = get_ids_generator(cache, true, OmsType::Netting);
513
514 let venue_order_id1 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
515 let venue_order_id2 = ids_generator
516 .get_venue_order_id(&market_order_sell)
517 .unwrap();
518 assert_eq!(venue_order_id1, VenueOrderId::from("BINANCE-1-1"));
519 assert_eq!(venue_order_id2, VenueOrderId::from("BINANCE-1-2"));
520
521 let venue_order_id3 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
523 assert_eq!(venue_order_id3, VenueOrderId::from("BINANCE-1-1"));
524 }
525
526 #[rstest]
527 fn test_get_venue_order_id_probes_past_preclaimed_candidates(market_order_buy: OrderAny) {
528 let cache = Rc::new(RefCell::new(Cache::default()));
529 let owner_id = ClientOrderId::from("O-OWNER");
530
531 for suffix in 1..=3 {
532 cache
533 .borrow_mut()
534 .add_venue_order_id(
535 &owner_id,
536 &VenueOrderId::from(format!("BINANCE-1-{suffix}")),
537 true,
538 )
539 .unwrap();
540 }
541 let mut ids_generator = get_ids_generator(Rc::clone(&cache), true, OmsType::Netting);
542
543 let venue_order_id = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
544
545 assert_eq!(venue_order_id, VenueOrderId::from("BINANCE-1-4"));
546 assert_eq!(
547 cache
548 .borrow()
549 .venue_order_id(&market_order_buy.client_order_id()),
550 Some(&venue_order_id)
551 );
552
553 for suffix in 1..=3 {
554 assert_eq!(
555 cache
556 .borrow()
557 .client_order_id(&VenueOrderId::from(format!("BINANCE-1-{suffix}"))),
558 Some(&owner_id)
559 );
560 }
561 }
562
563 #[rstest]
564 fn test_get_venue_order_id_fails_when_counter_is_exhausted(market_order_buy: OrderAny) {
565 let cache = Rc::new(RefCell::new(Cache::default()));
566 let mut ids_generator = get_ids_generator(Rc::clone(&cache), true, OmsType::Netting);
567 ids_generator.order_count = usize::MAX;
568
569 let error = ids_generator
570 .get_venue_order_id(&market_order_buy)
571 .unwrap_err();
572
573 assert!(error.to_string().contains("counter exhausted"));
574 assert_eq!(
575 cache
576 .borrow()
577 .venue_order_id(&market_order_buy.client_order_id()),
578 None
579 );
580 }
581
582 #[rstest]
583 fn test_get_venue_order_id_does_not_replace_authoritative_id(mut market_order_buy: OrderAny) {
584 let cache = Rc::new(RefCell::new(Cache::default()));
585 let venue_order_id = VenueOrderId::from("V-AUTHORITATIVE");
586 let owner_id = ClientOrderId::from("O-OWNER");
587 cache
588 .borrow_mut()
589 .add_venue_order_id(&owner_id, &venue_order_id, false)
590 .unwrap();
591 let accepted = TestOrderEventStubs::accepted(
592 &market_order_buy,
593 AccountId::from("ACCOUNT-001"),
594 venue_order_id,
595 );
596 market_order_buy.apply(accepted).unwrap();
597 let mut ids_generator = get_ids_generator(Rc::clone(&cache), true, OmsType::Netting);
598
599 let returned_id = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
600 let error = cache
601 .borrow_mut()
602 .add_venue_order_id(&market_order_buy.client_order_id(), &returned_id, false)
603 .unwrap_err();
604
605 assert_eq!(returned_id, venue_order_id);
606 assert!(error.is::<VenueOrderIdOwnershipError>());
607 assert_eq!(
608 cache.borrow().client_order_id(&venue_order_id),
609 Some(&owner_id)
610 );
611 assert_eq!(
612 cache
613 .borrow()
614 .venue_order_id(&market_order_buy.client_order_id()),
615 None
616 );
617 }
618
619 fn build_ids_generator(venue: Venue, raw_id: u32) -> IdsGenerator {
620 let cache = Rc::new(RefCell::new(Cache::default()));
621 IdsGenerator::new(venue, OmsType::Netting, raw_id, false, true, cache)
622 }
623
624 #[rstest]
625 fn test_generate_trade_id_format_and_length_bound() {
626 let mut generator =
627 build_ids_generator(Venue::from("SOMETHING_VERY_LONG_FOR_SAFETY"), 4_294_967_295);
628 let ts = UnixNanos::from(u64::MAX);
629
630 let trade_id = generator.generate_trade_id(ts);
631 let value = trade_id.as_str();
632
633 assert!(value.len() <= 36);
634 assert!(value.starts_with("T-"));
635 assert_eq!(value.len(), "T-0123456789abcdef-001".len());
636 }
637
638 #[rstest]
639 fn test_generate_trade_id_is_deterministic_across_reset_for_same_ts() {
640 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
641 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
642
643 let first = generator.generate_trade_id(ts);
644 generator.reset();
645 let second = generator.generate_trade_id(ts);
646 assert_eq!(
647 first, second,
648 "same ts_init and reset execution_count must reproduce the same id"
649 );
650 }
651
652 #[rstest]
653 fn test_generate_trade_id_differs_when_ts_init_changes() {
654 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
655 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
656
657 let first = generator.generate_trade_id(ts);
658 generator.reset();
659 let second = generator.generate_trade_id(ts + DurationNanos::new(1));
660 assert_ne!(
661 first, second,
662 "distinct ts_init must produce distinct ids across a reset"
663 );
664 }
665
666 #[rstest]
667 fn test_generate_trade_id_counter_tiebreaker_for_same_ts() {
668 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
669 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
670
671 let first = generator.generate_trade_id(ts);
672 let second = generator.generate_trade_id(ts);
673 let third = generator.generate_trade_id(ts);
674 assert_ne!(first, second);
675 assert_ne!(second, third);
676 assert!(first.as_str().ends_with("-001"));
677 assert!(second.as_str().ends_with("-002"));
678 assert!(third.as_str().ends_with("-003"));
679 }
680
681 #[rstest]
682 #[case(8, "T-5c080ffb681dc0d4-009")]
683 #[case(9, "T-5c080ffb681dc0d4-010")]
684 #[case(98, "T-5c080ffb681dc0d4-099")]
685 #[case(99, "T-5c080ffb681dc0d4-100")]
686 #[case(998, "T-5c080ffb681dc0d4-999")]
687 #[case(999, "T-5c080ffb681dc0d4-1000")]
688 fn test_generate_trade_id_counter_width(
689 #[case] execution_count: usize,
690 #[case] expected: &str,
691 ) {
692 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
693 generator.execution_count = execution_count;
694
695 let trade_id = generator.generate_trade_id(UnixNanos::from(1_700_000_000_000_000_000_u64));
696
697 assert_eq!(trade_id.as_str(), expected);
698 }
699
700 #[rstest]
701 fn test_generate_trade_id_preserves_full_lowercase_hash_width() {
702 let mut generator = build_ids_generator(Venue::from("BINANCE"), 0);
703
704 let trade_id = generator.generate_trade_id(UnixNanos::from(3));
705
706 assert_eq!(trade_id.as_str(), "T-0114760c17a06c2c-001");
707 }
708
709 #[cfg(target_pointer_width = "64")]
710 #[rstest]
711 fn test_generate_trade_id_accepts_maximum_length() {
712 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
713 generator.execution_count = 99_999_999_999_999_998;
714
715 let trade_id = generator.generate_trade_id(UnixNanos::from(1_700_000_000_000_000_000_u64));
716
717 assert_eq!(trade_id.as_str(), "T-5c080ffb681dc0d4-99999999999999999");
718 assert_eq!(trade_id.as_str().len(), 36);
719 }
720
721 #[cfg(target_pointer_width = "64")]
722 #[rstest]
723 #[case(99_999_999_999_999_999, 37)]
724 #[case(9_999_999_999_999_999_998, 38)]
725 #[case(usize::MAX - 1, 39)]
726 fn test_generate_trade_id_preserves_length_validation(
727 #[case] execution_count: usize,
728 #[case] expected_length: usize,
729 ) {
730 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
731 generator.execution_count = execution_count;
732
733 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
734 generator.generate_trade_id(UnixNanos::from(1_700_000_000_000_000_000_u64));
735 }))
736 .expect_err("an oversized trade ID should fail validation");
737 let message = panic
738 .downcast_ref::<String>()
739 .map(String::as_str)
740 .or_else(|| panic.downcast_ref::<&str>().copied())
741 .expect("trade ID validation should panic with a string message");
742
743 assert!(
744 message.contains(&format!(
745 "String exceeds maximum length of 36 characters, was {expected_length}"
746 )),
747 "unexpected trade ID validation panic: {message}",
748 );
749 assert_eq!(generator.execution_count, execution_count + 1);
750
751 generator.reset();
752 let trade_id = generator.generate_trade_id(UnixNanos::from(1_700_000_000_000_000_000_u64));
753 assert_eq!(trade_id.as_str(), "T-5c080ffb681dc0d4-001");
754 }
755
756 #[rstest]
757 fn test_generate_trade_id_preserves_counter_overflow_semantics() {
758 let expected = std::panic::catch_unwind(|| {
759 let mut counter = std::hint::black_box(usize::MAX);
760 counter += 1;
761 counter
762 });
763 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
764 generator.execution_count = usize::MAX;
765
766 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
767 generator.generate_trade_id(UnixNanos::from(1_700_000_000_000_000_000_u64))
768 }));
769
770 match (expected, result) {
771 (Ok(expected_count), Ok(trade_id)) => {
772 assert_eq!(trade_id.as_str(), "T-5c080ffb681dc0d4-000");
773 assert_eq!(generator.execution_count, expected_count);
774 }
775 (Err(_), Err(panic)) => {
776 let message = panic
777 .downcast_ref::<String>()
778 .map(String::as_str)
779 .or_else(|| panic.downcast_ref::<&str>().copied())
780 .expect("counter overflow should panic with a string message");
781
782 assert!(
783 message.contains("attempt to add with overflow"),
784 "unexpected execution counter overflow panic: {message}",
785 );
786 assert_eq!(generator.execution_count, usize::MAX);
787 }
788 (Ok(_), Err(_)) => panic!("execution counter unexpectedly used checked overflow"),
789 (Err(_), Ok(_)) => panic!("execution counter unexpectedly used unchecked overflow"),
790 }
791 }
792
793 #[rstest]
794 fn test_generate_trade_id_differs_when_venue_or_raw_id_changes() {
795 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
796
797 let mut gen_a = build_ids_generator(Venue::from("BINANCE"), 1);
798 let mut gen_b = build_ids_generator(Venue::from("BYBIT"), 1);
799 let mut gen_c = build_ids_generator(Venue::from("BINANCE"), 2);
800
801 let a = gen_a.generate_trade_id(ts);
802 let b = gen_b.generate_trade_id(ts);
803 let c = gen_c.generate_trade_id(ts);
804 assert_ne!(a, b, "venue must distinguish ids");
805 assert_ne!(a, c, "raw_id must distinguish ids");
806 }
807
808 #[rstest]
812 #[case::zero("BINANCE", 1_u32, 0_u64, "T-59d6cf33c843f0cc-001")]
813 #[case::nanos(
814 "BINANCE",
815 1_u32,
816 1_700_000_000_000_000_000_u64,
817 "T-5c080ffb681dc0d4-001"
818 )]
819 #[case::long_venue(
820 "SOMETHING_VERY_LONG_FOR_SAFETY",
821 42_u32,
822 1_700_000_000_000_000_000_u64,
823 "T-2a2238c5cc0cbaf2-001"
824 )]
825 fn test_generate_trade_id_matches_python_parity_fixture(
826 #[case] venue: &str,
827 #[case] raw_id: u32,
828 #[case] ts_init: u64,
829 #[case] expected: &str,
830 ) {
831 let mut generator = build_ids_generator(Venue::from(venue), raw_id);
832 let trade_id = generator.generate_trade_id(UnixNanos::from(ts_init));
833 assert_eq!(trade_id.as_str(), expected);
834 }
835
836 #[rstest]
841 fn test_generate_trade_id_multi_tick_matches_python_parity_fixture() {
842 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
843 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
844 let sequence: Vec<String> = (0..4)
845 .map(|_| generator.generate_trade_id(ts).as_str().to_string())
846 .collect();
847
848 assert_eq!(
849 sequence,
850 vec![
851 "T-5c080ffb681dc0d4-001".to_string(),
852 "T-5c080ffb681dc0d4-002".to_string(),
853 "T-5c080ffb681dc0d4-003".to_string(),
854 "T-5c080ffb681dc0d4-004".to_string(),
855 ],
856 );
857 }
858}