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