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
162#[must_use]
165pub fn funding_rate_from_percent(percent: Decimal) -> Decimal {
166 percent / Decimal::ONE_HUNDRED
167}
168
169pub fn parse_millis_to_nanos(millis: u64) -> anyhow::Result<UnixNanos> {
177 let nanos = millis
178 .checked_mul(NANOSECONDS_IN_MILLISECOND)
179 .ok_or_else(|| {
180 anyhow::anyhow!("millisecond timestamp {millis} overflows when scaled to nanoseconds")
181 })?;
182 Ok(UnixNanos::from(nanos))
183}
184
185pub fn parse_micros_to_nanos(micros: u64) -> anyhow::Result<UnixNanos> {
191 let nanos = micros.checked_mul(1_000).ok_or_else(|| {
192 anyhow::anyhow!("microsecond timestamp {micros} overflows when scaled to nanoseconds")
193 })?;
194 Ok(UnixNanos::from(nanos))
195}
196
197pub fn parse_secs_to_nanos(secs: u64) -> anyhow::Result<UnixNanos> {
203 let nanos = secs.checked_mul(NANOSECONDS_IN_SECOND).ok_or_else(|| {
204 anyhow::anyhow!("second timestamp {secs} overflows when scaled to nanoseconds")
205 })?;
206 Ok(UnixNanos::from(nanos))
207}
208
209pub fn parse_optional_millis_to_nanos(millis: i64) -> anyhow::Result<Option<UnixNanos>> {
222 if millis < 0 {
223 Ok(None)
224 } else {
225 parse_millis_to_nanos(millis as u64).map(Some)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use rstest::rstest;
232
233 use super::*;
234
235 #[rstest]
236 fn parse_price_zero_decimals_is_integer() {
237 let price = parse_price_from_ticks(42, 0).unwrap();
238 assert_eq!(price.precision, 0);
239 assert_eq!(price.to_string(), "42");
240 }
241
242 #[rstest]
243 fn parse_price_with_decimals_inserts_decimal_point() {
244 let price = parse_price_from_ticks(405_000, 2).unwrap();
246 assert_eq!(price.precision, 2);
247 assert_eq!(price.to_string(), "4050.00");
248 }
249
250 #[rstest]
251 fn parse_price_at_max_decimals() {
252 let price = parse_price_from_ticks(1, MAX_DECIMALS).unwrap();
253 assert_eq!(price.precision, MAX_DECIMALS);
254 }
255
256 #[rstest]
257 fn parse_price_rejects_decimals_above_max() {
258 let err = parse_price_from_ticks(1, MAX_DECIMALS + 1).unwrap_err();
259 assert!(err.to_string().contains("exceeds maximum"));
260 }
261
262 #[rstest]
263 fn parse_quantity_with_decimals_inserts_decimal_point() {
264 let qty = parse_quantity_from_ticks(1_000, 3).unwrap();
266 assert_eq!(qty.precision, 3);
267 assert_eq!(qty.to_string(), "1.000");
268 }
269
270 #[rstest]
271 fn parse_quantity_rejects_negative_ticks() {
272 let err = parse_quantity_from_ticks(-1, 2).unwrap_err();
273 assert!(err.to_string().contains("negative tick count"));
274 }
275
276 #[rstest]
277 fn parse_quantity_rejects_oversized_ticks() {
278 let err = parse_quantity_from_ticks(i64::MAX, 0).unwrap_err();
280 assert!(err.to_string().contains("Quantity overflow"));
281 }
282
283 #[rstest]
284 fn parse_quantity_zero_is_valid() {
285 let qty = parse_quantity_from_ticks(0, 4).unwrap();
286 assert_eq!(qty.as_f64(), 0.0);
287 assert_eq!(qty.precision, 4);
288 }
289
290 #[rstest]
291 fn parse_price_from_decimal_string() {
292 let price = parse_price("2352.73", 2).unwrap();
293 assert_eq!(price.to_string(), "2352.73");
294 assert_eq!(price.precision, 2);
295 }
296
297 #[rstest]
298 fn parse_price_rejects_invalid_decimal() {
299 let err = parse_price("not-a-price", 2).unwrap_err();
300 assert!(err.to_string().contains("invalid price"));
301 }
302
303 #[rstest]
304 fn parse_quantity_from_decimal_string() {
305 let quantity = parse_quantity("0.1336", 4).unwrap();
306 assert_eq!(quantity.to_string(), "0.1336");
307 assert_eq!(quantity.precision, 4);
308 }
309
310 #[rstest]
311 fn parse_quantity_rejects_negative_decimal_string() {
312 let err = parse_quantity("-0.1", 4).unwrap_err();
313 assert!(err.to_string().contains("negative quantity"));
314 }
315
316 #[rstest]
317 fn parse_millis_to_nanos_scales_correctly() {
318 assert_eq!(parse_millis_to_nanos(0).unwrap(), UnixNanos::from(0));
319 assert_eq!(
320 parse_millis_to_nanos(1).unwrap(),
321 UnixNanos::from(1_000_000),
322 );
323 assert_eq!(
324 parse_millis_to_nanos(1_700_000_000_000).unwrap(),
325 UnixNanos::from(1_700_000_000_000_000_000),
326 );
327 }
328
329 #[rstest]
330 fn parse_millis_to_nanos_rejects_overflow() {
331 let err = parse_millis_to_nanos(u64::MAX).unwrap_err();
332 assert!(err.to_string().contains("overflows"));
333 }
334
335 #[rstest]
336 fn parse_micros_to_nanos_scales_correctly() {
337 assert_eq!(parse_micros_to_nanos(0).unwrap(), UnixNanos::from(0));
338 assert_eq!(parse_micros_to_nanos(1).unwrap(), UnixNanos::from(1_000));
339 assert_eq!(
340 parse_micros_to_nanos(1_700_000_000_000_000).unwrap(),
341 UnixNanos::from(1_700_000_000_000_000_000),
342 );
343 }
344
345 #[rstest]
346 fn parse_micros_to_nanos_rejects_overflow() {
347 let err = parse_micros_to_nanos(u64::MAX).unwrap_err();
348 assert!(err.to_string().contains("overflows"));
349 }
350
351 #[rstest]
352 fn parse_secs_to_nanos_scales_correctly() {
353 assert_eq!(parse_secs_to_nanos(0).unwrap(), UnixNanos::from(0));
354 assert_eq!(
355 parse_secs_to_nanos(1).unwrap(),
356 UnixNanos::from(1_000_000_000),
357 );
358 }
359
360 #[rstest]
361 fn parse_secs_to_nanos_rejects_overflow() {
362 let err = parse_secs_to_nanos(u64::MAX).unwrap_err();
363 assert!(err.to_string().contains("overflows"));
364 }
365
366 #[rstest]
367 fn parse_optional_millis_returns_none_for_negative() {
368 assert!(parse_optional_millis_to_nanos(-1).unwrap().is_none());
369 assert!(parse_optional_millis_to_nanos(i64::MIN).unwrap().is_none());
370 }
371
372 #[rstest]
373 fn parse_optional_millis_returns_some_for_non_negative() {
374 assert_eq!(
375 parse_optional_millis_to_nanos(0).unwrap(),
376 Some(UnixNanos::from(0)),
377 );
378 assert_eq!(
379 parse_optional_millis_to_nanos(1_500).unwrap(),
380 Some(UnixNanos::from(1_500_000_000)),
381 );
382 }
383
384 #[rstest]
385 fn parse_optional_millis_propagates_overflow() {
386 let err = parse_optional_millis_to_nanos(i64::MAX).unwrap_err();
387 assert!(err.to_string().contains("overflows"));
388 }
389}