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