1use 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::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 #[getter]
83 #[pyo3(name = "timedelta")]
84 fn py_timedelta(&self) -> PyResult<chrono::TimeDelta> {
85 match self.aggregation {
86 BarAggregation::Millisecond
87 | BarAggregation::Second
88 | BarAggregation::Minute
89 | BarAggregation::Hour
90 | BarAggregation::Day => Ok(self.timedelta()),
91 _ => Err(to_pyvalue_err(format!(
92 "Timedelta not supported for aggregation type: {:?}",
93 self.aggregation
94 ))),
95 }
96 }
97}
98
99#[pymethods]
100impl BarType {
101 #[new]
102 #[pyo3(signature = (instrument_id, spec, aggregation_source = AggregationSource::External)
103 )]
104 fn py_new(
105 instrument_id: InstrumentId,
106 spec: BarSpecification,
107 aggregation_source: AggregationSource,
108 ) -> Self {
109 Self::new(instrument_id, spec, aggregation_source)
110 }
111
112 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
113 match op {
114 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
115 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
116 _ => py.NotImplemented(),
117 }
118 }
119
120 fn __hash__(&self) -> isize {
121 let mut h = DefaultHasher::new();
122 self.hash(&mut h);
123 h.finish() as isize
124 }
125
126 fn __repr__(&self) -> String {
127 format!("{self:?}")
128 }
129
130 fn __str__(&self) -> String {
131 self.to_string()
132 }
133
134 #[staticmethod]
135 #[pyo3(name = "fully_qualified_name")]
136 fn py_fully_qualified_name() -> String {
137 format!("{}:{}", PY_MODULE_MODEL, stringify!(BarType))
138 }
139
140 #[staticmethod]
141 #[pyo3(name = "from_str")]
142 fn py_from_str(value: &str) -> PyResult<Self> {
143 Self::from_str(value).map_err(to_pyvalue_err)
144 }
145
146 #[staticmethod]
147 #[pyo3(name = "new_composite")]
148 fn py_new_composite(
149 instrument_id: InstrumentId,
150 spec: BarSpecification,
151 aggregation_source: AggregationSource,
152 composite_step: usize,
153 composite_aggregation: BarAggregation,
154 composite_aggregation_source: AggregationSource,
155 ) -> Self {
156 Self::new_composite(
157 instrument_id,
158 spec,
159 aggregation_source,
160 composite_step,
161 composite_aggregation,
162 composite_aggregation_source,
163 )
164 }
165
166 #[pyo3(name = "is_standard")]
167 fn py_is_standard(&self) -> bool {
168 self.is_standard()
169 }
170
171 #[pyo3(name = "is_composite")]
172 fn py_is_composite(&self) -> bool {
173 self.is_composite()
174 }
175
176 #[pyo3(name = "standard")]
177 fn py_standard(&self) -> Self {
178 self.standard()
179 }
180
181 #[pyo3(name = "composite")]
182 fn py_composite(&self) -> Self {
183 self.composite()
184 }
185}
186
187impl Bar {
188 pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
194 let bar_type_obj: Bound<'_, PyAny> = obj.getattr("bar_type")?.extract()?;
195 let bar_type_str: String = bar_type_obj.call_method0("__str__")?.extract()?;
196 let bar_type = BarType::from(bar_type_str.as_str());
197
198 let open_py: Bound<'_, PyAny> = obj.getattr("open")?;
199 let price_prec: u8 = open_py.getattr("precision")?.extract()?;
200 let open_raw: PriceRaw = open_py.getattr("raw")?.extract()?;
201 let open = Price::from_raw(open_raw, price_prec);
202
203 let high_py: Bound<'_, PyAny> = obj.getattr("high")?;
204 let high_raw: PriceRaw = high_py.getattr("raw")?.extract()?;
205 let high = Price::from_raw(high_raw, price_prec);
206
207 let low_py: Bound<'_, PyAny> = obj.getattr("low")?;
208 let low_raw: PriceRaw = low_py.getattr("raw")?.extract()?;
209 let low = Price::from_raw(low_raw, price_prec);
210
211 let close_py: Bound<'_, PyAny> = obj.getattr("close")?;
212 let close_raw: PriceRaw = close_py.getattr("raw")?.extract()?;
213 let close = Price::from_raw(close_raw, price_prec);
214
215 let volume_py: Bound<'_, PyAny> = obj.getattr("volume")?;
216 let volume_raw: QuantityRaw = volume_py.getattr("raw")?.extract()?;
217 let volume_prec: u8 = volume_py.getattr("precision")?.extract()?;
218 let volume = Quantity::from_raw(volume_raw, volume_prec);
219
220 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
221 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
222
223 Ok(Self::new(
224 bar_type,
225 open,
226 high,
227 low,
228 close,
229 volume,
230 ts_event.into(),
231 ts_init.into(),
232 ))
233 }
234}
235
236#[pymethods]
237#[allow(clippy::too_many_arguments)]
238impl Bar {
239 #[new]
240 fn py_new(
241 bar_type: BarType,
242 open: Price,
243 high: Price,
244 low: Price,
245 close: Price,
246 volume: Quantity,
247 ts_event: u64,
248 ts_init: u64,
249 ) -> PyResult<Self> {
250 Self::new_checked(
251 bar_type,
252 open,
253 high,
254 low,
255 close,
256 volume,
257 ts_event.into(),
258 ts_init.into(),
259 )
260 .map_err(to_pyvalue_err)
261 }
262
263 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
264 match op {
265 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
266 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
267 _ => py.NotImplemented(),
268 }
269 }
270
271 fn __hash__(&self) -> isize {
272 let mut h = DefaultHasher::new();
273 self.hash(&mut h);
274 h.finish() as isize
275 }
276
277 fn __repr__(&self) -> String {
278 format!("{self:?}")
279 }
280
281 fn __str__(&self) -> String {
282 self.to_string()
283 }
284
285 #[getter]
286 #[pyo3(name = "bar_type")]
287 fn py_bar_type(&self) -> BarType {
288 self.bar_type
289 }
290
291 #[getter]
292 #[pyo3(name = "open")]
293 fn py_open(&self) -> Price {
294 self.open
295 }
296
297 #[getter]
298 #[pyo3(name = "high")]
299 fn py_high(&self) -> Price {
300 self.high
301 }
302
303 #[getter]
304 #[pyo3(name = "low")]
305 fn py_low(&self) -> Price {
306 self.low
307 }
308
309 #[getter]
310 #[pyo3(name = "close")]
311 fn py_close(&self) -> Price {
312 self.close
313 }
314
315 #[getter]
316 #[pyo3(name = "volume")]
317 fn py_volume(&self) -> Quantity {
318 self.volume
319 }
320
321 #[getter]
322 #[pyo3(name = "ts_event")]
323 fn py_ts_event(&self) -> u64 {
324 self.ts_event.as_u64()
325 }
326
327 #[getter]
328 #[pyo3(name = "ts_init")]
329 fn py_ts_init(&self) -> u64 {
330 self.ts_init.as_u64()
331 }
332
333 #[staticmethod]
334 #[pyo3(name = "fully_qualified_name")]
335 fn py_fully_qualified_name() -> String {
336 format!("{}:{}", PY_MODULE_MODEL, stringify!(Bar))
337 }
338
339 #[staticmethod]
340 #[pyo3(name = "get_metadata")]
341 fn py_get_metadata(
342 bar_type: &BarType,
343 price_precision: u8,
344 size_precision: u8,
345 ) -> PyResult<HashMap<String, String>> {
346 Ok(Self::get_metadata(
347 bar_type,
348 price_precision,
349 size_precision,
350 ))
351 }
352
353 #[staticmethod]
354 #[pyo3(name = "get_fields")]
355 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
356 let py_dict = PyDict::new(py);
357 for (k, v) in Self::get_fields() {
358 py_dict.set_item(k, v)?;
359 }
360
361 Ok(py_dict)
362 }
363
364 #[staticmethod]
366 #[pyo3(name = "from_dict")]
367 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
368 from_dict_pyo3(py, values)
369 }
370
371 #[staticmethod]
372 #[pyo3(name = "from_json")]
373 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
374 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
375 }
376
377 #[staticmethod]
378 #[pyo3(name = "from_msgpack")]
379 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
380 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
381 }
382
383 #[pyo3(name = "as_pycapsule")]
399 fn py_as_pycapsule(&self, py: Python<'_>) -> Py<PyAny> {
400 data_to_pycapsule(py, Data::Bar(*self))
401 }
402
403 #[pyo3(name = "to_dict")]
405 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
406 to_dict_pyo3(py, self)
407 }
408
409 #[pyo3(name = "to_json_bytes")]
411 fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
412 self.to_json_bytes().unwrap().into_py_any_unwrap(py)
414 }
415
416 #[pyo3(name = "to_msgpack_bytes")]
418 fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
419 self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
421 }
422}
423
424#[cfg(test)]
428mod tests {
429 use nautilus_core::python::IntoPyObjectNautilusExt;
430 use pyo3::Python;
431 use rstest::rstest;
432
433 use crate::{
434 data::{Bar, BarType},
435 types::{Price, Quantity},
436 };
437
438 #[rstest]
439 #[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(
445 #[case] open: &str,
446 #[case] high: &str,
447 #[case] low: &str,
448 #[case] close: &str,
449 ) {
450 let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL");
451 let open = Price::from(open);
452 let high = Price::from(high);
453 let low = Price::from(low);
454 let close = Price::from(close);
455 let volume = Quantity::from(100_000);
456 let ts_event = 0;
457 let ts_init = 1;
458
459 let result = Bar::py_new(bar_type, open, high, low, close, volume, ts_event, ts_init);
460 assert!(result.is_err());
461 }
462
463 #[rstest]
464 fn test_bar_py_new() {
465 let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL");
466 let open = Price::from("1.00005");
467 let high = Price::from("1.00010");
468 let low = Price::from("1.00000");
469 let close = Price::from("1.00007");
470 let volume = Quantity::from(100_000);
471 let ts_event = 0;
472 let ts_init = 1;
473
474 let result = Bar::py_new(bar_type, open, high, low, close, volume, ts_event, ts_init);
475 assert!(result.is_ok());
476 }
477
478 #[rstest]
479 fn test_to_dict() {
480 let bar = Bar::default();
481
482 Python::initialize();
483 Python::attach(|py| {
484 let dict_string = bar.py_to_dict(py).unwrap().to_string();
485 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}";
486 assert_eq!(dict_string, expected_string);
487 });
488 }
489
490 #[rstest]
491 fn test_as_from_dict() {
492 let bar = Bar::default();
493
494 Python::initialize();
495 Python::attach(|py| {
496 let dict = bar.py_to_dict(py).unwrap();
497 let parsed = Bar::py_from_dict(py, dict).unwrap();
498 assert_eq!(parsed, bar);
499 });
500 }
501
502 #[rstest]
503 fn test_from_pyobject() {
504 let bar = Bar::default();
505
506 Python::initialize();
507 Python::attach(|py| {
508 let bar_pyobject = bar.into_py_any_unwrap(py);
509 let parsed_bar = Bar::from_pyobject(bar_pyobject.bind(py)).unwrap();
510 assert_eq!(parsed_bar, bar);
511 });
512 }
513}