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::{
34    IntoPyObjectExt,
35    prelude::*,
36    pyclass::CompareOp,
37    types::{PyDict, PyTuple},
38};
39
40use crate::{
41    data::bar::{Bar, BarSpecification, BarType},
42    enums::{AggregationSource, BarAggregation, PriceType},
43    identifiers::InstrumentId,
44    python::common::PY_MODULE_MODEL,
45    types::{
46        price::{Price, PriceRaw},
47        quantity::{Quantity, QuantityRaw},
48    },
49};
50
51#[pymethods]
52#[pyo3_stub_gen::derive::gen_stub_pymethods]
53impl BarSpecification {
54    /// Represents a bar aggregation specification including a step, aggregation
55    /// method/rule and price type.
56    #[new]
57    fn py_new(step: usize, aggregation: BarAggregation, price_type: PriceType) -> PyResult<Self> {
58        Self::new_checked(step, aggregation, price_type).map_err(to_pyvalue_err)
59    }
60
61    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
62        match op {
63            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
64            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
65            _ => py.NotImplemented(),
66        }
67    }
68
69    fn __hash__(&self) -> isize {
70        let mut h = DefaultHasher::new();
71        self.hash(&mut h);
72        h.finish() as isize
73    }
74
75    fn __repr__(&self) -> String {
76        format!("{self:?}")
77    }
78
79    fn __str__(&self) -> String {
80        self.to_string()
81    }
82
83    #[getter]
84    #[pyo3(name = "step")]
85    fn py_step(&self) -> usize {
86        self.step.get()
87    }
88
89    #[getter]
90    #[pyo3(name = "aggregation")]
91    fn py_aggregation(&self) -> BarAggregation {
92        self.aggregation
93    }
94
95    #[getter]
96    #[pyo3(name = "price_type")]
97    fn py_price_type(&self) -> PriceType {
98        self.price_type
99    }
100
101    #[staticmethod]
102    #[pyo3(name = "fully_qualified_name")]
103    fn py_fully_qualified_name() -> String {
104        format!("{}:{}", PY_MODULE_MODEL, stringify!(BarSpecification))
105    }
106
107    /// Returns the `SignedDuration` interval for this bar specification.
108    ///
109    /// # Notes
110    ///
111    /// For `BarAggregation.Month` and `BarAggregation.Year`, proxy values are used
112    /// (30 days for months, 365 days for years) to estimate their respective durations,
113    /// since months and years have variable lengths.
114    #[getter]
115    #[pyo3(name = "timedelta")]
116    fn py_timedelta(&self) -> PyResult<jiff::SignedDuration> {
117        if !self.is_time_aggregated() {
118            return Err(to_pyvalue_err(format!(
119                "Timedelta not supported for aggregation type: {:?}",
120                self.aggregation
121            )));
122        }
123        Ok(self.timedelta())
124    }
125
126    /// Return a value indicating whether the aggregation method is time-driven:
127    ///  - `BarAggregation.Millisecond`
128    ///  - `BarAggregation.Second`
129    ///  - `BarAggregation.Minute`
130    ///  - `BarAggregation.Hour`
131    ///  - `BarAggregation.Day`
132    ///  - `BarAggregation.Week`
133    ///  - `BarAggregation.Month`
134    ///  - `BarAggregation.Year`
135    #[pyo3(name = "is_time_aggregated")]
136    fn py_is_time_aggregated(&self) -> bool {
137        self.is_time_aggregated()
138    }
139
140    /// Return a value indicating whether the aggregation method is threshold-driven:
141    ///  - `BarAggregation.Tick`
142    ///  - `BarAggregation.TickImbalance`
143    ///  - `BarAggregation.Volume`
144    ///  - `BarAggregation.VolumeImbalance`
145    ///  - `BarAggregation.Value`
146    ///  - `BarAggregation.ValueImbalance`
147    #[pyo3(name = "is_threshold_aggregated")]
148    fn py_is_threshold_aggregated(&self) -> bool {
149        self.is_threshold_aggregated()
150    }
151
152    /// Return a value indicating whether the aggregation method is information-driven:
153    ///  - `BarAggregation.TickRuns`
154    ///  - `BarAggregation.VolumeRuns`
155    ///  - `BarAggregation.ValueRuns`
156    #[pyo3(name = "is_information_aggregated")]
157    fn py_is_information_aggregated(&self) -> bool {
158        self.is_information_aggregated()
159    }
160
161    /// Returns the interval length in nanoseconds for time-based bar specifications.
162    #[pyo3(name = "get_interval_ns")]
163    fn py_get_interval_ns(&self) -> PyResult<u64> {
164        if !self.is_time_aggregated() {
165            return Err(to_pyvalue_err(format!(
166                "Aggregation not time based, was {:?}",
167                self.aggregation
168            )));
169        }
170        let td = self.timedelta();
171        u64::try_from(td.as_nanos())
172            .map_err(|_| to_pyvalue_err(format!("Interval overflows nanoseconds, was {td:?}")))
173    }
174
175    /// Creates a `BarSpecification` from a Python `timedelta` and price type.
176    #[staticmethod]
177    #[pyo3(name = "from_timedelta")]
178    fn py_from_timedelta(duration: jiff::SignedDuration, price_type: PriceType) -> PyResult<Self> {
179        if duration.as_millis() <= 0 {
180            return Err(to_pyvalue_err(format!(
181                "Duration must be positive, was {duration:?}"
182            )));
183        }
184        let total_secs_f64 = duration.as_millis() as f64 / 1000.0;
185        let days = duration.as_hours() / 24;
186
187        let (step, aggregation) = if days >= 7 {
188            (days / 7, BarAggregation::Week)
189        } else if days >= 1 {
190            (days, BarAggregation::Day)
191        } else if total_secs_f64 >= 3600.0 {
192            ((total_secs_f64 / 3600.0) as i64, BarAggregation::Hour)
193        } else if total_secs_f64 >= 60.0 {
194            ((total_secs_f64 / 60.0) as i64, BarAggregation::Minute)
195        } else if total_secs_f64 >= 1.0 {
196            (total_secs_f64 as i64, BarAggregation::Second)
197        } else {
198            (
199                (total_secs_f64 * 1000.0) as i64,
200                BarAggregation::Millisecond,
201            )
202        };
203
204        let spec =
205            Self::new_checked(step as usize, aggregation, price_type).map_err(to_pyvalue_err)?;
206
207        // Validate roundtrip
208        let roundtrip = spec.timedelta();
209        if roundtrip != duration {
210            return Err(to_pyvalue_err(format!(
211                "Duration {duration:?} is ambiguous"
212            )));
213        }
214
215        Ok(spec)
216    }
217
218    /// Returns whether the given aggregation is time-based.
219    #[staticmethod]
220    #[pyo3(name = "check_time_aggregated")]
221    fn py_check_time_aggregated(aggregation: BarAggregation) -> bool {
222        matches!(
223            aggregation,
224            BarAggregation::Millisecond
225                | BarAggregation::Second
226                | BarAggregation::Minute
227                | BarAggregation::Hour
228                | BarAggregation::Day
229                | BarAggregation::Week
230                | BarAggregation::Month
231                | BarAggregation::Year
232        )
233    }
234
235    /// Returns whether the given aggregation is threshold-based.
236    #[staticmethod]
237    #[pyo3(name = "check_threshold_aggregated")]
238    fn py_check_threshold_aggregated(aggregation: BarAggregation) -> bool {
239        matches!(
240            aggregation,
241            BarAggregation::Tick
242                | BarAggregation::TickImbalance
243                | BarAggregation::Volume
244                | BarAggregation::VolumeImbalance
245                | BarAggregation::Value
246                | BarAggregation::ValueImbalance
247        )
248    }
249
250    /// Returns whether the given aggregation is information-based.
251    #[staticmethod]
252    #[pyo3(name = "check_information_aggregated")]
253    fn py_check_information_aggregated(aggregation: BarAggregation) -> bool {
254        matches!(
255            aggregation,
256            BarAggregation::TickRuns | BarAggregation::VolumeRuns | BarAggregation::ValueRuns
257        )
258    }
259
260    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
261        let from_str = py.get_type::<Self>().getattr("from_str")?;
262        (from_str, (self.to_string(),)).into_py_any(py)
263    }
264
265    /// Creates a `BarSpecification` from a string representation.
266    #[staticmethod]
267    #[pyo3(name = "from_str")]
268    fn py_from_str(value: &str) -> PyResult<Self> {
269        let pieces: Vec<&str> = value.rsplitn(3, '-').collect();
270        if pieces.len() != 3 {
271            return Err(to_pyvalue_err(format!(
272                "The `BarSpecification` string value was malformed, was {value}"
273            )));
274        }
275        let step: usize = pieces[2].parse().map_err(to_pyvalue_err)?;
276        let aggregation = BarAggregation::from_str(pieces[1]).map_err(to_pyvalue_err)?;
277        let price_type = PriceType::from_str(pieces[0]).map_err(to_pyvalue_err)?;
278        Self::new_checked(step, aggregation, price_type).map_err(to_pyvalue_err)
279    }
280}
281
282#[pymethods]
283#[pyo3_stub_gen::derive::gen_stub_pymethods]
284impl BarType {
285    /// Represents a bar type including the instrument ID, bar specification and
286    /// aggregation source.
287    #[new]
288    #[pyo3(signature = (instrument_id, spec, aggregation_source = AggregationSource::External)
289    )]
290    fn py_new(
291        instrument_id: InstrumentId,
292        spec: BarSpecification,
293        aggregation_source: AggregationSource,
294    ) -> Self {
295        Self::new(instrument_id, spec, aggregation_source)
296    }
297
298    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
299        match op {
300            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
301            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
302            _ => py.NotImplemented(),
303        }
304    }
305
306    fn __hash__(&self) -> isize {
307        let mut h = DefaultHasher::new();
308        self.hash(&mut h);
309        h.finish() as isize
310    }
311
312    fn __repr__(&self) -> String {
313        format!("{self:?}")
314    }
315
316    fn __str__(&self) -> String {
317        self.to_string()
318    }
319
320    #[staticmethod]
321    #[pyo3(name = "fully_qualified_name")]
322    fn py_fully_qualified_name() -> String {
323        format!("{}:{}", PY_MODULE_MODEL, stringify!(BarType))
324    }
325
326    #[staticmethod]
327    #[pyo3(name = "from_str")]
328    fn py_from_str(value: &str) -> PyResult<Self> {
329        Self::from_str(value).map_err(to_pyvalue_err)
330    }
331
332    /// Creates a new composite `BarType` instance.
333    #[staticmethod]
334    #[pyo3(name = "new_composite")]
335    fn py_new_composite(
336        instrument_id: InstrumentId,
337        spec: BarSpecification,
338        aggregation_source: AggregationSource,
339        composite_step: usize,
340        composite_aggregation: BarAggregation,
341        composite_aggregation_source: AggregationSource,
342    ) -> PyResult<Self> {
343        Self::new_composite_checked(
344            instrument_id,
345            spec,
346            aggregation_source,
347            composite_step,
348            composite_aggregation,
349            composite_aggregation_source,
350        )
351        .map_err(to_pyvalue_err)
352    }
353
354    /// Returns whether this instance is a standard bar type.
355    #[pyo3(name = "is_standard")]
356    fn py_is_standard(&self) -> bool {
357        self.is_standard()
358    }
359
360    /// Returns whether this instance is a composite bar type.
361    #[pyo3(name = "is_composite")]
362    fn py_is_composite(&self) -> bool {
363        self.is_composite()
364    }
365
366    /// Returns the standard bar type component.
367    #[pyo3(name = "standard")]
368    fn py_standard(&self) -> Self {
369        self.standard()
370    }
371
372    /// Returns any composite bar type component.
373    #[pyo3(name = "composite")]
374    fn py_composite(&self) -> Self {
375        self.composite()
376    }
377
378    /// Returns the instrument ID and bar specification as a tuple key.
379    ///
380    /// Useful as a hashmap key when aggregation source should be ignored,
381    /// such as for indicator registration where INTERNAL and EXTERNAL bars
382    /// should trigger the same indicators.
383    #[pyo3(name = "id_spec_key")]
384    fn py_id_spec_key(&self) -> (InstrumentId, BarSpecification) {
385        self.id_spec_key()
386    }
387
388    /// Returns whether the bar aggregation source is `EXTERNAL`.
389    #[pyo3(name = "is_externally_aggregated")]
390    fn py_is_externally_aggregated(&self) -> bool {
391        self.is_externally_aggregated()
392    }
393
394    /// Returns whether the bar aggregation source is `INTERNAL`.
395    #[pyo3(name = "is_internally_aggregated")]
396    fn py_is_internally_aggregated(&self) -> bool {
397        self.is_internally_aggregated()
398    }
399
400    /// Returns the `InstrumentId` for this bar type.
401    #[getter]
402    #[pyo3(name = "instrument_id")]
403    fn py_instrument_id(&self) -> InstrumentId {
404        self.instrument_id()
405    }
406
407    /// Returns the `BarSpecification` for this bar type.
408    #[getter]
409    #[pyo3(name = "spec")]
410    fn py_spec(&self) -> BarSpecification {
411        self.spec()
412    }
413
414    /// Returns the `AggregationSource` for this bar type.
415    #[getter]
416    #[pyo3(name = "aggregation_source")]
417    fn py_aggregation_source(&self) -> AggregationSource {
418        self.aggregation_source()
419    }
420
421    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
422        let from_str = py.get_type::<Self>().getattr("from_str")?;
423        (from_str, (self.to_string(),)).into_py_any(py)
424    }
425}
426
427#[pymethods]
428#[pyo3_stub_gen::derive::gen_stub_pymethods]
429#[expect(clippy::too_many_arguments)]
430impl Bar {
431    /// Represents an aggregated bar.
432    #[new]
433    fn py_new(
434        bar_type: BarType,
435        open: Price,
436        high: Price,
437        low: Price,
438        close: Price,
439        volume: Quantity,
440        ts_event: u64,
441        ts_init: u64,
442    ) -> PyResult<Self> {
443        Self::new_checked(
444            bar_type,
445            open,
446            high,
447            low,
448            close,
449            volume,
450            ts_event.into(),
451            ts_init.into(),
452        )
453        .map_err(to_pyvalue_err)
454    }
455
456    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
457        match op {
458            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
459            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
460            _ => py.NotImplemented(),
461        }
462    }
463
464    fn __hash__(&self) -> isize {
465        let mut h = DefaultHasher::new();
466        self.hash(&mut h);
467        h.finish() as isize
468    }
469
470    fn __repr__(&self) -> String {
471        format!("{self:?}")
472    }
473
474    fn __str__(&self) -> String {
475        self.to_string()
476    }
477
478    #[getter]
479    #[pyo3(name = "bar_type")]
480    fn py_bar_type(&self) -> BarType {
481        self.bar_type
482    }
483
484    #[getter]
485    #[pyo3(name = "open")]
486    fn py_open(&self) -> Price {
487        self.open
488    }
489
490    #[getter]
491    #[pyo3(name = "high")]
492    fn py_high(&self) -> Price {
493        self.high
494    }
495
496    #[getter]
497    #[pyo3(name = "low")]
498    fn py_low(&self) -> Price {
499        self.low
500    }
501
502    #[getter]
503    #[pyo3(name = "close")]
504    fn py_close(&self) -> Price {
505        self.close
506    }
507
508    #[getter]
509    #[pyo3(name = "volume")]
510    fn py_volume(&self) -> Quantity {
511        self.volume
512    }
513
514    #[getter]
515    #[pyo3(name = "ts_event")]
516    fn py_ts_event(&self) -> u64 {
517        self.ts_event.as_u64()
518    }
519
520    #[getter]
521    #[pyo3(name = "ts_init")]
522    fn py_ts_init(&self) -> u64 {
523        self.ts_init.as_u64()
524    }
525
526    #[staticmethod]
527    #[pyo3(name = "fully_qualified_name")]
528    fn py_fully_qualified_name() -> String {
529        format!("{}:{}", PY_MODULE_MODEL, stringify!(Bar))
530    }
531
532    /// Returns the metadata for the type, for use with serialization formats.
533    #[staticmethod]
534    #[pyo3(name = "get_metadata")]
535    fn py_get_metadata(
536        bar_type: &BarType,
537        price_precision: u8,
538        size_precision: u8,
539    ) -> HashMap<String, String> {
540        Self::get_metadata(bar_type, price_precision, size_precision)
541    }
542
543    /// Returns the field map for the type, for use with Arrow schemas.
544    #[staticmethod]
545    #[pyo3(name = "get_fields")]
546    fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
547        let py_dict = PyDict::new(py);
548        for (k, v) in Self::get_fields() {
549            py_dict.set_item(k, v)?;
550        }
551
552        Ok(py_dict)
553    }
554
555    /// Returns a new object from the given dictionary representation.
556    #[staticmethod]
557    #[pyo3(name = "from_dict")]
558    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
559        from_dict_pyo3(py, values)
560    }
561
562    /// Return a dictionary representation of the object.
563    #[pyo3(name = "to_dict")]
564    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
565        to_dict_pyo3(py, self)
566    }
567
568    /// Return JSON encoded bytes representation of the object.
569    #[pyo3(name = "to_json_bytes")]
570    fn py_to_json_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
571        self.to_json_bytes()
572            .map_err(to_pyvalue_err)?
573            .into_py_any(py)
574    }
575
576    /// Return `MsgPack` encoded bytes representation of the object.
577    #[pyo3(name = "to_msgpack_bytes")]
578    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
579        self.to_msgpack_bytes()
580            .map_err(to_pyvalue_err)?
581            .into_py_any(py)
582    }
583
584    fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
585        let py_tuple: &Bound<'_, PyTuple> = state.cast::<PyTuple>()?;
586        let bar_type_str: String = py_tuple.get_item(0)?.extract()?;
587        let open_raw: PriceRaw = py_tuple.get_item(1)?.extract()?;
588        let open_prec: u8 = py_tuple.get_item(2)?.extract()?;
589        let high_raw: PriceRaw = py_tuple.get_item(3)?.extract()?;
590        let low_raw: PriceRaw = py_tuple.get_item(4)?.extract()?;
591        let close_raw: PriceRaw = py_tuple.get_item(5)?.extract()?;
592        let volume_raw: QuantityRaw = py_tuple.get_item(6)?.extract()?;
593        let volume_prec: u8 = py_tuple.get_item(7)?.extract()?;
594        let ts_event: u64 = py_tuple.get_item(8)?.extract()?;
595        let ts_init: u64 = py_tuple.get_item(9)?.extract()?;
596
597        self.bar_type = BarType::from_str(&bar_type_str).map_err(to_pyvalue_err)?;
598        self.open = Price::from_raw(open_raw, open_prec);
599        self.high = Price::from_raw(high_raw, open_prec);
600        self.low = Price::from_raw(low_raw, open_prec);
601        self.close = Price::from_raw(close_raw, open_prec);
602        self.volume = Quantity::from_raw(volume_raw, volume_prec);
603        self.ts_event = ts_event.into();
604        self.ts_init = ts_init.into();
605        Ok(())
606    }
607
608    fn __getstate__(&self, py: Python) -> PyResult<Py<PyAny>> {
609        (
610            self.bar_type.to_string(),
611            self.open.raw,
612            self.open.precision,
613            self.high.raw,
614            self.low.raw,
615            self.close.raw,
616            self.volume.raw,
617            self.volume.precision,
618            self.ts_event.as_u64(),
619            self.ts_init.as_u64(),
620        )
621            .into_py_any(py)
622    }
623
624    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
625        let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
626        let state = self.__getstate__(py)?;
627        (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
628    }
629
630    #[staticmethod]
631    fn _safe_constructor() -> Self {
632        Self::new(
633            BarType::from("NULL.NULL-1-TICK-LAST-EXTERNAL"),
634            Price::zero(0),
635            Price::zero(0),
636            Price::zero(0),
637            Price::zero(0),
638            Quantity::from(1),
639            0.into(),
640            0.into(),
641        )
642    }
643}
644
645#[pymethods]
646impl Bar {
647    #[staticmethod]
648    #[pyo3(name = "from_json")]
649    fn py_from_json(data: &[u8]) -> PyResult<Self> {
650        Self::from_json_bytes(data).map_err(to_pyvalue_err)
651    }
652
653    #[staticmethod]
654    #[pyo3(name = "from_msgpack")]
655    fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
656        Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use pyo3::Python;
663    use rstest::rstest;
664
665    use crate::{
666        data::{Bar, BarType},
667        types::{Price, Quantity},
668    };
669
670    #[rstest]
671    #[case("10.0000", "10.0010", "10.0020", "10.0005")] // low > high
672    #[case("10.0000", "10.0010", "10.0005", "10.0030")] // close > high
673    #[case("10.0000", "9.9990", "9.9980", "9.9995")] // high < open
674    #[case("10.0000", "10.0010", "10.0015", "10.0020")] // low > close
675    #[case("10.0000", "10.0000", "10.0001", "10.0002")] // low > high (equal high/open edge case)
676    fn test_bar_py_new_invalid(
677        #[case] open: &str,
678        #[case] high: &str,
679        #[case] low: &str,
680        #[case] close: &str,
681    ) {
682        let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL");
683        let open = Price::from(open);
684        let high = Price::from(high);
685        let low = Price::from(low);
686        let close = Price::from(close);
687        let volume = Quantity::from(100_000);
688        let ts_event = 0;
689        let ts_init = 1;
690
691        let result = Bar::py_new(bar_type, open, high, low, close, volume, ts_event, ts_init);
692        assert!(result.is_err());
693    }
694
695    #[rstest]
696    fn test_bar_py_new() {
697        let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL");
698        let open = Price::from("1.00005");
699        let high = Price::from("1.00010");
700        let low = Price::from("1.00000");
701        let close = Price::from("1.00007");
702        let volume = Quantity::from(100_000);
703        let ts_event = 0;
704        let ts_init = 1;
705
706        let result = Bar::py_new(bar_type, open, high, low, close, volume, ts_event, ts_init);
707        assert!(result.is_ok());
708    }
709
710    #[rstest]
711    fn test_to_dict() {
712        let bar = Bar::default();
713
714        Python::initialize();
715        Python::attach(|py| {
716            let dict_string = bar.py_to_dict(py).unwrap().to_string();
717            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}";
718            assert_eq!(dict_string, expected_string);
719        });
720    }
721
722    #[rstest]
723    fn test_as_from_dict() {
724        let bar = Bar::default();
725
726        Python::initialize();
727        Python::attach(|py| {
728            let dict = bar.py_to_dict(py).unwrap();
729            let parsed = Bar::py_from_dict(py, dict).unwrap();
730            assert_eq!(parsed, bar);
731        });
732    }
733}