Skip to main content

nautilus_persistence/
test_data.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//! Rust custom data types used for catalog roundtrip testing.
17//!
18//! Exposed to Python via the persistence PyO3 module so Python tests can exercise
19//! custom data write/query roundtrips.
20
21use std::collections::HashMap;
22
23use indexmap::IndexMap;
24use nautilus_core::{Params, UnixNanos};
25use nautilus_model::{
26    data::BarType,
27    identifiers::{AccountId, InstrumentId},
28    types::{Currency, Money, Price, Quantity},
29};
30use nautilus_persistence_macros::custom_data;
31
32/// A simple Rust custom data type for roundtrip testing.
33///
34/// Used in persistence integration tests (`test_catalog.rs`) and Python roundtrip tests.
35/// Tests call `ensure_custom_data_registered::<RustTestCustomData>()` before using the catalog.
36#[cfg_attr(
37    feature = "python",
38    expect(
39        clippy::unsafe_derive_deserialize,
40        reason = "test data uses the custom data macro output under test"
41    )
42)]
43#[custom_data(pyo3)]
44pub struct RustTestCustomData {
45    pub instrument_id: InstrumentId,
46    pub value: f64,
47    pub flag: bool,
48    pub ts_event: UnixNanos,
49    pub ts_init: UnixNanos,
50}
51
52/// Rust custom data type that exercises raw byte field support.
53#[custom_data]
54pub struct RustTestBytesCustomData {
55    pub value: Vec<u8>,
56    pub ts_event: UnixNanos,
57    pub ts_init: UnixNanos,
58}
59
60/// YieldCurveData-equivalent custom data type using the macro with `Vec<f64>` fields.
61///
62/// Tests `Vec<f64>` / `ListFloat64` support. Exposed to Python for roundtrip tests.
63#[cfg_attr(
64    feature = "python",
65    expect(
66        clippy::unsafe_derive_deserialize,
67        reason = "test data uses the custom data macro output under test"
68    )
69)]
70#[custom_data(pyo3)]
71pub struct MacroYieldCurveData {
72    pub curve_name: String,
73    pub tenors: Vec<f64>,
74    pub interest_rates: Vec<f64>,
75    pub ts_event: UnixNanos,
76    pub ts_init: UnixNanos,
77}
78
79/// Rust custom data type that exercises `Params` field support in the macro.
80#[cfg_attr(
81    feature = "python",
82    expect(
83        clippy::unsafe_derive_deserialize,
84        reason = "test data uses the custom data macro output under test"
85    )
86)]
87#[custom_data(pyo3)]
88pub struct RustTestParamsCustomData {
89    pub name: String,
90    pub params: Params,
91    pub ts_event: UnixNanos,
92    pub ts_init: UnixNanos,
93}
94
95/// Rust custom data type that exercises typed map field support in the macro.
96#[cfg_attr(
97    feature = "python",
98    expect(
99        clippy::unsafe_derive_deserialize,
100        reason = "test data uses the custom data macro output under test"
101    )
102)]
103#[custom_data(pyo3)]
104pub struct RustTestPriceMapCustomData {
105    pub name: String,
106    #[custom_data_field(serde)]
107    pub prices: IndexMap<InstrumentId, Price>,
108    pub ts_event: UnixNanos,
109    pub ts_init: UnixNanos,
110}
111
112/// Rust custom data type that exercises typed JSON map values across PyO3-supported types.
113#[cfg_attr(
114    feature = "python",
115    expect(
116        clippy::unsafe_derive_deserialize,
117        reason = "test data uses the custom data macro output under test"
118    )
119)]
120#[custom_data(pyo3)]
121pub struct RustTestTypedMapCustomData {
122    pub name: String,
123    #[custom_data_field(serde)]
124    pub instrument_ids: IndexMap<String, InstrumentId>,
125    #[custom_data_field(serde)]
126    pub account_ids: IndexMap<String, AccountId>,
127    #[custom_data_field(serde)]
128    pub currencies: IndexMap<String, Currency>,
129    #[custom_data_field(serde)]
130    pub bar_types: IndexMap<String, BarType>,
131    #[custom_data_field(serde)]
132    pub prices: IndexMap<String, Price>,
133    #[custom_data_field(serde)]
134    pub quantities: IndexMap<String, Quantity>,
135    #[custom_data_field(serde)]
136    pub monies: IndexMap<String, Money>,
137    #[custom_data_field(serde)]
138    pub prices_by_instrument: IndexMap<InstrumentId, Price>,
139    #[custom_data_field(serde)]
140    pub quantities_by_account: IndexMap<AccountId, Quantity>,
141    #[custom_data_field(serde)]
142    pub monies_by_currency: IndexMap<Currency, Money>,
143    #[custom_data_field(serde)]
144    pub prices_by_bar_type: IndexMap<BarType, Price>,
145    #[custom_data_field(serde)]
146    pub hash_prices_by_instrument: HashMap<InstrumentId, Price>,
147    #[custom_data_field(serde)]
148    pub strings: HashMap<String, String>,
149    #[custom_data_field(serde)]
150    pub floats_64: HashMap<String, f64>,
151    #[custom_data_field(serde)]
152    pub floats_32: HashMap<String, f32>,
153    #[custom_data_field(serde)]
154    pub booleans: HashMap<String, bool>,
155    #[custom_data_field(serde)]
156    pub integers_u64: HashMap<String, u64>,
157    #[custom_data_field(serde)]
158    pub integers_i64: HashMap<String, i64>,
159    #[custom_data_field(serde)]
160    pub integers_u32: HashMap<String, u32>,
161    #[custom_data_field(serde)]
162    pub integers_i32: HashMap<String, i32>,
163    pub ts_event: UnixNanos,
164    pub ts_init: UnixNanos,
165}
166
167/// Rust custom data type that exercises generic JSON map field support.
168#[custom_data]
169pub struct RustTestHashMapCustomData {
170    pub name: String,
171    #[custom_data_field(serde)]
172    pub prices: HashMap<String, Price>,
173    pub ts_event: UnixNanos,
174    pub ts_init: UnixNanos,
175}
176
177/// Plain Serde enum stored inside custom data as a field.
178#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
179pub enum RustTestSerdeFieldKind {
180    Alpha,
181    Beta { count: u64 },
182}
183
184/// Plain Serde payload stored inside custom data without its own timestamps.
185#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
186pub struct RustTestSerdeFieldPayload {
187    pub kind: RustTestSerdeFieldKind,
188    pub label: String,
189    pub values: Vec<f64>,
190}
191
192/// Rust custom data type that exercises arbitrary Serde field support.
193#[custom_data]
194pub struct RustTestSerdeFieldCustomData {
195    pub name: String,
196    #[custom_data_field(serde)]
197    pub payload: RustTestSerdeFieldPayload,
198    pub ts_event: UnixNanos,
199    pub ts_init: UnixNanos,
200}
201
202#[cfg(test)]
203mod tests {
204    use arrow::datatypes::DataType;
205    use nautilus_serialization::arrow::{
206        ArrowSchemaProvider, DecodeDataFromRecordBatch, EncodeToRecordBatch,
207    };
208    use rstest::rstest;
209
210    use super::*;
211
212    #[rstest]
213    fn test_macro_yield_curve_data_schema_has_ts_init() {
214        let schema = MacroYieldCurveData::get_schema(None);
215        let field_names: Vec<_> = schema.fields().iter().map(|f| f.name().clone()).collect();
216        assert!(
217            field_names.iter().any(|f| f == "ts_init"),
218            "Schema must have ts_init for DataFusion ORDER BY; got: {field_names:?}",
219        );
220        assert!(
221            field_names.iter().any(|f| f == "ts_event"),
222            "Schema must have ts_event; got: {field_names:?}",
223        );
224    }
225
226    #[rstest]
227    fn test_rust_test_params_custom_data_schema_uses_utf8_for_params() {
228        let schema = RustTestParamsCustomData::get_schema(None);
229        let params_field = schema.field_with_name("params").unwrap();
230
231        assert_eq!(params_field.data_type(), &DataType::Utf8);
232    }
233
234    #[rstest]
235    fn test_rust_test_price_map_custom_data_schema_uses_utf8_for_prices() {
236        let schema = RustTestPriceMapCustomData::get_schema(None);
237        let prices_field = schema.field_with_name("prices").unwrap();
238
239        assert_eq!(prices_field.data_type(), &DataType::Utf8);
240    }
241
242    #[rstest]
243    fn test_rust_test_hash_map_custom_data_schema_uses_utf8_for_prices() {
244        let schema = RustTestHashMapCustomData::get_schema(None);
245        let prices_field = schema.field_with_name("prices").unwrap();
246
247        assert_eq!(prices_field.data_type(), &DataType::Utf8);
248    }
249
250    #[rstest]
251    fn test_rust_test_serde_field_custom_data_schema_uses_utf8_for_payload() {
252        let schema = RustTestSerdeFieldCustomData::get_schema(None);
253        let payload_field = schema.field_with_name("payload").unwrap();
254
255        assert_eq!(payload_field.data_type(), &DataType::Utf8);
256    }
257
258    #[rstest]
259    fn test_rust_test_serde_field_custom_data_roundtrip_decodes_exact_payload() {
260        let original = RustTestSerdeFieldCustomData {
261            name: "serde-field".to_string(),
262            payload: RustTestSerdeFieldPayload {
263                kind: RustTestSerdeFieldKind::Beta { count: 7 },
264                label: "payload".to_string(),
265                values: vec![1.0, 2.0, 3.0],
266            },
267            ts_event: UnixNanos::from(10),
268            ts_init: UnixNanos::from(11),
269        };
270        let metadata = original.metadata();
271        let batch =
272            RustTestSerdeFieldCustomData::encode_batch(&metadata, std::slice::from_ref(&original))
273                .unwrap();
274        let decoded = RustTestSerdeFieldCustomData::decode_data_batch(&metadata, batch).unwrap();
275
276        assert_eq!(decoded.len(), 1);
277        let decoded =
278            RustTestSerdeFieldCustomData::try_from(decoded.into_iter().next().unwrap()).unwrap();
279        assert_eq!(decoded, original);
280    }
281
282    #[rstest]
283    fn test_rust_test_typed_map_custom_data_schema_uses_utf8_for_json_maps() {
284        let schema = RustTestTypedMapCustomData::get_schema(None);
285
286        for field_name in [
287            "instrument_ids",
288            "account_ids",
289            "currencies",
290            "bar_types",
291            "prices",
292            "quantities",
293            "monies",
294            "prices_by_instrument",
295            "quantities_by_account",
296            "monies_by_currency",
297            "prices_by_bar_type",
298            "hash_prices_by_instrument",
299            "strings",
300            "floats_64",
301            "floats_32",
302            "booleans",
303            "integers_u64",
304            "integers_i64",
305            "integers_u32",
306            "integers_i32",
307        ] {
308            let field = schema.field_with_name(field_name).unwrap();
309            assert_eq!(field.data_type(), &DataType::Utf8);
310        }
311    }
312}