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