nautilus_model/types/
price.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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
16//! Represents a price in a market with a specified precision.
17
18use std::{
19    cmp::Ordering,
20    fmt::{Debug, Display},
21    hash::{Hash, Hasher},
22    ops::{Add, AddAssign, Deref, Mul, Neg, Sub, SubAssign},
23    str::FromStr,
24};
25
26use nautilus_core::correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true};
27use rust_decimal::{Decimal, prelude::ToPrimitive};
28use serde::{Deserialize, Deserializer, Serialize};
29use thousands::Separable;
30
31use super::fixed::{FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision};
32#[cfg(feature = "high-precision")]
33use super::fixed::{PRECISION_DIFF_SCALAR, f64_to_fixed_i128, fixed_i128_to_f64};
34#[cfg(not(feature = "high-precision"))]
35use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
36#[cfg(feature = "defi")]
37use crate::types::fixed::MAX_FLOAT_PRECISION;
38
39// -----------------------------------------------------------------------------
40// PriceRaw
41// -----------------------------------------------------------------------------
42
43// Use 128-bit integers when either `high-precision` or `defi` features are enabled. This is
44// required for the extended 18-decimal wei precision used in DeFi contexts.
45
46#[cfg(feature = "high-precision")]
47pub type PriceRaw = i128;
48
49#[cfg(not(feature = "high-precision"))]
50pub type PriceRaw = i64;
51
52// -----------------------------------------------------------------------------
53
54/// The maximum raw price integer value.
55///
56/// # Safety
57///
58/// This value is computed at compile time from PRICE_MAX * FIXED_SCALAR.
59/// The multiplication is guaranteed not to overflow because PRICE_MAX and FIXED_SCALAR
60/// are chosen such that their product fits within PriceRaw's range in both
61/// high-precision (i128) and standard-precision (i64) modes.
62#[unsafe(no_mangle)]
63#[allow(unsafe_code)]
64pub static PRICE_RAW_MAX: PriceRaw = (PRICE_MAX * FIXED_SCALAR) as PriceRaw;
65
66/// The minimum raw price integer value.
67///
68/// # Safety
69///
70/// This value is computed at compile time from PRICE_MIN * FIXED_SCALAR.
71/// The multiplication is guaranteed not to overflow because PRICE_MIN and FIXED_SCALAR
72/// are chosen such that their product fits within PriceRaw's range in both
73/// high-precision (i128) and standard-precision (i64) modes.
74#[unsafe(no_mangle)]
75#[allow(unsafe_code)]
76pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN * FIXED_SCALAR) as PriceRaw;
77
78/// The sentinel value for an unset or null price.
79pub const PRICE_UNDEF: PriceRaw = PriceRaw::MAX;
80
81/// The sentinel value for an error or invalid price.
82pub const PRICE_ERROR: PriceRaw = PriceRaw::MIN;
83
84// -----------------------------------------------------------------------------
85// PRICE_MAX
86// -----------------------------------------------------------------------------
87
88/// The maximum valid price value that can be represented.
89#[cfg(feature = "high-precision")]
90pub const PRICE_MAX: f64 = 17_014_118_346_046.0;
91
92#[cfg(not(feature = "high-precision"))]
93/// The maximum valid price value that can be represented.
94pub const PRICE_MAX: f64 = 9_223_372_036.0;
95
96// -----------------------------------------------------------------------------
97// PRICE_MIN
98// -----------------------------------------------------------------------------
99
100#[cfg(feature = "high-precision")]
101/// The minimum valid price value that can be represented.
102pub const PRICE_MIN: f64 = -17_014_118_346_046.0;
103
104#[cfg(not(feature = "high-precision"))]
105/// The minimum valid price value that can be represented.
106pub const PRICE_MIN: f64 = -9_223_372_036.0;
107
108// -----------------------------------------------------------------------------
109
110/// The sentinel `Price` representing errors (this will be removed when Cython is gone).
111pub const ERROR_PRICE: Price = Price {
112    raw: 0,
113    precision: 255,
114};
115
116/// Represents a price in a market with a specified precision.
117///
118/// The number of decimal places may vary. For certain asset classes, prices may
119/// have negative values. For example, prices for options instruments can be
120/// negative under certain conditions.
121///
122/// Handles up to [`FIXED_PRECISION`] decimals of precision.
123///
124/// - [`PRICE_MAX`] - Maximum representable price value.
125/// - [`PRICE_MIN`] - Minimum representable price value.
126#[repr(C)]
127#[derive(Clone, Copy, Default, Eq)]
128#[cfg_attr(
129    feature = "python",
130    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", frozen)
131)]
132pub struct Price {
133    /// Represents the raw fixed-point value, with `precision` defining the number of decimal places.
134    pub raw: PriceRaw,
135    /// The number of decimal places, with a maximum of [`FIXED_PRECISION`].
136    pub precision: u8,
137}
138
139impl Price {
140    /// Creates a new [`Price`] instance with correctness checking.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if:
145    /// - `value` is invalid outside the representable range [`PRICE_MIN`, `PRICE_MAX`].
146    /// - `precision` is invalid outside the representable range [0, `FIXED_PRECISION``].
147    ///
148    /// # Notes
149    ///
150    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
151    pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
152        check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?;
153
154        #[cfg(feature = "defi")]
155        if precision > MAX_FLOAT_PRECISION {
156            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
157            anyhow::bail!(
158                "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Price::from_wei()` for wei values instead"
159            );
160        }
161
162        check_fixed_precision(precision)?;
163
164        #[cfg(feature = "high-precision")]
165        let raw = f64_to_fixed_i128(value, precision);
166
167        #[cfg(not(feature = "high-precision"))]
168        let raw = f64_to_fixed_i64(value, precision);
169
170        Ok(Self { raw, precision })
171    }
172
173    /// Creates a new [`Price`] instance.
174    ///
175    /// # Panics
176    ///
177    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
178    pub fn new(value: f64, precision: u8) -> Self {
179        Self::new_checked(value, precision).expect(FAILED)
180    }
181
182    /// Creates a new [`Price`] instance from the given `raw` fixed-point value and `precision`.
183    ///
184    /// # Panics
185    ///
186    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
187    pub fn from_raw(raw: PriceRaw, precision: u8) -> Self {
188        if raw == PRICE_UNDEF {
189            check_predicate_true(
190                precision == 0,
191                "`precision` must be 0 when `raw` is PRICE_UNDEF",
192            )
193            .expect(FAILED);
194        }
195        check_predicate_true(
196            raw == PRICE_ERROR
197                || raw == PRICE_UNDEF
198                || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
199            &format!("raw value outside valid range, was {raw}"),
200        )
201        .expect(FAILED);
202        check_fixed_precision(precision).expect(FAILED);
203        Self { raw, precision }
204    }
205
206    /// Creates a new [`Price`] instance with a value of zero with the given `precision`.
207    ///
208    /// # Panics
209    ///
210    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
211    #[must_use]
212    pub fn zero(precision: u8) -> Self {
213        check_fixed_precision(precision).expect(FAILED);
214        Self { raw: 0, precision }
215    }
216
217    /// Creates a new [`Price`] instance with the maximum representable value with the given `precision`.
218    ///
219    /// # Panics
220    ///
221    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
222    #[must_use]
223    pub fn max(precision: u8) -> Self {
224        check_fixed_precision(precision).expect(FAILED);
225        Self {
226            raw: PRICE_RAW_MAX,
227            precision,
228        }
229    }
230
231    /// Creates a new [`Price`] instance with the minimum representable value with the given `precision`.
232    ///
233    /// # Panics
234    ///
235    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
236    #[must_use]
237    pub fn min(precision: u8) -> Self {
238        check_fixed_precision(precision).expect(FAILED);
239        Self {
240            raw: PRICE_RAW_MIN,
241            precision,
242        }
243    }
244
245    /// Returns `true` if the value of this instance is undefined.
246    #[must_use]
247    pub fn is_undefined(&self) -> bool {
248        self.raw == PRICE_UNDEF
249    }
250
251    /// Returns `true` if the value of this instance is zero.
252    #[must_use]
253    pub fn is_zero(&self) -> bool {
254        self.raw == 0
255    }
256
257    /// Returns `true` if the value of this instance is position (> 0).
258    #[must_use]
259    pub fn is_positive(&self) -> bool {
260        self.raw != PRICE_UNDEF && self.raw > 0
261    }
262
263    #[cfg(feature = "high-precision")]
264    /// Returns the value of this instance as an `f64`.
265    ///
266    /// # Panics
267    ///
268    /// Panics if precision is beyond [`MAX_FLOAT_PRECISION`] (16).
269    #[must_use]
270    pub fn as_f64(&self) -> f64 {
271        #[cfg(feature = "defi")]
272        if self.precision > MAX_FLOAT_PRECISION {
273            panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
274        }
275
276        fixed_i128_to_f64(self.raw)
277    }
278
279    #[cfg(not(feature = "high-precision"))]
280    /// Returns the value of this instance as an `f64`.
281    ///
282    /// # Panics
283    ///
284    /// Panics if precision is beyond [`MAX_FLOAT_PRECISION`] (16).
285    #[must_use]
286    pub fn as_f64(&self) -> f64 {
287        #[cfg(feature = "defi")]
288        if self.precision > MAX_FLOAT_PRECISION {
289            panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
290        }
291
292        fixed_i64_to_f64(self.raw)
293    }
294
295    /// Returns the value of this instance as a `Decimal`.
296    #[must_use]
297    pub fn as_decimal(&self) -> Decimal {
298        // Scale down the raw value to match the precision
299        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
300        let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
301        #[allow(clippy::unnecessary_cast, reason = "Required for precision modes")]
302        Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
303    }
304
305    /// Returns a formatted string representation of this instance.
306    #[must_use]
307    pub fn to_formatted_string(&self) -> String {
308        format!("{self}").separate_with_underscores()
309    }
310
311    /// Creates a new [`Price`] from a `Decimal` value with specified precision.
312    ///
313    /// This method provides more reliable parsing by using Decimal arithmetic
314    /// to avoid floating-point precision issues during conversion.
315    ///
316    /// # Errors
317    ///
318    /// Returns an error if:
319    /// - `precision` exceeds [`FIXED_PRECISION`].
320    /// - The decimal value cannot be converted to the raw representation.
321    /// - Overflow occurs during scaling.
322    pub fn from_decimal(decimal: Decimal, precision: u8) -> anyhow::Result<Self> {
323        check_fixed_precision(precision)?;
324
325        // Scale the decimal to the target precision
326        let scale_factor = Decimal::from(10_i64.pow(precision as u32));
327        let scaled = decimal * scale_factor;
328        let rounded = scaled.round();
329
330        #[cfg(feature = "high-precision")]
331        let raw_at_precision: PriceRaw = rounded.to_i128().ok_or_else(|| {
332            anyhow::anyhow!("Decimal value '{decimal}' cannot be converted to i128")
333        })?;
334        #[cfg(not(feature = "high-precision"))]
335        let raw_at_precision: PriceRaw = rounded.to_i64().ok_or_else(|| {
336            anyhow::anyhow!("Decimal value '{decimal}' cannot be converted to i64")
337        })?;
338
339        let scale_up = 10_i64.pow((FIXED_PRECISION - precision) as u32) as PriceRaw;
340        let raw = raw_at_precision
341            .checked_mul(scale_up)
342            .ok_or_else(|| anyhow::anyhow!("Overflow when scaling to fixed precision"))?;
343
344        check_predicate_true(
345            raw == PRICE_UNDEF || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
346            &format!("raw value outside valid range, was {raw}"),
347        )?;
348
349        Ok(Self { raw, precision })
350    }
351}
352
353impl FromStr for Price {
354    type Err = String;
355
356    fn from_str(value: &str) -> Result<Self, Self::Err> {
357        let clean_value = value.replace('_', "");
358
359        let decimal = if clean_value.contains('e') || clean_value.contains('E') {
360            Decimal::from_scientific(&clean_value)
361                .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
362        } else {
363            Decimal::from_str(&clean_value)
364                .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
365        };
366
367        // Determine precision from the final decimal result
368        let decimal_str = decimal.to_string();
369        let precision = if let Some(dot_pos) = decimal_str.find('.') {
370            let decimal_part = &decimal_str[dot_pos + 1..];
371            decimal_part.len().min(u8::MAX as usize) as u8
372        } else {
373            0
374        };
375
376        Self::from_decimal(decimal, precision).map_err(|e| e.to_string())
377    }
378}
379
380impl<T: AsRef<str>> From<T> for Price {
381    fn from(value: T) -> Self {
382        Self::from_str(value.as_ref()).expect(FAILED)
383    }
384}
385
386impl From<Price> for f64 {
387    fn from(price: Price) -> Self {
388        price.as_f64()
389    }
390}
391
392impl From<&Price> for f64 {
393    fn from(price: &Price) -> Self {
394        price.as_f64()
395    }
396}
397
398impl Hash for Price {
399    fn hash<H: Hasher>(&self, state: &mut H) {
400        self.raw.hash(state);
401    }
402}
403
404impl PartialEq for Price {
405    fn eq(&self, other: &Self) -> bool {
406        self.raw == other.raw
407    }
408}
409
410impl PartialOrd for Price {
411    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
412        Some(self.cmp(other))
413    }
414
415    fn lt(&self, other: &Self) -> bool {
416        self.raw.lt(&other.raw)
417    }
418
419    fn le(&self, other: &Self) -> bool {
420        self.raw.le(&other.raw)
421    }
422
423    fn gt(&self, other: &Self) -> bool {
424        self.raw.gt(&other.raw)
425    }
426
427    fn ge(&self, other: &Self) -> bool {
428        self.raw.ge(&other.raw)
429    }
430}
431
432impl Ord for Price {
433    fn cmp(&self, other: &Self) -> Ordering {
434        self.raw.cmp(&other.raw)
435    }
436}
437
438impl Deref for Price {
439    type Target = PriceRaw;
440
441    fn deref(&self) -> &Self::Target {
442        &self.raw
443    }
444}
445
446impl Neg for Price {
447    type Output = Self;
448    fn neg(self) -> Self::Output {
449        Self {
450            raw: -self.raw,
451            precision: self.precision,
452        }
453    }
454}
455
456impl Add for Price {
457    type Output = Self;
458    fn add(self, rhs: Self) -> Self::Output {
459        // SAFETY: Current precision logic ensures only equal or higher precision operations
460        // are allowed to prevent silent precision loss. When self.precision >= rhs.precision,
461        // the rhs value is effectively scaled up internally by the fixed-point representation,
462        // so no actual precision is lost in the addition. However, the result is limited
463        // to self.precision decimal places.
464        assert!(
465            self.precision >= rhs.precision,
466            "Precision mismatch: cannot add precision {} to precision {} (precision loss)",
467            rhs.precision,
468            self.precision,
469        );
470        Self {
471            raw: self
472                .raw
473                .checked_add(rhs.raw)
474                .expect("Overflow occurred when adding `Price`"),
475            precision: self.precision,
476        }
477    }
478}
479
480impl Sub for Price {
481    type Output = Self;
482    fn sub(self, rhs: Self) -> Self::Output {
483        // SAFETY: Current precision logic ensures only equal or higher precision operations
484        // are allowed to prevent silent precision loss. When self.precision >= rhs.precision,
485        // the rhs value is effectively scaled up internally by the fixed-point representation,
486        // so no actual precision is lost in the subtraction. However, the result is limited
487        // to self.precision decimal places.
488        assert!(
489            self.precision >= rhs.precision,
490            "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
491            rhs.precision,
492            self.precision,
493        );
494        Self {
495            raw: self
496                .raw
497                .checked_sub(rhs.raw)
498                .expect("Underflow occurred when subtracting `Price`"),
499            precision: self.precision,
500        }
501    }
502}
503
504impl AddAssign for Price {
505    fn add_assign(&mut self, other: Self) {
506        assert!(
507            self.precision >= other.precision,
508            "Precision mismatch: cannot add precision {} to precision {} (precision loss)",
509            other.precision,
510            self.precision,
511        );
512        self.raw = self
513            .raw
514            .checked_add(other.raw)
515            .expect("Overflow occurred when adding `Price`");
516    }
517}
518
519impl SubAssign for Price {
520    fn sub_assign(&mut self, other: Self) {
521        assert!(
522            self.precision >= other.precision,
523            "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
524            other.precision,
525            self.precision,
526        );
527        self.raw = self
528            .raw
529            .checked_sub(other.raw)
530            .expect("Underflow occurred when subtracting `Price`");
531    }
532}
533
534impl Add<f64> for Price {
535    type Output = f64;
536    fn add(self, rhs: f64) -> Self::Output {
537        self.as_f64() + rhs
538    }
539}
540
541impl Sub<f64> for Price {
542    type Output = f64;
543    fn sub(self, rhs: f64) -> Self::Output {
544        self.as_f64() - rhs
545    }
546}
547
548impl Mul<f64> for Price {
549    type Output = f64;
550    fn mul(self, rhs: f64) -> Self::Output {
551        self.as_f64() * rhs
552    }
553}
554
555impl Debug for Price {
556    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557        if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
558            write!(f, "{}({})", stringify!(Price), self.raw)
559        } else {
560            write!(f, "{}({})", stringify!(Price), self.as_decimal())
561        }
562    }
563}
564
565impl Display for Price {
566    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567        if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
568            write!(f, "{}", self.raw)
569        } else {
570            write!(f, "{}", self.as_decimal())
571        }
572    }
573}
574
575impl Serialize for Price {
576    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
577    where
578        S: serde::Serializer,
579    {
580        serializer.serialize_str(&self.to_string())
581    }
582}
583
584impl<'de> Deserialize<'de> for Price {
585    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
586    where
587        D: Deserializer<'de>,
588    {
589        let price_str: &str = Deserialize::deserialize(_deserializer)?;
590        let price: Self = price_str.into();
591        Ok(price)
592    }
593}
594
595/// Checks the price `value` is positive.
596///
597/// # Errors
598///
599/// Returns an error if `value` is `PRICE_UNDEF` or not positive.
600pub fn check_positive_price(value: Price, param: &str) -> anyhow::Result<()> {
601    if value.raw == PRICE_UNDEF {
602        anyhow::bail!("invalid `Price` for '{param}', was PRICE_UNDEF")
603    }
604    if !value.is_positive() {
605        anyhow::bail!("invalid `Price` for '{param}' not positive, was {value}")
606    }
607    Ok(())
608}
609
610#[cfg(feature = "high-precision")]
611/// The raw i64 price has already been scaled by 10^9. Further scale it by the difference to
612/// `FIXED_PRECISION` to make it high/defi-precision raw price.
613pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
614    value as PriceRaw * PRECISION_DIFF_SCALAR as PriceRaw
615}
616
617#[cfg(not(feature = "high-precision"))]
618pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
619    value
620}
621
622////////////////////////////////////////////////////////////////////////////////
623// Tests
624////////////////////////////////////////////////////////////////////////////////
625#[cfg(test)]
626mod tests {
627    use nautilus_core::approx_eq;
628    use rstest::rstest;
629    use rust_decimal_macros::dec;
630
631    use super::*;
632
633    #[rstest]
634    #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
635    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 50")]
636    fn test_invalid_precision_new() {
637        // Precision exceeds float precision limit
638        let _ = Price::new(1.0, 50);
639    }
640
641    #[rstest]
642    #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
643    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 50")]
644    fn test_invalid_precision_new() {
645        // Precision exceeds float precision limit
646        let _ = Price::new(1.0, 50);
647    }
648
649    #[rstest]
650    #[cfg(not(feature = "defi"))]
651    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
652    fn test_invalid_precision_from_raw() {
653        // Precision out of range for fixed
654        let _ = Price::from_raw(1, FIXED_PRECISION + 1);
655    }
656
657    #[rstest]
658    #[cfg(not(feature = "defi"))]
659    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
660    fn test_invalid_precision_max() {
661        // Precision out of range for fixed
662        let _ = Price::max(FIXED_PRECISION + 1);
663    }
664
665    #[rstest]
666    #[cfg(not(feature = "defi"))]
667    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
668    fn test_invalid_precision_min() {
669        // Precision out of range for fixed
670        let _ = Price::min(FIXED_PRECISION + 1);
671    }
672
673    #[rstest]
674    #[cfg(not(feature = "defi"))]
675    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
676    fn test_invalid_precision_zero() {
677        // Precision out of range for fixed
678        let _ = Price::zero(FIXED_PRECISION + 1);
679    }
680
681    #[rstest]
682    #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
683    fn test_max_value_exceeded() {
684        Price::new(PRICE_MAX + 0.1, FIXED_PRECISION);
685    }
686
687    #[rstest]
688    #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
689    fn test_min_value_exceeded() {
690        Price::new(PRICE_MIN - 0.1, FIXED_PRECISION);
691    }
692
693    #[rstest]
694    fn test_is_positive_ok() {
695        // A normal, non‑zero price should be positive.
696        let price = Price::new(42.0, 2);
697        assert!(price.is_positive());
698
699        // `check_positive_price` should accept it without error.
700        check_positive_price(price, "price").unwrap();
701    }
702
703    #[rstest]
704    #[should_panic(expected = "invalid `Price` for 'price' not positive")]
705    fn test_is_positive_rejects_non_positive() {
706        // Zero is NOT positive.
707        let zero = Price::zero(2);
708        check_positive_price(zero, "price").unwrap();
709    }
710
711    #[rstest]
712    #[should_panic(expected = "invalid `Price` for 'price', was PRICE_UNDEF")]
713    fn test_is_positive_rejects_undefined() {
714        // PRICE_UNDEF must also be rejected.
715        let undef = Price::from_raw(PRICE_UNDEF, 0);
716        check_positive_price(undef, "price").unwrap();
717    }
718
719    #[rstest]
720    fn test_construction() {
721        let price = Price::new_checked(1.23456, 4);
722        assert!(price.is_ok());
723        let price = price.unwrap();
724        assert_eq!(price.precision, 4);
725        assert!(approx_eq!(f64, price.as_f64(), 1.23456, epsilon = 0.0001));
726    }
727
728    #[rstest]
729    fn test_negative_price_in_range() {
730        // Use max fixed precision which varies based on feature flags
731        let neg_price = Price::new(PRICE_MIN / 2.0, FIXED_PRECISION);
732        assert!(neg_price.raw < 0);
733    }
734
735    #[rstest]
736    fn test_new_checked() {
737        // Use max fixed precision which varies based on feature flags
738        assert!(Price::new_checked(1.0, FIXED_PRECISION).is_ok());
739        assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
740        assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
741    }
742
743    #[rstest]
744    fn test_from_raw() {
745        let raw = 100 * FIXED_SCALAR as PriceRaw;
746        let price = Price::from_raw(raw, 2);
747        assert_eq!(price.raw, raw);
748        assert_eq!(price.precision, 2);
749    }
750
751    #[rstest]
752    fn test_zero_constructor() {
753        let zero = Price::zero(3);
754        assert!(zero.is_zero());
755        assert_eq!(zero.precision, 3);
756    }
757
758    #[rstest]
759    fn test_max_constructor() {
760        let max = Price::max(4);
761        assert_eq!(max.raw, PRICE_RAW_MAX);
762        assert_eq!(max.precision, 4);
763    }
764
765    #[rstest]
766    fn test_min_constructor() {
767        let min = Price::min(4);
768        assert_eq!(min.raw, PRICE_RAW_MIN);
769        assert_eq!(min.precision, 4);
770    }
771
772    #[rstest]
773    fn test_nan_validation() {
774        assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
775    }
776
777    #[rstest]
778    fn test_infinity_validation() {
779        assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
780        assert!(Price::new_checked(f64::NEG_INFINITY, FIXED_PRECISION).is_err());
781    }
782
783    #[rstest]
784    fn test_special_values() {
785        let zero = Price::zero(5);
786        assert!(zero.is_zero());
787        assert_eq!(zero.to_string(), "0.00000");
788
789        let undef = Price::from_raw(PRICE_UNDEF, 0);
790        assert!(undef.is_undefined());
791
792        let error = ERROR_PRICE;
793        assert_eq!(error.precision, 255);
794    }
795
796    #[rstest]
797    fn test_string_parsing() {
798        let price: Price = "123.456".into();
799        assert_eq!(price.precision, 3);
800        assert_eq!(price, Price::from("123.456"));
801    }
802
803    #[rstest]
804    fn test_negative_price_from_str() {
805        let price: Price = "-123.45".parse().unwrap();
806        assert_eq!(price.precision, 2);
807        assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-9));
808    }
809
810    #[rstest]
811    fn test_string_parsing_errors() {
812        assert!(Price::from_str("invalid").is_err());
813    }
814
815    #[rstest]
816    #[case("1e7", 0, 10_000_000.0)]
817    #[case("1.5e3", 0, 1_500.0)]
818    #[case("1.234e-2", 5, 0.01234)]
819    #[case("5E-3", 3, 0.005)]
820    fn test_from_str_scientific_notation(
821        #[case] input: &str,
822        #[case] expected_precision: u8,
823        #[case] expected_value: f64,
824    ) {
825        let price = Price::from_str(input).unwrap();
826        assert_eq!(price.precision, expected_precision);
827        assert!(approx_eq!(
828            f64,
829            price.as_f64(),
830            expected_value,
831            epsilon = 1e-10
832        ));
833    }
834
835    #[rstest]
836    #[case("1_234.56", 2, 1234.56)]
837    #[case("1_000_000", 0, 1_000_000.0)]
838    #[case("99_999.999_99", 5, 99_999.999_99)]
839    fn test_from_str_with_underscores(
840        #[case] input: &str,
841        #[case] expected_precision: u8,
842        #[case] expected_value: f64,
843    ) {
844        let price = Price::from_str(input).unwrap();
845        assert_eq!(price.precision, expected_precision);
846        assert!(approx_eq!(
847            f64,
848            price.as_f64(),
849            expected_value,
850            epsilon = 1e-10
851        ));
852    }
853
854    #[rstest]
855    fn test_from_decimal_precision_preservation() {
856        use rust_decimal::Decimal;
857
858        // Test that decimal conversion preserves exact values
859        let decimal = Decimal::from_str("123.456789").unwrap();
860        let price = Price::from_decimal(decimal, 6).unwrap();
861        assert_eq!(price.precision, 6);
862        assert!(approx_eq!(f64, price.as_f64(), 123.456789, epsilon = 1e-10));
863
864        // Verify raw value is exact
865        let expected_raw = 123456789 * 10_i64.pow((FIXED_PRECISION - 6) as u32);
866        assert_eq!(price.raw, expected_raw as PriceRaw);
867    }
868
869    #[rstest]
870    fn test_from_decimal_rounding() {
871        use rust_decimal::Decimal;
872
873        // Test banker's rounding (round half to even)
874        let decimal = Decimal::from_str("1.005").unwrap();
875        let price = Price::from_decimal(decimal, 2).unwrap();
876        assert_eq!(price.as_f64(), 1.0); // 1.005 rounds to 1.00 (even)
877
878        let decimal = Decimal::from_str("1.015").unwrap();
879        let price = Price::from_decimal(decimal, 2).unwrap();
880        assert_eq!(price.as_f64(), 1.02); // 1.015 rounds to 1.02 (even)
881    }
882
883    #[rstest]
884    fn test_string_formatting() {
885        assert_eq!(format!("{}", Price::new(1234.5678, 4)), "1234.5678");
886        assert_eq!(
887            format!("{:?}", Price::new(1234.5678, 4)),
888            "Price(1234.5678)"
889        );
890        assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678");
891    }
892
893    #[rstest]
894    #[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] // Normal precision
895    #[case(123.456789012345, 8, "Price(123.45678901)", "123.45678901")] // At max normal precision
896    #[cfg_attr(
897        feature = "defi",
898        case(
899            2_000_000_000_000_000_000.0,
900            18,
901            "Price(2000000000000000000)",
902            "2000000000000000000"
903        )
904    )] // High precision
905    fn test_string_formatting_precision_handling(
906        #[case] value: f64,
907        #[case] precision: u8,
908        #[case] expected_debug: &str,
909        #[case] expected_display: &str,
910    ) {
911        let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
912            Price::from_raw(value as PriceRaw, precision)
913        } else {
914            Price::new(value, precision)
915        };
916
917        assert_eq!(format!("{price:?}"), expected_debug);
918        assert_eq!(format!("{price}"), expected_display);
919        assert_eq!(
920            price.to_formatted_string().replace("_", ""),
921            expected_display
922        );
923    }
924
925    #[rstest]
926    fn test_decimal_conversions() {
927        let price = Price::new(123.456, 3);
928        assert_eq!(price.as_decimal(), dec!(123.456));
929
930        let price = Price::new(0.000001, 6);
931        assert_eq!(price.as_decimal(), dec!(0.000001));
932    }
933
934    #[rstest]
935    fn test_basic_arithmetic() {
936        let p1 = Price::new(10.5, 2);
937        let p2 = Price::new(5.25, 2);
938        assert_eq!(p1 + p2, Price::from("15.75"));
939        assert_eq!(p1 - p2, Price::from("5.25"));
940        assert_eq!(-p1, Price::from("-10.5"));
941    }
942
943    #[rstest]
944    #[should_panic(expected = "Precision mismatch: cannot add precision 2 to precision 1")]
945    fn test_precision_mismatch_add() {
946        let p1 = Price::new(10.5, 1);
947        let p2 = Price::new(5.25, 2);
948        let _ = p1 + p2;
949    }
950
951    #[rstest]
952    #[should_panic(expected = "Precision mismatch: cannot subtract precision 2 from precision 1")]
953    fn test_precision_mismatch_sub() {
954        let p1 = Price::new(10.5, 1);
955        let p2 = Price::new(5.25, 2);
956        let _ = p1 - p2;
957    }
958
959    #[rstest]
960    fn test_f64_operations() {
961        let p = Price::new(10.5, 2);
962        assert_eq!(p + 1.0, 11.5);
963        assert_eq!(p - 1.0, 9.5);
964        assert_eq!(p * 2.0, 21.0);
965    }
966
967    #[rstest]
968    fn test_assignment_operators() {
969        let mut p = Price::new(10.5, 2);
970        p += Price::new(5.25, 2);
971        assert_eq!(p, Price::from("15.75"));
972        p -= Price::new(5.25, 2);
973        assert_eq!(p, Price::from("10.5"));
974    }
975
976    #[rstest]
977    fn test_equality_and_comparisons() {
978        let p1 = Price::new(10.0, 1);
979        let p2 = Price::new(20.0, 1);
980        let p3 = Price::new(10.0, 1);
981
982        assert!(p1 < p2);
983        assert!(p2 > p1);
984        assert!(p1 <= p3);
985        assert!(p1 >= p3);
986        assert_eq!(p1, p3);
987        assert_ne!(p1, p2);
988
989        assert_eq!(Price::from("1.0"), Price::from("1.0"));
990        assert_ne!(Price::from("1.1"), Price::from("1.0"));
991        assert!(Price::from("1.0") <= Price::from("1.0"));
992        assert!(Price::from("1.1") > Price::from("1.0"));
993        assert!(Price::from("1.0") >= Price::from("1.0"));
994        assert!(Price::from("1.0") >= Price::from("1.0"));
995        assert!(Price::from("1.0") >= Price::from("1.0"));
996        assert!(Price::from("0.9") < Price::from("1.0"));
997        assert!(Price::from("0.9") <= Price::from("1.0"));
998        assert!(Price::from("0.9") <= Price::from("1.0"));
999    }
1000
1001    #[rstest]
1002    fn test_deref() {
1003        let price = Price::new(10.0, 1);
1004        assert_eq!(*price, price.raw);
1005    }
1006
1007    #[rstest]
1008    fn test_decode_raw_price_i64() {
1009        let raw_scaled_by_1e9 = 42_000_000_000i64; // 42.0 * 10^9
1010        let decoded = decode_raw_price_i64(raw_scaled_by_1e9);
1011        let price = Price::from_raw(decoded, FIXED_PRECISION);
1012        assert!(
1013            approx_eq!(f64, price.as_f64(), 42.0, epsilon = 1e-9),
1014            "Expected 42.0 f64, was {} (precision = {})",
1015            price.as_f64(),
1016            price.precision
1017        );
1018    }
1019
1020    #[rstest]
1021    fn test_hash() {
1022        use std::{
1023            collections::hash_map::DefaultHasher,
1024            hash::{Hash, Hasher},
1025        };
1026
1027        let price1 = Price::new(1.0, 2);
1028        let price2 = Price::new(1.0, 2);
1029        let price3 = Price::new(1.1, 2);
1030
1031        let mut hasher1 = DefaultHasher::new();
1032        let mut hasher2 = DefaultHasher::new();
1033        let mut hasher3 = DefaultHasher::new();
1034
1035        price1.hash(&mut hasher1);
1036        price2.hash(&mut hasher2);
1037        price3.hash(&mut hasher3);
1038
1039        assert_eq!(hasher1.finish(), hasher2.finish());
1040        assert_ne!(hasher1.finish(), hasher3.finish());
1041    }
1042
1043    #[rstest]
1044    fn test_price_serde_json_round_trip() {
1045        let price = Price::new(1.0500, 4);
1046        let json = serde_json::to_string(&price).unwrap();
1047        let deserialized: Price = serde_json::from_str(&json).unwrap();
1048        assert_eq!(deserialized, price);
1049    }
1050}
1051
1052////////////////////////////////////////////////////////////////////////////////
1053// Property-based tests
1054////////////////////////////////////////////////////////////////////////////////
1055#[cfg(test)]
1056mod property_tests {
1057    use proptest::prelude::*;
1058    use rstest::rstest;
1059
1060    use super::*;
1061
1062    /// Strategy to generate valid price values within the allowed range.
1063    fn price_value_strategy() -> impl Strategy<Value = f64> {
1064        // Use a reasonable range that's well within PRICE_MIN/PRICE_MAX
1065        // but still tests edge cases with various scales
1066        prop_oneof![
1067            // Small positive values
1068            0.00001..1.0,
1069            // Normal trading range
1070            1.0..100_000.0,
1071            // Large values (but safe)
1072            100_000.0..1_000_000.0,
1073            // Small negative values (for spreads, etc.)
1074            -1_000.0..0.0,
1075            // Boundary values close to the extremes
1076            Just(PRICE_MIN / 2.0),
1077            Just(PRICE_MAX / 2.0),
1078        ]
1079    }
1080
1081    fn float_precision_upper_bound() -> u8 {
1082        FIXED_PRECISION.min(crate::types::fixed::MAX_FLOAT_PRECISION)
1083    }
1084
1085    /// Strategy to exercise both typical and extreme precision values.
1086    fn precision_strategy() -> impl Strategy<Value = u8> {
1087        let upper = float_precision_upper_bound();
1088        prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1089    }
1090
1091    fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1092        let upper = float_precision_upper_bound().max(1);
1093        prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1094    }
1095
1096    fn price_raw_strategy() -> impl Strategy<Value = PriceRaw> {
1097        prop_oneof![
1098            Just(PRICE_RAW_MIN),
1099            Just(PRICE_RAW_MAX),
1100            PRICE_RAW_MIN..=PRICE_RAW_MAX,
1101        ]
1102    }
1103
1104    /// Strategy to generate valid precision values for float-based constructors.
1105    fn float_precision_strategy() -> impl Strategy<Value = u8> {
1106        precision_strategy()
1107    }
1108
1109    proptest! {
1110        /// Property: Price string serialization round-trip should preserve value and precision
1111        #[rstest]
1112        fn prop_price_serde_round_trip(
1113            value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1114            precision in precision_strategy()
1115        ) {
1116            let original = Price::new(value, precision);
1117
1118            // String round-trip (this should be exact and is the most important)
1119            let string_repr = original.to_string();
1120            let from_string: Price = string_repr.parse().unwrap();
1121            prop_assert_eq!(from_string.raw, original.raw);
1122            prop_assert_eq!(from_string.precision, original.precision);
1123
1124            // JSON round-trip basic validation (just ensure it doesn't crash and preserves precision)
1125            let json = serde_json::to_string(&original).unwrap();
1126            let from_json: Price = serde_json::from_str(&json).unwrap();
1127            prop_assert_eq!(from_json.precision, original.precision);
1128            // Note: JSON may have minor floating-point precision differences due to f64 limitations
1129        }
1130
1131        /// Property: Price arithmetic should be associative for same precision
1132        #[rstest]
1133        fn prop_price_arithmetic_associative(
1134            a in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1135            b in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1136            c in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1137            precision in precision_strategy()
1138        ) {
1139            let p_a = Price::new(a, precision);
1140            let p_b = Price::new(b, precision);
1141            let p_c = Price::new(c, precision);
1142
1143            // Check if we can perform the operations without overflow using raw arithmetic
1144            let ab_raw = p_a.raw.checked_add(p_b.raw);
1145            let bc_raw = p_b.raw.checked_add(p_c.raw);
1146
1147            if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1148                let ab_c_raw = ab_raw.checked_add(p_c.raw);
1149                let a_bc_raw = p_a.raw.checked_add(bc_raw);
1150
1151                if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1152                    // (a + b) + c == a + (b + c) using raw arithmetic (exact)
1153                    prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1154                }
1155            }
1156        }
1157
1158        /// Property: Price addition/subtraction should be inverse operations
1159        #[rstest]
1160        fn prop_price_addition_subtraction_inverse(
1161            base in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1162            delta in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1163            precision in precision_strategy()
1164        ) {
1165            let p_base = Price::new(base, precision);
1166            let p_delta = Price::new(delta, precision);
1167
1168            // Use raw arithmetic to avoid floating-point precision issues
1169            if let Some(added_raw) = p_base.raw.checked_add(p_delta.raw)
1170                && let Some(result_raw) = added_raw.checked_sub(p_delta.raw) {
1171                    // (base + delta) - delta should equal base exactly using raw arithmetic
1172                    prop_assert_eq!(result_raw, p_base.raw, "Inverse operation failed in raw arithmetic");
1173                }
1174        }
1175
1176        /// Property: Price ordering should be transitive
1177        #[rstest]
1178        fn prop_price_ordering_transitive(
1179            a in price_value_strategy(),
1180            b in price_value_strategy(),
1181            c in price_value_strategy(),
1182            precision in float_precision_strategy()
1183        ) {
1184            let p_a = Price::new(a, precision);
1185            let p_b = Price::new(b, precision);
1186            let p_c = Price::new(c, precision);
1187
1188            // If a <= b and b <= c, then a <= c
1189            if p_a <= p_b && p_b <= p_c {
1190                prop_assert!(p_a <= p_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1191                    p_a.as_f64(), p_b.as_f64(), p_c.as_f64(), p_a.as_f64(), p_c.as_f64());
1192            }
1193        }
1194
1195        /// Property: String parsing should be consistent with precision inference
1196        #[rstest]
1197        fn prop_price_string_parsing_precision(
1198            integral in 0u32..1000000,
1199            fractional in 0u32..1000000,
1200            precision in precision_strategy_non_zero()
1201        ) {
1202            // Create a decimal string with exactly 'precision' decimal places
1203            let pow = 10u128.pow(u32::from(precision));
1204            let fractional_mod = (fractional as u128) % pow;
1205            let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
1206            let price_str = format!("{integral}.{fractional_str}");
1207
1208            let parsed: Price = price_str.parse().unwrap();
1209            prop_assert_eq!(parsed.precision, precision);
1210
1211            // Round-trip should preserve the original string (after normalization)
1212            let round_trip = parsed.to_string();
1213            let expected_value = format!("{integral}.{fractional_str}");
1214            prop_assert_eq!(round_trip, expected_value);
1215        }
1216
1217        /// Property: Price with higher precision should contain more or equal information
1218        #[rstest]
1219        fn prop_price_precision_information_preservation(
1220            value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1221            precision1 in precision_strategy_non_zero(),
1222            precision2 in precision_strategy_non_zero()
1223        ) {
1224            // Skip cases where precisions are equal (trivial case)
1225            prop_assume!(precision1 != precision2);
1226
1227            let _p1 = Price::new(value, precision1);
1228            let _p2 = Price::new(value, precision2);
1229
1230            // When both prices are created from the same value with different precisions,
1231            // converting both to the lower precision should yield the same result
1232            let min_precision = precision1.min(precision2);
1233
1234            // Round the original value to the minimum precision first
1235            let scale = 10.0_f64.powi(min_precision as i32);
1236            let rounded_value = (value * scale).round() / scale;
1237
1238            let p1_reduced = Price::new(rounded_value, min_precision);
1239            let p2_reduced = Price::new(rounded_value, min_precision);
1240
1241            // They should be exactly equal when created from the same rounded value
1242            prop_assert_eq!(p1_reduced.raw, p2_reduced.raw, "Precision reduction inconsistent");
1243        }
1244
1245        /// Property: Price arithmetic should never produce invalid values
1246        #[rstest]
1247        fn prop_price_arithmetic_bounds(
1248            a in price_value_strategy(),
1249            b in price_value_strategy(),
1250            precision in float_precision_strategy()
1251        ) {
1252            let p_a = Price::new(a, precision);
1253            let p_b = Price::new(b, precision);
1254
1255            // Addition should either succeed or fail predictably
1256            let sum_f64 = p_a.as_f64() + p_b.as_f64();
1257            if sum_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&sum_f64) {
1258                let sum = p_a + p_b;
1259                prop_assert!(sum.as_f64().is_finite());
1260                prop_assert!(!sum.is_undefined());
1261            }
1262
1263            // Subtraction should either succeed or fail predictably
1264            let diff_f64 = p_a.as_f64() - p_b.as_f64();
1265            if diff_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&diff_f64) {
1266                let diff = p_a - p_b;
1267                prop_assert!(diff.as_f64().is_finite());
1268                prop_assert!(!diff.is_undefined());
1269            }
1270        }
1271    }
1272
1273    proptest! {
1274        /// Property: constructing from raw bounds preserves raw/precision fields
1275        #[rstest]
1276        fn prop_price_from_raw_round_trip(
1277            raw in price_raw_strategy(),
1278            precision in precision_strategy()
1279        ) {
1280            let price = Price::from_raw(raw, precision);
1281            prop_assert_eq!(price.raw, raw);
1282            prop_assert_eq!(price.precision, precision);
1283        }
1284    }
1285}