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//! - Accessing the `.raw` field 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::fmt::Display;
62
63use nautilus_core::correctness::{
64    CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
65};
66
67use crate::types::{price::PriceRaw, quantity::QuantityRaw};
68
69/// Indicates if high-precision mode is enabled.
70///
71/// # Safety
72///
73/// This static variable is initialized at compile time and never mutated,
74/// making it safe to read from multiple threads without synchronization.
75/// The value is determined by the "high-precision" feature flag.
76#[unsafe(no_mangle)]
77#[allow(unsafe_code)]
78pub static HIGH_PRECISION_MODE: u8 = cfg!(feature = "high-precision") as u8;
79
80// -----------------------------------------------------------------------------
81// FIXED_PRECISION
82// -----------------------------------------------------------------------------
83
84#[cfg(feature = "high-precision")]
85/// The maximum fixed-point precision.
86pub const FIXED_PRECISION: u8 = 16;
87
88#[cfg(not(feature = "high-precision"))]
89/// The maximum fixed-point precision.
90pub const FIXED_PRECISION: u8 = 9;
91
92// -----------------------------------------------------------------------------
93// PRECISION_BYTES (size of integer backing the fixed-point values)
94// -----------------------------------------------------------------------------
95
96#[cfg(feature = "high-precision")]
97/// The width in bytes for fixed-point value types in high-precision mode (128-bit).
98pub const PRECISION_BYTES: i32 = 16;
99
100#[cfg(not(feature = "high-precision"))]
101/// The width in bytes for fixed-point value types in standard-precision mode (64-bit).
102pub const PRECISION_BYTES: i32 = 8;
103
104// -----------------------------------------------------------------------------
105// FIXED_BINARY_SIZE
106// -----------------------------------------------------------------------------
107
108#[cfg(feature = "high-precision")]
109/// The data type name for the Arrow fixed-size binary representation.
110pub const FIXED_SIZE_BINARY: &str = "FixedSizeBinary(16)";
111
112#[cfg(not(feature = "high-precision"))]
113/// The data type name for the Arrow fixed-size binary representation.
114pub const FIXED_SIZE_BINARY: &str = "FixedSizeBinary(8)";
115
116// -----------------------------------------------------------------------------
117// FIXED_SCALAR
118// -----------------------------------------------------------------------------
119
120#[cfg(feature = "high-precision")]
121/// The scalar value corresponding to the maximum precision (10^16).
122pub const FIXED_SCALAR: f64 = 10_000_000_000_000_000.0;
123
124#[cfg(not(feature = "high-precision"))]
125/// The scalar value corresponding to the maximum precision (10^9).
126pub const FIXED_SCALAR: f64 = 1_000_000_000.0;
127
128// -----------------------------------------------------------------------------
129// PRECISION_DIFF_SCALAR
130// -----------------------------------------------------------------------------
131
132#[cfg(feature = "high-precision")]
133/// The scalar representing the difference between high-precision and standard-precision modes.
134pub const PRECISION_DIFF_SCALAR: f64 = 10_000_000.0; // 10^(16-9)
135
136#[cfg(not(feature = "high-precision"))]
137/// The scalar representing the difference between high-precision and standard-precision modes.
138pub const PRECISION_DIFF_SCALAR: f64 = 1.0;
139
140// -----------------------------------------------------------------------------
141// POWERS_OF_10 (lookup table for fast validation)
142// -----------------------------------------------------------------------------
143
144/// Precomputed powers of 10 for fast scale lookup.
145///
146/// Index i contains 10^i. Table covers 10^0 through 10^16 (sufficient for `FIXED_PRECISION`).
147/// Used by `check_fixed_raw_*` functions to avoid runtime exponentiation.
148const POWERS_OF_10: [u64; 17] = [
149    1,                      // 10^0
150    10,                     // 10^1
151    100,                    // 10^2
152    1_000,                  // 10^3
153    10_000,                 // 10^4
154    100_000,                // 10^5
155    1_000_000,              // 10^6
156    10_000_000,             // 10^7
157    100_000_000,            // 10^8
158    1_000_000_000,          // 10^9
159    10_000_000_000,         // 10^10
160    100_000_000_000,        // 10^11
161    1_000_000_000_000,      // 10^12
162    10_000_000_000_000,     // 10^13
163    100_000_000_000_000,    // 10^14
164    1_000_000_000_000_000,  // 10^15
165    10_000_000_000_000_000, // 10^16
166];
167
168// Compile-time verification that FIXED_PRECISION is within table bounds.
169// We index POWERS_OF_10[FIXED_PRECISION] when precision=0, so need strict `<`.
170const _: () = assert!(
171    (FIXED_PRECISION as usize) < POWERS_OF_10.len(),
172    "FIXED_PRECISION exceeds POWERS_OF_10 table size"
173);
174
175// -----------------------------------------------------------------------------
176
177/// The maximum precision that can be safely used with f64-based constructors.
178///
179/// This is a hard limit imposed by IEEE 754 double-precision floating-point representation,
180/// which has approximately 15-17 significant decimal digits. Beyond 16 decimal places,
181/// floating-point arithmetic becomes unreliable due to rounding errors.
182///
183/// For higher precision values (such as 18-decimal wei values in DeFi), specialized
184/// constructors that work with integer representations should be used instead.
185pub const MAX_FLOAT_PRECISION: u8 = 16;
186
187/// Checks if a given `precision` value is within the allowed fixed-point precision range.
188///
189/// # Errors
190///
191/// Returns an error if `precision` exceeds the maximum allowed:
192/// - With `defi` feature: [`WEI_PRECISION`](crate::defi::WEI_PRECISION) (18)
193/// - Without `defi` feature: [`FIXED_PRECISION`]
194pub fn check_fixed_precision(precision: u8) -> CorrectnessResult<()> {
195    #[cfg(feature = "defi")]
196    if precision > crate::defi::WEI_PRECISION {
197        return Err(CorrectnessError::PredicateViolation {
198            message: format!("`precision` exceeded maximum `WEI_PRECISION` (18), was {precision}"),
199        });
200    }
201
202    #[cfg(not(feature = "defi"))]
203    if precision > FIXED_PRECISION {
204        return Err(CorrectnessError::PredicateViolation {
205            message: format!(
206                "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {precision}"
207            ),
208        });
209    }
210
211    Ok(())
212}
213
214/// Returns `true` when two precisions encode their `raw` values at the same scale.
215///
216/// The effective scale for a given precision is `max(precision, FIXED_PRECISION)`:
217/// - Standard precisions (`<= FIXED_PRECISION`) all store raw at `FIXED_SCALAR` scale.
218/// - Defi precisions (`> FIXED_PRECISION`, e.g. 17 or 18) each store raw at their own
219///   native `10^precision` scale via constructors like `Price::from_wei` /
220///   `Quantity::from_u256`.
221///
222/// Two precisions match iff their effective scales are identical. Mixing different
223/// scales in raw arithmetic produces wrong results.
224#[inline]
225#[must_use]
226pub fn raw_scales_match(a: u8, b: u8) -> bool {
227    a.max(FIXED_PRECISION) == b.max(FIXED_PRECISION)
228}
229
230// -----------------------------------------------------------------------------
231// Raw value validation
232// -----------------------------------------------------------------------------
233
234/// Returns `true` if validation should be skipped, `false` to proceed.
235///
236/// Validation is skipped when precision >= `FIXED_PRECISION` because every bit of the raw
237/// value is significant. For precision > `FIXED_PRECISION` without the defi feature,
238/// a debug assertion fires to surface potential misuse during development.
239#[inline(always)]
240fn should_skip_validation(precision: u8) -> bool {
241    if precision == FIXED_PRECISION {
242        return true;
243    }
244
245    if precision > FIXED_PRECISION {
246        // Only assert when defi feature is disabled - with defi, 18dp is legitimate
247        #[cfg(not(feature = "defi"))]
248        debug_assert!(
249            false,
250            "precision {precision} exceeds FIXED_PRECISION {FIXED_PRECISION}: \
251             raw value validation is not possible at this precision"
252        );
253        return true;
254    }
255
256    false
257}
258
259/// Builds the error for invalid fixed-point raw values (cold path).
260#[cold]
261fn invalid_raw_error(
262    raw: impl Display,
263    precision: u8,
264    remainder: impl Display,
265    scale: impl Display,
266) -> anyhow::Error {
267    anyhow::anyhow!(
268        "Invalid fixed-point raw value {raw} for precision {precision}: \
269         remainder {remainder} when divided by scale {scale}. \
270         Raw value should be a multiple of {scale}. \
271         This indicates data corruption or incorrect precision/scaling upstream"
272    )
273}
274
275/// Checks that a raw unsigned fixed-point value has no spurious bits beyond the precision scale.
276///
277/// For a given precision P where P < `FIXED_PRECISION`, valid raw values must be exact
278/// multiples of `10^(FIXED_PRECISION` - P). Any non-zero remainder indicates data corruption
279/// or incorrect scaling upstream.
280///
281/// # Precision Limits
282///
283/// This check only validates when `precision < FIXED_PRECISION`:
284/// - When `precision == FIXED_PRECISION`, every bit of the raw value is significant and
285///   the check passes trivially (no "extra" bits to validate).
286/// - When `precision > FIXED_PRECISION` (possible with defi feature allowing up to 18dp),
287///   validation is not possible because the requested precision exceeds our internal
288///   representation. A debug assertion will fire to surface this during development.
289///
290/// **Important**: For defi 18dp values, this check provides NO protection against incorrectly scaled
291/// raw values. The inherent limitation is that we cannot detect if a 16dp raw is incorrectly
292/// labeled as 18dp, since both would appear valid at full internal precision.
293///
294/// # Example
295///
296/// With `FIXED_PRECISION=9` and precision=0:
297/// - Valid: `raw=120_000_000_000` (120 * 10^9, divisible by 10^9)
298/// - Invalid: `raw=119_582_001_968_421_736` (remainder `968_421_736` when divided by 10^9)
299///
300/// # Errors
301///
302/// Returns an error if the raw value has non-zero bits beyond the precision scale
303/// (only when `precision < FIXED_PRECISION`).
304#[inline(always)]
305pub fn check_fixed_raw_u128(raw: u128, precision: u8) -> anyhow::Result<()> {
306    if should_skip_validation(precision) {
307        return Ok(());
308    }
309
310    let exp = usize::from(FIXED_PRECISION - precision);
311    let scale = u128::from(POWERS_OF_10[exp]);
312    let remainder = raw % scale;
313
314    if remainder != 0 {
315        return Err(invalid_raw_error(raw, precision, remainder, scale));
316    }
317
318    Ok(())
319}
320
321/// Checks that a raw unsigned fixed-point value (64-bit) has no spurious bits.
322///
323/// Uses direct u64 arithmetic for better performance than widening to u128.
324/// See [`check_fixed_raw_u128`] for full documentation on precision limits and behavior.
325///
326/// # Errors
327///
328/// Returns an error if the raw value has non-zero bits beyond the precision scale.
329#[inline(always)]
330pub fn check_fixed_raw_u64(raw: u64, precision: u8) -> anyhow::Result<()> {
331    if should_skip_validation(precision) {
332        return Ok(());
333    }
334
335    let exp = usize::from(FIXED_PRECISION - precision);
336    let scale = POWERS_OF_10[exp];
337    let remainder = raw % scale;
338
339    if remainder != 0 {
340        return Err(invalid_raw_error(raw, precision, remainder, scale));
341    }
342
343    Ok(())
344}
345
346/// Checks that a raw signed fixed-point value has no spurious bits beyond the precision scale.
347///
348/// For a given precision P where P < `FIXED_PRECISION`, valid raw values must be exact
349/// multiples of `10^(FIXED_PRECISION` - P). Any non-zero remainder indicates data corruption
350/// or incorrect scaling upstream.
351///
352/// # Precision Limits
353///
354/// This check only validates when `precision < FIXED_PRECISION`:
355/// - When `precision == FIXED_PRECISION`, every bit of the raw value is significant and
356///   the check passes trivially (no "extra" bits to validate).
357/// - When `precision > FIXED_PRECISION` (possible with defi feature allowing up to 18dp),
358///   validation is not possible because the requested precision exceeds our internal
359///   representation. A debug assertion will fire to surface this during development.
360///
361/// **Important**: For defi 18dp values, this check provides NO protection against incorrectly scaled
362/// raw values. The inherent limitation is that we cannot detect if a 16dp raw is incorrectly
363/// labeled as 18dp, since both would appear valid at full internal precision.
364///
365/// # Example
366///
367/// With `FIXED_PRECISION=9` and precision=0:
368/// - Valid: `raw=120_000_000_000` (120 * 10^9, divisible by 10^9)
369/// - Invalid: `raw=119_582_001_968_421_736` (remainder `968_421_736` when divided by 10^9)
370///
371/// # Errors
372///
373/// Returns an error if the raw value has non-zero bits beyond the precision scale
374/// (only when `precision < FIXED_PRECISION`).
375#[inline(always)]
376pub fn check_fixed_raw_i128(raw: i128, precision: u8) -> anyhow::Result<()> {
377    if should_skip_validation(precision) {
378        return Ok(());
379    }
380
381    let exp = usize::from(FIXED_PRECISION - precision);
382    let scale = i128::from(POWERS_OF_10[exp]);
383    let remainder = raw % scale;
384
385    if remainder != 0 {
386        return Err(invalid_raw_error(raw, precision, remainder, scale));
387    }
388
389    Ok(())
390}
391
392/// Checks that a raw signed fixed-point value (64-bit) has no spurious bits.
393///
394/// Uses direct i64 arithmetic for better performance than widening to i128.
395/// See [`check_fixed_raw_i128`] for full documentation on precision limits and behavior.
396///
397/// # Errors
398///
399/// Returns an error if the raw value has non-zero bits beyond the precision scale.
400#[inline(always)]
401pub fn check_fixed_raw_i64(raw: i64, precision: u8) -> anyhow::Result<()> {
402    if should_skip_validation(precision) {
403        return Ok(());
404    }
405
406    let exp = usize::from(FIXED_PRECISION - precision);
407    let scale = POWERS_OF_10[exp].cast_signed();
408    let remainder = raw % scale;
409
410    if remainder != 0 {
411        return Err(invalid_raw_error(raw, precision, remainder, scale));
412    }
413
414    Ok(())
415}
416
417// -----------------------------------------------------------------------------
418// Raw value correction functions
419// -----------------------------------------------------------------------------
420// These functions round raw values to the nearest valid multiple of the scale
421// factor for a given precision. This is needed when reading data from catalogs
422// or other sources that may have been created with floating-point precision
423// errors (e.g., `int(value * FIXED_SCALAR)` instead of the correct
424// `round(value * 10^precision) * scale` approach).
425
426/// Rounds a raw `u128` value to the nearest valid multiple of the scale for the given precision.
427///
428/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
429/// from floating-point conversion errors during data creation.
430///
431/// Rounds half away from zero; when rounding away would overflow the integer range,
432/// rounds toward zero instead.
433#[must_use]
434pub fn correct_raw_u128(raw: u128, precision: u8) -> u128 {
435    if precision >= FIXED_PRECISION {
436        return raw;
437    }
438    let exp = usize::from(FIXED_PRECISION - precision);
439    let scale = u128::from(POWERS_OF_10[exp]);
440    let half_scale = scale / 2;
441    let remainder = raw % scale;
442    if remainder == 0 {
443        raw
444    } else if remainder >= half_scale {
445        raw.checked_add(scale - remainder)
446            .unwrap_or(raw - remainder)
447    } else {
448        raw - remainder
449    }
450}
451
452/// Rounds a raw `u64` value to the nearest valid multiple of the scale for the given precision.
453///
454/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
455/// from floating-point conversion errors during data creation.
456///
457/// Rounds half away from zero; when rounding away would overflow the integer range,
458/// rounds toward zero instead.
459#[must_use]
460pub fn correct_raw_u64(raw: u64, precision: u8) -> u64 {
461    if precision >= FIXED_PRECISION {
462        return raw;
463    }
464    let exp = usize::from(FIXED_PRECISION - precision);
465    let scale = POWERS_OF_10[exp];
466    let half_scale = scale / 2;
467    let remainder = raw % scale;
468    if remainder == 0 {
469        raw
470    } else if remainder >= half_scale {
471        raw.checked_add(scale - remainder)
472            .unwrap_or(raw - remainder)
473    } else {
474        raw - remainder
475    }
476}
477
478/// Rounds a raw `i128` value to the nearest valid multiple of the scale for the given precision.
479///
480/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
481/// from floating-point conversion errors during data creation.
482///
483/// Rounds half away from zero; when rounding away would overflow the integer range,
484/// rounds toward zero instead.
485#[must_use]
486pub fn correct_raw_i128(raw: i128, precision: u8) -> i128 {
487    if precision >= FIXED_PRECISION {
488        return raw;
489    }
490    let exp = usize::from(FIXED_PRECISION - precision);
491    let scale = i128::from(POWERS_OF_10[exp]);
492    let half_scale = scale / 2;
493    let remainder = raw % scale;
494    if remainder == 0 {
495        raw
496    } else if raw >= 0 {
497        if remainder >= half_scale {
498            raw.checked_add(scale - remainder)
499                .unwrap_or(raw - remainder)
500        } else {
501            raw - remainder
502        }
503    } else {
504        // For negative values, remainder is negative
505        if remainder.abs() >= half_scale {
506            raw.checked_sub(scale + remainder)
507                .unwrap_or(raw - remainder)
508        } else {
509            raw - remainder
510        }
511    }
512}
513
514/// Rounds a raw `i64` value to the nearest valid multiple of the scale for the given precision.
515///
516/// This corrects raw values that have spurious bits beyond the precision scale, which can occur
517/// from floating-point conversion errors during data creation.
518///
519/// Rounds half away from zero; when rounding away would overflow the integer range,
520/// rounds toward zero instead.
521#[must_use]
522pub fn correct_raw_i64(raw: i64, precision: u8) -> i64 {
523    if precision >= FIXED_PRECISION {
524        return raw;
525    }
526    let exp = usize::from(FIXED_PRECISION - precision);
527    let scale = POWERS_OF_10[exp].cast_signed();
528    let half_scale = scale / 2;
529    let remainder = raw % scale;
530    if remainder == 0 {
531        raw
532    } else if raw >= 0 {
533        if remainder >= half_scale {
534            raw.checked_add(scale - remainder)
535                .unwrap_or(raw - remainder)
536        } else {
537            raw - remainder
538        }
539    } else {
540        // For negative values, remainder is negative
541        if remainder.abs() >= half_scale {
542            raw.checked_sub(scale + remainder)
543                .unwrap_or(raw - remainder)
544        } else {
545            raw - remainder
546        }
547    }
548}
549
550/// Rounds a raw price value to the nearest valid multiple of the scale for the given precision.
551///
552/// This is a type-aliased wrapper that calls the appropriate underlying function based on
553/// whether the `high-precision` feature is enabled. Use this when working with `PriceRaw` values
554/// to ensure consistent feature-flag handling.
555#[must_use]
556#[inline]
557pub fn correct_price_raw(raw: PriceRaw, precision: u8) -> PriceRaw {
558    #[cfg(feature = "high-precision")]
559    {
560        correct_raw_i128(raw, precision)
561    }
562    #[cfg(not(feature = "high-precision"))]
563    {
564        correct_raw_i64(raw, precision)
565    }
566}
567
568/// Rounds a raw quantity value to the nearest valid multiple of the scale for the given precision.
569///
570/// This is a type-aliased wrapper that calls the appropriate underlying function based on
571/// whether the `high-precision` feature is enabled. Use this when working with `QuantityRaw` values
572/// to ensure consistent feature-flag handling.
573#[must_use]
574#[inline]
575pub fn correct_quantity_raw(raw: QuantityRaw, precision: u8) -> QuantityRaw {
576    #[cfg(feature = "high-precision")]
577    {
578        correct_raw_u128(raw, precision)
579    }
580    #[cfg(not(feature = "high-precision"))]
581    {
582        correct_raw_u64(raw, precision)
583    }
584}
585
586/// Rounds a mantissa by removing `excess` decimal digits using banker's rounding (half to even).
587///
588/// Given a mantissa representing a number with `excess` extra decimal places beyond the desired
589/// precision, divides by `10^excess` and rounds the result using round-half-to-even semantics.
590#[must_use]
591#[inline]
592pub fn bankers_round(mantissa: i128, excess: u32) -> i128 {
593    if excess == 0 {
594        return mantissa;
595    }
596
597    // 10^39 overflows i128, and any i64-origin mantissa divided by 10^39+ is 0
598    if excess >= 39 {
599        return 0;
600    }
601
602    let divisor = 10i128.pow(excess);
603    let quotient = mantissa / divisor;
604    let remainder = mantissa % divisor;
605    let half = divisor / 2;
606
607    if remainder.abs() > half || (remainder.abs() == half && quotient % 2 != 0) {
608        quotient + mantissa.signum()
609    } else {
610        quotient
611    }
612}
613
614/// Converts a mantissa/exponent pair to a raw fixed-point `i128` value at the given precision.
615///
616/// The value is `mantissa * 10^exponent`. Uses pure integer arithmetic with banker's rounding
617/// when fractional digits exceed `precision`. The result is scaled to [`FIXED_PRECISION`].
618///
619/// This is the shared core for `from_decimal`, `from_decimal_dp`, and `from_mantissa_exponent`
620/// across Money, Price, and Quantity.
621///
622/// # Errors
623///
624/// Returns an error if:
625/// - `precision` exceeds the maximum allowed by [`check_fixed_precision`].
626/// - The scale factor exceeds `10^38` (i128 range).
627/// - Overflow occurs during multiplication.
628pub fn mantissa_exponent_to_fixed_i128(
629    mantissa: i128,
630    exponent: i8,
631    precision: u8,
632) -> CorrectnessResult<i128> {
633    check_fixed_precision(precision)?;
634
635    let precision_i16 = i16::from(precision);
636    let target_scale = i16::from(FIXED_PRECISION).max(precision_i16);
637    let frac_digits = -i16::from(exponent);
638
639    let mantissa = if frac_digits > precision_i16 {
640        let excess = u32::from((frac_digits - precision_i16).cast_unsigned());
641        bankers_round(mantissa, excess)
642    } else {
643        mantissa
644    };
645
646    let scale_after_rounding = frac_digits.min(precision_i16);
647    let scale_exp = target_scale - scale_after_rounding;
648    if scale_exp > 38 {
649        return Err(CorrectnessError::PredicateViolation {
650            message: format!(
651                "Exponent {exponent} produces scale factor 10^{scale_exp} which exceeds i128 range"
652            ),
653        });
654    }
655
656    if scale_exp >= 0 {
657        mantissa.checked_mul(10i128.pow(u32::from(scale_exp.cast_unsigned())))
658    } else {
659        Some(mantissa / 10i128.pow(u32::from((-scale_exp).cast_unsigned())))
660    }
661    .ok_or_else(|| CorrectnessError::PredicateViolation {
662        message: "Overflow when scaling mantissa to fixed precision".to_string(),
663    })
664}
665
666pub(crate) fn mantissa_exponent_to_raw_checked<R>(
667    mantissa: i128,
668    exponent: i8,
669    precision: u8,
670    context: &'static str,
671    raw_type_name: &'static str,
672    value_type_name: &'static str,
673) -> CorrectnessResult<R>
674where
675    R: TryFrom<i128>,
676{
677    check_fixed_precision(precision)?;
678
679    let raw_i128 = if mantissa == 0 {
680        0
681    } else {
682        mantissa_exponent_to_fixed_i128(mantissa, exponent, precision).map_err(|_| {
683            CorrectnessError::PredicateViolation {
684                message: format!(
685                    "Overflow in {context} (mantissa={mantissa}, exponent={exponent}, precision={precision})"
686                ),
687            }
688        })?
689    };
690
691    raw_i128
692        .try_into()
693        .map_err(|_| CorrectnessError::PredicateViolation {
694            message: format!(
695                "Raw value {raw_i128} exceeds {raw_type_name} range for {value_type_name}"
696            ),
697        })
698}
699
700/// Converts an `f64` value to a raw fixed-point `i64` representation with a specified precision.
701///
702/// # Precision and Rounding
703///
704/// This function performs IEEE 754 "round half to even" rounding at the specified precision
705/// before scaling to the fixed-point representation. The rounding is intentionally applied
706/// at the user-specified precision level to ensure values are correctly represented
707/// without accumulating floating-point errors during scaling.
708///
709/// Callers are expected to validate that `value` is finite and within range; non-finite
710/// values saturate at the integer bounds during the float-to-integer cast.
711///
712/// # Panics
713///
714/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
715/// overflows the raw integer range.
716#[must_use]
717#[expect(
718    clippy::cast_precision_loss,
719    clippy::cast_possible_truncation,
720    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
721)]
722pub fn f64_to_fixed_i64(value: f64, precision: u8) -> i64 {
723    check_fixed_precision(precision).expect_display(FAILED);
724    let pow1 = 10_i64.pow(u32::from(precision));
725    let pow2 = 10_i64.pow(u32::from(FIXED_PRECISION - precision));
726    let rounded = (value * pow1 as f64).round() as i64;
727    rounded
728        .checked_mul(pow2)
729        .expect("Overflow when scaling f64 to fixed-point i64")
730}
731
732/// Converts an `f64` value to a raw fixed-point `i128` representation with a specified precision.
733///
734/// Callers are expected to validate that `value` is finite and within range; non-finite
735/// values saturate at the integer bounds during the float-to-integer cast.
736///
737/// # Panics
738///
739/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
740/// overflows the raw integer range.
741#[must_use]
742#[expect(
743    clippy::cast_precision_loss,
744    clippy::cast_possible_truncation,
745    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
746)]
747pub fn f64_to_fixed_i128(value: f64, precision: u8) -> i128 {
748    check_fixed_precision(precision).expect_display(FAILED);
749    let pow1 = 10_i128.pow(u32::from(precision));
750    let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
751    let rounded = (value * pow1 as f64).round() as i128;
752    rounded
753        .checked_mul(pow2)
754        .expect("Overflow when scaling f64 to fixed-point i128")
755}
756
757/// Converts an `f64` value to a raw fixed-point `u64` representation with a specified precision.
758///
759/// Callers are expected to validate that `value` is finite and non-negative; non-finite
760/// and negative values saturate at the integer bounds during the float-to-integer cast.
761///
762/// # Panics
763///
764/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
765/// overflows the raw integer range.
766#[must_use]
767#[expect(
768    clippy::cast_precision_loss,
769    clippy::cast_possible_truncation,
770    clippy::cast_sign_loss,
771    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
772)]
773pub fn f64_to_fixed_u64(value: f64, precision: u8) -> u64 {
774    check_fixed_precision(precision).expect_display(FAILED);
775    let pow1 = 10_u64.pow(u32::from(precision));
776    let pow2 = 10_u64.pow(u32::from(FIXED_PRECISION - precision));
777    let rounded = (value * pow1 as f64).round() as u64;
778    rounded
779        .checked_mul(pow2)
780        .expect("Overflow when scaling f64 to fixed-point u64")
781}
782
783/// Converts an `f64` value to a raw fixed-point `u128` representation with a specified precision.
784///
785/// Callers are expected to validate that `value` is finite and non-negative; non-finite
786/// and negative values saturate at the integer bounds during the float-to-integer cast.
787///
788/// # Panics
789///
790/// Panics if `precision` exceeds [`FIXED_PRECISION`], or if scaling the rounded value
791/// overflows the raw integer range.
792#[must_use]
793#[expect(
794    clippy::cast_precision_loss,
795    clippy::cast_possible_truncation,
796    clippy::cast_sign_loss,
797    reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
798)]
799pub fn f64_to_fixed_u128(value: f64, precision: u8) -> u128 {
800    check_fixed_precision(precision).expect_display(FAILED);
801    let pow1 = 10_u128.pow(u32::from(precision));
802    let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
803    let rounded = (value * pow1 as f64).round() as u128;
804    rounded
805        .checked_mul(pow2)
806        .expect("Overflow when scaling f64 to fixed-point u128")
807}
808
809/// Converts a raw fixed-point `i64` value back to an `f64` value.
810#[must_use]
811#[expect(
812    clippy::cast_precision_loss,
813    reason = "i64 to f64 is inherently lossy above 2^53; accepted for float interop"
814)]
815pub fn fixed_i64_to_f64(value: i64) -> f64 {
816    (value as f64) / FIXED_SCALAR
817}
818
819/// Converts a raw fixed-point `i128` value back to an `f64` value.
820#[must_use]
821#[expect(
822    clippy::cast_precision_loss,
823    reason = "i128 to f64 is inherently lossy above 2^53; accepted for float interop"
824)]
825pub fn fixed_i128_to_f64(value: i128) -> f64 {
826    (value as f64) / FIXED_SCALAR
827}
828
829/// Converts a raw fixed-point `u64` value back to an `f64` value.
830#[must_use]
831#[expect(
832    clippy::cast_precision_loss,
833    reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
834)]
835pub fn fixed_u64_to_f64(value: u64) -> f64 {
836    (value as f64) / FIXED_SCALAR
837}
838
839/// Converts a raw fixed-point `u128` value back to an `f64` value.
840#[must_use]
841#[expect(
842    clippy::cast_precision_loss,
843    reason = "u128 to f64 is inherently lossy above 2^53; accepted for float interop"
844)]
845pub fn fixed_u128_to_f64(value: u128) -> f64 {
846    (value as f64) / FIXED_SCALAR
847}
848
849#[cfg(feature = "high-precision")]
850#[cfg(test)]
851mod tests {
852    use nautilus_core::approx_eq;
853    use rstest::rstest;
854
855    use super::*;
856
857    #[cfg(not(feature = "defi"))]
858    #[rstest]
859    fn test_precision_boundaries() {
860        assert!(check_fixed_precision(0).is_ok());
861        assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
862        assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
863    }
864
865    #[cfg(feature = "defi")]
866    #[rstest]
867    fn test_precision_boundaries() {
868        use crate::defi::WEI_PRECISION;
869
870        assert!(check_fixed_precision(0).is_ok());
871        assert!(check_fixed_precision(WEI_PRECISION).is_ok());
872        assert!(check_fixed_precision(WEI_PRECISION + 1).is_err());
873    }
874
875    #[rstest]
876    #[case(0.0)]
877    #[case(1.0)]
878    #[case(-1.0)]
879    fn test_basic_roundtrip(#[case] value: f64) {
880        for precision in 0..=FIXED_PRECISION {
881            let fixed = f64_to_fixed_i128(value, precision);
882            let result = fixed_i128_to_f64(fixed);
883            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
884        }
885    }
886
887    #[rstest]
888    #[case(1_000_000.0)]
889    #[case(-1_000_000.0)]
890    fn test_large_value_roundtrip(#[case] value: f64) {
891        for precision in 0..=FIXED_PRECISION {
892            let fixed = f64_to_fixed_i128(value, precision);
893            let result = fixed_i128_to_f64(fixed);
894            assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
895        }
896    }
897
898    #[rstest]
899    #[case(0, 123_456.0)]
900    #[case(0, 123_456.7)]
901    #[case(1, 123_456.7)]
902    #[case(2, 123_456.78)]
903    #[case(8, 123_456.123_456_78)]
904    fn test_precision_specific_values_basic(#[case] precision: u8, #[case] value: f64) {
905        let result = f64_to_fixed_i128(value, precision);
906        let back_converted = fixed_i128_to_f64(result);
907        // Round-trip should preserve the value up to the specified precision
908        let scale = 10.0_f64.powi(i32::from(precision));
909        let expected_rounded = (value * scale).round() / scale;
910        assert!((back_converted - expected_rounded).abs() < 1e-10);
911    }
912
913    #[rstest]
914    fn test_max_precision_values() {
915        // Test with maximum precision that the current feature set supports
916        let test_value = 123_456.123_456_789;
917        let result = f64_to_fixed_i128(test_value, FIXED_PRECISION);
918        let back_converted = fixed_i128_to_f64(result);
919        // For maximum precision, we expect some floating-point limitations
920        assert!((back_converted - test_value).abs() < 1e-6);
921    }
922
923    #[rstest]
924    #[case(0.0)]
925    #[case(1.0)]
926    #[case(1_000_000.0)]
927    fn test_unsigned_basic_roundtrip(#[case] value: f64) {
928        for precision in 0..=FIXED_PRECISION {
929            let fixed = f64_to_fixed_u128(value, precision);
930            let result = fixed_u128_to_f64(fixed);
931            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
932        }
933    }
934
935    #[rstest]
936    #[case(0)]
937    #[case(FIXED_PRECISION)]
938    fn test_valid_precision(#[case] precision: u8) {
939        let result = check_fixed_precision(precision);
940        assert!(result.is_ok());
941    }
942
943    #[cfg(not(feature = "defi"))]
944    #[rstest]
945    fn test_invalid_precision() {
946        let precision = FIXED_PRECISION + 1;
947        let result = check_fixed_precision(precision);
948        assert!(result.is_err());
949    }
950
951    #[cfg(feature = "defi")]
952    #[rstest]
953    fn test_invalid_precision() {
954        use crate::defi::WEI_PRECISION;
955        let precision = WEI_PRECISION + 1;
956        let result = check_fixed_precision(precision);
957        assert!(result.is_err());
958    }
959
960    #[cfg(not(feature = "defi"))]
961    #[rstest]
962    fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
963        let error = check_fixed_precision(FIXED_PRECISION + 1).unwrap_err();
964
965        assert_eq!(
966            error,
967            CorrectnessError::PredicateViolation {
968                message: format!(
969                    "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
970                    FIXED_PRECISION + 1
971                ),
972            }
973        );
974        assert_eq!(
975            error.to_string(),
976            format!(
977                "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
978                FIXED_PRECISION + 1
979            )
980        );
981    }
982
983    #[cfg(feature = "defi")]
984    #[rstest]
985    fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
986        use crate::defi::WEI_PRECISION;
987
988        let error = check_fixed_precision(WEI_PRECISION + 1).unwrap_err();
989
990        assert_eq!(
991            error,
992            CorrectnessError::PredicateViolation {
993                message: format!(
994                    "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
995                    WEI_PRECISION + 1
996                ),
997            }
998        );
999        assert_eq!(
1000            error.to_string(),
1001            format!(
1002                "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1003                WEI_PRECISION + 1
1004            )
1005        );
1006    }
1007
1008    #[rstest]
1009    #[case(0, 0.0)]
1010    #[case(1, 1.0)]
1011    #[case(1, 1.1)]
1012    #[case(9, 0.000_000_001)]
1013    #[case(16, 0.000_000_000_000_000_1)]
1014    #[case(0, -0.0)]
1015    #[case(1, -1.0)]
1016    #[case(1, -1.1)]
1017    #[case(9, -0.000_000_001)]
1018    #[case(16, -0.000_000_000_000_000_1)]
1019    fn test_f64_to_fixed_i128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1020        let fixed = f64_to_fixed_i128(value, precision);
1021        let result = fixed_i128_to_f64(fixed);
1022        assert_eq!(result, value);
1023    }
1024
1025    #[rstest]
1026    #[case(0, 0.0)]
1027    #[case(1, 1.0)]
1028    #[case(1, 1.1)]
1029    #[case(9, 0.000_000_001)]
1030    #[case(16, 0.000_000_000_000_000_1)]
1031    fn test_f64_to_fixed_u128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1032        let fixed = f64_to_fixed_u128(value, precision);
1033        let result = fixed_u128_to_f64(fixed);
1034        assert_eq!(result, value);
1035    }
1036
1037    #[rstest]
1038    #[case(0, 123_456.0)]
1039    #[case(0, 123_456.7)]
1040    #[case(0, 123_456.4)]
1041    #[case(1, 123_456.0)]
1042    #[case(1, 123_456.7)]
1043    #[case(1, 123_456.4)]
1044    #[case(2, 123_456.0)]
1045    #[case(2, 123_456.7)]
1046    #[case(2, 123_456.4)]
1047    fn test_f64_to_fixed_i128_with_precision(#[case] precision: u8, #[case] value: f64) {
1048        let result = f64_to_fixed_i128(value, precision);
1049
1050        // Calculate expected value dynamically based on current FIXED_PRECISION
1051        let pow1 = 10_i128.pow(u32::from(precision));
1052        let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1053        let rounded = (value * pow1 as f64).round() as i128;
1054        let expected = rounded * pow2;
1055
1056        assert_eq!(
1057            result, expected,
1058            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1059        );
1060    }
1061
1062    #[rstest]
1063    #[case(0, 5.555_555_555_555_555)]
1064    #[case(1, 5.555_555_555_555_555)]
1065    #[case(2, 5.555_555_555_555_555)]
1066    #[case(3, 5.555_555_555_555_555)]
1067    #[case(4, 5.555_555_555_555_555)]
1068    #[case(5, 5.555_555_555_555_555)]
1069    #[case(6, 5.555_555_555_555_555)]
1070    #[case(7, 5.555_555_555_555_555)]
1071    #[case(8, 5.555_555_555_555_555)]
1072    #[case(9, 5.555_555_555_555_555)]
1073    #[case(10, 5.555_555_555_555_555)]
1074    #[case(11, 5.555_555_555_555_555)]
1075    #[case(12, 5.555_555_555_555_555)]
1076    #[case(13, 5.555_555_555_555_555)]
1077    #[case(14, 5.555_555_555_555_555)]
1078    #[case(15, 5.555_555_555_555_555)]
1079    #[case(0, -5.555_555_555_555_555)]
1080    #[case(1, -5.555_555_555_555_555)]
1081    #[case(2, -5.555_555_555_555_555)]
1082    #[case(3, -5.555_555_555_555_555)]
1083    #[case(4, -5.555_555_555_555_555)]
1084    #[case(5, -5.555_555_555_555_555)]
1085    #[case(6, -5.555_555_555_555_555)]
1086    #[case(7, -5.555_555_555_555_555)]
1087    #[case(8, -5.555_555_555_555_555)]
1088    #[case(9, -5.555_555_555_555_555)]
1089    #[case(10, -5.555_555_555_555_555)]
1090    #[case(11, -5.555_555_555_555_555)]
1091    #[case(12, -5.555_555_555_555_555)]
1092    #[case(13, -5.555_555_555_555_555)]
1093    #[case(14, -5.555_555_555_555_555)]
1094    #[case(15, -5.555_555_555_555_555)]
1095    fn test_f64_to_fixed_i128(#[case] precision: u8, #[case] value: f64) {
1096        // Only test up to the current FIXED_PRECISION
1097        if precision > FIXED_PRECISION {
1098            return;
1099        }
1100
1101        let result = f64_to_fixed_i128(value, precision);
1102
1103        // Calculate expected value dynamically based on current FIXED_PRECISION
1104        let pow1 = 10_i128.pow(u32::from(precision));
1105        let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1106        let rounded = (value * pow1 as f64).round() as i128;
1107        let expected = rounded * pow2;
1108
1109        assert_eq!(
1110            result, expected,
1111            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1112        );
1113    }
1114
1115    #[rstest]
1116    #[case(0, 5.555_555_555_555_555)]
1117    #[case(1, 5.555_555_555_555_555)]
1118    #[case(2, 5.555_555_555_555_555)]
1119    #[case(3, 5.555_555_555_555_555)]
1120    #[case(4, 5.555_555_555_555_555)]
1121    #[case(5, 5.555_555_555_555_555)]
1122    #[case(6, 5.555_555_555_555_555)]
1123    #[case(7, 5.555_555_555_555_555)]
1124    #[case(8, 5.555_555_555_555_555)]
1125    #[case(9, 5.555_555_555_555_555)]
1126    #[case(10, 5.555_555_555_555_555)]
1127    #[case(11, 5.555_555_555_555_555)]
1128    #[case(12, 5.555_555_555_555_555)]
1129    #[case(13, 5.555_555_555_555_555)]
1130    #[case(14, 5.555_555_555_555_555)]
1131    #[case(15, 5.555_555_555_555_555)]
1132    #[case(16, 5.555_555_555_555_555)]
1133    fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64) {
1134        // Only test up to the current FIXED_PRECISION
1135        if precision > FIXED_PRECISION {
1136            return;
1137        }
1138
1139        let result = f64_to_fixed_u128(value, precision);
1140
1141        // Calculate expected value dynamically based on current FIXED_PRECISION
1142        let pow1 = 10_u128.pow(u32::from(precision));
1143        let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1144        let rounded = (value * pow1 as f64).round() as u128;
1145        let expected = rounded * pow2;
1146
1147        assert_eq!(
1148            result, expected,
1149            "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1150        );
1151    }
1152
1153    #[rstest]
1154    fn test_fixed_i128_to_f64(
1155        #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000, -10_000, -100_000)] value: i128,
1156    ) {
1157        assert_eq!(fixed_i128_to_f64(value), value as f64 / FIXED_SCALAR);
1158    }
1159
1160    #[rstest]
1161    fn test_fixed_u128_to_f64(
1162        #[values(
1163            0,
1164            1,
1165            2,
1166            3,
1167            10,
1168            100,
1169            1_000,
1170            10_000,
1171            100_000,
1172            1_000_000,
1173            10_000_000,
1174            100_000_000,
1175            1_000_000_000,
1176            10_000_000_000,
1177            100_000_000_000,
1178            1_000_000_000_000,
1179            10_000_000_000_000,
1180            100_000_000_000_000,
1181            1_000_000_000_000_000,
1182            10_000_000_000_000_000,
1183            100_000_000_000_000_000,
1184            1_000_000_000_000_000_000,
1185            10_000_000_000_000_000_000,
1186            100_000_000_000_000_000_000
1187        )]
1188        value: u128,
1189    ) {
1190        let result = fixed_u128_to_f64(value);
1191        assert_eq!(result, (value as f64) / FIXED_SCALAR);
1192    }
1193
1194    // -------------------------------------------------------------------------
1195    // Raw value validation tests (high-precision: FIXED_PRECISION = 16)
1196    // -------------------------------------------------------------------------
1197
1198    #[rstest]
1199    #[case(0, 0)] // Zero is always valid
1200    #[case(0, 10_000_000_000_000_000)] // 1 * 10^16 at precision 0
1201    #[case(0, 1_200_000_000_000_000_000)] // 120 * 10^16 at precision 0
1202    #[case(8, 12_345_678_900_000_000)] // 123456789 * 10^8 at precision 8
1203    #[case(15, 1_234_567_890_123_450)] // Multiple of 10 at precision 15
1204    fn test_check_fixed_raw_u128_valid(#[case] precision: u8, #[case] raw: u128) {
1205        assert!(check_fixed_raw_u128(raw, precision).is_ok());
1206    }
1207
1208    #[rstest]
1209    #[case(0, 1)] // Not multiple of 10^16
1210    #[case(0, 9_999_999_999_999_999)] // One less than scale
1211    #[case(0, 10_000_000_000_000_001)] // One more than 10^16
1212    #[case(8, 12_345_678_900_000_001)] // Not multiple of 10^8
1213    #[case(15, 1_234_567_890_123_451)] // Not multiple of 10
1214    fn test_check_fixed_raw_u128_invalid(#[case] precision: u8, #[case] raw: u128) {
1215        assert!(check_fixed_raw_u128(raw, precision).is_err());
1216    }
1217
1218    #[rstest]
1219    fn test_check_fixed_raw_u128_at_max_precision() {
1220        // At FIXED_PRECISION (16), validation is skipped
1221        assert!(check_fixed_raw_u128(0, FIXED_PRECISION).is_ok());
1222        assert!(check_fixed_raw_u128(1, FIXED_PRECISION).is_ok());
1223        assert!(check_fixed_raw_u128(123_456_789, FIXED_PRECISION).is_ok());
1224        assert!(check_fixed_raw_u128(u128::MAX, FIXED_PRECISION).is_ok());
1225    }
1226
1227    #[rstest]
1228    #[case(0, 0)]
1229    #[case(0, 10_000_000_000_000_000)]
1230    #[case(0, -10_000_000_000_000_000)]
1231    #[case(8, 12_345_678_900_000_000)]
1232    #[case(8, -12_345_678_900_000_000)]
1233    fn test_check_fixed_raw_i128_valid(#[case] precision: u8, #[case] raw: i128) {
1234        assert!(check_fixed_raw_i128(raw, precision).is_ok());
1235    }
1236
1237    #[rstest]
1238    #[case(0, 1)]
1239    #[case(0, -1)]
1240    #[case(0, 9_999_999_999_999_999)]
1241    #[case(0, -9_999_999_999_999_999)]
1242    fn test_check_fixed_raw_i128_invalid(#[case] precision: u8, #[case] raw: i128) {
1243        assert!(check_fixed_raw_i128(raw, precision).is_err());
1244    }
1245
1246    #[rstest]
1247    fn test_check_fixed_raw_i128_at_max_precision() {
1248        assert!(check_fixed_raw_i128(0, FIXED_PRECISION).is_ok());
1249        assert!(check_fixed_raw_i128(1, FIXED_PRECISION).is_ok());
1250        assert!(check_fixed_raw_i128(-1, FIXED_PRECISION).is_ok());
1251        assert!(check_fixed_raw_i128(i128::MAX, FIXED_PRECISION).is_ok());
1252        assert!(check_fixed_raw_i128(i128::MIN, FIXED_PRECISION).is_ok());
1253    }
1254
1255    #[rstest]
1256    #[should_panic(expected = "Overflow when scaling f64 to fixed-point i128")]
1257    fn test_f64_to_fixed_i128_overflow_panics() {
1258        let _ = f64_to_fixed_i128(1e30, 0);
1259    }
1260
1261    #[rstest]
1262    #[should_panic(expected = "Overflow when scaling f64 to fixed-point u128")]
1263    fn test_f64_to_fixed_u128_overflow_panics() {
1264        let _ = f64_to_fixed_u128(1e30, 0);
1265    }
1266}
1267
1268#[cfg(not(feature = "high-precision"))]
1269#[cfg(test)]
1270mod tests {
1271    use nautilus_core::approx_eq;
1272    use rstest::rstest;
1273
1274    use super::*;
1275
1276    #[rstest]
1277    fn test_precision_boundaries() {
1278        assert!(check_fixed_precision(0).is_ok());
1279        assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1280        assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1281    }
1282
1283    #[rstest]
1284    #[case(0.0)]
1285    #[case(1.0)]
1286    #[case(-1.0)]
1287    fn test_basic_roundtrip(#[case] value: f64) {
1288        for precision in 0..=FIXED_PRECISION {
1289            let fixed = f64_to_fixed_i64(value, precision);
1290            let result = fixed_i64_to_f64(fixed);
1291            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1292        }
1293    }
1294
1295    #[rstest]
1296    #[case(1000000.0)]
1297    #[case(-1000000.0)]
1298    fn test_large_value_roundtrip(#[case] value: f64) {
1299        for precision in 0..=FIXED_PRECISION {
1300            let fixed = f64_to_fixed_i64(value, precision);
1301            let result = fixed_i64_to_f64(fixed);
1302            assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1303        }
1304    }
1305
1306    #[rstest]
1307    #[case(0, 123456.0, 123_456_000_000_000)]
1308    #[case(0, 123456.7, 123_457_000_000_000)]
1309    #[case(1, 123456.7, 123_456_700_000_000)]
1310    #[case(2, 123456.78, 123_456_780_000_000)]
1311    #[case(8, 123456.123_456_78, 123_456_123_456_780)]
1312    #[case(9, 123456.123_456_789, 123_456_123_456_789)]
1313    fn test_precision_specific_values(
1314        #[case] precision: u8,
1315        #[case] value: f64,
1316        #[case] expected: i64,
1317    ) {
1318        assert_eq!(f64_to_fixed_i64(value, precision), expected);
1319    }
1320
1321    #[rstest]
1322    #[case(0.0)]
1323    #[case(1.0)]
1324    #[case(1000000.0)]
1325    fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1326        for precision in 0..=FIXED_PRECISION {
1327            let fixed = f64_to_fixed_u64(value, precision);
1328            let result = fixed_u64_to_f64(fixed);
1329            assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1330        }
1331    }
1332
1333    #[rstest]
1334    #[case(0, 1.4, 1.0)]
1335    #[case(0, 1.5, 2.0)]
1336    #[case(0, 1.6, 2.0)]
1337    #[case(1, 1.44, 1.4)]
1338    #[case(1, 1.45, 1.5)]
1339    #[case(1, 1.46, 1.5)]
1340    #[case(2, 1.444, 1.44)]
1341    #[case(2, 1.445, 1.45)]
1342    #[case(2, 1.446, 1.45)]
1343    fn test_rounding(#[case] precision: u8, #[case] input: f64, #[case] expected: f64) {
1344        let fixed = f64_to_fixed_i128(input, precision);
1345        assert!(approx_eq!(
1346            f64,
1347            fixed_i128_to_f64(fixed),
1348            expected,
1349            epsilon = 0.000_000_001
1350        ));
1351    }
1352
1353    #[rstest]
1354    fn test_special_values() {
1355        // Zero handling
1356        assert_eq!(f64_to_fixed_i128(0.0, FIXED_PRECISION), 0);
1357        assert_eq!(f64_to_fixed_i128(-0.0, FIXED_PRECISION), 0);
1358
1359        // Small values
1360        let smallest_positive = 1.0 / FIXED_SCALAR;
1361        let fixed_smallest = f64_to_fixed_i128(smallest_positive, FIXED_PRECISION);
1362        assert_eq!(fixed_smallest, 1);
1363
1364        // Large integers
1365        let large_int = 1_000_000_000.0;
1366        let fixed_large = f64_to_fixed_i128(large_int, 0);
1367        assert_eq!(fixed_i128_to_f64(fixed_large), large_int);
1368    }
1369
1370    #[rstest]
1371    #[case(0)]
1372    #[case(FIXED_PRECISION)]
1373    fn test_valid_precision(#[case] precision: u8) {
1374        let result = check_fixed_precision(precision);
1375        assert!(result.is_ok());
1376    }
1377
1378    #[rstest]
1379    fn test_invalid_precision() {
1380        let precision = FIXED_PRECISION + 1;
1381        let result = check_fixed_precision(precision);
1382        assert!(result.is_err());
1383    }
1384
1385    #[rstest]
1386    #[case(0, 0.0)]
1387    #[case(1, 1.0)]
1388    #[case(1, 1.1)]
1389    #[case(9, 0.000_000_001)]
1390    #[case(0, -0.0)]
1391    #[case(1, -1.0)]
1392    #[case(1, -1.1)]
1393    #[case(9, -0.000_000_001)]
1394    fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1395        let fixed = f64_to_fixed_i64(value, precision);
1396        let result = fixed_i64_to_f64(fixed);
1397        assert_eq!(result, value);
1398    }
1399
1400    #[rstest]
1401    #[case(0, 0.0)]
1402    #[case(1, 1.0)]
1403    #[case(1, 1.1)]
1404    #[case(9, 0.000_000_001)]
1405    fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1406        let fixed = f64_to_fixed_u64(value, precision);
1407        let result = fixed_u64_to_f64(fixed);
1408        assert_eq!(result, value);
1409    }
1410
1411    #[rstest]
1412    #[case(0, 123456.0, 123_456_000_000_000)]
1413    #[case(0, 123456.7, 123_457_000_000_000)]
1414    #[case(0, 123_456.4, 123_456_000_000_000)]
1415    #[case(1, 123456.0, 123_456_000_000_000)]
1416    #[case(1, 123456.7, 123_456_700_000_000)]
1417    #[case(1, 123_456.4, 123_456_400_000_000)]
1418    #[case(2, 123456.0, 123_456_000_000_000)]
1419    #[case(2, 123456.7, 123_456_700_000_000)]
1420    #[case(2, 123_456.4, 123_456_400_000_000)]
1421    fn test_f64_to_fixed_i64_with_precision(
1422        #[case] precision: u8,
1423        #[case] value: f64,
1424        #[case] expected: i64,
1425    ) {
1426        assert_eq!(f64_to_fixed_i64(value, precision), expected);
1427    }
1428
1429    #[rstest]
1430    #[case(0, 5.5, 6_000_000_000)]
1431    #[case(1, 5.55, 5_600_000_000)]
1432    #[case(2, 5.555, 5_560_000_000)]
1433    #[case(3, 5.5555, 5_556_000_000)]
1434    #[case(4, 5.55555, 5_555_600_000)]
1435    #[case(5, 5.555_555, 5_555_560_000)]
1436    #[case(6, 5.555_555_5, 5_555_556_000)]
1437    #[case(7, 5.555_555_55, 5_555_555_600)]
1438    #[case(8, 5.555_555_555, 5_555_555_560)]
1439    #[case(9, 5.555_555_555_5, 5_555_555_556)]
1440    #[case(0, -5.5, -6_000_000_000)]
1441    #[case(1, -5.55, -5_600_000_000)]
1442    #[case(2, -5.555, -5_560_000_000)]
1443    #[case(3, -5.5555, -5_556_000_000)]
1444    #[case(4, -5.55555, -5_555_600_000)]
1445    #[case(5, -5.555_555, -5_555_560_000)]
1446    #[case(6, -5.555_555_5, -5_555_556_000)]
1447    #[case(7, -5.555_555_55, -5_555_555_600)]
1448    #[case(8, -5.555_555_555, -5_555_555_560)]
1449    #[case(9, -5.555_555_555_5, -5_555_555_556)]
1450    fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) {
1451        assert_eq!(f64_to_fixed_i64(value, precision), expected);
1452    }
1453
1454    #[rstest]
1455    #[case(0, 5.5, 6_000_000_000)]
1456    #[case(1, 5.55, 5_600_000_000)]
1457    #[case(2, 5.555, 5_560_000_000)]
1458    #[case(3, 5.5555, 5_556_000_000)]
1459    #[case(4, 5.55555, 5_555_600_000)]
1460    #[case(5, 5.555_555, 5_555_560_000)]
1461    #[case(6, 5.555_555_5, 5_555_556_000)]
1462    #[case(7, 5.555_555_55, 5_555_555_600)]
1463    #[case(8, 5.555_555_555, 5_555_555_560)]
1464    #[case(9, 5.555_555_555_5, 5_555_555_556)]
1465    fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) {
1466        assert_eq!(f64_to_fixed_u64(value, precision), expected);
1467    }
1468
1469    #[rstest]
1470    fn test_fixed_i64_to_f64(
1471        #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000)] value: i64,
1472    ) {
1473        assert_eq!(fixed_i64_to_f64(value), value as f64 / FIXED_SCALAR);
1474    }
1475
1476    #[rstest]
1477    fn test_fixed_u64_to_f64(
1478        #[values(
1479            0,
1480            1,
1481            2,
1482            3,
1483            10,
1484            100,
1485            1_000,
1486            10_000,
1487            100_000,
1488            1_000_000,
1489            10_000_000,
1490            100_000_000,
1491            1_000_000_000,
1492            10_000_000_000,
1493            100_000_000_000,
1494            1_000_000_000_000,
1495            10_000_000_000_000,
1496            100_000_000_000_000,
1497            1_000_000_000_000_000
1498        )]
1499        value: u64,
1500    ) {
1501        let result = fixed_u64_to_f64(value);
1502        assert_eq!(result, (value as f64) / FIXED_SCALAR);
1503    }
1504
1505    #[rstest]
1506    #[case(0, 0)] // Zero is always valid
1507    #[case(0, 1_000_000_000)] // 1 * 10^9 at precision 0
1508    #[case(0, 120_000_000_000)] // 120 * 10^9 at precision 0
1509    #[case(2, 123_450_000_000)] // 12345 * 10^7 at precision 2
1510    #[case(8, 1_234_567_890)] // 123456789 * 10 at precision 8
1511    fn test_check_fixed_raw_u64_valid(#[case] precision: u8, #[case] raw: u64) {
1512        assert!(check_fixed_raw_u64(raw, precision).is_ok());
1513    }
1514
1515    #[rstest]
1516    #[case(0, 1)] // Not multiple of 10^9
1517    #[case(0, 999_999_999)] // One less than scale
1518    #[case(0, 1_000_000_001)] // One more than 10^9
1519    #[case(0, 119_582_001_968_421_736)] // The original bug case
1520    #[case(2, 123_456_789_000)] // Not multiple of 10^7
1521    #[case(8, 1_234_567_891)] // Not multiple of 10
1522    fn test_check_fixed_raw_u64_invalid(#[case] precision: u8, #[case] raw: u64) {
1523        assert!(check_fixed_raw_u64(raw, precision).is_err());
1524    }
1525
1526    #[rstest]
1527    fn test_check_fixed_raw_u64_at_max_precision() {
1528        // At FIXED_PRECISION, validation is skipped - any value is valid
1529        assert!(check_fixed_raw_u64(0, FIXED_PRECISION).is_ok());
1530        assert!(check_fixed_raw_u64(1, FIXED_PRECISION).is_ok());
1531        assert!(check_fixed_raw_u64(123_456_789, FIXED_PRECISION).is_ok());
1532        assert!(check_fixed_raw_u64(u64::MAX, FIXED_PRECISION).is_ok());
1533    }
1534
1535    #[rstest]
1536    #[case(0, 0)]
1537    #[case(0, 1_000_000_000)]
1538    #[case(0, -1_000_000_000)]
1539    #[case(2, 123_450_000_000)]
1540    #[case(2, -123_450_000_000)]
1541    fn test_check_fixed_raw_i64_valid(#[case] precision: u8, #[case] raw: i64) {
1542        assert!(check_fixed_raw_i64(raw, precision).is_ok());
1543    }
1544
1545    #[rstest]
1546    #[case(0, 1)]
1547    #[case(0, -1)]
1548    #[case(0, 999_999_999)]
1549    #[case(0, -999_999_999)]
1550    fn test_check_fixed_raw_i64_invalid(#[case] precision: u8, #[case] raw: i64) {
1551        assert!(check_fixed_raw_i64(raw, precision).is_err());
1552    }
1553
1554    #[rstest]
1555    fn test_check_fixed_raw_i64_at_max_precision() {
1556        assert!(check_fixed_raw_i64(0, FIXED_PRECISION).is_ok());
1557        assert!(check_fixed_raw_i64(1, FIXED_PRECISION).is_ok());
1558        assert!(check_fixed_raw_i64(-1, FIXED_PRECISION).is_ok());
1559        assert!(check_fixed_raw_i64(i64::MAX, FIXED_PRECISION).is_ok());
1560        assert!(check_fixed_raw_i64(i64::MIN, FIXED_PRECISION).is_ok());
1561    }
1562
1563    #[rstest]
1564    #[should_panic(expected = "Overflow when scaling f64 to fixed-point i64")]
1565    fn test_f64_to_fixed_i64_overflow_panics() {
1566        let _ = f64_to_fixed_i64(2e18, 0);
1567    }
1568
1569    #[rstest]
1570    #[should_panic(expected = "Overflow when scaling f64 to fixed-point u64")]
1571    fn test_f64_to_fixed_u64_overflow_panics() {
1572        let _ = f64_to_fixed_u64(2e19, 0);
1573    }
1574}
1575
1576#[cfg(test)]
1577mod bankers_round_tests {
1578    use std::str::FromStr;
1579
1580    use rstest::rstest;
1581    use rust_decimal::{Decimal, RoundingStrategy};
1582
1583    use super::*;
1584
1585    #[rstest]
1586    // Excess=0: no rounding, identity
1587    #[case(0, 0, 0)]
1588    #[case(1, 0, 1)]
1589    #[case(5, 0, 5)]
1590    #[case(99, 0, 99)]
1591    #[case(-7, 0, -7)]
1592    // Excess >= 39: overflow guard returns 0
1593    #[case(12345, 39, 0)]
1594    #[case(i128::from(i64::MAX), 100, 0)]
1595    #[case(-99999, 50, 0)]
1596    // Excess=1: halfway cases (remainder == 5, half of 10)
1597    #[case(15, 1, 2)] // 1.5 -> 2 (round up to even)
1598    #[case(25, 1, 2)] // 2.5 -> 2 (round down to even)
1599    #[case(35, 1, 4)] // 3.5 -> 4 (round up to even)
1600    #[case(45, 1, 4)] // 4.5 -> 4 (round down to even)
1601    #[case(55, 1, 6)] // 5.5 -> 6 (round up to even)
1602    #[case(65, 1, 6)] // 6.5 -> 6 (round down to even)
1603    #[case(75, 1, 8)] // 7.5 -> 8 (round up to even)
1604    #[case(85, 1, 8)] // 8.5 -> 8 (round down to even)
1605    #[case(95, 1, 10)] // 9.5 -> 10 (round up to even)
1606    #[case(105, 1, 10)] // 10.5 -> 10 (round down to even)
1607    // Excess=1: non-halfway cases
1608    #[case(14, 1, 1)] // 1.4 -> 1 (truncate)
1609    #[case(16, 1, 2)] // 1.6 -> 2 (round up)
1610    #[case(24, 1, 2)] // 2.4 -> 2 (truncate)
1611    #[case(26, 1, 3)] // 2.6 -> 3 (round up)
1612    #[case(11, 1, 1)] // 1.1 -> 1 (truncate)
1613    #[case(19, 1, 2)] // 1.9 -> 2 (round up)
1614    // Excess=2: halfway cases (remainder == 50, half of 100)
1615    #[case(150, 2, 2)] // 1.50 -> 2 (round up to even)
1616    #[case(250, 2, 2)] // 2.50 -> 2 (round down to even)
1617    #[case(350, 2, 4)] // 3.50 -> 4 (round up to even)
1618    #[case(450, 2, 4)] // 4.50 -> 4 (round down to even)
1619    #[case(550, 2, 6)] // 5.50 -> 6 (round up to even)
1620    #[case(1050, 2, 10)] // 10.50 -> 10 (round down to even)
1621    #[case(1150, 2, 12)] // 11.50 -> 12 (round up to even)
1622    // Excess=2: non-halfway cases
1623    #[case(149, 2, 1)] // 1.49 -> 1 (truncate)
1624    #[case(151, 2, 2)] // 1.51 -> 2 (round up)
1625    #[case(199, 2, 2)] // 1.99 -> 2 (round up)
1626    #[case(101, 2, 1)] // 1.01 -> 1 (truncate)
1627    // Excess=3: halfway cases (remainder == 500, half of 1000)
1628    #[case(1500, 3, 2)] // 1.500 -> 2 (round up to even)
1629    #[case(2500, 3, 2)] // 2.500 -> 2 (round down to even)
1630    #[case(3500, 3, 4)] // 3.500 -> 4 (round up to even)
1631    #[case(10500, 3, 10)] // 10.500 -> 10 (round down to even)
1632    #[case(11500, 3, 12)] // 11.500 -> 12 (round up to even)
1633    // Excess=3: non-halfway cases
1634    #[case(1499, 3, 1)] // 1.499 -> 1 (truncate)
1635    #[case(1501, 3, 2)] // 1.501 -> 2 (round up)
1636    // Negative halfway cases
1637    #[case(-15, 1, -2)] // -1.5 -> -2 (round away from zero to even)
1638    #[case(-25, 1, -2)] // -2.5 -> -2 (round toward zero to even)
1639    #[case(-35, 1, -4)] // -3.5 -> -4 (round away from zero to even)
1640    #[case(-45, 1, -4)] // -4.5 -> -4 (round toward zero to even)
1641    #[case(-55, 1, -6)] // -5.5 -> -6 (round away from zero to even)
1642    #[case(-65, 1, -6)] // -6.5 -> -6 (round toward zero to even)
1643    #[case(-150, 2, -2)] // -1.50 -> -2 (round away from zero to even)
1644    #[case(-250, 2, -2)] // -2.50 -> -2 (round toward zero to even)
1645    #[case(-350, 2, -4)] // -3.50 -> -4 (round away from zero to even)
1646    // Negative non-halfway cases
1647    #[case(-14, 1, -1)] // -1.4 -> -1 (truncate toward zero)
1648    #[case(-16, 1, -2)] // -1.6 -> -2 (round away from zero)
1649    #[case(-24, 1, -2)] // -2.4 -> -2 (truncate toward zero)
1650    #[case(-26, 1, -3)] // -2.6 -> -3 (round away from zero)
1651    // Zero mantissa
1652    #[case(0, 1, 0)]
1653    #[case(0, 2, 0)]
1654    #[case(0, 5, 0)]
1655    // Large excess values
1656    #[case(123_456_789, 3, 123_457)] // 123456.789 -> 123457
1657    #[case(123_456_500, 3, 123_456)] // 123456.500 -> 123456 (half, even quotient)
1658    #[case(123_457_500, 3, 123_458)] // 123457.500 -> 123458 (half, odd quotient)
1659    #[case(100_005, 1, 10_000)] // 10000.5 -> 10000 (half, even quotient)
1660    #[case(100_015, 1, 10_002)] // 10001.5 -> 10002 (half, odd quotient)
1661    // Large mantissa values
1662    #[case(999_999_999_999_999_995, 1, 100_000_000_000_000_000)]
1663    #[case(1_000_000_000_000_000_005, 1, 100_000_000_000_000_000)]
1664    fn test_bankers_round(#[case] mantissa: i128, #[case] excess: u32, #[case] expected: i128) {
1665        assert_eq!(
1666            bankers_round(mantissa, excess),
1667            expected,
1668            "bankers_round({mantissa}, {excess}) expected {expected}"
1669        );
1670    }
1671
1672    // Symmetry: bankers_round(-x, e) == -bankers_round(x, e) for all positive x
1673    #[rstest]
1674    #[case(15, 1)]
1675    #[case(25, 1)]
1676    #[case(35, 1)]
1677    #[case(150, 2)]
1678    #[case(250, 2)]
1679    #[case(1500, 3)]
1680    #[case(2500, 3)]
1681    #[case(123_456_789, 3)]
1682    #[case(14, 1)]
1683    #[case(16, 1)]
1684    fn test_bankers_round_negative_symmetry(#[case] mantissa: i128, #[case] excess: u32) {
1685        assert_eq!(
1686            bankers_round(-mantissa, excess),
1687            -bankers_round(mantissa, excess),
1688            "Negative symmetry failed for mantissa={mantissa}, excess={excess}"
1689        );
1690    }
1691
1692    // Verify consistency with Rust Decimal's banker's rounding
1693    #[rstest]
1694    #[case("1.005", 2, "1.00")] // 0.005 remainder, even quotient -> truncate
1695    #[case("1.015", 2, "1.02")] // 0.005 remainder, odd quotient -> round up
1696    #[case("1.025", 2, "1.02")] // 0.005 remainder, even quotient -> truncate
1697    #[case("1.035", 2, "1.04")] // 0.005 remainder, odd quotient -> round up
1698    #[case("1.045", 2, "1.04")] // 0.005 remainder, even quotient -> truncate
1699    #[case("2.5", 0, "2")] // 0.5 remainder, even quotient -> truncate
1700    #[case("3.5", 0, "4")] // 0.5 remainder, odd quotient -> round up
1701    #[case("-2.5", 0, "-2")]
1702    #[case("-3.5", 0, "-4")]
1703    #[case("123.456", 2, "123.46")]
1704    #[case("123.455", 2, "123.46")] // Odd quotient at half
1705    #[case("123.445", 2, "123.44")] // Even quotient at half
1706    fn test_bankers_round_matches_decimal(
1707        #[case] input: &str,
1708        #[case] target_precision: u8,
1709        #[case] expected: &str,
1710    ) {
1711        let dec = Decimal::from_str(input).unwrap();
1712        let expected_dec = Decimal::from_str(expected).unwrap();
1713
1714        let decimal_rounded = dec.round_dp_with_strategy(
1715            u32::from(target_precision),
1716            RoundingStrategy::MidpointNearestEven,
1717        );
1718        assert_eq!(
1719            decimal_rounded, expected_dec,
1720            "Decimal rounding sanity check failed for {input}"
1721        );
1722
1723        let mantissa = dec.mantissa();
1724        let scale = dec.scale() as u8;
1725        let excess = u32::from(scale.saturating_sub(target_precision));
1726        if excess > 0 {
1727            let rounded = bankers_round(mantissa, excess);
1728
1729            // Reconstruct expected mantissa at target precision
1730            let expected_mantissa = expected_dec.mantissa();
1731            let expected_scale = expected_dec.scale() as u8;
1732            let scale_diff = u32::from(target_precision.saturating_sub(expected_scale));
1733            let normalized_expected = expected_mantissa * 10i128.pow(scale_diff);
1734
1735            assert_eq!(
1736                rounded, normalized_expected,
1737                "bankers_round disagrees with Decimal for {input} at precision {target_precision}"
1738            );
1739        }
1740    }
1741}
1742
1743#[cfg(test)]
1744mod correct_raw_tests {
1745    use rstest::rstest;
1746
1747    use super::*;
1748
1749    // All cases use precision = FIXED_PRECISION - 1 so the scale is 10 in both
1750    // standard-precision and high-precision modes.
1751
1752    #[rstest]
1753    #[case(0, 0)]
1754    #[case(10, 10)] // Already a multiple
1755    #[case(14, 10)] // Rounds down
1756    #[case(15, 20)] // Half rounds up
1757    #[case(16, 20)] // Rounds up
1758    #[case(u64::MAX, u64::MAX - 5)] // Rounding up would overflow; rounds down instead
1759    fn test_correct_raw_u64(#[case] raw: u64, #[case] expected: u64) {
1760        assert_eq!(correct_raw_u64(raw, FIXED_PRECISION - 1), expected);
1761    }
1762
1763    #[rstest]
1764    #[case(0, 0)]
1765    #[case(14, 10)]
1766    #[case(15, 20)]
1767    #[case(-14, -10)] // Rounds toward zero
1768    #[case(-15, -20)] // Half rounds away from zero
1769    #[case(-16, -20)] // Rounds away from zero
1770    #[case(i64::MAX, i64::MAX - 7)] // Rounding up would overflow; rounds down instead
1771    #[case(i64::MIN, i64::MIN + 8)] // Rounding down would overflow; rounds toward zero instead
1772    fn test_correct_raw_i64(#[case] raw: i64, #[case] expected: i64) {
1773        assert_eq!(correct_raw_i64(raw, FIXED_PRECISION - 1), expected);
1774    }
1775
1776    #[rstest]
1777    #[case(0, 0)]
1778    #[case(14, 10)]
1779    #[case(15, 20)]
1780    #[case(u128::MAX, u128::MAX - 5)] // Rounding up would overflow; rounds down instead
1781    fn test_correct_raw_u128(#[case] raw: u128, #[case] expected: u128) {
1782        assert_eq!(correct_raw_u128(raw, FIXED_PRECISION - 1), expected);
1783    }
1784
1785    #[rstest]
1786    #[case(0, 0)]
1787    #[case(14, 10)]
1788    #[case(15, 20)]
1789    #[case(-15, -20)]
1790    #[case(i128::MAX, i128::MAX - 7)] // Rounding up would overflow; rounds down instead
1791    #[case(i128::MIN, i128::MIN + 8)] // Rounding down would overflow; rounds toward zero instead
1792    fn test_correct_raw_i128(#[case] raw: i128, #[case] expected: i128) {
1793        assert_eq!(correct_raw_i128(raw, FIXED_PRECISION - 1), expected);
1794    }
1795
1796    #[rstest]
1797    fn test_correct_raw_identity_at_max_precision() {
1798        assert_eq!(correct_raw_u64(12_345, FIXED_PRECISION), 12_345);
1799        assert_eq!(correct_raw_i64(-12_345, FIXED_PRECISION), -12_345);
1800        assert_eq!(correct_raw_u128(12_345, FIXED_PRECISION), 12_345);
1801        assert_eq!(correct_raw_i128(-12_345, FIXED_PRECISION), -12_345);
1802    }
1803}