1use std::sync::atomic::{AtomicU32, Ordering};
42
43use dashmap::{DashMap, mapref::entry::Entry};
44use nautilus_model::identifiers::ClientOrderId;
45use thiserror::Error;
46
47pub const DYDX_BASE_EPOCH: i64 = 1577836800;
50
51pub const DEFAULT_RUST_CLIENT_METADATA: u32 = 4;
54
55pub const MAX_SAFE_CLIENT_ID: u32 = u32::MAX - 1000;
58
59const TRADER_SHIFT: u32 = 22; const STRATEGY_SHIFT: u32 = 12; const COUNT_MASK: u32 = 0xFFF; const TRADER_MASK: u32 = 0x3FF; const STRATEGY_MASK: u32 = 0x3FF; const SEQUENTIAL_METADATA_MARKER: u32 = u32::MAX;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct EncodedClientOrderId {
78 pub client_id: u32,
80 pub client_metadata: u32,
82}
83
84#[derive(Debug, Clone, Error)]
86pub enum EncoderError {
87 #[error(
89 "Client order ID counter overflow: current value {0} exceeds safe limit {MAX_SAFE_CLIENT_ID}"
90 )]
91 CounterOverflow(u32),
92
93 #[error("Failed to parse O-format ClientOrderId: {0}")]
95 ParseError(String),
96
97 #[error("Value overflow in encoding: {0}")]
99 ValueOverflow(String),
100}
101
102#[derive(Debug)]
114pub struct ClientOrderIdEncoder {
115 forward: DashMap<ClientOrderId, EncodedClientOrderId>,
117 reverse: DashMap<(u32, u32), ClientOrderId>,
119 next_id: AtomicU32,
121}
122
123impl Default for ClientOrderIdEncoder {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl ClientOrderIdEncoder {
130 #[must_use]
132 pub fn new() -> Self {
133 Self {
134 forward: DashMap::new(),
135 reverse: DashMap::new(),
136 next_id: AtomicU32::new(1),
137 }
138 }
139
140 pub fn encode(&self, id: ClientOrderId) -> Result<EncodedClientOrderId, EncoderError> {
154 if let Some(existing) = self.forward.get(&id) {
156 let encoded = *existing.value();
157 return Ok(encoded);
158 }
159
160 let id_str = id.as_str();
161
162 if let Ok(numeric_id) = id_str.parse::<u32>() {
164 let encoded = EncodedClientOrderId {
165 client_id: numeric_id,
166 client_metadata: DEFAULT_RUST_CLIENT_METADATA,
167 };
168 self.forward.insert(id, encoded);
170 self.reverse
171 .insert((encoded.client_id, encoded.client_metadata), id);
172 return Ok(encoded);
173 }
174
175 if id_str.starts_with("O-") {
177 match self.encode_o_format(id_str) {
178 Ok(encoded) => {
179 return Ok(encoded);
181 }
182 Err(e) => {
183 log::warn!(
184 "[ENCODER] O-format parse failed for '{id}': {e}, falling back to sequential",
185 );
186 }
188 }
189 }
190
191 self.allocate_sequential(id)
193 }
194
195 fn encode_o_format(&self, id_str: &str) -> Result<EncodedClientOrderId, EncoderError> {
204 let parts: Vec<&str> = id_str.split('-').collect();
206 if parts.len() != 6 || parts[0] != "O" {
207 return Err(EncoderError::ParseError(format!(
208 "Expected O-YYYYMMDD-HHMMSS-TTT-SSS-CCC, received: {id_str}",
209 )));
210 }
211
212 let date_str = parts[1]; let time_str = parts[2]; let trader_str = parts[3]; let strategy_str = parts[4]; let count_str = parts[5]; if date_str.len() != 8 || time_str.len() != 6 {
220 return Err(EncoderError::ParseError(format!(
221 "Invalid date/time format in: {id_str}"
222 )));
223 }
224
225 let year: i32 = date_str[0..4]
227 .parse()
228 .map_err(|_| EncoderError::ParseError(format!("Invalid year in: {id_str}")))?;
229 let month: u32 = date_str[4..6]
230 .parse()
231 .map_err(|_| EncoderError::ParseError(format!("Invalid month in: {id_str}")))?;
232 let day: u32 = date_str[6..8]
233 .parse()
234 .map_err(|_| EncoderError::ParseError(format!("Invalid day in: {id_str}")))?;
235 let hour: u32 = time_str[0..2]
236 .parse()
237 .map_err(|_| EncoderError::ParseError(format!("Invalid hour in: {id_str}")))?;
238 let minute: u32 = time_str[2..4]
239 .parse()
240 .map_err(|_| EncoderError::ParseError(format!("Invalid minute in: {id_str}")))?;
241 let second: u32 = time_str[4..6]
242 .parse()
243 .map_err(|_| EncoderError::ParseError(format!("Invalid second in: {id_str}")))?;
244
245 let trader: u32 = trader_str
247 .parse()
248 .map_err(|_| EncoderError::ParseError(format!("Invalid trader in: {id_str}")))?;
249 let strategy: u32 = strategy_str
250 .parse()
251 .map_err(|_| EncoderError::ParseError(format!("Invalid strategy in: {id_str}")))?;
252 let count: u32 = count_str
253 .parse()
254 .map_err(|_| EncoderError::ParseError(format!("Invalid count in: {id_str}")))?;
255
256 if trader > TRADER_MASK {
258 return Err(EncoderError::ValueOverflow(format!(
259 "Trader tag {trader} exceeds max {TRADER_MASK}"
260 )));
261 }
262 if strategy > STRATEGY_MASK {
263 return Err(EncoderError::ValueOverflow(format!(
264 "Strategy tag {strategy} exceeds max {STRATEGY_MASK}"
265 )));
266 }
267 if count > COUNT_MASK {
268 return Err(EncoderError::ValueOverflow(format!(
269 "Count {count} exceeds max {COUNT_MASK}"
270 )));
271 }
272
273 let dt = chrono::NaiveDate::from_ymd_opt(year, month, day)
275 .and_then(|d| d.and_hms_opt(hour, minute, second))
276 .ok_or_else(|| EncoderError::ParseError(format!("Invalid datetime in: {id_str}")))?;
277
278 let timestamp = dt.and_utc().timestamp();
279
280 let seconds_since_epoch = timestamp - DYDX_BASE_EPOCH;
282 if seconds_since_epoch < 0 {
283 return Err(EncoderError::ValueOverflow(format!(
284 "Timestamp {timestamp} is before base epoch {DYDX_BASE_EPOCH}"
285 )));
286 }
287
288 let client_id =
295 (trader << TRADER_SHIFT) | (strategy << STRATEGY_SHIFT) | (count & COUNT_MASK);
296 let client_metadata = seconds_since_epoch as u32;
297
298 Ok(EncodedClientOrderId {
299 client_id,
300 client_metadata,
301 })
302 }
303
304 fn allocate_sequential(&self, id: ClientOrderId) -> Result<EncodedClientOrderId, EncoderError> {
306 let current = self.next_id.load(Ordering::Relaxed);
308 if current >= MAX_SAFE_CLIENT_ID {
309 log::error!(
310 "[ENCODER] allocate_sequential() OVERFLOW: counter {current} >= MAX_SAFE {MAX_SAFE_CLIENT_ID}"
311 );
312 return Err(EncoderError::CounterOverflow(current));
313 }
314
315 match self.forward.entry(id) {
317 Entry::Occupied(entry) => {
318 let encoded = *entry.get();
319 Ok(encoded)
320 }
321 Entry::Vacant(vacant) => {
322 let counter = self.next_id.fetch_add(1, Ordering::Relaxed);
323 let encoded = EncodedClientOrderId {
326 client_id: counter,
327 client_metadata: SEQUENTIAL_METADATA_MARKER,
328 };
329 vacant.insert(encoded);
330 self.reverse
331 .insert((encoded.client_id, encoded.client_metadata), id);
332 Ok(encoded)
333 }
334 }
335 }
336
337 #[must_use]
347 pub fn decode(&self, client_id: u32, client_metadata: u32) -> Option<ClientOrderId> {
348 if client_metadata == DEFAULT_RUST_CLIENT_METADATA {
350 let id = ClientOrderId::from(client_id.to_string().as_str());
351 return Some(id);
352 }
353
354 if client_metadata == SEQUENTIAL_METADATA_MARKER {
356 let result = self
357 .reverse
358 .get(&(client_id, client_metadata))
359 .map(|r| *r.value());
360 return result;
361 }
362
363 self.decode_o_format(client_id, client_metadata)
365 }
366
367 fn decode_o_format(&self, client_id: u32, client_metadata: u32) -> Option<ClientOrderId> {
373 let trader = (client_id >> TRADER_SHIFT) & TRADER_MASK;
375 let strategy = (client_id >> STRATEGY_SHIFT) & STRATEGY_MASK;
376 let count = client_id & COUNT_MASK;
377
378 let timestamp = (client_metadata as i64) + DYDX_BASE_EPOCH;
380
381 let dt = chrono::DateTime::from_timestamp(timestamp, 0)?;
383
384 let id_str = format!(
386 "O-{:04}{:02}{:02}-{:02}{:02}{:02}-{:03}-{:03}-{}",
387 dt.year(),
388 dt.month(),
389 dt.day(),
390 dt.hour(),
391 dt.minute(),
392 dt.second(),
393 trader,
394 strategy,
395 count
396 );
397
398 let id = ClientOrderId::from(id_str.as_str());
399 Some(id)
400 }
401
402 #[must_use]
407 pub fn get(&self, id: &ClientOrderId) -> Option<EncodedClientOrderId> {
408 if let Some(entry) = self.forward.get(id) {
410 return Some(*entry.value());
411 }
412
413 let id_str = id.as_str();
414
415 if let Ok(numeric_id) = id_str.parse::<u32>() {
417 return Some(EncodedClientOrderId {
418 client_id: numeric_id,
419 client_metadata: DEFAULT_RUST_CLIENT_METADATA,
420 });
421 }
422
423 if id_str.starts_with("O-")
425 && let Ok(encoded) = self.encode_o_format(id_str)
426 {
427 return Some(encoded);
428 }
429
430 None
431 }
432
433 pub fn remove(&self, client_id: u32, client_metadata: u32) -> Option<ClientOrderId> {
438 if client_metadata == SEQUENTIAL_METADATA_MARKER {
440 if let Some((_, client_order_id)) = self.reverse.remove(&(client_id, client_metadata)) {
441 self.forward.remove(&client_order_id);
442 return Some(client_order_id);
443 }
444 return None;
445 }
446
447 if client_metadata == DEFAULT_RUST_CLIENT_METADATA
449 && let Some((_, client_order_id)) = self.reverse.remove(&(client_id, client_metadata))
450 {
451 self.forward.remove(&client_order_id);
452 return Some(client_order_id);
453 }
454
455 self.decode_o_format(client_id, client_metadata)
457 }
458
459 pub fn remove_by_client_id(&self, client_id: u32) -> Option<ClientOrderId> {
462 if let result @ Some(_) = self.remove(client_id, DEFAULT_RUST_CLIENT_METADATA) {
464 return result;
465 }
466
467 let key_to_remove = self
469 .reverse
470 .iter()
471 .find(|r| r.key().0 == client_id)
472 .map(|r| *r.key());
473
474 if let Some((cid, meta)) = key_to_remove {
475 return self.remove(cid, meta);
476 }
477
478 None
479 }
480
481 #[must_use]
483 pub fn current_counter(&self) -> u32 {
484 self.next_id.load(Ordering::Relaxed)
485 }
486
487 #[must_use]
489 pub fn len(&self) -> usize {
490 self.forward.len()
491 }
492
493 #[must_use]
495 pub fn is_empty(&self) -> bool {
496 self.forward.is_empty()
497 }
498}
499
500use chrono::{Datelike, Timelike};
502
503#[cfg(test)]
504mod tests {
505 use rstest::rstest;
506
507 use super::*;
508
509 #[rstest]
510 fn test_encode_numeric_id() {
511 let encoder = ClientOrderIdEncoder::new();
512 let id = ClientOrderId::from("12345");
513
514 let result = encoder.encode(id);
515 assert!(result.is_ok());
516 let encoded = result.unwrap();
517 assert_eq!(encoded.client_id, 12345);
518 assert_eq!(encoded.client_metadata, DEFAULT_RUST_CLIENT_METADATA);
519 }
520
521 #[rstest]
522 fn test_encode_o_format() {
523 let encoder = ClientOrderIdEncoder::new();
524 let id = ClientOrderId::from("O-20260131-174827-001-001-1");
525
526 let result = encoder.encode(id);
527 assert!(result.is_ok());
528 let encoded = result.unwrap();
529
530 let expected_client_id = (1 << TRADER_SHIFT) | (1 << STRATEGY_SHIFT) | 1;
536 assert_eq!(encoded.client_id, expected_client_id);
537
538 let expected_timestamp = chrono::NaiveDate::from_ymd_opt(2026, 1, 31)
541 .unwrap()
542 .and_hms_opt(17, 48, 27)
543 .unwrap()
544 .and_utc()
545 .timestamp();
546 let expected_metadata = (expected_timestamp - DYDX_BASE_EPOCH) as u32;
547 assert_eq!(encoded.client_metadata, expected_metadata);
548 }
549
550 #[rstest]
551 fn test_roundtrip_o_format() {
552 let encoder = ClientOrderIdEncoder::new();
553 let id = ClientOrderId::from("O-20260131-174827-001-001-1");
554
555 let encoded = encoder.encode(id).unwrap();
556 let decoded = encoder.decode(encoded.client_id, encoded.client_metadata);
557
558 assert_eq!(decoded, Some(id));
559 }
560
561 #[rstest]
562 fn test_roundtrip_o_format_various() {
563 let encoder = ClientOrderIdEncoder::new();
564 let test_cases = vec![
565 "O-20260131-000000-001-001-1",
566 "O-20260131-235959-999-999-4095",
567 "O-20200101-000000-000-000-0",
568 "O-20251215-123456-123-456-789",
569 ];
570
571 for id_str in test_cases {
572 let id = ClientOrderId::from(id_str);
573 let encoded = encoder.encode(id).unwrap();
574 let decoded = encoder.decode(encoded.client_id, encoded.client_metadata);
575 assert_eq!(decoded, Some(id), "Roundtrip failed for {id_str}");
576 }
577 }
578
579 #[rstest]
580 fn test_roundtrip_numeric() {
581 let encoder = ClientOrderIdEncoder::new();
582 let id = ClientOrderId::from("12345");
583
584 let encoded = encoder.encode(id).unwrap();
585 let decoded = encoder.decode(encoded.client_id, encoded.client_metadata);
586
587 assert_eq!(decoded, Some(id));
588 }
589
590 #[rstest]
591 fn test_encode_non_standard_uses_sequential() {
592 let encoder = ClientOrderIdEncoder::new();
593 let id = ClientOrderId::from("custom-order-id");
594
595 let result = encoder.encode(id);
596 assert!(result.is_ok());
597 let encoded = result.unwrap();
598
599 assert_eq!(
601 encoded.client_metadata, SEQUENTIAL_METADATA_MARKER,
602 "Expected client_metadata == SEQUENTIAL_METADATA_MARKER"
603 );
604 }
605
606 #[rstest]
607 fn test_roundtrip_sequential() {
608 let encoder = ClientOrderIdEncoder::new();
609 let id = ClientOrderId::from("custom-order-id");
610
611 let encoded = encoder.encode(id).unwrap();
612 let decoded = encoder.decode(encoded.client_id, encoded.client_metadata);
613
614 assert_eq!(decoded, Some(id));
615 }
616
617 #[rstest]
618 fn test_sequential_lost_after_restart() {
619 let encoder1 = ClientOrderIdEncoder::new();
621 let id = ClientOrderId::from("custom-order-id");
622
623 let encoded = encoder1.encode(id).unwrap();
624
625 let encoder2 = ClientOrderIdEncoder::new();
627 let decoded = encoder2.decode(encoded.client_id, encoded.client_metadata);
628
629 assert!(decoded.is_none());
631 }
632
633 #[rstest]
634 fn test_o_format_survives_restart() {
635 let encoder1 = ClientOrderIdEncoder::new();
636 let id = ClientOrderId::from("O-20260131-174827-001-001-1");
637
638 let encoded = encoder1.encode(id).unwrap();
639
640 let encoder2 = ClientOrderIdEncoder::new();
642 let decoded = encoder2.decode(encoded.client_id, encoded.client_metadata);
643
644 assert_eq!(decoded, Some(id));
646 }
647
648 #[rstest]
649 fn test_get_without_encode() {
650 let encoder = ClientOrderIdEncoder::new();
651
652 let numeric_id = ClientOrderId::from("12345");
654 let got = encoder.get(&numeric_id);
655 assert_eq!(
656 got,
657 Some(EncodedClientOrderId {
658 client_id: 12345,
659 client_metadata: DEFAULT_RUST_CLIENT_METADATA
660 })
661 );
662
663 let o_id = ClientOrderId::from("O-20260131-174827-001-001-1");
665 let got = encoder.get(&o_id);
666 assert!(got.is_some());
667
668 let custom_id = ClientOrderId::from("custom");
670 let got = encoder.get(&custom_id);
671 assert!(got.is_none());
672 }
673
674 #[rstest]
675 fn test_remove_sequential() {
676 let encoder = ClientOrderIdEncoder::new();
677 let id = ClientOrderId::from("custom-order-id");
678
679 let encoded = encoder.encode(id).unwrap();
680 assert_eq!(encoder.len(), 1);
681
682 let removed = encoder.remove(encoded.client_id, encoded.client_metadata);
683 assert_eq!(removed, Some(id));
684 assert_eq!(encoder.len(), 0);
685 }
686
687 #[rstest]
688 fn test_max_values_o_format() {
689 let encoder = ClientOrderIdEncoder::new();
690 let id = ClientOrderId::from("O-20260131-235959-999-999-4095");
692
693 let result = encoder.encode(id);
694 assert!(result.is_ok());
695
696 let encoded = result.unwrap();
697 let decoded = encoder.decode(encoded.client_id, encoded.client_metadata);
698 assert_eq!(decoded, Some(id));
699 }
700
701 #[rstest]
702 fn test_overflow_trader_tag() {
703 let encoder = ClientOrderIdEncoder::new();
704 let id = ClientOrderId::from("O-20260131-174827-1024-001-1");
706
707 let result = encoder.encode(id);
708 assert!(result.is_ok());
710 assert_eq!(
711 result.unwrap().client_metadata,
712 SEQUENTIAL_METADATA_MARKER,
713 "Overflow should fall back to sequential allocation"
714 );
715 }
716
717 #[rstest]
718 fn test_date_before_base_epoch_falls_back_to_sequential() {
719 let encoder = ClientOrderIdEncoder::new();
720 let id = ClientOrderId::from("O-20191231-235959-001-001-1");
722
723 let result = encoder.encode(id);
724 assert!(result.is_ok());
726 let encoded = result.unwrap();
727 assert_eq!(
728 encoded.client_metadata, SEQUENTIAL_METADATA_MARKER,
729 "Pre-2020 dates should fall back to sequential allocation"
730 );
731
732 let decoded = encoder.decode(encoded.client_id, encoded.client_metadata);
734 assert_eq!(decoded, Some(id));
735 }
736
737 #[rstest]
738 fn test_encode_same_id_returns_same_value() {
739 let encoder = ClientOrderIdEncoder::new();
740 let id = ClientOrderId::from("O-20260131-174827-001-001-1");
741
742 let first = encoder.encode(id).unwrap();
743 let second = encoder.encode(id).unwrap();
744
745 assert_eq!(first, second);
746 }
747
748 #[rstest]
749 fn test_same_second_different_count_has_unique_client_ids() {
750 let encoder = ClientOrderIdEncoder::new();
753
754 let id1 = ClientOrderId::from("O-20260201-084653-001-001-1");
756 let id2 = ClientOrderId::from("O-20260201-084653-001-001-2");
757
758 let encoded1 = encoder.encode(id1).unwrap();
759 let encoded2 = encoder.encode(id2).unwrap();
760
761 assert_ne!(
763 encoded1.client_id, encoded2.client_id,
764 "Orders in the same second must have different client_ids for dYdX"
765 );
766
767 assert_eq!(encoded1.client_metadata, encoded2.client_metadata);
769
770 assert_eq!(
772 encoder.decode(encoded1.client_id, encoded1.client_metadata),
773 Some(id1)
774 );
775 assert_eq!(
776 encoder.decode(encoded2.client_id, encoded2.client_metadata),
777 Some(id2)
778 );
779 }
780
781 #[rstest]
782 fn test_encode_different_ids_returns_different_values() {
783 let encoder = ClientOrderIdEncoder::new();
784 let id1 = ClientOrderId::from("O-20260131-174827-001-001-1");
785 let id2 = ClientOrderId::from("O-20260131-174828-001-001-2");
786
787 let result1 = encoder.encode(id1).unwrap();
788 let result2 = encoder.encode(id2).unwrap();
789
790 assert_ne!(result1, result2);
791 }
792
793 #[rstest]
794 fn test_current_counter() {
795 let encoder = ClientOrderIdEncoder::new();
796 assert_eq!(encoder.current_counter(), 1);
797
798 encoder.encode(ClientOrderId::from("custom-1")).unwrap();
799 assert_eq!(encoder.current_counter(), 2);
800
801 encoder.encode(ClientOrderId::from("custom-2")).unwrap();
802 assert_eq!(encoder.current_counter(), 3);
803
804 encoder
806 .encode(ClientOrderId::from("O-20260131-174827-001-001-1"))
807 .unwrap();
808 assert_eq!(encoder.current_counter(), 3);
809 }
810
811 #[rstest]
812 fn test_is_empty() {
813 let encoder = ClientOrderIdEncoder::new();
814 assert!(encoder.is_empty());
815
816 encoder.encode(ClientOrderId::from("custom")).unwrap();
817 assert!(!encoder.is_empty());
818 }
819}