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 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; // "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 = 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    /// Creates a new [`ClientOrderIdGenerator`] instance.
127    #[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        // Rewrite the fixed prefix only when the second changes; the same-second hot path reuses
179        // the existing prefix in `buf`.
180        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        // The hot path only truncates the old count and appends the new count, avoiding repeated
206        // copies of the fixed prefix.
207        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        // UUID should be 36 characters with hyphens
351        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        // UUID without hyphens should be 32 characters
361        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}