Skip to main content

nautilus_model/types/
price.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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//!
18//! [`Price`] is an immutable value type for representing market prices, bid/ask quotes,
19//! and price levels. Unlike [`Quantity`](super::Quantity), prices can be negative (useful for spreads,
20//! basis trades, or certain derivative instruments).
21//!
22//! # Arithmetic behavior
23//!
24//! | Operation         | Result    | Notes                              |
25//! |-------------------|-----------|------------------------------------|
26//! | `Price + Price`   | `Price`   | Precision is max of both operands. |
27//! | `Price - Price`   | `Price`   | Precision is max of both operands. |
28//! | `Price + Decimal` | `Decimal` |                                    |
29//! | `Price - Decimal` | `Decimal` |                                    |
30//! | `Price * Decimal` | `Decimal` |                                    |
31//! | `Price / Decimal` | `Decimal` |                                    |
32//! | `Price + f64`     | `f64`     |                                    |
33//! | `Price - f64`     | `f64`     |                                    |
34//! | `Price * f64`     | `f64`     |                                    |
35//! | `Price / f64`     | `f64`     |                                    |
36//! | `-Price`          | `Price`   |                                    |
37//!
38//! # Immutability
39//!
40//! `Price` is immutable. All arithmetic operations return new instances.
41
42use std::{
43    cmp::Ordering,
44    fmt::{Debug, Display},
45    hash::{Hash, Hasher},
46    ops::{Add, Deref, Div, Mul, Neg, Sub},
47    str::FromStr,
48};
49
50use nautilus_core::{
51    correctness::{
52        CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
53        check_in_range_inclusive_f64,
54    },
55    string::formatting::Separable,
56};
57use rust_decimal::Decimal;
58use serde::{Deserialize, Deserializer, Serialize};
59
60use super::fixed::{
61    FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision, mantissa_exponent_to_fixed_i128,
62    raw_scales_match,
63};
64#[cfg(feature = "high-precision")]
65use super::fixed::{PRECISION_DIFF_SCALAR, f64_to_fixed_i128, fixed_i128_to_f64};
66#[cfg(not(feature = "high-precision"))]
67use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
68#[cfg(feature = "defi")]
69use crate::types::fixed::MAX_FLOAT_PRECISION;
70
71// -----------------------------------------------------------------------------
72// PriceRaw
73// -----------------------------------------------------------------------------
74
75// Use 128-bit integers when either `high-precision` or `defi` features are enabled. This is
76// required for the extended 18-decimal wei precision used in DeFi contexts.
77
78#[cfg(feature = "high-precision")]
79pub type PriceRaw = i128;
80
81#[cfg(not(feature = "high-precision"))]
82pub type PriceRaw = i64;
83
84// -----------------------------------------------------------------------------
85
86/// The maximum raw price integer value.
87///
88/// # Safety
89///
90/// This value is computed at compile time from `PRICE_MAX` * `FIXED_SCALAR`.
91/// The multiplication is guaranteed not to overflow because `PRICE_MAX` and `FIXED_SCALAR`
92/// are chosen such that their product fits within `PriceRaw`'s range in both
93/// high-precision (i128) and standard-precision (i64) modes.
94#[unsafe(no_mangle)]
95#[allow(unsafe_code)]
96pub static PRICE_RAW_MAX: PriceRaw = (PRICE_MAX * FIXED_SCALAR) as PriceRaw;
97
98/// The minimum raw price integer value.
99///
100/// # Safety
101///
102/// This value is computed at compile time from `PRICE_MIN` * `FIXED_SCALAR`.
103/// The multiplication is guaranteed not to overflow because `PRICE_MIN` and `FIXED_SCALAR`
104/// are chosen such that their product fits within `PriceRaw`'s range in both
105/// high-precision (i128) and standard-precision (i64) modes.
106#[unsafe(no_mangle)]
107#[allow(unsafe_code)]
108pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN * FIXED_SCALAR) as PriceRaw;
109
110/// The sentinel value for an unset or null price.
111pub const PRICE_UNDEF: PriceRaw = PriceRaw::MAX;
112
113/// The sentinel value for an error or invalid price.
114pub const PRICE_ERROR: PriceRaw = PriceRaw::MIN;
115
116// -----------------------------------------------------------------------------
117// PRICE_MAX
118// -----------------------------------------------------------------------------
119
120/// The maximum valid price value that can be represented.
121#[cfg(feature = "high-precision")]
122pub const PRICE_MAX: f64 = 17_014_118_346_046.0;
123
124#[cfg(not(feature = "high-precision"))]
125/// The maximum valid price value that can be represented.
126pub const PRICE_MAX: f64 = 9_223_372_036.0;
127
128// -----------------------------------------------------------------------------
129// PRICE_MIN
130// -----------------------------------------------------------------------------
131
132#[cfg(feature = "high-precision")]
133/// The minimum valid price value that can be represented.
134pub const PRICE_MIN: f64 = -17_014_118_346_046.0;
135
136#[cfg(not(feature = "high-precision"))]
137/// The minimum valid price value that can be represented.
138pub const PRICE_MIN: f64 = -9_223_372_036.0;
139
140// -----------------------------------------------------------------------------
141
142/// The sentinel `Price` representing errors (this will be removed when Cython is gone).
143pub const ERROR_PRICE: Price = Price {
144    raw: 0,
145    precision: 255,
146};
147
148/// Represents a price in a market with a specified precision.
149///
150/// The number of decimal places may vary. For certain asset classes, prices may
151/// have negative values. For example, prices for options instruments can be
152/// negative under certain conditions.
153///
154/// Handles up to [`FIXED_PRECISION`] decimals of precision.
155///
156/// - [`PRICE_MAX`] - Maximum representable price value.
157/// - [`PRICE_MIN`] - Minimum representable price value.
158#[repr(C)]
159#[derive(Clone, Copy, Default, Eq)]
160#[cfg_attr(
161    feature = "python",
162    pyo3::pyclass(
163        module = "nautilus_trader.core.nautilus_pyo3.model",
164        frozen,
165        from_py_object
166    )
167)]
168#[cfg_attr(
169    feature = "python",
170    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
171)]
172pub struct Price {
173    /// Represents the raw fixed-point value, with `precision` defining the number of decimal places.
174    pub raw: PriceRaw,
175    /// The number of decimal places, with a maximum of [`FIXED_PRECISION`].
176    pub precision: u8,
177}
178
179impl Price {
180    /// Creates a new [`Price`] instance with correctness checking.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error if:
185    /// - `value` is invalid outside the representable range [`PRICE_MIN`, `PRICE_MAX`].
186    /// - `precision` is invalid outside the representable range [0, `FIXED_PRECISION`].
187    ///
188    /// # Notes
189    ///
190    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
191    pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
192        check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?;
193
194        #[cfg(feature = "defi")]
195        if precision > MAX_FLOAT_PRECISION {
196            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
197            return Err(CorrectnessError::PredicateViolation {
198                message: format!(
199                    "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Price::from_wei()` for wei values instead"
200                ),
201            });
202        }
203
204        check_fixed_precision(precision)?;
205
206        #[cfg(feature = "high-precision")]
207        let raw = f64_to_fixed_i128(value, precision);
208
209        #[cfg(not(feature = "high-precision"))]
210        let raw = f64_to_fixed_i64(value, precision);
211
212        Ok(Self { raw, precision })
213    }
214
215    /// Creates a new [`Price`] instance.
216    ///
217    /// # Panics
218    ///
219    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
220    #[must_use]
221    pub fn new(value: f64, precision: u8) -> Self {
222        Self::new_checked(value, precision).expect_display(FAILED)
223    }
224
225    /// Creates a new [`Price`] instance from the given `raw` fixed-point value and `precision`.
226    ///
227    /// # Panics
228    ///
229    /// Panics if `raw` is outside the valid range and is not a sentinel value.
230    /// Panics if `precision` exceeds [`FIXED_PRECISION`].
231    #[must_use]
232    pub fn from_raw(raw: PriceRaw, precision: u8) -> Self {
233        assert!(
234            raw == PRICE_ERROR
235                || raw == PRICE_UNDEF
236                || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
237            "`raw` value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
238        );
239
240        if raw == PRICE_UNDEF {
241            assert!(
242                precision == 0,
243                "`precision` must be 0 when `raw` is PRICE_UNDEF"
244            );
245        }
246        check_fixed_precision(precision).expect_display(FAILED);
247
248        // TODO: Enforce spurious bits validation in v2
249        // if !matches!(raw, PRICE_UNDEF | PRICE_ERROR) && raw != 0 {
250        //     #[cfg(feature = "high-precision")]
251        //     super::fixed::check_fixed_raw_i128(raw, precision).expect(FAILED);
252        //     #[cfg(not(feature = "high-precision"))]
253        //     super::fixed::check_fixed_raw_i64(raw, precision).expect(FAILED);
254        // }
255
256        Self { raw, precision }
257    }
258
259    /// Creates a new [`Price`] instance from the given `raw` fixed-point value and `precision`
260    /// with correctness checking.
261    ///
262    /// # Errors
263    ///
264    /// Returns an error if:
265    /// - `precision` exceeds the maximum fixed precision.
266    /// - `precision` is not 0 when `raw` is `PRICE_UNDEF`.
267    /// - `raw` is outside the valid range `[PRICE_RAW_MIN, PRICE_RAW_MAX]`
268    ///   and is not a sentinel value.
269    pub fn from_raw_checked(raw: PriceRaw, precision: u8) -> CorrectnessResult<Self> {
270        if raw == PRICE_UNDEF && precision != 0 {
271            return Err(CorrectnessError::PredicateViolation {
272                message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
273            });
274        }
275
276        if raw != PRICE_ERROR && raw != PRICE_UNDEF && (raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX)
277        {
278            return Err(CorrectnessError::PredicateViolation {
279                message: format!(
280                    "raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
281                ),
282            });
283        }
284
285        check_fixed_precision(precision)?;
286
287        Ok(Self { raw, precision })
288    }
289
290    /// Creates a new [`Price`] instance with a value of zero with the given `precision`.
291    ///
292    /// # Panics
293    ///
294    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
295    #[must_use]
296    pub fn zero(precision: u8) -> Self {
297        check_fixed_precision(precision).expect_display(FAILED);
298        Self { raw: 0, precision }
299    }
300
301    /// Creates a new [`Price`] instance with the maximum representable value with the given `precision`.
302    ///
303    /// # Panics
304    ///
305    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
306    #[must_use]
307    pub fn max(precision: u8) -> Self {
308        check_fixed_precision(precision).expect_display(FAILED);
309        Self {
310            raw: PRICE_RAW_MAX,
311            precision,
312        }
313    }
314
315    /// Creates a new [`Price`] instance with the minimum representable value with the given `precision`.
316    ///
317    /// # Panics
318    ///
319    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
320    #[must_use]
321    pub fn min(precision: u8) -> Self {
322        check_fixed_precision(precision).expect_display(FAILED);
323        Self {
324            raw: PRICE_RAW_MIN,
325            precision,
326        }
327    }
328
329    /// Performs a checked addition, returning `None` on raw integer overflow, when the
330    /// result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand is a
331    /// sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the operands
332    /// have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a defi
333    /// `WEI_PRECISION` scale).
334    ///
335    /// Precision follows the `Add` implementation: uses the maximum precision of both operands.
336    #[must_use]
337    pub fn checked_add(self, rhs: Self) -> Option<Self> {
338        if self.is_sentinel() || rhs.is_sentinel() {
339            return None;
340        }
341
342        if !raw_scales_match(self.precision, rhs.precision) {
343            return None;
344        }
345        let raw = self.raw.checked_add(rhs.raw)?;
346        if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
347            return None;
348        }
349        Some(Self {
350            raw,
351            precision: self.precision.max(rhs.precision),
352        })
353    }
354
355    /// Performs a checked subtraction, returning `None` on raw integer underflow, when
356    /// the result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand
357    /// is a sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the
358    /// operands have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a
359    /// defi `WEI_PRECISION` scale).
360    ///
361    /// Precision follows the `Sub` implementation: uses the maximum precision of both operands.
362    #[must_use]
363    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
364        if self.is_sentinel() || rhs.is_sentinel() {
365            return None;
366        }
367
368        if !raw_scales_match(self.precision, rhs.precision) {
369            return None;
370        }
371        let raw = self.raw.checked_sub(rhs.raw)?;
372        if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
373            return None;
374        }
375        Some(Self {
376            raw,
377            precision: self.precision.max(rhs.precision),
378        })
379    }
380
381    #[inline]
382    fn is_sentinel(self) -> bool {
383        // ERROR_PRICE uses precision == u8::MAX as its sentinel marker, distinct from
384        // valid high-precision values (e.g. defi `from_wei` uses precision 18 which is
385        // > FIXED_PRECISION but is not a sentinel).
386        self.raw == PRICE_UNDEF || self.raw == PRICE_ERROR || self.precision == u8::MAX
387    }
388
389    /// Returns `true` if the value of this instance is undefined.
390    #[must_use]
391    pub fn is_undefined(&self) -> bool {
392        self.raw == PRICE_UNDEF
393    }
394
395    /// Returns `true` if the value of this instance is zero.
396    #[must_use]
397    pub fn is_zero(&self) -> bool {
398        self.raw == 0
399    }
400
401    /// Returns `true` if the value of this instance is position (> 0).
402    #[must_use]
403    pub fn is_positive(&self) -> bool {
404        self.raw != PRICE_UNDEF && self.raw > 0
405    }
406
407    #[cfg(feature = "high-precision")]
408    /// Returns the value of this instance as an `f64`.
409    ///
410    /// # Panics
411    ///
412    /// Panics if precision is beyond `MAX_FLOAT_PRECISION` (16).
413    #[must_use]
414    pub fn as_f64(&self) -> f64 {
415        #[cfg(feature = "defi")]
416        assert!(
417            self.precision <= MAX_FLOAT_PRECISION,
418            "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
419        );
420
421        fixed_i128_to_f64(self.raw)
422    }
423
424    #[cfg(not(feature = "high-precision"))]
425    /// Returns the value of this instance as an `f64`.
426    ///
427    /// # Panics
428    ///
429    /// Panics if precision is beyond `MAX_FLOAT_PRECISION` (16).
430    #[must_use]
431    pub fn as_f64(&self) -> f64 {
432        #[cfg(feature = "defi")]
433        if self.precision > MAX_FLOAT_PRECISION {
434            panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
435        }
436
437        fixed_i64_to_f64(self.raw)
438    }
439
440    /// Returns the value of this instance as a `Decimal`.
441    #[must_use]
442    pub fn as_decimal(&self) -> Decimal {
443        // Scale down the raw value to match the precision
444        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
445        let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
446        #[allow(
447            clippy::unnecessary_cast,
448            clippy::cast_lossless,
449            reason = "cast is real when PriceRaw is i64, no-op when i128"
450        )]
451        Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
452    }
453
454    /// Returns a formatted string representation of this instance.
455    #[must_use]
456    pub fn to_formatted_string(&self) -> String {
457        format!("{self}").separate_with_underscores()
458    }
459
460    /// Creates a new [`Price`] from a `Decimal` value with specified precision.
461    ///
462    /// Uses pure integer arithmetic on the Decimal's mantissa and scale for fast conversion.
463    /// The value is rounded to the specified precision using banker's rounding (round half to even).
464    ///
465    /// # Errors
466    ///
467    /// Returns an error if:
468    /// - `precision` exceeds [`FIXED_PRECISION`].
469    /// - The decimal value cannot be converted to the raw representation.
470    /// - Overflow occurs during scaling.
471    pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
472        let exponent = -(decimal.scale() as i8);
473        let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
474
475        #[allow(
476            clippy::useless_conversion,
477            reason = "i128 to PriceRaw is real when not high-precision"
478        )]
479        let raw: PriceRaw =
480            raw_i128
481                .try_into()
482                .map_err(|_| CorrectnessError::PredicateViolation {
483                    message: format!(
484                        "Decimal value exceeds PriceRaw range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
485                    ),
486                })?;
487
488        if !(raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX) {
489            return Err(CorrectnessError::PredicateViolation {
490                message: format!(
491                    "Raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
492                ),
493            });
494        }
495
496        Ok(Self { raw, precision })
497    }
498
499    /// Creates a new [`Price`] from a [`Decimal`] value with precision inferred from the decimal's scale.
500    ///
501    /// The precision is determined by the scale of the decimal (number of decimal places).
502    /// The value is rounded to the inferred precision using banker's rounding (round half to even).
503    ///
504    /// # Errors
505    ///
506    /// Returns an error if:
507    /// - The inferred precision exceeds [`FIXED_PRECISION`].
508    /// - The decimal value cannot be converted to the raw representation.
509    /// - Overflow occurs during scaling.
510    pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
511        let precision = decimal.scale() as u8;
512        Self::from_decimal_dp(decimal, precision)
513    }
514
515    /// Creates a new [`Price`] from a mantissa/exponent pair using pure integer arithmetic.
516    ///
517    /// The value is `mantissa * 10^exponent`. This avoids all floating-point and Decimal
518    /// operations, making it ideal for exchange data that arrives as mantissa/exponent pairs.
519    ///
520    /// # Panics
521    ///
522    /// Panics if the resulting raw value exceeds [`PRICE_RAW_MAX`] or [`PRICE_RAW_MIN`].
523    #[must_use]
524    pub fn from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> Self {
525        check_fixed_precision(precision).expect_display(FAILED);
526
527        if mantissa == 0 {
528            return Self { raw: 0, precision };
529        }
530
531        let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
532            .expect("Overflow in Price::from_mantissa_exponent");
533
534        #[allow(
535            clippy::useless_conversion,
536            reason = "i128 to PriceRaw is real when not high-precision"
537        )]
538        let raw: PriceRaw = raw_i128
539            .try_into()
540            .expect("Raw value exceeds PriceRaw range in Price::from_mantissa_exponent");
541        assert!(
542            raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX,
543            "`raw` value {raw} exceeded bounds [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
544        );
545
546        Self { raw, precision }
547    }
548}
549
550impl FromStr for Price {
551    type Err = String;
552
553    fn from_str(value: &str) -> Result<Self, Self::Err> {
554        let clean_value = value.replace('_', "");
555
556        let decimal = if clean_value.contains('e') || clean_value.contains('E') {
557            Decimal::from_scientific(&clean_value)
558                .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
559        } else {
560            Decimal::from_str(&clean_value)
561                .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
562        };
563
564        // Use decimal scale to preserve caller-specified precision (including trailing zeros)
565        let precision = decimal.scale() as u8;
566
567        Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string())
568    }
569}
570
571impl<T: AsRef<str>> From<T> for Price {
572    fn from(value: T) -> Self {
573        Self::from_str(value.as_ref()).expect(FAILED)
574    }
575}
576
577impl From<Price> for f64 {
578    fn from(price: Price) -> Self {
579        price.as_f64()
580    }
581}
582
583impl From<&Price> for f64 {
584    fn from(price: &Price) -> Self {
585        price.as_f64()
586    }
587}
588
589impl From<Price> for Decimal {
590    fn from(value: Price) -> Self {
591        value.as_decimal()
592    }
593}
594
595impl From<&Price> for Decimal {
596    fn from(value: &Price) -> Self {
597        value.as_decimal()
598    }
599}
600
601impl Hash for Price {
602    fn hash<H: Hasher>(&self, state: &mut H) {
603        self.raw.hash(state);
604    }
605}
606
607impl PartialEq for Price {
608    fn eq(&self, other: &Self) -> bool {
609        self.raw == other.raw
610    }
611}
612
613impl PartialOrd for Price {
614    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
615        Some(self.cmp(other))
616    }
617
618    fn lt(&self, other: &Self) -> bool {
619        self.raw.lt(&other.raw)
620    }
621
622    fn le(&self, other: &Self) -> bool {
623        self.raw.le(&other.raw)
624    }
625
626    fn gt(&self, other: &Self) -> bool {
627        self.raw.gt(&other.raw)
628    }
629
630    fn ge(&self, other: &Self) -> bool {
631        self.raw.ge(&other.raw)
632    }
633}
634
635impl Ord for Price {
636    fn cmp(&self, other: &Self) -> Ordering {
637        self.raw.cmp(&other.raw)
638    }
639}
640
641impl Deref for Price {
642    type Target = PriceRaw;
643
644    fn deref(&self) -> &Self::Target {
645        &self.raw
646    }
647}
648
649impl Neg for Price {
650    type Output = Self;
651    fn neg(self) -> Self::Output {
652        // Preserve sentinel values (negating PRICE_ERROR would also overflow)
653        if self.raw == PRICE_ERROR || self.raw == PRICE_UNDEF {
654            return self;
655        }
656        Self {
657            raw: -self.raw,
658            precision: self.precision,
659        }
660    }
661}
662
663impl Add for Price {
664    type Output = Self;
665    fn add(self, rhs: Self) -> Self::Output {
666        Self {
667            raw: self
668                .raw
669                .checked_add(rhs.raw)
670                .expect("Overflow occurred when adding `Price`"),
671            precision: self.precision.max(rhs.precision),
672        }
673    }
674}
675
676impl Sub for Price {
677    type Output = Self;
678    fn sub(self, rhs: Self) -> Self::Output {
679        Self {
680            raw: self
681                .raw
682                .checked_sub(rhs.raw)
683                .expect("Underflow occurred when subtracting `Price`"),
684            precision: self.precision.max(rhs.precision),
685        }
686    }
687}
688
689impl Add<Decimal> for Price {
690    type Output = Decimal;
691    fn add(self, rhs: Decimal) -> Self::Output {
692        self.as_decimal() + rhs
693    }
694}
695
696impl Sub<Decimal> for Price {
697    type Output = Decimal;
698    fn sub(self, rhs: Decimal) -> Self::Output {
699        self.as_decimal() - rhs
700    }
701}
702
703impl Mul<Decimal> for Price {
704    type Output = Decimal;
705    fn mul(self, rhs: Decimal) -> Self::Output {
706        self.as_decimal() * rhs
707    }
708}
709
710impl Div<Decimal> for Price {
711    type Output = Decimal;
712    fn div(self, rhs: Decimal) -> Self::Output {
713        self.as_decimal() / rhs
714    }
715}
716
717impl Add<f64> for Price {
718    type Output = f64;
719    fn add(self, rhs: f64) -> Self::Output {
720        self.as_f64() + rhs
721    }
722}
723
724impl Sub<f64> for Price {
725    type Output = f64;
726    fn sub(self, rhs: f64) -> Self::Output {
727        self.as_f64() - rhs
728    }
729}
730
731impl Mul<f64> for Price {
732    type Output = f64;
733    fn mul(self, rhs: f64) -> Self::Output {
734        self.as_f64() * rhs
735    }
736}
737
738impl Div<f64> for Price {
739    type Output = f64;
740    fn div(self, rhs: f64) -> Self::Output {
741        self.as_f64() / rhs
742    }
743}
744
745impl Debug for Price {
746    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
747        if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
748            write!(f, "{}({})", stringify!(Price), self.raw)
749        } else {
750            write!(f, "{}({})", stringify!(Price), self.as_decimal())
751        }
752    }
753}
754
755impl Display for Price {
756    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757        if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
758            write!(f, "{}", self.raw)
759        } else {
760            write!(f, "{}", self.as_decimal())
761        }
762    }
763}
764
765impl Serialize for Price {
766    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
767    where
768        S: serde::Serializer,
769    {
770        serializer.serialize_str(&self.to_string())
771    }
772}
773
774impl<'de> Deserialize<'de> for Price {
775    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
776    where
777        D: Deserializer<'de>,
778    {
779        let price_str: &str = Deserialize::deserialize(deserializer)?;
780        let price: Self = price_str.into();
781        Ok(price)
782    }
783}
784
785/// Checks the price `value` is positive.
786///
787/// # Errors
788///
789/// Returns an error if `value` is `PRICE_UNDEF` or not positive.
790pub fn check_positive_price(value: Price, param: &str) -> CorrectnessResult<()> {
791    if value.raw == PRICE_UNDEF {
792        return Err(CorrectnessError::InvalidValue {
793            param: param.to_string(),
794            value: "PRICE_UNDEF".to_string(),
795            type_name: "`Price`",
796        });
797    }
798
799    if !value.is_positive() {
800        return Err(CorrectnessError::NotPositive {
801            param: param.to_string(),
802            value: value.to_string(),
803            type_name: "`Price`",
804        });
805    }
806    Ok(())
807}
808
809#[cfg(feature = "high-precision")]
810/// The raw i64 price has already been scaled by 10^9. Further scale it by the difference to
811/// `FIXED_PRECISION` to make it high/defi-precision raw price.
812#[must_use]
813pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
814    PriceRaw::from(value) * PRECISION_DIFF_SCALAR as PriceRaw
815}
816
817#[cfg(not(feature = "high-precision"))]
818#[must_use]
819pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
820    value
821}
822
823#[cfg(test)]
824mod tests {
825    use nautilus_core::{approx_eq, correctness::CorrectnessError};
826    use rstest::rstest;
827    use rust_decimal_macros::dec;
828
829    use super::*;
830
831    #[rstest]
832    #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
833    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 50")]
834    fn test_invalid_precision_new() {
835        // Precision exceeds float precision limit
836        let _ = Price::new(1.0, 50);
837    }
838
839    #[rstest]
840    #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
841    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 50")]
842    fn test_invalid_precision_new() {
843        // Precision exceeds float precision limit
844        let _ = Price::new(1.0, 50);
845    }
846
847    #[rstest]
848    #[cfg(not(feature = "defi"))]
849    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
850    fn test_invalid_precision_from_raw() {
851        // Precision out of range for fixed
852        let _ = Price::from_raw(1, FIXED_PRECISION + 1);
853    }
854
855    #[rstest]
856    #[cfg(not(feature = "defi"))]
857    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
858    fn test_invalid_precision_max() {
859        // Precision out of range for fixed
860        let _ = Price::max(FIXED_PRECISION + 1);
861    }
862
863    #[rstest]
864    #[cfg(not(feature = "defi"))]
865    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
866    fn test_invalid_precision_min() {
867        // Precision out of range for fixed
868        let _ = Price::min(FIXED_PRECISION + 1);
869    }
870
871    #[rstest]
872    #[cfg(not(feature = "defi"))]
873    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
874    fn test_invalid_precision_zero() {
875        // Precision out of range for fixed
876        let _ = Price::zero(FIXED_PRECISION + 1);
877    }
878
879    #[rstest]
880    #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
881    fn test_max_value_exceeded() {
882        let _ = Price::new(PRICE_MAX + 0.1, FIXED_PRECISION);
883    }
884
885    #[rstest]
886    #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
887    fn test_min_value_exceeded() {
888        let _ = Price::new(PRICE_MIN - 0.1, FIXED_PRECISION);
889    }
890
891    #[rstest]
892    fn test_is_positive_ok() {
893        // A normal, non‑zero price should be positive.
894        let price = Price::new(42.0, 2);
895        assert!(price.is_positive());
896
897        // `check_positive_price` should accept it without error.
898        check_positive_price(price, "price").unwrap();
899    }
900
901    #[rstest]
902    fn test_is_positive_rejects_non_positive() {
903        // Zero is NOT positive.
904        let zero = Price::zero(2);
905        let error = check_positive_price(zero, "price").unwrap_err();
906
907        assert_eq!(
908            error,
909            CorrectnessError::NotPositive {
910                param: "price".to_string(),
911                value: "0.00".to_string(),
912                type_name: "`Price`",
913            }
914        );
915        assert_eq!(
916            error.to_string(),
917            "invalid `Price` for 'price' not positive, was 0.00"
918        );
919    }
920
921    #[rstest]
922    fn test_is_positive_rejects_undefined() {
923        // PRICE_UNDEF must also be rejected.
924        let undef = Price::from_raw(PRICE_UNDEF, 0);
925        let error = check_positive_price(undef, "price").unwrap_err();
926
927        assert_eq!(
928            error,
929            CorrectnessError::InvalidValue {
930                param: "price".to_string(),
931                value: "PRICE_UNDEF".to_string(),
932                type_name: "`Price`",
933            }
934        );
935        assert_eq!(
936            error.to_string(),
937            "invalid `Price` for 'price', was PRICE_UNDEF"
938        );
939    }
940
941    #[rstest]
942    fn test_construction() {
943        let price = Price::new_checked(1.23456, 4);
944        assert!(price.is_ok());
945        let price = price.unwrap();
946        assert_eq!(price.precision, 4);
947        assert!(approx_eq!(f64, price.as_f64(), 1.23456, epsilon = 0.0001));
948    }
949
950    #[rstest]
951    fn test_negative_price_in_range() {
952        // Use max fixed precision which varies based on feature flags
953        let neg_price = Price::new(PRICE_MIN / 2.0, FIXED_PRECISION);
954        assert!(neg_price.raw < 0);
955    }
956
957    #[rstest]
958    fn test_new_checked() {
959        // Use max fixed precision which varies based on feature flags
960        assert!(Price::new_checked(1.0, FIXED_PRECISION).is_ok());
961        assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
962        assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
963    }
964
965    #[rstest]
966    fn test_new_checked_returns_typed_error_with_stable_display() {
967        let error = Price::new_checked(PRICE_MAX + 1.0, FIXED_PRECISION).unwrap_err();
968
969        assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
970        assert_eq!(
971            error.to_string(),
972            format!(
973                "invalid f64 for 'value' not in range [{PRICE_MIN}, {PRICE_MAX}], was {}",
974                PRICE_MAX + 1.0
975            )
976        );
977    }
978
979    #[rstest]
980    fn test_from_raw_checked_returns_typed_error_with_stable_display() {
981        let error = Price::from_raw_checked(PRICE_UNDEF, 3).unwrap_err();
982
983        assert_eq!(
984            error,
985            CorrectnessError::PredicateViolation {
986                message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
987            }
988        );
989        assert_eq!(
990            error.to_string(),
991            "`precision` must be 0 when `raw` is PRICE_UNDEF"
992        );
993    }
994
995    #[rstest]
996    fn test_from_raw() {
997        let raw = 100 * FIXED_SCALAR as PriceRaw;
998        let price = Price::from_raw(raw, 2);
999        assert_eq!(price.raw, raw);
1000        assert_eq!(price.precision, 2);
1001    }
1002
1003    #[rstest]
1004    fn test_zero_constructor() {
1005        let zero = Price::zero(3);
1006        assert!(zero.is_zero());
1007        assert_eq!(zero.precision, 3);
1008    }
1009
1010    #[rstest]
1011    fn test_max_constructor() {
1012        let max = Price::max(4);
1013        assert_eq!(max.raw, PRICE_RAW_MAX);
1014        assert_eq!(max.precision, 4);
1015    }
1016
1017    #[rstest]
1018    fn test_min_constructor() {
1019        let min = Price::min(4);
1020        assert_eq!(min.raw, PRICE_RAW_MIN);
1021        assert_eq!(min.precision, 4);
1022    }
1023
1024    #[rstest]
1025    fn test_nan_validation() {
1026        assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
1027    }
1028
1029    #[rstest]
1030    fn test_infinity_validation() {
1031        assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
1032        assert!(Price::new_checked(f64::NEG_INFINITY, FIXED_PRECISION).is_err());
1033    }
1034
1035    #[rstest]
1036    fn test_special_values() {
1037        let zero = Price::zero(5);
1038        assert!(zero.is_zero());
1039        assert_eq!(zero.to_string(), "0.00000");
1040
1041        let undef = Price::from_raw(PRICE_UNDEF, 0);
1042        assert!(undef.is_undefined());
1043
1044        let error = ERROR_PRICE;
1045        assert_eq!(error.precision, 255);
1046    }
1047
1048    #[rstest]
1049    fn test_string_parsing() {
1050        let price: Price = "123.456".into();
1051        assert_eq!(price.precision, 3);
1052        assert_eq!(price, Price::from("123.456"));
1053    }
1054
1055    #[rstest]
1056    fn test_negative_price_from_str() {
1057        let price: Price = "-123.45".parse().unwrap();
1058        assert_eq!(price.precision, 2);
1059        assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-9));
1060    }
1061
1062    #[rstest]
1063    fn test_string_parsing_errors() {
1064        assert!(Price::from_str("invalid").is_err());
1065    }
1066
1067    #[rstest]
1068    #[case("1e7", 0, 10_000_000.0)]
1069    #[case("1.5e3", 0, 1_500.0)]
1070    #[case("1.234e-2", 5, 0.01234)]
1071    #[case("5E-3", 3, 0.005)]
1072    fn test_from_str_scientific_notation(
1073        #[case] input: &str,
1074        #[case] expected_precision: u8,
1075        #[case] expected_value: f64,
1076    ) {
1077        let price = Price::from_str(input).unwrap();
1078        assert_eq!(price.precision, expected_precision);
1079        assert!(approx_eq!(
1080            f64,
1081            price.as_f64(),
1082            expected_value,
1083            epsilon = 1e-10
1084        ));
1085    }
1086
1087    #[rstest]
1088    #[case("1_234.56", 2, 1234.56)]
1089    #[case("1000000", 0, 1_000_000.0)]
1090    #[case("99_999.999_99", 5, 99_999.999_99)]
1091    fn test_from_str_with_underscores(
1092        #[case] input: &str,
1093        #[case] expected_precision: u8,
1094        #[case] expected_value: f64,
1095    ) {
1096        let price = Price::from_str(input).unwrap();
1097        assert_eq!(price.precision, expected_precision);
1098        assert!(approx_eq!(
1099            f64,
1100            price.as_f64(),
1101            expected_value,
1102            epsilon = 1e-10
1103        ));
1104    }
1105
1106    #[rstest]
1107    fn test_from_decimal_dp_preservation() {
1108        // Test that decimal conversion preserves exact values
1109        let decimal = dec!(123.456789);
1110        let price = Price::from_decimal_dp(decimal, 6).unwrap();
1111        assert_eq!(price.precision, 6);
1112        assert!(approx_eq!(
1113            f64,
1114            price.as_f64(),
1115            123.456_789,
1116            epsilon = 1e-10
1117        ));
1118
1119        // Verify raw value is exact
1120        let expected_raw = 123_456_789 * 10_i64.pow(u32::from(FIXED_PRECISION - 6));
1121        assert_eq!(price.raw, PriceRaw::from(expected_raw));
1122    }
1123
1124    #[rstest]
1125    fn test_from_decimal_dp_rounding() {
1126        // Test banker's rounding (round half to even)
1127        let decimal = dec!(1.005);
1128        let price = Price::from_decimal_dp(decimal, 2).unwrap();
1129        assert_eq!(price.as_f64(), 1.0); // 1.005 rounds to 1.00 (even)
1130
1131        let decimal = dec!(1.015);
1132        let price = Price::from_decimal_dp(decimal, 2).unwrap();
1133        assert_eq!(price.as_f64(), 1.02); // 1.015 rounds to 1.02 (even)
1134    }
1135
1136    #[rstest]
1137    fn test_from_decimal_infers_precision() {
1138        // Test that precision is inferred from decimal's scale
1139        let decimal = dec!(123.456);
1140        let price = Price::from_decimal(decimal).unwrap();
1141        assert_eq!(price.precision, 3);
1142        assert!(approx_eq!(f64, price.as_f64(), 123.456, epsilon = 1e-10));
1143
1144        // Test with integer (precision 0)
1145        let decimal = dec!(100);
1146        let price = Price::from_decimal(decimal).unwrap();
1147        assert_eq!(price.precision, 0);
1148        assert_eq!(price.as_f64(), 100.0);
1149
1150        // Test with high precision
1151        let decimal = dec!(1.23456789);
1152        let price = Price::from_decimal(decimal).unwrap();
1153        assert_eq!(price.precision, 8);
1154        assert!(approx_eq!(
1155            f64,
1156            price.as_f64(),
1157            1.234_567_89,
1158            epsilon = 1e-10
1159        ));
1160    }
1161
1162    #[rstest]
1163    fn test_from_decimal_trailing_zeros() {
1164        // Decimal preserves trailing zeros in scale
1165        let decimal = dec!(1.230);
1166        assert_eq!(decimal.scale(), 3); // Has 3 decimal places
1167
1168        // from_decimal infers precision from scale (includes trailing zeros)
1169        let price = Price::from_decimal(decimal).unwrap();
1170        assert_eq!(price.precision, 3);
1171        assert!(approx_eq!(f64, price.as_f64(), 1.23, epsilon = 1e-10));
1172
1173        // Normalized removes trailing zeros
1174        let normalized = decimal.normalize();
1175        assert_eq!(normalized.scale(), 2);
1176        let price_normalized = Price::from_decimal(normalized).unwrap();
1177        assert_eq!(price_normalized.precision, 2);
1178    }
1179
1180    #[rstest]
1181    #[case("1.00", 2)]
1182    #[case("1.0", 1)]
1183    #[case("1.000", 3)]
1184    #[case("100.00", 2)]
1185    #[case("0.10", 2)]
1186    #[case("0.100", 3)]
1187    fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1188        let price = Price::from_str(input).unwrap();
1189        assert_eq!(price.precision, expected_precision);
1190    }
1191
1192    #[rstest]
1193    fn test_from_decimal_excessive_precision_inference() {
1194        // Create a decimal with more precision than FIXED_PRECISION
1195        // Decimal supports up to 28 decimal places
1196        let decimal = dec!(1.1234567890123456789012345678);
1197
1198        // If scale exceeds FIXED_PRECISION, from_decimal should error
1199        if decimal.scale() > u32::from(FIXED_PRECISION) {
1200            assert!(Price::from_decimal(decimal).is_err());
1201        }
1202    }
1203
1204    #[rstest]
1205    fn test_from_decimal_dp_out_of_range_returns_typed_error_with_stable_display() {
1206        let huge = Decimal::from_str("99999999999999999999.99").unwrap();
1207        let error = Price::from_decimal_dp(huge, 2).unwrap_err();
1208        match error {
1209            CorrectnessError::PredicateViolation { ref message } => {
1210                assert!(
1211                    message.contains("PriceRaw range") || message.contains("for Price"),
1212                    "unexpected message: {message:?}",
1213                );
1214            }
1215            _ => panic!("expected PredicateViolation, was {error:?}"),
1216        }
1217    }
1218
1219    #[rstest]
1220    fn test_from_decimal_negative_price() {
1221        // Negative prices are valid for Price
1222        let decimal = dec!(-123.45);
1223        let price = Price::from_decimal(decimal).unwrap();
1224        assert_eq!(price.precision, 2);
1225        assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-10));
1226        assert!(price.raw < 0);
1227    }
1228
1229    #[rstest]
1230    fn test_string_formatting() {
1231        assert_eq!(format!("{}", Price::new(1234.5678, 4)), "1234.5678");
1232        assert_eq!(
1233            format!("{:?}", Price::new(1234.5678, 4)),
1234            "Price(1234.5678)"
1235        );
1236        assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678");
1237    }
1238
1239    #[rstest]
1240    #[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] // Normal precision
1241    #[case(123.456_789_012_345, 8, "Price(123.45678901)", "123.45678901")] // At max normal precision
1242    #[cfg_attr(
1243        feature = "defi",
1244        case(
1245            2_000_000_000_000_000_000.0,
1246            18,
1247            "Price(2000000000000000000)",
1248            "2000000000000000000"
1249        )
1250    )] // High precision
1251    fn test_string_formatting_precision_handling(
1252        #[case] value: f64,
1253        #[case] precision: u8,
1254        #[case] expected_debug: &str,
1255        #[case] expected_display: &str,
1256    ) {
1257        let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1258            Price::from_raw(value as PriceRaw, precision)
1259        } else {
1260            Price::new(value, precision)
1261        };
1262
1263        assert_eq!(format!("{price:?}"), expected_debug);
1264        assert_eq!(format!("{price}"), expected_display);
1265        assert_eq!(
1266            price.to_formatted_string().replace('_', ""),
1267            expected_display
1268        );
1269    }
1270
1271    #[rstest]
1272    fn test_decimal_conversions() {
1273        let price = Price::new(123.456, 3);
1274        assert_eq!(price.as_decimal(), dec!(123.456));
1275
1276        let price = Price::new(0.000_001, 6);
1277        assert_eq!(price.as_decimal(), dec!(0.000001));
1278    }
1279
1280    #[rstest]
1281    fn test_basic_arithmetic() {
1282        let p1 = Price::new(10.5, 2);
1283        let p2 = Price::new(5.25, 2);
1284        assert_eq!(p1 + p2, Price::from("15.75"));
1285        assert_eq!(p1 - p2, Price::from("5.25"));
1286        assert_eq!(-p1, Price::from("-10.5"));
1287    }
1288
1289    #[rstest]
1290    fn test_price_checked_add_within_bounds() {
1291        let a = Price::new(10.0, 2);
1292        let b = Price::new(5.0, 2);
1293        assert_eq!(a.checked_add(b), Some(Price::new(15.0, 2)));
1294
1295        let neg = Price::new(-3.0, 2);
1296        assert_eq!(a.checked_add(neg), Some(Price::new(7.0, 2)));
1297    }
1298
1299    #[rstest]
1300    fn test_price_checked_add_above_max_returns_none() {
1301        let near_max = Price::from_raw(PRICE_RAW_MAX, 0);
1302        let one = Price::new(1.0, 0);
1303        assert_eq!(near_max.checked_add(one), None);
1304    }
1305
1306    #[rstest]
1307    fn test_price_checked_sub_within_bounds() {
1308        let a = Price::new(10.0, 2);
1309        let b = Price::new(3.0, 2);
1310        assert_eq!(a.checked_sub(b), Some(Price::new(7.0, 2)));
1311        assert_eq!(b.checked_sub(a), Some(Price::new(-7.0, 2)));
1312    }
1313
1314    #[rstest]
1315    fn test_price_checked_sub_below_min_returns_none() {
1316        let near_min = Price::from_raw(PRICE_RAW_MIN, 0);
1317        let one = Price::new(1.0, 0);
1318        assert_eq!(near_min.checked_sub(one), None);
1319    }
1320
1321    #[rstest]
1322    fn test_price_checked_arith_uses_max_precision() {
1323        let a = Price::new(10.5, 1);
1324        let b = Price::new(5.25, 2);
1325        let sum = a.checked_add(b).unwrap();
1326        assert_eq!(sum.precision, 2);
1327        assert_eq!(sum.as_f64(), 15.75);
1328    }
1329
1330    #[rstest]
1331    fn test_price_checked_add_rejects_sentinel_undef() {
1332        let undef = Price::from_raw(PRICE_UNDEF, 0);
1333        let one = Price::new(1.0, 0);
1334        assert_eq!(undef.checked_add(one), None);
1335        assert_eq!(one.checked_add(undef), None);
1336    }
1337
1338    #[rstest]
1339    fn test_price_checked_sub_rejects_sentinel_undef() {
1340        let undef = Price::from_raw(PRICE_UNDEF, 0);
1341        let neg_one = Price::new(-1.0, 0);
1342        assert_eq!(undef.checked_sub(neg_one), None);
1343    }
1344
1345    #[rstest]
1346    fn test_price_checked_arith_rejects_error_price() {
1347        let one = Price::new(1.0, 0);
1348        assert_eq!(ERROR_PRICE.checked_add(one), None);
1349        assert_eq!(one.checked_sub(ERROR_PRICE), None);
1350    }
1351
1352    #[rstest]
1353    fn test_price_checked_arith_rejects_raw_error() {
1354        let error = Price::from_raw(PRICE_ERROR, 0);
1355        let one = Price::new(1.0, 0);
1356        assert_eq!(error.checked_add(one), None);
1357        assert_eq!(one.checked_add(error), None);
1358        assert_eq!(error.checked_sub(one), None);
1359        assert_eq!(one.checked_sub(error), None);
1360    }
1361
1362    #[rstest]
1363    fn test_price_checked_add_at_exact_max_returns_some() {
1364        let near_max = Price::from_raw(PRICE_RAW_MAX - 1, 0);
1365        let one_unit = Price::from_raw(1, 0);
1366        assert_eq!(
1367            near_max.checked_add(one_unit),
1368            Some(Price::from_raw(PRICE_RAW_MAX, 0)),
1369        );
1370    }
1371
1372    #[rstest]
1373    fn test_price_checked_sub_at_exact_min_returns_some() {
1374        let near_min = Price::from_raw(PRICE_RAW_MIN + 1, 0);
1375        let one_unit = Price::from_raw(1, 0);
1376        assert_eq!(
1377            near_min.checked_sub(one_unit),
1378            Some(Price::from_raw(PRICE_RAW_MIN, 0)),
1379        );
1380    }
1381
1382    #[rstest]
1383    fn test_mixed_precision_add() {
1384        let p1 = Price::new(10.5, 1);
1385        let p2 = Price::new(5.25, 2);
1386        let result = p1 + p2;
1387        assert_eq!(result.precision, 2);
1388        assert_eq!(result.as_f64(), 15.75);
1389    }
1390
1391    #[rstest]
1392    fn test_mixed_precision_sub() {
1393        let p1 = Price::new(10.5, 1);
1394        let p2 = Price::new(5.25, 2);
1395        let result = p1 - p2;
1396        assert_eq!(result.precision, 2);
1397        assert_eq!(result.as_f64(), 5.25);
1398    }
1399
1400    #[rstest]
1401    fn test_f64_operations() {
1402        let p = Price::new(10.5, 2);
1403        assert_eq!(p + 1.0, 11.5);
1404        assert_eq!(p - 1.0, 9.5);
1405        assert_eq!(p * 2.0, 21.0);
1406        assert_eq!(p / 2.0, 5.25);
1407    }
1408
1409    #[rstest]
1410    fn test_equality_and_comparisons() {
1411        let p1 = Price::new(10.0, 1);
1412        let p2 = Price::new(20.0, 1);
1413        let p3 = Price::new(10.0, 1);
1414
1415        assert!(p1 < p2);
1416        assert!(p2 > p1);
1417        assert!(p1 <= p3);
1418        assert!(p1 >= p3);
1419        assert_eq!(p1, p3);
1420        assert_ne!(p1, p2);
1421
1422        assert_eq!(Price::from("1.0"), Price::from("1.0"));
1423        assert_ne!(Price::from("1.1"), Price::from("1.0"));
1424        assert!(Price::from("1.0") <= Price::from("1.0"));
1425        assert!(Price::from("1.1") > Price::from("1.0"));
1426        assert!(Price::from("1.0") >= Price::from("1.0"));
1427        assert!(Price::from("1.0") >= Price::from("1.0"));
1428        assert!(Price::from("1.0") >= Price::from("1.0"));
1429        assert!(Price::from("0.9") < Price::from("1.0"));
1430        assert!(Price::from("0.9") <= Price::from("1.0"));
1431        assert!(Price::from("0.9") <= Price::from("1.0"));
1432    }
1433
1434    #[rstest]
1435    fn test_deref() {
1436        let price = Price::new(10.0, 1);
1437        assert_eq!(*price, price.raw);
1438    }
1439
1440    #[rstest]
1441    fn test_decode_raw_price_i64() {
1442        let raw_scaled_by_1e9 = 42_000_000_000i64; // 42.0 * 10^9
1443        let decoded = decode_raw_price_i64(raw_scaled_by_1e9);
1444        let price = Price::from_raw(decoded, FIXED_PRECISION);
1445        assert!(
1446            approx_eq!(f64, price.as_f64(), 42.0, epsilon = 1e-9),
1447            "Expected 42.0 f64, was {} (precision = {})",
1448            price.as_f64(),
1449            price.precision
1450        );
1451    }
1452
1453    #[rstest]
1454    fn test_hash() {
1455        use std::{
1456            collections::hash_map::DefaultHasher,
1457            hash::{Hash, Hasher},
1458        };
1459
1460        let price1 = Price::new(1.0, 2);
1461        let price2 = Price::new(1.0, 2);
1462        let price3 = Price::new(1.1, 2);
1463
1464        let mut hasher1 = DefaultHasher::new();
1465        let mut hasher2 = DefaultHasher::new();
1466        let mut hasher3 = DefaultHasher::new();
1467
1468        price1.hash(&mut hasher1);
1469        price2.hash(&mut hasher2);
1470        price3.hash(&mut hasher3);
1471
1472        assert_eq!(hasher1.finish(), hasher2.finish());
1473        assert_ne!(hasher1.finish(), hasher3.finish());
1474    }
1475
1476    #[rstest]
1477    fn test_price_serde_json_round_trip() {
1478        let price = Price::new(1.0500, 4);
1479        let json = serde_json::to_string(&price).unwrap();
1480        let deserialized: Price = serde_json::from_str(&json).unwrap();
1481        assert_eq!(deserialized, price);
1482    }
1483
1484    #[rstest]
1485    fn test_from_mantissa_exponent_exact_precision() {
1486        let price = Price::from_mantissa_exponent(12345, -2, 2);
1487        assert_eq!(price.as_f64(), 123.45);
1488    }
1489
1490    #[rstest]
1491    fn test_from_mantissa_exponent_excess_rounds_down() {
1492        // 12.345 rounds to 12.34 (4 is even, banker's rounding)
1493        let price = Price::from_mantissa_exponent(12345, -3, 2);
1494        assert_eq!(price.as_f64(), 12.34);
1495    }
1496
1497    #[rstest]
1498    fn test_from_mantissa_exponent_excess_rounds_up() {
1499        // 12.355 rounds to 12.36 (5 is odd, banker's rounding)
1500        let price = Price::from_mantissa_exponent(12355, -3, 2);
1501        assert_eq!(price.as_f64(), 12.36);
1502    }
1503
1504    #[rstest]
1505    fn test_from_mantissa_exponent_positive_exponent() {
1506        let price = Price::from_mantissa_exponent(5, 2, 0);
1507        assert_eq!(price.as_f64(), 500.0);
1508    }
1509
1510    #[rstest]
1511    fn test_from_mantissa_exponent_negative_mantissa() {
1512        let price = Price::from_mantissa_exponent(-12345, -2, 2);
1513        assert_eq!(price.as_f64(), -123.45);
1514    }
1515
1516    #[rstest]
1517    fn test_from_mantissa_exponent_zero() {
1518        let price = Price::from_mantissa_exponent(0, 2, 2);
1519        assert_eq!(price.as_f64(), 0.0);
1520    }
1521
1522    #[rstest]
1523    #[should_panic(expected = "Overflow")]
1524    fn test_from_mantissa_exponent_overflow_panics() {
1525        let _ = Price::from_mantissa_exponent(i64::MAX, 9, 0);
1526    }
1527
1528    #[rstest]
1529    #[should_panic(expected = "exceeds i128 range")]
1530    fn test_from_mantissa_exponent_large_exponent_panics() {
1531        let _ = Price::from_mantissa_exponent(1, 119, 0);
1532    }
1533
1534    #[rstest]
1535    fn test_from_mantissa_exponent_zero_with_large_exponent() {
1536        let price = Price::from_mantissa_exponent(0, 119, 0);
1537        assert_eq!(price.as_f64(), 0.0);
1538    }
1539
1540    #[rstest]
1541    fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1542        let price = Price::from_mantissa_exponent(12345, -120, 2);
1543        assert_eq!(price.as_f64(), 0.0);
1544    }
1545
1546    #[rstest]
1547    fn test_decimal_arithmetic_operations() {
1548        let price = Price::new(100.0, 2);
1549        assert_eq!(price + dec!(50.25), dec!(150.25));
1550        assert_eq!(price - dec!(30.50), dec!(69.50));
1551        assert_eq!(price * dec!(1.5), dec!(150.00));
1552        assert_eq!(price / dec!(4), dec!(25.00));
1553    }
1554}
1555
1556#[cfg(test)]
1557mod property_tests {
1558    use proptest::prelude::*;
1559    use rstest::rstest;
1560
1561    use super::*;
1562
1563    /// Strategy to generate valid price values within the allowed range.
1564    fn price_value_strategy() -> impl Strategy<Value = f64> {
1565        // Use a reasonable range that's well within PRICE_MIN/PRICE_MAX
1566        // but still tests edge cases with various scales
1567        prop_oneof![
1568            // Small positive values
1569            0.00001..1.0,
1570            // Normal trading range
1571            1.0..100_000.0,
1572            // Large values (but safe)
1573            100_000.0..1_000_000.0,
1574            // Small negative values (for spreads, etc.)
1575            -1_000.0..0.0,
1576            // Boundary values close to the extremes
1577            Just(PRICE_MIN / 2.0),
1578            Just(PRICE_MAX / 2.0),
1579        ]
1580    }
1581
1582    fn float_precision_upper_bound() -> u8 {
1583        FIXED_PRECISION.min(crate::types::fixed::MAX_FLOAT_PRECISION)
1584    }
1585
1586    /// Strategy to exercise both typical and extreme precision values.
1587    fn precision_strategy() -> impl Strategy<Value = u8> {
1588        let upper = float_precision_upper_bound();
1589        prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1590    }
1591
1592    fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1593        let upper = float_precision_upper_bound().max(1);
1594        prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1595    }
1596
1597    /// Strategy to generate a valid (precision, raw) pair where raw is properly scaled.
1598    ///
1599    /// Raw values must be multiples of `10^(FIXED_PRECISION` - precision) to pass validation.
1600    fn valid_precision_raw_strategy() -> impl Strategy<Value = (u8, PriceRaw)> {
1601        precision_strategy().prop_flat_map(|precision| {
1602            let scale: PriceRaw = if precision >= FIXED_PRECISION {
1603                1
1604            } else {
1605                (10 as PriceRaw).pow(u32::from(FIXED_PRECISION - precision))
1606            };
1607            // Generate a base value, then multiply by scale to ensure valid raw
1608            let max_base = PRICE_RAW_MAX / scale;
1609            let min_base = PRICE_RAW_MIN / scale;
1610            (min_base..=max_base).prop_map(move |base| (precision, base * scale))
1611        })
1612    }
1613
1614    /// Strategy to generate valid precision values for float-based constructors.
1615    fn float_precision_strategy() -> impl Strategy<Value = u8> {
1616        precision_strategy()
1617    }
1618
1619    const DECIMAL_MAX_MANTISSA: i128 = 79_228_162_514_264_337_593_543_950_335;
1620
1621    #[expect(
1622        clippy::useless_conversion,
1623        reason = "PriceRaw is i64 or i128 depending on feature"
1624    )]
1625    fn decimal_compatible(raw: PriceRaw, precision: u8) -> bool {
1626        if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1627            return false;
1628        }
1629        let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1630        let divisor = (10 as PriceRaw).pow(precision_diff);
1631        let rescaled_raw = raw / divisor;
1632        i128::from(rescaled_raw.abs()) <= DECIMAL_MAX_MANTISSA
1633    }
1634
1635    proptest! {
1636        /// Property: Price string serialization round-trip should preserve value and precision
1637        #[rstest]
1638        fn prop_price_serde_round_trip(
1639            value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1640            precision in precision_strategy()
1641        ) {
1642            let original = Price::new(value, precision);
1643
1644            // String round-trip (this should be exact and is the most important)
1645            let string_repr = original.to_string();
1646            let from_string: Price = string_repr.parse().unwrap();
1647            prop_assert_eq!(from_string.raw, original.raw);
1648            prop_assert_eq!(from_string.precision, original.precision);
1649
1650            // JSON round-trip basic validation (just ensure it doesn't crash and preserves precision)
1651            let json = serde_json::to_string(&original).unwrap();
1652            let from_json: Price = serde_json::from_str(&json).unwrap();
1653            prop_assert_eq!(from_json.precision, original.precision);
1654            // Note: JSON may have minor floating-point precision differences due to f64 limitations
1655        }
1656
1657        /// Property: Price arithmetic should be associative for same precision
1658        #[rstest]
1659        fn prop_price_arithmetic_associative(
1660            a in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1661            b in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1662            c in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1663            precision in precision_strategy()
1664        ) {
1665            let p_a = Price::new(a, precision);
1666            let p_b = Price::new(b, precision);
1667            let p_c = Price::new(c, precision);
1668
1669            // Check if we can perform the operations without overflow using raw arithmetic
1670            let ab_raw = p_a.raw.checked_add(p_b.raw);
1671            let bc_raw = p_b.raw.checked_add(p_c.raw);
1672
1673            if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1674                let ab_c_raw = ab_raw.checked_add(p_c.raw);
1675                let a_bc_raw = p_a.raw.checked_add(bc_raw);
1676
1677                if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1678                    // (a + b) + c == a + (b + c) using raw arithmetic (exact)
1679                    prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1680                }
1681            }
1682        }
1683
1684        /// Property: Price addition/subtraction should be inverse operations
1685        #[rstest]
1686        fn prop_price_addition_subtraction_inverse(
1687            base in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1688            delta in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1689            precision in precision_strategy()
1690        ) {
1691            let p_base = Price::new(base, precision);
1692            let p_delta = Price::new(delta, precision);
1693
1694            // Use raw arithmetic to avoid floating-point precision issues
1695            if let Some(added_raw) = p_base.raw.checked_add(p_delta.raw)
1696                && let Some(result_raw) = added_raw.checked_sub(p_delta.raw) {
1697                    // (base + delta) - delta should equal base exactly using raw arithmetic
1698                    prop_assert_eq!(result_raw, p_base.raw, "Inverse operation failed in raw arithmetic");
1699                }
1700        }
1701
1702        /// Property: Price ordering should be transitive
1703        #[rstest]
1704        fn prop_price_ordering_transitive(
1705            a in price_value_strategy(),
1706            b in price_value_strategy(),
1707            c in price_value_strategy(),
1708            precision in float_precision_strategy()
1709        ) {
1710            let p_a = Price::new(a, precision);
1711            let p_b = Price::new(b, precision);
1712            let p_c = Price::new(c, precision);
1713
1714            // If a <= b and b <= c, then a <= c
1715            if p_a <= p_b && p_b <= p_c {
1716                prop_assert!(p_a <= p_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1717                    p_a.as_f64(), p_b.as_f64(), p_c.as_f64(), p_a.as_f64(), p_c.as_f64());
1718            }
1719        }
1720
1721        /// Property: String parsing should be consistent with precision inference
1722        #[rstest]
1723        fn prop_price_string_parsing_precision(
1724            integral in 0u32..1_000_000,
1725            fractional in 0u32..1_000_000,
1726            precision in precision_strategy_non_zero()
1727        ) {
1728            // Create a decimal string with exactly 'precision' decimal places
1729            let pow = 10u128.pow(u32::from(precision));
1730            let fractional_mod = u128::from(fractional) % pow;
1731            let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
1732            let price_str = format!("{integral}.{fractional_str}");
1733
1734            let parsed: Price = price_str.parse().unwrap();
1735            prop_assert_eq!(parsed.precision, precision);
1736
1737            // Round-trip should preserve the original string (after normalization)
1738            let round_trip = parsed.to_string();
1739            let expected_value = format!("{integral}.{fractional_str}");
1740            prop_assert_eq!(round_trip, expected_value);
1741        }
1742
1743        /// Property: Price with higher precision should contain more or equal information
1744        #[rstest]
1745        fn prop_price_precision_information_preservation(
1746            value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1747            precision1 in precision_strategy_non_zero(),
1748            precision2 in precision_strategy_non_zero()
1749        ) {
1750            // Skip cases where precisions are equal (trivial case)
1751            prop_assume!(precision1 != precision2);
1752
1753            let _p1 = Price::new(value, precision1);
1754            let _p2 = Price::new(value, precision2);
1755
1756            // When both prices are created from the same value with different precisions,
1757            // converting both to the lower precision should yield the same result
1758            let min_precision = precision1.min(precision2);
1759
1760            // Round the original value to the minimum precision first
1761            let scale = 10.0_f64.powi(i32::from(min_precision));
1762            let rounded_value = (value * scale).round() / scale;
1763
1764            let p1_reduced = Price::new(rounded_value, min_precision);
1765            let p2_reduced = Price::new(rounded_value, min_precision);
1766
1767            // They should be exactly equal when created from the same rounded value
1768            prop_assert_eq!(p1_reduced.raw, p2_reduced.raw, "Precision reduction inconsistent");
1769        }
1770
1771        /// Property: Price arithmetic should never produce invalid values
1772        #[rstest]
1773        fn prop_price_arithmetic_bounds(
1774            a in price_value_strategy(),
1775            b in price_value_strategy(),
1776            precision in float_precision_strategy()
1777        ) {
1778            let p_a = Price::new(a, precision);
1779            let p_b = Price::new(b, precision);
1780
1781            // Addition should either succeed or fail predictably
1782            let sum_f64 = p_a.as_f64() + p_b.as_f64();
1783            if sum_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&sum_f64) {
1784                let sum = p_a + p_b;
1785                prop_assert!(sum.as_f64().is_finite());
1786                prop_assert!(!sum.is_undefined());
1787            }
1788
1789            // Subtraction should either succeed or fail predictably
1790            let diff_f64 = p_a.as_f64() - p_b.as_f64();
1791            if diff_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&diff_f64) {
1792                let diff = p_a - p_b;
1793                prop_assert!(diff.as_f64().is_finite());
1794                prop_assert!(!diff.is_undefined());
1795            }
1796        }
1797
1798        /// Property: checked_add agrees with Add when bounds and sentinel guards hold,
1799        /// and returns None otherwise.
1800        #[rstest]
1801        fn prop_price_checked_add_matches_spec(
1802            a in price_value_strategy(),
1803            b in price_value_strategy(),
1804            precision in float_precision_strategy()
1805        ) {
1806            let p_a = Price::new(a, precision);
1807            let p_b = Price::new(b, precision);
1808            let expected = p_a.raw
1809                .checked_add(p_b.raw)
1810                .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
1811                .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
1812                .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
1813            prop_assert_eq!(p_a.checked_add(p_b), expected);
1814        }
1815
1816        /// Property: checked_sub agrees with Sub when bounds and sentinel guards hold,
1817        /// and returns None otherwise.
1818        #[rstest]
1819        fn prop_price_checked_sub_matches_spec(
1820            a in price_value_strategy(),
1821            b in price_value_strategy(),
1822            precision in float_precision_strategy()
1823        ) {
1824            let p_a = Price::new(a, precision);
1825            let p_b = Price::new(b, precision);
1826            let expected = p_a.raw
1827                .checked_sub(p_b.raw)
1828                .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
1829                .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
1830                .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
1831            prop_assert_eq!(p_a.checked_sub(p_b), expected);
1832        }
1833    }
1834
1835    proptest! {
1836        /// Property: as_decimal scale always matches precision
1837        #[rstest]
1838        fn prop_price_as_decimal_preserves_precision(
1839            (precision, raw) in valid_precision_raw_strategy()
1840        ) {
1841            prop_assume!(decimal_compatible(raw, precision));
1842            let price = Price::from_raw(raw, precision);
1843            let decimal = price.as_decimal();
1844            prop_assert_eq!(decimal.scale(), u32::from(precision));
1845        }
1846
1847        /// Property: as_decimal and Display produce the same string
1848        #[rstest]
1849        fn prop_price_as_decimal_matches_display(
1850            value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1851            precision in float_precision_strategy()
1852        ) {
1853            let price = Price::new(value, precision);
1854            prop_assume!(decimal_compatible(price.raw, precision));
1855            let display_str = format!("{price}");
1856            let decimal_str = price.as_decimal().to_string();
1857            prop_assert_eq!(display_str, decimal_str);
1858        }
1859
1860        /// Property: from_decimal roundtrip preserves exact value
1861        #[rstest]
1862        fn prop_price_from_decimal_roundtrip(
1863            (precision, raw) in valid_precision_raw_strategy()
1864        ) {
1865            prop_assume!(decimal_compatible(raw, precision));
1866            let original = Price::from_raw(raw, precision);
1867            let decimal = original.as_decimal();
1868            let reconstructed = Price::from_decimal(decimal).unwrap();
1869            prop_assert_eq!(original.raw, reconstructed.raw);
1870            prop_assert_eq!(original.precision, reconstructed.precision);
1871        }
1872
1873        /// Property: constructing from valid raw values preserves raw/precision fields
1874        #[rstest]
1875        fn prop_price_from_raw_round_trip(
1876            (precision, raw) in valid_precision_raw_strategy()
1877        ) {
1878            let price = Price::from_raw(raw, precision);
1879            prop_assert_eq!(price.raw, raw);
1880            prop_assert_eq!(price.precision, precision);
1881        }
1882    }
1883}