Skip to main content

nautilus_model/types/
fixed.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//! Functions for handling fixed-point arithmetic.
17//!
18//! This module provides constants and functions that enforce a fixed-point precision strategy,
19//! ensuring consistent precision and scaling across various types and calculations.
20//!
21//! # Raw Value Requirements
22//!
23//! When constructing value types like [`Price`] or [`Quantity`] using `from_raw`, the raw value
24//! **must** be a valid multiple of the scale factor for the given precision. Valid raw values
25//! should ideally come from:
26//!
27//! - Calling the `raw()` accessor of an existing value (e.g., `price.raw()`)
28//! - Using the fixed-point conversion functions in this module
29//! - Values from Nautilus-produced Arrow data
30//!
31//! Raw values that are not valid multiples will cause a panic on construction in debug builds,
32//! and may result in incorrect values in release builds.
33//!
34//! # Legacy Catalog Data and Floating-Point Errors
35//!
36//! Data written to catalogs using V2 wranglers before 16th December 2025 may contain raw values with
37//! floating-point precision errors. This occurred because the wranglers used:
38//!
39//! ```text
40//! int(value * FIXED_SCALAR)  # Introduces floating-point errors
41//! ```
42//!
43//! instead of the correct precision-aware approach:
44//!
45//! ```text
46//! round(value * 10^precision) * scale  # Correct
47//! ```
48//!
49//! # Raw Value Correction
50//!
51//! To handle legacy data with floating-point errors, the Arrow decode path uses correction
52//! functions ([`correct_raw_i64`], [`correct_raw_i128`], etc.) to round raw values to the
53//! nearest valid multiple. This ensures backward compatibility with existing catalogs.
54//!
55//! **Note:** This correction adds a small amount of overhead during decoding. In a future
56//! version, once catalogs have been repaired or migrated, this correction will become opt-in.
57//!
58//! [`Price`]: crate::types::Price
59//! [`Quantity`]: crate::types::Quantity
60
61use std::{cmp::Ordering, fmt::Display};
62
63use nautilus_core::correctness::{
64    CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
65};
66use rust_decimal::Decimal;
67
68use crate::types::{price::PriceRaw, quantity::QuantityRaw};
69
70/// Indicates if high-precision mode is enabled.
71///
72/// # Safety
73///
74/// This static variable is initialized at compile time and never mutated,
75/// making it safe to read from multiple threads without synchronization.
76/// The value is determined by the "high-precision" feature flag.
77#[unsafe(no_mangle)]
78#[allow(unsafe_code)]
79pub static HIGH_PRECISION_MODE: u8 = cfg!(feature = "high-precision") as u8;
80
81// -----------------------------------------------------------------------------
82// FIXED_PRECISION
83// -----------------------------------------------------------------------------
84
85#[cfg(feature = "high-precision")]
86/// The maximum fixed-point precision.
87pub const FIXED_PRECISION: u8 = 16;
88
89/// The maximum fixed-point precision used by standard-precision catalog data.
90pub const FIXED_PRECISION_STANDARD: u8 = 9;
91
92#[cfg(not(feature = "high-precision"))]
93/// The maximum fixed-point precision.
94pub const FIXED_PRECISION: u8 = FIXED_PRECISION_STANDARD;
95
96// -----------------------------------------------------------------------------
97// PRECISION_BYTES (size of integer backing the fixed-point values)
98// -----------------------------------------------------------------------------
99
100#[cfg(feature = "high-precision")]
101/// The width in bytes for fixed-point value types in high-precision mode (128-bit).
102pub const PRECISION_BYTES: i32 = 16;
103
104#[cfg(not(feature = "high-precision"))]
105/// The width in bytes for fixed-point value types in standard-precision mode (64-bit).
106pub const PRECISION_BYTES: i32 = 8;
107
108/// The Arrow data type name for fixed-point value types.
109pub const FIXED_DECIMAL: &str = "Decimal128(38, 16)";
110
111// -----------------------------------------------------------------------------
112// FIXED_SCALAR
113// -----------------------------------------------------------------------------
114
115#[cfg(feature = "high-precision")]
116pub(crate) const FIXED_SCALAR_RAW: QuantityRaw = 10_000_000_000_000_000;
117
118#[cfg(not(feature = "high-precision"))]
119pub(crate) const FIXED_SCALAR_RAW: QuantityRaw = 1_000_000_000;
120
121#[cfg(feature = "high-precision")]
122/// The scalar value corresponding to the maximum precision (10^16).
123pub const FIXED_SCALAR: f64 = 10_000_000_000_000_000.0;
124
125#[cfg(not(feature = "high-precision"))]
126/// The scalar value corresponding to the maximum precision (10^9).
127pub const FIXED_SCALAR: f64 = 1_000_000_000.0;
128
129// -----------------------------------------------------------------------------
130// PRECISION_DIFF_SCALAR
131// -----------------------------------------------------------------------------
132
133#[cfg(feature = "high-precision")]
134/// The scalar representing the difference between high-precision and standard-precision modes.
135pub const PRECISION_DIFF_SCALAR: f64 = 10_000_000.0; // 10^(16-9)
136
137#[cfg(not(feature = "high-precision"))]
138/// The scalar representing the difference between high-precision and standard-precision modes.
139pub const PRECISION_DIFF_SCALAR: f64 = 1.0;
140
141// -----------------------------------------------------------------------------
142// POWERS_OF_10 (lookup table for fast validation)
143// -----------------------------------------------------------------------------
144
145/// Precomputed powers of 10 for fast scale lookup.
146///
147/// Index i contains 10^i. Table covers 10^0 through 10^16 (sufficient for `FIXED_PRECISION`).
148/// Used by `check_fixed_raw_*` functions to avoid runtime exponentiation.
149const POWERS_OF_10: [u64; 17] = [
150    1,                      // 10^0
151    10,                     // 10^1
152    100,                    // 10^2
153    1_000,                  // 10^3
154    10_000,                 // 10^4
155    100_000,                // 10^5
156    1_000_000,              // 10^6
157    10_000_000,             // 10^7
158    100_000_000,            // 10^8
159    1_000_000_000,          // 10^9
160    10_000_000_000,         // 10^10
161    100_000_000_000,        // 10^11
162    1_000_000_000_000,      // 10^12
163    10_000_000_000_000,     // 10^13
164    100_000_000_000_000,    // 10^14
165    1_000_000_000_000_000,  // 10^15
166    10_000_000_000_000_000, // 10^16
167];
168
169// Compile-time verification that FIXED_PRECISION is within table bounds.
170// We index POWERS_OF_10[FIXED_PRECISION] when precision=0, so need strict `<`.
171const _: () = assert!(
172    (FIXED_PRECISION as usize) < POWERS_OF_10.len(),
173    "FIXED_PRECISION exceeds POWERS_OF_10 table size"
174);
175
176// -----------------------------------------------------------------------------
177
178/// The maximum precision that can be safely used with f64-based constructors.
179///
180/// This is a hard limit imposed by IEEE 754 double-precision floating-point representation,
181/// which has approximately 15-17 significant decimal digits. Beyond 16 decimal places,
182/// floating-point arithmetic becomes unreliable due to rounding errors.
183///
184/// For higher precision values (such as 18-decimal wei values in DeFi), specialized
185/// constructors that work with integer representations should be used instead.
186pub const MAX_FLOAT_PRECISION: u8 = 16;
187
188/// Checks if a given `precision` value is within the allowed fixed-point precision range.
189///
190/// # Errors
191///
192/// Returns an error if:
193/// - The `defi` feature is enabled and `precision` exceeds `WEI_PRECISION` (18).
194/// - The `defi` feature is disabled and `precision` exceeds [`FIXED_PRECISION`].
195pub fn check_fixed_precision(precision: u8) -> CorrectnessResult<()> {
196    #[cfg(feature = "defi")]
197    if precision > crate::defi::WEI_PRECISION {
198        return Err(CorrectnessError::PredicateViolation {
199            message: format!("`precision` exceeded maximum `WEI_PRECISION` (18), was {precision}"),
200        });
201    }
202
203    #[cfg(not(feature = "defi"))]
204    if precision > FIXED_PRECISION {
205        return Err(CorrectnessError::PredicateViolation {
206            message: format!(
207                "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {precision}"
208            ),
209        });
210    }
211
212    Ok(())
213}
214
215/// Checks that a value with the given `precision` can be converted to `f64`.
216///
217/// # Errors
218///
219/// Returns an error if `precision` exceeds [`MAX_FLOAT_PRECISION`].
220pub fn check_float_precision(precision: u8) -> CorrectnessResult<()> {
221    if precision > MAX_FLOAT_PRECISION {
222        return Err(CorrectnessError::PredicateViolation {
223            message: format!(
224                "Fixed-point precision {precision} exceeds maximum float precision {MAX_FLOAT_PRECISION}"
225            ),
226        });
227    }
228
229    Ok(())
230}
231
232/// Returns `true` when two precisions encode their `raw` values at the same scale.
233///
234/// The effective scale for a given precision is `max(precision, FIXED_PRECISION)`:
235/// - Standard precisions (`<= FIXED_PRECISION`) all store raw at `FIXED_SCALAR` scale.
236/// - Defi precisions (`> FIXED_PRECISION`, e.g. 17 or 18) each store raw at their own
237///   native `10^precision` scale via constructors like `Price::from_wei` /
238///   `Quantity::from_u256`.
239///
240/// Two precisions match iff their effective scales are identical. Mixing different
241/// scales in raw arithmetic produces wrong results.
242#[inline]
243#[must_use]
244pub fn raw_scales_match(a: u8, b: u8) -> bool {
245    a == b || a.max(b) <= FIXED_PRECISION
246}
247
248/// Returns the effective integer scale for a raw fixed-point value.
249#[inline]
250#[must_use]
251pub(crate) fn raw_scale(precision: u8) -> u128 {
252    10_u128.pow(u32::from(precision.max(FIXED_PRECISION)))
253}
254
255// Removing only native-scale trailing zeros gives equal values identical hash inputs
256#[must_use]
257pub(crate) fn canonical_raw(raw: impl Into<u128>, precision: u8) -> (u128, u8) {
258    let mut raw = raw.into();
259    let mut precision = if raw == 0 {
260        FIXED_PRECISION
261    } else {
262        precision.max(FIXED_PRECISION)
263    };
264
265    while precision > FIXED_PRECISION && raw % 10 == 0 {
266        raw /= 10;
267        precision -= 1;
268    }
269
270    (raw, precision)
271}
272
273#[inline]
274#[must_use]
275pub(crate) fn compare_raw_signed(
276    lhs: PriceRaw,
277    lhs_precision: u8,
278    rhs: PriceRaw,
279    rhs_precision: u8,
280) -> Ordering {
281    if raw_scales_match(lhs_precision, rhs_precision) {
282        return lhs.cmp(&rhs);
283    }
284
285    lhs.signum().cmp(&rhs.signum()).then_with(|| {
286        let ordering = compare_raw(
287            lhs.unsigned_abs(),
288            lhs_precision,
289            rhs.unsigned_abs(),
290            rhs_precision,
291        );
292
293        if lhs < 0 {
294            ordering.reverse()
295        } else {
296            ordering
297        }
298    })
299}
300
301#[inline]
302#[must_use]
303pub(crate) fn compare_raw(
304    lhs: impl Into<u128>,
305    lhs_precision: u8,
306    rhs: impl Into<u128>,
307    rhs_precision: u8,
308) -> Ordering {
309    let lhs = lhs.into();
310    let rhs = rhs.into();
311
312    // The zero-valued ERROR_PRICE sentinel has precision 255, which is not a numeric scale
313    if (lhs == 0 && rhs == 0) || raw_scales_match(lhs_precision, rhs_precision) {
314        return lhs.cmp(&rhs);
315    }
316
317    let lhs_scale = raw_scale(lhs_precision);
318    let rhs_scale = raw_scale(rhs_precision);
319    let scale = lhs_scale.max(rhs_scale);
320
321    // Compare whole parts first so aligning fractional parts cannot overflow
322    (lhs / lhs_scale).cmp(&(rhs / rhs_scale)).then_with(|| {
323        let lhs_fraction = (lhs % lhs_scale) * (scale / lhs_scale);
324        let rhs_fraction = (rhs % rhs_scale) * (scale / rhs_scale);
325        lhs_fraction.cmp(&rhs_fraction)
326    })
327}
328
329/// Converts a raw value already rescaled to `10^precision` into a `Decimal`.
330///
331/// `Decimal` stores a 96-bit mantissa, so `Decimal::from_i128_with_scale` panics once the raw
332/// value exceeds `79_228_162_514_264_337_593_543_950_335`. Valid values reach that: under
333/// `high-precision` a precision-16 amount does so above roughly 7.92e12, and a `defi`
334/// precision-18 amount above roughly 7.92e10. Those values fall back to adding the whole and
335/// fractional parts, which keeps both operands small enough that `Decimal` drops scale rather
336/// than panicking. Every value the direct conversion accepts keeps its exact value and scale.
337#[must_use]
338pub(crate) fn scaled_raw_to_decimal(scaled_raw: i128, precision: u8) -> Decimal {
339    let scale = u32::from(precision);
340
341    Decimal::try_from_i128_with_scale(scaled_raw, scale).unwrap_or_else(|_| {
342        let divisor = 10_i128.pow(scale);
343
344        Decimal::from(scaled_raw / divisor)
345            + Decimal::from_i128_with_scale(scaled_raw % divisor, scale)
346    })
347}
348
349pub(crate) fn format_scaled_i128(raw: i128, precision: u8) -> String {
350    let sign = if raw < 0 { "-" } else { "" };
351    format!(
352        "{sign}{}",
353        format_scaled_u128(raw.unsigned_abs(), precision)
354    )
355}
356
357/// Parses a plain decimal string into its signed mantissa and fractional precision.
358pub(crate) fn parse_decimal_mantissa(value: &str) -> Result<(i128, u8), String> {
359    let (negative, unsigned) = value
360        .strip_prefix('-')
361        .map_or((false, value), |value| (true, value));
362    let unsigned = if negative {
363        unsigned
364    } else {
365        unsigned.strip_prefix('+').unwrap_or(unsigned)
366    };
367    let (whole, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
368    if fraction.contains('.') {
369        return Err(format!("Invalid decimal value '{value}'"));
370    }
371    let digits = format!("{whole}{fraction}");
372    if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
373        return Err(format!("Invalid decimal value '{value}'"));
374    }
375    let precision = u8::try_from(fraction.len())
376        .map_err(|_| format!("Decimal value '{value}' has too many fractional digits"))?;
377    let mut mantissa = 0_i128;
378    for digit in digits.bytes().map(|byte| i128::from(byte - b'0')) {
379        mantissa = if negative {
380            mantissa
381                .checked_mul(10)
382                .and_then(|value| value.checked_sub(digit))
383        } else {
384            mantissa
385                .checked_mul(10)
386                .and_then(|value| value.checked_add(digit))
387        }
388        .ok_or_else(|| format!("Decimal value '{value}' exceeds i128 range"))?;
389    }
390    Ok((mantissa, precision))
391}
392
393pub(crate) fn format_scaled_u128(raw: u128, precision: u8) -> String {
394    if precision == 0 {
395        return raw.to_string();
396    }
397
398    let scale = 10_u128.pow(u32::from(precision));
399    format!(
400        "{}.{:0>width$}",
401        raw / scale,
402        raw % scale,
403        width = usize::from(precision),
404    )
405}
406
407/// Returns `lhs * rhs / FIXED_SCALAR`, truncated toward zero.
408///
409/// Returns `None` only when the scaled result exceeds [`QuantityRaw::MAX`].
410#[must_use]
411pub(crate) fn checked_mul_div_fixed(lhs: QuantityRaw, rhs: QuantityRaw) -> Option<QuantityRaw> {
412    checked_mul_div_raw(lhs, rhs, FIXED_SCALAR_RAW)
413}
414
415// Splitting both operands avoids intermediate overflow, the remainder product fits
416// QuantityRaw for every supported fixed-point scale (up to 10^18 with defi).
417#[must_use]
418pub(crate) fn checked_mul_div_raw(
419    lhs: QuantityRaw,
420    rhs: QuantityRaw,
421    scalar: QuantityRaw,
422) -> Option<QuantityRaw> {
423    let lhs_whole = lhs / scalar;
424    let lhs_remainder = lhs % scalar;
425    let rhs_whole = rhs / scalar;
426    let rhs_remainder = rhs % scalar;
427
428    lhs_whole
429        .checked_mul(rhs)
430        .and_then(|whole| {
431            lhs_remainder
432                .checked_mul(rhs_whole)
433                .and_then(|mixed| whole.checked_add(mixed))
434        })
435        .and_then(|whole_and_mixed| {
436            lhs_remainder
437                .checked_mul(rhs_remainder)
438                .map(|fractional| fractional / scalar)
439                .and_then(|fractional| whole_and_mixed.checked_add(fractional))
440        })
441}
442
443const _: () = {
444    assert!(FIXED_SCALAR_RAW > 0);
445    assert!((FIXED_SCALAR_RAW as f64).to_bits() == FIXED_SCALAR.to_bits());
446    let max_remainder = FIXED_SCALAR_RAW - 1;
447    assert!(max_remainder.checked_mul(max_remainder).is_some());
448};
449
450// -----------------------------------------------------------------------------
451// Raw value validation
452// -----------------------------------------------------------------------------
453
454/// Returns `true` if validation should be skipped, `false` to proceed.
455///
456/// Validation is skipped when precision >= `FIXED_PRECISION` because every bit of the raw
457/// value is significant. For precision > `FIXED_PRECISION` without the defi feature,
458/// a debug assertion fires to surface potential misuse during development.
459#[inline(always)]
460fn should_skip_validation(precision: u8) -> bool {
461    #[cfg(not(feature = "defi"))]
462    debug_assert!(
463        precision <= FIXED_PRECISION,
464        "precision {precision} exceeds FIXED_PRECISION {FIXED_PRECISION}: \
465         raw value validation is not possible at this precision"
466    );
467
468    precision >= FIXED_PRECISION
469}
470
471/// Builds the error for invalid fixed-point raw values (cold path).
472#[cold]
473fn invalid_raw_error(
474    raw: impl Display,
475    precision: u8,
476    remainder: impl Display,
477    scale: impl Display,
478) -> anyhow::Error {
479    anyhow::anyhow!(
480        "Invalid fixed-point raw value {raw} for precision {precision}: \
481         remainder {remainder} when divided by scale {scale}. \
482         Raw value should be a multiple of {scale}. \
483         This indicates data corruption or incorrect precision/scaling upstream"
484    )
485}
486
487/// Checks that a raw unsigned fixed-point value has no spurious bits beyond the precision scale.
488///
489/// For a given precision P where P < `FIXED_PRECISION`, valid raw values must be exact
490/// multiples of `10^(FIXED_PRECISION` - P). Any non-zero remainder indicates data corruption
491/// or incorrect scaling upstream.
492///
493/// # Precision Limits
494///
495/// This check only validates when `precision < FIXED_PRECISION`:
496/// - When `precision == FIXED_PRECISION`, every bit of the raw value is significant and
497///   the check passes trivially (no "extra" bits to validate).
498/// - When `precision > FIXED_PRECISION` (possible with defi feature allowing up to 18dp),
499///   validation is not possible because the requested precision exceeds our internal
500///   representation. A debug assertion will fire to surface this during development.
501///
502/// **Important**: For defi 18dp values, this check provides NO protection against incorrectly scaled
503/// raw values. The inherent limitation is that we cannot detect if a 16dp raw is incorrectly
504/// labeled as 18dp, since both would appear valid at full internal precision.
505///
506/// # Example
507///
508/// With `FIXED_PRECISION=9` and precision=0:
509/// - Valid: `raw=120_000_000_000` (120 * 10^9, divisible by 10^9)
510/// - Invalid: `raw=119_582_001_968_421_736` (remainder `968_421_736` when divided by 10^9)
511///
512/// # Errors
513///
514/// Returns an error if the raw value has non-zero bits beyond the precision scale
515/// (only when `precision < FIXED_PRECISION`).
516#[inline(always)]
517pub fn check_fixed_raw_u128(raw: u128, precision: u8) -> anyhow::Result<()> {
518    if should_skip_validation(precision) {
519        return Ok(());
520    }
521
522    let exp = usize::from(FIXED_PRECISION - precision);
523    let scale = u128::from(POWERS_OF_10[exp]);
524    let remainder = raw % scale;
525
526    if remainder != 0 {
527        return Err(invalid_raw_error(raw, precision, remainder, scale));
528    }
529
530    Ok(())
531}
532
533/// Checks that a raw unsigned fixed-point value (64-bit) has no spurious bits.
534///
535/// Uses direct u64 arithmetic for better performance than widening to u128.
536/// See [`check_fixed_raw_u128`] for full documentation on precision limits and behavior.
537///
538/// # Errors
539///
540/// Returns an error if the raw value has non-zero bits beyond the precision scale.
541#[inline(always)]
542pub fn check_fixed_raw_u64(raw: u64, precision: u8) -> anyhow::Result<()> {
543    if should_skip_validation(precision) {
544        return Ok(());
545    }
546
547    let exp = usize::from(FIXED_PRECISION - precision);
548    let scale = POWERS_OF_10[exp];
549    let remainder = raw % scale;
550
551    if remainder != 0 {
552        return Err(invalid_raw_error(raw, precision, remainder, scale));
553    }
554
555    Ok(())
556}
557
558/// Checks that a raw signed fixed-point value has no spurious bits beyond the precision scale.
559///
560/// For a given precision P where P < `FIXED_PRECISION`, valid raw values must be exact
561/// multiples of `10^(FIXED_PRECISION` - P). Any non-zero remainder indicates data corruption
562/// or incorrect scaling upstream.
563///
564/// # Precision Limits
565///
566/// This check only validates when `precision < FIXED_PRECISION`:
567/// - When `precision == FIXED_PRECISION`, every bit of the raw value is significant and
568///   the check passes trivially (no "extra" bits to validate).
569/// - When `precision > FIXED_PRECISION` (possible with defi feature allowing up to 18dp),
570///   validation is not possible because the requested precision exceeds our internal
571///   representation. A debug assertion will fire to surface this during development.
572///
573/// **Important**: For defi 18dp values, this check provides NO protection against incorrectly scaled
574/// raw values. The inherent limitation is that we cannot detect if a 16dp raw is incorrectly
575/// labeled as 18dp, since both would appear valid at full internal precision.
576///
577/// # Example
578///
579/// With `FIXED_PRECISION=9` and precision=0:
580/// - Valid: `raw=120_000_000_000` (120 * 10^9, divisible by 10^9)
581/// - Invalid: `raw=119_582_001_968_421_736` (remainder `968_421_736` when divided by 10^9)
582///
583/// # Errors
584///
585/// Returns an error if the raw value has non-zero bits beyond the precision scale
586/// (only when `precision < FIXED_PRECISION`).
587#[inline(always)]
588pub fn check_fixed_raw_i128(raw: i128, precision: u8) -> anyhow::Result<()> {
589    if should_skip_validation(precision) {
590        return Ok(());
591    }
592
593    let exp = usize::from(FIXED_PRECISION - precision);
594    let scale = i128::from(POWERS_OF_10[exp]);
595    let remainder = raw % scale;
596
597    if remainder != 0 {
598        return Err(invalid_raw_error(raw, precision, remainder, scale));
599    }
600
601    Ok(())
602}
603
604/// Checks that a raw signed fixed-point value (64-bit) has no spurious bits.
605///
606/// Uses direct i64 arithmetic for better performance than widening to i128.
607/// See [`check_fixed_raw_i128`] for full documentation on precision limits and behavior.
608///
609/// # Errors
610///
611/// Returns an error if the raw value has non-zero bits beyond the precision scale.
612#[inline(always)]
613pub fn check_fixed_raw_i64(raw: i64, precision: u8) -> anyhow::Result<()> {
614    if should_skip_validation(precision) {
615        return Ok(());
616    }
617
618    let exp = usize::from(FIXED_PRECISION - precision);
619    let scale = POWERS_OF_10[exp].cast_signed();
620    let remainder = raw % scale;
621
622    if remainder != 0 {
623        return Err(invalid_raw_error(raw, precision, remainder, scale));
624    }
625
626    Ok(())
627}
628
629// -----------------------------------------------------------------------------
630// Raw value correction functions
631// -----------------------------------------------------------------------------
632// These functions round raw values to the nearest valid multiple of the scale
633// factor for a given precision. This is needed when reading data from catalogs
634// or other sources that may have been created with floating-point precision
635// errors (e.g., `int(value * FIXED_SCALAR)` instead of the correct
636// `round(value * 10^precision) * scale` approach).
637
638/// Rounds a raw `u128` value to the nearest valid multiple of the scale for the given precision.
639///
640/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
641/// from floating-point conversion errors during data creation.
642///
643/// Rounds half away from zero; when rounding away would overflow the integer range,
644/// rounds toward zero instead.
645#[must_use]
646pub fn correct_raw_u128(raw: u128, precision: u8) -> u128 {
647    if precision >= FIXED_PRECISION {
648        return raw;
649    }
650    let exp = usize::from(FIXED_PRECISION - precision);
651    let scale = u128::from(POWERS_OF_10[exp]);
652    let half_scale = scale / 2;
653    let remainder = raw % scale;
654    if remainder == 0 {
655        raw
656    } else if remainder >= half_scale {
657        raw.checked_add(scale - remainder)
658            .unwrap_or(raw - remainder)
659    } else {
660        raw - remainder
661    }
662}
663
664/// Rounds a raw `u64` value to the nearest valid multiple of the scale for the given precision.
665///
666/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
667/// from floating-point conversion errors during data creation.
668///
669/// Rounds half away from zero; when rounding away would overflow the integer range,
670/// rounds toward zero instead.
671#[must_use]
672pub fn correct_raw_u64(raw: u64, precision: u8) -> u64 {
673    if precision >= FIXED_PRECISION {
674        return raw;
675    }
676    let exp = usize::from(FIXED_PRECISION - precision);
677    let scale = POWERS_OF_10[exp];
678    let half_scale = scale / 2;
679    let remainder = raw % scale;
680    if remainder == 0 {
681        raw
682    } else if remainder >= half_scale {
683        raw.checked_add(scale - remainder)
684            .unwrap_or(raw - remainder)
685    } else {
686        raw - remainder
687    }
688}
689
690/// Rounds a raw `i128` value to the nearest valid multiple of the scale for the given precision.
691///
692/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
693/// from floating-point conversion errors during data creation.
694///
695/// Rounds half away from zero; when rounding away would overflow the integer range,
696/// rounds toward zero instead.
697#[must_use]
698pub fn correct_raw_i128(raw: i128, precision: u8) -> i128 {
699    if precision >= FIXED_PRECISION {
700        return raw;
701    }
702    let exp = usize::from(FIXED_PRECISION - precision);
703    let scale = i128::from(POWERS_OF_10[exp]);
704    let half_scale = scale / 2;
705    let remainder = raw % scale;
706    if remainder == 0 {
707        raw
708    } else if raw >= 0 {
709        if remainder >= half_scale {
710            raw.checked_add(scale - remainder)
711                .unwrap_or(raw - remainder)
712        } else {
713            raw - remainder
714        }
715    } else {
716        // For negative values, remainder is negative
717        if remainder.abs() >= half_scale {
718            raw.checked_sub(scale + remainder)
719                .unwrap_or(raw - remainder)
720        } else {
721            raw - remainder
722        }
723    }
724}
725
726/// Rounds a raw `i64` value to the nearest valid multiple of the scale for the given precision.
727///
728/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
729/// from floating-point conversion errors during data creation.
730///
731/// Rounds half away from zero; when rounding away would overflow the integer range,
732/// rounds toward zero instead.
733#[must_use]
734pub fn correct_raw_i64(raw: i64, precision: u8) -> i64 {
735    if precision >= FIXED_PRECISION {
736        return raw;
737    }
738    let exp = usize::from(FIXED_PRECISION - precision);
739    let scale = POWERS_OF_10[exp].cast_signed();
740    let half_scale = scale / 2;
741    let remainder = raw % scale;
742    if remainder == 0 {
743        raw
744    } else if raw >= 0 {
745        if remainder >= half_scale {
746            raw.checked_add(scale - remainder)
747                .unwrap_or(raw - remainder)
748        } else {
749            raw - remainder
750        }
751    } else {
752        // For negative values, remainder is negative
753        if remainder.abs() >= half_scale {
754            raw.checked_sub(scale + remainder)
755                .unwrap_or(raw - remainder)
756        } else {
757            raw - remainder
758        }
759    }
760}
761
762/// Rounds a raw price value to the nearest valid multiple of the scale for the given precision.
763///
764/// This is a type-aliased wrapper that calls the appropriate underlying function based on
765/// whether the `high-precision` feature is enabled. Use this when working with `PriceRaw` values
766/// to ensure consistent feature-flag handling.
767#[must_use]
768#[inline]
769pub fn correct_price_raw(raw: PriceRaw, precision: u8) -> PriceRaw {
770    #[cfg(feature = "high-precision")]
771    {
772        correct_raw_i128(raw, precision)
773    }
774    #[cfg(not(feature = "high-precision"))]
775    {
776        correct_raw_i64(raw, precision)
777    }
778}
779
780/// Rounds a raw quantity value to the nearest valid multiple of the scale for the given precision.
781///
782/// This is a type-aliased wrapper that calls the appropriate underlying function based on
783/// whether the `high-precision` feature is enabled. Use this when working with `QuantityRaw` values
784/// to ensure consistent feature-flag handling.
785#[must_use]
786#[inline]
787pub fn correct_quantity_raw(raw: QuantityRaw, precision: u8) -> QuantityRaw {
788    #[cfg(feature = "high-precision")]
789    {
790        correct_raw_u128(raw, precision)
791    }
792    #[cfg(not(feature = "high-precision"))]
793    {
794        correct_raw_u64(raw, precision)
795    }
796}
797
798/// Rounds a mantissa by removing `excess` decimal digits using banker's rounding (half to even).
799///
800/// Given a mantissa representing a number with `excess` extra decimal places beyond the desired
801/// precision, divides by `10^excess` and rounds the result using round-half-to-even semantics.
802#[must_use]
803#[inline]
804pub fn bankers_round(mantissa: i128, excess: u32) -> i128 {
805    if excess == 0 {
806        return mantissa;
807    }
808
809    // 10^39 overflows i128, and any i64-origin mantissa divided by 10^39+ is 0
810    if excess >= 39 {
811        return 0;
812    }
813
814    let divisor = 10i128.pow(excess);
815    let quotient = mantissa / divisor;
816    let remainder = mantissa % divisor;
817    let half = divisor / 2;
818
819    if remainder.abs() > half || (remainder.abs() == half && quotient % 2 != 0) {
820        quotient + mantissa.signum()
821    } else {
822        quotient
823    }
824}
825
826/// Converts a mantissa/exponent pair to a raw fixed-point `i128` value at the given precision.
827///
828/// The value is `mantissa * 10^exponent`. Uses pure integer arithmetic with banker's rounding
829/// when fractional digits exceed `precision`. The result is scaled to [`FIXED_PRECISION`].
830///
831/// This is the shared core for `from_decimal`, `from_decimal_dp`, and `from_mantissa_exponent`
832/// across Money, Price, and Quantity.
833///
834/// # Errors
835///
836/// Returns an error if:
837/// - `precision` exceeds the maximum allowed by [`check_fixed_precision`].
838/// - The scale factor exceeds `10^38` (i128 range).
839/// - Overflow occurs during multiplication.
840pub fn mantissa_exponent_to_fixed_i128(
841    mantissa: i128,
842    exponent: i8,
843    precision: u8,
844) -> CorrectnessResult<i128> {
845    check_fixed_precision(precision)?;
846
847    let precision_i16 = i16::from(precision);
848    let target_scale = i16::from(FIXED_PRECISION).max(precision_i16);
849    let frac_digits = -i16::from(exponent);
850
851    let mantissa = if frac_digits > precision_i16 {
852        let excess = u32::from((frac_digits - precision_i16).cast_unsigned());
853        bankers_round(mantissa, excess)
854    } else {
855        mantissa
856    };
857
858    let scale_after_rounding = frac_digits.min(precision_i16);
859    let scale_exp = target_scale - scale_after_rounding;
860    if scale_exp > 38 {
861        return Err(CorrectnessError::PredicateViolation {
862            message: format!(
863                "Exponent {exponent} produces scale factor 10^{scale_exp} which exceeds i128 range"
864            ),
865        });
866    }
867
868    if scale_exp >= 0 {
869        mantissa.checked_mul(10i128.pow(u32::from(scale_exp.cast_unsigned())))
870    } else {
871        Some(mantissa / 10i128.pow(u32::from((-scale_exp).cast_unsigned())))
872    }
873    .ok_or_else(|| CorrectnessError::PredicateViolation {
874        message: "Overflow when scaling mantissa to fixed precision".to_string(),
875    })
876}
877
878pub(crate) fn mantissa_exponent_to_raw_checked<R>(
879    mantissa: i128,
880    exponent: i8,
881    precision: u8,
882    context: &'static str,
883    raw_type_name: &'static str,
884    value_type_name: &'static str,
885) -> CorrectnessResult<R>
886where
887    R: TryFrom<i128>,
888{
889    check_fixed_precision(precision)?;
890
891    let raw_i128 = if mantissa == 0 {
892        0
893    } else {
894        mantissa_exponent_to_fixed_i128(mantissa, exponent, precision).map_err(|_| {
895            CorrectnessError::PredicateViolation {
896                message: format!(
897                    "Overflow in {context} (mantissa={mantissa}, exponent={exponent}, precision={precision})"
898                ),
899            }
900        })?
901    };
902
903    raw_i128
904        .try_into()
905        .map_err(|_| CorrectnessError::PredicateViolation {
906            message: format!(
907                "Raw value {raw_i128} exceeds {raw_type_name} range for {value_type_name}"
908            ),
909        })
910}
911
912/// Converts an `f64` value to a raw fixed-point `i64` representation with a specified precision.
913///
914/// # Precision and Rounding
915///
916/// This function performs IEEE 754 "round half to even" rounding at the specified precision
917/// before scaling to the fixed-point representation. The rounding is intentionally applied
918/// at the user-specified precision level to ensure values are correctly represented
919/// without accumulating floating-point errors during scaling.
920///
921/// Callers are expected to validate that `value` is finite and within range; non-finite
922/// values saturate at the integer bounds during the float-to-integer cast.
923///
924/// # Panics
925///
926/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
927/// overflows the raw integer range.
928#[must_use]
929#[expect(
930    clippy::cast_precision_loss,
931    clippy::cast_possible_truncation,
932    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
933)]
934pub fn f64_to_fixed_i64(value: f64, precision: u8) -> i64 {
935    check_fixed_precision(precision).expect_display(FAILED);
936    let pow1 = 10_i64.pow(u32::from(precision));
937    let pow2 = 10_i64.pow(u32::from(FIXED_PRECISION - precision));
938    let rounded = (value * pow1 as f64).round() as i64;
939    rounded
940        .checked_mul(pow2)
941        .expect("Overflow when scaling f64 to fixed-point i64")
942}
943
944/// Converts an `f64` value to a raw fixed-point `i128` representation with a specified precision.
945///
946/// Callers are expected to validate that `value` is finite and within range; non-finite
947/// values saturate at the integer bounds during the float-to-integer cast.
948///
949/// # Panics
950///
951/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
952/// overflows the raw integer range.
953#[must_use]
954#[expect(
955    clippy::cast_precision_loss,
956    clippy::cast_possible_truncation,
957    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
958)]
959pub fn f64_to_fixed_i128(value: f64, precision: u8) -> i128 {
960    check_fixed_precision(precision).expect_display(FAILED);
961    let pow1 = 10_i128.pow(u32::from(precision));
962    let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
963    let rounded = (value * pow1 as f64).round() as i128;
964    rounded
965        .checked_mul(pow2)
966        .expect("Overflow when scaling f64 to fixed-point i128")
967}
968
969/// Converts an `f64` value to a raw fixed-point `u64` representation with a specified precision.
970///
971/// Callers are expected to validate that `value` is finite and non-negative; non-finite
972/// and negative values saturate at the integer bounds during the float-to-integer cast.
973///
974/// # Panics
975///
976/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
977/// overflows the raw integer range.
978#[must_use]
979#[expect(
980    clippy::cast_precision_loss,
981    clippy::cast_possible_truncation,
982    clippy::cast_sign_loss,
983    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
984)]
985pub fn f64_to_fixed_u64(value: f64, precision: u8) -> u64 {
986    check_fixed_precision(precision).expect_display(FAILED);
987    let pow1 = 10_u64.pow(u32::from(precision));
988    let pow2 = 10_u64.pow(u32::from(FIXED_PRECISION - precision));
989    let rounded = (value * pow1 as f64).round() as u64;
990    rounded
991        .checked_mul(pow2)
992        .expect("Overflow when scaling f64 to fixed-point u64")
993}
994
995/// Converts an `f64` value to a raw fixed-point `u128` representation with a specified precision.
996///
997/// Callers are expected to validate that `value` is finite and non-negative; non-finite
998/// and negative values saturate at the integer bounds during the float-to-integer cast.
999///
1000/// # Panics
1001///
1002/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
1003/// overflows the raw integer range.
1004#[must_use]
1005#[expect(
1006    clippy::cast_precision_loss,
1007    clippy::cast_possible_truncation,
1008    clippy::cast_sign_loss,
1009    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
1010)]
1011pub fn f64_to_fixed_u128(value: f64, precision: u8) -> u128 {
1012    check_fixed_precision(precision).expect_display(FAILED);
1013    let pow1 = 10_u128.pow(u32::from(precision));
1014    let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1015    let rounded = (value * pow1 as f64).round() as u128;
1016    rounded
1017        .checked_mul(pow2)
1018        .expect("Overflow when scaling f64 to fixed-point u128")
1019}
1020
1021/// Converts a raw fixed-point `i64` value back to an `f64` value.
1022#[must_use]
1023#[expect(
1024    clippy::cast_precision_loss,
1025    reason = "i64 to f64 is inherently lossy above 2^53; accepted for float interop"
1026)]
1027pub fn fixed_i64_to_f64(value: i64) -> f64 {
1028    (value as f64) / FIXED_SCALAR
1029}
1030
1031/// Converts a raw fixed-point `i128` value back to an `f64` value.
1032#[must_use]
1033#[expect(
1034    clippy::cast_precision_loss,
1035    reason = "i128 to f64 is inherently lossy above 2^53; accepted for float interop"
1036)]
1037pub fn fixed_i128_to_f64(value: i128) -> f64 {
1038    (value as f64) / FIXED_SCALAR
1039}
1040
1041/// Converts a raw fixed-point `u64` value back to an `f64` value.
1042#[must_use]
1043#[expect(
1044    clippy::cast_precision_loss,
1045    reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
1046)]
1047pub fn fixed_u64_to_f64(value: u64) -> f64 {
1048    (value as f64) / FIXED_SCALAR
1049}
1050
1051/// Converts a raw fixed-point `u128` value back to an `f64` value.
1052#[must_use]
1053#[expect(
1054    clippy::cast_precision_loss,
1055    reason = "u128 to f64 is inherently lossy above 2^53; accepted for float interop"
1056)]
1057pub fn fixed_u128_to_f64(value: u128) -> f64 {
1058    (value as f64) / FIXED_SCALAR
1059}
1060
1061#[cfg(feature = "high-precision")]
1062#[cfg(test)]
1063mod tests {
1064    use nautilus_core::approx_eq;
1065    use rstest::rstest;
1066
1067    use super::*;
1068
1069    #[rstest]
1070    fn test_correct_raw_rounds_half_away_from_zero() {
1071        let precision = FIXED_PRECISION - 1;
1072
1073        assert_eq!(correct_raw_u128(20, precision), 20);
1074        assert_eq!(correct_raw_u128(14, precision), 10);
1075        assert_eq!(correct_raw_u128(15, precision), 20);
1076        assert_eq!(correct_raw_u64(14, precision), 10);
1077        assert_eq!(correct_raw_u64(15, precision), 20);
1078        assert_eq!(correct_raw_i128(15, precision), 20);
1079        assert_eq!(correct_raw_i128(-14, precision), -10);
1080        assert_eq!(correct_raw_i128(-15, precision), -20);
1081        assert_eq!(correct_raw_i64(15, precision), 20);
1082        assert_eq!(correct_raw_i64(-14, precision), -10);
1083        assert_eq!(correct_raw_i64(-15, precision), -20);
1084    }
1085
1086    #[rstest]
1087    fn test_f64_fixed_u128_round_trip() {
1088        let raw = f64_to_fixed_u128(1.5, 1);
1089
1090        assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1091        assert_eq!(fixed_u128_to_f64(raw), 1.5);
1092    }
1093
1094    #[rstest]
1095    fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1096        let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1097
1098        assert_eq!(
1099            mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1100            10_i128.pow(38)
1101        );
1102        assert_eq!(
1103            mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1104                .unwrap_err()
1105                .to_string(),
1106            format!(
1107                "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1108                exponent + 1
1109            )
1110        );
1111    }
1112
1113    #[rstest]
1114    fn test_raw_scales_match_requires_equal_effective_scale() {
1115        assert!(raw_scales_match(0, FIXED_PRECISION));
1116        assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1117        assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1118    }
1119
1120    #[rstest]
1121    fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1122        assert_eq!(
1123            canonical_raw(0_u128, FIXED_PRECISION + 2),
1124            (0, FIXED_PRECISION)
1125        );
1126        assert_eq!(
1127            canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1128            (12, FIXED_PRECISION)
1129        );
1130        assert_eq!(
1131            canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1132            (120, FIXED_PRECISION)
1133        );
1134        assert_eq!(
1135            canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1136            (1_205, FIXED_PRECISION + 2)
1137        );
1138        assert_eq!(
1139            canonical_raw(5_u128, FIXED_PRECISION - 1),
1140            (5, FIXED_PRECISION)
1141        );
1142    }
1143
1144    #[rstest]
1145    fn test_compare_raw_zero_operands_are_equal_across_scales() {
1146        assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1147    }
1148
1149    #[rstest]
1150    #[case("1.00", 100, 2)]
1151    #[case("+1.00", 100, 2)]
1152    #[case("-1.00", -100, 2)]
1153    #[case("-0.00", 0, 2)]
1154    fn test_parse_decimal_mantissa_sign(
1155        #[case] input: &str,
1156        #[case] mantissa: i128,
1157        #[case] precision: u8,
1158    ) {
1159        assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1160    }
1161
1162    #[rstest]
1163    #[case("-+1.00")]
1164    #[case("+-1.00")]
1165    #[case("--1.00")]
1166    #[case("++1.00")]
1167    #[case("-+0.00")]
1168    fn test_parse_decimal_mantissa_rejects_multiple_signs(#[case] input: &str) {
1169        assert_eq!(
1170            parse_decimal_mantissa(input),
1171            Err(format!("Invalid decimal value '{input}'")),
1172        );
1173        assert!(input.parse::<crate::types::Price>().is_err());
1174        assert!(input.parse::<crate::types::Quantity>().is_err());
1175        assert!(
1176            format!("{input} USD")
1177                .parse::<crate::types::Money>()
1178                .is_err()
1179        );
1180    }
1181
1182    #[rstest]
1183    #[case(i128::MIN)]
1184    #[case(i128::MAX)]
1185    fn test_parse_decimal_mantissa_integer_limits(#[case] value: i128) {
1186        assert_eq!(parse_decimal_mantissa(&value.to_string()), Ok((value, 0)));
1187    }
1188
1189    #[rstest]
1190    #[case("170141183460469231731687303715884105728")]
1191    #[case("-170141183460469231731687303715884105729")]
1192    fn test_parse_decimal_mantissa_overflow(#[case] input: &str) {
1193        assert_eq!(
1194            parse_decimal_mantissa(input),
1195            Err(format!("Decimal value '{input}' exceeds i128 range")),
1196        );
1197    }
1198
1199    #[rstest]
1200    #[case(".5", 5, 1)]
1201    #[case("-.5", -5, 1)]
1202    #[case("1.", 1, 0)]
1203    #[case("0001.0200", 10200, 4)]
1204    fn test_parse_decimal_mantissa_syntax(
1205        #[case] input: &str,
1206        #[case] mantissa: i128,
1207        #[case] precision: u8,
1208    ) {
1209        assert_eq!(parse_decimal_mantissa(input), Ok((mantissa, precision)));
1210    }
1211
1212    #[rstest]
1213    #[case("")]
1214    #[case(".")]
1215    #[case("+")]
1216    #[case("-")]
1217    #[case("1.2.3")]
1218    #[case(" 1")]
1219    #[case("1 ")]
1220    #[case("1 2")]
1221    #[case("1")]
1222    fn test_parse_decimal_mantissa_invalid_syntax(#[case] input: &str) {
1223        assert_eq!(
1224            parse_decimal_mantissa(input),
1225            Err(format!("Invalid decimal value '{input}'")),
1226        );
1227    }
1228
1229    #[rstest]
1230    fn test_parse_decimal_mantissa_fraction_length_limit() {
1231        let accepted = format!("0.{}", "0".repeat(255));
1232        let rejected = format!("{accepted}0");
1233
1234        assert_eq!(parse_decimal_mantissa(&accepted), Ok((0, 255)));
1235        assert_eq!(
1236            parse_decimal_mantissa(&rejected),
1237            Err(format!(
1238                "Decimal value '{rejected}' has too many fractional digits"
1239            )),
1240        );
1241    }
1242
1243    #[rstest]
1244    fn test_decimal_string_domain_precision_limit() {
1245        use crate::types::{Price, Quantity};
1246
1247        #[cfg(feature = "defi")]
1248        let precision = crate::defi::WEI_PRECISION;
1249        #[cfg(not(feature = "defi"))]
1250        let precision = FIXED_PRECISION;
1251        let accepted = format!("0.{}1", "0".repeat(usize::from(precision - 1)));
1252        let rejected = format!("{accepted}0");
1253        let price = accepted.parse::<Price>().unwrap();
1254        let quantity = accepted.parse::<Quantity>().unwrap();
1255
1256        assert_eq!(price.raw, 1);
1257        assert_eq!(price.precision, precision);
1258        assert_eq!(quantity.raw, 1);
1259        assert_eq!(quantity.precision, precision);
1260        assert!(rejected.parse::<Price>().is_err());
1261        assert!(rejected.parse::<Quantity>().is_err());
1262    }
1263
1264    #[rstest]
1265    #[case(0, 0, "0")]
1266    #[case(125, 2, "1.25")]
1267    #[case(-1234, 2, "-12.34")]
1268    #[case(1, 16, "0.0000000000000001")]
1269    #[case(-1, 16, "-0.0000000000000001")]
1270    #[case(1_000_000_000_000_000_000, 18, "1.000000000000000000")]
1271    fn test_scaled_raw_to_decimal_matches_plain_conversion(
1272        #[case] raw: i128,
1273        #[case] precision: u8,
1274        #[case] expected: &str,
1275    ) {
1276        let plain = Decimal::from_i128_with_scale(raw, u32::from(precision));
1277        let result = scaled_raw_to_decimal(raw, precision);
1278
1279        assert_eq!(result, plain);
1280        assert_eq!(result.scale(), plain.scale());
1281        assert_eq!(result.to_string(), expected);
1282    }
1283
1284    #[rstest]
1285    #[case(80_000_000_000_000_000_000_000_000_000, 16, "8000000000000")]
1286    #[case(340_282_366_920_930_000_000_000_000_000, 16, "34028236692093")]
1287    #[case(170_141_183_460_460_000_000_000_000_000, 16, "17014118346046")]
1288    #[case(-170_141_183_460_460_000_000_000_000_000, 16, "-17014118346046")]
1289    // Non-zero remainders exercise the fractional addition, including sign composition across
1290    // the truncating division, and a precision beyond `FIXED_PRECISION`.
1291    #[case(
1292        80_000_000_000_000_005_000_000_000_000,
1293        16,
1294        "8000000000000.000500000000000"
1295    )]
1296    #[case(-80_000_000_000_000_005_000_000_000_000, 16, "-8000000000000.000500000000000")]
1297    #[case(
1298        80_000_000_000_000_000_000_000_000_001,
1299        16,
1300        "8000000000000.000000000000000"
1301    )]
1302    #[case(
1303        80_000_000_000_000_000_250_000_000_000,
1304        18,
1305        "80000000000.00000025000000000"
1306    )]
1307    #[case(-80_000_000_000_000_000_250_000_000_000, 18, "-80000000000.00000025000000000")]
1308    fn test_scaled_raw_to_decimal_beyond_mantissa_rounds_rather_than_panics(
1309        #[case] raw: i128,
1310        #[case] precision: u8,
1311        #[case] expected: &str,
1312    ) {
1313        // `Decimal::from_i128_with_scale` panics on each of these raw values. Splitting the whole
1314        // and fractional parts lets `Decimal` drop scale instead, which is the only representable
1315        // outcome once the value needs more than a 96-bit mantissa.
1316        assert_eq!(scaled_raw_to_decimal(raw, precision).to_string(), expected);
1317    }
1318
1319    #[cfg(not(feature = "defi"))]
1320    #[rstest]
1321    fn test_precision_boundaries() {
1322        assert!(check_fixed_precision(0).is_ok());
1323        assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1324        assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1325    }
1326
1327    #[cfg(feature = "defi")]
1328    #[rstest]
1329    fn test_precision_boundaries() {
1330        use crate::defi::WEI_PRECISION;
1331
1332        assert!(check_fixed_precision(0).is_ok());
1333        assert!(check_fixed_precision(WEI_PRECISION).is_ok());
1334        assert!(check_fixed_precision(WEI_PRECISION + 1).is_err());
1335    }
1336
1337    #[rstest]
1338    #[case(0.0)]
1339    #[case(1.0)]
1340    #[case(-1.0)]
1341    fn test_basic_roundtrip(#[case] value: f64) {
1342        for precision in 0..=FIXED_PRECISION {
1343            let fixed = f64_to_fixed_i128(value, precision);
1344            let result = fixed_i128_to_f64(fixed);
1345            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1346        }
1347    }
1348
1349    #[rstest]
1350    #[case(1_000_000.0)]
1351    #[case(-1_000_000.0)]
1352    fn test_large_value_roundtrip(#[case] value: f64) {
1353        for precision in 0..=FIXED_PRECISION {
1354            let fixed = f64_to_fixed_i128(value, precision);
1355            let result = fixed_i128_to_f64(fixed);
1356            assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1357        }
1358    }
1359
1360    #[rstest]
1361    #[case(0, 123_456.0)]
1362    #[case(0, 123_456.7)]
1363    #[case(1, 123_456.7)]
1364    #[case(2, 123_456.78)]
1365    #[case(8, 123_456.123_456_78)]
1366    fn test_precision_specific_values_basic(#[case] precision: u8, #[case] value: f64) {
1367        let result = f64_to_fixed_i128(value, precision);
1368        let back_converted = fixed_i128_to_f64(result);
1369        // Round-trip should preserve the value up to the specified precision
1370        let scale = 10.0_f64.powi(i32::from(precision));
1371        let expected_rounded = (value * scale).round() / scale;
1372        assert!((back_converted - expected_rounded).abs() < 1e-10);
1373    }
1374
1375    #[rstest]
1376    fn test_max_precision_values() {
1377        // Test with maximum precision that the current feature set supports
1378        let test_value = 123_456.123_456_789;
1379        let result = f64_to_fixed_i128(test_value, FIXED_PRECISION);
1380        let back_converted = fixed_i128_to_f64(result);
1381        // For maximum precision, we expect some floating-point limitations
1382        assert!((back_converted - test_value).abs() < 1e-6);
1383    }
1384
1385    #[rstest]
1386    #[case(0.0)]
1387    #[case(1.0)]
1388    #[case(1_000_000.0)]
1389    fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1390        for precision in 0..=FIXED_PRECISION {
1391            let fixed = f64_to_fixed_u128(value, precision);
1392            let result = fixed_u128_to_f64(fixed);
1393            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1394        }
1395    }
1396
1397    #[rstest]
1398    #[case(0)]
1399    #[case(FIXED_PRECISION)]
1400    fn test_valid_precision(#[case] precision: u8) {
1401        let result = check_fixed_precision(precision);
1402        assert!(result.is_ok());
1403    }
1404
1405    #[cfg(not(feature = "defi"))]
1406    #[rstest]
1407    fn test_invalid_precision() {
1408        let precision = FIXED_PRECISION + 1;
1409        let result = check_fixed_precision(precision);
1410        assert!(result.is_err());
1411    }
1412
1413    #[cfg(feature = "defi")]
1414    #[rstest]
1415    fn test_invalid_precision() {
1416        use crate::defi::WEI_PRECISION;
1417        let precision = WEI_PRECISION + 1;
1418        let result = check_fixed_precision(precision);
1419        assert!(result.is_err());
1420    }
1421
1422    #[rstest]
1423    fn test_check_float_precision_matches_supported_range() {
1424        for precision in 0..=u8::MAX {
1425            let expected = if precision <= MAX_FLOAT_PRECISION {
1426                Ok(())
1427            } else {
1428                Err(CorrectnessError::PredicateViolation {
1429                    message: format!(
1430                        "Fixed-point precision {precision} exceeds maximum float precision 16"
1431                    ),
1432                })
1433            };
1434
1435            assert_eq!(check_float_precision(precision), expected);
1436        }
1437    }
1438
1439    #[rstest]
1440    fn test_check_float_precision_error_display() {
1441        let error = check_float_precision(18).unwrap_err();
1442
1443        assert_eq!(
1444            error.to_string(),
1445            "Fixed-point precision 18 exceeds maximum float precision 16"
1446        );
1447    }
1448
1449    #[cfg(not(feature = "defi"))]
1450    #[rstest]
1451    fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1452        let error = check_fixed_precision(FIXED_PRECISION + 1).unwrap_err();
1453
1454        assert_eq!(
1455            error,
1456            CorrectnessError::PredicateViolation {
1457                message: format!(
1458                    "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1459                    FIXED_PRECISION + 1
1460                ),
1461            }
1462        );
1463        assert_eq!(
1464            error.to_string(),
1465            format!(
1466                "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1467                FIXED_PRECISION + 1
1468            )
1469        );
1470    }
1471
1472    #[cfg(feature = "defi")]
1473    #[rstest]
1474    fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1475        use crate::defi::WEI_PRECISION;
1476
1477        let error = check_fixed_precision(WEI_PRECISION + 1).unwrap_err();
1478
1479        assert_eq!(
1480            error,
1481            CorrectnessError::PredicateViolation {
1482                message: format!(
1483                    "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1484                    WEI_PRECISION + 1
1485                ),
1486            }
1487        );
1488        assert_eq!(
1489            error.to_string(),
1490            format!(
1491                "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1492                WEI_PRECISION + 1
1493            )
1494        );
1495    }
1496
1497    #[rstest]
1498    #[case(0, 0.0)]
1499    #[case(1, 1.0)]
1500    #[case(1, 1.1)]
1501    #[case(9, 0.000_000_001)]
1502    #[case(16, 0.000_000_000_000_000_1)]
1503    #[case(0, -0.0)]
1504    #[case(1, -1.0)]
1505    #[case(1, -1.1)]
1506    #[case(9, -0.000_000_001)]
1507    #[case(16, -0.000_000_000_000_000_1)]
1508    fn test_f64_to_fixed_i128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1509        let fixed = f64_to_fixed_i128(value, precision);
1510        let result = fixed_i128_to_f64(fixed);
1511        assert_eq!(result, value);
1512    }
1513
1514    #[rstest]
1515    #[case(0, 0.0)]
1516    #[case(1, 1.0)]
1517    #[case(1, 1.1)]
1518    #[case(9, 0.000_000_001)]
1519    #[case(16, 0.000_000_000_000_000_1)]
1520    fn test_f64_to_fixed_u128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1521        let fixed = f64_to_fixed_u128(value, precision);
1522        let result = fixed_u128_to_f64(fixed);
1523        assert_eq!(result, value);
1524    }
1525
1526    #[rstest]
1527    #[case(0, 123_456.0)]
1528    #[case(0, 123_456.7)]
1529    #[case(0, 123_456.4)]
1530    #[case(1, 123_456.0)]
1531    #[case(1, 123_456.7)]
1532    #[case(1, 123_456.4)]
1533    #[case(2, 123_456.0)]
1534    #[case(2, 123_456.7)]
1535    #[case(2, 123_456.4)]
1536    fn test_f64_to_fixed_i128_with_precision(#[case] precision: u8, #[case] value: f64) {
1537        let result = f64_to_fixed_i128(value, precision);
1538
1539        // Calculate expected value dynamically based on current FIXED_PRECISION
1540        let pow1 = 10_i128.pow(u32::from(precision));
1541        let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1542        let rounded = (value * pow1 as f64).round() as i128;
1543        let expected = rounded * pow2;
1544
1545        assert_eq!(
1546            result, expected,
1547            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1548        );
1549    }
1550
1551    #[rstest]
1552    #[case(0, 5.555_555_555_555_555)]
1553    #[case(1, 5.555_555_555_555_555)]
1554    #[case(2, 5.555_555_555_555_555)]
1555    #[case(3, 5.555_555_555_555_555)]
1556    #[case(4, 5.555_555_555_555_555)]
1557    #[case(5, 5.555_555_555_555_555)]
1558    #[case(6, 5.555_555_555_555_555)]
1559    #[case(7, 5.555_555_555_555_555)]
1560    #[case(8, 5.555_555_555_555_555)]
1561    #[case(9, 5.555_555_555_555_555)]
1562    #[case(10, 5.555_555_555_555_555)]
1563    #[case(11, 5.555_555_555_555_555)]
1564    #[case(12, 5.555_555_555_555_555)]
1565    #[case(13, 5.555_555_555_555_555)]
1566    #[case(14, 5.555_555_555_555_555)]
1567    #[case(15, 5.555_555_555_555_555)]
1568    #[case(0, -5.555_555_555_555_555)]
1569    #[case(1, -5.555_555_555_555_555)]
1570    #[case(2, -5.555_555_555_555_555)]
1571    #[case(3, -5.555_555_555_555_555)]
1572    #[case(4, -5.555_555_555_555_555)]
1573    #[case(5, -5.555_555_555_555_555)]
1574    #[case(6, -5.555_555_555_555_555)]
1575    #[case(7, -5.555_555_555_555_555)]
1576    #[case(8, -5.555_555_555_555_555)]
1577    #[case(9, -5.555_555_555_555_555)]
1578    #[case(10, -5.555_555_555_555_555)]
1579    #[case(11, -5.555_555_555_555_555)]
1580    #[case(12, -5.555_555_555_555_555)]
1581    #[case(13, -5.555_555_555_555_555)]
1582    #[case(14, -5.555_555_555_555_555)]
1583    #[case(15, -5.555_555_555_555_555)]
1584    fn test_f64_to_fixed_i128(#[case] precision: u8, #[case] value: f64) {
1585        // Only test up to the current FIXED_PRECISION
1586        if precision > FIXED_PRECISION {
1587            return;
1588        }
1589
1590        let result = f64_to_fixed_i128(value, precision);
1591
1592        // Calculate expected value dynamically based on current FIXED_PRECISION
1593        let pow1 = 10_i128.pow(u32::from(precision));
1594        let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1595        let rounded = (value * pow1 as f64).round() as i128;
1596        let expected = rounded * pow2;
1597
1598        assert_eq!(
1599            result, expected,
1600            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1601        );
1602    }
1603
1604    #[rstest]
1605    #[case(0, 5.555_555_555_555_555)]
1606    #[case(1, 5.555_555_555_555_555)]
1607    #[case(2, 5.555_555_555_555_555)]
1608    #[case(3, 5.555_555_555_555_555)]
1609    #[case(4, 5.555_555_555_555_555)]
1610    #[case(5, 5.555_555_555_555_555)]
1611    #[case(6, 5.555_555_555_555_555)]
1612    #[case(7, 5.555_555_555_555_555)]
1613    #[case(8, 5.555_555_555_555_555)]
1614    #[case(9, 5.555_555_555_555_555)]
1615    #[case(10, 5.555_555_555_555_555)]
1616    #[case(11, 5.555_555_555_555_555)]
1617    #[case(12, 5.555_555_555_555_555)]
1618    #[case(13, 5.555_555_555_555_555)]
1619    #[case(14, 5.555_555_555_555_555)]
1620    #[case(15, 5.555_555_555_555_555)]
1621    #[case(16, 5.555_555_555_555_555)]
1622    fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64) {
1623        // Only test up to the current FIXED_PRECISION
1624        if precision > FIXED_PRECISION {
1625            return;
1626        }
1627
1628        let result = f64_to_fixed_u128(value, precision);
1629
1630        // Calculate expected value dynamically based on current FIXED_PRECISION
1631        let pow1 = 10_u128.pow(u32::from(precision));
1632        let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1633        let rounded = (value * pow1 as f64).round() as u128;
1634        let expected = rounded * pow2;
1635
1636        assert_eq!(
1637            result, expected,
1638            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1639        );
1640    }
1641
1642    #[rstest]
1643    fn test_fixed_i128_to_f64(
1644        #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000, -10_000, -100_000)] value: i128,
1645    ) {
1646        assert_eq!(fixed_i128_to_f64(value), value as f64 / FIXED_SCALAR);
1647    }
1648
1649    #[rstest]
1650    fn test_fixed_u128_to_f64(
1651        #[values(
1652            0,
1653            1,
1654            2,
1655            3,
1656            10,
1657            100,
1658            1_000,
1659            10_000,
1660            100_000,
1661            1_000_000,
1662            10_000_000,
1663            100_000_000,
1664            1_000_000_000,
1665            10_000_000_000,
1666            100_000_000_000,
1667            1_000_000_000_000,
1668            10_000_000_000_000,
1669            100_000_000_000_000,
1670            1_000_000_000_000_000,
1671            10_000_000_000_000_000,
1672            100_000_000_000_000_000,
1673            1_000_000_000_000_000_000,
1674            10_000_000_000_000_000_000,
1675            100_000_000_000_000_000_000
1676        )]
1677        value: u128,
1678    ) {
1679        let result = fixed_u128_to_f64(value);
1680        assert_eq!(result, (value as f64) / FIXED_SCALAR);
1681    }
1682
1683    // -------------------------------------------------------------------------
1684    // Raw value validation tests (high-precision: FIXED_PRECISION = 16)
1685    // -------------------------------------------------------------------------
1686
1687    #[rstest]
1688    #[case(0, 0)] // Zero is always valid
1689    #[case(0, 10_000_000_000_000_000)] // 1 * 10^16 at precision 0
1690    #[case(0, 1_200_000_000_000_000_000)] // 120 * 10^16 at precision 0
1691    #[case(8, 12_345_678_900_000_000)] // 123456789 * 10^8 at precision 8
1692    #[case(15, 1_234_567_890_123_450)] // Multiple of 10 at precision 15
1693    fn test_check_fixed_raw_u128_valid(#[case] precision: u8, #[case] raw: u128) {
1694        assert!(check_fixed_raw_u128(raw, precision).is_ok());
1695    }
1696
1697    #[rstest]
1698    #[case(0, 1)] // Not multiple of 10^16
1699    #[case(0, 9_999_999_999_999_999)] // One less than scale
1700    #[case(0, 10_000_000_000_000_001)] // One more than 10^16
1701    #[case(8, 12_345_678_900_000_001)] // Not multiple of 10^8
1702    #[case(15, 1_234_567_890_123_451)] // Not multiple of 10
1703    fn test_check_fixed_raw_u128_invalid(#[case] precision: u8, #[case] raw: u128) {
1704        assert!(check_fixed_raw_u128(raw, precision).is_err());
1705    }
1706
1707    #[rstest]
1708    fn test_check_fixed_raw_u128_at_max_precision() {
1709        // At FIXED_PRECISION (16), validation is skipped
1710        assert!(check_fixed_raw_u128(0, FIXED_PRECISION).is_ok());
1711        assert!(check_fixed_raw_u128(1, FIXED_PRECISION).is_ok());
1712        assert!(check_fixed_raw_u128(123_456_789, FIXED_PRECISION).is_ok());
1713        assert!(check_fixed_raw_u128(u128::MAX, FIXED_PRECISION).is_ok());
1714    }
1715
1716    #[rstest]
1717    #[case(0, 0)]
1718    #[case(0, 10_000_000_000_000_000)]
1719    #[case(0, -10_000_000_000_000_000)]
1720    #[case(8, 12_345_678_900_000_000)]
1721    #[case(8, -12_345_678_900_000_000)]
1722    fn test_check_fixed_raw_i128_valid(#[case] precision: u8, #[case] raw: i128) {
1723        assert!(check_fixed_raw_i128(raw, precision).is_ok());
1724    }
1725
1726    #[rstest]
1727    #[case(0, 1)]
1728    #[case(0, -1)]
1729    #[case(0, 9_999_999_999_999_999)]
1730    #[case(0, -9_999_999_999_999_999)]
1731    fn test_check_fixed_raw_i128_invalid(#[case] precision: u8, #[case] raw: i128) {
1732        assert!(check_fixed_raw_i128(raw, precision).is_err());
1733    }
1734
1735    #[rstest]
1736    fn test_check_fixed_raw_i128_at_max_precision() {
1737        assert!(check_fixed_raw_i128(0, FIXED_PRECISION).is_ok());
1738        assert!(check_fixed_raw_i128(1, FIXED_PRECISION).is_ok());
1739        assert!(check_fixed_raw_i128(-1, FIXED_PRECISION).is_ok());
1740        assert!(check_fixed_raw_i128(i128::MAX, FIXED_PRECISION).is_ok());
1741        assert!(check_fixed_raw_i128(i128::MIN, FIXED_PRECISION).is_ok());
1742    }
1743
1744    #[rstest]
1745    #[should_panic(expected = "Overflow when scaling f64 to fixed-point i128")]
1746    fn test_f64_to_fixed_i128_overflow_panics() {
1747        let _ = f64_to_fixed_i128(1e30, 0);
1748    }
1749
1750    #[rstest]
1751    #[should_panic(expected = "Overflow when scaling f64 to fixed-point u128")]
1752    fn test_f64_to_fixed_u128_overflow_panics() {
1753        let _ = f64_to_fixed_u128(1e30, 0);
1754    }
1755}
1756
1757#[cfg(not(feature = "high-precision"))]
1758#[cfg(test)]
1759mod tests {
1760    use nautilus_core::approx_eq;
1761    use rstest::rstest;
1762
1763    use super::*;
1764
1765    #[rstest]
1766    fn test_correct_raw_rounds_half_away_from_zero() {
1767        let precision = FIXED_PRECISION - 1;
1768
1769        assert_eq!(correct_raw_u128(20, precision), 20);
1770        assert_eq!(correct_raw_u128(14, precision), 10);
1771        assert_eq!(correct_raw_u128(15, precision), 20);
1772        assert_eq!(correct_raw_u64(14, precision), 10);
1773        assert_eq!(correct_raw_u64(15, precision), 20);
1774        assert_eq!(correct_raw_i128(15, precision), 20);
1775        assert_eq!(correct_raw_i128(-14, precision), -10);
1776        assert_eq!(correct_raw_i128(-15, precision), -20);
1777        assert_eq!(correct_raw_i64(15, precision), 20);
1778        assert_eq!(correct_raw_i64(-14, precision), -10);
1779        assert_eq!(correct_raw_i64(-15, precision), -20);
1780    }
1781
1782    #[rstest]
1783    fn test_f64_fixed_u128_round_trip() {
1784        let raw = f64_to_fixed_u128(1.5, 1);
1785
1786        assert_eq!(raw, 15 * 10_u128.pow(u32::from(FIXED_PRECISION - 1)));
1787        assert_eq!(fixed_u128_to_f64(raw), 1.5);
1788    }
1789
1790    #[rstest]
1791    fn test_mantissa_exponent_to_fixed_i128_allows_max_scale_factor() {
1792        let exponent = i8::try_from(38 - FIXED_PRECISION).unwrap();
1793
1794        assert_eq!(
1795            mantissa_exponent_to_fixed_i128(1, exponent, 0).unwrap(),
1796            10_i128.pow(38)
1797        );
1798        assert_eq!(
1799            mantissa_exponent_to_fixed_i128(1, exponent + 1, 0)
1800                .unwrap_err()
1801                .to_string(),
1802            format!(
1803                "Exponent {} produces scale factor 10^39 which exceeds i128 range",
1804                exponent + 1
1805            )
1806        );
1807    }
1808
1809    #[rstest]
1810    fn test_raw_scales_match_requires_equal_effective_scale() {
1811        assert!(raw_scales_match(0, FIXED_PRECISION));
1812        assert!(raw_scales_match(FIXED_PRECISION + 1, FIXED_PRECISION + 1));
1813        assert!(!raw_scales_match(FIXED_PRECISION, FIXED_PRECISION + 1));
1814    }
1815
1816    #[rstest]
1817    fn test_canonical_raw_trims_native_scale_trailing_zeros() {
1818        assert_eq!(
1819            canonical_raw(0_u128, FIXED_PRECISION + 2),
1820            (0, FIXED_PRECISION)
1821        );
1822        assert_eq!(
1823            canonical_raw(1_200_u128, FIXED_PRECISION + 2),
1824            (12, FIXED_PRECISION)
1825        );
1826        assert_eq!(
1827            canonical_raw(12_000_u128, FIXED_PRECISION + 2),
1828            (120, FIXED_PRECISION)
1829        );
1830        assert_eq!(
1831            canonical_raw(1_205_u128, FIXED_PRECISION + 2),
1832            (1_205, FIXED_PRECISION + 2)
1833        );
1834        assert_eq!(
1835            canonical_raw(5_u128, FIXED_PRECISION - 1),
1836            (5, FIXED_PRECISION)
1837        );
1838    }
1839
1840    #[rstest]
1841    fn test_compare_raw_zero_operands_are_equal_across_scales() {
1842        assert_eq!(compare_raw(0_u128, u8::MAX, 0_u128, 2), Ordering::Equal);
1843    }
1844
1845    #[rstest]
1846    fn test_precision_boundaries() {
1847        assert!(check_fixed_precision(0).is_ok());
1848        assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1849        assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1850    }
1851
1852    #[rstest]
1853    #[case(0.0)]
1854    #[case(1.0)]
1855    #[case(-1.0)]
1856    fn test_basic_roundtrip(#[case] value: f64) {
1857        for precision in 0..=FIXED_PRECISION {
1858            let fixed = f64_to_fixed_i64(value, precision);
1859            let result = fixed_i64_to_f64(fixed);
1860            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1861        }
1862    }
1863
1864    #[rstest]
1865    #[case(1_000_000.0)]
1866    #[case(-1_000_000.0)]
1867    fn test_large_value_roundtrip(#[case] value: f64) {
1868        for precision in 0..=FIXED_PRECISION {
1869            let fixed = f64_to_fixed_i64(value, precision);
1870            let result = fixed_i64_to_f64(fixed);
1871            assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1872        }
1873    }
1874
1875    #[rstest]
1876    #[case(0, 123_456.0, 123_456_000_000_000)]
1877    #[case(0, 123_456.7, 123_457_000_000_000)]
1878    #[case(1, 123_456.7, 123_456_700_000_000)]
1879    #[case(2, 123_456.78, 123_456_780_000_000)]
1880    #[case(8, 123_456.123_456_78, 123_456_123_456_780)]
1881    #[case(9, 123_456.123_456_789, 123_456_123_456_789)]
1882    fn test_precision_specific_values(
1883        #[case] precision: u8,
1884        #[case] value: f64,
1885        #[case] expected: i64,
1886    ) {
1887        assert_eq!(f64_to_fixed_i64(value, precision), expected);
1888    }
1889
1890    #[rstest]
1891    #[case(0.0)]
1892    #[case(1.0)]
1893    #[case(1_000_000.0)]
1894    fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1895        for precision in 0..=FIXED_PRECISION {
1896            let fixed = f64_to_fixed_u64(value, precision);
1897            let result = fixed_u64_to_f64(fixed);
1898            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1899        }
1900    }
1901
1902    #[rstest]
1903    #[case(0, 1.4, 1.0)]
1904    #[case(0, 1.5, 2.0)]
1905    #[case(0, 1.6, 2.0)]
1906    #[case(1, 1.44, 1.4)]
1907    #[case(1, 1.45, 1.5)]
1908    #[case(1, 1.46, 1.5)]
1909    #[case(2, 1.444, 1.44)]
1910    #[case(2, 1.445, 1.45)]
1911    #[case(2, 1.446, 1.45)]
1912    fn test_rounding(#[case] precision: u8, #[case] input: f64, #[case] expected: f64) {
1913        let fixed = f64_to_fixed_i128(input, precision);
1914        assert!(approx_eq!(
1915            f64,
1916            fixed_i128_to_f64(fixed),
1917            expected,
1918            epsilon = 0.000_000_001
1919        ));
1920    }
1921
1922    #[rstest]
1923    fn test_special_values() {
1924        // Zero handling
1925        assert_eq!(f64_to_fixed_i128(0.0, FIXED_PRECISION), 0);
1926        assert_eq!(f64_to_fixed_i128(-0.0, FIXED_PRECISION), 0);
1927
1928        // Small values
1929        let smallest_positive = 1.0 / FIXED_SCALAR;
1930        let fixed_smallest = f64_to_fixed_i128(smallest_positive, FIXED_PRECISION);
1931        assert_eq!(fixed_smallest, 1);
1932
1933        // Large integers
1934        let large_int = 1_000_000_000.0;
1935        let fixed_large = f64_to_fixed_i128(large_int, 0);
1936        assert_eq!(fixed_i128_to_f64(fixed_large), large_int);
1937    }
1938
1939    #[rstest]
1940    #[case(0)]
1941    #[case(FIXED_PRECISION)]
1942    fn test_valid_precision(#[case] precision: u8) {
1943        let result = check_fixed_precision(precision);
1944        assert!(result.is_ok());
1945    }
1946
1947    #[rstest]
1948    fn test_invalid_precision() {
1949        let precision = FIXED_PRECISION + 1;
1950        let result = check_fixed_precision(precision);
1951        assert!(result.is_err());
1952    }
1953
1954    #[rstest]
1955    #[case(0, 0.0)]
1956    #[case(1, 1.0)]
1957    #[case(1, 1.1)]
1958    #[case(9, 0.000_000_001)]
1959    #[case(0, -0.0)]
1960    #[case(1, -1.0)]
1961    #[case(1, -1.1)]
1962    #[case(9, -0.000_000_001)]
1963    fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1964        let fixed = f64_to_fixed_i64(value, precision);
1965        let result = fixed_i64_to_f64(fixed);
1966        assert_eq!(result, value);
1967    }
1968
1969    #[rstest]
1970    #[case(0, 0.0)]
1971    #[case(1, 1.0)]
1972    #[case(1, 1.1)]
1973    #[case(9, 0.000_000_001)]
1974    fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1975        let fixed = f64_to_fixed_u64(value, precision);
1976        let result = fixed_u64_to_f64(fixed);
1977        assert_eq!(result, value);
1978    }
1979
1980    #[rstest]
1981    #[case(0, 123_456.0, 123_456_000_000_000)]
1982    #[case(0, 123_456.7, 123_457_000_000_000)]
1983    #[case(0, 123_456.4, 123_456_000_000_000)]
1984    #[case(1, 123_456.0, 123_456_000_000_000)]
1985    #[case(1, 123_456.7, 123_456_700_000_000)]
1986    #[case(1, 123_456.4, 123_456_400_000_000)]
1987    #[case(2, 123_456.0, 123_456_000_000_000)]
1988    #[case(2, 123_456.7, 123_456_700_000_000)]
1989    #[case(2, 123_456.4, 123_456_400_000_000)]
1990    fn test_f64_to_fixed_i64_with_precision(
1991        #[case] precision: u8,
1992        #[case] value: f64,
1993        #[case] expected: i64,
1994    ) {
1995        assert_eq!(f64_to_fixed_i64(value, precision), expected);
1996    }
1997
1998    #[rstest]
1999    #[case(0, 5.5, 6_000_000_000)]
2000    #[case(1, 5.55, 5_600_000_000)]
2001    #[case(2, 5.555, 5_560_000_000)]
2002    #[case(3, 5.5555, 5_556_000_000)]
2003    #[case(4, 5.55555, 5_555_600_000)]
2004    #[case(5, 5.555_555, 5_555_560_000)]
2005    #[case(6, 5.555_555_5, 5_555_556_000)]
2006    #[case(7, 5.555_555_55, 5_555_555_600)]
2007    #[case(8, 5.555_555_555, 5_555_555_560)]
2008    #[case(9, 5.555_555_555_5, 5_555_555_556)]
2009    #[case(0, -5.5, -6_000_000_000)]
2010    #[case(1, -5.55, -5_600_000_000)]
2011    #[case(2, -5.555, -5_560_000_000)]
2012    #[case(3, -5.5555, -5_556_000_000)]
2013    #[case(4, -5.55555, -5_555_600_000)]
2014    #[case(5, -5.555_555, -5_555_560_000)]
2015    #[case(6, -5.555_555_5, -5_555_556_000)]
2016    #[case(7, -5.555_555_55, -5_555_555_600)]
2017    #[case(8, -5.555_555_555, -5_555_555_560)]
2018    #[case(9, -5.555_555_555_5, -5_555_555_556)]
2019    fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) {
2020        assert_eq!(f64_to_fixed_i64(value, precision), expected);
2021    }
2022
2023    #[rstest]
2024    #[case(0, 5.5, 6_000_000_000)]
2025    #[case(1, 5.55, 5_600_000_000)]
2026    #[case(2, 5.555, 5_560_000_000)]
2027    #[case(3, 5.5555, 5_556_000_000)]
2028    #[case(4, 5.55555, 5_555_600_000)]
2029    #[case(5, 5.555_555, 5_555_560_000)]
2030    #[case(6, 5.555_555_5, 5_555_556_000)]
2031    #[case(7, 5.555_555_55, 5_555_555_600)]
2032    #[case(8, 5.555_555_555, 5_555_555_560)]
2033    #[case(9, 5.555_555_555_5, 5_555_555_556)]
2034    fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) {
2035        assert_eq!(f64_to_fixed_u64(value, precision), expected);
2036    }
2037
2038    #[rstest]
2039    fn test_fixed_i64_to_f64(
2040        #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000)] value: i64,
2041    ) {
2042        assert_eq!(fixed_i64_to_f64(value), value as f64 / FIXED_SCALAR);
2043    }
2044
2045    #[rstest]
2046    fn test_fixed_u64_to_f64(
2047        #[values(
2048            0,
2049            1,
2050            2,
2051            3,
2052            10,
2053            100,
2054            1_000,
2055            10_000,
2056            100_000,
2057            1_000_000,
2058            10_000_000,
2059            100_000_000,
2060            1_000_000_000,
2061            10_000_000_000,
2062            100_000_000_000,
2063            1_000_000_000_000,
2064            10_000_000_000_000,
2065            100_000_000_000_000,
2066            1_000_000_000_000_000
2067        )]
2068        value: u64,
2069    ) {
2070        let result = fixed_u64_to_f64(value);
2071        assert_eq!(result, (value as f64) / FIXED_SCALAR);
2072    }
2073
2074    #[rstest]
2075    #[case(0, 0)] // Zero is always valid
2076    #[case(0, 1_000_000_000)] // 1 * 10^9 at precision 0
2077    #[case(0, 120_000_000_000)] // 120 * 10^9 at precision 0
2078    #[case(2, 123_450_000_000)] // 12345 * 10^7 at precision 2
2079    #[case(8, 1_234_567_890)] // 123456789 * 10 at precision 8
2080    fn test_check_fixed_raw_u64_valid(#[case] precision: u8, #[case] raw: u64) {
2081        assert!(check_fixed_raw_u64(raw, precision).is_ok());
2082    }
2083
2084    #[rstest]
2085    #[case(0, 1)] // Not multiple of 10^9
2086    #[case(0, 999_999_999)] // One less than scale
2087    #[case(0, 1_000_000_001)] // One more than 10^9
2088    #[case(0, 119_582_001_968_421_736)] // The original bug case
2089    #[case(2, 123_456_789_000)] // Not multiple of 10^7
2090    #[case(8, 1_234_567_891)] // Not multiple of 10
2091    fn test_check_fixed_raw_u64_invalid(#[case] precision: u8, #[case] raw: u64) {
2092        assert!(check_fixed_raw_u64(raw, precision).is_err());
2093    }
2094
2095    #[rstest]
2096    fn test_check_fixed_raw_u64_at_max_precision() {
2097        // At FIXED_PRECISION, validation is skipped - any value is valid
2098        assert!(check_fixed_raw_u64(0, FIXED_PRECISION).is_ok());
2099        assert!(check_fixed_raw_u64(1, FIXED_PRECISION).is_ok());
2100        assert!(check_fixed_raw_u64(123_456_789, FIXED_PRECISION).is_ok());
2101        assert!(check_fixed_raw_u64(u64::MAX, FIXED_PRECISION).is_ok());
2102    }
2103
2104    #[rstest]
2105    #[case(0, 0)]
2106    #[case(0, 1_000_000_000)]
2107    #[case(0, -1_000_000_000)]
2108    #[case(2, 123_450_000_000)]
2109    #[case(2, -123_450_000_000)]
2110    fn test_check_fixed_raw_i64_valid(#[case] precision: u8, #[case] raw: i64) {
2111        assert!(check_fixed_raw_i64(raw, precision).is_ok());
2112    }
2113
2114    #[rstest]
2115    #[case(0, 1)]
2116    #[case(0, -1)]
2117    #[case(0, 999_999_999)]
2118    #[case(0, -999_999_999)]
2119    fn test_check_fixed_raw_i64_invalid(#[case] precision: u8, #[case] raw: i64) {
2120        assert!(check_fixed_raw_i64(raw, precision).is_err());
2121    }
2122
2123    #[rstest]
2124    fn test_check_fixed_raw_i64_at_max_precision() {
2125        assert!(check_fixed_raw_i64(0, FIXED_PRECISION).is_ok());
2126        assert!(check_fixed_raw_i64(1, FIXED_PRECISION).is_ok());
2127        assert!(check_fixed_raw_i64(-1, FIXED_PRECISION).is_ok());
2128        assert!(check_fixed_raw_i64(i64::MAX, FIXED_PRECISION).is_ok());
2129        assert!(check_fixed_raw_i64(i64::MIN, FIXED_PRECISION).is_ok());
2130    }
2131
2132    #[rstest]
2133    #[should_panic(expected = "Overflow when scaling f64 to fixed-point i64")]
2134    fn test_f64_to_fixed_i64_overflow_panics() {
2135        let _ = f64_to_fixed_i64(2e18, 0);
2136    }
2137
2138    #[rstest]
2139    #[should_panic(expected = "Overflow when scaling f64 to fixed-point u64")]
2140    fn test_f64_to_fixed_u64_overflow_panics() {
2141        let _ = f64_to_fixed_u64(2e19, 0);
2142    }
2143}
2144
2145#[cfg(test)]
2146mod bankers_round_tests {
2147    use std::str::FromStr;
2148
2149    use rstest::rstest;
2150    use rust_decimal::{Decimal, RoundingStrategy};
2151
2152    use super::*;
2153
2154    #[rstest]
2155    // Excess=0: no rounding, identity
2156    #[case(0, 0, 0)]
2157    #[case(1, 0, 1)]
2158    #[case(5, 0, 5)]
2159    #[case(99, 0, 99)]
2160    #[case(-7, 0, -7)]
2161    // Excess >= 39: overflow guard returns 0
2162    #[case(12345, 39, 0)]
2163    #[case(i128::from(i64::MAX), 100, 0)]
2164    #[case(-99999, 50, 0)]
2165    // Excess=1: halfway cases (remainder == 5, half of 10)
2166    #[case(15, 1, 2)] // 1.5 -> 2 (round up to even)
2167    #[case(25, 1, 2)] // 2.5 -> 2 (round down to even)
2168    #[case(35, 1, 4)] // 3.5 -> 4 (round up to even)
2169    #[case(45, 1, 4)] // 4.5 -> 4 (round down to even)
2170    #[case(55, 1, 6)] // 5.5 -> 6 (round up to even)
2171    #[case(65, 1, 6)] // 6.5 -> 6 (round down to even)
2172    #[case(75, 1, 8)] // 7.5 -> 8 (round up to even)
2173    #[case(85, 1, 8)] // 8.5 -> 8 (round down to even)
2174    #[case(95, 1, 10)] // 9.5 -> 10 (round up to even)
2175    #[case(105, 1, 10)] // 10.5 -> 10 (round down to even)
2176    // Excess=1: non-halfway cases
2177    #[case(14, 1, 1)] // 1.4 -> 1 (truncate)
2178    #[case(16, 1, 2)] // 1.6 -> 2 (round up)
2179    #[case(24, 1, 2)] // 2.4 -> 2 (truncate)
2180    #[case(26, 1, 3)] // 2.6 -> 3 (round up)
2181    #[case(11, 1, 1)] // 1.1 -> 1 (truncate)
2182    #[case(19, 1, 2)] // 1.9 -> 2 (round up)
2183    // Excess=2: halfway cases (remainder == 50, half of 100)
2184    #[case(150, 2, 2)] // 1.50 -> 2 (round up to even)
2185    #[case(250, 2, 2)] // 2.50 -> 2 (round down to even)
2186    #[case(350, 2, 4)] // 3.50 -> 4 (round up to even)
2187    #[case(450, 2, 4)] // 4.50 -> 4 (round down to even)
2188    #[case(550, 2, 6)] // 5.50 -> 6 (round up to even)
2189    #[case(1050, 2, 10)] // 10.50 -> 10 (round down to even)
2190    #[case(1150, 2, 12)] // 11.50 -> 12 (round up to even)
2191    // Excess=2: non-halfway cases
2192    #[case(149, 2, 1)] // 1.49 -> 1 (truncate)
2193    #[case(151, 2, 2)] // 1.51 -> 2 (round up)
2194    #[case(199, 2, 2)] // 1.99 -> 2 (round up)
2195    #[case(101, 2, 1)] // 1.01 -> 1 (truncate)
2196    // Excess=3: halfway cases (remainder == 500, half of 1000)
2197    #[case(1500, 3, 2)] // 1.500 -> 2 (round up to even)
2198    #[case(2500, 3, 2)] // 2.500 -> 2 (round down to even)
2199    #[case(3500, 3, 4)] // 3.500 -> 4 (round up to even)
2200    #[case(10500, 3, 10)] // 10.500 -> 10 (round down to even)
2201    #[case(11500, 3, 12)] // 11.500 -> 12 (round up to even)
2202    // Excess=3: non-halfway cases
2203    #[case(1499, 3, 1)] // 1.499 -> 1 (truncate)
2204    #[case(1501, 3, 2)] // 1.501 -> 2 (round up)
2205    // Negative halfway cases
2206    #[case(-15, 1, -2)] // -1.5 -> -2 (round away from zero to even)
2207    #[case(-25, 1, -2)] // -2.5 -> -2 (round toward zero to even)
2208    #[case(-35, 1, -4)] // -3.5 -> -4 (round away from zero to even)
2209    #[case(-45, 1, -4)] // -4.5 -> -4 (round toward zero to even)
2210    #[case(-55, 1, -6)] // -5.5 -> -6 (round away from zero to even)
2211    #[case(-65, 1, -6)] // -6.5 -> -6 (round toward zero to even)
2212    #[case(-150, 2, -2)] // -1.50 -> -2 (round away from zero to even)
2213    #[case(-250, 2, -2)] // -2.50 -> -2 (round toward zero to even)
2214    #[case(-350, 2, -4)] // -3.50 -> -4 (round away from zero to even)
2215    // Negative non-halfway cases
2216    #[case(-14, 1, -1)] // -1.4 -> -1 (truncate toward zero)
2217    #[case(-16, 1, -2)] // -1.6 -> -2 (round away from zero)
2218    #[case(-24, 1, -2)] // -2.4 -> -2 (truncate toward zero)
2219    #[case(-26, 1, -3)] // -2.6 -> -3 (round away from zero)
2220    // Zero mantissa
2221    #[case(0, 1, 0)]
2222    #[case(0, 2, 0)]
2223    #[case(0, 5, 0)]
2224    // Large excess values
2225    #[case(123_456_789, 3, 123_457)] // 123456.789 -> 123457
2226    #[case(123_456_500, 3, 123_456)] // 123456.500 -> 123456 (half, even quotient)
2227    #[case(123_457_500, 3, 123_458)] // 123457.500 -> 123458 (half, odd quotient)
2228    #[case(100_005, 1, 10_000)] // 10000.5 -> 10000 (half, even quotient)
2229    #[case(100_015, 1, 10_002)] // 10001.5 -> 10002 (half, odd quotient)
2230    // Large mantissa values
2231    #[case(999_999_999_999_999_995, 1, 100_000_000_000_000_000)]
2232    #[case(1_000_000_000_000_000_005, 1, 100_000_000_000_000_000)]
2233    fn test_bankers_round(#[case] mantissa: i128, #[case] excess: u32, #[case] expected: i128) {
2234        assert_eq!(
2235            bankers_round(mantissa, excess),
2236            expected,
2237            "bankers_round({mantissa}, {excess}) expected {expected}"
2238        );
2239    }
2240
2241    // Symmetry: bankers_round(-x, e) == -bankers_round(x, e) for all positive x
2242    #[rstest]
2243    #[case(15, 1)]
2244    #[case(25, 1)]
2245    #[case(35, 1)]
2246    #[case(150, 2)]
2247    #[case(250, 2)]
2248    #[case(1500, 3)]
2249    #[case(2500, 3)]
2250    #[case(123_456_789, 3)]
2251    #[case(14, 1)]
2252    #[case(16, 1)]
2253    fn test_bankers_round_negative_symmetry(#[case] mantissa: i128, #[case] excess: u32) {
2254        assert_eq!(
2255            bankers_round(-mantissa, excess),
2256            -bankers_round(mantissa, excess),
2257            "Negative symmetry failed for mantissa={mantissa}, excess={excess}"
2258        );
2259    }
2260
2261    // Verify consistency with Rust Decimal's banker's rounding
2262    #[rstest]
2263    #[case("1.005", 2, "1.00")] // 0.005 remainder, even quotient -> truncate
2264    #[case("1.015", 2, "1.02")] // 0.005 remainder, odd quotient -> round up
2265    #[case("1.025", 2, "1.02")] // 0.005 remainder, even quotient -> truncate
2266    #[case("1.035", 2, "1.04")] // 0.005 remainder, odd quotient -> round up
2267    #[case("1.045", 2, "1.04")] // 0.005 remainder, even quotient -> truncate
2268    #[case("2.5", 0, "2")] // 0.5 remainder, even quotient -> truncate
2269    #[case("3.5", 0, "4")] // 0.5 remainder, odd quotient -> round up
2270    #[case("-2.5", 0, "-2")]
2271    #[case("-3.5", 0, "-4")]
2272    #[case("123.456", 2, "123.46")]
2273    #[case("123.455", 2, "123.46")] // Odd quotient at half
2274    #[case("123.445", 2, "123.44")] // Even quotient at half
2275    fn test_bankers_round_matches_decimal(
2276        #[case] input: &str,
2277        #[case] target_precision: u8,
2278        #[case] expected: &str,
2279    ) {
2280        let dec = Decimal::from_str(input).unwrap();
2281        let expected_dec = Decimal::from_str(expected).unwrap();
2282
2283        let decimal_rounded = dec.round_dp_with_strategy(
2284            u32::from(target_precision),
2285            RoundingStrategy::MidpointNearestEven,
2286        );
2287        assert_eq!(
2288            decimal_rounded, expected_dec,
2289            "Decimal rounding sanity check failed for {input}"
2290        );
2291
2292        let mantissa = dec.mantissa();
2293        let scale = dec.scale() as u8;
2294        let excess = u32::from(scale.saturating_sub(target_precision));
2295        if excess > 0 {
2296            let rounded = bankers_round(mantissa, excess);
2297
2298            // Reconstruct expected mantissa at target precision
2299            let expected_mantissa = expected_dec.mantissa();
2300            let expected_scale = expected_dec.scale() as u8;
2301            let scale_diff = u32::from(target_precision.saturating_sub(expected_scale));
2302            let normalized_expected = expected_mantissa * 10i128.pow(scale_diff);
2303
2304            assert_eq!(
2305                rounded, normalized_expected,
2306                "bankers_round disagrees with Decimal for {input} at precision {target_precision}"
2307            );
2308        }
2309    }
2310}
2311
2312#[cfg(test)]
2313mod correct_raw_tests {
2314    use rstest::rstest;
2315
2316    use super::*;
2317
2318    // All cases use precision = FIXED_PRECISION - 1 so the scale is 10 in both
2319    // standard-precision and high-precision modes.
2320
2321    #[rstest]
2322    #[case(0, 0)]
2323    #[case(10, 10)] // Already a multiple
2324    #[case(14, 10)] // Rounds down
2325    #[case(15, 20)] // Half rounds up
2326    #[case(16, 20)] // Rounds up
2327    #[case(u64::MAX, u64::MAX - 5)] // Rounding up would overflow; rounds down instead
2328    fn test_correct_raw_u64(#[case] raw: u64, #[case] expected: u64) {
2329        assert_eq!(correct_raw_u64(raw, FIXED_PRECISION - 1), expected);
2330    }
2331
2332    #[rstest]
2333    #[case(0, 0)]
2334    #[case(14, 10)]
2335    #[case(15, 20)]
2336    #[case(-14, -10)] // Rounds toward zero
2337    #[case(-15, -20)] // Half rounds away from zero
2338    #[case(-16, -20)] // Rounds away from zero
2339    #[case(i64::MAX, i64::MAX - 7)] // Rounding up would overflow; rounds down instead
2340    #[case(i64::MIN, i64::MIN + 8)] // Rounding down would overflow; rounds toward zero instead
2341    fn test_correct_raw_i64(#[case] raw: i64, #[case] expected: i64) {
2342        assert_eq!(correct_raw_i64(raw, FIXED_PRECISION - 1), expected);
2343    }
2344
2345    #[rstest]
2346    #[case(0, 0)]
2347    #[case(14, 10)]
2348    #[case(15, 20)]
2349    #[case(u128::MAX, u128::MAX - 5)] // Rounding up would overflow; rounds down instead
2350    fn test_correct_raw_u128(#[case] raw: u128, #[case] expected: u128) {
2351        assert_eq!(correct_raw_u128(raw, FIXED_PRECISION - 1), expected);
2352    }
2353
2354    #[rstest]
2355    #[case(0, 0)]
2356    #[case(14, 10)]
2357    #[case(15, 20)]
2358    #[case(-15, -20)]
2359    #[case(i128::MAX, i128::MAX - 7)] // Rounding up would overflow; rounds down instead
2360    #[case(i128::MIN, i128::MIN + 8)] // Rounding down would overflow; rounds toward zero instead
2361    fn test_correct_raw_i128(#[case] raw: i128, #[case] expected: i128) {
2362        assert_eq!(correct_raw_i128(raw, FIXED_PRECISION - 1), expected);
2363    }
2364
2365    #[rstest]
2366    fn test_correct_raw_identity_at_max_precision() {
2367        assert_eq!(correct_raw_u64(12_345, FIXED_PRECISION), 12_345);
2368        assert_eq!(correct_raw_i64(-12_345, FIXED_PRECISION), -12_345);
2369        assert_eq!(correct_raw_u128(12_345, FIXED_PRECISION), 12_345);
2370        assert_eq!(correct_raw_i128(-12_345, FIXED_PRECISION), -12_345);
2371    }
2372}
2373
2374#[cfg(test)]
2375mod checked_mul_div_tests {
2376    #[cfg(feature = "defi")]
2377    use alloy_primitives::U256;
2378    use proptest::{prelude::*, test_runner::Config as ProptestConfig};
2379    use rstest::rstest;
2380
2381    use super::{FIXED_SCALAR_RAW, checked_mul_div_fixed};
2382    use crate::types::quantity::QuantityRaw;
2383
2384    #[rstest]
2385    fn test_checked_mul_div_fixed_exact_boundaries() {
2386        let scalar = FIXED_SCALAR_RAW;
2387
2388        assert_eq!(checked_mul_div_fixed(0, QuantityRaw::MAX), Some(0));
2389        assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, 0), Some(0));
2390        assert_eq!(checked_mul_div_fixed(scalar, scalar), Some(scalar));
2391        assert_eq!(
2392            checked_mul_div_fixed(scalar - 1, scalar - 1),
2393            Some(scalar - 2)
2394        );
2395        assert_eq!(
2396            checked_mul_div_fixed(scalar + 1, scalar + 1),
2397            Some(scalar + 2)
2398        );
2399        assert_eq!(
2400            checked_mul_div_fixed(QuantityRaw::MAX, scalar),
2401            Some(QuantityRaw::MAX)
2402        );
2403        assert_eq!(
2404            checked_mul_div_fixed(scalar, QuantityRaw::MAX),
2405            Some(QuantityRaw::MAX)
2406        );
2407        assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, scalar + 1), None);
2408        assert_eq!(checked_mul_div_fixed(scalar + 1, QuantityRaw::MAX), None);
2409    }
2410
2411    #[cfg(not(feature = "high-precision"))]
2412    proptest! {
2413        #![proptest_config(ProptestConfig::with_cases(4_096))]
2414
2415        #[rstest]
2416        fn prop_checked_mul_div_fixed_matches_u128_full_range(
2417            lhs in any::<QuantityRaw>(),
2418            rhs in any::<QuantityRaw>(),
2419        ) {
2420            let expected = u128::from(lhs)
2421                .checked_mul(u128::from(rhs))
2422                .map(|product| product / u128::from(FIXED_SCALAR_RAW))
2423                .and_then(|result| QuantityRaw::try_from(result).ok());
2424
2425            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2426        }
2427
2428        #[rstest]
2429        fn prop_checked_mul_div_fixed_matches_u128_final_fit(
2430            (lhs, rhs) in standard_final_fit_strategy(),
2431        ) {
2432            let expected =
2433                u128::from(lhs) * u128::from(rhs) / u128::from(FIXED_SCALAR_RAW);
2434            let expected = QuantityRaw::try_from(expected).expect("strategy result fits u64");
2435
2436            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2437        }
2438
2439        #[rstest]
2440        fn prop_checked_mul_div_fixed_avoids_u64_phantom_overflow(
2441            (lhs, rhs, expected) in standard_phantom_overflow_strategy(),
2442        ) {
2443            prop_assert!(lhs.checked_mul(rhs).is_none());
2444            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2445        }
2446    }
2447
2448    #[cfg(feature = "high-precision")]
2449    proptest! {
2450        #![proptest_config(ProptestConfig::with_cases(4_096))]
2451
2452        #[rstest]
2453        fn prop_checked_mul_div_fixed_matches_u128_ordinary(
2454            (lhs, rhs) in high_precision_ordinary_strategy(),
2455        ) {
2456            let expected = lhs
2457                .checked_mul(rhs)
2458                .expect("ordinary strategy product fits u128")
2459                / FIXED_SCALAR_RAW;
2460
2461            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2462        }
2463
2464        #[rstest]
2465        fn prop_checked_mul_div_fixed_avoids_u128_phantom_overflow(
2466            (lhs, rhs, expected) in high_precision_phantom_overflow_strategy(),
2467        ) {
2468            prop_assert!(lhs.checked_mul(rhs).is_none());
2469            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2470        }
2471
2472        #[rstest]
2473        fn prop_checked_mul_div_fixed_handles_remainders_after_u128_overflow(
2474            rhs in high_precision_remainder_overflow_strategy(),
2475        ) {
2476            let lhs = 2 * FIXED_SCALAR_RAW - 1;
2477            let expected = 2 * rhs - rhs.div_ceil(FIXED_SCALAR_RAW);
2478
2479            prop_assert!(lhs.checked_mul(rhs).is_none());
2480            prop_assert_ne!(lhs % FIXED_SCALAR_RAW, 0);
2481            prop_assert_ne!(rhs % FIXED_SCALAR_RAW, 0);
2482            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2483            prop_assert_eq!(checked_mul_div_fixed(rhs, lhs), Some(expected));
2484        }
2485
2486        #[rstest]
2487        fn prop_checked_mul_div_fixed_is_commutative(
2488            lhs in any::<QuantityRaw>(),
2489            rhs in any::<QuantityRaw>(),
2490        ) {
2491            prop_assert_eq!(
2492                checked_mul_div_fixed(lhs, rhs),
2493                checked_mul_div_fixed(rhs, lhs)
2494            );
2495        }
2496    }
2497
2498    #[cfg(feature = "defi")]
2499    proptest! {
2500        #![proptest_config(ProptestConfig::with_cases(4_096))]
2501
2502        #[rstest]
2503        fn prop_checked_mul_div_raw_matches_u256_full_range(
2504            lhs in any::<QuantityRaw>(),
2505            rhs in any::<QuantityRaw>(),
2506            precision in 16_u32..=18,
2507        ) {
2508            let scalar = 10_u128.pow(precision);
2509            let expected = U256::from(lhs) * U256::from(rhs) / U256::from(scalar);
2510            let expected = QuantityRaw::try_from(expected).ok();
2511            prop_assert_eq!(super::checked_mul_div_raw(lhs, rhs, scalar), expected);
2512        }
2513
2514        #[rstest]
2515        fn prop_checked_mul_div_fixed_matches_u256_full_range(
2516            lhs in any::<QuantityRaw>(),
2517            rhs in any::<QuantityRaw>(),
2518        ) {
2519            let expected = U256::from(lhs)
2520                .checked_mul(U256::from(rhs))
2521                .expect("u128 product fits U256")
2522                / U256::from(FIXED_SCALAR_RAW);
2523            let expected = QuantityRaw::try_from(expected).ok();
2524
2525            prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2526        }
2527    }
2528
2529    #[cfg(not(feature = "high-precision"))]
2530    fn standard_final_fit_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2531        let scalar = FIXED_SCALAR_RAW;
2532
2533        (0_u64..=1_000, 0_u64..=1_000, 0_u64..scalar, 0_u64..scalar).prop_map(
2534            move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2535                (
2536                    lhs_whole * scalar + lhs_remainder,
2537                    rhs_whole * scalar + rhs_remainder,
2538                )
2539            },
2540        )
2541    }
2542
2543    #[cfg(not(feature = "high-precision"))]
2544    fn standard_phantom_overflow_strategy()
2545    -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2546        let scalar = FIXED_SCALAR_RAW;
2547
2548        (8_000_000_000_u64..=9_000_000_000, 0_u64..scalar).prop_map(
2549            move |(lhs_whole, rhs_remainder)| {
2550                let lhs = lhs_whole * scalar;
2551                let rhs = scalar + rhs_remainder;
2552                (lhs, rhs, lhs_whole * rhs)
2553            },
2554        )
2555    }
2556
2557    #[cfg(feature = "high-precision")]
2558    fn high_precision_ordinary_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2559        let scalar = FIXED_SCALAR_RAW;
2560
2561        (
2562            0_u128..=1_000,
2563            0_u128..=1_000,
2564            0_u128..scalar,
2565            0_u128..scalar,
2566        )
2567            .prop_map(
2568                move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2569                    (
2570                        lhs_whole * scalar + lhs_remainder,
2571                        rhs_whole * scalar + rhs_remainder,
2572                    )
2573                },
2574            )
2575    }
2576
2577    #[cfg(feature = "high-precision")]
2578    fn high_precision_phantom_overflow_strategy()
2579    -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2580        let scalar = FIXED_SCALAR_RAW;
2581
2582        (10_000_u128..=1_000_000, 1_000_u128..=10_000, 0_u128..scalar).prop_map(
2583            move |(lhs_whole, rhs_whole, rhs_remainder)| {
2584                let lhs = lhs_whole * scalar;
2585                let rhs = rhs_whole * scalar + rhs_remainder;
2586                (lhs, rhs, lhs_whole * rhs)
2587            },
2588        )
2589    }
2590
2591    #[cfg(feature = "high-precision")]
2592    fn high_precision_remainder_overflow_strategy() -> impl Strategy<Value = QuantityRaw> {
2593        let lhs = 2 * FIXED_SCALAR_RAW - 1;
2594        let min = QuantityRaw::MAX / lhs + 1;
2595
2596        (min..=QuantityRaw::MAX / 2).prop_filter("rhs remainder is nonzero", |rhs| {
2597            rhs % FIXED_SCALAR_RAW != 0
2598        })
2599    }
2600}