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
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> {
173 let bar_type_obj: Bound<'_, PyAny> = obj.getattr("bar_type")?.extract()?;
174 let bar_type_str: String = bar_type_obj.call_method0("__str__")?.extract()?;
175 let bar_type = BarType::from(bar_type_str.as_str());
176
177 let open_py: Bound<'_, PyAny> = obj.getattr("open")?;
178 let price_prec: u8 = open_py.getattr("precision")?.extract()?;
179 let open_raw: PriceRaw = open_py.getattr("raw")?.extract()?;
180 let open = Price::from_raw(open_raw, price_prec);
181
182 let high_py: Bound<'_, PyAny> = obj.getattr("high")?;
183 let high_raw: PriceRaw = high_py.getattr("raw")?.extract()?;
184 let high = Price::from_raw(high_raw, price_prec);
185
186 let low_py: Bound<'_, PyAny> = obj.getattr("low")?;
187 let low_raw: PriceRaw = low_py.getattr("raw")?.extract()?;
188 let low = Price::from_raw(low_raw, price_prec);
189
190 let close_py: Bound<'_, PyAny> = obj.getattr("close")?;
191 let close_raw: PriceRaw = close_py.getattr("raw")?.extract()?;
192 let close = Price::from_raw(close_raw, price_prec);
193
194 let volume_py: Bound<'_, PyAny> = obj.getattr("volume")?;
195 let volume_raw: QuantityRaw = volume_py.getattr("raw")?.extract()?;
196 let volume_prec: u8 = volume_py.getattr("precision")?.extract()?;
197 let volume = Quantity::from_raw(volume_raw, volume_prec);
198
199 let ts_event: u64 = obj.getattr("ts_event")?.extract()?;
200 let ts_init: u64 = obj.getattr("ts_init")?.extract()?;
201
202 Ok(Self::new(
203 bar_type,
204 open,
205 high,
206 low,
207 close,
208 volume,
209 ts_event.into(),
210 ts_init.into(),
211 ))
212 }
213}
214
215#[pymethods]
216#[allow(clippy::too_many_arguments)]
217impl Bar {
218 #[new]
219 fn py_new(
220 bar_type: BarType,
221 open: Price,
222 high: Price,
223 low: Price,
224 close: Price,
225 volume: Quantity,
226 ts_event: u64,
227 ts_init: u64,
228 ) -> PyResult<Self> {
229 Self::new_checked(
230 bar_type,
231 open,
232 high,
233 low,
234 close,
235 volume,
236 ts_event.into(),
237 ts_init.into(),
238 )
239 .map_err(to_pyvalue_err)
240 }
241
242 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
243 match op {
244 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
245 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
246 _ => py.NotImplemented(),
247 }
248 }
249
250 fn __hash__(&self) -> isize {
251 let mut h = DefaultHasher::new();
252 self.hash(&mut h);
253 h.finish() as isize
254 }
255
256 fn __repr__(&self) -> String {
257 format!("{self:?}")
258 }
259
260 fn __str__(&self) -> String {
261 self.to_string()
262 }
263
264 #[getter]
265 #[pyo3(name = "bar_type")]
266 fn py_bar_type(&self) -> BarType {
267 self.bar_type
268 }
269
270 #[getter]
271 #[pyo3(name = "open")]
272 fn py_open(&self) -> Price {
273 self.open
274 }
275
276 #[getter]
277 #[pyo3(name = "high")]
278 fn py_high(&self) -> Price {
279 self.high
280 }
281
282 #[getter]
283 #[pyo3(name = "low")]
284 fn py_low(&self) -> Price {
285 self.low
286 }
287
288 #[getter]
289 #[pyo3(name = "close")]
290 fn py_close(&self) -> Price {
291 self.close
292 }
293
294 #[getter]
295 #[pyo3(name = "volume")]
296 fn py_volume(&self) -> Quantity {
297 self.volume
298 }
299
300 #[getter]
301 #[pyo3(name = "ts_event")]
302 fn py_ts_event(&self) -> u64 {
303 self.ts_event.as_u64()
304 }
305
306 #[getter]
307 #[pyo3(name = "ts_init")]
308 fn py_ts_init(&self) -> u64 {
309 self.ts_init.as_u64()
310 }
311
312 #[staticmethod]
313 #[pyo3(name = "fully_qualified_name")]
314 fn py_fully_qualified_name() -> String {
315 format!("{}:{}", PY_MODULE_MODEL, stringify!(Bar))
316 }
317
318 #[staticmethod]
319 #[pyo3(name = "get_metadata")]
320 fn py_get_metadata(
321 bar_type: &BarType,
322 price_precision: u8,
323 size_precision: u8,
324 ) -> PyResult<HashMap<String, String>> {
325 Ok(Self::get_metadata(
326 bar_type,
327 price_precision,
328 size_precision,
329 ))
330 }
331
332 #[staticmethod]
333 #[pyo3(name = "get_fields")]
334 fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
335 let py_dict = PyDict::new(py);
336 for (k, v) in Self::get_fields() {
337 py_dict.set_item(k, v)?;
338 }
339
340 Ok(py_dict)
341 }
342
343 #[staticmethod]
345 #[pyo3(name = "from_dict")]
346 fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
347 from_dict_pyo3(py, values)
348 }
349
350 #[staticmethod]
351 #[pyo3(name = "from_json")]
352 fn py_from_json(data: Vec<u8>) -> PyResult<Self> {
353 Self::from_json_bytes(&data).map_err(to_pyvalue_err)
354 }
355
356 #[staticmethod]
357 #[pyo3(name = "from_msgpack")]
358 fn py_from_msgpack(data: Vec<u8>) -> PyResult<Self> {
359 Self::from_msgpack_bytes(&data).map_err(to_pyvalue_err)
360 }
361
362 #[pyo3(name = "as_pycapsule")]
378 fn py_as_pycapsule(&self, py: Python<'_>) -> PyObject {
379 data_to_pycapsule(py, Data::Bar(*self))
380 }
381
382 #[pyo3(name = "as_dict")]
384 fn py_as_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
385 to_dict_pyo3(py, self)
386 }
387
388 #[pyo3(name = "as_json")]
390 fn py_as_json(&self, py: Python<'_>) -> Py<PyAny> {
391 self.as_json_bytes().unwrap().into_py_any_unwrap(py)
393 }
394
395 #[pyo3(name = "as_msgpack")]
397 fn py_as_msgpack(&self, py: Python<'_>) -> Py<PyAny> {
398 self.as_msgpack_bytes().unwrap().into_py_any_unwrap(py)
400 }
401}
402
403#[cfg(test)]
407mod tests {
408 use nautilus_core::python::IntoPyObjectNautilusExt;
409 use pyo3::Python;
410 use rstest::rstest;
411
412 use crate::{
413 data::{Bar, BarType},
414 types::{Price, Quantity},
415 };
416
417 #[rstest]
418 #[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(
424 #[case] open: &str,
425 #[case] high: &str,
426 #[case] low: &str,
427 #[case] close: &str,
428 ) {
429 pyo3::prepare_freethreaded_python();
430
431 let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL");
432 let open = Price::from(open);
433 let high = Price::from(high);
434 let low = Price::from(low);
435 let close = Price::from(close);
436 let volume = Quantity::from(100_000);
437 let ts_event = 0;
438 let ts_init = 1;
439
440 let result = Bar::py_new(bar_type, open, high, low, close, volume, ts_event, ts_init);
441 assert!(result.is_err());
442 }
443
444 #[rstest]
445 fn test_bar_py_new() {
446 pyo3::prepare_freethreaded_python();
447
448 let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL");
449 let open = Price::from("1.00005");
450 let high = Price::from("1.00010");
451 let low = Price::from("1.00000");
452 let close = Price::from("1.00007");
453 let volume = Quantity::from(100_000);
454 let ts_event = 0;
455 let ts_init = 1;
456
457 let result = Bar::py_new(bar_type, open, high, low, close, volume, ts_event, ts_init);
458 assert!(result.is_ok());
459 }
460
461 #[rstest]
462 fn test_as_dict() {
463 pyo3::prepare_freethreaded_python();
464 let bar = Bar::default();
465
466 Python::with_gil(|py| {
467 let dict_string = bar.py_as_dict(py).unwrap().to_string();
468 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}";
469 assert_eq!(dict_string, expected_string);
470 });
471 }
472
473 #[rstest]
474 fn test_as_from_dict() {
475 pyo3::prepare_freethreaded_python();
476 let bar = Bar::default();
477
478 Python::with_gil(|py| {
479 let dict = bar.py_as_dict(py).unwrap();
480 let parsed = Bar::py_from_dict(py, dict).unwrap();
481 assert_eq!(parsed, bar);
482 });
483 }
484
485 #[rstest]
486 fn test_from_pyobject() {
487 pyo3::prepare_freethreaded_python();
488 let bar = Bar::default();
489
490 Python::with_gil(|py| {
491 let bar_pyobject = bar.into_py_any_unwrap(py);
492 let parsed_bar = Bar::from_pyobject(bar_pyobject.bind(py)).unwrap();
493 assert_eq!(parsed_bar, bar);
494 });
495 }
496}