Skip to main content

nautilus_model/types/
fixed.rs

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