nautilus_model/python/types/
price.rs1use std::{
17 collections::hash_map::DefaultHasher,
18 hash::{Hash, Hasher},
19 str::FromStr,
20};
21
22use nautilus_core::python::{get_pytype_name, to_pytype_err, to_pyvalue_err};
23use pyo3::{basic::CompareOp, conversion::IntoPyObjectExt, prelude::*, types::PyFloat};
24use rust_decimal::{Decimal, RoundingStrategy};
25
26#[cfg(not(feature = "high-precision"))]
27use crate::types::fixed::fixed_i64_to_f64;
28#[cfg(feature = "high-precision")]
29use crate::types::fixed::fixed_i128_to_f64;
30use crate::types::price::{Price, PriceRaw};
31
32#[pymethods]
33#[pyo3_stub_gen::derive::gen_stub_pymethods]
34impl Price {
35 #[new]
46 fn py_new(value: f64, precision: u8) -> PyResult<Self> {
47 Self::new_checked(value, precision).map_err(to_pyvalue_err)
48 }
49
50 fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
51 let from_raw = py.get_type::<Self>().getattr("from_raw")?;
52 let args = (self.raw, self.precision).into_py_any(py)?;
53 (from_raw, args).into_py_any(py)
54 }
55
56 fn __richcmp__(
57 &self,
58 other: &Bound<'_, PyAny>,
59 op: CompareOp,
60 py: Python<'_>,
61 ) -> PyResult<Py<PyAny>> {
62 if let Ok(other_price) = other.extract::<Self>() {
63 let result = match op {
64 CompareOp::Eq => self.eq(&other_price),
65 CompareOp::Ne => self.ne(&other_price),
66 CompareOp::Ge => self.ge(&other_price),
67 CompareOp::Gt => self.gt(&other_price),
68 CompareOp::Le => self.le(&other_price),
69 CompareOp::Lt => self.lt(&other_price),
70 };
71 result.into_py_any(py)
72 } else if let Ok(other_dec) = other.extract::<Decimal>() {
73 let result = match op {
74 CompareOp::Eq => self.as_decimal() == other_dec,
75 CompareOp::Ne => self.as_decimal() != other_dec,
76 CompareOp::Ge => self.as_decimal() >= other_dec,
77 CompareOp::Gt => self.as_decimal() > other_dec,
78 CompareOp::Le => self.as_decimal() <= other_dec,
79 CompareOp::Lt => self.as_decimal() < other_dec,
80 };
81 result.into_py_any(py)
82 } else {
83 Ok(py.NotImplemented())
84 }
85 }
86
87 fn __hash__(&self) -> isize {
88 let mut h = DefaultHasher::new();
89 self.hash(&mut h);
90 h.finish() as isize
91 }
92
93 fn __add__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
94 if other.is_instance_of::<PyFloat>() {
95 let other_float: f64 = other.extract()?;
96 (self.as_f64() + other_float).into_py_any(py)
97 } else if let Ok(other_price) = other.extract::<Self>() {
98 (*self + other_price).into_py_any(py)
99 } else if let Ok(other_dec) = other.extract::<Decimal>() {
100 (self.as_decimal() + other_dec).into_py_any(py)
101 } else {
102 let pytype_name = get_pytype_name(other)?;
103 Err(to_pytype_err(format!(
104 "Unsupported type for __add__, was `{pytype_name}`"
105 )))
106 }
107 }
108
109 fn __radd__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
110 if other.is_instance_of::<PyFloat>() {
111 let other_float: f64 = other.extract()?;
112 (other_float + self.as_f64()).into_py_any(py)
113 } else if let Ok(other_price) = other.extract::<Self>() {
114 (other_price + *self).into_py_any(py)
115 } else if let Ok(other_dec) = other.extract::<Decimal>() {
116 (other_dec + self.as_decimal()).into_py_any(py)
117 } else {
118 let pytype_name = get_pytype_name(other)?;
119 Err(to_pytype_err(format!(
120 "Unsupported type for __radd__, was `{pytype_name}`"
121 )))
122 }
123 }
124
125 fn __sub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
126 if other.is_instance_of::<PyFloat>() {
127 let other_float: f64 = other.extract()?;
128 (self.as_f64() - other_float).into_py_any(py)
129 } else if let Ok(other_price) = other.extract::<Self>() {
130 (*self - other_price).into_py_any(py)
131 } else if let Ok(other_dec) = other.extract::<Decimal>() {
132 (self.as_decimal() - other_dec).into_py_any(py)
133 } else {
134 let pytype_name = get_pytype_name(other)?;
135 Err(to_pytype_err(format!(
136 "Unsupported type for __sub__, was `{pytype_name}`"
137 )))
138 }
139 }
140
141 fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
142 if other.is_instance_of::<PyFloat>() {
143 let other_float: f64 = other.extract()?;
144 (other_float - self.as_f64()).into_py_any(py)
145 } else if let Ok(other_price) = other.extract::<Self>() {
146 (other_price - *self).into_py_any(py)
147 } else if let Ok(other_dec) = other.extract::<Decimal>() {
148 (other_dec - self.as_decimal()).into_py_any(py)
149 } else {
150 let pytype_name = get_pytype_name(other)?;
151 Err(to_pytype_err(format!(
152 "Unsupported type for __rsub__, was `{pytype_name}`"
153 )))
154 }
155 }
156
157 fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
158 if other.is_instance_of::<PyFloat>() {
159 let other_float: f64 = other.extract()?;
160 (self.as_f64() * other_float).into_py_any(py)
161 } else if let Ok(other_price) = other.extract::<Self>() {
162 (self.as_decimal() * other_price.as_decimal()).into_py_any(py)
163 } else if let Ok(other_dec) = other.extract::<Decimal>() {
164 (self.as_decimal() * other_dec).into_py_any(py)
165 } else {
166 let pytype_name = get_pytype_name(other)?;
167 Err(to_pytype_err(format!(
168 "Unsupported type for __mul__, was `{pytype_name}`"
169 )))
170 }
171 }
172
173 fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
174 if other.is_instance_of::<PyFloat>() {
175 let other_float: f64 = other.extract()?;
176 (other_float * self.as_f64()).into_py_any(py)
177 } else if let Ok(other_price) = other.extract::<Self>() {
178 (other_price.as_decimal() * self.as_decimal()).into_py_any(py)
179 } else if let Ok(other_dec) = other.extract::<Decimal>() {
180 (other_dec * self.as_decimal()).into_py_any(py)
181 } else {
182 let pytype_name = get_pytype_name(other)?;
183 Err(to_pytype_err(format!(
184 "Unsupported type for __rmul__, was `{pytype_name}`"
185 )))
186 }
187 }
188
189 fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
190 if other.is_instance_of::<PyFloat>() {
191 let other_float: f64 = other.extract()?;
192 (self.as_f64() / other_float).into_py_any(py)
193 } else if let Ok(other_price) = other.extract::<Self>() {
194 (self.as_decimal() / other_price.as_decimal()).into_py_any(py)
195 } else if let Ok(other_dec) = other.extract::<Decimal>() {
196 (self.as_decimal() / other_dec).into_py_any(py)
197 } else {
198 let pytype_name = get_pytype_name(other)?;
199 Err(to_pytype_err(format!(
200 "Unsupported type for __truediv__, was `{pytype_name}`"
201 )))
202 }
203 }
204
205 fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
206 if other.is_instance_of::<PyFloat>() {
207 let other_float: f64 = other.extract()?;
208 (other_float / self.as_f64()).into_py_any(py)
209 } else if let Ok(other_price) = other.extract::<Self>() {
210 (other_price.as_decimal() / self.as_decimal()).into_py_any(py)
211 } else if let Ok(other_dec) = other.extract::<Decimal>() {
212 (other_dec / self.as_decimal()).into_py_any(py)
213 } else {
214 let pytype_name = get_pytype_name(other)?;
215 Err(to_pytype_err(format!(
216 "Unsupported type for __rtruediv__, was `{pytype_name}`"
217 )))
218 }
219 }
220
221 fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
222 if other.is_instance_of::<PyFloat>() {
223 let other_float: f64 = other.extract()?;
224 (self.as_f64() / other_float).floor().into_py_any(py)
225 } else if let Ok(other_price) = other.extract::<Self>() {
226 (self.as_decimal() / other_price.as_decimal())
227 .floor()
228 .into_py_any(py)
229 } else if let Ok(other_dec) = other.extract::<Decimal>() {
230 (self.as_decimal() / other_dec).floor().into_py_any(py)
231 } else {
232 let pytype_name = get_pytype_name(other)?;
233 Err(to_pytype_err(format!(
234 "Unsupported type for __floordiv__, was `{pytype_name}`"
235 )))
236 }
237 }
238
239 fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
240 if other.is_instance_of::<PyFloat>() {
241 let other_float: f64 = other.extract()?;
242 (other_float / self.as_f64()).floor().into_py_any(py)
243 } else if let Ok(other_price) = other.extract::<Self>() {
244 (other_price.as_decimal() / self.as_decimal())
245 .floor()
246 .into_py_any(py)
247 } else if let Ok(other_dec) = other.extract::<Decimal>() {
248 (other_dec / self.as_decimal()).floor().into_py_any(py)
249 } else {
250 let pytype_name = get_pytype_name(other)?;
251 Err(to_pytype_err(format!(
252 "Unsupported type for __rfloordiv__, was `{pytype_name}`"
253 )))
254 }
255 }
256
257 fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
258 if other.is_instance_of::<PyFloat>() {
259 let other_float: f64 = other.extract()?;
260 (self.as_f64() % other_float).into_py_any(py)
261 } else if let Ok(other_price) = other.extract::<Self>() {
262 (self.as_decimal() % other_price.as_decimal()).into_py_any(py)
263 } else if let Ok(other_dec) = other.extract::<Decimal>() {
264 (self.as_decimal() % other_dec).into_py_any(py)
265 } else {
266 let pytype_name = get_pytype_name(other)?;
267 Err(to_pytype_err(format!(
268 "Unsupported type for __mod__, was `{pytype_name}`"
269 )))
270 }
271 }
272
273 fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
274 if other.is_instance_of::<PyFloat>() {
275 let other_float: f64 = other.extract()?;
276 (other_float % self.as_f64()).into_py_any(py)
277 } else if let Ok(other_price) = other.extract::<Self>() {
278 (other_price.as_decimal() % self.as_decimal()).into_py_any(py)
279 } else if let Ok(other_dec) = other.extract::<Decimal>() {
280 (other_dec % self.as_decimal()).into_py_any(py)
281 } else {
282 let pytype_name = get_pytype_name(other)?;
283 Err(to_pytype_err(format!(
284 "Unsupported type for __rmod__, was `{pytype_name}`"
285 )))
286 }
287 }
288
289 fn __neg__(&self) -> Self {
290 -*self
291 }
292
293 fn __pos__(&self) -> Self {
294 *self
295 }
296
297 fn __abs__(&self) -> Self {
298 if self.raw < 0 { -*self } else { *self }
299 }
300
301 fn __int__(&self) -> i64 {
302 self.as_f64() as i64
303 }
304
305 fn __float__(&self) -> f64 {
306 self.as_f64()
307 }
308
309 #[pyo3(signature = (ndigits=None))]
310 fn __round__(&self, ndigits: Option<u32>) -> Decimal {
311 self.as_decimal()
312 .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
313 }
314
315 fn __repr__(&self) -> String {
316 format!("{self:?}")
317 }
318
319 fn __str__(&self) -> String {
320 self.to_string()
321 }
322
323 #[getter]
324 fn raw(&self) -> PriceRaw {
325 self.raw
326 }
327
328 #[getter]
329 fn precision(&self) -> u8 {
330 self.precision
331 }
332
333 #[staticmethod]
335 #[pyo3(name = "from_raw")]
336 fn py_from_raw(raw: PriceRaw, precision: u8) -> Self {
337 Self::from_raw(raw, precision)
338 }
339
340 #[staticmethod]
342 #[pyo3(name = "zero")]
343 #[pyo3(signature = (precision = 0))]
344 fn py_zero(precision: u8) -> PyResult<Self> {
345 Self::new_checked(0.0, precision).map_err(to_pyvalue_err)
346 }
347
348 #[staticmethod]
349 #[pyo3(name = "from_int")]
350 fn py_from_int(value: u64) -> PyResult<Self> {
351 Self::new_checked(value as f64, 0).map_err(to_pyvalue_err)
352 }
353
354 #[staticmethod]
355 #[pyo3(name = "from_str")]
356 fn py_from_str(value: &str) -> PyResult<Self> {
357 Self::from_str(value).map_err(to_pyvalue_err)
358 }
359
360 #[staticmethod]
372 #[pyo3(name = "from_decimal")]
373 fn py_from_decimal(decimal: Decimal) -> PyResult<Self> {
374 Self::from_decimal(decimal).map_err(to_pyvalue_err)
375 }
376
377 #[staticmethod]
389 #[pyo3(name = "from_decimal_dp")]
390 fn py_from_decimal_dp(decimal: Decimal, precision: u8) -> PyResult<Self> {
391 Self::from_decimal_dp(decimal, precision).map_err(to_pyvalue_err)
392 }
393
394 #[staticmethod]
399 #[pyo3(name = "from_mantissa_exponent")]
400 fn py_from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> Self {
401 Self::from_mantissa_exponent(mantissa, exponent, precision)
402 }
403
404 #[pyo3(name = "is_zero")]
406 fn py_is_zero(&self) -> bool {
407 self.is_zero()
408 }
409
410 #[pyo3(name = "is_positive")]
412 fn py_is_positive(&self) -> bool {
413 self.is_positive()
414 }
415
416 #[pyo3(name = "as_decimal")]
418 fn py_as_decimal(&self) -> Decimal {
419 self.as_decimal()
420 }
421
422 #[pyo3(name = "to_formatted_str")]
423 fn py_to_formatted_str(&self) -> String {
424 self.to_formatted_string()
425 }
426
427 #[pyo3(name = "checked_add")]
435 fn py_checked_add(&self, other: Self) -> Option<Self> {
436 self.checked_add(other)
437 }
438
439 #[pyo3(name = "checked_sub")]
447 fn py_checked_sub(&self, other: Self) -> Option<Self> {
448 self.checked_sub(other)
449 }
450}
451
452#[pymethods]
453impl Price {
454 #[cfg(feature = "high-precision")]
455 #[pyo3(name = "as_double")]
456 fn py_as_double(&self) -> f64 {
457 fixed_i128_to_f64(self.raw)
458 }
459
460 #[cfg(not(feature = "high-precision"))]
461 #[pyo3(name = "as_double")]
462 fn py_as_double(&self) -> f64 {
463 fixed_i64_to_f64(self.raw)
464 }
465}