Expand description
Primitive fixed-point decimal types.
For example, ConstScaleFpdec<i64, 4>
means using i64
as the underlying
representation, and the static scale is 4
.
§Features
-
Fixed-point. The scale is bound to the type but not each value.
-
Decimal. Using integer types to represent numbers with a scaling factor (also called as “scale”) in base 10 to achieve the accuracy. This is a common idea.
-
The
+
and-
operations only perform between same types in same scale. There is no implicitly type or scale conversion. This makes sense, for we do not want to addBalance
type byPrice
type. -
The
*
and/
operations accept operand with different types and scales, and allow the result’s scale specified. Certainly we need to multiply betweenBalance
type andPrice
type. -
Supports 2 ways to specify the scale: const and out-of-band. See the Specify Scale section for details.
-
Supports cumulative error. See the Cumulative Error section for details.
-
Supports both signed and unsigned types.
-
Supports scale larger than the significant digits of the underlying integer type. For example
ConstScaleFpdec<i8, 4>
represents numbers in range [-0.0128, 0.0127]. -
Supports negative scale. For example
ConstScaleFpdec<i8, -2>
represents numbers in range [-12800, 12700] with step 100. -
no_std
.
§Specify Scale
There are 2 ways to specify the scale: const and out-of-band:
-
For the const type
ConstScaleFpdec
, we use Rust’s const generics to specify the scale. For example,ConstScaleFpdec<i64, 4>
means scale is 4. -
For the out-of-band type
OobScaleFpdec
, we do NOT save the scale with decimal types, so it’s your job to save it somewhere and apply it in the following operations later. For example,OobScaleFpdec<i64>
takes no scale information.
Generally, the const type is more convenient and suitable for most
scenarios. For example, in traditional currency exchange, you can use
ConstScaleFpdec<i64, 2>
to represent balance, e.g. 1234.56
USD and
8888800.00
JPY. And use ConstScaleFpdec<u32, 6>
to represent all
market prices since 6-digit-scale is big enough for all currency
pairs, e.g. 146.4730
JPY/USD and 0.006802
USD/JPY:
use primitive_fixed_point_decimal::{ConstScaleFpdec, fpdec};
type Balance = ConstScaleFpdec<i64, 2>; // 2 is enough for all currencies
type Price = ConstScaleFpdec<u32, 6>; // 6 is enough for all markets
let usd: Balance = fpdec!(1234.56);
let price: Price = fpdec!(146.4730);
let jpy: Balance = usd * price;
assert_eq!(jpy, fpdec!(180829.70688));
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 scales for each market. So it’s
the Out-of-band type:
use primitive_fixed_point_decimal::{OobScaleFpdec, fpdec};
type Balance = OobScaleFpdec<i64>; // no global scale set
type Price = OobScaleFpdec<u32>; // no global scale set
// each market has its own scale configuration
struct Market {
base_asset_scale: i32,
quote_asset_scale: i32,
price_scale: i32,
}
// let's take BTC/USDT market as example
let btc_usdt = Market {
base_asset_scale: 8,
quote_asset_scale: 6,
price_scale: 1,
};
// we need tell the scale to `fpdec!`
let btc: Balance = fpdec!(0.34, btc_usdt.base_asset_scale);
let price: Price = fpdec!(81234.0, btc_usdt.price_scale);
// we need tell the scale difference to `checked_mul()` method
let diff = btc_usdt.base_asset_scale + btc_usdt.price_scale - btc_usdt.quote_asset_scale;
let usdt = btc.checked_mul(price, diff).unwrap();
assert_eq!(usdt, fpdec!(27619.56, btc_usdt.quote_asset_scale));
Obviously it’s verbose to use, but offers greater flexibility.
Another example is the SQL Decimal
data type.
In the server end, the scale of each decimal column is fixed on created
(at runtime), so it fits OobScaleFpdec
.
While in the client end, the application knows the business logical and
the scale of each decimal column ahead (at compilation time), so it fits
ConstScaleFpdec
.
§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_err
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 scale 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_err mechanism.
use primitive_fixed_point_decimal::{ConstScaleFpdec, CumErr, Rounding, fpdec};
type Balance = ConstScaleFpdec<i64, 2>;
type FeeRate = ConstScaleFpdec<i16, 6>;
let deal: Balance = fpdec!(2.00); // 2.00 for each deal
let fee_rate: FeeRate = fpdec!(0.003);
// normal case
let mut total_fee = Balance::ZERO;
for _ in 0..5 {
total_fee += deal.checked_mul(fee_rate).unwrap(); // 2.00*0.003=0.006 ~> 0.01
}
assert_eq!(total_fee, fpdec!(0.05)); // 0.05 is too big
// use `cum_err`
let mut cum_err = CumErr::new();
let mut total_fee = Balance::ZERO;
for _ in 0..5 {
total_fee += deal.checked_mul_ext(fee_rate, Rounding::Round, Some(&mut cum_err)).unwrap();
}
assert_eq!(total_fee, fpdec!(0.03)); // 0.03 is right
§Features
serde
enables serde traits integration (Serialize
/Deserialize
).
Macros§
- fpdec
- Build decimal from integer or float number easily.
Structs§
- Const
Scale Fpdec - Const-scale fixed-point decimal.
- CumErr
- Cumulative error.
- OobFmt
- Wrapper to display/load OobScaleFpdec.
- OobScale
Fpdec - Out-of-band-scale fixed-point decimal.
Enums§
- Parse
Error - Error in converting from string.
- Rounding
- Rounding kinds.
Traits§
- Fpdec
Inner - The trait for underlying representation.