Skip to main content

nautilus_common/generators/
client_order_id.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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; // "YYYYMMDD-HHMMSS"
30const DATETIME_TAG_COMPACT_LEN: usize = 14; // "YYYYMMDDHHMMSS"
31const MAX_USIZE_DECIMAL_LEN: usize = 20; // Maximum decimal digits for a 64-bit usize
32
33#[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
48/// Slow path across second boundaries: rebuilds the fixed prefix directly in the output buffer.
49fn 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    /// Creates a new [`ClientOrderIdGenerator`] instance.
123    #[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        // Rewrite the fixed prefix only when the second changes; the same-second hot path reuses
175        // the existing prefix in `buf`.
176        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        // The hot path only truncates the old count and appends the new count, avoiding repeated
202        // copies of the fixed prefix.
203        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        // UUID should be 36 characters with hyphens
346        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        // UUID without hyphens should be 32 characters
356        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}