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