Module: Finrb::TVM

Defined in:
lib/finrb/tvm.rb,
sig/finrb.rbs

Overview

Time-value-of-money calculations for periodic rates and cashflows.

Class Method Summary collapse

Class Method Details

.discount_rate(n:, pv:, fv:, pmt:, type: 0, guess: nil, lower: UNSET_BOUND, upper: UNSET_BOUND) ⇒ decimal

Parameters:

  • n: (number)
  • pv: (number)
  • fv: (number)
  • pmt: (number)
  • type: (Integer) (defaults to: 0)
  • guess: (number, nil) (defaults to: nil)
  • lower: (number, nil) (defaults to: UNSET_BOUND)
  • upper: (number, nil) (defaults to: UNSET_BOUND)

Returns:

  • (decimal)


20
21
22
23
24
25
26
27
28
29
30
# File 'lib/finrb/tvm.rb', line 20

def discount_rate(n:, pv:, fv:, pmt:, type: 0, guess: nil, lower: UNSET_BOUND, upper: UNSET_BOUND)
  n = period_count(n)
  pv, fv, pmt = decimal_inputs(pv:, fv:, pmt:).values
  type = payment_type(type)
  function = ->(rate) { fv_simple(r: rate, n:, pv:) + fv_annuity(r: rate, n:, pmt:, type:) - fv }

  bounds = rate_bounds(function, guess:, lower:, upper:)
  return bounds.first if bounds.first == bounds.last

  Numerical::Brent.new(tolerance: Finrb.config.eps).solve(function, lower: bounds.first, upper: bounds.last)
end

.fv(r:, n:, pv: 0, pmt: 0, type: 0) ⇒ decimal

Parameters:

  • r: (number)
  • n: (number)
  • pv: (number) (defaults to: 0)
  • pmt: (number) (defaults to: 0)
  • type: (Integer) (defaults to: 0)

Returns:

  • (decimal)


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

def fv(r:, n:, pv: 0, pmt: 0, type: 0)
  rate = periodic_rate(r)
  periods = period_count(n)
  payment_type(type)

  fv_simple(r: rate, n: periods, pv:) + fv_annuity(r: rate, n: periods, pmt:, type:)
end

.fv_annuity(r:, n:, pmt:, type: 0) ⇒ decimal

Parameters:

  • r: (number)
  • n: (number)
  • pmt: (number)
  • type: (Integer) (defaults to: 0)

Returns:

  • (decimal)


40
41
42
43
44
45
46
47
48
# File 'lib/finrb/tvm.rb', line 40

def fv_annuity(r:, n:, pmt:, type: 0)
  rate = periodic_rate(r)
  periods = period_count(n)
  payment = Validation.decimal(pmt, name: 'payment')
  payment_timing = payment_type(type)
  return -payment * periods if rate.zero?

  (payment / rate * (((rate + 1)**periods) - 1)) * ((rate + 1)**payment_timing) * -1
end

.fv_simple(r:, n:, pv:) ⇒ decimal

Parameters:

  • r: (number)
  • n: (number)
  • pv: (number)

Returns:

  • (decimal)


50
51
52
53
54
55
# File 'lib/finrb/tvm.rb', line 50

def fv_simple(r:, n:, pv:)
  rate = periodic_rate(r)
  periods = period_count(n)
  present_value = Validation.decimal(pv, name: 'present value')
  (present_value * ((rate + 1)**periods)) * -1
end

.fv_uneven(r:, cf:) ⇒ decimal

Parameters:

  • r: (number)
  • cf: (number, numbers)

Returns:

  • (decimal)


57
58
59
60
61
62
63
64
# File 'lib/finrb/tvm.rb', line 57

def fv_uneven(r:, cf:)
  rate = periodic_rate(r)
  cashflows = cashflow_values(cf)

  cashflows.each_with_index.sum do |cashflow, index|
    fv_simple(r: rate, n: cashflows.size - index - 1, pv: cashflow)
  end
end

.n_period(r:, pv:, fv:, pmt:, type: 0) ⇒ decimal

Parameters:

  • r: (number)
  • pv: (number)
  • fv: (number)
  • pmt: (number)
  • type: (Integer) (defaults to: 0)

Returns:

  • (decimal)


66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/finrb/tvm.rb', line 66

def n_period(r:, pv:, fv:, pmt:, type: 0)
  rate = periodic_rate(r)
  values = decimal_inputs(pv:, fv:, pmt:)
  payment_timing = payment_type(type)

  return zero_rate_periods(**values) if rate.zero?

  numerator = ((values[:fv] * rate) - (values[:pmt] * ((rate + 1)**payment_timing))) * -1
  denominator = (values[:pv] * rate) + (values[:pmt] * ((rate + 1)**payment_timing))
  periods = (numerator / denominator).log / (rate + 1).log
  raise(DomainError, 'Inputs do not produce a finite non-negative period count.') unless periods.finite? && !periods.negative?

  periods
rescue Flt::Num::Exception, Math::DomainError, ZeroDivisionError => e
  raise(DomainError, "Inputs do not produce a real period count: #{e.message}", e.backtrace)
end

.npv(r:, cf:) ⇒ decimal

