nautilus_model/types/
money.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 an amount of money in a specified currency denomination.
17
18use std::{
19    cmp::Ordering,
20    fmt::{Debug, Display},
21    hash::{Hash, Hasher},
22    ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign},
23    str::FromStr,
24};
25
26use nautilus_core::{
27    correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true},
28    formatting::Separable,
29};
30use rust_decimal::{Decimal, prelude::ToPrimitive};
31use serde::{Deserialize, Deserializer, Serialize};
32
33#[cfg(not(any(feature = "defi", feature = "high-precision")))]
34use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
35#[cfg(any(feature = "defi", feature = "high-precision"))]
36use super::fixed::{f64_to_fixed_i128, fixed_i128_to_f64};
37#[cfg(feature = "defi")]
38use crate::types::fixed::MAX_FLOAT_PRECISION;
39use crate::types::{
40    Currency,
41    fixed::{FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision},
42};
43
44// -----------------------------------------------------------------------------
45// MoneyRaw
46// -----------------------------------------------------------------------------
47
48#[cfg(feature = "high-precision")]
49pub type MoneyRaw = i128;
50
51#[cfg(not(feature = "high-precision"))]
52pub type MoneyRaw = i64;
53
54// -----------------------------------------------------------------------------
55
56/// The maximum raw money integer value.
57///
58/// # Safety
59///
60/// This value is computed at compile time from MONEY_MAX * FIXED_SCALAR.
61/// The multiplication is guaranteed not to overflow because MONEY_MAX and FIXED_SCALAR
62/// are chosen such that their product fits within MoneyRaw's range in both
63/// high-precision (i128) and standard-precision (i64) modes.
64#[unsafe(no_mangle)]
65#[allow(unsafe_code)]
66pub static MONEY_RAW_MAX: MoneyRaw = (MONEY_MAX * FIXED_SCALAR) as MoneyRaw;
67
68/// The minimum raw money integer value.
69///
70/// # Safety
71///
72/// This value is computed at compile time from MONEY_MIN * FIXED_SCALAR.
73/// The multiplication is guaranteed not to overflow because MONEY_MIN and FIXED_SCALAR
74/// are chosen such that their product fits within MoneyRaw's range in both
75/// high-precision (i128) and standard-precision (i64) modes.
76#[unsafe(no_mangle)]
77#[allow(unsafe_code)]
78pub static MONEY_RAW_MIN: MoneyRaw = (MONEY_MIN * FIXED_SCALAR) as MoneyRaw;
79
80// -----------------------------------------------------------------------------
81// MONEY_MAX
82// -----------------------------------------------------------------------------
83
84#[cfg(feature = "high-precision")]
85/// The maximum valid money amount that can be represented.
86pub const MONEY_MAX: f64 = 17_014_118_346_046.0;
87
88#[cfg(not(feature = "high-precision"))]
89/// The maximum valid money amount that can be represented.
90pub const MONEY_MAX: f64 = 9_223_372_036.0;
91
92// -----------------------------------------------------------------------------
93// MONEY_MIN
94// -----------------------------------------------------------------------------
95
96#[cfg(feature = "high-precision")]
97/// The minimum valid money amount that can be represented.
98pub const MONEY_MIN: f64 = -17_014_118_346_046.0;
99
100#[cfg(not(feature = "high-precision"))]
101/// The minimum valid money amount that can be represented.
102pub const MONEY_MIN: f64 = -9_223_372_036.0;
103
104// -----------------------------------------------------------------------------
105
106/// Represents an amount of money in a specified currency denomination.
107///
108/// - [`MONEY_MAX`] - Maximum representable money amount
109/// - [`MONEY_MIN`] - Minimum representable money amount
110#[repr(C)]
111#[derive(Clone, Copy, Eq)]
112#[cfg_attr(
113    feature = "python",
114    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", frozen)
115)]
116pub struct Money {
117    /// Represents the raw fixed-point amount, with `currency.precision` defining the number of decimal places.
118    pub raw: MoneyRaw,
119    /// The currency denomination associated with the monetary amount.
120    pub currency: Currency,
121}
122
123impl Money {
124    /// Creates a new [`Money`] instance with correctness checking.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if `amount` is invalid outside the representable range [MONEY_MIN, MONEY_MAX].
129    ///
130    /// # Notes
131    ///
132    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
133    pub fn new_checked(amount: f64, currency: Currency) -> anyhow::Result<Self> {
134        // SAFETY: check_in_range_inclusive_f64 already validates that amount is finite
135        // (not NaN or infinite) as part of its range validation logic, so no additional
136        // infinity checks are needed here.
137        check_in_range_inclusive_f64(amount, MONEY_MIN, MONEY_MAX, "amount")?;
138
139        #[cfg(feature = "defi")]
140        if currency.precision > MAX_FLOAT_PRECISION {
141            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
142            anyhow::bail!(
143                "`currency.precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Money::from_wei()` for wei values instead"
144            );
145        }
146
147        #[cfg(feature = "high-precision")]
148        let raw = f64_to_fixed_i128(amount, currency.precision);
149
150        #[cfg(not(feature = "high-precision"))]
151        let raw = f64_to_fixed_i64(amount, currency.precision);
152
153        Ok(Self { raw, currency })
154    }
155
156    /// Creates a new [`Money`] instance.
157    ///
158    /// # Panics
159    ///
160    /// Panics if a correctness check fails. See [`Money::new_checked`] for more details.
161    pub fn new(amount: f64, currency: Currency) -> Self {
162        Self::new_checked(amount, currency).expect(FAILED)
163    }
164
165    /// Creates a new [`Money`] instance from the given `raw` fixed-point value and the specified `currency`.
166    ///
167    /// # Panics
168    ///
169    /// Panics if `raw` is outside the representable range [`MONEY_RAW_MIN`, `MONEY_RAW_MAX`].
170    /// Panics if `currency.precision` exceeds [`FIXED_PRECISION`].
171    #[must_use]
172    pub fn from_raw(raw: MoneyRaw, currency: Currency) -> Self {
173        check_predicate_true(
174            raw >= MONEY_RAW_MIN && raw <= MONEY_RAW_MAX,
175            &format!(
176                "`raw` value {raw} exceeded bounds [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}] for Money"
177            ),
178        )
179        .expect(FAILED);
180        check_fixed_precision(currency.precision).expect(FAILED);
181
182        // TODO: Enforce spurious bits validation in v2
183        // Validate raw value has no spurious bits beyond the precision scale
184        // if raw != 0 {
185        //     #[cfg(feature = "high-precision")]
186        //     super::fixed::check_fixed_raw_i128(raw, currency.precision).expect(FAILED);
187        //     #[cfg(not(feature = "high-precision"))]
188        //     super::fixed::check_fixed_raw_i64(raw, currency.precision).expect(FAILED);
189        // }
190
191        Self { raw, currency }
192    }
193
194    /// Creates a new [`Money`] instance with a value of zero with the given [`Currency`].
195    ///
196    /// # Panics
197    ///
198    /// Panics if a correctness check fails. See [`Money::new_checked`] for more details.
199    #[must_use]
200    pub fn zero(currency: Currency) -> Self {
201        Self::new(0.0, currency)
202    }
203
204    /// Returns `true` if the value of this instance is zero.
205    #[must_use]
206    pub fn is_zero(&self) -> bool {
207        self.raw == 0
208    }
209
210    #[cfg(feature = "high-precision")]
211    /// Returns the value of this instance as an `f64`.
212    ///
213    /// # Panics
214    ///
215    /// Panics if precision is beyond `MAX_FLOAT_PRECISION` (16).
216    #[must_use]
217    pub fn as_f64(&self) -> f64 {
218        #[cfg(feature = "defi")]
219        if self.currency.precision > MAX_FLOAT_PRECISION {
220            panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
221        }
222
223        fixed_i128_to_f64(self.raw)
224    }
225
226    #[cfg(not(feature = "high-precision"))]
227    /// Returns the value of this instance as an `f64`.
228    ///
229    /// # Panics
230    ///
231    /// Panics if precision is beyond `MAX_FLOAT_PRECISION` (16).
232    #[must_use]
233    pub fn as_f64(&self) -> f64 {
234        #[cfg(feature = "defi")]
235        if self.currency.precision > MAX_FLOAT_PRECISION {
236            panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
237        }
238
239        fixed_i64_to_f64(self.raw)
240    }
241
242    /// Returns the value of this instance as a `Decimal`.
243    #[must_use]
244    pub fn as_decimal(&self) -> Decimal {
245        // Scale down the raw value to match the precision
246        let precision = self.currency.precision;
247        let precision_diff = FIXED_PRECISION.saturating_sub(precision);
248
249        // Money's raw value is stored at fixed precision scale, but needs to be adjusted
250        // to the currency's actual precision for decimal conversion.
251        let rescaled_raw = self.raw / MoneyRaw::pow(10, u32::from(precision_diff));
252
253        #[allow(clippy::useless_conversion)]
254        Decimal::from_i128_with_scale(i128::from(rescaled_raw), u32::from(precision))
255    }
256
257    /// Returns a formatted string representation of this instance.
258    #[must_use]
259    pub fn to_formatted_string(&self) -> String {
260        let amount_str = format!("{:.*}", self.currency.precision as usize, self.as_f64())
261            .separate_with_underscores();
262        format!("{} {}", amount_str, self.currency.code)
263    }
264
265    /// Creates a new [`Money`] from a `Decimal` value with specified currency.
266    ///
267    /// This method provides more reliable parsing by using Decimal arithmetic
268    /// to avoid floating-point precision issues during conversion.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if:
273    /// - The decimal value cannot be converted to the raw representation.
274    /// - Overflow occurs during scaling.
275    pub fn from_decimal(decimal: Decimal, currency: Currency) -> anyhow::Result<Self> {
276        let precision = currency.precision;
277
278        let scale_factor = Decimal::from(10_i64.pow(precision as u32));
279        let scaled = decimal * scale_factor;
280        let rounded = scaled.round();
281
282        #[cfg(feature = "high-precision")]
283        let raw_at_precision: MoneyRaw = rounded.to_i128().ok_or_else(|| {
284            anyhow::anyhow!("Decimal value '{decimal}' cannot be converted to i128")
285        })?;
286        #[cfg(not(feature = "high-precision"))]
287        let raw_at_precision: MoneyRaw = rounded.to_i64().ok_or_else(|| {
288            anyhow::anyhow!("Decimal value '{decimal}' cannot be converted to i64")
289        })?;
290
291        let scale_up = 10_i64.pow((FIXED_PRECISION - precision) as u32) as MoneyRaw;
292        let raw = raw_at_precision
293            .checked_mul(scale_up)
294            .ok_or_else(|| anyhow::anyhow!("Overflow when scaling to fixed precision"))?;
295
296        Ok(Self { raw, currency })
297    }
298}
299
300impl FromStr for Money {
301    type Err = String;
302
303    fn from_str(value: &str) -> Result<Self, Self::Err> {
304        let parts: Vec<&str> = value.split_whitespace().collect();
305
306        // Ensure we have both the amount and currency
307        if parts.len() != 2 {
308            return Err(format!(
309                "Error invalid input format '{value}'. Expected '<amount> <currency>'"
310            ));
311        }
312
313        let clean_amount = parts[0].replace('_', "");
314
315        let decimal = if clean_amount.contains('e') || clean_amount.contains('E') {
316            Decimal::from_scientific(&clean_amount)
317                .map_err(|e| format!("Error parsing amount '{}' as Decimal: {e}", parts[0]))?
318        } else {
319            Decimal::from_str(&clean_amount)
320                .map_err(|e| format!("Error parsing amount '{}' as Decimal: {e}", parts[0]))?
321        };
322
323        let currency = Currency::from_str(parts[1]).map_err(|e: anyhow::Error| e.to_string())?;
324        Self::from_decimal(decimal, currency).map_err(|e| e.to_string())
325    }
326}
327
328impl<T: AsRef<str>> From<T> for Money {
329    fn from(value: T) -> Self {
330        Self::from_str(value.as_ref()).expect(FAILED)
331    }
332}
333
334impl From<Money> for f64 {
335    fn from(money: Money) -> Self {
336        money.as_f64()
337    }
338}
339
340impl From<&Money> for f64 {
341    fn from(money: &Money) -> Self {
342        money.as_f64()
343    }
344}
345
346impl Hash for Money {
347    fn hash<H: Hasher>(&self, state: &mut H) {
348        self.raw.hash(state);
349        self.currency.hash(state);
350    }
351}
352
353impl PartialEq for Money {
354    fn eq(&self, other: &Self) -> bool {
355        self.raw == other.raw && self.currency == other.currency
356    }
357}
358
359impl PartialOrd for Money {
360    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
361        Some(self.cmp(other))
362    }
363
364    fn lt(&self, other: &Self) -> bool {
365        assert_eq!(self.currency, other.currency);
366        self.raw.lt(&other.raw)
367    }
368
369    fn le(&self, other: &Self) -> bool {
370        assert_eq!(self.currency, other.currency);
371        self.raw.le(&other.raw)
372    }
373
374    fn gt(&self, other: &Self) -> bool {
375        assert_eq!(self.currency, other.currency);
376        self.raw.gt(&other.raw)
377    }
378
379    fn ge(&self, other: &Self) -> bool {
380        assert_eq!(self.currency, other.currency);
381        self.raw.ge(&other.raw)
382    }
383}
384
385impl Ord for Money {
386    fn cmp(&self, other: &Self) -> Ordering {
387        assert_eq!(self.currency, other.currency);
388        self.raw.cmp(&other.raw)
389    }
390}
391
392impl Neg for Money {
393    type Output = Self;
394    fn neg(self) -> Self::Output {
395        Self {
396            raw: -self.raw,
397            currency: self.currency,
398        }
399    }
400}
401
402impl Add for Money {
403    type Output = Self;
404    fn add(self, rhs: Self) -> Self::Output {
405        assert_eq!(
406            self.currency, rhs.currency,
407            "Currency mismatch: cannot add {} to {}",
408            rhs.currency.code, self.currency.code
409        );
410        Self {
411            raw: self
412                .raw
413                .checked_add(rhs.raw)
414                .expect("Overflow occurred when adding `Money`"),
415            currency: self.currency,
416        }
417    }
418}
419
420impl Sub for Money {
421    type Output = Self;
422    fn sub(self, rhs: Self) -> Self::Output {
423        assert_eq!(
424            self.currency, rhs.currency,
425            "Currency mismatch: cannot subtract {} from {}",
426            rhs.currency.code, self.currency.code
427        );
428        Self {
429            raw: self
430                .raw
431                .checked_sub(rhs.raw)
432                .expect("Underflow occurred when subtracting `Money`"),
433            currency: self.currency,
434        }
435    }
436}
437
438impl AddAssign for Money {
439    fn add_assign(&mut self, other: Self) {
440        assert_eq!(
441            self.currency, other.currency,
442            "Currency mismatch: cannot add {} to {}",
443            other.currency.code, self.currency.code
444        );
445        self.raw = self
446            .raw
447            .checked_add(other.raw)
448            .expect("Overflow occurred when adding `Money`");
449    }
450}
451
452impl SubAssign for Money {
453    fn sub_assign(&mut self, other: Self) {
454        assert_eq!(
455            self.currency, other.currency,
456            "Currency mismatch: cannot subtract {} from {}",
457            other.currency.code, self.currency.code
458        );
459        self.raw = self
460            .raw
461            .checked_sub(other.raw)
462            .expect("Underflow occurred when subtracting `Money`");
463    }
464}
465
466impl Add<f64> for Money {
467    type Output = f64;
468    fn add(self, rhs: f64) -> Self::Output {
469        self.as_f64() + rhs
470    }
471}
472
473impl Sub<f64> for Money {
474    type Output = f64;
475    fn sub(self, rhs: f64) -> Self::Output {
476        self.as_f64() - rhs
477    }
478}
479
480impl Mul<f64> for Money {
481    type Output = f64;
482    fn mul(self, rhs: f64) -> Self::Output {
483        self.as_f64() * rhs
484    }
485}
486
487impl Div<f64> for Money {
488    type Output = f64;
489    fn div(self, rhs: f64) -> Self::Output {
490        self.as_f64() / rhs
491    }
492}
493
494impl Debug for Money {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        if self.currency.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
497            write!(f, "{}({}, {})", stringify!(Money), self.raw, self.currency)
498        } else {
499            write!(
500                f,
501                "{}({:.*}, {})",
502                stringify!(Money),
503                self.currency.precision as usize,
504                self.as_f64(),
505                self.currency
506            )
507        }
508    }
509}
510
511impl Display for Money {
512    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513        if self.currency.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
514            write!(f, "{} {}", self.raw, self.currency)
515        } else {
516            write!(f, "{} {}", self.as_decimal(), self.currency)
517        }
518    }
519}
520
521impl Serialize for Money {
522    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
523    where
524        S: serde::Serializer,
525    {
526        serializer.serialize_str(&self.to_string())
527    }
528}
529
530impl<'de> Deserialize<'de> for Money {
531    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
532    where
533        D: Deserializer<'de>,
534    {
535        let money_str: String = Deserialize::deserialize(deserializer)?;
536        Ok(Self::from(money_str.as_str()))
537    }
538}
539
540/// Checks if the money `value` is positive.
541///
542/// # Errors
543///
544/// Returns an error if `value` is not positive.
545#[inline(always)]
546pub fn check_positive_money(value: Money, param: &str) -> anyhow::Result<()> {
547    if value.raw <= 0 {
548        anyhow::bail!("invalid `Money` for '{param}' not positive, was {value}");
549    }
550    Ok(())
551}
552
553#[cfg(test)]
554mod tests {
555    use nautilus_core::approx_eq;
556    use rstest::rstest;
557    use rust_decimal_macros::dec;
558
559    use super::*;
560
561    #[rstest]
562    fn test_debug() {
563        let money = Money::new(1010.12, Currency::USD());
564        let result = format!("{money:?}");
565        let expected = "Money(1010.12, USD)";
566        assert_eq!(result, expected);
567    }
568
569    #[rstest]
570    fn test_display() {
571        let money = Money::new(1010.12, Currency::USD());
572        let result = format!("{money}");
573        let expected = "1010.12 USD";
574        assert_eq!(result, expected);
575    }
576
577    #[rstest]
578    #[case(1010.12, 2, "USD", "Money(1010.12, USD)", "1010.12 USD")] // Normal precision
579    #[case(123.456789, 8, "BTC", "Money(123.45678900, BTC)", "123.45678900 BTC")] // At max normal precision
580    fn test_formatting_normal_precision(
581        #[case] value: f64,
582        #[case] precision: u8,
583        #[case] currency_code: &str,
584        #[case] expected_debug: &str,
585        #[case] expected_display: &str,
586    ) {
587        use crate::enums::CurrencyType;
588        let currency = Currency::new(
589            currency_code,
590            precision,
591            0,
592            currency_code,
593            CurrencyType::Fiat,
594        );
595        let money = Money::new(value, currency);
596
597        assert_eq!(format!("{money:?}"), expected_debug);
598        assert_eq!(format!("{money}"), expected_display);
599    }
600
601    #[rstest]
602    #[cfg(feature = "defi")]
603    #[case(
604        1_000_000_000_000_000_000_i128,
605        18,
606        "wei",
607        "Money(1000000000000000000, wei)",
608        "1000000000000000000 wei"
609    )] // High precision
610    #[case(
611        2_500_000_000_000_000_000_i128,
612        18,
613        "ETH",
614        "Money(2500000000000000000, ETH)",
615        "2500000000000000000 ETH"
616    )] // High precision
617    fn test_formatting_high_precision(
618        #[case] raw_value: i128,
619        #[case] precision: u8,
620        #[case] currency_code: &str,
621        #[case] expected_debug: &str,
622        #[case] expected_display: &str,
623    ) {
624        use crate::enums::CurrencyType;
625        let currency = Currency::new(
626            currency_code,
627            precision,
628            0,
629            currency_code,
630            CurrencyType::Crypto,
631        );
632        let money = Money::from_raw(raw_value, currency);
633
634        assert_eq!(format!("{money:?}"), expected_debug);
635        assert_eq!(format!("{money}"), expected_display);
636    }
637
638    #[rstest]
639    fn test_zero_constructor() {
640        let usd = Currency::USD();
641        let money = Money::zero(usd);
642        assert_eq!(money.raw, 0);
643        assert_eq!(money.currency, usd);
644    }
645
646    #[rstest]
647    #[should_panic]
648    fn test_money_different_currency_addition() {
649        let usd = Money::new(1000.0, Currency::USD());
650        let btc = Money::new(1.0, Currency::BTC());
651        let _result = usd + btc; // This should panic since currencies are different
652    }
653
654    #[rstest] // Test does not panic rather than exact value
655    fn test_with_maximum_value() {
656        let money = Money::new_checked(MONEY_MAX, Currency::USD());
657        assert!(money.is_ok());
658    }
659
660    #[rstest] // Test does not panic rather than exact value
661    fn test_with_minimum_value() {
662        let money = Money::new_checked(MONEY_MIN, Currency::USD());
663        assert!(money.is_ok());
664    }
665
666    #[rstest]
667    fn test_money_is_zero() {
668        let zero_usd = Money::new(0.0, Currency::USD());
669        assert!(zero_usd.is_zero());
670        assert_eq!(zero_usd, Money::from("0.0 USD"));
671
672        let non_zero_usd = Money::new(100.0, Currency::USD());
673        assert!(!non_zero_usd.is_zero());
674    }
675
676    #[rstest]
677    fn test_money_comparisons() {
678        let usd = Currency::USD();
679        let m1 = Money::new(100.0, usd);
680        let m2 = Money::new(200.0, usd);
681
682        assert!(m1 < m2);
683        assert!(m2 > m1);
684        assert!(m1 <= m2);
685        assert!(m2 >= m1);
686
687        // Equality
688        let m3 = Money::new(100.0, usd);
689        assert!(m1 == m3);
690    }
691
692    #[rstest]
693    fn test_add() {
694        let a = 1000.0;
695        let b = 500.0;
696        let money1 = Money::new(a, Currency::USD());
697        let money2 = Money::new(b, Currency::USD());
698        let money3 = money1 + money2;
699        assert_eq!(money3.raw, Money::new(a + b, Currency::USD()).raw);
700    }
701
702    #[rstest]
703    fn test_add_assign() {
704        let usd = Currency::USD();
705        let mut money = Money::new(100.0, usd);
706        money += Money::new(50.0, usd);
707        assert!(approx_eq!(f64, money.as_f64(), 150.0, epsilon = 1e-9));
708        assert_eq!(money.currency, usd);
709    }
710
711    #[rstest]
712    fn test_sub() {
713        let usd = Currency::USD();
714        let money1 = Money::new(1000.0, usd);
715        let money2 = Money::new(250.0, usd);
716        let result = money1 - money2;
717        assert!(approx_eq!(f64, result.as_f64(), 750.0, epsilon = 1e-9));
718        assert_eq!(result.currency, usd);
719    }
720
721    #[rstest]
722    fn test_sub_assign() {
723        let usd = Currency::USD();
724        let mut money = Money::new(100.0, usd);
725        money -= Money::new(25.0, usd);
726        assert!(approx_eq!(f64, money.as_f64(), 75.0, epsilon = 1e-9));
727        assert_eq!(money.currency, usd);
728    }
729
730    #[rstest]
731    fn test_money_negation() {
732        let money = Money::new(100.0, Currency::USD());
733        let result = -money;
734        assert_eq!(result, Money::from("-100.0 USD"));
735        assert_eq!(result.currency, Currency::USD().clone());
736    }
737
738    #[rstest]
739    fn test_money_multiplication_by_f64() {
740        let money = Money::new(100.0, Currency::USD());
741        let result = money * 1.5;
742        assert!(approx_eq!(f64, result, 150.0, epsilon = 1e-9));
743    }
744
745    #[rstest]
746    fn test_money_division_by_f64() {
747        let money = Money::new(100.0, Currency::USD());
748        let result = money / 4.0;
749        assert!(approx_eq!(f64, result, 25.0, epsilon = 1e-9));
750    }
751
752    #[rstest]
753    fn test_money_new_usd() {
754        let money = Money::new(1000.0, Currency::USD());
755        assert_eq!(money.currency.code.as_str(), "USD");
756        assert_eq!(money.currency.precision, 2);
757        assert_eq!(money.to_string(), "1000.00 USD");
758        assert_eq!(money.to_formatted_string(), "1_000.00 USD");
759        assert_eq!(money.as_decimal(), dec!(1000.00));
760        assert!(approx_eq!(f64, money.as_f64(), 1000.0, epsilon = 0.001));
761    }
762
763    #[rstest]
764    fn test_money_new_btc() {
765        let money = Money::new(10.3, Currency::BTC());
766        assert_eq!(money.currency.code.as_str(), "BTC");
767        assert_eq!(money.currency.precision, 8);
768        assert_eq!(money.to_string(), "10.30000000 BTC");
769        assert_eq!(money.to_formatted_string(), "10.30000000 BTC");
770    }
771
772    #[rstest]
773    #[case("0USD")] // <-- No whitespace separator
774    #[case("0x00 USD")] // <-- Invalid float
775    #[case("0 US")] // <-- Invalid currency
776    #[case("0 USD USD")] // <-- Too many parts
777    #[should_panic]
778    fn test_from_str_invalid_input(#[case] input: &str) {
779        let _ = Money::from(input);
780    }
781
782    #[rstest]
783    #[case("0 USD", Currency::USD(), dec!(0.00))]
784    #[case("1.1 AUD", Currency::AUD(), dec!(1.10))]
785    #[case("1.12345678 BTC", Currency::BTC(), dec!(1.12345678))]
786    #[case("10_000.10 USD", Currency::USD(), dec!(10000.10))]
787    fn test_from_str_valid_input(
788        #[case] input: &str,
789        #[case] expected_currency: Currency,
790        #[case] expected_dec: Decimal,
791    ) {
792        let money = Money::from(input);
793        assert_eq!(money.currency, expected_currency);
794        assert_eq!(money.as_decimal(), expected_dec);
795    }
796
797    #[rstest]
798    fn test_money_from_str_negative() {
799        let money = Money::from("-123.45 USD");
800        assert!(approx_eq!(f64, money.as_f64(), -123.45, epsilon = 1e-9));
801        assert_eq!(money.currency, Currency::USD());
802    }
803
804    #[rstest]
805    #[case("1e7 USD", 10_000_000.0)]
806    #[case("2.5e3 EUR", 2_500.0)]
807    #[case("1.234e-2 GBP", 0.01)] // GBP has 2 decimal places, so 0.01234 becomes 0.01
808    #[case("5E-3 JPY", 0.0)] // JPY has 0 decimal places, so 0.005 becomes 0
809    fn test_from_str_scientific_notation(#[case] input: &str, #[case] expected_value: f64) {
810        let money = Money::from_str(input).unwrap();
811        assert!(approx_eq!(
812            f64,
813            money.as_f64(),
814            expected_value,
815            epsilon = 1e-10
816        ));
817    }
818
819    #[rstest]
820    #[case("1_234.56 USD", 1234.56)]
821    #[case("1_000_000 EUR", 1_000_000.0)]
822    #[case("99_999.99 GBP", 99_999.99)]
823    fn test_from_str_with_underscores(#[case] input: &str, #[case] expected_value: f64) {
824        let money = Money::from_str(input).unwrap();
825        assert!(approx_eq!(
826            f64,
827            money.as_f64(),
828            expected_value,
829            epsilon = 1e-10
830        ));
831    }
832
833    #[rstest]
834    fn test_from_decimal_precision_preservation() {
835        use rust_decimal::Decimal;
836
837        let decimal = Decimal::from_str("123.45").unwrap();
838        let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
839        assert_eq!(money.currency.precision, 2);
840        assert!(approx_eq!(f64, money.as_f64(), 123.45, epsilon = 1e-10));
841
842        // Verify raw value is exact for USD (2 decimal places)
843        let expected_raw = 12345 * 10_i64.pow((FIXED_PRECISION - 2) as u32);
844        assert_eq!(money.raw, expected_raw as MoneyRaw);
845    }
846
847    #[rstest]
848    fn test_from_decimal_rounding() {
849        use rust_decimal::Decimal;
850
851        // Test banker's rounding with USD (2 decimal places)
852        let decimal = Decimal::from_str("1.005").unwrap();
853        let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
854        assert_eq!(money.as_f64(), 1.0); // 1.005 rounds to 1.00 (even)
855
856        let decimal = Decimal::from_str("1.015").unwrap();
857        let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
858        assert_eq!(money.as_f64(), 1.02); // 1.015 rounds to 1.02 (even)
859    }
860
861    #[rstest]
862    fn test_money_hash() {
863        use std::{
864            collections::hash_map::DefaultHasher,
865            hash::{Hash, Hasher},
866        };
867
868        let m1 = Money::new(100.0, Currency::USD());
869        let m2 = Money::new(100.0, Currency::USD());
870        let m3 = Money::new(100.0, Currency::AUD());
871
872        let mut s1 = DefaultHasher::new();
873        let mut s2 = DefaultHasher::new();
874        let mut s3 = DefaultHasher::new();
875
876        m1.hash(&mut s1);
877        m2.hash(&mut s2);
878        m3.hash(&mut s3);
879
880        assert_eq!(
881            s1.finish(),
882            s2.finish(),
883            "Same amount + same currency => same hash"
884        );
885        assert_ne!(
886            s1.finish(),
887            s3.finish(),
888            "Same amount + different currency => different hash"
889        );
890    }
891
892    #[rstest]
893    fn test_money_serialization_deserialization() {
894        let money = Money::new(123.45, Currency::USD());
895        let serialized = serde_json::to_string(&money);
896        let deserialized: Money = serde_json::from_str(&serialized.unwrap()).unwrap();
897        assert_eq!(money, deserialized);
898    }
899
900    #[rstest]
901    #[should_panic(expected = "`raw` value")]
902    fn test_money_from_raw_out_of_range_panics() {
903        let usd = Currency::USD();
904        let raw = MONEY_RAW_MAX.saturating_add(1);
905        let _ = Money::from_raw(raw, usd);
906    }
907
908    ////////////////////////////////////////////////////////////////////////////////
909    // Property-based testing
910    ////////////////////////////////////////////////////////////////////////////////
911
912    use proptest::prelude::*;
913
914    fn currency_strategy() -> impl Strategy<Value = Currency> {
915        prop_oneof![
916            Just(Currency::USD()),
917            Just(Currency::EUR()),
918            Just(Currency::GBP()),
919            Just(Currency::JPY()),
920            Just(Currency::AUD()),
921            Just(Currency::CAD()),
922            Just(Currency::CHF()),
923            Just(Currency::BTC()),
924            Just(Currency::ETH()),
925            Just(Currency::USDT()),
926        ]
927    }
928
929    fn money_amount_strategy() -> impl Strategy<Value = f64> {
930        // Generate amounts within valid range, avoiding edge cases that might cause precision issues
931        prop_oneof![
932            // Small amounts
933            -1000.0..1000.0,
934            // Medium amounts
935            -100_000.0..100_000.0,
936            // Large amounts within safe range (avoid max values that could overflow when added)
937            -1_000_000.0..1_000_000.0,
938            // Edge cases
939            Just(0.0),
940            // Use smaller values than MONEY_MAX to avoid overflow in arithmetic operations
941            Just(MONEY_MIN / 2.0),
942            Just(MONEY_MAX / 2.0),
943            Just(MONEY_MIN + 1.0),
944            Just(MONEY_MAX - 1.0),
945            Just(MONEY_MIN),
946            Just(MONEY_MAX),
947        ]
948    }
949
950    fn money_strategy() -> impl Strategy<Value = Money> {
951        (money_amount_strategy(), currency_strategy())
952            .prop_filter_map("constructible money", |(amount, currency)| {
953                Money::new_checked(amount, currency).ok()
954            })
955    }
956
957    proptest! {
958        #[rstest]
959        fn prop_money_construction_roundtrip(
960            amount in money_amount_strategy(),
961            currency in currency_strategy()
962        ) {
963            // Test that valid amounts can be constructed and round-trip through f64
964            if let Ok(money) = Money::new_checked(amount, currency) {
965                let roundtrip = money.as_f64();
966                // Allow for precision loss based on currency precision and magnitude
967                let precision_epsilon = if currency.precision == 0 {
968                    1.0 // For JPY and other zero-precision currencies, allow rounding to nearest integer
969                } else {
970                    let currency_epsilon = 10.0_f64.powi(-(currency.precision as i32));
971                    let magnitude_epsilon = amount.abs() * 1e-10; // Allow relative error for large numbers
972                    currency_epsilon.max(magnitude_epsilon)
973                };
974                prop_assert!((roundtrip - amount).abs() <= precision_epsilon,
975                    "Roundtrip failed: {} -> {} -> {} (precision: {}, epsilon: {})",
976                    amount, money.raw, roundtrip, currency.precision, precision_epsilon);
977                prop_assert_eq!(money.currency, currency);
978            }
979        }
980
981        #[rstest]
982        fn prop_money_addition_commutative(
983            money1 in money_strategy(),
984            money2 in money_strategy(),
985        ) {
986            // Addition should be commutative for same currency
987            if money1.currency == money2.currency {
988                // Check if addition would overflow before performing it
989                if let (Some(_), Some(_)) = (
990                    money1.raw.checked_add(money2.raw),
991                    money2.raw.checked_add(money1.raw)
992                ) {
993                    let sum1 = money1 + money2;
994                    let sum2 = money2 + money1;
995                    prop_assert_eq!(sum1, sum2, "Addition should be commutative");
996                    prop_assert_eq!(sum1.currency, money1.currency);
997                }
998                // If overflow would occur, skip this test case - it's expected
999            }
1000        }
1001
1002        #[rstest]
1003        fn prop_money_addition_associative(
1004            money1 in money_strategy(),
1005            money2 in money_strategy(),
1006            money3 in money_strategy(),
1007        ) {
1008            // Addition should be associative for same currency
1009            if money1.currency == money2.currency && money2.currency == money3.currency {
1010                // Test (a + b) + c == a + (b + c)
1011                // Use checked arithmetic to avoid overflow in property tests
1012                if let (Some(sum1), Some(sum2)) = (
1013                    money1.raw.checked_add(money2.raw),
1014                    money2.raw.checked_add(money3.raw)
1015                )
1016                    && let (Some(left), Some(right)) = (
1017                        sum1.checked_add(money3.raw),
1018                        money1.raw.checked_add(sum2)
1019                    ) {
1020                        // Check if results are within bounds before constructing Money
1021                        if (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&left)
1022                            && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&right)
1023                        {
1024                            let left_result = Money::from_raw(left, money1.currency);
1025                            let right_result = Money::from_raw(right, money1.currency);
1026                            prop_assert_eq!(left_result, right_result, "Addition should be associative");
1027                        }
1028                    }
1029            }
1030        }
1031
1032        #[rstest]
1033        fn prop_money_subtraction_inverse(
1034            money1 in money_strategy(),
1035            money2 in money_strategy(),
1036        ) {
1037            // Subtraction should be the inverse of addition for same currency
1038            if money1.currency == money2.currency {
1039                // Test (a + b) - b == a, avoiding overflow
1040                if let Some(sum_raw) = money1.raw.checked_add(money2.raw)
1041                    && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&sum_raw) {
1042                        let sum = Money::from_raw(sum_raw, money1.currency);
1043                        let diff = sum - money2;
1044                        prop_assert_eq!(diff, money1, "Subtraction should be inverse of addition");
1045                    }
1046            }
1047        }
1048
1049        #[rstest]
1050        fn prop_money_zero_identity(money in money_strategy()) {
1051            // Zero should be additive identity
1052            let zero = Money::zero(money.currency);
1053            prop_assert_eq!(money + zero, money, "Zero should be additive identity");
1054            prop_assert_eq!(zero + money, money, "Zero should be additive identity (commutative)");
1055            prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
1056        }
1057
1058        #[rstest]
1059        fn prop_money_negation_inverse(money in money_strategy()) {
1060            // Negation should be its own inverse
1061            let negated = -money;
1062            let double_neg = -negated;
1063            prop_assert_eq!(money, double_neg, "Double negation should equal original");
1064            prop_assert_eq!(negated.currency, money.currency, "Negation preserves currency");
1065
1066            // Test additive inverse property (if no overflow)
1067            if let Some(sum_raw) = money.raw.checked_add(negated.raw)
1068                && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&sum_raw) {
1069                    let sum = Money::from_raw(sum_raw, money.currency);
1070                    prop_assert!(sum.is_zero(), "Money + (-Money) should equal zero");
1071                }
1072        }
1073
1074        #[rstest]
1075        fn prop_money_comparison_consistency(
1076            money1 in money_strategy(),
1077            money2 in money_strategy(),
1078        ) {
1079            // Comparison operations should be consistent for same currency
1080            if money1.currency == money2.currency {
1081                let eq = money1 == money2;
1082                let lt = money1 < money2;
1083                let gt = money1 > money2;
1084                let le = money1 <= money2;
1085                let ge = money1 >= money2;
1086
1087                // Exactly one of eq, lt, gt should be true
1088                let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
1089                prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
1090
1091                // Consistency checks
1092                prop_assert_eq!(le, eq || lt, "<= should equal == || <");
1093                prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
1094                prop_assert_eq!(lt, money2 > money1, "< should be symmetric with >");
1095                prop_assert_eq!(le, money2 >= money1, "<= should be symmetric with >=");
1096            }
1097        }
1098
1099        #[rstest]
1100        fn prop_money_string_roundtrip(money in money_strategy()) {
1101            // String serialization should round-trip correctly
1102            let string_repr = money.to_string();
1103            let parsed = Money::from_str(&string_repr);
1104            prop_assert!(parsed.is_ok(), "String parsing should succeed for valid money");
1105            if let Ok(parsed_money) = parsed {
1106                prop_assert_eq!(parsed_money.currency, money.currency, "Currency should round-trip");
1107                // Allow for small precision differences due to string formatting
1108                let diff = (parsed_money.as_f64() - money.as_f64()).abs();
1109                prop_assert!(diff < 0.01, "Amount should round-trip within precision: {} vs {}",
1110                    money.as_f64(), parsed_money.as_f64());
1111            }
1112        }
1113
1114        #[rstest]
1115        fn prop_money_decimal_conversion(money in money_strategy()) {
1116            // Decimal conversion should preserve value within precision limits
1117            let decimal = money.as_decimal();
1118
1119            #[cfg(feature = "defi")]
1120            {
1121                // In DeFi mode, as_f64() is unreliable for high-precision values
1122                // Just ensure decimal conversion doesn't panic and produces reasonable values
1123                let decimal_f64: f64 = decimal.try_into().unwrap_or(0.0);
1124                prop_assert!(decimal_f64.is_finite(), "Decimal should convert to finite f64");
1125
1126                // For DeFi mode, we mainly care that decimal conversion preserves the currency precision
1127                prop_assert_eq!(decimal.scale(), u32::from(money.currency.precision));
1128            }
1129            #[cfg(not(feature = "defi"))]
1130            {
1131                let decimal_f64: f64 = decimal.try_into().unwrap_or(0.0);
1132                let original_f64 = money.as_f64();
1133
1134                // Allow for precision differences based on currency precision and high-precision mode
1135                let base_epsilon = 10.0_f64.powi(-(money.currency.precision as i32));
1136                let precision_epsilon = if cfg!(feature = "high-precision") {
1137                    // More tolerant epsilon for high-precision modes due to f64 limitations
1138                    base_epsilon.max(1e-10)
1139                } else {
1140                    base_epsilon
1141                };
1142                let diff = (decimal_f64 - original_f64).abs();
1143                prop_assert!(diff <= precision_epsilon,
1144                    "Decimal conversion should preserve value within currency precision: {} vs {} (diff: {}, epsilon: {})",
1145                    original_f64, decimal_f64, diff, precision_epsilon);
1146            }
1147        }
1148
1149        #[rstest]
1150        fn prop_money_arithmetic_with_f64(
1151            money in money_strategy(),
1152            factor in -1000.0..1000.0_f64,
1153        ) {
1154            // Arithmetic with f64 should produce reasonable results
1155            if factor != 0.0 {
1156                let original_f64 = money.as_f64();
1157
1158                let mul_result = money * factor;
1159                let expected_mul = original_f64 * factor;
1160                prop_assert!((mul_result - expected_mul).abs() < 0.01,
1161                    "Multiplication with f64 should be accurate");
1162
1163                let div_result = money / factor;
1164                let expected_div = original_f64 / factor;
1165                if expected_div.is_finite() {
1166                    prop_assert!((div_result - expected_div).abs() < 0.01,
1167                        "Division with f64 should be accurate");
1168                }
1169
1170                let add_result = money + factor;
1171                let expected_add = original_f64 + factor;
1172                prop_assert!((add_result - expected_add).abs() < 0.01,
1173                    "Addition with f64 should be accurate");
1174
1175                let sub_result = money - factor;
1176                let expected_sub = original_f64 - factor;
1177                prop_assert!((sub_result - expected_sub).abs() < 0.01,
1178                    "Subtraction with f64 should be accurate");
1179            }
1180        }
1181    }
1182
1183    #[rstest]
1184    #[case(42.0, true, "positive value")]
1185    #[case(0.0, false, "zero value")]
1186    #[case( -13.5,  false, "negative value")]
1187    fn test_check_positive_money(
1188        #[case] amount: f64,
1189        #[case] should_succeed: bool,
1190        #[case] _case_name: &str,
1191    ) {
1192        let money = Money::new(amount, Currency::USD());
1193
1194        let res = check_positive_money(money, "money");
1195
1196        match should_succeed {
1197            true => assert!(res.is_ok(), "expected Ok(..) for {amount}"),
1198            false => {
1199                assert!(res.is_err(), "expected Err(..) for {amount}");
1200                let msg = res.unwrap_err().to_string();
1201                assert!(
1202                    msg.contains("not positive"),
1203                    "error message should mention positivity; got: {msg:?}"
1204                );
1205            }
1206        }
1207    }
1208}