primitive_fixed_point_decimal
Primitive fixed-point decimal types.
Rust built-in f32 and f64 types are not suitable for some fields
(e.g. finance) because of two drawbacks:
-
can not represent decimal numbers in base 10 accurately, because they are in base 2;
-
can not guarantee fraction precision, because they are floating-point.
This crate provides fixed-point decimal types to address the issues by
-
using integer types to represent number and handling radix-point in base 10, to achieve the accuracy. This is a common idea. Many other decimal crates do the same thing;
-
specifying precision staticly (fixed-point) to guarantee the fraction precision. The precision is part of the type. This feature is unique!
For example, StaticPrecFpdec<i64, 4> means using i64 as the underlying
representation, and 4 is the static precision.
The "primitive" in the crate name means straightforward representation, compact memory layout, high performance, and clean APIs, just like Rust's primitive number types.
This crate is no_std.
Distinctive
Although other decimal crates also claim to be fixed-point, they all bind the precision (or called "scale") to each decimal instance, which changes during operations. See the comparison document for more details.
While this crate binds the precision to decimal type. It's static. The decimal types keep their precision for their whole lifetime instead of changing their precision during operations.
The +, - and comparison operations only perform between same types in
same precision. There is no implicitly type or precision conversion.
This makes sence, for we do not want to add balance type by
fee-rate type. Even for two balance types we do not want to add
USD currency by CNY. This also makes the operations very fast.
However, the * and / operations accept operand with different
types and precisions, and allow the result's precision specified.
Certainly we need to multiply between balance type and fee-rate type
and get balance type.
See the examples below for more details.
When to or Not to Use This
Because of the real fixed-point, the application scenarios are very clear.
For specific applications, if you know the precisions required, such as in a financial system using 2 precisions for balance and 6 for prices, then it is suitable for this crate. See the following examples.
While for general-purpose applications or libraries, where you don't know the precision that the end users will need, such as in storage systems like Redis, then it is not suitable for this crate.
Besides, the real fixed-point is suitable for simple operations but not complex mathematical formulas, e.g. options pricing and Greeks. However, in my opinon, complex mathematical formulas do not require accurate precision generally. So in this case you can convert the decimal inputs (e.g. prices, balances and volumes) to floats and then perform the complex calculations.
Specify Precision
There are 2 ways to specify the precision: static and out-of-band:
-
For the static type, [
StaticPrecFpdec], we use Rust's const generics to specify the precision. For example,StaticPrecFpdec<i64, 4>means 4 precision. -
For the out-of-band type, [
OobPrecFpdec], we do NOT save the precision with our decimal types, so it's your job to save it somewhere and apply it in the following operations later. For example,OobPrecFpdec<i64>takes no precision information.
Generally, the static type is more convenient and suitable for most
scenarios. For example, in traditional currency exchange, you can use
StaticPrecFpdec<i64, 2> to represent balance, e.g. 1234.56 USD and
8888800.00 JPY. And use StaticPrecFpdec<i32, 6> to represent all
market prices since 6-digit-precision is big enough for all currency
pairs, e.g. 146.4730 JPY/USD and 0.006802 USD/JPY:
use ;
type Balance = ; // 2 is enough for all currencies
type Price = ; // 6 is enough for all markets
let usd: Balance = fpdec!;
let price: Price = fpdec!;
let jpy: Balance = usd.checked_mul.unwrap;
assert_eq!;
However in some scenarios, such as in cryptocurrency exchange, the
price differences across various markets are very significant. For
example 81234.0 in BTC/USDT and 0.000004658 in PEPE/USDT. Here
we need to select different precisions for each market. So it's
the Out-of-band type:
use ;
type Balance = ; // no global precision set
type Price = ; // no global precision set
// each market has its own precision configuration
// let's take BTC/USDT market as example
let btc_usdt = Market ;
// we need tell the precision to `fpdec!`
let btc: Balance = fpdec!;
let price: Price = fpdec!;
// we need tell the precision difference to `checked_mul()` method
let diff = btc_usdt.base_asset_precision + btc_usdt.price_precision - btc_usdt.quote_asset_precision;
let usdt = btc.checked_mul.unwrap;
assert_eq!;
Obviously it's verbose to use, but offers greater flexibility.
Cumulative Error
As is well known, integer division can lead to precision loss; multiplication of decimals can also create higher precision and may potentially cause precision loss.
What we are discussing here is another issue: multiple multiplication and
division may cause cumulative error, thereby exacerbating the issue of
precision loss. See int-div-cum-error
for more information.
In this crate, functions with the cum_error parameter provide control
over cumulative error based on int-div-cum-error.
Take the transaction fees in an exchange as an example. An order may be
executed in multiple deals, with each deal independently charged a fee.
For instance, the funds precision is 2 decimal places, one order quantity
is 10.00 USD, and the fee rate is 0.003. If the order is executed all
at once, the fee would be 10.00 × 0.003 = 0.03 USD. However, if the
order is executed in five separate deals, each worth 2.00 USD, then the
fee for each deal would be 2.00 × 0.003 = 0.006 USD, which rounds up
to 0.01 USD. Then the total fee for the 5 deals would be 0.05 USD,
which is significantly higher than the original 0.03 USD.
However, this issue can be avoid if using the cum_error mechanism.
use ;
type Balance = ;
type FeeRate = ;
let deal: Balance = fpdec!; // 2.00 for each deal
let fee_rate: FeeRate = fpdec!;
// normal case
let mut total_fee = ZERO;
for _ in 0..5
assert_eq!; // 0.05 is too big
// use `cum_error`
let mut cum_error = 0;
let mut total_fee = ZERO;
for _ in 0..5
assert_eq!; // 0.03 is right
Features
serdeenables serde traits integration (Serialize/Deserialize).
Status
More tests are need before ready for production.
License: MIT