Skip to main content

nautilus_model/data/
prices.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//! Domain types representing *price* data (index-price, mark-price, etc.).
17
18use std::{collections::HashMap, fmt::Display};
19
20use indexmap::IndexMap;
21use nautilus_core::{UnixNanos, serialization::Serializable};
22use serde::{Deserialize, Serialize};
23
24use super::{ARROW_TIMESTAMP_NANOSECOND, HasTsInit};
25use crate::{
26    identifiers::InstrumentId,
27    types::{Price, fixed::FIXED_DECIMAL},
28};
29
30/// Represents a mark price update.
31#[repr(C)]
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(tag = "type")]
34#[cfg_attr(
35    feature = "python",
36    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
37)]
38#[cfg_attr(
39    feature = "python",
40    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
41)]
42pub struct MarkPriceUpdate {
43    /// The instrument ID for the mark price.
44    pub instrument_id: InstrumentId,
45    /// The mark price.
46    pub value: Price,
47    /// UNIX timestamp (nanoseconds) when the price event occurred.
48    pub ts_event: UnixNanos,
49    /// UNIX timestamp (nanoseconds) when the instance was created.
50    pub ts_init: UnixNanos,
51}
52
53impl MarkPriceUpdate {
54    /// Creates a new [`MarkPriceUpdate`] instance.
55    #[must_use]
56    pub fn new(
57        instrument_id: InstrumentId,
58        value: Price,
59        ts_event: UnixNanos,
60        ts_init: UnixNanos,
61    ) -> Self {
62        Self {
63            instrument_id,
64            value,
65            ts_event,
66            ts_init,
67        }
68    }
69
70    /// Returns the metadata for the type, for use with serialization formats.
71    #[must_use]
72    pub fn get_metadata(
73        instrument_id: &InstrumentId,
74        price_precision: u8,
75    ) -> HashMap<String, String> {
76        let mut metadata = HashMap::new();
77        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
78        metadata.insert("price_precision".to_string(), price_precision.to_string());
79        metadata
80    }
81
82    /// Returns the field map for the type, for use with Arrow schemas.
83    #[must_use]
84    pub fn get_fields() -> IndexMap<String, String> {
85        let mut metadata = IndexMap::new();
86        metadata.insert("value".to_string(), FIXED_DECIMAL.to_string());
87        metadata.insert(
88            "ts_event".to_string(),
89            ARROW_TIMESTAMP_NANOSECOND.to_string(),
90        );
91        metadata.insert(
92            "ts_init".to_string(),
93            ARROW_TIMESTAMP_NANOSECOND.to_string(),
94        );
95        metadata
96    }
97}
98
99impl Display for MarkPriceUpdate {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        write!(
102            f,
103            "{},{},{},{}",
104            self.instrument_id, self.value, self.ts_event, self.ts_init
105        )
106    }
107}
108
109impl Serializable for MarkPriceUpdate {}
110
111impl HasTsInit for MarkPriceUpdate {
112    fn ts_init(&self) -> UnixNanos {
113        self.ts_init
114    }
115}
116
117/// Represents an index price update.
118#[repr(C)]
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
120#[serde(tag = "type")]
121#[cfg_attr(
122    feature = "python",
123    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
124)]
125#[cfg_attr(
126    feature = "python",
127    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
128)]
129pub struct IndexPriceUpdate {
130    /// The instrument ID for the index price.
131    pub instrument_id: InstrumentId,
132    /// The index price.
133    pub value: Price,
134    /// UNIX timestamp (nanoseconds) when the price event occurred.
135    pub ts_event: UnixNanos,
136    /// UNIX timestamp (nanoseconds) when the instance was created.
137    pub ts_init: UnixNanos,
138}
139
140impl IndexPriceUpdate {
141    /// Creates a new [`IndexPriceUpdate`] instance.
142    #[must_use]
143    pub fn new(
144        instrument_id: InstrumentId,
145        value: Price,
146        ts_event: UnixNanos,
147        ts_init: UnixNanos,
148    ) -> Self {
149        Self {
150            instrument_id,
151            value,
152            ts_event,
153            ts_init,
154        }
155    }
156
157    /// Returns the metadata for the type, for use with serialization formats.
158    #[must_use]
159    pub fn get_metadata(
160        instrument_id: &InstrumentId,
161        price_precision: u8,
162    ) -> HashMap<String, String> {
163        let mut metadata = HashMap::new();
164        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
165        metadata.insert("price_precision".to_string(), price_precision.to_string());
166        metadata
167    }
168
169    /// Returns the field map for the type, for use with Arrow schemas.
170    #[must_use]
171    pub fn get_fields() -> IndexMap<String, String> {
172        let mut metadata = IndexMap::new();
173        metadata.insert("value".to_string(), FIXED_DECIMAL.to_string());
174        metadata.insert(
175            "ts_event".to_string(),
176            ARROW_TIMESTAMP_NANOSECOND.to_string(),
177        );
178        metadata.insert(
179            "ts_init".to_string(),
180            ARROW_TIMESTAMP_NANOSECOND.to_string(),
181        );
182        metadata
183    }
184}
185
186impl Display for IndexPriceUpdate {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        write!(
189            f,
190            "{},{},{},{}",
191            self.instrument_id, self.value, self.ts_event, self.ts_init
192        )
193    }
194}
195
196impl Serializable for IndexPriceUpdate {}
197
198impl HasTsInit for IndexPriceUpdate {
199    fn ts_init(&self) -> UnixNanos {
200        self.ts_init
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use std::{
207        collections::hash_map::DefaultHasher,
208        hash::{Hash, Hasher},
209    };
210
211    use nautilus_core::serialization::{
212        Serializable,
213        msgpack::{FromMsgPack, ToMsgPack},
214    };
215    use rstest::{fixture, rstest};
216    use serde_json;
217
218    use super::*;
219
220    #[fixture]
221    fn instrument_id() -> InstrumentId {
222        InstrumentId::from("BTC-USDT.OKX")
223    }
224
225    #[fixture]
226    fn price() -> Price {
227        Price::from("150_500.10")
228    }
229
230    #[rstest]
231    fn test_mark_price_update_new(instrument_id: InstrumentId, price: Price) {
232        let ts_event = UnixNanos::from(1);
233        let ts_init = UnixNanos::from(2);
234
235        let mark_price = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
236
237        assert_eq!(mark_price.instrument_id, instrument_id);
238        assert_eq!(mark_price.value, price);
239        assert_eq!(mark_price.ts_event, ts_event);
240        assert_eq!(mark_price.ts_init, ts_init);
241    }
242
243    #[rstest]
244    fn test_mark_price_update_display(instrument_id: InstrumentId, price: Price) {
245        let ts_event = UnixNanos::from(1);
246        let ts_init = UnixNanos::from(2);
247
248        let mark_price = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
249
250        assert_eq!(format!("{mark_price}"), "BTC-USDT.OKX,150500.10,1,2");
251    }
252
253    #[rstest]
254    fn test_mark_price_update_get_ts_init(instrument_id: InstrumentId, price: Price) {
255        let ts_event = UnixNanos::from(1);
256        let ts_init = UnixNanos::from(2);
257
258        let mark_price = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
259
260        assert_eq!(mark_price.ts_init(), ts_init);
261    }
262
263    #[rstest]
264    fn test_mark_price_update_eq_hash(instrument_id: InstrumentId, price: Price) {
265        use std::{
266            collections::hash_map::DefaultHasher,
267            hash::{Hash, Hasher},
268        };
269
270        let ts_event = UnixNanos::from(1);
271        let ts_init = UnixNanos::from(2);
272
273        let mark_price1 = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
274        let mark_price2 = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
275        let mark_price3 =
276            MarkPriceUpdate::new(instrument_id, Price::from("143_500.50"), ts_event, ts_init);
277
278        assert_eq!(mark_price1, mark_price2);
279        assert_ne!(mark_price1, mark_price3);
280
281        // Test Hash implementation
282        let mut hasher1 = DefaultHasher::new();
283        let mut hasher2 = DefaultHasher::new();
284        mark_price1.hash(&mut hasher1);
285        mark_price2.hash(&mut hasher2);
286        assert_eq!(hasher1.finish(), hasher2.finish());
287    }
288
289    #[rstest]
290    fn test_mark_price_update_json_serialization(instrument_id: InstrumentId, price: Price) {
291        let ts_event = UnixNanos::from(1);
292        let ts_init = UnixNanos::from(2);
293
294        let mark_price = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
295
296        let serialized = mark_price.to_json_bytes().unwrap();
297        let deserialized = MarkPriceUpdate::from_json_bytes(&serialized).unwrap();
298
299        assert_eq!(mark_price, deserialized);
300    }
301
302    #[rstest]
303    fn test_mark_price_update_msgpack_serialization(instrument_id: InstrumentId, price: Price) {
304        let ts_event = UnixNanos::from(1);
305        let ts_init = UnixNanos::from(2);
306
307        let mark_price = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
308
309        let serialized = mark_price.to_msgpack_bytes().unwrap();
310        let deserialized = MarkPriceUpdate::from_msgpack_bytes(&serialized).unwrap();
311
312        assert_eq!(mark_price, deserialized);
313    }
314
315    #[rstest]
316    fn test_mark_price_update_clone(instrument_id: InstrumentId, price: Price) {
317        let ts_event = UnixNanos::from(1);
318        let ts_init = UnixNanos::from(2);
319
320        let mark_price = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
321        let cloned = mark_price;
322
323        assert_eq!(mark_price, cloned);
324    }
325
326    #[rstest]
327    fn test_mark_price_update_serde_json(instrument_id: InstrumentId, price: Price) {
328        let ts_event = UnixNanos::from(1);
329        let ts_init = UnixNanos::from(2);
330
331        let mark_price = MarkPriceUpdate::new(instrument_id, price, ts_event, ts_init);
332
333        let json_str = serde_json::to_string(&mark_price).unwrap();
334        let deserialized: MarkPriceUpdate = serde_json::from_str(&json_str).unwrap();
335
336        assert_eq!(mark_price, deserialized);
337    }
338
339    #[rstest]
340    fn test_index_price_update_new(instrument_id: InstrumentId, price: Price) {
341        let ts_event = UnixNanos::from(1);
342        let ts_init = UnixNanos::from(2);
343
344        let index_price = IndexPriceUpdate::new(instrument_id, price, ts_event, ts_init);
345
346        assert_eq!(index_price.instrument_id, instrument_id);
347        assert_eq!(index_price.value, price);
348        assert_eq!(index_price.ts_event, ts_event);
349        assert_eq!(index_price.ts_init, ts_init);
350    }
351
352    #[rstest]
353    fn test_index_price_update_display(instrument_id: InstrumentId, price: Price) {
354        let ts_event = UnixNanos::from(1);
355        let ts_init = UnixNanos::from(2);
356
357        let index_price = IndexPriceUpdate::new(instrument_id, price, ts_event, ts_init);
358
359        assert_eq!(format!("{index_price}"), "BTC-USDT.OKX,150500.10,1,2");
360    }
361
362    #[rstest]
363    fn test_index_price_update_get_ts_init(instrument_id: InstrumentId, price: Price) {
364        let ts_event = UnixNanos::from(1);
365        let ts_init = UnixNanos::from(2);
366
367        let index_price = IndexPriceUpdate::new(instrument_id, price, ts_event, ts_init);
368
369        assert_eq!(index_price.ts_init(), ts_init);
370    }
371
372    #[rstest]
373    fn test_index_price_update_eq_hash(instrument_id: InstrumentId, price: Price) {
374        let ts_event = UnixNanos::from(1);
375        let ts_init = UnixNanos::from(2);
376
377        let index_price1 = IndexPriceUpdate::new(instrument_id, price, ts_event, ts_init);
378        let index_price2 = IndexPriceUpdate::new(instrument_id, price, ts_event, ts_init);
379        let index_price3 = IndexPriceUpdate::new(instrument_id, price, UnixNanos::from(3), ts_init);
380
381        assert_eq!(index_price1, index_price2);
382        assert_ne!(index_price1, index_price3);
383
384        let mut hasher1 = DefaultHasher::new();
385        let mut hasher2 = DefaultHasher::new();
386        index_price1.hash(&mut hasher1);
387        index_price2.hash(&mut hasher2);
388        assert_eq!(hasher1.finish(), hasher2.finish());
389    }
390
391    #[rstest]
392    fn test_index_price_update_json_serialization(instrument_id: InstrumentId, price: Price) {
393        let ts_event = UnixNanos::from(1);
394        let ts_init = UnixNanos::from(2);
395
396        let index_price = IndexPriceUpdate::new(instrument_id, price, ts_event, ts_init);
397
398        let serialized = index_price.to_json_bytes().unwrap();
399        let deserialized = IndexPriceUpdate::from_json_bytes(&serialized).unwrap();
400
401        assert_eq!(index_price, deserialized);
402    }
403
404    #[rstest]
405    fn test_index_price_update_msgpack_serialization(instrument_id: InstrumentId, price: Price) {
406        let ts_event = UnixNanos::from(1);
407        let ts_init = UnixNanos::from(2);
408
409        let index_price = IndexPriceUpdate::new(instrument_id, price, ts_event, ts_init);
410
411        let serialized = index_price.to_msgpack_bytes().unwrap();
412        let deserialized = IndexPriceUpdate::from_msgpack_bytes(&serialized).unwrap();
413
414        assert_eq!(index_price, deserialized);
415    }
416
417    #[rstest]
418    fn test_index_price_update_serde_json(instrument_id: InstrumentId, price: Price) {
419        let ts_event = UnixNanos::from(1);
420        let ts_init = UnixNanos::from(2);
421
422        let index_price = IndexPriceUpdate::new(instrument_id, price, ts_event, ts_init);
423
424        let json_str = serde_json::to_string(&index_price).unwrap();
425        let deserialized: IndexPriceUpdate = serde_json::from_str(&json_str).unwrap();
426
427        assert_eq!(index_price, deserialized);
428    }
429}