Skip to main content

nautilus_model/python/data/
bar.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
16use std::{
17    collections::{HashMap, hash_map::DefaultHasher},
18    hash::{Hash, Hasher},
19    str::FromStr,
20};
21
22use nautilus_core::{
23    python::{
24        IntoPyObjectNautilusExt,
25        serialization::{from_dict_pyo3, to_dict_pyo3},
26        to_pyvalue_err,
27    },
28    serialization::{
29        Serializable,
30        msgpack::{FromMsgPack, ToMsgPack},
31    },
32};
33use pyo3::{prelude::*, pyclass::CompareOp, types::PyDict};
34
35use super::data_to_pycapsule;
36use crate::{
37    data::{
38        Data,
39        bar::{Bar, BarSpecification, BarType},
40    },
41    enums::{AggregationSource, BarAggregation, PriceType},
42    identifiers::InstrumentId,
43    python::common::PY_MODULE_MODEL,
44    types::{
45        price::{Price, PriceRaw},
46        quantity::{Quantity, QuantityRaw},
47    },
48};
49
50#[pymethods]
51impl BarSpecification {
52    #[new]
53    fn py_new(step: usize, aggregation: BarAggregation, price_type: PriceType) -> PyResult<Self> {
54        Self::new_checked(step, aggregation, price_type).map_err(to_pyvalue_err)
55    }
56
57    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
58        match op {
59            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
60            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
61            _ => py.NotImplemented(),
62        }
63    }
64
65    fn __hash__(&self) -> isize {
66        let mut h = DefaultHasher::new();
67        self.hash(&mut h);
68        h.finish() as isize
69    }
70
71    fn __repr__(&self) -> String {
72        format!("{self:?}")
73    }
74
75    fn __str__(&self) -> String {
76        self.to_string()
77    }
78
79    #[staticmethod]
80    #[pyo3(name = "fully_qualified_name")]
81    fn py_fully_qualified_name() -> String {
82        format!("{}:{}", PY_MODULE_MODEL, stringify!(BarSpecification))
83    }
84
85    #[getter]
86    #[pyo3(name = "timedelta")]
87    fn py_timedelta(&self) -> PyResult<chrono::TimeDelta> {
88        match self.aggregation {
89            BarAggregation::Millisecond
90            | BarAggregation::Second
91            | BarAggregation::Minute
92            | BarAggregation::Hour
93            | BarAggregation::Day => Ok(self.timedelta()),
94            _ => Err(to_pyvalue_err(format!(
95                "Timedelta not supported for aggregation type: {:?}",
96                self.aggregation
97            ))),
98        }
99    }
100}
101
102#[pymethods]
103impl BarType {
104    #[new]
105    #[pyo3(signature = (instrument_id, spec, aggregation_source = AggregationSource::External)
106    )]
107    fn py_new(
108        instrument_id: InstrumentId,
109        spec: BarSpecification,
110        aggregation_source: AggregationSource,
111    ) -> Self {
112        Self::new(instrument_id, spec, aggregation_source)
113    }
114
115    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
116        match op {
117            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
118            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
119            _ => py.NotImplemented(),
120        }
121    }
122
123    fn __hash__(&self) -> isize {
124        let mut h = DefaultHasher::new();
125        self.hash(&mut h);
126        h.finish() as isize
127    }
128
129    fn __repr__(&self) -> String {
130        format!("{self:?}")
131    }
132
133    fn __str__(&self) -> String {
134        self.to_string()
135    }
136
137    #[staticmethod]
138    #[pyo3(name = "fully_qualified_name")]
139    fn py_fully_qualified_name() -> String {
140        format!("{}:{}", PY_MODULE_MODEL, stringify!(BarType))
141    }
142
143    #[staticmethod]
144    #[pyo3(name = "from_str")]
145    fn py_from_str(value: &str) -> PyResult<Self> {
146        Self::from_str(value).map_err(to_pyvalue_err)
147    }
148
149    #[staticmethod]
150    #[pyo3(name = "new_composite")]
151    fn py_new_composite(
152        instrument_id: InstrumentId,
153        spec: BarSpecification,
154        aggregation_source: AggregationSource,
155        composite_step: usize,
156        composite_aggregation: BarAggregation,
157        composite_aggregation_source: AggregationSource,
158    ) -> Self {
159        Self::new_composite(
160            instrument_id,
161            spec,
162            aggregation_source,
163            composite_step,
164            composite_aggregation,
165            composite_aggregation_source,
166        )
167    }
168
169    #[pyo3(name = "is_standard")]
170    fn py_is_standard(&self) -> bool {
171        self.is_standard()
172    }
173
174    #[pyo3(name = "is_composite")]
175    fn py_is_composite(&self) -> bool {
176        self.is_composite()
177    }
178
179    #[pyo3(name = "standard")]
180    fn py_standard(&self) -> Self {
181        self.standard()
182    }
183
184    #[pyo3(name = "composite")]
185    fn py_composite(&self) -> Self {
186        self.composite()
187    }
188
189    #[pyo3(name = "id_spec_key")]
190    fn py_id_spec_key(&self) -> (InstrumentId, BarSpecification) {
191        self.id_spec_key()
192    }
193}
194
195impl Bar {
196    /// Creates a Rust `Bar` instance from a Python object.
197    ///
198    /// # Errors
199    ///
200    /// Returns a `PyErr` if retrieving any attribute or converting types fails.
201    pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
202        let bar_type_obj: Bound<'_, PyAny> = obj.getattr("bar_type")?.extract()?;
203        let bar_type_str: String = bar_type_obj.call_method0("__str__")?.extract()?;
204        let bar_type = BarType::from(bar_type_str);
205
206        let open_py: Bound<'_, PyAny> = obj.getattr("open")?;
207        let price_prec: u8 = open_py.getattr("precision")?.extract()?;
208        let open_raw: PriceRaw = open_py.getattr("raw")?.extract()?;
209        let open = Price::from_raw(open_raw, price_prec);
210
211        let high_py: Bound<'_, PyAny> = obj.getattr("high")?;
212        let high_raw: PriceRaw = high_py.getattr("raw")?.extract()?;
213        let high = Price::from_raw(high_raw, price_prec);
214
215        let low_py: Bound<'_, PyAny> = obj.getattr("low")?;
216        let low_raw: PriceRaw = low_py.getattr("raw")?.extract()?;
217        let low = Price::from_raw(low_raw, price_prec);
218
219        let close_py: Bound<'_, PyAny> = obj.getattr("close")?;
220        let close_raw: PriceRaw = close_py.getattr("raw")?.extract()?;
221        let close = Price::from_raw(close_raw, price_prec);
222
223        let volume_py: Bound<'_, PyAny> = obj.getattr("volume")?;
224        let volume_raw: QuantityRaw = volume_py.getattr("raw")?.extract()?;
225        let volume_prec: u8 = volume_py.getattr("precision")?.extract()?;
226        let volume = Quantity::from_raw(volume_raw, volume_prec);
227
228        let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
229        let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
230
231        Ok(Self::new(
232            bar_type,
233            open,
234            high,
235            low,
236            close,
237            volume,
238            ts_event.into(),
239            ts_init.into(),
240        ))
241    }
242}
243
244#[pymethods]
245#[allow(clippy::too_many_arguments)]
246impl Bar {
247    #[new]
248    fn py_new(
249        bar_type: BarType,
250        open: Price,
251        high: Price,
252        low: Price,
253        close: Price,
254        volume: Quantity,
255        ts_event: u64,
256        ts_init: u64,
257    ) -> PyResult<Self> {
258        Self::new_checked(
259            bar_type,
260            open,
261            high,
262            low,
263            close,
264            volume,
265            ts_event.into(),
266            ts_init.into(),
267        )
268        .map_err(to_pyvalue_err)
269    }
270
271    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
272        match op {
273            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
274            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
275            _ => py.NotImplemented(),
276        }
277    }
278
279    fn __hash__(&self) -> isize {
280        let mut h = DefaultHasher::new();
281        self.hash(&mut h);
282        h.finish() as isize
283    }
284
285    fn __repr__(&self) -> String {
286        format!("{self:?}")
287    }
288
289    fn __str__(&self) -> String {
290        self.to_string()
291    }
292
293    #[getter]
294    #[pyo3(name = "bar_type")]
295    fn py_bar_type(&self) -> BarType {
296        self.bar_type
297    }
298
299    #[getter]
300    #[pyo3(name = "open")]
301    fn py_open(&self) -> Price {
302        self.open
303    }
304
305    #[getter]
306    #[pyo3(name = "high")]
307    fn py_high(&self) -> Price {
308        self.high
309    }
310
311    #[getter]
312    #[pyo3(name = "low")]
313    fn py_low(&self) -> Price {
314        self.low
315    }
316
317    #[getter]
318    #[pyo3(name = "close")]
319    fn py_close(&self) -> Price {
320        self.close
321    }
322
323    #[getter]
324    #[pyo3(name = "volume")]
325    fn py_volume(&self) -> Quantity {
326        self.volume
327    }
328
329    #[getter]
330    #[pyo3(name = "ts_event")]
331    fn py_ts_event(&self) -> u64 {
332        self.ts_event.as_u64()
333    }
334
335    #[getter]
336    #[pyo3(name = "ts_init")]
337    fn py_ts_init(&self) -> u64 {
338        self.ts_init.as_u64()
339    }
340
341    #[staticmethod]
342    #[pyo3(name = "fully_qualified_name")]
343    fn py_fully_qualified_name() -> String {
344        format!("{}:{}", PY_MODULE_MODEL, stringify!(Bar))
345    }
346
347    #[staticmethod]
348    #[pyo3(name = "get_metadata")]
349    fn py_get_metadata(
350        bar_type: &BarType,
351        price_precision: u8,
352        size_precision: u8,
353    ) -> PyResult<HashMap<String, String>> {
354        Ok(Self::get_metadata(
355            bar_type,
356            price_precision,
357            size_precision,
358        ))
359    }
360
361    #[staticmethod]
362    #[pyo3(name = "get_fields")]
363    fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
364        let py_dict = PyDict::new(py);
365        for (k, v) in Self::get_fields() {
366            py_dict.set_item(k, v)?;
367        }
368
369        Ok(py_dict)
370    }
371
372    /// Returns a new object from the given dictionary representation.
373    #[staticmethod]
374    #[pyo3(name = "from_dict")]
375    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
376        from_dict_pyo3(py, values)
377    }
378
379    #[staticmethod]
380    #[pyo3(name = "from_json")]
381    fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
382        Self::from_json_bytes(&data).map_err(to_pyvalue_err)
383    }
384
385    #[staticmethod]
386    #[pyo3(name = "from_msgpack")]
387    fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
388        Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
389    }
390
391    /// Creates a `PyCapsule` containing a raw pointer to a `Data::Bar` object.
392    ///
393    /// This function takes the current object (assumed to be of a type that can be represented as
394    /// `Data::Bar`), and encapsulates a raw pointer to it within a `PyCapsule`.
395    ///
396    /// # Safety
397    ///
398    /// This function is safe as long as the following conditions are met:
399    /// - The `Data::Delta` object pointed to by the capsule must remain valid for the lifetime of the capsule.
400    /// - The consumer of the capsule must ensure proper handling to avoid dereferencing a dangling pointer.
401    ///
402    /// # Panics
403    ///
404    /// The function will panic if the `PyCapsule` creation fails, which can occur if the
405    /// `Data::Bar` object cannot be converted into a raw pointer.
406    #[pyo3(name = "as_pycapsule")]
407    fn py_as_pycapsule(&self, py: Python<'_>) -> Py<PyAny> {
408        data_to_pycapsule(py, Data::Bar(*self))
409    }
410
411    /// Return a dictionary representation of the object.
412    #[pyo3(name = "to_dict")]
413    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
414        to_dict_pyo3(py, self)
415    }
416
417    /// Return JSON encoded bytes representation of the object.
418    #[pyo3(name = "to_json_bytes")]
419    fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
420        self.to_json_bytes().unwrap().into_py_any_unwrap(py)
421    }
422
423    /// Return MsgPack encoded bytes representation of the object.
424    #[pyo3(name = "to_msgpack_bytes")]
425    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
426        self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use nautilus_core::python::IntoPyObjectNautilusExt;
433    use pyo3::Python;
434    use rstest::rstest;
435
436    use crate::{
437        data::{Bar, BarType},
438        types::{Price, Quantity},
439    };
440
441    #[rstest]
442    #[case("10.0000", "10.0010", "10.0020", "10.0005")] // low > high
443    #[case("10.0000", "10.0010", "10.0005", "10.0030")] // close > high
444    #[case("10.0000", "9.9990", "9.9980", "9.9995")] // high < open
445    #[case("10.0000", "10.0010", "10.0015", "10.0020")] // low > close
446    #[case("10.0000", "10.0000", "10.0001", "10.0002")] // low > high (equal high/open edge case)
447    fn test_bar_py_new_invalid(
448        #[case] open: &str,
449        #[case] high: &str,
450        #[case] low: &str,
451        #[case] close: &str,
452    ) {
453        let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL");
454        let open = Price::from(open);
455        let high = Price::from(high);
456        let low = Price::from(low);
457        let close = Price::from(close);
458        let volume = Quantity::from(100_000);
459        let ts_event = 0;
460        let ts_init = 1;
461
462        let result = Bar::py_new(bar_type, open, high, low, close, volume, ts_event, ts_init);
463        assert!(result.is_err());
464    }
465
466    #[rstest]
467    fn test_bar_py_new() {
468        let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL");
469        let open = Price::from("1.00005");
470        let high = Price::from("1.00010");
471        let low = Price::from("1.00000");
472        let close = Price::from("1.00007");
473        let volume = Quantity::from(100_000);
474        let ts_event = 0;
475        let ts_init = 1;
476
477        let result = Bar::py_new(bar_type, open, high, low, close, volume, ts_event, ts_init);
478        assert!(result.is_ok());
479    }
480
481    #[rstest]
482    fn test_to_dict() {
483        let bar = Bar::default();
484
485        Python::initialize();
486        Python::attach(|py| {
487            let dict_string = bar.py_to_dict(py).unwrap().to_string();
488            let expected_string = "{'type': 'Bar', 'bar_type': 'AUDUSD.SIM-1-MINUTE-LAST-INTERNAL', 'open': '1.00010', 'high': '1.00020', 'low': '1.00000', 'close': '1.00010', 'volume': '100000', 'ts_event': 0, 'ts_init': 0}";
489            assert_eq!(dict_string, expected_string);
490        });
491    }
492
493    #[rstest]
494    fn test_as_from_dict() {
495        let bar = Bar::default();
496
497        Python::initialize();
498        Python::attach(|py| {
499            let dict = bar.py_to_dict(py).unwrap();
500            let parsed = Bar::py_from_dict(py, dict).unwrap();
501            assert_eq!(parsed, bar);
502        });
503    }
504
505    #[rstest]
506    fn test_from_pyobject() {
507        let bar = Bar::default();
508
509        Python::initialize();
510        Python::attach(|py| {
511            let bar_pyobject = bar.into_py_any_unwrap(py);
512            let parsed_bar = Bar::from_pyobject(bar_pyobject.bind(py)).unwrap();
513            assert_eq!(parsed_bar, bar);
514        });
515    }
516}