nautilus_model/data/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! Data types for the trading domain model.
17
18pub mod bar;
19pub mod bet;
20pub mod delta;
21pub mod deltas;
22pub mod depth;
23pub mod greeks;
24pub mod order;
25pub mod quote;
26pub mod status;
27pub mod trade;
28
29#[cfg(feature = "stubs")]
30pub mod stubs;
31
32use std::{
33    fmt::{Debug, Display},
34    hash::{Hash, Hasher},
35    num::NonZeroU64,
36    str::FromStr,
37};
38
39use indexmap::IndexMap;
40use nautilus_core::UnixNanos;
41use serde::{Deserialize, Serialize};
42use serde_json::to_string;
43
44// Re-exports
45#[rustfmt::skip]  // Keep these grouped
46pub use bar::{Bar, BarSpecification, BarType};
47pub use delta::OrderBookDelta;
48pub use deltas::{OrderBookDeltas, OrderBookDeltas_API};
49pub use depth::{OrderBookDepth10, DEPTH10_LEN};
50pub use greeks::{black_scholes_greeks, BlackScholesGreeksResult};
51pub use order::{BookOrder, NULL_ORDER};
52pub use quote::QuoteTick;
53pub use status::InstrumentStatus;
54pub use trade::TradeTick;
55
56use crate::{
57    enums::BookType,
58    identifiers::{InstrumentId, Venue},
59};
60
61/// A built-in Nautilus data type.
62///
63/// Not recommended for storing large amounts of data, as the largest variant is significantly
64/// larger (10x) than the smallest.
65#[repr(C)]
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67#[allow(clippy::large_enum_variant)]
68pub enum Data {
69    Delta(OrderBookDelta),
70    Deltas(OrderBookDeltas_API),
71    Depth10(OrderBookDepth10), // This variant is significantly larger
72    Quote(QuoteTick),
73    Trade(TradeTick),
74    Bar(Bar),
75}
76
77macro_rules! impl_try_from_data {
78    ($variant:ident, $type:ty) => {
79        impl TryFrom<Data> for $type {
80            type Error = ();
81
82            fn try_from(value: Data) -> Result<Self, Self::Error> {
83                match value {
84                    Data::$variant(x) => Ok(x),
85                    _ => Err(()),
86                }
87            }
88        }
89    };
90}
91
92impl_try_from_data!(Delta, OrderBookDelta);
93impl_try_from_data!(Deltas, OrderBookDeltas_API);
94impl_try_from_data!(Depth10, OrderBookDepth10);
95impl_try_from_data!(Quote, QuoteTick);
96impl_try_from_data!(Trade, TradeTick);
97impl_try_from_data!(Bar, Bar);
98
99pub fn to_variant<T: TryFrom<Data>>(data: Vec<Data>) -> Vec<T> {
100    data.into_iter()
101        .filter_map(|d| T::try_from(d).ok())
102        .collect()
103}
104
105impl Data {
106    /// Returns the instrument ID for the data.
107    pub fn instrument_id(&self) -> InstrumentId {
108        match self {
109            Self::Delta(delta) => delta.instrument_id,
110            Self::Deltas(deltas) => deltas.instrument_id,
111            Self::Depth10(depth) => depth.instrument_id,
112            Self::Quote(quote) => quote.instrument_id,
113            Self::Trade(trade) => trade.instrument_id,
114            Self::Bar(bar) => bar.bar_type.instrument_id(),
115        }
116    }
117
118    /// Returns whether the data is a type of order book data.
119    pub fn is_order_book_data(&self) -> bool {
120        matches!(self, Self::Delta(_) | Self::Deltas(_) | Self::Depth10(_))
121    }
122}
123
124pub trait GetTsInit {
125    fn ts_init(&self) -> UnixNanos;
126}
127
128impl GetTsInit for Data {
129    fn ts_init(&self) -> UnixNanos {
130        match self {
131            Self::Delta(d) => d.ts_init,
132            Self::Deltas(d) => d.ts_init,
133            Self::Depth10(d) => d.ts_init,
134            Self::Quote(q) => q.ts_init,
135            Self::Trade(t) => t.ts_init,
136            Self::Bar(b) => b.ts_init,
137        }
138    }
139}
140
141pub fn is_monotonically_increasing_by_init<T: GetTsInit>(data: &[T]) -> bool {
142    data.windows(2)
143        .all(|window| window[0].ts_init() <= window[1].ts_init())
144}
145
146impl From<OrderBookDelta> for Data {
147    fn from(value: OrderBookDelta) -> Self {
148        Self::Delta(value)
149    }
150}
151
152impl From<OrderBookDeltas_API> for Data {
153    fn from(value: OrderBookDeltas_API) -> Self {
154        Self::Deltas(value)
155    }
156}
157
158impl From<OrderBookDepth10> for Data {
159    fn from(value: OrderBookDepth10) -> Self {
160        Self::Depth10(value)
161    }
162}
163
164impl From<QuoteTick> for Data {
165    fn from(value: QuoteTick) -> Self {
166        Self::Quote(value)
167    }
168}
169
170impl From<TradeTick> for Data {
171    fn from(value: TradeTick) -> Self {
172        Self::Trade(value)
173    }
174}
175
176impl From<Bar> for Data {
177    fn from(value: Bar) -> Self {
178        Self::Bar(value)
179    }
180}
181
182// TODO: https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
183// i128 and u128 is now FFI compatible. However, since the clippy lint
184// hasn't been removed yet. We'll suppress with #[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
185#[no_mangle]
186#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
187pub extern "C" fn data_clone(data: &Data) -> Data {
188    // Dummy function for cbindgen to export types
189    data.clone()
190}
191
192/// Represents a data type including metadata.
193#[derive(Clone, Serialize, Deserialize)]
194#[cfg_attr(
195    feature = "python",
196    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
197)]
198pub struct DataType {
199    type_name: String,
200    metadata: Option<IndexMap<String, String>>,
201    topic: String,
202    hash: u64,
203}
204
205impl DataType {
206    /// Creates a new [`DataType`] instance.
207    pub fn new(type_name: &str, metadata: Option<IndexMap<String, String>>) -> Self {
208        // Precompute topic
209        let topic = if let Some(ref meta) = metadata {
210            let meta_str = meta
211                .iter()
212                .map(|(k, v)| format!("{}={}", k, v))
213                .collect::<Vec<_>>()
214                .join(".");
215            format!("{}.{}", type_name, meta_str)
216        } else {
217            type_name.to_string()
218        };
219
220        // Precompute hash
221        let mut hasher = std::collections::hash_map::DefaultHasher::new();
222        topic.hash(&mut hasher);
223
224        Self {
225            type_name: type_name.to_owned(),
226            metadata,
227            topic,
228            hash: hasher.finish(),
229        }
230    }
231
232    /// Returns the type name for the data type.
233    pub fn type_name(&self) -> &str {
234        self.type_name.as_str()
235    }
236
237    /// Returns the metadata for the data type.
238    pub fn metadata(&self) -> Option<&IndexMap<String, String>> {
239        self.metadata.as_ref()
240    }
241
242    /// Returns a string representation of the metadata.
243    pub fn metadata_str(&self) -> String {
244        self.metadata
245            .as_ref()
246            .map(|metadata| to_string(metadata).unwrap_or_default())
247            .unwrap_or_else(|| "null".to_string())
248    }
249
250    /// Returns the messaging topic for the data type.
251    pub fn topic(&self) -> &str {
252        self.topic.as_str()
253    }
254
255    /// Returns an [`Option<InstrumentId>`] from the metadata.
256    ///
257    /// # Panics
258    ///
259    /// This function panics:
260    /// - If there is no metadata.
261    /// - If the `instrument_id` value contained in the metadata is invalid.
262    pub fn instrument_id(&self) -> Option<InstrumentId> {
263        let metadata = self.metadata.as_ref().expect("metadata was `None`");
264        let instrument_id = metadata.get("instrument_id")?;
265        Some(
266            InstrumentId::from_str(instrument_id)
267                .expect("Invalid `InstrumentId` for 'instrument_id'"),
268        )
269    }
270
271    /// Returns an [`Option<Venue>`] from the metadata.
272    ///
273    /// # Panics
274    ///
275    /// This function panics:
276    /// - If there is no metadata.
277    /// - If the `venue` value contained in the metadata is invalid.
278    pub fn venue(&self) -> Option<Venue> {
279        let metadata = self.metadata.as_ref().expect("metadata was `None`");
280        let venue_str = metadata.get("venue")?;
281        Some(Venue::from(venue_str.as_str()))
282    }
283
284    /// Returns a [`BarType`] from the metadata.
285    ///
286    /// # Panics
287    ///
288    /// This function panics:
289    /// - If there is no metadata.
290    /// - If the metadata does not contain a `bar_type`.
291    /// - If the `bar_type` value contained in the metadata is invalid.
292    pub fn bar_type(&self) -> BarType {
293        let metadata = self.metadata.as_ref().expect("metadata was `None`");
294        let bar_type_str = metadata
295            .get("bar_type")
296            .expect("No 'bar_type' found in metadata");
297        BarType::from_str(bar_type_str).expect("Invalid `BarType` for 'bar_type'")
298    }
299
300    /// Returns an [`Option<UnixNanos>`] start from the metadata.
301    ///
302    /// # Panics
303    ///
304    /// This function panics:
305    /// - If there is no metadata.
306    /// - If the `start` value contained in the metadata is invalid.
307    pub fn start(&self) -> Option<UnixNanos> {
308        let metadata = self.metadata.as_ref()?;
309        let start_str = metadata.get("start")?;
310        Some(UnixNanos::from_str(start_str).expect("Invalid `UnixNanos` for 'start'"))
311    }
312
313    /// Returns an [`Option<UnixNanos>`] end from the metadata.
314    ///
315    /// # Panics
316    ///
317    /// This function panics:
318    /// - If there is no metadata.
319    /// - If the `end` value contained in the metadata is invalid.
320    pub fn end(&self) -> Option<UnixNanos> {
321        let metadata = self.metadata.as_ref()?;
322        let end_str = metadata.get("end")?;
323        Some(UnixNanos::from_str(end_str).expect("Invalid `UnixNanos` for 'end'"))
324    }
325
326    /// Returns an [`Option<usize>`] limit from the metadata.
327    ///
328    /// # Panics
329    ///
330    /// This function panics:
331    /// - If there is no metadata.
332    /// - If the `limit` value contained in the metadata is invalid.
333    pub fn limit(&self) -> Option<usize> {
334        let metadata = self.metadata.as_ref()?;
335        let depth_str = metadata.get("limit")?;
336        Some(
337            depth_str
338                .parse::<usize>()
339                .expect("Invalid `usize` for 'limit'"),
340        )
341    }
342
343    /// Returns a [`BookType`] from the metadata.
344    ///
345    /// # Panics
346    ///
347    /// This function panics:
348    /// - If there is no metadata.
349    /// - If the `book_type` value contained in the metadata is invalid.
350    pub fn book_type(&self) -> BookType {
351        let metadata = self.metadata.as_ref().expect("metadata was `None`");
352        let book_type_str = metadata
353            .get("book_type")
354            .expect("'book_type' not found in metadata");
355        BookType::from_str(book_type_str).expect("Invalid `BookType` for 'book_type'")
356    }
357
358    /// Returns an [`Option<usize>`] depth from the metadata.
359    ///
360    /// # Panics
361    ///
362    /// This function panics:
363    /// - If there is no metadata.
364    /// - If the `depth` value contained in the metadata is invalid.
365    pub fn depth(&self) -> Option<usize> {
366        let metadata = self.metadata.as_ref()?;
367        let depth_str = metadata.get("depth")?;
368        Some(
369            depth_str
370                .parse::<usize>()
371                .expect("Invalid `usize` for 'depth'"),
372        )
373    }
374
375    /// Returns a [`NonZeroU64`] interval (milliseconds) from the metadata.
376    ///
377    /// # Panics
378    ///
379    /// This function panics:
380    /// - If there is no metadata.
381    /// - If the `interval_ms` value contained in the metadata is invalid.
382    pub fn interval_ms(&self) -> NonZeroU64 {
383        let metadata = self.metadata.as_ref().expect("metadata was `None`");
384        let interval_ms_str = metadata
385            .get("interval_ms")
386            .expect("No 'interval_ms' in metadata");
387
388        interval_ms_str
389            .parse::<NonZeroU64>()
390            .expect("Invalid `NonZeroU64` for 'interval_ms'")
391    }
392
393    /// Returns a [`bool`] from the metadata indicating whether the book should be managed.
394    ///
395    /// # Panics
396    ///
397    /// This function panics:
398    /// - If there is no metadata.
399    /// - If the `managed` value contained in the metadata is invalid.
400    pub fn managed(&self) -> bool {
401        let metadata = self.metadata.as_ref().expect("metadata was `None`");
402        let managed_str = metadata.get("managed").expect("No 'managed' in metadata");
403        managed_str
404            .parse::<bool>()
405            .expect("Invalid `bool` for 'managed'")
406    }
407}
408
409impl PartialEq for DataType {
410    fn eq(&self, other: &Self) -> bool {
411        self.topic == other.topic
412    }
413}
414
415impl Eq for DataType {}
416
417impl PartialOrd for DataType {
418    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
419        Some(self.cmp(other))
420    }
421}
422
423impl Ord for DataType {
424    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
425        self.topic.cmp(&other.topic)
426    }
427}
428
429impl Hash for DataType {
430    fn hash<H: Hasher>(&self, state: &mut H) {
431        self.hash.hash(state);
432    }
433}
434
435impl Display for DataType {
436    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437        write!(f, "{}", self.topic)
438    }
439}
440
441impl Debug for DataType {
442    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443        write!(
444            f,
445            "DataType(type_name={}, metadata={:?})",
446            self.type_name, self.metadata
447        )
448    }
449}
450
451////////////////////////////////////////////////////////////////////////////////
452// Tests
453////////////////////////////////////////////////////////////////////////////////
454#[cfg(test)]
455mod tests {
456    use std::hash::DefaultHasher;
457
458    use rstest::*;
459
460    use super::*;
461
462    #[rstest]
463    fn test_data_type_creation_with_metadata() {
464        let metadata = Some(
465            [
466                ("key1".to_string(), "value1".to_string()),
467                ("key2".to_string(), "value2".to_string()),
468            ]
469            .iter()
470            .cloned()
471            .collect(),
472        );
473        let data_type = DataType::new("ExampleType", metadata.clone());
474
475        assert_eq!(data_type.type_name(), "ExampleType");
476        assert_eq!(data_type.topic(), "ExampleType.key1=value1.key2=value2");
477        assert_eq!(data_type.metadata(), metadata.as_ref());
478    }
479
480    #[rstest]
481    fn test_data_type_creation_without_metadata() {
482        let data_type = DataType::new("ExampleType", None);
483
484        assert_eq!(data_type.type_name(), "ExampleType");
485        assert_eq!(data_type.topic(), "ExampleType");
486        assert_eq!(data_type.metadata(), None);
487    }
488
489    #[rstest]
490    fn test_data_type_equality() {
491        let metadata1 = Some(
492            [("key1".to_string(), "value1".to_string())]
493                .iter()
494                .cloned()
495                .collect(),
496        );
497        let metadata2 = Some(
498            [("key1".to_string(), "value1".to_string())]
499                .iter()
500                .cloned()
501                .collect(),
502        );
503
504        let data_type1 = DataType::new("ExampleType", metadata1);
505        let data_type2 = DataType::new("ExampleType", metadata2);
506
507        assert_eq!(data_type1, data_type2);
508    }
509
510    #[rstest]
511    fn test_data_type_inequality() {
512        let metadata1 = Some(
513            [("key1".to_string(), "value1".to_string())]
514                .iter()
515                .cloned()
516                .collect(),
517        );
518        let metadata2 = Some(
519            [("key2".to_string(), "value2".to_string())]
520                .iter()
521                .cloned()
522                .collect(),
523        );
524
525        let data_type1 = DataType::new("ExampleType", metadata1);
526        let data_type2 = DataType::new("ExampleType", metadata2);
527
528        assert_ne!(data_type1, data_type2);
529    }
530
531    #[rstest]
532    fn test_data_type_ordering() {
533        let metadata1 = Some(
534            [("key1".to_string(), "value1".to_string())]
535                .iter()
536                .cloned()
537                .collect(),
538        );
539        let metadata2 = Some(
540            [("key2".to_string(), "value2".to_string())]
541                .iter()
542                .cloned()
543                .collect(),
544        );
545
546        let data_type1 = DataType::new("ExampleTypeA", metadata1);
547        let data_type2 = DataType::new("ExampleTypeB", metadata2);
548
549        assert!(data_type1 < data_type2);
550    }
551
552    #[rstest]
553    fn test_data_type_hash() {
554        let metadata = Some(
555            [("key1".to_string(), "value1".to_string())]
556                .iter()
557                .cloned()
558                .collect(),
559        );
560
561        let data_type1 = DataType::new("ExampleType", metadata.clone());
562        let data_type2 = DataType::new("ExampleType", metadata.clone());
563
564        let mut hasher1 = DefaultHasher::new();
565        data_type1.hash(&mut hasher1);
566        let hash1 = hasher1.finish();
567
568        let mut hasher2 = DefaultHasher::new();
569        data_type2.hash(&mut hasher2);
570        let hash2 = hasher2.finish();
571
572        assert_eq!(hash1, hash2);
573    }
574
575    #[test]
576    fn test_data_type_display() {
577        let metadata = Some(
578            [("key1".to_string(), "value1".to_string())]
579                .iter()
580                .cloned()
581                .collect(),
582        );
583        let data_type = DataType::new("ExampleType", metadata);
584
585        assert_eq!(format!("{}", data_type), "ExampleType.key1=value1");
586    }
587
588    #[test]
589    fn test_data_type_debug() {
590        let metadata = Some(
591            [("key1".to_string(), "value1".to_string())]
592                .iter()
593                .cloned()
594                .collect(),
595        );
596        let data_type = DataType::new("ExampleType", metadata.clone());
597
598        assert_eq!(
599            format!("{:?}", data_type),
600            format!("DataType(type_name=ExampleType, metadata={:?})", metadata)
601        );
602    }
603
604    #[rstest]
605    fn test_parse_instrument_id_from_metadata() {
606        let instrument_id_str = "MSFT.XNAS";
607        let metadata = Some(
608            [("instrument_id".to_string(), instrument_id_str.to_string())]
609                .iter()
610                .cloned()
611                .collect(),
612        );
613        let data_type = DataType::new("InstrumentAny", metadata);
614
615        assert_eq!(
616            data_type.instrument_id().unwrap(),
617            InstrumentId::from_str(instrument_id_str).unwrap()
618        );
619    }
620
621    #[rstest]
622    fn test_parse_venue_from_metadata() {
623        let venue_str = "BINANCE";
624        let metadata = Some(
625            [("venue".to_string(), venue_str.to_string())]
626                .iter()
627                .cloned()
628                .collect(),
629        );
630        let data_type = DataType::new(stringify!(InstrumentAny), metadata);
631
632        assert_eq!(data_type.venue().unwrap(), Venue::new(venue_str));
633    }
634
635    #[rstest]
636    fn test_parse_bar_type_from_metadata() {
637        let bar_type_str = "MSFT.XNAS-1000-TICK-LAST-INTERNAL";
638        let metadata = Some(
639            [("bar_type".to_string(), bar_type_str.to_string())]
640                .iter()
641                .cloned()
642                .collect(),
643        );
644        let data_type = DataType::new(stringify!(BarType), metadata);
645
646        assert_eq!(
647            data_type.bar_type(),
648            BarType::from_str(bar_type_str).unwrap()
649        );
650    }
651
652    #[rstest]
653    fn test_parse_start_from_metadata() {
654        let start_ns = 1600054595844758000;
655        let metadata = Some(
656            [("start".to_string(), start_ns.to_string())]
657                .iter()
658                .cloned()
659                .collect(),
660        );
661        let data_type = DataType::new(stringify!(TradeTick), metadata);
662
663        assert_eq!(data_type.start().unwrap(), UnixNanos::from(start_ns),);
664    }
665
666    #[rstest]
667    fn test_parse_end_from_metadata() {
668        let end_ns = 1720954595844758000;
669        let metadata = Some(
670            [("end".to_string(), end_ns.to_string())]
671                .iter()
672                .cloned()
673                .collect(),
674        );
675        let data_type = DataType::new(stringify!(TradeTick), metadata);
676
677        assert_eq!(data_type.end().unwrap(), UnixNanos::from(end_ns),);
678    }
679
680    #[rstest]
681    fn test_parse_limit_from_metadata() {
682        let limit = 1000;
683        let metadata = Some(
684            [("limit".to_string(), limit.to_string())]
685                .iter()
686                .cloned()
687                .collect(),
688        );
689        let data_type = DataType::new(stringify!(TradeTick), metadata);
690
691        assert_eq!(data_type.limit().unwrap(), limit);
692    }
693
694    #[rstest]
695    fn test_parse_book_type_from_metadata() {
696        let book_type_str = "L3_MBO";
697        let metadata = Some(
698            [("book_type".to_string(), book_type_str.to_string())]
699                .iter()
700                .cloned()
701                .collect(),
702        );
703        let data_type = DataType::new(stringify!(OrderBookDelta), metadata);
704
705        assert_eq!(
706            data_type.book_type(),
707            BookType::from_str(book_type_str).unwrap()
708        );
709    }
710
711    #[rstest]
712    fn test_parse_depth_from_metadata() {
713        let depth = 25;
714        let metadata = Some(
715            [("depth".to_string(), depth.to_string())]
716                .iter()
717                .cloned()
718                .collect(),
719        );
720        let data_type = DataType::new(stringify!(OrderBookDeltas), metadata);
721
722        assert_eq!(data_type.depth().unwrap(), depth);
723    }
724}