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 + other_qty).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 + *self).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 if other_qty.raw > self.raw {
116 return Err(to_pyvalue_err(format!(
117 "Quantity subtraction would result in negative value: {self} - {other_qty}"
118 )));
119 }
120 (*self - other_qty).into_py_any(py)
121 } else if let Ok(other_dec) = other.extract::<Decimal>() {
122 (self.as_decimal() - other_dec).into_py_any(py)
123 } else {
124 let pytype_name = get_pytype_name(other)?;
125 Err(to_pytype_err(format!(
126 "Unsupported type for __sub__, was `{pytype_name}`"
127 )))
128 }
129 }
130
131 fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
132 if other.is_instance_of::<PyFloat>() {
133 let other_float: f64 = other.extract()?;
134 (other_float - self.as_f64()).into_py_any(py)
135 } else if let Ok(other_qty) = other.extract::<Self>() {
136 if self.raw > other_qty.raw {
137 return Err(to_pyvalue_err(format!(
138 "Quantity subtraction would result in negative value: {other_qty} - {self}"
139 )));
140 }
141 (other_qty - *self).into_py_any(py)
142 } else if let Ok(other_dec) = other.extract::<Decimal>() {
143 (other_dec - self.as_decimal()).into_py_any(py)
144 } else {
145 let pytype_name = get_pytype_name(other)?;
146 Err(to_pytype_err(format!(
147 "Unsupported type for __rsub__, was `{pytype_name}`"
148 )))
149 }
150 }
151
152 fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
153 if other.is_instance_of::<PyFloat>() {
154 let other_float: f64 = other.extract()?;
155 (self.as_f64() * other_float).into_py_any(py)
156 } else if let Ok(other_qty) = other.extract::<Self>() {
157 (self.as_decimal() * other_qty.as_decimal()).into_py_any(py)
158 } else if let Ok(other_dec) = other.extract::<Decimal>() {
159 (self.as_decimal() * other_dec).into_py_any(py)
160 } else {
161 let pytype_name = get_pytype_name(other)?;
162 Err(to_pytype_err(format!(
163 "Unsupported type for __mul__, was `{pytype_name}`"
164 )))
165 }
166 }
167
168 fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
169 if other.is_instance_of::<PyFloat>() {
170 let other_float: f64 = other.extract()?;
171 (other_float * self.as_f64()).into_py_any(py)
172 } else if let Ok(other_qty) = other.extract::<Self>() {
173 (other_qty.as_decimal() * self.as_decimal()).into_py_any(py)
174 } else if let Ok(other_dec) = other.extract::<Decimal>() {
175 (other_dec * self.as_decimal()).into_py_any(py)
176 } else {
177 let pytype_name = get_pytype_name(other)?;
178 Err(to_pytype_err(format!(
179 "Unsupported type for __rmul__, was `{pytype_name}`"
180 )))
181 }
182 }
183
184 fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
185 if other.is_instance_of::<PyFloat>() {
186 let other_float: f64 = other.extract()?;
187 (self.as_f64() / other_float).into_py_any(py)
188 } else if let Ok(other_qty) = other.extract::<Self>() {
189 (self.as_decimal() / other_qty.as_decimal()).into_py_any(py)
190 } else if let Ok(other_dec) = other.extract::<Decimal>() {
191 (self.as_decimal() / other_dec).into_py_any(py)
192 } else {
193 let pytype_name = get_pytype_name(other)?;
194 Err(to_pytype_err(format!(
195 "Unsupported type for __truediv__, was `{pytype_name}`"
196 )))
197 }
198 }
199
200 fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
201 if other.is_instance_of::<PyFloat>() {
202 let other_float: f64 = other.extract()?;
203 (other_float / self.as_f64()).into_py_any(py)
204 } else if let Ok(other_qty) = other.extract::<Self>() {
205 (other_qty.as_decimal() / self.as_decimal()).into_py_any(py)
206 } else if let Ok(other_dec) = other.extract::<Decimal>() {
207 (other_dec / self.as_decimal()).into_py_any(py)
208 } else {
209 let pytype_name = get_pytype_name(other)?;
210 Err(to_pytype_err(format!(
211 "Unsupported type for __rtruediv__, was `{pytype_name}`"
212 )))
213 }
214 }
215
216 fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
217 if other.is_instance_of::<PyFloat>() {
218 let other_float: f64 = other.extract()?;
219 (self.as_f64() / other_float).floor().into_py_any(py)
220 } else if let Ok(other_qty) = other.extract::<Self>() {
221 (self.as_decimal() / other_qty.as_decimal())
222 .floor()
223 .into_py_any(py)
224 } else if let Ok(other_dec) = other.extract::<Decimal>() {
225 (self.as_decimal() / other_dec).floor().into_py_any(py)
226 } else {
227 let pytype_name = get_pytype_name(other)?;
228 Err(to_pytype_err(format!(
229 "Unsupported type for __floordiv__, was `{pytype_name}`"
230 )))
231 }
232 }
233
234 fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
235 if other.is_instance_of::<PyFloat>() {
236 let other_float: f64 = other.extract()?;
237 (other_float / self.as_f64()).floor().into_py_any(py)
238 } else if let Ok(other_qty) = other.extract::<Self>() {
239 (other_qty.as_decimal() / self.as_decimal())
240 .floor()
241 .into_py_any(py)
242 } else if let Ok(other_dec) = other.extract::<Decimal>() {
243 (other_dec / self.as_decimal()).floor().into_py_any(py)
244 } else {
245 let pytype_name = get_pytype_name(other)?;
246 Err(to_pytype_err(format!(
247 "Unsupported type for __rfloordiv__, was `{pytype_name}`"
248 )))
249 }
250 }
251
252 fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
253 if other.is_instance_of::<PyFloat>() {
254 let other_float: f64 = other.extract()?;
255 (self.as_f64() % other_float).into_py_any(py)
256 } else if let Ok(other_qty) = other.extract::<Self>() {
257 (self.as_decimal() % other_qty.as_decimal()).into_py_any(py)
258 } else if let Ok(other_dec) = other.extract::<Decimal>() {
259 (self.as_decimal() % other_dec).into_py_any(py)
260 } else {
261 let pytype_name = get_pytype_name(other)?;
262 Err(to_pytype_err(format!(
263 "Unsupported type for __mod__, was `{pytype_name}`"
264 )))
265 }
266 }
267
268 fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
269 if other.is_instance_of::<PyFloat>() {
270 let other_float: f64 = other.extract()?;
271 (other_float % self.as_f64()).into_py_any(py)
272 } else if let Ok(other_qty) = other.extract::<Self>() {
273 (other_qty.as_decimal() % self.as_decimal()).into_py_any(py)
274 } else if let Ok(other_dec) = other.extract::<Decimal>() {
275 (other_dec % self.as_decimal()).into_py_any(py)
276 } else {
277 let pytype_name = get_pytype_name(other)?;
278 Err(to_pytype_err(format!(
279 "Unsupported type for __rmod__, was `{pytype_name}`"
280 )))
281 }
282 }
283
284 fn __neg__(&self) -> Decimal {
285 self.as_decimal().neg()
286 }
287
288 fn __pos__(&self) -> Decimal {
289 let mut value = self.as_decimal();
290 value.set_sign_positive(true);
291 value
292 }
293
294 fn __abs__(&self) -> Decimal {
295 self.as_decimal().abs()
296 }
297
298 fn __int__(&self) -> u64 {
299 self.as_f64() as u64
300 }
301
302 fn __float__(&self) -> f64 {
303 self.as_f64()
304 }
305
306 #[pyo3(signature = (ndigits=None))]
307 fn __round__(&self, ndigits: Option<u32>) -> Decimal {
308 self.as_decimal()
309 .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
310 }
311
312 fn __repr__(&self) -> String {
313 format!("{self:?}")
314 }
315
316 fn __str__(&self) -> String {
317 self.to_string()
318 }
319
320 #[getter]
321 fn raw(&self) -> QuantityRaw {
322 self.raw
323 }
324
325 #[getter]
326 fn precision(&self) -> u8 {
327 self.precision
328 }
329
330 #[staticmethod]
331 #[pyo3(name = "from_raw")]
332 fn py_from_raw(raw: QuantityRaw, precision: u8) -> Self {
333 Self::from_raw(raw, precision)
334 }
335
336 #[staticmethod]
337 #[pyo3(name = "zero")]
338 #[pyo3(signature = (precision = 0))]
339 fn py_zero(precision: u8) -> PyResult<Self> {
340 Self::new_checked(0.0, precision).map_err(to_pyvalue_err)
341 }
342
343 #[staticmethod]
344 #[pyo3(name = "from_int")]
345 fn py_from_int(value: u64) -> PyResult<Self> {
346 Self::new_checked(value as f64, 0).map_err(to_pyvalue_err)
347 }
348
349 #[staticmethod]
350 #[pyo3(name = "from_str")]
351 fn py_from_str(value: &str) -> Self {
352 Self::from(value)
353 }
354
355 #[staticmethod]
356 #[pyo3(name = "from_decimal")]
357 fn py_from_decimal(decimal: Decimal) -> PyResult<Self> {
358 Self::from_decimal(decimal).map_err(to_pyvalue_err)
359 }
360
361 #[staticmethod]
362 #[pyo3(name = "from_decimal_dp")]
363 fn py_from_decimal_dp(decimal: Decimal, precision: u8) -> PyResult<Self> {
364 Self::from_decimal_dp(decimal, precision).map_err(to_pyvalue_err)
365 }
366
367 #[staticmethod]
368 #[pyo3(name = "from_mantissa_exponent")]
369 fn py_from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
370 Self::from_mantissa_exponent(mantissa, exponent, precision)
371 }
372
373 #[pyo3(name = "is_zero")]
374 fn py_is_zero(&self) -> bool {
375 self.is_zero()
376 }
377
378 #[pyo3(name = "is_positive")]
379 fn py_is_positive(&self) -> bool {
380 self.is_positive()
381 }
382
383 #[pyo3(name = "as_decimal")]
384 fn py_as_decimal(&self) -> Decimal {
385 self.as_decimal()
386 }
387
388 #[pyo3(name = "as_double")]
389 fn py_as_double(&self) -> f64 {
390 self.as_f64()
391 }
392
393 #[pyo3(name = "to_formatted_str")]
394 fn py_to_formatted_str(&self) -> String {
395 self.to_formatted_string()
396 }
397
398 #[pyo3(name = "saturating_sub")]
399 fn py_saturating_sub(&self, other: Self) -> Self {
400 self.saturating_sub(other)
401 }
402}