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 core::fmt::NumBuffer;
17use std::{
18    cell::RefCell,
19    fmt::{Debug, Write},
20    rc::Rc,
21};
22
23use jiff::{Timestamp, tz::Offset};
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: NumBuffer<usize>,
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: NumBuffer::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
109            .push_str(self.count.format_into(&mut self.count_buf));
110
111        OrderListId::from(self.buf.as_str())
112    }
113
114    #[inline]
115    fn refresh_fixed_prefix(&mut self, timestamp_ms: u64) {
116        let epoch_second = timestamp_ms / 1_000;
117        if epoch_second == self.epoch_second {
118            return;
119        }
120
121        write_fixed_prefix(
122            &mut self.buf,
123            &self.trader_tag,
124            &self.strategy_tag,
125            epoch_second,
126        );
127        self.fixed_prefix_len = self.buf.len();
128        self.epoch_second = epoch_second;
129    }
130}
131
132#[inline]
133fn fixed_prefix_capacity(trader_tag: &str, strategy_tag: &str) -> usize {
134    "OL-".len()
135        + DATETIME_TAG_LEN
136        + "-".len()
137        + trader_tag.len()
138        + "-".len()
139        + strategy_tag.len()
140        + "-".len()
141}
142
143fn write_fixed_prefix(buf: &mut String, trader_tag: &str, strategy_tag: &str, epoch_second: u64) {
144    let now_utc = Offset::UTC.to_datetime(
145        Timestamp::from_second(
146            i64::try_from(epoch_second).expect("seconds timestamp should fit i64"),
147        )
148        .expect("seconds timestamp should be within valid range"),
149    );
150
151    buf.clear();
152
153    write!(
154        buf,
155        "OL-{:04}{:02}{:02}-{:02}{:02}{:02}-{trader_tag}-{strategy_tag}-",
156        now_utc.year(),
157        now_utc.month(),
158        now_utc.day(),
159        now_utc.hour(),
160        now_utc.minute(),
161        now_utc.second(),
162    )
163    .expect("writing to String should not fail");
164}
165
166#[cfg(test)]
167mod tests {
168    use std::{cell::RefCell, rc::Rc};
169
170    use nautilus_core::UnixNanos;
171    use nautilus_model::{
172        identifiers::{OrderListId, StrategyId, TraderId},
173        stubs::TestDefault,
174    };
175    use rstest::rstest;
176
177    use crate::{clock::TestClock, generators::order_list_id::OrderListIdGenerator};
178
179    fn get_order_list_id_generator(initial_count: Option<usize>) -> OrderListIdGenerator {
180        let clock = Rc::new(RefCell::new(TestClock::new()));
181        OrderListIdGenerator::new(
182            TraderId::test_default(),
183            StrategyId::test_default(),
184            initial_count.unwrap_or(0),
185            clock,
186        )
187    }
188
189    #[rstest]
190    fn test_init() {
191        let generator = get_order_list_id_generator(None);
192        assert_eq!(generator.count(), 0);
193    }
194
195    #[rstest]
196    fn test_init_with_initial_count() {
197        let generator = get_order_list_id_generator(Some(7));
198        assert_eq!(generator.count(), 7);
199    }
200
201    #[rstest]
202    fn test_generate_order_list_id_from_start() {
203        let mut generator = get_order_list_id_generator(None);
204        let result1 = generator.generate();
205        let result2 = generator.generate();
206        let result3 = generator.generate();
207
208        assert_eq!(result1, OrderListId::new("OL-19700101-000000-001-001-1"));
209        assert_eq!(result2, OrderListId::new("OL-19700101-000000-001-001-2"));
210        assert_eq!(result3, OrderListId::new("OL-19700101-000000-001-001-3"));
211    }
212
213    #[rstest]
214    fn test_generate_order_list_id_from_initial() {
215        let mut generator = get_order_list_id_generator(Some(5));
216        let result1 = generator.generate();
217        let result2 = generator.generate();
218        let result3 = generator.generate();
219
220        assert_eq!(result1, OrderListId::new("OL-19700101-000000-001-001-6"));
221        assert_eq!(result2, OrderListId::new("OL-19700101-000000-001-001-7"));
222        assert_eq!(result3, OrderListId::new("OL-19700101-000000-001-001-8"));
223    }
224
225    #[rstest]
226    fn test_generate_persists_fixed_prefix_in_buffer_within_same_second() {
227        let mut generator = get_order_list_id_generator(None);
228
229        let result1 = generator.generate();
230        let fixed_prefix = "OL-19700101-000000-001-001-";
231        let capacity_after_first = generator.buf.capacity();
232
233        assert_eq!(result1, OrderListId::new("OL-19700101-000000-001-001-1"));
234        assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
235        assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
236
237        let result2 = generator.generate();
238
239        assert_eq!(result2, OrderListId::new("OL-19700101-000000-001-001-2"));
240        assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
241        assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
242        assert_eq!(generator.buf.capacity(), capacity_after_first);
243    }
244
245    #[rstest]
246    fn test_generate_refreshes_persistent_fixed_prefix_when_second_changes() {
247        let clock = Rc::new(RefCell::new(TestClock::new()));
248        let mut generator = OrderListIdGenerator::new(
249            TraderId::test_default(),
250            StrategyId::test_default(),
251            0,
252            clock.clone(),
253        );
254
255        let result1 = generator.generate();
256        clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
257        let result2 = generator.generate();
258
259        assert_eq!(result1, OrderListId::new("OL-19700101-000000-001-001-1"));
260        assert_eq!(result2, OrderListId::new("OL-19700101-000001-001-001-2"));
261        assert_eq!(generator.epoch_second, 1);
262        assert_eq!(
263            &generator.buf[..generator.fixed_prefix_len],
264            "OL-19700101-000001-001-001-"
265        );
266    }
267
268    #[rstest]
269    fn test_reset() {
270        let mut generator = get_order_list_id_generator(None);
271        generator.generate();
272        generator.generate();
273        generator.reset();
274        let result = generator.generate();
275
276        assert_eq!(result, OrderListId::new("OL-19700101-000000-001-001-1"));
277    }
278}