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