nautilus_model/python/types/
money.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::{
25 IntoPyObjectExt,
26 exceptions::PyValueError,
27 prelude::*,
28 pyclass::CompareOp,
29 types::{PyFloat, PyInt, PyString, PyTuple},
30};
31use rust_decimal::{Decimal, RoundingStrategy};
32
33use crate::types::{Currency, Money, money::MoneyRaw};
34
35#[pymethods]
36impl Money {
37 #[new]
38 fn py_new(amount: f64, currency: Currency) -> PyResult<Self> {
39 Self::new_checked(amount, currency).map_err(to_pyvalue_err)
40 }
41
42 fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
43 let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
44 self.raw = py_tuple
45 .get_item(0)?
46 .downcast::<PyInt>()?
47 .extract::<MoneyRaw>()?;
48 let currency_code: String = py_tuple
49 .get_item(1)?
50 .downcast::<PyString>()?
51 .extract::<String>()?;
52 self.currency = Currency::from_str(currency_code.as_str()).map_err(to_pyvalue_err)?;
53 Ok(())
54 }
55
56 fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
57 (self.raw, self.currency.code.to_string()).into_py_any(py)
58 }
59
60 fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
61 let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
62 let state = self.__getstate__(py)?;
63 (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
64 }
65
66 #[staticmethod]
67 fn _safe_constructor() -> Self {
68 Self::new(0.0, Currency::AUD())
69 }
70
71 fn __add__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
72 if other.as_ref().is_instance_of::<PyFloat>() {
73 let other_float: f64 = other.extract::<f64>()?;
74 (self.as_f64() + other_float).into_py_any(py)
75 } else if let Ok(other_qty) = other.extract::<Self>() {
76 (self.as_decimal() + other_qty.as_decimal()).into_py_any(py)
77 } else if let Ok(other_dec) = other.extract::<Decimal>() {
78 (self.as_decimal() + other_dec).into_py_any(py)
79 } else {
80 let pytype_name = get_pytype_name(other)?;
81 Err(to_pytype_err(format!(
82 "Unsupported type for __add__, was `{pytype_name}`"
83 )))
84 }
85 }
86
87 fn __radd__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
88 if other.as_ref().is_instance_of::<PyFloat>() {
89 let other_float: f64 = other.extract()?;
90 (other_float + self.as_f64()).into_py_any(py)
91 } else if let Ok(other_qty) = other.extract::<Self>() {
92 (other_qty.as_decimal() + self.as_decimal()).into_py_any(py)
93 } else if let Ok(other_dec) = other.extract::<Decimal>() {
94 (other_dec + self.as_decimal()).into_py_any(py)
95 } else {
96 let pytype_name = get_pytype_name(other)?;
97 Err(to_pytype_err(format!(
98 "Unsupported type for __radd__, was `{pytype_name}`"
99 )))
100 }
101 }
102
103 fn __sub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
104 if other.as_ref().is_instance_of::<PyFloat>() {
105 let other_float: f64 = other.extract()?;
106 (self.as_f64() - other_float).into_py_any(py)
107 } else if let Ok(other_qty) = other.extract::<Self>() {
108 (self.as_decimal() - other_qty.as_decimal()).into_py_any(py)
109 } else if let Ok(other_dec) = other.extract::<Decimal>() {
110 (self.as_decimal() - other_dec).into_py_any(py)
111 } else {
112 let pytype_name = get_pytype_name(other)?;
113 Err(to_pytype_err(format!(
114 "Unsupported type for __sub__, was `{pytype_name}`"
115 )))
116 }
117 }
118
119 fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
120 if other.as_ref().is_instance_of::<PyFloat>() {
121 let other_float: f64 = other.extract()?;
122 (other_float - self.as_f64()).into_py_any(py)
123 } else if let Ok(other_qty) = other.extract::<Self>() {
124 (other_qty.as_decimal() - self.as_decimal()).into_py_any(py)
125 } else if let Ok(other_dec) = other.extract::<Decimal>() {
126 (other_dec - self.as_decimal()).into_py_any(py)
127 } else {
128 let pytype_name = get_pytype_name(other)?;
129 Err(to_pytype_err(format!(
130 "Unsupported type for __rsub__, was `{pytype_name}`"
131 )))
132 }
133 }
134
135 fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
136 if other.as_ref().is_instance_of::<PyFloat>() {
137 let other_float: f64 = other.extract()?;
138 (self.as_f64() * other_float).into_py_any(py)
139 } else if let Ok(other_qty) = other.extract::<Self>() {
140 (self.as_decimal() * other_qty.as_decimal()).into_py_any(py)
141 } else if let Ok(other_dec) = other.extract::<Decimal>() {
142 (self.as_decimal() * other_dec).into_py_any(py)
143 } else {
144 let pytype_name = get_pytype_name(other)?;
145 Err(to_pytype_err(format!(
146 "Unsupported type for __mul__, was `{pytype_name}`"
147 )))
148 }
149 }
150
151 fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
152 if other.as_ref().is_instance_of::<PyFloat>() {
153 let other_float: f64 = other.extract()?;
154 (other_float * self.as_f64()).into_py_any(py)
155 } else if let Ok(other_qty) = other.extract::<Self>() {
156 (other_qty.as_decimal() * self.as_decimal()).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 __rmul__, was `{pytype_name}`"
163 )))
164 }
165 }
166
167 fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
168 if other.as_ref().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 __truediv__, was `{pytype_name}`"
179 )))
180 }
181 }
182
183 fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
184 if other.as_ref().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 __rtruediv__, was `{pytype_name}`"
195 )))
196 }
197 }
198
199 fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
200 if other.as_ref().is_instance_of::<PyFloat>() {
201 let other_float: f64 = other.extract()?;
202 (self.as_f64() / other_float).floor().into_py_any(py)
203 } else if let Ok(other_qty) = other.extract::<Self>() {
204 (self.as_decimal() / other_qty.as_decimal())
205 .floor()
206 .into_py_any(py)
207 } else if let Ok(other_dec) = other.extract::<Decimal>() {
208 (self.as_decimal() / other_dec).floor().into_py_any(py)
209 } else {
210 let pytype_name = get_pytype_name(other)?;
211 Err(to_pytype_err(format!(
212 "Unsupported type for __floordiv__, was `{pytype_name}`"
213 )))
214 }
215 }
216
217 fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
218 if other.as_ref().is_instance_of::<PyFloat>() {
219 let other_float: f64 = other.extract()?;
220 (other_float / self.as_f64()).floor().into_py_any(py)
221 } else if let Ok(other_qty) = other.extract::<Self>() {
222 (other_qty.as_decimal() / self.as_decimal())
223 .floor()
224 .into_py_any(py)
225 } else if let Ok(other_dec) = other.extract::<Decimal>() {
226 (other_dec / self.as_decimal()).floor().into_py_any(py)
227 } else {
228 let pytype_name = get_pytype_name(other)?;
229 Err(to_pytype_err(format!(
230 "Unsupported type for __rfloordiv__, was `{pytype_name}`"
231 )))
232 }
233 }
234
235 fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
236 if other.as_ref().is_instance_of::<PyFloat>() {
237 let other_float: f64 = other.extract()?;
238 (self.as_f64() % other_float).into_py_any(py)
239 } else if let Ok(other_qty) = other.extract::<Self>() {
240 (self.as_decimal() % other_qty.as_decimal()).into_py_any(py)
241 } else if let Ok(other_dec) = other.extract::<Decimal>() {
242 (self.as_decimal() % other_dec).into_py_any(py)
243 } else {
244 let pytype_name = get_pytype_name(other)?;
245 Err(to_pytype_err(format!(
246 "Unsupported type for __mod__, was `{pytype_name}`"
247 )))
248 }
249 }
250
251 fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
252 if other.as_ref().is_instance_of::<PyFloat>() {
253 let other_float: f64 = other.extract()?;
254 (other_float % self.as_f64()).into_py_any(py)
255 } else if let Ok(other_qty) = other.extract::<Self>() {
256 (other_qty.as_decimal() % self.as_decimal()).into_py_any(py)
257 } else if let Ok(other_dec) = other.extract::<Decimal>() {
258 (other_dec % self.as_decimal()).into_py_any(py)
259 } else {
260 let pytype_name = get_pytype_name(other)?;
261 Err(to_pytype_err(format!(
262 "Unsupported type for __rmod__, was `{pytype_name}`"
263 )))
264 }
265 }
266
267 fn __neg__(&self) -> Decimal {
268 self.as_decimal().neg()
269 }
270
271 fn __pos__(&self) -> Decimal {
272 let mut value = self.as_decimal();
273 value.set_sign_positive(true);
274 value
275 }
276
277 fn __abs__(&self) -> Decimal {
278 self.as_decimal().abs()
279 }
280
281 fn __int__(&self) -> u64 {
282 self.as_f64() as u64
283 }
284
285 fn __float__(&self) -> f64 {
286 self.as_f64()
287 }
288
289 #[pyo3(signature = (ndigits=None))]
290 fn __round__(&self, ndigits: Option<u32>) -> Decimal {
291 self.as_decimal()
292 .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
293 }
294
295 fn __richcmp__(&self, other: PyObject, op: CompareOp, py: Python<'_>) -> PyResult<Py<PyAny>> {
296 if let Ok(other_money) = other.extract::<Self>(py) {
297 if self.currency != other_money.currency {
298 return Err(PyErr::new::<PyValueError, _>(
299 "Cannot compare `Money` with different currencies",
300 ));
301 }
302
303 let result = match op {
304 CompareOp::Eq => self.eq(&other_money),
305 CompareOp::Ne => self.ne(&other_money),
306 CompareOp::Ge => self.ge(&other_money),
307 CompareOp::Gt => self.gt(&other_money),
308 CompareOp::Le => self.le(&other_money),
309 CompareOp::Lt => self.lt(&other_money),
310 };
311 result.into_py_any(py)
312 } else {
313 Ok(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) -> MoneyRaw {
333 self.raw
334 }
335
336 #[getter]
337 fn currency(&self) -> Currency {
338 self.currency
339 }
340
341 #[staticmethod]
342 #[pyo3(name = "zero")]
343 fn py_zero(currency: Currency) -> Self {
344 Self::new(0.0, currency)
345 }
346
347 #[staticmethod]
348 #[pyo3(name = "from_raw")]
349 fn py_from_raw(raw: MoneyRaw, currency: Currency) -> PyResult<Self> {
350 Ok(Self::from_raw(raw, currency))
351 }
352
353 #[staticmethod]
354 #[pyo3(name = "from_str")]
355 fn py_from_str(value: &str) -> PyResult<Self> {
356 Self::from_str(value).map_err(to_pyvalue_err)
357 }
358
359 #[pyo3(name = "is_zero")]
360 fn py_is_zero(&self) -> bool {
361 self.is_zero()
362 }
363
364 #[pyo3(name = "as_decimal")]
365 fn py_as_decimal(&self) -> Decimal {
366 self.as_decimal()
367 }
368
369 #[pyo3(name = "as_double")]
370 fn py_as_double(&self) -> f64 {
371 self.as_f64()
372 }
373
374 #[pyo3(name = "to_formatted_str")]
375 fn py_to_formatted_str(&self) -> String {
376 self.to_formatted_string()
377 }
378}