nautilus_binance/common/
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//! Fixed-point conversion utilities for Binance SBE mantissa/exponent values.
17//!
18//! Binance SBE responses encode numeric values as mantissa + exponent pairs.
19//! These utilities convert directly to Nautilus fixed-point types using pure
20//! integer arithmetic, avoiding floating-point precision loss.
21
22use nautilus_model::types::{
23    Price, Quantity, fixed::FIXED_PRECISION, price::PriceRaw, quantity::QuantityRaw,
24};
25
26/// Precomputed powers of 10 for efficient scaling (covers 0..=18).
27const POWERS_OF_10: [i128; 19] = [
28    1,
29    10,
30    100,
31    1_000,
32    10_000,
33    100_000,
34    1_000_000,
35    10_000_000,
36    100_000_000,
37    1_000_000_000,
38    10_000_000_000,
39    100_000_000_000,
40    1_000_000_000_000,
41    10_000_000_000_000,
42    100_000_000_000_000,
43    1_000_000_000_000_000,
44    10_000_000_000_000_000,
45    100_000_000_000_000_000,
46    1_000_000_000_000_000_000,
47];
48
49/// Returns 10^exp using precomputed table.
50#[inline]
51fn pow10(exp: u8) -> i128 {
52    POWERS_OF_10[exp as usize]
53}
54
55/// Scales a mantissa by 10^(FIXED_PRECISION + exponent) to produce a Nautilus raw value.
56///
57/// Uses i128 arithmetic internally to handle high-precision mode (FIXED_PRECISION=16).
58#[inline]
59fn scale_mantissa(mantissa: i64, exponent: i8) -> PriceRaw {
60    let scale_exp = FIXED_PRECISION as i8 + exponent;
61    let mantissa_wide = mantissa as i128;
62
63    let scaled = if scale_exp >= 0 {
64        mantissa_wide * pow10(scale_exp as u8)
65    } else {
66        mantissa_wide / pow10((-scale_exp) as u8)
67    };
68
69    scaled as PriceRaw
70}
71
72/// Converts a mantissa/exponent pair to a Nautilus [`Price`].
73///
74/// Uses pure integer arithmetic: `raw = mantissa * 10^(FIXED_PRECISION + exponent)`.
75#[must_use]
76pub fn mantissa_to_price(mantissa: i64, exponent: i8, precision: u8) -> Price {
77    let raw = scale_mantissa(mantissa, exponent);
78    Price::from_raw(raw, precision)
79}
80
81/// Converts a mantissa/exponent pair to a Nautilus [`Quantity`].
82///
83/// Uses pure integer arithmetic: `raw = mantissa * 10^(FIXED_PRECISION + exponent)`.
84///
85/// # Panics
86///
87/// Panics if `mantissa` is negative.
88#[must_use]
89pub fn mantissa_to_quantity(mantissa: i64, exponent: i8, precision: u8) -> Quantity {
90    assert!(mantissa >= 0, "Quantity cannot be negative: {mantissa}");
91    let raw = scale_mantissa(mantissa, exponent);
92    Quantity::from_raw(raw as QuantityRaw, precision)
93}
94
95/// Converts a mantissa/exponent pair to f64 for display/debugging only.
96///
97/// This should NOT be used for domain type conversion - use [`mantissa_to_price`]
98/// or [`mantissa_to_quantity`] instead.
99#[must_use]
100#[inline]
101pub fn mantissa_to_f64(mantissa: i64, exponent: i8) -> f64 {
102    mantissa as f64 * 10_f64.powi(exponent as i32)
103}
104
105#[cfg(test)]
106mod tests {
107    use nautilus_core::approx_eq;
108    use rstest::rstest;
109
110    use super::*;
111
112    #[rstest]
113    #[case(12345678, -8, 8, 0.12345678)]
114    #[case(9876543210, -8, 8, 98.7654321)]
115    #[case(100000000, -8, 8, 1.0)]
116    #[case(50000, -2, 2, 500.0)]
117    #[case(123, 0, 0, 123.0)]
118    fn test_mantissa_to_price(
119        #[case] mantissa: i64,
120        #[case] exponent: i8,
121        #[case] precision: u8,
122        #[case] expected: f64,
123    ) {
124        let price = mantissa_to_price(mantissa, exponent, precision);
125        assert!(
126            approx_eq!(f64, price.as_f64(), expected, epsilon = 1e-10),
127            "Expected {expected}, got {}",
128            price.as_f64()
129        );
130        assert_eq!(price.precision, precision);
131    }
132
133    #[rstest]
134    #[case(12345678, -8, 8, 0.12345678)]
135    #[case(100000000, -8, 8, 1.0)]
136    #[case(50000, -2, 2, 500.0)]
137    fn test_mantissa_to_quantity(
138        #[case] mantissa: i64,
139        #[case] exponent: i8,
140        #[case] precision: u8,
141        #[case] expected: f64,
142    ) {
143        let qty = mantissa_to_quantity(mantissa, exponent, precision);
144        assert!(
145            approx_eq!(f64, qty.as_f64(), expected, epsilon = 1e-10),
146            "Expected {expected}, got {}",
147            qty.as_f64()
148        );
149        assert_eq!(qty.precision, precision);
150    }
151
152    #[rstest]
153    fn test_mantissa_to_f64() {
154        assert!(approx_eq!(
155            f64,
156            mantissa_to_f64(12345678, -8),
157            0.12345678,
158            epsilon = 1e-15
159        ));
160        assert!(approx_eq!(
161            f64,
162            mantissa_to_f64(100, 2),
163            10000.0,
164            epsilon = 1e-10
165        ));
166    }
167
168    #[rstest]
169    #[should_panic(expected = "Quantity cannot be negative")]
170    fn test_negative_quantity_panics() {
171        let _ = mantissa_to_quantity(-100, 0, 0);
172    }
173}