finance-solution
finance-solution is a Rust library for time-value-of-money (TVM), cashflow, amortization, and related finance formulas — with detailed solution structs, period-by-period series, and pretty-printed tables. Scalar math executes in nanoseconds, making it suitable for quant systems. Solution structs execute in microseconds, providing observability with speed.
finance-solution is a financial library for solving time-value-of-money problems and financial math, including common quant calculations. 💸
People who will find this crate helpful include:
- Students of Finance who want to solve their financial problems using something better than a crude handheld calculator. Using this library also reduces the chance of human error, and provides better output and data displays than Excel in less time thereby providing a better surface for internalizing financial math concepts.
- New developers who want to learn Rust using Finance as a topic.
- Experienced Rust developers who want to learn more about finance and/or incorporate safe financial calculations into their projects.
- Serious Rust developers who want to build financial software, and prefer to rely on a rigourously tested library instead of reinventing the wheel and spending hundreds of hours to develop and test their own library of financial calculations.
In the v0.0.0 release, this library was geared only towards the basic financial equations, regarding:
- Simple Time-Value-of-Money formulas -- this includes
present_value,future_value,rate, andperiods(known as NPER in Excel). - Cashflow Time-Value-of-Money formulas -- this includes
present_value_annuity,future_value_annuity,net_present_value, andpayment(known as PMT in Excel). - Rate conversions -- this includes all conversions between
apr,ear, andepr, and also includes conversion for continuous compounding (apr_continuous,ear_continuous).
The v0.1.0 release revamped to eliminate panicking APIs, enforcing all methods to return a
Result(asFinanceResultorFinanceError) and added an amortization module.
The v0.2.0 release added domain newtypes, full
stocks::tatechnical analysis indicators like Bollinger and Stochastics for both batch + incremental*State, and added teaching solutions/tables, and Criterion TA benches.
The v0.3.0 release added the
derivativesmodule (European Black–Scholes–Merton price, Greeks ΔΓθνρ, implied vol, put–call parity helpers,BsmState) plus a real 1m OHLCV example joining underlier TA with a BSM snapshot.
Current Status: 0.4.0 (edition 2021, MSRV 1.70). Extends derivatives (cross Greeks; Black ’76; Garman–Kohlhagen; CRR American/European tree + American IV) and TA (RSI, ATR), with deeper proptests and audit polish. Public math remains Result-only — no dual panicking /
try_*APIs. The crate is a math library for engines and frameworks (not a market-data feed or order router).
Modules (0.4 layout)
| Module | Contents |
|---|---|
tvm |
Present/future value, rate, periods (simple + continuous, fixed + schedule) |
cashflow |
Payment (PMT), annuities, NPV, NPER |
convert_rate |
APR ↔ EPR ↔ EAR (+ continuous) |
amortization |
Solution/series/tables + PPMT, IPMT, CUMPRINC, CUMIPMT |
returns |
Doubling rules: solution + comparison tables (72/70/69 vs exact) |
stocks |
Price-path solution/series/tables; returns, vol, Sharpe, Sortino, beta, drawdowns |
stocks::ta |
SMA/EMA/WMA/HMA, Stoch, MACD, BB, Keltner, Donchian, VWAP, RVOL, RSI, ATR, LinReg (+ *State) |
derivatives |
BSM (+ cross Greeks), Black ’76, Garman–Kohlhagen, CRR American/European tree + American IV; live *State |
util |
FinanceError, FinanceResult, domain newtypes (Rate, PeriodLength, …) |
round |
Rounding + test assert helpers |
Quick start
use *;
// Present value of $4,000 in 3 years at 5% (simple compounding)
// Fixture rates/amounts are valid; production code should use `?` or match.
let pv = present_value.expect;
assert_rounded_2!; // sign: opposite of future value
// Prefer the solution API for formulas + period series
let answer = present_value_solution.expect;
println!;
answer.series.print_table;
// Handle bad input without panicking
match present_value
Payment + amortization table
use *;
let rate = 0.08 / 12.0;
let periods = 60;
let principal = 13_000.0;
let solution = payment_solution.expect;
solution.print_table;
// Excel-style period components (1-based period index)
let interest_m1 = ipmt.expect;
let principal_m1 = ppmt.expect;
let year1_interest = cumipmt.expect;
Rate conversion
use *;
let r = apr.expect;
println!;
Doubling rules & price paths
use *;
let d = doubling_solution.expect;
assert_rounded_2!;
d.print_table;
let prices = ;
let path = price_path_solution.expect;
path.print_summary;
path.series.print_table;
Technical analysis — quant pattern (stochastics)
finance-solution is a math library, not a trading engine: you own the bar loop and data feed.
For indicators, define each variation once as a const parameter pack, validate once, then
call .compute on every batch of bars (or free functions for one-off scripts).
use *;
// 1) Strategy knobs — fixed packs (FastStoch(9,3), FullStoch(14,3,3), …)
const FAST_9_3: StochasticParams = fast;
const FULL_14_3_3: StochasticParams = full;
const MACD_STD: MacdParams = standard; // (12, 26, 9)
const BB_20_2: BollingerParams = standard;
// 2) Validate once at process/strategy startup (O(1))
// Fallible constructors are named `new` → FinanceResult (not `try_new`):
// fallibility lives in the return type, matching Result-only crate policy.
let stoch = new.expect;
let macd_eng = new.expect;
let bb = new.expect;
// 3) Hot path — reuse engines on each symbol / day batch
// let kd = stoch.compute(&high, &low, &close)?;
// let m = macd_eng.compute(&closes)?;
// let bands = bb.compute(&closes)?;
// Teaching / audit path (formulas + print_table with n/a warm-up):
// let sol = stochastics_solution(&high, &low, &close, FAST_9_3)?;
// sol.print_table();
Common stochastic packs: StochasticParams::fast(9, 3), fast(14, 3), full(14, 3, 3),
full(60, 10, 1). Same idea for MACD, Bollinger, Keltner, Donchian, VWAP, RVOL, RSI, ATR,
WMA/HMA, LinReg — see stocks::ta rustdoc.
Options — closed forms + CRR tree (BSM / Black ’76 / GK / American)
Trading: model mark vs mid, delta hedge ratio, vega/theta carry, surface risk (vanna/volga), IV from mid; American early-exercise premium via tree.
Engineering: sync pure functions; your feed calls set_spot / set_forward / set_vol_from_price on per-contract state.
| Model | When | Entry points |
|---|---|---|
| BSM | Equity / index spot + continuous yield (q) | bsm_price, bsm_greeks, bsm_cross_greeks, BsmState |
| Black ’76 | Options on forward/futures (F) | black76_price, Black76State (forward Δ) |
| Garman–Kohlhagen | FX spot with (r_d), (r_f) | gk_price, gk_greeks (ρ_d + ρ_f), GkState |
| CRR tree | European or American exercise | crr_price, crr_solution (node tables), american_implied_vol |
| Input | Unit in this crate |
|---|---|
time_years |
years (21.0/365.25 ≈ 21 calendar days) |
| rates | continuous absolute (0.05 = 5%) |
vol |
annualized absolute (0.20 = 20%) |
| raw vega | per +1.0 in σ → use vega_per_vol_point() for per 1% |
| raw theta / charm | per year → use *_per_calendar_day() |
use *;
let p = atm_one_year;
let call = bsm_price.expect;
let g = bsm_greeks.expect;
let x = bsm_cross_greeks.expect;
let _vanna = x.vanna;
let _vega_1pct = g.vega_per_vol_point;
let iv = bsm_implied_vol.expect;
assert!;
// Futures-style mark
let f = atm_one_year;
let _fut_call = black76_price.expect;
// FX-style
let fx = atm_one_year;
let _fx_call = gk_price.expect;
// Live: per-contract state in *your* HashMap
let mut opt = new.expect;
opt.set_spot.expect;
let _ = opt.greeks;
// opt.set_vol_from_price(mid)?;
// American put via CRR (N steps; teaching tables when N is small)
let tree = atm_one_year;
let _am_put = crr_price.expect;
Join underlier TA with the option chain in your engine: StochState / RsiState on bars + BsmState / Black76State / GkState on strikes.
Examples
# Real 1m OHLCV (GitHub tree only; excluded from crates.io package size)
# REAL_BARS_SYMBOL=SPY cargo run --example real_bars_ta_bsm --release
Design notes
- Result-only public math — domain failures are
FinanceResult/FinanceError. There is no dual panicking +try_*pair; the ordinary name is the fallible function. - Solution + series —
*_solutiontypes carry inputs, symbolic/numeric formulas, and.series()for period detail. - Excel-compatible signs — loans/payments follow spreadsheet conventions (positive principal → negative payment, etc.).
- Enums for flags — prefer [
Compounding] and [PaymentTiming];boolstill converts viaFromfor ergonomics. TA uses enums where modes matter (e.g.VwapMode,VwapPriceSource,StdevKind). - Tables —
.print_table()/.print_table_locale()for teaching and debugging (copy/paste friendly). - Hot path vs solution path — scalar / series APIs are the production path (
FinanceResult, no formula strings). Solution APIs are the teaching/observability path. Same formulas; different packaging. Seebenches/RESULTS.md. - TA params — one core function per indicator +
Copy*Params+ optionalValidated*; presets viaconstconstructors (fast,standard, …), not a zoo of nearly identical free functions. - Derivatives units — BSM vega/theta raw model units differ from desk screens; always use the helper methods above when mapping to “per vol point” / “per day” P&L.
- Math library, not an engine — no websockets, symbol books, calendars, or order routing. You own
HashMaps, session resets, and concurrency (rayon/tokiooutside this crate).
Live bars: incremental state (not a market-data engine)
This crate does not subscribe to feeds or manage multi-symbol books. It does provide
pure *State machines so your quant engine can update indicators on each payload
(tick / 5s / 1m) without recomputing the full history:
| Batch (research / backtest) | Incremental (live) |
|---|---|
stoch.compute(&h,&l,&c) |
StochState::push(h,l,c) |
macd_eng.compute(&closes) |
MacdState::push(close) |
vwap(...) cumulative |
VwapState::push(...) + reset() when you open a session |
bsm_price / bsm_greeks |
BsmState::set_spot / set_vol_from_price |
Quant engine sketch (per-symbol underlier + optional option chain):
use *;
use HashMap;
const FAST: StochasticParams = fast;
const MACD: MacdParams = standard;
const VWAP: VwapParams = cumulative_typical;
Maximum control: continue multi-day series (never reset), or reset VWAP/RVOL at open, or rebuild state from a sliced history. All of that is your policy; we only expose the levers.
State is parity-tested against batch series (see stocks::ta::state tests). Also:
SmaState, BollingerState, KeltnerState, RvolState.
Parameter order (when possible)
rate, periods, present_value, future_value, …
Money arguments often accept any Into<f64> (i32, f32, …). Rates stay f64; period counts are u32 (NPER returns f64 for fractional periods).
Error handling
Why. Libraries used from web handlers, batch jobs, or CLIs must not abort when a user types -150% interest or period 0.
Canonical pattern (structured match + ?):
use ;
match loan_payment
Affordability example (compose with ?):
use ;
// Call from a fallible main or handler:
// let pmt = affordability(50_000.0, 0.05, 0.06, 10, 60)?;
// pmt ≈ -593.43
| Quantity | Value | Meaning |
|---|---|---|
pv |
≈ −30,695.66 | Today’s value of $50k in 10 years at 5% |
| Loan principal | ≈ +30,695.66 | pv.abs() |
| Monthly payment | ≈ −593.43 | Negative = cash you pay |
| 60 × payment | ≈ −35,606 | Total cash paid over the loan |
Early payoff / mortgage what-ifs use the same Result APIs — see cargo run --example early_payoff_what_if and the amortization tables below.
Benefits:
- No surprise panics on bad external input
- Matchable variants for UI / metrics (
FinanceError::code()) std::error::Error+Displayfor logging and?into app error types- One name per formula — fallibility lives in the return type
Amortization: solution / series / tables
amortization_solution → .series() → .print_table()
↓ ↓
formulas + payment period rows (principal, interest, balance, formulas)
use *;
let rate = 0.08 / 12.0;
let periods = 12;
let principal = 10_000.0;
let solution = amortization_solution
.expect;
println!;
let series = solution.series;
assert_eq!;
series.print_table;
assert_approx_equal!;
assert_approx_equal!;
let _first_half = solution.cumipmt.unwrap;
Sample print_table(true, true) (12-month $10k loan at 8% APR monthly):
period payment principal interest balance principal_to_date interest_to_date payments_to_date principal_remaining interest_remaining payments_remaining
------ --------- --------- -------- ---------- ----------------- ---------------- ---------------- ------------------- ------------------ ------------------
1 -869.8843 -803.2176 -66.6667 9_196.7824 -803.2176 -66.6667 -869.8843 -9_196.7824 -371.9448 -9_568.7272
2 -869.8843 -808.5724 -61.3119 8_388.2100 -1_611.7900 -127.9785 -1_739.7686 -8_388.2100 -310.6329 -8_698.8429
...
12 -869.8843 -864.1235 -5.7608 -0.0000 -10_000.0000 -438.6115 -10_438.6115 0.0000 -0.0000 0.0000
| Need | API |
|---|---|
One Excel cell (IPMT month 3) |
ipmt(...)? / solution.ipmt(3)? |
| Full schedule | amortization_solution(...)?.series() |
| Terminal table | .print_table() / .print_table_locale() |
| Safe construction | amortization_solution → FinanceResult |
Returns & stocks
use *;
let s = doubling_solution.expect;
s.print_table;
doubling_compare_rates
.expect
.print_table;
let prices = ;
let path = price_path_solution
.expect;
path.print_summary;
path.series.print_table;
Testing & CI
Symmetry tests, Excel golden values, integration tests, TA proptests (batch ↔ stream parity), and TVM identity proptests cover the core surface. Derivatives unit tests cover BSM (incl. cross Greeks), Black ’76, Garman–Kohlhagen, CRR / American IV, and live *State types. CI runs build, tests, doctests, clippy, and fmt on stable and MSRV 1.70, and asserts there are no public try_* dual-API symbols.
Package notes
- crates.io ships the library + small examples; large OHLCV under
examples/data/is excluded (seeCargo.tomlexclude). - Clone from GitHub if you want
cargo run --example real_bars_ta_bsm --release.
License
MIT — see LICENSE.