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_uses_final_strategy_tag() {
179 let mut generator = get_position_id_generator();
180 let result = generator.generate(StrategyId::from("ExampleStrategy-XNAS-T01"), false);
181
182 assert_eq!(result, PositionId::from("P-19700101-000000-001-T01-1"));
183 }
184
185 #[rstest]
186 fn test_generate_position_id_multiple_strategies() {
187 let mut generator = get_position_id_generator();
188 let result1 = generator.generate(StrategyId::from("S-001"), false);
189 let result2 = generator.generate(StrategyId::from("S-002"), false);
190 let result3 = generator.generate(StrategyId::from("S-002"), false);
191
192 assert_eq!(result1, PositionId::from("P-19700101-000000-001-001-1"));
193 assert_eq!(result2, PositionId::from("P-19700101-000000-001-002-1"));
194 assert_eq!(result3, PositionId::from("P-19700101-000000-001-002-2"));
195 }
196
197 #[rstest]
198 fn test_generate_position_id_with_flipped_appends_correctly() {
199 let mut generator = get_position_id_generator();
200 let result1 = generator.generate(StrategyId::from("S-001"), false);
201 let result2 = generator.generate(StrategyId::from("S-002"), true);
202 let result3 = generator.generate(StrategyId::from("S-001"), true);
203
204 assert_eq!(result1, PositionId::from("P-19700101-000000-001-001-1"));
205 assert_eq!(result2, PositionId::from("P-19700101-000000-001-002-1F"));
206 assert_eq!(result3, PositionId::from("P-19700101-000000-001-001-2F"));
207 }
208
209 #[rstest]
210 fn test_generate_persists_fixed_prefix_in_buffer_within_same_second() {
211 let mut generator = get_position_id_generator();
212
213 let result1 = generator.generate(StrategyId::from("S-001"), false);
214 let fixed_prefix = "P-19700101-000000-001-";
215 let capacity_after_first = generator.buf.capacity();
216
217 assert_eq!(result1, PositionId::from("P-19700101-000000-001-001-1"));
218 assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
219 assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
220
221 let result2 = generator.generate(StrategyId::from("S-001"), false);
222
223 assert_eq!(result2, PositionId::from("P-19700101-000000-001-001-2"));
224 assert_eq!(generator.fixed_prefix_len, fixed_prefix.len());
225 assert_eq!(&generator.buf[..generator.fixed_prefix_len], fixed_prefix);
226 assert_eq!(generator.buf.capacity(), capacity_after_first);
227 }
228
229 #[rstest]
230 fn test_generate_capacity_stable_across_strategies_same_second() {
231 let mut generator = get_position_id_generator();
232
233 generator.generate(StrategyId::from("S-001"), false);
236 let capacity_after_warmup = generator.buf.capacity();
237
238 for tag in ["S-002", "S-003", "STRATEGY-LONGER-001"] {
239 generator.generate(StrategyId::from(tag), false);
240 }
241
242 assert_eq!(generator.buf.capacity(), capacity_after_warmup);
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 = PositionIdGenerator::new(TraderId::test_default(), clock.clone());
249
250 let result1 = generator.generate(StrategyId::from("S-001"), false);
251 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
252 let result2 = generator.generate(StrategyId::from("S-001"), false);
253
254 assert_eq!(result1, PositionId::from("P-19700101-000000-001-001-1"));
255 assert_eq!(result2, PositionId::from("P-19700101-000001-001-001-2"));
256 assert_eq!(generator.epoch_second, 1);
257 assert_eq!(
258 &generator.buf[..generator.fixed_prefix_len],
259 "P-19700101-000001-001-"
260 );
261 }
262
263 #[rstest]
264 fn test_get_count_when_strategy_id_has_not_been_used() {
265 let generator = get_position_id_generator();
266 let result = generator.count(StrategyId::from("S-001"));
267
268 assert_eq!(result, 0);
269 }
270
271 #[rstest]
272 fn set_count_with_valid_strategy() {
273 let mut generator = get_position_id_generator();
274 generator.set_count(7, StrategyId::from("S-001"));
275 let result = generator.count(StrategyId::from("S-001"));
276
277 assert_eq!(result, 7);
278 }
279
280 #[rstest]
281 fn test_reset() {
282 let mut generator = get_position_id_generator();
283 generator.generate(StrategyId::from("S-001"), false);
284 generator.generate(StrategyId::from("S-001"), false);
285 generator.reset();
286 let result = generator.generate(StrategyId::from("S-001"), false);
287
288 assert_eq!(result, PositionId::from("P-19700101-000000-001-001-1"));
289 }
290}