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}