Expand description

Number type and helpers Provides a number of data types, methods, and traits to have more fine-grained and strongly-typed control of numeric data.

Base, quote, notional, and collateral

In general markets, a currency pair like BTC/USD consists of a base currency (BTC) and a quote currency (USD). In our platform, we talk about the notional and collateral assets, where the collateral asset is what gets deposited in the platform and notional is (generally) the risk asset being speculated on.

Generally speaking, in most perpetual swaps platforms, the base and notional assets are the same, and the quote and collateral assets are the same. However, our platform supports a concept called crypto denominated pairs. In these, we use the base/risk asset as collateral and quote is the notional. This causes a cascade of changes around leverage and price management.

However, all those changes are internal to the protocol. The user-facing API should almost exclusively care about base and quote (besides the fact that the user will interact with the contracts by depositing and withdrawing collateral assets). The purpose of this module is to provide data types that provide safety and clarity around whether we’re dealing with the base/quote view of the world (user-facing) or notional/collateral (internal) view.

Decimal256, NonZero, Signed, and UnsignedDecimal

Math generally uses Decimal256. However, this type alone cannot express negative numbers, and we often want to ensure additional constraints at compile time.

A combination of traits and newtype wrappers gives us a robust framework:

  • UnsignedDecimal: a trait, not a concrete type, which is implemented for Collateral, Notional, and several other numeric types.

  • NonZero<T>: a newtype wrapper which ensures that the value is not zero. It’s generally used for types where T: UnsignedDecimal.

  • Signed<T>: a newtype wrapper which allows for positive or negative values. It’s also generally used for types where T: UnsignedDecimal.

Putting it all together, here are some examples. Note that these are merely illustrative. Real-world problems would require a price point to convert between Collateral and Notional:

UnsignedDecimal

Collateral implements UnsignedDecimal, and so we can add two Collateral values together via .checked_add().

However, we cannot add a Collateral and some other Decimal256. Instead we need to call .into_decimal256(), do our math with another Decimal256, and then convert it to any T: UnsignedDecimal via T::from_decimal256().

example

use levana_perpswap_cosmos::number::*;
use cosmwasm_std::Decimal256;
use std::str::FromStr;

let lhs:Collateral = "1.23".parse().unwrap();
let rhs:Decimal256 = "4.56".parse().unwrap();
let decimal_result = lhs.into_decimal256().checked_add(rhs).unwrap();
let output:Notional = Notional::from_decimal256(decimal_result);

NonZero

NonZero<Collateral> allows us to call various .checked_* math methods with another NonZero<Collateral>.

However, if we want to do math with a different underlying type - we do need to drop down to that common type. There’s two approaches (both of which return an Option, in case the resulting value is zero):

  1. If the inner NonZero type stays the same (i.e. it’s all Collateral) then call .raw() to get the inner type, do your math, and then convert back to the NonZero wrapper via NonZero::new()
  2. If you need a Decimal256, then call .into_decimal256() to get the underlying Decimal256 type, do your math, and then convert back to NonZero<T> via NonZero::new(T::from_decimal256(value)). This is usually the case when the type of T has changed (i.e. from Collateral to Notional)

example 1

use levana_perpswap_cosmos::number::*;
use cosmwasm_std::Decimal256;
use std::str::FromStr;

let lhs:NonZero<Collateral> = "1.23".parse().unwrap();
let rhs:Collateral = "4.56".parse().unwrap();
let collateral_result = lhs.raw().checked_add(rhs).unwrap();
let output:NonZero<Collateral> = NonZero::new(collateral_result).unwrap();

example 2

use levana_perpswap_cosmos::number::*;
use cosmwasm_std::Decimal256;
use std::str::FromStr;

let lhs:NonZero<Collateral> = "1.23".parse().unwrap();
let rhs:Decimal256 = "4.56".parse().unwrap();
let decimal_result = lhs.into_decimal256().checked_add(rhs).unwrap();
let notional_result = Notional::from_decimal256(decimal_result);
let output:NonZero<Notional> = NonZero::new(notional_result).unwrap();

Signed

Signed<Collateral> also allows us to call various .checked_* math methods when the inner type is the same. However, there are some differences when comparing to the NonZero methods:

  1. To get the underlying T, call .abs_unsigned() instead of .raw(). The sign is now lost, it’s not a pure raw conversion.

  2. To get back from the underlying T, call T::into_signed()

  3. There is no direct conversion to Decimal256.

  4. There are helpers for the ubiquitous use-case of Signed<Decimal256> This is such a common occurance, it has its own type alias: Number.

example 1

use levana_perpswap_cosmos::number::*;
use cosmwasm_std::Decimal256;
use std::str::FromStr;

let lhs:Signed<Collateral> = "-1.23".parse().unwrap();
let rhs:Decimal256 = "4.56".parse().unwrap();
let decimal_result = lhs.abs_unsigned().into_decimal256().checked_mul(rhs).unwrap();
let notional_result = Notional::from_decimal256(decimal_result);
// bring back our negative sign
let output:Signed<Notional> = -notional_result.into_signed();

example 2

use levana_perpswap_cosmos::number::*;
use cosmwasm_std::Decimal256;
use std::str::FromStr;

let lhs:Signed<Collateral> = "-1.23".parse().unwrap();
let rhs:Number = "4.56".parse().unwrap();
let number_result = lhs.into_number().checked_mul(rhs).unwrap();
let output:Signed<Notional> = Signed::<Notional>::from_number(number_result);

Modules

  • Provides specialized types that define a ratio that must be within a specific range. This can be helpful when defining an interface that contains a ratio represented by a decimal but the ratio is logically constrained by specific bounds.

Structs

Traits

Type Aliases