levana_perpswap_cosmos/number.rs
1//! Provides a number of data types, methods, and traits to have more
2//! fine-grained and strongly-typed control of numeric data.
3//!
4//! # Base, quote, notional, and collateral
5//!
6//! In general markets, [a currency
7//! pair](https://www.investopedia.com/terms/c/currencypair.asp) like `BTC/USD`
8//! consists of a *base currency* (`BTC`) and a *quote currency* (`USD`). In our
9//! platform, we talk about the *notional* and *collateral* assets, where the
10//! collateral asset is what gets deposited in the platform and notional is
11//! (generally) the risk asset being speculated on.
12//!
13//! Generally speaking, in most perpetual swaps platforms, the base and notional
14//! assets are the same, and the quote and collateral assets are the same.
15//! However, our platform supports a concept called *crypto denominated pairs*.
16//! In these, we use the base/risk asset as collateral and quote is the
17//! notional. This causes a cascade of changes around leverage and price
18//! management.
19//!
20//! However, all those changes are _internal to the protocol_. The user-facing
21//! API should almost exclusively care about base and quote (besides the fact
22//! that the user will interact with the contracts by depositing and withdrawing
23//! collateral assets). The purpose of this module is to provide data types that
24//! provide safety and clarity around whether we're dealing with the base/quote
25//! view of the world (user-facing) or notional/collateral (internal) view.
26//!
27//! # Decimal256, NonZero, Signed, and UnsignedDecimal
28//!
29//! Math generally uses [Decimal256](cosmwasm_std::Decimal256).
30//! However, this type alone cannot express negative numbers, and we often want
31//! to ensure additional constraints at compile time.
32//!
33//! A combination of traits and newtype wrappers gives us a robust framework:
34//!
35//! * `UnsignedDecimal`: a _trait_, not a concrete type, which is implemented
36//! for `Collateral`, `Notional`, and several other numeric types.
37//!
38//! * `NonZero<T>`: a newtype wrapper which ensures that the value is not zero.
39//! It's generally used for types where `T: UnsignedDecimal`.
40//!
41//! * `Signed<T>`: a newtype wrapper which allows for positive or negative
42//! values. It's also generally used for types where `T: UnsignedDecimal`.
43//!
44//! Putting it all together, here are some examples. Note that these are merely
45//! illustrative. Real-world problems would require a price point to convert
46//! between Collateral and Notional:
47//!
48//! ### UnsignedDecimal
49//!
50//! `Collateral` implements `UnsignedDecimal`, and so we can add two `Collateral`
51//! values together via `.checked_add()`.
52//!
53//! However, we cannot add a `Collateral` and some other `Decimal256`. Instead
54//! we need to call `.into_decimal256()`, do our math with another `Decimal256`,
55//! and then convert it to any `T: UnsignedDecimal` via `T::from_decimal256()`.
56//!
57//! *example*
58//!
59//! ```
60//! use levana_perpswap_cosmos::number::*;
61//! use cosmwasm_std::Decimal256;
62//! use std::str::FromStr;
63//!
64//! let lhs:Collateral = "1.23".parse().unwrap();
65//! let rhs:Decimal256 = "4.56".parse().unwrap();
66//! let decimal_result = lhs.into_decimal256().checked_add(rhs).unwrap();
67//! let output:Notional = Notional::from_decimal256(decimal_result);
68//! ```
69//!
70//! ### NonZero
71//!
72//! `NonZero<Collateral>` allows us to call various `.checked_*` math methods
73//! with another `NonZero<Collateral>`.
74//!
75//! However, if we want to do math with a different underlying type - we do need
76//! to drop down to that common type. There's two approaches (both of which
77//! return an Option, in case the resulting value is zero):
78//!
79//! 1. If the inner NonZero type stays the same (i.e. it's all `Collateral`)
80//! then call `.raw()` to get the inner type, do your math, and then convert
81//! back to the NonZero wrapper via `NonZero::new()`
82//! 2. If you need a `Decimal256`, then call `.into_decimal256()` to get the
83//! underlying `Decimal256` type, do your math, and then convert back to
84//! `NonZero<T>` via `NonZero::new(T::from_decimal256(value))`. This is
85//! usually the case when the type of `T` has changed
86//! (i.e. from `Collateral` to `Notional`)
87//!
88//! *example 1*
89//!
90//! ```
91//! use levana_perpswap_cosmos::number::*;
92//! use cosmwasm_std::Decimal256;
93//! use std::str::FromStr;
94//!
95//! let lhs:NonZero<Collateral> = "1.23".parse().unwrap();
96//! let rhs:Collateral = "4.56".parse().unwrap();
97//! let collateral_result = lhs.raw().checked_add(rhs).unwrap();
98//! let output:NonZero<Collateral> = NonZero::new(collateral_result).unwrap();
99//!
100//! ```
101//!
102//! *example 2*
103//!
104//! ```
105//! use levana_perpswap_cosmos::number::*;
106//! use cosmwasm_std::Decimal256;
107//! use std::str::FromStr;
108//!
109//! let lhs:NonZero<Collateral> = "1.23".parse().unwrap();
110//! let rhs:Decimal256 = "4.56".parse().unwrap();
111//! let decimal_result = lhs.into_decimal256().checked_add(rhs).unwrap();
112//! let notional_result = Notional::from_decimal256(decimal_result);
113//! let output:NonZero<Notional> = NonZero::new(notional_result).unwrap();
114//!
115//! ```
116//! ### Signed
117//!
118//! `Signed<Collateral>` also allows us to call various `.checked_*` math methods
119//! when the inner type is the same. However, there are some differences when
120//! comparing to the `NonZero` methods:
121//!
122//! 1. To get the underlying `T`, call `.abs_unsigned()` instead of `.raw()`.
123//! The sign is now lost, it's not a pure raw conversion.
124//!
125//! 2. To get back from the underlying `T`, call `T::into_signed()`
126//!
127//! 3. There is no direct conversion to `Decimal256`.
128//!
129//! 4. There are helpers for the ubiquitous use-case of `Signed<Decimal256>`
130//! This is such a common occurance, it has its own type alias: `Number`.
131//!
132//! *example 1*
133//!
134//! ```
135//! use levana_perpswap_cosmos::number::*;
136//! use cosmwasm_std::Decimal256;
137//! use std::str::FromStr;
138//!
139//! let lhs:Signed<Collateral> = "-1.23".parse().unwrap();
140//! let rhs:Decimal256 = "4.56".parse().unwrap();
141//! let decimal_result = lhs.abs_unsigned().into_decimal256().checked_mul(rhs).unwrap();
142//! let notional_result = Notional::from_decimal256(decimal_result);
143//! // bring back our negative sign
144//! let output:Signed<Notional> = -notional_result.into_signed();
145//! ```
146//!
147//! *example 2*
148//! ```
149//! use levana_perpswap_cosmos::number::*;
150//! use cosmwasm_std::Decimal256;
151//! use std::str::FromStr;
152//!
153//! let lhs:Signed<Collateral> = "-1.23".parse().unwrap();
154//! let rhs:Number = "4.56".parse().unwrap();
155//! let number_result = lhs.into_number().checked_mul(rhs).unwrap();
156//! let output:Signed<Notional> = Signed::<Notional>::from_number(number_result);
157//! ```
158
159mod convert;
160mod ops;
161mod serialize;
162use schemars::schema::{InstanceType, SchemaObject};
163use schemars::JsonSchema;
164mod nonzero;
165pub use self::types::*;
166
167pub mod ratio;
168mod types;
169
170// schemars could not figure out that it is serialized as a string
171// so gotta impl it manually
172impl<T: UnsignedDecimal> JsonSchema for Signed<T> {
173 fn schema_name() -> String {
174 "Signed decimal".to_owned()
175 }
176
177 fn is_referenceable() -> bool {
178 false
179 }
180
181 fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
182 let mut obj = SchemaObject {
183 instance_type: Some(InstanceType::String.into()),
184 ..Default::default()
185 };
186
187 let mut meta = obj.metadata.unwrap_or_default();
188
189 // would be nice to re-use the doc comments above...
190 meta.description = Some(
191 r#"
192 A signed number type with high fidelity.
193 Similar in spirit to cosmwasm_bignumber::Decimal256 - it is
194 a more ergonomic wrapper around cosmwasm-std by making more things public
195 but we also add negative values and other methods as-needed
196 "#
197 .to_string(),
198 );
199
200 obj.metadata = Some(meta);
201
202 obj.into()
203 }
204}
205
206impl<T: UnsignedDecimal> Signed<T> {
207 /// absolute value
208 pub fn abs(self) -> Self {
209 Self::new_positive(self.value())
210 }
211
212 /// Absolute value, but return the `T` underlying type directly
213 pub fn abs_unsigned(self) -> T {
214 self.value()
215 }
216
217 /// Checks if this number is greater than 0.
218 pub fn is_strictly_positive(&self) -> bool {
219 !self.is_zero() && !self.is_negative()
220 }
221
222 /// Checks if this number is greater than or equal to 0.
223 pub fn is_positive_or_zero(&self) -> bool {
224 !self.is_negative()
225 }
226
227 /// Is the value 0?
228 pub fn is_zero(&self) -> bool {
229 self.value().is_zero()
230 }
231
232 /// Apply a function to the inner value and rewrap.
233 ///
234 /// This will keep the current sign (positive or negative) in place,
235 /// respecting invariants that a value of 0 must have negative set to false.
236 pub fn map<U: UnsignedDecimal, F: FnOnce(T) -> U>(self, f: F) -> Signed<U> {
237 let value = f(self.value());
238 if self.is_negative() {
239 Signed::new_negative(value)
240 } else {
241 Signed::new_positive(value)
242 }
243 }
244
245 /// Like `map` but may fail
246 pub fn try_map<E, U: UnsignedDecimal, F: FnOnce(T) -> Result<U, E>>(
247 self,
248 f: F,
249 ) -> Result<Signed<U>, E> {
250 f(self.value()).map(|value| {
251 if self.is_negative() {
252 Signed::new_negative(value)
253 } else {
254 Signed::new_positive(value)
255 }
256 })
257 }
258}
259
260#[cfg(test)]
261mod test {
262 use super::Number;
263 use std::str::FromStr;
264
265 #[test]
266 fn number_default() {
267 assert_eq!(Number::ZERO, Number::default());
268 }
269
270 #[test]
271 fn number_serde() {
272 let a = Number::from(300u64);
273 let b = Number::from(7u64);
274 let res = (a / b).unwrap();
275
276 assert_eq!(serde_json::to_value(res).unwrap(), "42.857142857142857142");
277 assert_eq!(
278 serde_json::from_str::<Number>("\"42.857142857142857142\"").unwrap(),
279 res
280 );
281
282 let res = -res;
283
284 assert_eq!(serde_json::to_value(res).unwrap(), "-42.857142857142857142");
285 assert_eq!(
286 serde_json::from_str::<Number>("\"-42.857142857142857142\"").unwrap(),
287 res
288 );
289 }
290
291 #[test]
292 fn number_arithmetic() {
293 let a = Number::from(300u64);
294 let b = Number::from(7u64);
295
296 assert_eq!((a + b).unwrap().to_string(), "307");
297 assert_eq!((a - b).unwrap().to_string(), "293");
298 assert_eq!((b - a).unwrap().to_string(), "-293");
299 assert_eq!((a * b).unwrap().to_string(), "2100");
300 assert_eq!((a / b).unwrap().to_string(), "42.857142857142857142");
301
302 let a = -a;
303 let b = -b;
304 assert_eq!((a + b).unwrap().to_string(), "-307");
305 assert_eq!((a - b).unwrap().to_string(), "-293");
306 assert_eq!((b - a).unwrap().to_string(), "293");
307 assert_eq!((a * b).unwrap().to_string(), "2100");
308 assert_eq!((a / b).unwrap().to_string(), "42.857142857142857142");
309
310 let a = -a;
311 assert_eq!((a + b).unwrap().to_string(), "293");
312 assert_eq!((a - b).unwrap().to_string(), "307");
313 assert_eq!((b - a).unwrap().to_string(), "-307");
314 assert_eq!((a * b).unwrap().to_string(), "-2100");
315 assert_eq!((a / b).unwrap().to_string(), "-42.857142857142857142");
316 }
317
318 #[test]
319 fn number_cmp() {
320 let a = Number::from_str("4.2").unwrap();
321 let b = Number::from_str("0.007").unwrap();
322
323 assert!(a > b);
324 assert!(a.approx_gt_strict(b).unwrap());
325 assert!(a.approx_gt_relaxed(b).unwrap());
326 assert!(a != b);
327
328 let a = Number::from_str("4.2").unwrap();
329 let b = Number::from_str("4.2").unwrap();
330
331 assert!(a <= b);
332 assert!(a >= b);
333 assert!(a.approx_eq(b).unwrap());
334 assert!(a == b);
335
336 let a = Number::from_str("4.2").unwrap();
337 let b = Number::from_str("-4.2").unwrap();
338
339 assert!(a > b);
340 assert!(a.approx_gt_strict(b).unwrap());
341 assert!(a.approx_gt_relaxed(b).unwrap());
342 assert!(a != b);
343
344 let a = Number::from_str("-4.2").unwrap();
345 let b = Number::from_str("4.2").unwrap();
346
347 assert!(a < b);
348 assert!(a.approx_lt_relaxed(b).unwrap());
349 assert!(a != b);
350
351 let a = Number::from_str("-4.5").unwrap();
352 let b = Number::from_str("-4.2").unwrap();
353
354 assert!(a < b);
355 assert!(a.approx_lt_relaxed(b).unwrap());
356 assert!(a != b);
357
358 let a = Number::from_str("-4.2").unwrap();
359 let b = Number::from_str("-4.5").unwrap();
360
361 assert!(a > b);
362 assert!(a.approx_gt_strict(b).unwrap());
363 assert!(a.approx_gt_relaxed(b).unwrap());
364 assert!(a != b);
365 }
366
367 #[test]
368 fn unsigned_key_bytes() {
369 let a = Number::from_str("0.9")
370 .unwrap()
371 .to_unsigned_key_bytes()
372 .unwrap();
373 let b = Number::from_str("1.0")
374 .unwrap()
375 .to_unsigned_key_bytes()
376 .unwrap();
377 let c = Number::from_str("1.9")
378 .unwrap()
379 .to_unsigned_key_bytes()
380 .unwrap();
381 let d = Number::from_str("9.0")
382 .unwrap()
383 .to_unsigned_key_bytes()
384 .unwrap();
385 let e = Number::from_str("9.1")
386 .unwrap()
387 .to_unsigned_key_bytes()
388 .unwrap();
389 assert!(a < b);
390 assert!(b < c);
391 assert!(c < d);
392 assert!(d < e);
393
394 assert!(Number::from_str("-1.0")
395 .unwrap()
396 .to_unsigned_key_bytes()
397 .is_none());
398 }
399
400 #[test]
401 fn zero_str() {
402 let mut a = Number::from_str("0").unwrap();
403 a = -a;
404 assert_eq!(a.to_string(), "0");
405
406 let a = Number::from_str("-0").unwrap();
407 assert_eq!(a.to_string(), "0");
408 }
409
410 #[test]
411 fn number_u128_with_precision() {
412 let _a = Number::from_str("270.15").unwrap();
413 let b = Number::from_str("1.000000001").unwrap();
414 let c = Number::from(u128::MAX);
415
416 // Typcial use - we will send this number in a BankMsg normally
417 assert_eq!(_a.to_u128_with_precision(6).unwrap(), 270_150_000);
418
419 // Demonstrate inherent lossy-ness of doing Number -> u128
420 assert_eq!(b.to_u128_with_precision(6).unwrap(), 1_000_000);
421 assert_eq!(b.to_u128_with_precision(9).unwrap(), 1_000_000_001);
422
423 // Try 6-decimal precision on a number that would overflow
424 assert_eq!(c.to_u128_with_precision(6), None);
425
426 // Try 0-decimal precision on the largest number we can handle
427 assert_eq!(c.to_u128_with_precision(0).unwrap(), u128::MAX);
428 }
429
430 #[test]
431 fn catch_overflow() {
432 match Number::MAX.checked_mul(Number::MAX) {
433 Ok(_) => {
434 panic!("should overflow!");
435 }
436 Err(e) => {
437 if !e.to_string().contains("Overflow") {
438 panic!("wrong error! (got {e})");
439 }
440 }
441 }
442 }
443
444 #[test]
445 fn basic_multiplication() {
446 let num = Number::from_str("1.1").unwrap();
447 let twopointtwo = (num * 2u64).unwrap();
448 assert_eq!(twopointtwo, Number::from_str("2.2").unwrap());
449 }
450}