Skip to main content

nautilus_common/generators/
order_list_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_model::identifiers::{OrderListId, StrategyId, TraderId};
25
26use crate::clock::Clock;
27
28const DATETIME_TAG_LEN: usize = 15; // "YYYYMMDD-HHMMSS"
29const MAX_USIZE_DECIMAL_LEN: usize = 20; // Maximum decimal digits for a 64-bit usize
30
31pub struct OrderListIdGenerator {
32    clock: Rc<RefCell<dyn Clock>>,
33    trader_id: TraderId,
34    strategy_id: StrategyId,
35    count: usize,
36    trader_tag: String,
37    strategy_tag: String,
38    buf: String,
39    fixed_prefix_len: usize,
40    epoch_second: u64,
41    count_buf: Buffer,
42}
43
44impl Debug for OrderListIdGenerator {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct(stringify!(OrderListIdGenerator))
47            .field("clock", &self.clock)
48            .field("trader_id", &self.trader_id)
49            .field("strategy_id", &self.strategy_id)
50            .field("count", &self.count)
51            .field("trader_tag", &self.trader_tag)
52            .field("strategy_tag", &self.strategy_tag)
53            .field("buf", &self.buf)
54            .field("fixed_prefix_len", &self.fixed_prefix_len)
55            .field("epoch_second", &self.epoch_second)
56            .finish_non_exhaustive()
57    }
58}
59
60impl OrderListIdGenerator {
61    /// Creates a new [`OrderListIdGenerator`] instance.
62    #[must_use]
63    pub fn new(
64        trader_id: TraderId,
65        strategy_id: StrategyId,
66        initial_count: usize,
67        clock: Rc<RefCell<dyn Clock>>,
68    ) -> Self {
69        let trader_tag = trader_id.get_tag().to_string();
70        let strategy_tag = strategy_id.get_tag().to_string();
71        let buf = String::with_capacity(
72            fixed_prefix_capacity(&trader_tag, &strategy_tag) + MAX_USIZE_DECIMAL_LEN,
73        );
74
75        Self {
76            clock,
77            trader_id,
78            strategy_id,
79            count: initial_count,
80            trader_tag,
81            strategy_tag,
82            buf,
83            fixed_prefix_len: 0,
84            epoch_second: u64::MAX,
85            count_buf: Buffer::new(),
86        }
87    }
88
89    pub const fn set_count(&mut self, count: usize) {
90        self.count = count;
91    }
92
93    pub const fn reset(&mut self) {
94        self.count = 0;
95    }
96
97    #[must_use]
98    pub const fn count(&self) -> usize {
99        self.count
100    }
101
102    pub fn generate(&mut self) -> OrderListId {
103        let timestamp_ms = self.clock.borrow().timestamp_ms();
104        self.refresh_fixed_prefix(timestamp_ms);
105        self.count += 1;
106
107        self.buf.truncate(self.fixed_prefix_len);
108        self.buf.push_str(self.count_buf.format(self.count));
109
110        OrderListId::from(self.buf.as_str())
111    }
112
113    #[inline]
114    fn refresh_fixed_prefix(&mut self, timestamp_ms: u64) {
115        let epoch_second = timestamp_ms / 1_000;
116        if epoch_second == self.epoch_second {
117            return;
118        }
119
120        write_fixed_prefix(
121            &mut self.buf,
122            &self.trader_tag,
123            &self.strategy_tag,
124            epoch_second,
125        );
126        self.fixed_prefix_len = self.buf.len();
127        self.epoch_second = epoch_second;
128    }
129}
130
131#[inline]
132fn fixed_prefix_capacity(trader_tag: &str, strategy_tag: &str) -> usize {
133    "OL-".len()
134        + DATETIME_TAG_LEN
135        + "-".len()
136        + trader_tag.len()
137        + "-".len()
138        + strategy_tag.len()
139        + "-".len()
140}
141
142fn write_fixed_prefix(buf: &mut String, trader_tag: &str, strategy_tag: &str, epoch_second: u64) {
143    let now_utc = DateTime::from_timestamp_millis((epoch_second * 1_000) as i64)
144        .expect("Milliseconds timestamp should be within valid range");
145
146    buf.clear();
147
148    write!(
149        buf,
150        "OL-{:04}{:02}{:02}-{:02}{:02}{:02}-{trader_tag}-{strategy_tag}-",
151        now_utc.year(),
152        now_utc.month(),
153        now_utc.day(),
154        now_utc.hour(),
155        now_utc.minute(),
156        now_utc.second(),
157    )
158    .expect("writing to String should not fail");
159}
160
161#[cfg(test)]
162mod tests {
163    use std::{cell::RefCell, rc::Rc};
164
165    use nautilus_core::UnixNanos;
166    use nautilus_model::{
167        identifiers::{OrderListId, StrategyId, TraderId},
168        stubs::TestDefault,
169    };
170    use rstest::rstest;
171
172    use crate::{clock::TestClock, generators::order_list_id::OrderListIdGenerator};
173
174    fn get_order_list_id_generator(initial_count: Option<usize>) -> OrderListIdGenerator {
175        let clock = Rc::new(RefCell::new(TestClock::new()));
176        OrderListIdGenerator::new(
177            TraderId::test_default(),
178            StrategyId::test_default(),
179            initial_count.unwrap_or(0),
180            clock,
181        )
182    }
183
184    #[rstest]
185    fn test_init() {
186        let generator = get_order_list_id_generator(None);
187        assert_eq!(generator.count(), 0);
188    }
189
190    #[rstest]
191    fn test_init_with_initial_count() {
192        let generator = get_order_list_id_generator(Some(7));
193        assert_eq!(generator.count(), 7);
194    }
195
196    #[rstest]
197    fn test_generate_order_list_id_from_start() {
198        let mut generator = get_order_list_id_generator(None);
199        let result1 = generator.generate();
200        let result2 = generator.generate();
201        let result3 = generator.generate();
202
203        assert_eq!(result1, OrderListId::new("OL-19700101-000000-001-001-1"));
204        assert_eq!(result2, OrderListId::new("OL-19700101-000000-001-001-2"));
205        assert_eq!(result3, OrderListId::new("OL-19700101-000000-001-001-3"));
206    }
207
208    #[rstest]
209    fn test_generate_order_list_id_from_initial() {
210        let mut generator = get_order_list_id_generator(Some(5));
211        let result1 = generator.generate();
212        let result2 = generator.generate();
213        let result3 = generator.generate();
214
215        assert_eq!(result1, OrderListId::new("OL-19700101-000000-001-001-6"));
216        assert_eq!(result2, OrderListId::new("OL-19700101-000000-001-001-7"));
217        assert_eq!(result3, OrderListId::new("OL-19700101-000000-001-001-8"));
218    }
219
220    #[rstest]
221    fn test_generate_persists_fixed_prefix_in_buffer_within_same_second() {
222        let mut generator = get_order_list_id_generator(None);
223
224        let result1 = generator.generate();
225        let fixed_prefix = "OL-19700101-000000-001-001-";
226        let capacity_after_first = generator.buf.capacity();
227
228        assert_eq!(result1, OrderListId::new("OL-19700101-000000-001-001-1"));
229        assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
230        assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
231
232        let result2 = generator.generate();
233
234        assert_eq!(result2, OrderListId::new("OL-19700101-000000-001-001-2"));
235        assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
236        assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
237        assert_eq!(generator.buf.capacity(), capacity_after_first);
238    }
239
240    #[rstest]
241    fn test_generate_refreshes_persistent_fixed_prefix_when_second_changes() {
242        let clock = Rc::new(RefCell::new(TestClock::new()));
243        let mut generator = OrderListIdGenerator::new(
244            TraderId::test_default(),
245            StrategyId::test_default(),
246            0,
247            clock.clone(),
248        );
249
250        let result1 = generator.generate();
251        clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
252        let result2 = generator.generate();
253
254        assert_eq!(result1, OrderListId::new("OL-19700101-000000-001-001-1"));
255        assert_eq!(result2, OrderListId::new("OL-19700101-000001-001-001-2"));
256        assert_eq!(generator.epoch_second, 1);
257        assert_eq!(
258            &generator.buf[..generator.fixed_prefix_len],
259            "OL-19700101-000001-001-001-"
260        );
261    }
262
263    #[rstest]
264    fn test_reset() {
265        let mut generator = get_order_list_id_generator(None);
266        generator.generate();
267        generator.generate();
268        generator.reset();
269        let result = generator.generate();
270
271        assert_eq!(result, OrderListId::new("OL-19700101-000000-001-001-1"));
272    }
273}