Skip to main content

nautilus_serialization/arrow/display/
mod.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//! Display-mode Arrow encoders for Nautilus types.
17//!
18//! These encoders emit schemas suited to display pipelines rather than exact decimal analysis.
19//! Prices and quantities render as `Float64` via `.as_f64()`, `instrument_id` becomes a
20//! `Utf8` column rather than batch metadata (so mixed-instrument batches work), and
21//! timestamps render as `Timestamp(Nanosecond, Some("UTC"))` rather than `UInt64`.
22//!
23//! The conversion is lossy: precision metadata is discarded when values cast to `f64`.
24//! For catalog storage that must round-trip, use the `Decimal128` encoders in the parent
25//! [`crate::arrow`] module instead.
26
27pub mod account_state;
28pub mod bar;
29pub mod close;
30pub mod delta;
31pub mod depth;
32pub mod index_price;
33pub mod instrument;
34pub mod mark_price;
35pub mod order_filled;
36pub mod position;
37pub mod quote;
38pub mod report;
39pub mod trade;
40
41use arrow::datatypes::{DataType, Field};
42use nautilus_model::types::{Money, fixed::MAX_FLOAT_PRECISION};
43use rust_decimal::prelude::ToPrimitive;
44
45use super::display_conversion::DISPLAY_MAX_PRECISION;
46pub(super) use super::display_conversion::{
47    float64_field, price_to_f64, quantity_to_f64, timestamp_field, utf8_field,
48};
49
50/// Builds a `Boolean` field with the given name and nullability.
51pub(super) fn bool_field(name: &str, nullable: bool) -> Field {
52    Field::new(name, DataType::Boolean, nullable)
53}
54
55/// Builds a `UInt8` field with the given name and nullability.
56pub(super) fn uint8_field(name: &str, nullable: bool) -> Field {
57    Field::new(name, DataType::UInt8, nullable)
58}
59
60/// Builds a `UInt32` field with the given name and nullability.
61pub(super) fn uint32_field(name: &str, nullable: bool) -> Field {
62    Field::new(name, DataType::UInt32, nullable)
63}
64
65/// Builds a `UInt64` field with the given name and nullability.
66pub(super) fn uint64_field(name: &str, nullable: bool) -> Field {
67    Field::new(name, DataType::UInt64, nullable)
68}
69
70/// Converts a `u64` nanosecond timestamp to the `i64` expected by Arrow.
71///
72/// Nautilus timestamps fit comfortably in `i64`, but this clamps defensively
73/// to avoid an overflow panic on the cast.
74pub(super) fn unix_nanos_to_i64(value: u64) -> i64 {
75    i64::try_from(value).unwrap_or(i64::MAX)
76}
77
78/// Converts a [`Money`] amount to `f64` for display without panicking.
79///
80/// [`Money::as_f64`] panics under `feature = "defi"` when the currency
81/// precision exceeds [`MAX_FLOAT_PRECISION`] (16); high-precision tokens
82/// (e.g. 18-decimal ERC-20s) would otherwise abort an entire display batch.
83/// This guards pathological precisions and falls back to the decimal
84/// path for 17-18 decimal currencies. Returns [`f64::NAN`] if the value is
85/// outside `f64` range.
86pub(super) fn money_to_f64(money: &Money) -> f64 {
87    if money.currency.precision > DISPLAY_MAX_PRECISION {
88        return f64::NAN;
89    }
90
91    if money.currency.precision <= MAX_FLOAT_PRECISION {
92        money.as_f64()
93    } else {
94        money.as_decimal().to_f64().unwrap_or(f64::NAN)
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use nautilus_model::types::{
101        Currency, Money, Price, Quantity,
102        price::{ERROR_PRICE, PRICE_ERROR, PRICE_UNDEF},
103        quantity::QUANTITY_UNDEF,
104    };
105    use rstest::rstest;
106
107    use super::{money_to_f64, price_to_f64, quantity_to_f64};
108
109    #[rstest]
110    fn test_price_to_f64_normal_value() {
111        let price = Price::from("100.10");
112        assert!((price_to_f64(&price) - 100.10).abs() < 1e-9);
113    }
114
115    #[rstest]
116    fn test_price_to_f64_undef_sentinel_is_nan() {
117        let price = Price::from_raw(PRICE_UNDEF, 0);
118        assert!(price_to_f64(&price).is_nan());
119    }
120
121    #[rstest]
122    fn test_price_to_f64_error_sentinel_is_nan() {
123        let price = Price::from_raw(PRICE_ERROR, 0);
124        assert!(price_to_f64(&price).is_nan());
125    }
126
127    #[rstest]
128    fn test_price_to_f64_error_price_constant_is_nan() {
129        // ERROR_PRICE has precision = 255; must not panic and must emit NaN
130        assert!(price_to_f64(&ERROR_PRICE).is_nan());
131    }
132
133    #[rstest]
134    fn test_price_to_f64_wei_precision_boundary_is_finite() {
135        // Precision 18 is the upper bound for legitimate wei-precision inputs
136        // and must not be caught by the pathological-precision guard. Set precision
137        // after construction so the test runs across all feature combinations.
138        let mut price = Price::from_raw(1_000_000_000_000_000_000, 0);
139        price.precision = 18;
140        let value = price_to_f64(&price);
141
142        assert!(value.is_finite(), "precision 18 should not return NaN");
143        assert!((value - 1.0).abs() < 1e-9);
144    }
145
146    #[rstest]
147    fn test_quantity_to_f64_normal_value() {
148        let quantity = Quantity::from(1_000);
149        assert!((quantity_to_f64(&quantity) - 1_000.0).abs() < 1e-9);
150    }
151
152    #[rstest]
153    fn test_quantity_to_f64_undef_sentinel_is_nan() {
154        let quantity = Quantity::from_raw(QUANTITY_UNDEF, 0);
155        assert!(quantity_to_f64(&quantity).is_nan());
156    }
157
158    #[rstest]
159    fn test_quantity_to_f64_pathological_precision_is_nan() {
160        // Mirrors the `ERROR_PRICE` guard for `price_to_f64`: any precision
161        // beyond `DISPLAY_MAX_PRECISION` (18) must emit NaN rather than
162        // panic or render a bogus value.
163        let mut quantity = Quantity::zero(0);
164        quantity.precision = 200;
165        assert!(quantity_to_f64(&quantity).is_nan());
166    }
167
168    #[rstest]
169    fn test_money_to_f64_normal_value() {
170        let money = Money::new(123.45, Currency::USD());
171        assert!((money_to_f64(&money) - 123.45).abs() < 1e-9);
172    }
173
174    #[rstest]
175    fn test_money_to_f64_pathological_precision_is_nan() {
176        // Simulates a DeFi currency whose precision exceeds DISPLAY_MAX_PRECISION
177        // so Money::as_f64 would panic under feature = "defi"; we mutate precision
178        // directly since Currency::new rejects values above FIXED_PRECISION.
179        let mut money = Money::new(0.0, Currency::USD());
180        money.currency.precision = 200;
181        assert!(money_to_f64(&money).is_nan());
182    }
183}