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