nautilus_lighter/common/
parse.rs1use std::str::FromStr;
25
26use nautilus_core::{
27 UnixNanos,
28 datetime::{NANOSECONDS_IN_MILLISECOND, NANOSECONDS_IN_SECOND},
29};
30use nautilus_model::types::{Price, Quantity, fixed::FIXED_PRECISION};
31use rust_decimal::Decimal;
32
33pub const MAX_DECIMALS: u8 = FIXED_PRECISION;
35
36pub fn parse_price_from_ticks(ticks: u32, decimals: u8) -> anyhow::Result<Price> {
46 anyhow::ensure!(
47 decimals <= MAX_DECIMALS,
48 "price decimals {decimals} exceeds maximum {MAX_DECIMALS}",
49 );
50 let exponent = -(decimals as i8);
51 Ok(Price::from_mantissa_exponent(
52 i64::from(ticks),
53 exponent,
54 decimals,
55 ))
56}
57
58pub fn parse_quantity_from_ticks(ticks: i64, decimals: u8) -> anyhow::Result<Quantity> {
74 anyhow::ensure!(
75 decimals <= MAX_DECIMALS,
76 "size decimals {decimals} exceeds maximum {MAX_DECIMALS}",
77 );
78 anyhow::ensure!(ticks >= 0, "negative tick count {ticks} for Quantity");
79 let decimal = Decimal::new(ticks, u32::from(decimals));
80 Quantity::from_decimal_dp(decimal, decimals).map_err(|e| {
81 anyhow::anyhow!("Quantity overflow for ticks={ticks}, decimals={decimals}: {e}")
82 })
83}
84
85pub fn parse_price(value: &str, precision: u8) -> anyhow::Result<Price> {
92 anyhow::ensure!(
93 precision <= MAX_DECIMALS,
94 "price precision {precision} exceeds maximum {MAX_DECIMALS}",
95 );
96 let decimal =
97 Decimal::from_str(value).map_err(|e| anyhow::anyhow!("invalid price `{value}`: {e}"))?;
98 Price::from_decimal_dp(decimal, precision)
99 .map_err(|e| anyhow::anyhow!("invalid price `{value}` at precision {precision}: {e}"))
100}
101
102pub fn parse_quantity(value: &str, precision: u8) -> anyhow::Result<Quantity> {
113 anyhow::ensure!(
114 precision <= MAX_DECIMALS,
115 "size precision {precision} exceeds maximum {MAX_DECIMALS}",
116 );
117 let decimal =
118 Decimal::from_str(value).map_err(|e| anyhow::anyhow!("invalid quantity `{value}`: {e}"))?;
119 anyhow::ensure!(decimal.is_sign_positive(), "negative quantity `{value}`");
120 Quantity::from_decimal_dp(decimal, precision)
121 .map_err(|e| anyhow::anyhow!("invalid quantity `{value}` at precision {precision}: {e}"))
122}
123
124pub fn price_from_decimal(value: Decimal, precision: u8) -> anyhow::Result<Price> {
134 anyhow::ensure!(
135 precision <= MAX_DECIMALS,
136 "price precision {precision} exceeds maximum {MAX_DECIMALS}",
137 );
138 Price::from_decimal_dp(value, precision)
139 .map_err(|e| anyhow::anyhow!("invalid price `{value}` at precision {precision}: {e}"))
140}
141
142pub fn quantity_from_decimal(value: Decimal, precision: u8) -> anyhow::Result<Quantity> {
153 anyhow::ensure!(
154 precision <= MAX_DECIMALS,
155 "size precision {precision} exceeds maximum {MAX_DECIMALS}",
156 );
157 anyhow::ensure!(value.is_sign_positive(), "negative quantity `{value}`");
158 Quantity::from_decimal_dp(value, precision)
159 .map_err(|e| anyhow::anyhow!("invalid quantity `{value}` at precision {precision}: {e}"))
160}
161
162pub fn parse_millis_to_nanos(millis: u64) -> anyhow::Result<UnixNanos> {
170 let nanos = millis
171 .checked_mul(NANOSECONDS_IN_MILLISECOND)
172 .ok_or_else(|| {
173 anyhow::anyhow!("millisecond timestamp {millis} overflows when scaled to nanoseconds")
174 })?;
175 Ok(UnixNanos::from(nanos))
176}
177
178pub fn parse_micros_to_nanos(micros: u64) -> anyhow::Result<UnixNanos> {
184 let nanos = micros.checked_mul(1_000).ok_or_else(|| {
185 anyhow::anyhow!("microsecond timestamp {micros} overflows when scaled to nanoseconds")
186 })?;
187 Ok(UnixNanos::from(nanos))
188}
189
190pub fn parse_secs_to_nanos(secs: u64) -> anyhow::Result<UnixNanos> {
196 let nanos = secs.checked_mul(NANOSECONDS_IN_SECOND).ok_or_else(|| {
197 anyhow::anyhow!("second timestamp {secs} overflows when scaled to nanoseconds")
198 })?;
199 Ok(UnixNanos::from(nanos))
200}
201
202pub fn parse_optional_millis_to_nanos(millis: i64) -> anyhow::Result<Option<UnixNanos>> {
215 if millis < 0 {
216 Ok(None)
217 } else {
218 parse_millis_to_nanos(millis as u64).map(Some)
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use rstest::rstest;
225
226 use super::*;
227
228 #[rstest]
229 fn parse_price_zero_decimals_is_integer() {
230 let price = parse_price_from_ticks(42, 0).unwrap();
231 assert_eq!(price.precision, 0);
232 assert_eq!(price.to_string(), "42");
233 }
234
235 #[rstest]
236 fn parse_price_with_decimals_inserts_decimal_point() {
237 let price = parse_price_from_ticks(405_000, 2).unwrap();
239 assert_eq!(price.precision, 2);
240 assert_eq!(price.to_string(), "4050.00");
241 }
242
243 #[rstest]
244 fn parse_price_at_max_decimals() {
245 let price = parse_price_from_ticks(1, MAX_DECIMALS).unwrap();
246 assert_eq!(price.precision, MAX_DECIMALS);
247 }
248
249 #[rstest]
250 fn parse_price_rejects_decimals_above_max() {
251 let err = parse_price_from_ticks(1, MAX_DECIMALS + 1).unwrap_err();
252 assert!(err.to_string().contains("exceeds maximum"));
253 }
254
255 #[rstest]
256 fn parse_quantity_with_decimals_inserts_decimal_point() {
257 let qty = parse_quantity_from_ticks(1_000, 3).unwrap();
259 assert_eq!(qty.precision, 3);
260 assert_eq!(qty.to_string(), "1.000");
261 }
262
263 #[rstest]
264 fn parse_quantity_rejects_negative_ticks() {
265 let err = parse_quantity_from_ticks(-1, 2).unwrap_err();
266 assert!(err.to_string().contains("negative tick count"));
267 }
268
269 #[rstest]
270 fn parse_quantity_rejects_oversized_ticks() {
271 let err = parse_quantity_from_ticks(i64::MAX, 0).unwrap_err();
273 assert!(err.to_string().contains("Quantity overflow"));
274 }
275
276 #[rstest]
277 fn parse_quantity_zero_is_valid() {
278 let qty = parse_quantity_from_ticks(0, 4).unwrap();
279 assert_eq!(qty.as_f64(), 0.0);
280 assert_eq!(qty.precision, 4);
281 }
282
283 #[rstest]
284 fn parse_price_from_decimal_string() {
285 let price = parse_price("2352.73", 2).unwrap();
286 assert_eq!(price.to_string(), "2352.73");
287 assert_eq!(price.precision, 2);
288 }
289
290 #[rstest]
291 fn parse_price_rejects_invalid_decimal() {
292 let err = parse_price("not-a-price", 2).unwrap_err();
293 assert!(err.to_string().contains("invalid price"));
294 }
295
296 #[rstest]
297 fn parse_quantity_from_decimal_string() {
298 let quantity = parse_quantity("0.1336", 4).unwrap();
299 assert_eq!(quantity.to_string(), "0.1336");
300 assert_eq!(quantity.precision, 4);
301 }
302
303 #[rstest]
304 fn parse_quantity_rejects_negative_decimal_string() {
305 let err = parse_quantity("-0.1", 4).unwrap_err();
306 assert!(err.to_string().contains("negative quantity"));
307 }
308
309 #[rstest]
310 fn parse_millis_to_nanos_scales_correctly() {
311 assert_eq!(parse_millis_to_nanos(0).unwrap(), UnixNanos::from(0));
312 assert_eq!(
313 parse_millis_to_nanos(1).unwrap(),
314 UnixNanos::from(1_000_000),
315 );
316 assert_eq!(
317 parse_millis_to_nanos(1_700_000_000_000).unwrap(),
318 UnixNanos::from(1_700_000_000_000_000_000),
319 );
320 }
321
322 #[rstest]
323 fn parse_millis_to_nanos_rejects_overflow() {
324 let err = parse_millis_to_nanos(u64::MAX).unwrap_err();
325 assert!(err.to_string().contains("overflows"));
326 }
327
328 #[rstest]
329 fn parse_micros_to_nanos_scales_correctly() {
330 assert_eq!(parse_micros_to_nanos(0).unwrap(), UnixNanos::from(0));
331 assert_eq!(parse_micros_to_nanos(1).unwrap(), UnixNanos::from(1_000));
332 assert_eq!(
333 parse_micros_to_nanos(1_700_000_000_000_000).unwrap(),
334 UnixNanos::from(1_700_000_000_000_000_000),
335 );
336 }
337
338 #[rstest]
339 fn parse_micros_to_nanos_rejects_overflow() {
340 let err = parse_micros_to_nanos(u64::MAX).unwrap_err();
341 assert!(err.to_string().contains("overflows"));
342 }
343
344 #[rstest]
345 fn parse_secs_to_nanos_scales_correctly() {
346 assert_eq!(parse_secs_to_nanos(0).unwrap(), UnixNanos::from(0));
347 assert_eq!(
348 parse_secs_to_nanos(1).unwrap(),
349 UnixNanos::from(1_000_000_000),
350 );
351 }
352
353 #[rstest]
354 fn parse_secs_to_nanos_rejects_overflow() {
355 let err = parse_secs_to_nanos(u64::MAX).unwrap_err();
356 assert!(err.to_string().contains("overflows"));
357 }
358
359 #[rstest]
360 fn parse_optional_millis_returns_none_for_negative() {
361 assert!(parse_optional_millis_to_nanos(-1).unwrap().is_none());
362 assert!(parse_optional_millis_to_nanos(i64::MIN).unwrap().is_none());
363 }
364
365 #[rstest]
366 fn parse_optional_millis_returns_some_for_non_negative() {
367 assert_eq!(
368 parse_optional_millis_to_nanos(0).unwrap(),
369 Some(UnixNanos::from(0)),
370 );
371 assert_eq!(
372 parse_optional_millis_to_nanos(1_500).unwrap(),
373 Some(UnixNanos::from(1_500_000_000)),
374 );
375 }
376
377 #[rstest]
378 fn parse_optional_millis_propagates_overflow() {
379 let err = parse_optional_millis_to_nanos(i64::MAX).unwrap_err();
380 assert!(err.to_string().contains("overflows"));
381 }
382}