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