nautilus_common/generators/
client_order_id.rs1use core::fmt::NumBuffer;
17use std::{
18 cell::RefCell,
19 fmt::{Debug, Write},
20 rc::Rc,
21};
22
23use jiff::{Timestamp, tz::Offset};
24use nautilus_core::uuid::UUID4;
25use nautilus_model::identifiers::{ClientOrderId, StrategyId, TraderId};
26
27use crate::clock::Clock;
28
29const DATETIME_TAG_LEN: usize = 15; const DATETIME_TAG_COMPACT_LEN: usize = 14; const MAX_USIZE_DECIMAL_LEN: usize = 20; #[inline]
34fn fixed_prefix_capacity(trader_tag: &str, strategy_tag: &str, use_hyphens: bool) -> usize {
35 if use_hyphens {
36 "O-".len()
37 + DATETIME_TAG_LEN
38 + "-".len()
39 + trader_tag.len()
40 + "-".len()
41 + strategy_tag.len()
42 + "-".len()
43 } else {
44 "O".len() + DATETIME_TAG_COMPACT_LEN + trader_tag.len() + strategy_tag.len()
45 }
46}
47
48fn write_fixed_prefix(
50 buf: &mut String,
51 trader_tag: &str,
52 strategy_tag: &str,
53 use_hyphens: bool,
54 epoch_second: u64,
55) {
56 let now_utc = Offset::UTC.to_datetime(
57 Timestamp::from_second(
58 i64::try_from(epoch_second).expect("seconds timestamp should fit i64"),
59 )
60 .expect("seconds timestamp should be within valid range"),
61 );
62
63 buf.clear();
64
65 if use_hyphens {
66 write!(
67 buf,
68 "O-{:04}{:02}{:02}-{:02}{:02}{:02}-{trader_tag}-{strategy_tag}-",
69 now_utc.year(),
70 now_utc.month(),
71 now_utc.day(),
72 now_utc.hour(),
73 now_utc.minute(),
74 now_utc.second(),
75 )
76 .expect("writing to String should not fail");
77 } else {
78 write!(
79 buf,
80 "O{:04}{:02}{:02}{:02}{:02}{:02}{trader_tag}{strategy_tag}",
81 now_utc.year(),
82 now_utc.month(),
83 now_utc.day(),
84 now_utc.hour(),
85 now_utc.minute(),
86 now_utc.second(),
87 )
88 .expect("writing to String should not fail");
89 }
90}
91
92pub struct ClientOrderIdGenerator {
93 clock: Rc<RefCell<dyn Clock>>,
94 trader_id: TraderId,
95 strategy_id: StrategyId,
96 count: usize,
97 use_uuids: bool,
98 use_hyphens: bool,
99 trader_tag: String,
100 strategy_tag: String,
101 buf: String,
102 fixed_prefix_len: usize,
103 epoch_second: u64,
104 count_buf: NumBuffer<usize>,
105}
106
107impl Debug for ClientOrderIdGenerator {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.debug_struct(stringify!(ClientOrderIdGenerator))
110 .field("clock", &self.clock)
111 .field("trader_id", &self.trader_id)
112 .field("strategy_id", &self.strategy_id)
113 .field("count", &self.count)
114 .field("use_uuids", &self.use_uuids)
115 .field("use_hyphens", &self.use_hyphens)
116 .field("trader_tag", &self.trader_tag)
117 .field("strategy_tag", &self.strategy_tag)
118 .field("buf", &self.buf)
119 .field("fixed_prefix_len", &self.fixed_prefix_len)
120 .field("epoch_second", &self.epoch_second)
121 .finish_non_exhaustive()
122 }
123}
124
125impl ClientOrderIdGenerator {
126 #[must_use]
128 pub fn new(
129 trader_id: TraderId,
130 strategy_id: StrategyId,
131 initial_count: usize,
132 clock: Rc<RefCell<dyn Clock>>,
133 use_uuids: bool,
134 use_hyphens: bool,
135 ) -> Self {
136 let trader_tag = trader_id.get_tag().to_string();
137 let strategy_tag = strategy_id.get_tag().to_string();
138 let buf = String::with_capacity(
139 fixed_prefix_capacity(&trader_tag, &strategy_tag, use_hyphens) + MAX_USIZE_DECIMAL_LEN,
140 );
141
142 Self {
143 trader_id,
144 strategy_id,
145 count: initial_count,
146 clock,
147 use_uuids,
148 use_hyphens,
149 trader_tag,
150 strategy_tag,
151 buf,
152 fixed_prefix_len: 0,
153 epoch_second: u64::MAX,
154 count_buf: NumBuffer::new(),
155 }
156 }
157
158 pub const fn set_count(&mut self, count: usize) {
159 self.count = count;
160 }
161
162 pub const fn reset(&mut self) {
163 self.count = 0;
164 }
165
166 #[must_use]
167 pub const fn count(&self) -> usize {
168 self.count
169 }
170
171 #[inline]
172 fn refresh_fixed_prefix(&mut self, timestamp_ms: u64) {
173 let epoch_second = timestamp_ms / 1_000;
174 if epoch_second == self.epoch_second {
175 return;
176 }
177
178 write_fixed_prefix(
181 &mut self.buf,
182 &self.trader_tag,
183 &self.strategy_tag,
184 self.use_hyphens,
185 epoch_second,
186 );
187 self.fixed_prefix_len = self.buf.len();
188 self.epoch_second = epoch_second;
189 }
190
191 pub fn generate(&mut self) -> ClientOrderId {
192 if self.use_uuids {
193 let mut uuid_value = UUID4::new().to_string();
194
195 if !self.use_hyphens {
196 uuid_value = uuid_value.replace('-', "");
197 }
198 return ClientOrderId::from(uuid_value);
199 }
200
201 let timestamp_ms = self.clock.borrow().timestamp_ms();
202 self.refresh_fixed_prefix(timestamp_ms);
203 self.count += 1;
204
205 self.buf.truncate(self.fixed_prefix_len);
208 self.buf
209 .push_str(self.count.format_into(&mut self.count_buf));
210
211 ClientOrderId::from(self.buf.as_str())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use std::{cell::RefCell, rc::Rc};
218
219 use nautilus_core::UnixNanos;
220 use nautilus_model::{
221 identifiers::{ClientOrderId, StrategyId, TraderId},
222 stubs::TestDefault,
223 };
224 use rstest::rstest;
225
226 use crate::{clock::TestClock, generators::client_order_id::ClientOrderIdGenerator};
227
228 fn get_client_order_id_generator(
229 initial_count: Option<usize>,
230 use_uuids: bool,
231 use_hyphens: bool,
232 ) -> ClientOrderIdGenerator {
233 let clock = Rc::new(RefCell::new(TestClock::new()));
234 ClientOrderIdGenerator::new(
235 TraderId::test_default(),
236 StrategyId::test_default(),
237 initial_count.unwrap_or(0),
238 clock,
239 use_uuids,
240 use_hyphens,
241 )
242 }
243
244 #[rstest]
245 fn test_init() {
246 let generator = get_client_order_id_generator(None, false, true);
247 assert_eq!(generator.count(), 0);
248 }
249
250 #[rstest]
251 fn test_init_with_initial_count() {
252 let generator = get_client_order_id_generator(Some(7), false, true);
253 assert_eq!(generator.count(), 7);
254 }
255
256 #[rstest]
257 fn test_generate_client_order_id_from_start() {
258 let mut generator = get_client_order_id_generator(None, false, true);
259 let result1 = generator.generate();
260 let result2 = generator.generate();
261 let result3 = generator.generate();
262
263 assert_eq!(result1, ClientOrderId::new("O-19700101-000000-001-001-1"));
264 assert_eq!(result2, ClientOrderId::new("O-19700101-000000-001-001-2"));
265 assert_eq!(result3, ClientOrderId::new("O-19700101-000000-001-001-3"));
266 }
267
268 #[rstest]
269 fn test_generate_client_order_id_from_initial() {
270 let mut generator = get_client_order_id_generator(Some(5), false, true);
271 let result1 = generator.generate();
272 let result2 = generator.generate();
273 let result3 = generator.generate();
274
275 assert_eq!(result1, ClientOrderId::new("O-19700101-000000-001-001-6"));
276 assert_eq!(result2, ClientOrderId::new("O-19700101-000000-001-001-7"));
277 assert_eq!(result3, ClientOrderId::new("O-19700101-000000-001-001-8"));
278 }
279
280 #[rstest]
281 fn test_generate_client_order_id_with_hyphens_removed() {
282 let mut generator = get_client_order_id_generator(None, false, false);
283 let result = generator.generate();
284
285 assert_eq!(result, ClientOrderId::new("O197001010000000010011"));
286 }
287
288 #[rstest]
289 fn test_generate_persists_fixed_prefix_in_buffer_within_same_second() {
290 let mut generator = get_client_order_id_generator(None, false, true);
291
292 let result1 = generator.generate();
293 let fixed_prefix = "O-19700101-000000-001-001-";
294 let capacity_after_first = generator.buf.capacity();
295
296 assert_eq!(result1, ClientOrderId::new("O-19700101-000000-001-001-1"));
297 assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
298 assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
299
300 let result2 = generator.generate();
301
302 assert_eq!(result2, ClientOrderId::new("O-19700101-000000-001-001-2"));
303 assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
304 assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
305 assert_eq!(generator.buf.capacity(), capacity_after_first);
306 }
307
308 #[rstest]
309 fn test_generate_persists_compact_fixed_prefix_in_buffer() {
310 let mut generator = get_client_order_id_generator(None, false, false);
311
312 let result = generator.generate();
313 let fixed_prefix = "O19700101000000001001";
314
315 assert_eq!(result, ClientOrderId::new("O197001010000000010011"));
316 assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
317 assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
318 }
319
320 #[rstest]
321 fn test_generate_refreshes_persistent_fixed_prefix_when_second_changes() {
322 let clock = Rc::new(RefCell::new(TestClock::new()));
323 let mut generator = ClientOrderIdGenerator::new(
324 TraderId::test_default(),
325 StrategyId::test_default(),
326 0,
327 clock.clone(),
328 false,
329 true,
330 );
331
332 let result1 = generator.generate();
333 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
334 let result2 = generator.generate();
335
336 assert_eq!(result1, ClientOrderId::new("O-19700101-000000-001-001-1"));
337 assert_eq!(result2, ClientOrderId::new("O-19700101-000001-001-001-2"));
338 assert_eq!(generator.epoch_second, 1);
339 assert_eq!(
340 &generator.buf[..generator.fixed_prefix_len],
341 "O-19700101-000001-001-001-"
342 );
343 }
344
345 #[rstest]
346 fn test_generate_uuid_client_order_id() {
347 let mut generator = get_client_order_id_generator(None, true, true);
348 let result = generator.generate();
349
350 assert_eq!(result.as_str().len(), 36);
352 assert!(result.as_str().contains('-'));
353 }
354
355 #[rstest]
356 fn test_generate_uuid_client_order_id_with_hyphens_removed() {
357 let mut generator = get_client_order_id_generator(None, true, false);
358 let result = generator.generate();
359
360 assert_eq!(result.as_str().len(), 32);
362 assert!(!result.as_str().contains('-'));
363 }
364
365 #[rstest]
366 fn test_reset() {
367 let mut generator = get_client_order_id_generator(None, false, true);
368 generator.generate();
369 generator.generate();
370 generator.reset();
371 let result = generator.generate();
372
373 assert_eq!(result, ClientOrderId::new("O-19700101-000000-001-001-1"));
374 }
375}