FinanceRustler

CIHex.pmHex Docs

A native solver backend for finance — the safeguarded Newton (rtsafe) root-finder behind irr, xirr, rate, and ytm, ported to Rust with Rustler.

It plugs into finance's Finance.Solver behaviour as a swappable backend, the way EXLA plugs into Nx. The dependency runs one way — finance_rustler depends on finance, never the reverse — so the core library stays pure and unaware this package exists.

The numbers don't change: results match the built-in solver to the requested :precision. What changes is throughput, above all solve_many/2, which solves a whole batch in one call across a rayon thread pool.

Installation

Add both packages — finance for the API, finance_rustler for the backend:

def deps do
[
{:finance, "~> 1.5"},
{:finance_rustler, "~> 0.1"}
]
end

Precompiled binaries ship for Linux — x86_64 and aarch64 (gnu) — so on those targets nothing extra is needed. On other platforms (musl, macOS, Windows), set FINANCE_RUSTLER_BUILD=1 and have a Rust toolchain (cargo/rustc) to compile the NIF from source.

Usage

Point finance at the backend once, in config:

# config/config.exs
config :finance, solver: FinanceRustler.Solver

or choose it per call:

Finance.CashFlow.xirr(flows, solver: FinanceRustler.Solver)
Finance.CashFlow.xirr_many(portfolio, solver: FinanceRustler.Solver)

Nothing else changes — the same finance functions, the same results.

Performance

The backend earns its keep on batches. bench/solve_many.exs pits solve_many/2 against the pure-Elixir solver (chunked Task.async_stream) and a plain sequential map — median time to solve the whole batch:

batchnative (rayon)pure (chunked)sequential
1,000 × 4-flow2.4 ms7.6 ms8.1 ms
1,000 × 60-period loan14.8 ms23.7 ms185 ms
5,000 × 60-period loan114 ms98 ms1,004 ms

Both parallel strategies beat a sequential map by an order of magnitude. The native backend leads on batches of many small series — one NIF crossing plus rayon, against one process spawn per series — and keeps the arithmetic off the BEAM schedulers. On large batches of costlier series the chunked pure solver draws level, so this backend is an option for throughput, not a requirement.

Run them yourself:

mix run bench/solve_many.exs # batch: native vs pure vs sequential
mix run bench/native_vs_pure.exs # single solve, by flow length

Building and testing

mix deps.get
FINANCE_RUSTLER_BUILD=1 mix compile # builds native/finance_rustler via cargo
FINANCE_RUSTLER_BUILD=1 mix test # parity tests against the pure-Elixir solver

The parity tests assert that every result — single and batched, success and error — matches finance's default solver exactly.

Downstream projects on Linux get the precompiled binary via rustler_precompiled and need no Rust toolchain. Cutting a release: push a v* tag, let the release workflow cross-build the Linux NIFs and attach them to the GitHub release, then run mix rustler_precompiled.download FinanceRustler.Solver --all to generate the checksum file, commit it, and mix hex.publish.

License

MIT — see LICENSE.