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: [i64; 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) -> i64 {
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#[inline]
57fn scale_mantissa(mantissa: i64, exponent: i8) -> i64 {
58    let scale_exp = FIXED_PRECISION as i8 + exponent;
59
60    if scale_exp >= 0 {
61        mantissa
62            .checked_mul(pow10(scale_exp as u8))
63            .expect("Overflow scaling mantissa")
64    } else {
65        // Scale down (divide) - may lose precision if scale_exp is very negative
66        mantissa / pow10((-scale_exp) as u8)
67    }
68}
69
70/// Converts a mantissa/exponent pair to a Nautilus [`Price`].
71///
72/// Uses pure integer arithmetic: `raw = mantissa * 10^(FIXED_PRECISION + exponent)`.
73///
74/// # Panics
75///
76/// Panics if the scaled value overflows i64.
77#[must_use]
78pub fn mantissa_to_price(mantissa: i64, exponent: i8, precision: u8) -> Price {
79    let raw = scale_mantissa(mantissa, exponent);
80    Price::from_raw(raw as PriceRaw, precision)
81}
82
83/// Converts a mantissa/exponent pair to a Nautilus [`Quantity`].
84///
85/// Uses pure integer arithmetic: `raw = mantissa * 10^(FIXED_PRECISION + exponent)`.
86///
87/// # Panics
88///
89/// - Panics if the scaled value overflows i64.
90/// - Panics if `mantissa` is negative.
91#[must_use]
92pub fn mantissa_to_quantity(mantissa: i64, exponent: i8, precision: u8) -> Quantity {
93    let raw = scale_mantissa(mantissa, exponent);
94    assert!(raw >= 0, "Quantity cannot be negative: {raw}");
95    Quantity::from_raw(raw as QuantityRaw, precision)
96}
97
98/// Converts a mantissa/exponent pair to f64 for display/debugging only.
99///
100/// This should NOT be used for domain type conversion - use [`mantissa_to_price`]
101/// or [`mantissa_to_quantity`] instead.
102#[must_use]
103#[inline]
104pub fn mantissa_to_f64(mantissa: i64, exponent: i8) -> f64 {
105    mantissa as f64 * 10_f64.powi(exponent as i32)
106}
107
108#[cfg(test)]
109mod tests {
110    use nautilus_core::approx_eq;
111    use rstest::rstest;
112
113    use super::*;
114
115    #[rstest]
116    #[case(12345678, -8, 8, 0.12345678)]
117    #[case(9876543210, -8, 8, 98.7654321)]
118    #[case(100000000, -8, 8, 1.0)]
119    #[case(50000, -2, 2, 500.0)]
120    #[case(123, 0, 0, 123.0)]
121    fn test_mantissa_to_price(
122        #[case] mantissa: i64,
123        #[case] exponent: i8,
124        #[case] precision: u8,
125        #[case] expected: f64,
126    ) {
127        let price = mantissa_to_price(mantissa, exponent, precision);
128        assert!(
129            approx_eq!(f64, price.as_f64(), expected, epsilon = 1e-10),
130            "Expected {expected}, got {}",
131            price.as_f64()
132        );
133        assert_eq!(price.precision, precision);
134    }
135
136    #[rstest]
137    #[case(12345678, -8, 8, 0.12345678)]
138    #[case(100000000, -8, 8, 1.0)]
139    #[case(50000, -2, 2, 500.0)]
140    fn test_mantissa_to_quantity(
141        #[case] mantissa: i64,
142        #[case] exponent: i8,
143        #[case] precision: u8,
144        #[case] expected: f64,
145    ) {
146        let qty = mantissa_to_quantity(mantissa, exponent, precision);
147        assert!(
148            approx_eq!(f64, qty.as_f64(), expected, epsilon = 1e-10),
149            "Expected {expected}, got {}",
150            qty.as_f64()
151        );
152        assert_eq!(qty.precision, precision);
153    }
154
155    #[rstest]
156    fn test_mantissa_to_f64() {
157        assert!(approx_eq!(
158            f64,
159            mantissa_to_f64(12345678, -8),
160            0.12345678,
161            epsilon = 1e-15
162        ));
163        assert!(approx_eq!(
164            f64,
165            mantissa_to_f64(100, 2),
166            10000.0,
167            epsilon = 1e-10
168        ));
169    }
170
171    #[rstest]
172    #[should_panic(expected = "Quantity cannot be negative")]
173    fn test_negative_quantity_panics() {
174        let _ = mantissa_to_quantity(-100, 0, 0);
175    }
176}