bat_markets_core/
numeric.rs1use core::fmt;
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6use crate::error::{ErrorKind, MarketError, Result};
7
8macro_rules! decimal_value {
9 ($name:ident) => {
10 #[derive(
11 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
12 )]
13 pub struct $name(Decimal);
14
15 impl $name {
16 #[must_use]
17 pub const fn new(value: Decimal) -> Self {
18 Self(value)
19 }
20
21 #[must_use]
22 pub const fn value(self) -> Decimal {
23 self.0
24 }
25 }
26
27 impl From<Decimal> for $name {
28 fn from(value: Decimal) -> Self {
29 Self::new(value)
30 }
31 }
32
33 impl fmt::Display for $name {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 write!(f, "{}", self.0)
36 }
37 }
38 };
39}
40
41macro_rules! fast_value {
42 ($name:ident) => {
43 #[derive(
44 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
45 )]
46 pub struct $name(i64);
47
48 impl $name {
49 #[must_use]
50 pub const fn new(value: i64) -> Self {
51 Self(value)
52 }
53
54 #[must_use]
55 pub const fn value(self) -> i64 {
56 self.0
57 }
58 }
59 };
60}
61
62decimal_value!(Amount);
63decimal_value!(Price);
64decimal_value!(Quantity);
65decimal_value!(Notional);
66decimal_value!(Leverage);
67decimal_value!(Rate);
68
69fast_value!(FastPrice);
70fast_value!(FastQuantity);
71fast_value!(FastNotional);
72
73impl Notional {
74 #[must_use]
75 pub fn from_price_qty(price: Price, quantity: Quantity) -> Self {
76 Self(price.value() * quantity.value())
77 }
78}
79
80impl Price {
81 pub fn quantize(self, scale: u32) -> Result<FastPrice> {
82 quantize_decimal(self.value(), scale).map(FastPrice::new)
83 }
84}
85
86impl Quantity {
87 pub fn quantize(self, scale: u32) -> Result<FastQuantity> {
88 quantize_decimal(self.value(), scale).map(FastQuantity::new)
89 }
90}
91
92impl Notional {
93 pub fn quantize(self, scale: u32) -> Result<FastNotional> {
94 quantize_decimal(self.value(), scale).map(FastNotional::new)
95 }
96}
97
98impl FastPrice {
99 #[must_use]
100 pub fn to_price(self, scale: u32) -> Price {
101 Price::new(Decimal::new(self.value(), scale))
102 }
103}
104
105impl FastQuantity {
106 #[must_use]
107 pub fn to_quantity(self, scale: u32) -> Quantity {
108 Quantity::new(Decimal::new(self.value(), scale))
109 }
110}
111
112impl FastNotional {
113 #[must_use]
114 pub fn to_notional(self, scale: u32) -> Notional {
115 Notional::new(Decimal::new(self.value(), scale))
116 }
117}
118
119fn quantize_decimal(mut value: Decimal, scale: u32) -> Result<i64> {
120 value.rescale(scale);
121 i64::try_from(value.mantissa()).map_err(|_| {
122 MarketError::new(
123 ErrorKind::ConfigError,
124 format!("value {value} does not fit into fast-path i64 representation"),
125 )
126 })
127}
128
129#[cfg(test)]
130mod tests {
131 use proptest::prelude::*;
132 use proptest::test_runner::TestCaseError;
133 use rust_decimal::Decimal;
134
135 use super::{Price, Quantity};
136
137 proptest! {
138 #[test]
139 fn price_quantize_roundtrip(units in 1_i64..1_000_000_i64, scale in 0_u32..4_u32) {
140 let value = Price::new(Decimal::new(units, scale));
141 let fast = match value.quantize(scale) {
142 Ok(value) => value,
143 Err(error) => return Err(TestCaseError::fail(error.to_string())),
144 };
145 prop_assert_eq!(fast.to_price(scale), value);
146 }
147
148 #[test]
149 fn quantity_quantize_roundtrip(units in 1_i64..1_000_000_i64, scale in 0_u32..4_u32) {
150 let value = Quantity::new(Decimal::new(units, scale));
151 let fast = match value.quantize(scale) {
152 Ok(value) => value,
153 Err(error) => return Err(TestCaseError::fail(error.to_string())),
154 };
155 prop_assert_eq!(fast.to_quantity(scale), value);
156 }
157 }
158}