Module: Xirr::TVM

Defined in:
lib/xirr/tvm.rb

Overview

Time-value-of-money scalars: TVM.fv, TVM.pv, TVM.pmt, TVM.ipmt, TVM.ppmt, TVM.nper, and TVM.rate, plus an TVM.amortization_schedule. Each solves the standard annuity equation for one unknown:

pv·(1+r)^n + pmt·(1 + r·type)·((1+r)^n − 1)/r + fv = 0

type is 0 for payments at the end of each period (an ordinary annuity) or 1 for the beginning (an annuity due). The sign convention follows spreadsheets: money you receive is positive, money you pay out is negative.

Class Method Summary collapse

Class Method Details

.amortization_schedule(rate, nper, pv, precision: 2) ⇒ Array<Hash>

Full amortization schedule for a loan of pv repaid with a level payment over nper periods at rate. Returns an array of row hashes with :period, :payment, :interest, :principal, and :balance; the balance runs from pv down to exactly 0. Monetary columns are rounded to precision (default 2, i.e. cents).

Returns:

  • (Array<Hash>)

Raises:

  • (ArgumentError)


105
106
107
108
109
110
# File 'lib/xirr/tvm.rb', line 105

def amortization_schedule(rate, nper, pv, precision: 2)
  n = nper.to_i
  raise ArgumentError, 'nper must be a positive whole number' unless nper == n && n >= 1

  build_schedule(rate.to_f, n, pv.to_f, precision)
end

.fv(rate, nper, pmt, pv = 0.0, type = 0) ⇒ Float

Future value: what an investment grows to after nper periods, starting from present value pv with a fixed pmt each period, compounding at rate.

Returns:

  • (Float)


20
21
22
23
24
25
26
27
# File 'lib/xirr/tvm.rb', line 20

def fv(rate, nper, pmt, pv = 0.0, type = 0)
  validate_type!(type)
  if rate.zero?
    -(pv + pmt * nper) * 1.0
  else
    -(pv * (1 + rate)**nper + pmt * annuity(rate, nper, type)) * 1.0
  end
end

.ipmt(rate, per, nper, pv, fv = 0.0, type = 0) ⇒ Float

Interest portion of the payment in period per (counting from 1). Pairs with ppmt: the two add up to pmt for every period.

Returns:

  • (Float)


55
56
57
58
# File 'lib/xirr/tvm.rb', line 55

def ipmt(rate, per, nper, pv, fv = 0.0, type = 0)
  validate_type!(type)
  split_payment(rate, per, nper, pv, fv, type).first
end

.nper(rate, pmt, pv, fv = 0.0, type = 0) ⇒ Float

Number of periods it takes for payments of pmt to pay off pv (reaching fv) at rate.

Returns:

  • (Float)

Raises:

  • (ArgumentError)


71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/xirr/tvm.rb', line 71

def nper(rate, pmt, pv, fv = 0.0, type = 0)
  validate_type!(type)
  raise ArgumentError, 'undefined' if rate.zero? && pmt.zero?
  return -(pv + fv) / pmt * 1.0 if rate.zero?
  raise ArgumentError, 'undefined' if 1 + rate <= 0

  k = pmt * (1 + rate * type) / rate
  denom = pv + k
  raise ArgumentError, 'undefined' if denom.zero? || (k - fv) / denom <= 0

  Math.log((k - fv) / denom) / Math.log(1 + rate)
end

.pmt(rate, nper, pv, fv = 0.0, type = 0) ⇒ Float

Level payment per period that pays off pv (and reaches fv) over nper periods at rate.

Returns:

  • (Float)

Raises:

  • (ArgumentError)


44
45
46
47
48
49
50
# File 'lib/xirr/tvm.rb', line 44

def pmt(rate, nper, pv, fv = 0.0, type = 0)
  validate_type!(type)
  raise ArgumentError, 'nper must be non-zero' if nper.zero?
  return -(pv + fv) / nper * 1.0 if rate.zero?

  -(pv * (1 + rate)**nper + fv) / annuity(rate, nper, type) * 1.0
end

.ppmt(rate, per, nper, pv, fv = 0.0, type = 0) ⇒ Float

Principal portion of the payment in period per (counting from 1). The companion of ipmt.

Returns:

  • (Float)


63
64
65
66
# File 'lib/xirr/tvm.rb', line 63

def ppmt(rate, per, nper, pv, fv = 0.0, type = 0)
  validate_type!(type)
  split_payment(rate, per, nper, pv, fv, type).last
end

.pv(rate, nper, pmt, fv = 0.0, type = 0) ⇒ Float

Present value: what a stream of pmt per period for nper periods plus a lump sum fv at the end is worth today, discounted at rate.

Returns:

  • (Float)


32
33
34
35
36
37
38
39
# File 'lib/xirr/tvm.rb', line 32

def pv(rate, nper, pmt, fv = 0.0, type = 0)
  validate_type!(type)
  if rate.zero?
    -(fv + pmt * nper) * 1.0
  else
    -(fv + pmt * annuity(rate, nper, type)) / (1 + rate)**nper * 1.0
  end
end

.rate(nper, pmt, pv, fv = 0.0, type = 0, guess: 0.1) ⇒ Float

Interest rate per period of an annuity of nper payments of pmt, present value pv, and future value fv. There is no closed form, so this reuses the RtSafe solver.

Returns:

  • (Float)

Raises:

  • (ArgumentError)


88
89
90
91
92
93
94
95
96
97
# File 'lib/xirr/tvm.rb', line 88

def rate(nper, pmt, pv, fv = 0.0, type = 0, guess: 0.1)
  validate_type!(type)
  n = nper.to_i
  raise ArgumentError, 'nper must be a positive whole number' unless nper == n && n.positive?

  result = RtSafe.find(tvm_flows(n, pmt, pv, fv, type), guess: guess)
  raise ArgumentError, 'rate did not converge' if result.nil?

  result
end