finrb
Precision-first financial mathematics for Ruby.
finrb provides decimal calculations for cashflows, interest rates, loan
amortization, time value of money, investment returns, financial ratios, and
basic accounting. Public APIs live under explicit Finrb namespaces, and
loading the gem does not modify Ruby's core classes.
finrb is a fork of the Ruby finance gem and includes calculations ported from R's FinCal package.
Installation
Add finrb to your bundle:
bundle add finrb
Or install it directly:
gem install finrb
finrb requires Ruby 3.3 or newer.
Quick start
require 'finrb'
cashflows = [-4000, 1200, 1410, 1875, 1050]
Finrb::Cashflow.irr(cashflows).round(6)
# => Flt::DecNum('0.142993')
Finrb::Cashflow.npv(cashflows, 0.10).round(2)
# => Flt::DecNum('382.08')
Inputs are validated and calculations return Flt::DecNum values unless an
API explicitly returns another financial object, such as Finrb::Rate.
Financial domains
| Namespace | Purpose |
|---|---|
Finrb::Cashflow |
NPV, XNPV, IRR, and XIRR |
Finrb::Rate |
Nominal APR, effective APY, and compounding conversions |
Finrb::Amortization |
Fixed and adjustable-rate loan amortization |
Finrb::TVM |
Present value, future value, payments, periods, and perpetuities |
Finrb::Returns |
Holding-period, time-weighted, portfolio, and risk-adjusted returns |
Finrb::Yields |
Money-market, bond-equivalent, effective, and continuous yield conversions |
Finrb::Ratios |
Liquidity, leverage, profitability, and per-share ratios |
Finrb::Accounting |
Inventory costing and depreciation |
The API and examples guide documents each financial domain and
its public calculations. Packaged RBS declarations are available under sig/.
Cashflows
Periodic IRR is a per-period rate. XIRR evaluates irregularly dated
Finrb::Transaction objects and returns an effective annual Finrb::Rate.
require 'date'
transactions = [
Finrb::Transaction.new(-10_000, date: Date.new(2020, 1, 1)),
Finrb::Transaction.new(12_500, date: Date.new(2022, 1, 1))
]
rate = Finrb::Cashflow.xirr(transactions, 0.10)
rate.apy.round(6)
# => Flt::DecNum('0.117863')
Cashflows must contain at least one positive and one negative amount. Dated
cashflows must be chronological and every transaction must have a date. For
ordinary discrete discounting, rates and guesses must be greater than -1.
IRR and XIRR can have more than one economically valid root. The optional guess controls which nearby sign-changing root finrb selects:
cashflows = [-100, 230, -132] # roots at 10% and 20%
Finrb::Cashflow.irr(cashflows, 0.05).round(2) # => 0.10
Finrb::Cashflow.irr(cashflows, 0.25).round(2) # => 0.20
Rates and amortization
APR is a nominal annual rate; APY is an effective annual rate. They are not aliases:
rate = Finrb::Rate.new(0.12, :apr)
rate.monthly # => Flt::DecNum('0.01')
rate.apy.round(6) # => Flt::DecNum('0.126825')
Create a fixed-rate loan by giving the rate a duration in months:
rate = Finrb::Rate.new(0.0425, :apr, duration: 30 * 12)
loan = Finrb::Amortization.new(250_000, rate)
loan.payment # => Flt::DecNum('-1229.85')
loan.interest.sum # => Flt::DecNum('192745.98')
loan.balance # => Flt::DecNum('0.00')
first = loan.schedule.first
first.opening_balance
first.interest
first.principal
first.payment
first.balloon_payment
first.interest_only?
first.closing_balance
Pass several duration-bearing rates for an adjustable-rate schedule. A block can modify each scheduled payment, for example to add a $150 principal payment:
faster = Finrb::Amortization.new(250_000, rate) do |period|
period.payment - 150
end
Payments and interest follow the sign convention used throughout finrb: money received is positive and money paid is negative. Schedule balances, interest, principal repaid, and additional principal are non-negative; the schedule's payment field is negative.
Set a residual principal target to create a balloon loan. Regular installments amortize only the non-balloon portion, and the final payment settles the stated balloon:
balloon_loan = Finrb::Amortization.new(250_000, rate, balloon: 100_000)
balloon_loan.schedule.last.balloon_payment # => Flt::DecNum('100000')
balloon is the contractual residual target. Because regular postings are
rounded to cents, the actual balloon_payment in the final schedule row can
differ from that target by a few cents.
Leading interest-only periods defer scheduled principal repayment and amortize the balance over the remaining term:
interest_only = Finrb::Amortization.new(250_000, rate, interest_only_periods: 24)
interest_only.schedule.first.interest_only? # => true
interest_only.schedule.first.principal # => Flt::DecNum('0')
Origination fees can either reduce the borrower's net proceeds or be added to the financed balance:
cash_fee = Finrb::Amortization.new(250_000, rate, origination_fee: 2_500)
cash_fee.net_proceeds # => Flt::DecNum('247500')
cash_fee.amount_financed # => Flt::DecNum('250000')
financed_fee = Finrb::Amortization.new(
250_000,
rate,
origination_fee: 2_500,
finance_origination_fee: true
)
financed_fee.net_proceeds # => Flt::DecNum('250000')
financed_fee.amount_financed # => Flt::DecNum('252500')
Configuration
Configure process-wide defaults during application startup:
Finrb.configure do |config|
config.eps = '1e-12'
config.guess = 0.10
config.business_days = false
config.periodic_compound = false
end
Configuration is validated and published as one immutable snapshot.
Finrb.config is read-only. Use Finrb.with_config for a temporary override
that is restored afterward and does not leak into another thread:
Finrb.with_config(guess: 0.25) do
Finrb::Cashflow.irr(cashflows)
end
The business_days compatibility option excludes weekends only. It is not a
holiday calendar or a market business-day convention.
Precision, rounding, and failures
General calculations retain the active Flt::DecNum context and are not
rounded for display. Callers choose presentation precision with round or a
formatter. Finrb.config.eps controls root-solver convergence; it does not set
decimal arithmetic precision.
Amortization is deliberately different because payments and interest are
monetary postings. They are rounded to cents using half-up rounding, and any
remaining cent-level balance is allocated to the final payment. These policies
are exposed through Finrb::Precision.
finrb reports invalid financial or numerical states explicitly:
Finrb::InvalidCashflowErrorfor malformed cashflow sequencesFinrb::DomainErrorfor values outside a calculation's legal domainFinrb::ConvergenceErrorwhen a root cannot be bracketed or solvedArgumentErrorfor invalid public inputs and options
Migrating from the legacy API
The current API contains intentional breaking changes:
Finrb::Utilswas removed. UseFinrb::TVM,Returns,Yields,Ratios, orAccountingaccording to the calculation's domain.- Loading
finrbno longer adds methods toArrayorNumeric. Finrb.configis immutable; useFinrb.configureorFinrb.with_config.- APR is nominal and APY is effective, so conversions now follow their stated financial semantics.
Applications migrating gradually can explicitly load the legacy fluent core extensions:
require 'finrb/core_ext'
[-4000, 1200, 1410, 1875, 1050].irr
250_000.amortize(rate)
New code should use Finrb::Cashflow.irr(cashflows) and
Finrb::Amortization.new(principal, rate).
Development and verification
Install the bundle and run the self-contained quality checks:
bundle install
bundle exec rake quality
bundle exec rubocop
bundle exec rake security:audit
bundle exec rake package:verify
The quality task runs the RSpec suite with line and branch coverage, generated IRR/XIRR properties, committed SciPy/QuantLib reference fixtures, and RBS validation.
security:audit updates ruby-advisory-db and checks the locked dependencies.
package:verify builds the gem, validates its contents and metadata, installs
it with only its declared runtime dependencies, and runs packaged API smoke
tests without publishing or retaining the temporary installation.
CI runs every supported MRI version on x86-64, native ARM64, and Docker, plus compatibility specs and Docker builds for the current stable JRuby and TruffleRuby. Alternative Ruby jobs are initially informational while their dependency and numerical compatibility is assessed.
Select the MRI version used by the ordinary Docker tasks with RUBY_VER:
RUBY_VER=3.3 bundle exec rake docker:build
RUBY_VER=3.3 bundle exec rake docker:test
RUBY_VER=3.3 bundle exec rake docker:run
On an x86-64 development machine with Docker Buildx and ARM64 emulation available, build, test, and run the ARM64 development image with:
bundle exec rake docker:arm64:build
bundle exec rake docker:arm64:test
bundle exec rake docker:arm64:run
Docker Desktop normally provides the required emulation. A Linux Docker Engine installation must have an ARM64-capable Buildx builder and binfmt/QEMU support configured by the operator.
The alternative Ruby development images share a minimal, package-manager- independent Dockerfile. Build, test, and run either implementation with:
bundle exec rake docker:jruby:build
bundle exec rake docker:jruby:test
bundle exec rake docker:jruby:run
bundle exec rake docker:truffleruby:build
bundle exec rake docker:truffleruby:test
bundle exec rake docker:truffleruby:run
The defaults track JRuby 10 on JDK 21 and the current TruffleRuby Community
image. Override them with JRUBY_IMAGE or TRUFFLERUBY_IMAGE when testing a
specific release. These images use gemfiles/engines.gemfile, which contains
only finrb's runtime dependencies and RSpec; MRI-only development tooling such
as RBS, RuboCop, and coverage is deliberately excluded from engine
compatibility runs.
Maintainers with the optional Python environment can run the larger seeded solver verification campaign:
python3 -m pip install --requirement script/requirements-solver-verification.txt
bundle exec rake solver:verify
The Python packages are verification references, not gem dependencies. See the fixture documentation for reproducibility, Docker, batching, and tolerance details.
Project links
Acknowledgements
- Martin Bjeldbak Madsen, Bill Kranec, and the contributors to the original finance gem
- Yanhui Fan and the contributors to R's FinCal package
License
finrb is available under the GNU Lesser General Public License v3.0 or later. See COPYING and COPYING.LESSER.