Parameters:

  • r: (number)
  • cf: (number, numbers)

Returns:

  • (decimal)


83
84
85
86
87
88
89
# File 'lib/finrb/tvm.rb', line 83

def npv(r:, cf:)
  rate = periodic_rate(r)
  cashflows = cashflow_values(cf)
  return cashflows.first if cashflows.one?

  (pv_uneven(r: rate, cf: cashflows.drop(1)) * -1) + cashflows.first
end

.pmt(r:, n:, pv:, fv:, type: 0) ⇒ decimal

Parameters:

  • r: (number)
  • n: (number)
  • pv: (number)
  • fv: (number)
  • type: (Integer) (defaults to: 0)

Returns:

  • (decimal)


91
92
93
94
95
96
97
98
99
# File 'lib/finrb/tvm.rb', line 91

def pmt(r:, n:, pv:, fv:, type: 0)
  rate = periodic_rate(r)
  periods = positive_period_count(n)
  values = decimal_inputs(pv:, fv:)
  payment_timing = payment_type(type)
  return -(values[:pv] + values[:fv]) / periods if rate.zero?

  (values[:pv] + (values[:fv] / ((rate + 1)**periods))) * rate / (1 - (Flt::DecNum(1) / ((rate + 1)**periods))) * -1 * ((rate + 1)**(payment_timing * -1))
end

.pv(r:, n:, fv: 0, pmt: 0, type: 0) ⇒ decimal

Parameters:

  • r: (number)
  • n: (number)
  • fv: (number) (defaults to: 0)
  • pmt: (number) (defaults to: 0)
  • type: (Integer) (defaults to: 0)

Returns:

  • (decimal)


101
102
103
104
105
106
107
# File 'lib/finrb/tvm.rb', line 101

def pv(r:, n:, fv: 0, pmt: 0, type: 0)
  rate = periodic_rate(r)
  periods = period_count(n)
  payment_type(type)

  pv_simple(r: rate, n: periods, fv:) + pv_annuity(r: rate, n: periods, pmt:, type:)
end

.pv_annuity(r:, n:, pmt:, type: 0) ⇒ decimal

Parameters:

  • r: (number)
  • n: (number)
  • pmt: (number)
  • type: (Integer) (defaults to: 0)

Returns:

  • (decimal)


109
110
111
112
113
114
115
116
117
# File 'lib/finrb/tvm.rb', line 109

def pv_annuity(r:, n:, pmt:, type: 0)
  rate = periodic_rate(r)
  periods = period_count(n)
  payment = Validation.decimal(pmt, name: 'payment')
  payment_timing = payment_type(type)
  return -payment * periods if rate.zero?

  (payment / rate * (1 - (Flt::DecNum(1) / ((rate + 1)**periods)))) * ((rate + 1)**payment_timing) * -1
end

.pv_perpetuity(r:, pmt:, g: 0, type: 0) ⇒ decimal

Parameters:

  • r: (number)
  • pmt: (number)
  • g: (number) (defaults to: 0)
  • type: (Integer) (defaults to: 0)

Returns:

  • (decimal)

Raises:



119
120
121
122
123
124
125
126
127
# File 'lib/finrb/tvm.rb', line 119

def pv_perpetuity(r:, pmt:, g: 0, type: 0)
  rate = periodic_rate(r)
  payment = Validation.decimal(pmt, name: 'payment')
  growth = periodic_rate(g, name: :g)
  payment_timing = payment_type(type)
  raise(DomainError, 'Growth rate must be smaller than the discount rate.') if growth >= rate

  (payment / (rate - growth)) * ((rate + 1)**payment_timing) * -1
end

.pv_simple(r:, n:, fv:) ⇒ decimal

Parameters:

  • r: (number)
  • n: (number)
  • fv: (number)

Returns:

  • (decimal)


129
130
131
132
133
134
# File 'lib/finrb/tvm.rb', line 129

def pv_simple(r:, n:, fv:)
  rate = periodic_rate(r)
  periods = period_count(n)
  future_value = Validation.decimal(fv, name: 'future value')
  (future_value / ((rate + 1)**periods)) * -1
end

.pv_uneven(r:, cf:) ⇒ decimal

Parameters:

  • r: (number)
  • cf: (number, numbers)

Returns:

  • (decimal)


136
137
138
139
140
141
# File 'lib/finrb/tvm.rb', line 136

def pv_uneven(r:, cf:)
  rate = periodic_rate(r)
  cashflow_values(cf).each_with_index.sum do |cashflow, index|
    pv_simple(r: rate, n: index + 1, fv: cashflow)
  end
end

.r_perpetuity(pmt:, pv:) ⇒ decimal

Parameters:

  • pmt: (number)
  • pv: (number)

Returns:

  • (decimal)

Raises:



143
144
145
146
147
148
149
# File 'lib/finrb/tvm.rb', line 143

def r_perpetuity(pmt:, pv:)
  payment = Validation.decimal(pmt, name: 'payment')
  present_value = Validation.decimal(pv, name: 'present value')
  raise(DomainError, 'Present value must be non-zero.') if present_value.zero?

  payment * -1 / present_value
end