1use std::{
17 collections::hash_map::DefaultHasher,
18 hash::{Hash, Hasher},
19 str::FromStr,
20};
21
22use nautilus_core::python::{
23 correctness_error_to_pyvalue_err, get_pytype_name, to_pytype_err, to_pyvalue_err,
24};
25use pyo3::{basic::CompareOp, conversion::IntoPyObjectExt, prelude::*, types::PyFloat};
26use rust_decimal::{Decimal, RoundingStrategy};
27
28use super::fixed::{
29 ArithmeticError, ArithmeticOperation, FloatArithmetic, check_raw_scales,
30 extract_arithmetic_decimal,
31};
32use crate::types::{
33 fixed::raw_scale,
34 price::{Price, PriceRaw},
35};
36
37#[pymethods]
38#[pyo3_stub_gen::derive::gen_stub_pymethods]
39impl Price {
40 #[new]
51 fn py_new(value: f64, precision: u8) -> PyResult<Self> {
52 Self::new_checked(value, precision).map_err(to_pyvalue_err)
53 }
54
55 fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
56 let from_raw = py.get_type::<Self>().getattr("from_raw")?;
57 let args = (self.raw(), self.precision).into_py_any(py)?;
58 (from_raw, args).into_py_any(py)
59 }
60
61 fn __richcmp__(
62 &self,
63 other: &Bound<'_, PyAny>,
64 op: CompareOp,
65 py: Python<'_>,
66 ) -> PyResult<Py<PyAny>> {
67 if let Ok(other_price) = other.extract::<Self>() {
68 let result = match op {
69 CompareOp::Eq => self.eq(&other_price),
70 CompareOp::Ne => self.ne(&other_price),
71 CompareOp::Ge => self.ge(&other_price),
72 CompareOp::Gt => self.gt(&other_price),
73 CompareOp::Le => self.le(&other_price),
74 CompareOp::Lt => self.lt(&other_price),
75 };
76 result.into_py_any(py)
77 } else if let Ok(other_dec) = other.extract::<Decimal>() {
78 let result = match op {
79 CompareOp::Eq => self.as_decimal() == other_dec,
80 CompareOp::Ne => self.as_decimal() != other_dec,
81 CompareOp::Ge => self.as_decimal() >= other_dec,
82 CompareOp::Gt => self.as_decimal() > other_dec,
83 CompareOp::Le => self.as_decimal() <= other_dec,
84 CompareOp::Lt => self.as_decimal() < other_dec,
85 };
86 result.into_py_any(py)
87 } else {
88 Ok(py.NotImplemented())
89 }
90 }
91
92 fn __hash__(&self) -> isize {
93 let mut h = DefaultHasher::new();
94 self.hash(&mut h);
95 h.finish() as isize
96 }
97
98 fn __add__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
99 if other.is_instance_of::<PyFloat>() {
100 let other_float: f64 = other.extract()?;
101 ArithmeticOperation::Add
102 .checked_f64(self.as_f64_checked()?, other_float)?
103 .into_py_any(py)
104 } else if let Ok(other_price) = other.extract::<Self>() {
105 check_raw_scales(self.precision, other_price.precision)?;
106 (*self)
107 .checked_add(other_price)
108 .ok_or(ArithmeticError::Overflow)?
109 .into_py_any(py)
110 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
111 ArithmeticOperation::Add
112 .checked_decimal(self.as_decimal(), other_dec)?
113 .into_py_any(py)
114 } else {
115 let pytype_name = get_pytype_name(other)?;
116 Err(to_pytype_err(format!(
117 "Unsupported type for __add__, was `{pytype_name}`"
118 )))
119 }
120 }
121
122 fn __radd__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
123 if other.is_instance_of::<PyFloat>() {
124 let other_float: f64 = other.extract()?;
125 ArithmeticOperation::Add
126 .checked_f64(other_float, self.as_f64_checked()?)?
127 .into_py_any(py)
128 } else if let Ok(other_price) = other.extract::<Self>() {
129 check_raw_scales(other_price.precision, self.precision)?;
130 other_price
131 .checked_add(*self)
132 .ok_or(ArithmeticError::Overflow)?
133 .into_py_any(py)
134 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
135 ArithmeticOperation::Add
136 .checked_decimal(other_dec, self.as_decimal())?
137 .into_py_any(py)
138 } else {
139 let pytype_name = get_pytype_name(other)?;
140 Err(to_pytype_err(format!(
141 "Unsupported type for __radd__, was `{pytype_name}`"
142 )))
143 }
144 }
145
146 fn __sub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
147 if other.is_instance_of::<PyFloat>() {
148 let other_float: f64 = other.extract()?;
149 ArithmeticOperation::Sub
150 .checked_f64(self.as_f64_checked()?, other_float)?
151 .into_py_any(py)
152 } else if let Ok(other_price) = other.extract::<Self>() {
153 check_raw_scales(self.precision, other_price.precision)?;
154 (*self)
155 .checked_sub(other_price)
156 .ok_or(ArithmeticError::Overflow)?
157 .into_py_any(py)
158 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
159 ArithmeticOperation::Sub
160 .checked_decimal(self.as_decimal(), other_dec)?
161 .into_py_any(py)
162 } else {
163 let pytype_name = get_pytype_name(other)?;
164 Err(to_pytype_err(format!(
165 "Unsupported type for __sub__, was `{pytype_name}`"
166 )))
167 }
168 }
169
170 fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
171 if other.is_instance_of::<PyFloat>() {
172 let other_float: f64 = other.extract()?;
173 ArithmeticOperation::Sub
174 .checked_f64(other_float, self.as_f64_checked()?)?
175 .into_py_any(py)
176 } else if let Ok(other_price) = other.extract::<Self>() {
177 check_raw_scales(other_price.precision, self.precision)?;
178 other_price
179 .checked_sub(*self)
180 .ok_or(ArithmeticError::Overflow)?
181 .into_py_any(py)
182 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
183 ArithmeticOperation::Sub
184 .checked_decimal(other_dec, self.as_decimal())?
185 .into_py_any(py)
186 } else {
187 let pytype_name = get_pytype_name(other)?;
188 Err(to_pytype_err(format!(
189 "Unsupported type for __rsub__, was `{pytype_name}`"
190 )))
191 }
192 }
193
194 fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
195 if other.is_instance_of::<PyFloat>() {
196 let other_float: f64 = other.extract()?;
197 ArithmeticOperation::Mul
198 .checked_f64(self.as_f64_checked()?, other_float)?
199 .into_py_any(py)
200 } else if let Ok(other_price) = other.extract::<Self>() {
201 ArithmeticOperation::Mul
202 .checked_decimal(self.as_decimal(), other_price.as_decimal())?
203 .into_py_any(py)
204 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
205 ArithmeticOperation::Mul
206 .checked_decimal(self.as_decimal(), other_dec)?
207 .into_py_any(py)
208 } else {
209 let pytype_name = get_pytype_name(other)?;
210 Err(to_pytype_err(format!(
211 "Unsupported type for __mul__, was `{pytype_name}`"
212 )))
213 }
214 }
215
216 fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
217 if other.is_instance_of::<PyFloat>() {
218 let other_float: f64 = other.extract()?;
219 ArithmeticOperation::Mul
220 .checked_f64(other_float, self.as_f64_checked()?)?
221 .into_py_any(py)
222 } else if let Ok(other_price) = other.extract::<Self>() {
223 ArithmeticOperation::Mul
224 .checked_decimal(other_price.as_decimal(), self.as_decimal())?
225 .into_py_any(py)
226 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
227 ArithmeticOperation::Mul
228 .checked_decimal(other_dec, self.as_decimal())?
229 .into_py_any(py)
230 } else {
231 let pytype_name = get_pytype_name(other)?;
232 Err(to_pytype_err(format!(
233 "Unsupported type for __rmul__, was `{pytype_name}`"
234 )))
235 }
236 }
237
238 fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
239 if other.is_instance_of::<PyFloat>() {
240 let other_float: f64 = other.extract()?;
241 ArithmeticOperation::Div
242 .checked_f64(self.as_f64_checked()?, other_float)?
243 .into_py_any(py)
244 } else if let Ok(other_price) = other.extract::<Self>() {
245 ArithmeticOperation::Div
246 .checked_decimal(self.as_decimal(), other_price.as_decimal())?
247 .into_py_any(py)
248 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
249 ArithmeticOperation::Div
250 .checked_decimal(self.as_decimal(), other_dec)?
251 .into_py_any(py)
252 } else {
253 let pytype_name = get_pytype_name(other)?;
254 Err(to_pytype_err(format!(
255 "Unsupported type for __truediv__, was `{pytype_name}`"
256 )))
257 }
258 }
259
260 fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
261 if other.is_instance_of::<PyFloat>() {
262 let other_float: f64 = other.extract()?;
263 ArithmeticOperation::Div
264 .checked_f64(other_float, self.as_f64_checked()?)?
265 .into_py_any(py)
266 } else if let Ok(other_price) = other.extract::<Self>() {
267 ArithmeticOperation::Div
268 .checked_decimal(other_price.as_decimal(), self.as_decimal())?
269 .into_py_any(py)
270 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
271 ArithmeticOperation::Div
272 .checked_decimal(other_dec, self.as_decimal())?
273 .into_py_any(py)
274 } else {
275 let pytype_name = get_pytype_name(other)?;
276 Err(to_pytype_err(format!(
277 "Unsupported type for __rtruediv__, was `{pytype_name}`"
278 )))
279 }
280 }
281
282 fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
283 if other.is_instance_of::<PyFloat>() {
284 let other_float: f64 = other.extract()?;
285 ArithmeticOperation::Div
286 .checked_f64(self.as_f64_checked()?, other_float)?
287 .floor()
288 .into_py_any(py)
289 } else if let Ok(other_price) = other.extract::<Self>() {
290 ArithmeticOperation::Div
291 .checked_decimal(self.as_decimal(), other_price.as_decimal())?
292 .floor()
293 .into_py_any(py)
294 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
295 ArithmeticOperation::Div
296 .checked_decimal(self.as_decimal(), other_dec)?
297 .floor()
298 .into_py_any(py)
299 } else {
300 let pytype_name = get_pytype_name(other)?;
301 Err(to_pytype_err(format!(
302 "Unsupported type for __floordiv__, was `{pytype_name}`"
303 )))
304 }
305 }
306
307 fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
308 if other.is_instance_of::<PyFloat>() {
309 let other_float: f64 = other.extract()?;
310 ArithmeticOperation::Div
311 .checked_f64(other_float, self.as_f64_checked()?)?
312 .floor()
313 .into_py_any(py)
314 } else if let Ok(other_price) = other.extract::<Self>() {
315 ArithmeticOperation::Div
316 .checked_decimal(other_price.as_decimal(), self.as_decimal())?
317 .floor()
318 .into_py_any(py)
319 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
320 ArithmeticOperation::Div
321 .checked_decimal(other_dec, self.as_decimal())?
322 .floor()
323 .into_py_any(py)
324 } else {
325 let pytype_name = get_pytype_name(other)?;
326 Err(to_pytype_err(format!(
327 "Unsupported type for __rfloordiv__, was `{pytype_name}`"
328 )))
329 }
330 }
331
332 fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
333 if other.is_instance_of::<PyFloat>() {
334 let other_float: f64 = other.extract()?;
335 ArithmeticOperation::Rem
336 .checked_f64(self.as_f64_checked()?, other_float)?
337 .into_py_any(py)
338 } else if let Ok(other_price) = other.extract::<Self>() {
339 ArithmeticOperation::Rem
340 .checked_decimal(self.as_decimal(), other_price.as_decimal())?
341 .into_py_any(py)
342 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
343 ArithmeticOperation::Rem
344 .checked_decimal(self.as_decimal(), other_dec)?
345 .into_py_any(py)
346 } else {
347 let pytype_name = get_pytype_name(other)?;
348 Err(to_pytype_err(format!(
349 "Unsupported type for __mod__, was `{pytype_name}`"
350 )))
351 }
352 }
353
354 fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
355 if other.is_instance_of::<PyFloat>() {
356 let other_float: f64 = other.extract()?;
357 ArithmeticOperation::Rem
358 .checked_f64(other_float, self.as_f64_checked()?)?
359 .into_py_any(py)
360 } else if let Ok(other_price) = other.extract::<Self>() {
361 ArithmeticOperation::Rem
362 .checked_decimal(other_price.as_decimal(), self.as_decimal())?
363 .into_py_any(py)
364 } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
365 ArithmeticOperation::Rem
366 .checked_decimal(other_dec, self.as_decimal())?
367 .into_py_any(py)
368 } else {
369 let pytype_name = get_pytype_name(other)?;
370 Err(to_pytype_err(format!(
371 "Unsupported type for __rmod__, was `{pytype_name}`"
372 )))
373 }
374 }
375
376 fn __neg__(&self) -> Self {
377 -*self
378 }
379
380 fn __pos__(&self) -> Self {
381 *self
382 }
383
384 fn __abs__(&self) -> Self {
385 if self.is_negative() { -*self } else { *self }
386 }
387
388 fn __int__(&self) -> PriceRaw {
389 let scale = PriceRaw::try_from(raw_scale(self.precision))
390 .expect("effective raw scale should fit in PriceRaw");
391 self.raw() / scale
392 }
393
394 fn __float__(&self) -> PyResult<f64> {
395 self.as_f64_checked()
396 }
397
398 #[pyo3(signature = (ndigits=None))]
399 fn __round__(&self, ndigits: Option<u32>) -> Decimal {
400 self.as_decimal()
401 .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
402 }
403
404 fn __repr__(&self) -> String {
405 format!("{self:?}")
406 }
407
408 fn __str__(&self) -> String {
409 self.to_string()
410 }
411
412 #[getter(raw)]
425 fn py_raw(&self) -> PriceRaw {
426 self.raw()
427 }
428
429 #[getter]
430 fn precision(&self) -> u8 {
431 self.precision
432 }
433
434 #[staticmethod]
436 #[pyo3(name = "from_raw")]
437 fn py_from_raw(raw: PriceRaw, precision: u8) -> PyResult<Self> {
438 Self::from_raw_checked(raw, precision).map_err(correctness_error_to_pyvalue_err)
439 }
440
441 #[staticmethod]
443 #[pyo3(name = "zero")]
444 #[pyo3(signature = (precision = 0))]
445 fn py_zero(precision: u8) -> PyResult<Self> {
446 Self::new_checked(0.0, precision).map_err(to_pyvalue_err)
447 }
448
449 #[staticmethod]
450 #[pyo3(name = "from_int")]
451 fn py_from_int(value: u64) -> PyResult<Self> {
452 Self::new_checked(value as f64, 0).map_err(to_pyvalue_err)
453 }
454
455 #[staticmethod]
456 #[pyo3(name = "from_str")]
457 fn py_from_str(value: &str) -> PyResult<Self> {
458 Self::from_str(value).map_err(to_pyvalue_err)
459 }
460
461 #[staticmethod]
473 #[pyo3(name = "from_decimal")]
474 fn py_from_decimal(decimal: Decimal) -> PyResult<Self> {
475 Self::from_decimal(decimal).map_err(to_pyvalue_err)
476 }
477
478 #[staticmethod]
490 #[pyo3(name = "from_decimal_dp")]
491 fn py_from_decimal_dp(decimal: Decimal, precision: u8) -> PyResult<Self> {
492 Self::from_decimal_dp(decimal, precision).map_err(to_pyvalue_err)
493 }
494
495 #[staticmethod]
500 #[pyo3(name = "from_mantissa_exponent")]
501 fn py_from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> PyResult<Self> {
502 Self::from_mantissa_exponent_checked(mantissa, exponent, precision)
503 .map_err(correctness_error_to_pyvalue_err)
504 }
505
506 #[pyo3(name = "is_zero")]
508 fn py_is_zero(&self) -> bool {
509 self.is_zero()
510 }
511
512 #[pyo3(name = "is_positive")]
514 fn py_is_positive(&self) -> bool {
515 self.is_positive()
516 }
517
518 #[pyo3(name = "as_decimal")]
520 fn py_as_decimal(&self) -> Decimal {
521 self.as_decimal()
522 }
523
524 #[pyo3(name = "to_formatted_str")]
525 fn py_to_formatted_str(&self) -> String {
526 self.to_formatted_string()
527 }
528
529 #[pyo3(name = "checked_add")]
537 fn py_checked_add(&self, other: Self) -> Option<Self> {
538 self.checked_add(other)
539 }
540
541 #[pyo3(name = "checked_sub")]
549 fn py_checked_sub(&self, other: Self) -> Option<Self> {
550 self.checked_sub(other)
551 }
552
553 #[pyo3(name = "as_double")]
554 fn py_as_double(&self) -> PyResult<f64> {
555 self.as_f64_checked()
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use pyo3::Python;
562 use rstest::rstest;
563
564 use super::*;
565 use crate::types::{
566 fixed::FIXED_PRECISION,
567 price::{PRICE_RAW_MAX, PRICE_RAW_MIN},
568 };
569
570 #[rstest]
571 #[case("0", 0)]
572 #[case("0.000000001", 0)]
573 #[case("-0.000000001", 0)]
574 #[case("1.999999999", 1)]
575 #[case("-1.999999999", -1)]
576 #[case("50.25", 50)]
577 #[case("9007199253.999999999", 9_007_199_253)]
578 fn test_int_uses_exact_raw_value(#[case] value: &str, #[case] expected: i64) {
579 let price = Price::from_str(value).unwrap();
580
581 assert_eq!(price.__int__(), PriceRaw::from(expected));
582 }
583
584 #[rstest]
585 fn test_int_preserves_domain_boundaries() {
586 let expected_max: PriceRaw = if cfg!(feature = "high-precision") {
587 17_014_118_346_046
588 } else {
589 9_223_372_036
590 };
591 let expected_min = -expected_max;
592
593 assert_eq!(
594 Price::from_raw(PRICE_RAW_MAX, FIXED_PRECISION).__int__(),
595 expected_max
596 );
597 assert_eq!(
598 Price::from_raw(PRICE_RAW_MIN, FIXED_PRECISION).__int__(),
599 expected_min
600 );
601 }
602
603 #[rstest]
604 #[cfg(feature = "defi")]
605 fn test_int_uses_defi_raw_scale() {
606 let price = Price::from_raw(1_999_999_999_999_999_999, crate::defi::WEI_PRECISION);
607
608 assert_eq!(price.__int__(), 1);
609 }
610
611 #[rstest]
612 fn test_py_from_raw_rejects_out_of_range_raw_value() {
613 Python::initialize();
614 Python::attach(|_| {
615 let raw = PRICE_RAW_MAX.saturating_add(1);
616 let error = Price::py_from_raw(raw, 0).unwrap_err();
617
618 assert_eq!(
619 error.to_string(),
620 format!(
621 "ValueError: raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
622 )
623 );
624 });
625 }
626
627 #[rstest]
628 fn test_py_from_mantissa_exponent_handles_precision_and_overflow() {
629 Python::initialize();
630 Python::attach(|_| {
631 #[cfg(feature = "defi")]
632 let max_precision = crate::defi::WEI_PRECISION;
633 #[cfg(not(feature = "defi"))]
634 let max_precision = FIXED_PRECISION;
635
636 let exponent = -i8::try_from(max_precision).unwrap();
637 let price = Price::py_from_mantissa_exponent(1, exponent, max_precision).unwrap();
638 let invalid_precision = max_precision + 1;
639 let precision_error =
640 Price::py_from_mantissa_exponent(1, 0, invalid_precision).unwrap_err();
641 let overflow_error = Price::py_from_mantissa_exponent(i64::MAX, 100, 0).unwrap_err();
642 let precision_name = if cfg!(feature = "defi") {
643 "WEI_PRECISION"
644 } else {
645 "FIXED_PRECISION"
646 };
647
648 assert_eq!(price.raw(), 1);
649 assert_eq!(price.precision, max_precision);
650 assert_eq!(
651 precision_error.to_string(),
652 format!(
653 "ValueError: `precision` exceeded maximum `{precision_name}` ({max_precision}), was {invalid_precision}"
654 )
655 );
656 assert_eq!(
657 overflow_error.to_string(),
658 "ValueError: Overflow in Price::from_mantissa_exponent \
659 (mantissa=9223372036854775807, exponent=100, precision=0)"
660 );
661 });
662 }
663}