Skip to main content

nautilus_model/types/
fixed.rs

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