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