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 =
136 cache.positions_open(None, Some(&order.instrument_id()), None, None, None);
137 if positions_open.is_empty() {
138 None
139 } else {
140 Some(positions_open[0].id)
141 }
142 }
143 }
144
145 pub fn generate_trade_id(&mut self, ts_init: UnixNanos) -> TradeId {
146 self.execution_count += 1;
147 let hash = fnv1a_trade_id_hash(self.venue, self.raw_id, ts_init.as_u64());
154 let trade_id = format!("T-{hash:016x}-{:03}", self.execution_count);
155 TradeId::from(trade_id.as_str())
156 }
157
158 pub fn generate_venue_position_id(&mut self) -> Option<PositionId> {
159 if !self.use_position_ids {
160 return None;
161 }
162
163 self.position_count += 1;
164
165 if self.use_random_ids {
166 Some(PositionId::new(UUID4::new().to_string()))
167 } else {
168 Some(PositionId::new(
169 format!("{}-{}-{}", self.venue, self.raw_id, self.position_count).as_str(),
170 ))
171 }
172 }
173
174 pub fn generate_venue_order_id(&mut self) -> VenueOrderId {
175 self.order_count += 1;
176
177 if self.use_random_ids {
178 VenueOrderId::new(UUID4::new().to_string())
179 } else {
180 VenueOrderId::new(
181 format!("{}-{}-{}", self.venue, self.raw_id, self.order_count).as_str(),
182 )
183 }
184 }
185}
186
187fn fnv1a_trade_id_hash(venue: Venue, raw_id: u32, ts_init_ns: u64) -> u64 {
188 let mut hash: u64 = FNV_OFFSET_BASIS;
189
190 for bytes in [
191 venue.as_str().as_bytes(),
192 b"\x1f",
193 &raw_id.to_le_bytes(),
194 b"\x1f",
195 &ts_init_ns.to_le_bytes(),
196 ] {
197 for &byte in bytes {
198 hash ^= u64::from(byte);
199 hash = hash.wrapping_mul(FNV_PRIME);
200 }
201 }
202 hash
203}
204
205#[cfg(test)]
206mod tests {
207 use std::{cell::RefCell, rc::Rc};
208
209 use nautilus_common::cache::Cache;
210 use nautilus_core::{UUID4, UnixNanos};
211 use nautilus_model::{
212 enums::{LiquiditySide, OmsType, OrderSide, OrderType},
213 events::OrderFilled,
214 identifiers::{
215 AccountId, ClientOrderId, PositionId, TradeId, Venue, VenueOrderId, stubs::account_id,
216 },
217 instruments::{
218 CryptoPerpetual, Instrument, InstrumentAny, stubs::crypto_perpetual_ethusdt,
219 },
220 orders::{Order, OrderAny, OrderTestBuilder},
221 position::Position,
222 types::{Price, Quantity},
223 };
224 use rstest::{fixture, rstest};
225
226 use crate::matching_engine::ids_generator::IdsGenerator;
227
228 #[fixture]
229 fn instrument_eth_usdt(crypto_perpetual_ethusdt: CryptoPerpetual) -> InstrumentAny {
230 InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt)
231 }
232
233 #[fixture]
234 fn market_order_buy(instrument_eth_usdt: InstrumentAny) -> OrderAny {
235 OrderTestBuilder::new(OrderType::Market)
236 .instrument_id(instrument_eth_usdt.id())
237 .side(OrderSide::Buy)
238 .quantity(Quantity::from("1.000"))
239 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
240 .submit(true)
241 .build()
242 }
243
244 #[fixture]
245 fn market_order_sell(instrument_eth_usdt: InstrumentAny) -> OrderAny {
246 OrderTestBuilder::new(OrderType::Market)
247 .instrument_id(instrument_eth_usdt.id())
248 .side(OrderSide::Sell)
249 .quantity(Quantity::from("1.000"))
250 .client_order_id(ClientOrderId::from("O-19700101-000000-001-001-2"))
251 .submit(true)
252 .build()
253 }
254
255 #[fixture]
256 fn market_order_fill(
257 instrument_eth_usdt: InstrumentAny,
258 account_id: AccountId,
259 market_order_buy: OrderAny,
260 ) -> OrderFilled {
261 OrderFilled::new(
262 market_order_buy.trader_id(),
263 market_order_buy.strategy_id(),
264 market_order_buy.instrument_id(),
265 market_order_buy.client_order_id(),
266 VenueOrderId::new("BINANCE-1"),
267 account_id,
268 TradeId::new("1"),
269 market_order_buy.order_side(),
270 market_order_buy.order_type(),
271 Quantity::from("1"),
272 Price::from("1000.000"),
273 instrument_eth_usdt.quote_currency(),
274 LiquiditySide::Taker,
275 UUID4::new(),
276 UnixNanos::default(),
277 UnixNanos::default(),
278 false,
279 Some(PositionId::new("P-1")),
280 None,
281 )
282 }
283
284 fn get_ids_generator(
285 cache: Rc<RefCell<Cache>>,
286 use_position_ids: bool,
287 oms_type: OmsType,
288 ) -> IdsGenerator {
289 IdsGenerator::new(
290 Venue::from("BINANCE"),
291 oms_type,
292 1,
293 false,
294 use_position_ids,
295 cache,
296 )
297 }
298
299 #[rstest]
300 fn test_get_position_id_hedging_with_existing_position(
301 instrument_eth_usdt: InstrumentAny,
302 market_order_buy: OrderAny,
303 market_order_fill: OrderFilled,
304 ) {
305 let cache = Rc::new(RefCell::new(Cache::default()));
306 let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Hedging);
307
308 let position = Position::new(&instrument_eth_usdt, market_order_fill);
309
310 cache
312 .borrow_mut()
313 .add_position(&position, OmsType::Hedging)
314 .unwrap();
315
316 let position_id = ids_generator.get_position_id(&market_order_buy, None);
317 assert_eq!(position_id, Some(position.id));
318 }
319
320 #[rstest]
321 fn test_get_position_id_hedging_with_generated_position(market_order_buy: OrderAny) {
322 let cache = Rc::new(RefCell::new(Cache::default()));
323 let mut ids_generator = get_ids_generator(cache, true, OmsType::Hedging);
324
325 let position_id = ids_generator.get_position_id(&market_order_buy, None);
326 assert_eq!(position_id, Some(PositionId::new("BINANCE-1-1")));
327 }
328
329 #[rstest]
330 fn test_get_position_id_netting(
331 instrument_eth_usdt: InstrumentAny,
332 market_order_buy: OrderAny,
333 market_order_fill: OrderFilled,
334 ) {
335 let cache = Rc::new(RefCell::new(Cache::default()));
336 let mut ids_generator = get_ids_generator(cache.clone(), false, OmsType::Netting);
337
338 let position_id = ids_generator.get_position_id(&market_order_buy, None);
340 assert_eq!(position_id, None);
341
342 let position = Position::new(&instrument_eth_usdt, market_order_fill);
344 cache
345 .as_ref()
346 .borrow_mut()
347 .add_position(&position, OmsType::Netting)
348 .unwrap();
349
350 let position_id = ids_generator.get_position_id(&market_order_buy, None);
352 assert_eq!(position_id, Some(position.id));
353 }
354
355 #[rstest]
356 fn test_generate_venue_position_id() {
357 let cache = Rc::new(RefCell::new(Cache::default()));
358 let mut ids_generator_with_position_ids =
359 get_ids_generator(cache.clone(), true, OmsType::Netting);
360 let mut ids_generator_no_position_ids = get_ids_generator(cache, false, OmsType::Netting);
361
362 assert_eq!(
363 ids_generator_no_position_ids.generate_venue_position_id(),
364 None
365 );
366
367 let position_id_1 = ids_generator_with_position_ids.generate_venue_position_id();
368 let position_id_2 = ids_generator_with_position_ids.generate_venue_position_id();
369 assert_eq!(position_id_1, Some(PositionId::new("BINANCE-1-1")));
370 assert_eq!(position_id_2, Some(PositionId::new("BINANCE-1-2")));
371 }
372
373 #[rstest]
374 fn test_generate_venue_position_id_random_uses_uuid4_seam() {
375 let cache = Rc::new(RefCell::new(Cache::default()));
379 let mut generator = IdsGenerator::new(
380 Venue::from("BINANCE"),
381 OmsType::Netting,
382 1,
383 true,
384 true,
385 cache,
386 );
387
388 let id = generator.generate_venue_position_id().expect("position id");
389 let s = id.as_str();
390
391 assert_eq!(s.len(), 36, "expected canonical UUID4 length");
392 assert_eq!(s.as_bytes()[14], b'4', "expected UUID v4 version digit");
393 assert!(
394 matches!(s.as_bytes()[19], b'8' | b'9' | b'a' | b'b'),
395 "expected RFC 4122 variant byte",
396 );
397 }
398
399 #[rstest]
400 fn get_venue_position_id(market_order_buy: OrderAny, market_order_sell: OrderAny) {
401 let cache = Rc::new(RefCell::new(Cache::default()));
402 let mut ids_generator = get_ids_generator(cache, true, OmsType::Netting);
403
404 let venue_order_id1 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
405 let venue_order_id2 = ids_generator
406 .get_venue_order_id(&market_order_sell)
407 .unwrap();
408 assert_eq!(venue_order_id1, VenueOrderId::from("BINANCE-1-1"));
409 assert_eq!(venue_order_id2, VenueOrderId::from("BINANCE-1-2"));
410
411 let venue_order_id3 = ids_generator.get_venue_order_id(&market_order_buy).unwrap();
413 assert_eq!(venue_order_id3, VenueOrderId::from("BINANCE-1-1"));
414 }
415
416 fn build_ids_generator(venue: Venue, raw_id: u32) -> IdsGenerator {
417 let cache = Rc::new(RefCell::new(Cache::default()));
418 IdsGenerator::new(venue, OmsType::Netting, raw_id, false, true, cache)
419 }
420
421 #[rstest]
422 fn test_generate_trade_id_format_and_length_bound() {
423 let mut generator =
424 build_ids_generator(Venue::from("SOMETHING_VERY_LONG_FOR_SAFETY"), 4_294_967_295);
425 let ts = UnixNanos::from(u64::MAX);
426
427 let trade_id = generator.generate_trade_id(ts);
428 let value = trade_id.as_str();
429
430 assert!(value.len() <= 36);
431 assert!(value.starts_with("T-"));
432 assert_eq!(value.len(), "T-0123456789abcdef-001".len());
433 }
434
435 #[rstest]
436 fn test_generate_trade_id_is_deterministic_across_reset_for_same_ts() {
437 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
438 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
439
440 let first = generator.generate_trade_id(ts);
441 generator.reset();
442 let second = generator.generate_trade_id(ts);
443 assert_eq!(
444 first, second,
445 "same ts_init and reset execution_count must reproduce the same id"
446 );
447 }
448
449 #[rstest]
450 fn test_generate_trade_id_differs_when_ts_init_changes() {
451 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
452 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
453
454 let first = generator.generate_trade_id(ts);
455 generator.reset();
456 let second = generator.generate_trade_id(ts + UnixNanos::from(1));
457 assert_ne!(
458 first, second,
459 "distinct ts_init must produce distinct ids across a reset"
460 );
461 }
462
463 #[rstest]
464 fn test_generate_trade_id_counter_tiebreaker_for_same_ts() {
465 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
466 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
467
468 let first = generator.generate_trade_id(ts);
469 let second = generator.generate_trade_id(ts);
470 let third = generator.generate_trade_id(ts);
471 assert_ne!(first, second);
472 assert_ne!(second, third);
473 assert!(first.as_str().ends_with("-001"));
474 assert!(second.as_str().ends_with("-002"));
475 assert!(third.as_str().ends_with("-003"));
476 }
477
478 #[rstest]
479 fn test_generate_trade_id_differs_when_venue_or_raw_id_changes() {
480 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
481
482 let mut gen_a = build_ids_generator(Venue::from("BINANCE"), 1);
483 let mut gen_b = build_ids_generator(Venue::from("BYBIT"), 1);
484 let mut gen_c = build_ids_generator(Venue::from("BINANCE"), 2);
485
486 let a = gen_a.generate_trade_id(ts);
487 let b = gen_b.generate_trade_id(ts);
488 let c = gen_c.generate_trade_id(ts);
489 assert_ne!(a, b, "venue must distinguish ids");
490 assert_ne!(a, c, "raw_id must distinguish ids");
491 }
492
493 #[rstest]
497 #[case::zero("BINANCE", 1_u32, 0_u64, "T-59d6cf33c843f0cc-001")]
498 #[case::nanos(
499 "BINANCE",
500 1_u32,
501 1_700_000_000_000_000_000_u64,
502 "T-5c080ffb681dc0d4-001"
503 )]
504 #[case::long_venue(
505 "SOMETHING_VERY_LONG_FOR_SAFETY",
506 42_u32,
507 1_700_000_000_000_000_000_u64,
508 "T-2a2238c5cc0cbaf2-001"
509 )]
510 fn test_generate_trade_id_matches_python_parity_fixture(
511 #[case] venue: &str,
512 #[case] raw_id: u32,
513 #[case] ts_init: u64,
514 #[case] expected: &str,
515 ) {
516 let mut generator = build_ids_generator(Venue::from(venue), raw_id);
517 let trade_id = generator.generate_trade_id(UnixNanos::from(ts_init));
518 assert_eq!(trade_id.as_str(), expected);
519 }
520
521 #[rstest]
526 fn test_generate_trade_id_multi_tick_matches_python_parity_fixture() {
527 let mut generator = build_ids_generator(Venue::from("BINANCE"), 1);
528 let ts = UnixNanos::from(1_700_000_000_000_000_000_u64);
529 let sequence: Vec<String> = (0..4)
530 .map(|_| generator.generate_trade_id(ts).as_str().to_string())
531 .collect();
532
533 assert_eq!(
534 sequence,
535 vec![
536 "T-5c080ffb681dc0d4-001".to_string(),
537 "T-5c080ffb681dc0d4-002".to_string(),
538 "T-5c080ffb681dc0d4-003".to_string(),
539 "T-5c080ffb681dc0d4-004".to_string(),
540 ],
541 );
542 }
543}