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