finmoney
A precise money library for Rust built for trading systems. Provides currency-aware arithmetic,
exchange-grade tick handling, configurable rounding, and fair allocation. Built on rust_decimal
for exact decimal calculations.
Design Principles
- Ergonomic by default. Comparisons (
<,>,<=,>=), scalar arithmetic (* Decimal), and in-place operations (+=,-=) return direct values — no.unwrap()ceremony on every line. - Result where it matters. Only operations with real runtime failure modes (currency mismatch
between two
FinMoneyvalues, division by zero, NaN/Infinity) returnResult. - Checked variants available. Every panicking method has a
Result-returning counterpart (unchecked_plus↔plus_money,* Decimal↔multiplied_by_decimal). - Precise.
rust_decimal128-bit decimals — no floating-point surprises.
Requirements
- Rust 1.90+ (edition 2024)
Quick Start
[]
= "4.0"
# With serde support
= { = "4.0", = ["serde"] }
use ;
use dec;
let usd = USD;
// Create money
let price = new;
let tax = new;
// Arithmetic: FinMoney + FinMoney returns Result (currency mismatch possible)
let total = ?; // 11.55 USD
// Scalar multiply returns FinMoney directly (no currency check needed)
let doubled = price * dec!; // 21.00 USD
// Comparisons work with standard operators
if price > tax
Constructors
use ;
use dec;
let usd = USD;
let btc = BTC;
// From Decimal
let m = new;
// From integer
let m = from_i64;
// From minor units (cents, satoshi)
let m = from_minor; // 10.50 USD
let m = from_minor; // 1.00000000 BTC
// From string
let m = from_str?;
// From f64 (returns Result — NaN/Infinity produce errors)
let m = from_f64?;
// Zero
let m = zero;
Arithmetic
let a = new;
let b = new;
// FinMoney ↔ FinMoney: returns Result (currency mismatch possible)
let sum = ?; // 130 USD
let diff = ?; // 70 USD
// FinMoney ↔ Decimal: returns FinMoney directly (no currency check needed)
let added = a + dec!; // 105 USD
let subbed = a - dec!; // 95 USD
let tripled = a * dec!; // 300 USD
let also = dec! * a; // 300 USD
// In-place operators (all direct, panic only on currency mismatch for FinMoney)
let mut total = zero;
total += a; // FinMoney += FinMoney
total -= b; // FinMoney -= FinMoney
total += dec!; // FinMoney += Decimal
total -= dec!; // FinMoney -= Decimal
total *= dec!; // FinMoney *= Decimal
// Division (returns Result — division by zero possible)
let half = a.divided_by_decimal?;
// Named methods (same as operators, available for readability)
let adjusted = a.plus_decimal; // 105 USD
let reduced = a.minus_decimal; // 95 USD
// Unchecked variants for hot paths (panic on mismatch)
let fast_sum = a.unchecked_plus; // 130 USD
let fast_diff = a.unchecked_minus; // 70 USD
Comparisons
FinMoney implements PartialOrd and Ord, so standard Rust operators work directly:
let price = new;
let limit = new;
// Standard operators (panic on currency mismatch)
if price > limit
if price <= limit
// Min/max
let lower = price.min; // 9.75 USD
let higher = price.max; // 10.50 USD
// Sorting works out of the box
let mut prices = vec!;
prices.sort;
// std::cmp::min/max
let m = min;
// Named methods (also direct, panic on mismatch)
price.is_greater_than; // true
price.is_less_than_or_equal; // false
// Checked variant (returns Result)
price.try_compare?; // Ordering::Greater
// Decimal comparisons (no currency check)
price.is_greater_than_decimal; // true
Tick Handling
Exchange-grade price/quantity rounding to valid tick sizes:
let price = new;
// Round to tick
let nearest = price.to_tick_nearest?; // 10.50 USD
let floor = price.to_tick_down?; // 10.50 USD
let ceil = price.to_tick_up?; // 10.75 USD
// Validate tick alignment
price.is_multiple_of_tick; // true
// Handles trailing zeros from exchange data (e.g., 0.000100000 → 0.0001)
let tick = new; // 0.000100000
price.to_tick_nearest?; // uses fast path correctly
Allocation and Splitting
let total = new;
// Equal split with fair remainder distribution
let parts = total.split?;
// [33.34 USD, 33.33 USD, 33.33 USD] — sum is always exact
// Weighted allocation
let parts = total.allocate?;
// [70.00 USD, 20.00 USD, 10.00 USD]
Conversion
let money = new;
// To minor units (cents, satoshi)
let cents = money.to_minor_units; // 12345
// To f64 (explicitly lossy — for UI/metrics only)
let f = money.to_f64_lossy; // 123.45
// Currency conversion at a rate
let eur = money.convert_to?; // 113.57 EUR
// Calculate exchange rate
let rate = money.exchange_rate_to?; // 0.92...
Formatting
let m = new;
println!; // 1234567.89 USD
println!; // 1,234,567.89 USD
println!; // 1234567.8900 USD
Rounding Strategies
let amount = new;
amount.rounded; // 10.56 (banker's)
amount.rounded; // 10.56
amount.rounded; // 10.55
amount.rounded; // 10.55 (floor)
amount.rounded; // 10.56 (ceil)
Currencies
No predefined currencies — create any currency for any domain:
// Standard constructor (validates inputs, returns Result)
let usd = new?;
let btc = new?;
let gold = new?;
// Lenient constructor (sanitizes invalid inputs, never fails)
let safe = new_sanitized;
// High-performance constructor (pre-parsed TinyAsciiStr, skips parsing)
let code: TinyAsciiStr = "USD".parse.unwrap;
let fast = new_from_tiny?;
Error Handling
use FinMoneyError;
match usd_money + eur_money
// Predicates for matching
if let Err = result
API Design: When Result vs Direct
Returns Result |
Returns direct value |
|---|---|
FinMoney + FinMoney (currency mismatch) |
FinMoney + Decimal, FinMoney - Decimal |
FinMoney - FinMoney (currency mismatch) |
FinMoney * Decimal, Decimal * FinMoney |
divided_by_decimal (division by zero) |
+= / -= (FinMoney and Decimal) |
divided_by_money (both) |
*= Decimal, - (Neg) |
convert_to (invalid rate) |
< / > / <= / >= (PartialOrd) |
from_f64 / from_str (invalid input) |
min / max / compare |
allocate / split (zero weights/parts) |
abs / floor / ceil / trunc |
sqrt (negative amount) |
rescale |
to_tick (invalid tick ≤ 0) |
unchecked_plus / unchecked_minus / unchecked_mul |
Every panicking method has a checked counterpart: plus_money() for unchecked_plus(),
multiplied_by_decimal() for *, try_compare() for compare().
FinMoney implements: Debug Clone Copy PartialEq Eq Hash PartialOrd Ord Display Neg Add Sub Mul AddAssign SubAssign MulAssign Sum.
Does NOT implement Default — use FinMoney::zero(currency) for explicit construction.
Migration from v3 to v4
Comparisons return direct values (not Result):
// v3:
if price.is_greater_than?
let min = a.min?;
// v4:
if price > other // PartialOrd
if price.is_greater_than // also works, returns bool
let min = a.min; // returns FinMoney
Scalar arithmetic returns direct values (and has operators):
// v3:
let result = money.plus_decimal?;
let doubled = ?;
let rescaled = money.rescale?;
// v4:
let result = money + dec!; // Add<Decimal> operator
let doubled = money * dec!; // Mul<Decimal> operator
let rescaled = money.rescale; // direct FinMoney
money += dec!; // AddAssign<Decimal>
money -= dec!; // SubAssign<Decimal>
money *= dec!; // MulAssign<Decimal>
New traits:
Hash— normalized hashing (10.50 and 10.5 hash equally), enablesHashSet<FinMoney>PartialOrd/Ord— standard<,>,<=,>=operators andsort()Add<Decimal>/Sub<Decimal>—money + dec!(5)works directlyAddAssign/SubAssign/MulAssignfor bothFinMoneyandDecimal- Removed
Default— prevents silent creation of UNDEFINED currency values
New methods:
from_minor()/to_minor_units()— minor unit conversion (cents, satoshi)from_str()— parse from decimal stringto_f64_lossy()— explicit lossy f64 conversion for UI/metricssplit(n)— equal division with fair remainder distributionunchecked_plus()/unchecked_minus()/unchecked_mul()— hot path variantstry_compare()— checked comparison returning Resultget_currency_code_tiny()/get_currency_name_tiny()— zero-copy TinyAsciiStr accessorstick_power10_dp()— now public
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.