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