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