finrb 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/finrb/tvm.rb CHANGED
@@ -4,123 +4,225 @@ require_relative 'config'
4
4
  require_relative 'decimal'
5
5
  require_relative 'errors'
6
6
  require_relative 'numerical/brent'
7
+ require_relative 'numerical/rate_search'
8
+ require_relative 'validation'
7
9
 
8
10
  module Finrb
9
11
  # Time-value-of-money calculations for periodic rates and cashflows.
10
12
  module TVM
11
13
  module_function
12
14
 
13
- def discount_rate(n:, pv:, fv:, pmt:, type: 0, lower: 0.0001, upper: 100)
14
- n, pv, fv, pmt, type, lower, upper = decimals(n, pv, fv, pmt, type, lower, upper)
15
+ UNSET_BOUND = Object.new.freeze
16
+ private_constant :UNSET_BOUND
17
+ INPUT_NAMES = { fv: 'future value', g: 'growth rate', guess: 'rate guess', lower: 'lower rate bound', n: 'period count', pmt: 'payment', pv: 'present value', r: 'periodic rate', upper: 'upper rate bound' }.freeze
18
+ private_constant :INPUT_NAMES
19
+
20
+ def discount_rate(n:, pv:, fv:, pmt:, type: 0, guess: nil, lower: UNSET_BOUND, upper: UNSET_BOUND)
21
+ n = period_count(n)
22
+ pv, fv, pmt = decimal_inputs(pv:, fv:, pmt:).values
23
+ type = payment_type(type)
15
24
  function = ->(rate) { fv_simple(r: rate, n:, pv:) + fv_annuity(r: rate, n:, pmt:, type:) - fv }
16
25
 
17
- Numerical::Brent.new(tolerance: Finrb.config.eps).solve(function, lower:, upper:)
26
+ bounds = rate_bounds(function, guess:, lower:, upper:)
27
+ return bounds.first if bounds.first == bounds.last
28
+
29
+ Numerical::Brent.new(tolerance: Finrb.config.eps).solve(function, lower: bounds.first, upper: bounds.last)
18
30
  end
19
31
 
20
32
  def fv(r:, n:, pv: 0, pmt: 0, type: 0)
21
- r, n, pv, pmt, type = decimals(r, n, pv, pmt, type)
22
- validate_payment_type!(type)
33
+ rate = periodic_rate(r)
34
+ periods = period_count(n)
35
+ payment_type(type)
23
36
 
24
- fv_simple(r:, n:, pv:) + fv_annuity(r:, n:, pmt:, type:)
37
+ fv_simple(r: rate, n: periods, pv:) + fv_annuity(r: rate, n: periods, pmt:, type:)
25
38
  end
26
39
 
27
40
  def fv_annuity(r:, n:, pmt:, type: 0)
28
- r, n, pmt, type = decimals(r, n, pmt, type)
29
- validate_payment_type!(type)
41
+ rate = periodic_rate(r)
42
+ periods = period_count(n)
43
+ payment = Validation.decimal(pmt, name: 'payment')
44
+ payment_timing = payment_type(type)
45
+ return -payment * periods if rate.zero?
30
46
 
31
- (pmt / r * (((r + 1)**n) - 1)) * ((r + 1)**type) * -1
47
+ (payment / rate * (((rate + 1)**periods) - 1)) * ((rate + 1)**payment_timing) * -1
32
48
  end
33
49
 
34
50
  def fv_simple(r:, n:, pv:)
35
- r, n, pv = decimals(r, n, pv)
36
- (pv * ((r + 1)**n)) * -1
51
+ rate = periodic_rate(r)
52
+ periods = period_count(n)
53
+ present_value = Validation.decimal(pv, name: 'present value')
54
+ (present_value * ((rate + 1)**periods)) * -1
37
55
  end
38
56
 
39
57
  def fv_uneven(r:, cf:)
40
- r = Flt::DecNum(r.to_s)
41
- cashflows = array(cf).map { |value| Flt::DecNum(value.to_s) }
58
+ rate = periodic_rate(r)
59
+ cashflows = cashflow_values(cf)
42
60
 
43
61
  cashflows.each_with_index.sum do |cashflow, index|
44
- fv_simple(r:, n: cashflows.size - index - 1, pv: cashflow)
62
+ fv_simple(r: rate, n: cashflows.size - index - 1, pv: cashflow)
45
63
  end
46
64
  end
47
65
 
48
66
  def n_period(r:, pv:, fv:, pmt:, type: 0)
49
- r, pv, fv, pmt, type = decimals(r, pv, fv, pmt, type)
50
- validate_payment_type!(type)
67
+ rate = periodic_rate(r)
68
+ values = decimal_inputs(pv:, fv:, pmt:)
69
+ payment_timing = payment_type(type)
70
+
71
+ return zero_rate_periods(**values) if rate.zero?
72
+
73
+ numerator = ((values[:fv] * rate) - (values[:pmt] * ((rate + 1)**payment_timing))) * -1
74
+ denominator = (values[:pv] * rate) + (values[:pmt] * ((rate + 1)**payment_timing))
75
+ periods = (numerator / denominator).log / (rate + 1).log
76
+ raise(DomainError, 'Inputs do not produce a finite non-negative period count.') unless periods.finite? && !periods.negative?
51
77
 
52
- numerator = ((fv * r) - (pmt * ((r + 1)**type))) * -1
53
- denominator = (pv * r) + (pmt * ((r + 1)**type))
54
- (numerator / denominator).log / (r + 1).log
78
+ periods
79
+ rescue Flt::Num::Exception, Math::DomainError, ZeroDivisionError => e
80
+ raise(DomainError, "Inputs do not produce a real period count: #{e.message}", e.backtrace)
55
81
  end
56
82
 
57
83
  def npv(r:, cf:)
58
- cashflows = array(cf).map { |value| Flt::DecNum(value.to_s) }
59
- (pv_uneven(r:, cf: cashflows.drop(1)) * -1) + cashflows.first
84
+ rate = periodic_rate(r)
85
+ cashflows = cashflow_values(cf)
86
+ return cashflows.first if cashflows.one?
87
+
88
+ (pv_uneven(r: rate, cf: cashflows.drop(1)) * -1) + cashflows.first
60
89
  end
61
90
 
62
91
  def pmt(r:, n:, pv:, fv:, type: 0)
63
- r, n, pv, fv, type = decimals(r, n, pv, fv, type)
64
- validate_payment_type!(type)
92
+ rate = periodic_rate(r)
93
+ periods = positive_period_count(n)
94
+ values = decimal_inputs(pv:, fv:)
95
+ payment_timing = payment_type(type)
96
+ return -(values[:pv] + values[:fv]) / periods if rate.zero?
65
97
 
66
- (pv + (fv / ((r + 1)**n))) * r / (1 - (Flt::DecNum(1) / ((r + 1)**n))) * -1 * ((r + 1)**(type * -1))
98
+ (values[:pv] + (values[:fv] / ((rate + 1)**periods))) * rate / (1 - (Flt::DecNum(1) / ((rate + 1)**periods))) * -1 * ((rate + 1)**(payment_timing * -1))
67
99
  end
68
100
 
69
101
  def pv(r:, n:, fv: 0, pmt: 0, type: 0)
70
- r, n, fv, pmt, type = decimals(r, n, fv, pmt, type)
71
- validate_payment_type!(type)
102
+ rate = periodic_rate(r)
103
+ periods = period_count(n)
104
+ payment_type(type)
72
105
 
73
- pv_simple(r:, n:, fv:) + pv_annuity(r:, n:, pmt:, type:)
106
+ pv_simple(r: rate, n: periods, fv:) + pv_annuity(r: rate, n: periods, pmt:, type:)
74
107
  end
75
108
 
76
109
  def pv_annuity(r:, n:, pmt:, type: 0)
77
- r, n, pmt, type = decimals(r, n, pmt, type)
78
- validate_payment_type!(type)
110
+ rate = periodic_rate(r)
111
+ periods = period_count(n)
112
+ payment = Validation.decimal(pmt, name: 'payment')
113
+ payment_timing = payment_type(type)
114
+ return -payment * periods if rate.zero?
79
115
 
80
- (pmt / r * (1 - (Flt::DecNum(1) / ((r + 1)**n)))) * ((r + 1)**type) * -1
116
+ (payment / rate * (1 - (Flt::DecNum(1) / ((rate + 1)**periods)))) * ((rate + 1)**payment_timing) * -1
81
117
  end
82
118
 
83
119
  def pv_perpetuity(r:, pmt:, g: 0, type: 0)
84
- r, pmt, g, type = decimals(r, pmt, g, type)
85
- validate_payment_type!(type)
86
- raise(Error, 'Error: g is not smaller than r!') if g >= r
120
+ rate = periodic_rate(r)
121
+ payment = Validation.decimal(pmt, name: 'payment')
122
+ growth = periodic_rate(g, name: :g)
123
+ payment_timing = payment_type(type)
124
+ raise(DomainError, 'Growth rate must be smaller than the discount rate.') if growth >= rate
87
125
 
88
- (pmt / (r - g)) * ((r + 1)**type) * -1
126
+ (payment / (rate - growth)) * ((rate + 1)**payment_timing) * -1
89
127
  end
90
128
 
91
129
  def pv_simple(r:, n:, fv:)
92
- r, n, fv = decimals(r, n, fv)
93
- (fv / ((r + 1)**n)) * -1
130
+ rate = periodic_rate(r)
131
+ periods = period_count(n)
132
+ future_value = Validation.decimal(fv, name: 'future value')
133
+ (future_value / ((rate + 1)**periods)) * -1
94
134
  end
95
135
 
96
136
  def pv_uneven(r:, cf:)
97
- r = Flt::DecNum(r.to_s)
98
- array(cf).each_with_index.sum do |cashflow, index|
99
- pv_simple(r:, n: index + 1, fv: cashflow)
137
+ rate = periodic_rate(r)
138
+ cashflow_values(cf).each_with_index.sum do |cashflow, index|
139
+ pv_simple(r: rate, n: index + 1, fv: cashflow)
100
140
  end
101
141
  end
102
142
 
103
143
  def r_perpetuity(pmt:, pv:)
104
- pmt, pv = decimals(pmt, pv)
105
- pmt * -1 / pv
144
+ payment = Validation.decimal(pmt, name: 'payment')
145
+ present_value = Validation.decimal(pv, name: 'present value')
146
+ raise(DomainError, 'Present value must be non-zero.') if present_value.zero?
147
+
148
+ payment * -1 / present_value
106
149
  end
107
150
 
108
- def array(value)
109
- return [] if value.nil?
110
- return value.to_ary || [value] if value.respond_to?(:to_ary)
151
+ def cashflow_values(value)
152
+ values =
153
+ if value.nil?
154
+ []
155
+ elsif value.respond_to?(:to_ary)
156
+ value.to_ary || [value]
157
+ else
158
+ [value]
159
+ end
160
+ raise(ArgumentError, 'cashflows cannot be empty.') if values.empty?
161
+
162
+ values.map { |cashflow| Validation.decimal(cashflow, name: 'cashflow') }
163
+ end
164
+ private_class_method :cashflow_values
165
+
166
+ def decimal_inputs(**values)
167
+ values.to_h { |name, value| [name, Validation.decimal(value, name: input_name(name))] }
168
+ end
169
+ private_class_method :decimal_inputs
170
+
171
+ def payment_type(value)
172
+ value = Validation.decimal(value, name: 'payment timing type')
173
+ raise(ArgumentError, 'payment timing type must be 0 (end) or 1 (beginning).') unless [Flt::DecNum(0), Flt::DecNum(1)].include?(value)
174
+
175
+ value
176
+ end
177
+ private_class_method :payment_type
111
178
 
112
- [value]
179
+ def period_count(value)
180
+ Validation.non_negative_decimal(value, name: 'period count', error: DomainError)
113
181
  end
114
- private_class_method :array
182
+ private_class_method :period_count
115
183
 
116
- def decimals(*values)
117
- values.map { |value| Flt::DecNum(value.to_s) }
184
+ def positive_period_count(value)
185
+ Validation.positive_decimal(value, name: 'period count', error: DomainError)
118
186
  end
119
- private_class_method :decimals
187
+ private_class_method :positive_period_count
188
+
189
+ def periodic_rate(value, name: :r)
190
+ Validation.decimal_greater_than(value, minimum: -1, name: input_name(name), error: DomainError)
191
+ end
192
+ private_class_method :periodic_rate
193
+
194
+ def input_name(name)
195
+ INPUT_NAMES.fetch(name, name.to_s.tr('_', ' '))
196
+ end
197
+ private_class_method :input_name
198
+
199
+ def rate_bounds(function, guess:, lower:, upper:)
200
+ return searched_rate_bounds(function, guess) if lower.equal?(UNSET_BOUND) && upper.equal?(UNSET_BOUND)
201
+
202
+ lower = '0.0001' if lower.equal?(UNSET_BOUND)
203
+ upper = 100 if upper.equal?(UNSET_BOUND)
204
+ lower = periodic_rate(lower, name: :lower)
205
+ upper = periodic_rate(upper, name: :upper)
206
+ raise(ArgumentError, 'lower rate bound must be less than upper rate bound.') if lower >= upper
207
+
208
+ [lower, upper]
209
+ end
210
+ private_class_method :rate_bounds
211
+
212
+ def searched_rate_bounds(function, guess)
213
+ guess = Finrb.config.guess if guess.nil?
214
+ Numerical::RateSearch.new.bracket(function, guess: periodic_rate(guess, name: :guess))
215
+ end
216
+ private_class_method :searched_rate_bounds
217
+
218
+ def zero_rate_periods(pv:, fv:, pmt:)
219
+ raise(DomainError, 'pmt must be non-zero when solving periods at a zero rate.') if pmt.zero?
220
+
221
+ periods = (-pv - fv) / pmt
222
+ raise(DomainError, 'Inputs do not produce a non-negative period count.') if periods.negative?
120
223
 
121
- def validate_payment_type!(type)
122
- raise(Error, 'Error: type should be 0 or 1!') unless [Flt::DecNum(0), Flt::DecNum(1)].include?(type)
224
+ periods
123
225
  end
124
- private_class_method :validate_payment_type!
226
+ private_class_method :zero_rate_periods
125
227
  end
126
228
  end
@@ -23,5 +23,47 @@ module Finrb
23
23
 
24
24
  value
25
25
  end
26
+
27
+ def positive_decimal(value, name:, error: ArgumentError, message: nil)
28
+ decimal = decimal(value, name:)
29
+ raise(error, message || "#{name} must be greater than zero.") unless decimal.positive?
30
+
31
+ decimal
32
+ end
33
+
34
+ def non_negative_decimal(value, name:, error: ArgumentError, message: nil)
35
+ decimal = decimal(value, name:)
36
+ raise(error, message || "#{name} must be greater than or equal to zero.") if decimal.negative?
37
+
38
+ decimal
39
+ end
40
+
41
+ def decimal_greater_than(value, minimum:, name:, error: ArgumentError)
42
+ decimal = decimal(value, name:)
43
+ raise(error, "#{name} must be greater than #{minimum}.") if decimal <= minimum
44
+
45
+ decimal
46
+ end
47
+
48
+ def decimal_at_least(value, minimum:, name:, error: ArgumentError)
49
+ decimal = decimal(value, name:)
50
+ raise(error, "#{name} must be greater than or equal to #{minimum}.") if decimal < minimum
51
+
52
+ decimal
53
+ end
54
+
55
+ def non_zero_decimal(value, name:, error: ArgumentError)
56
+ decimal = decimal(value, name:)
57
+ raise(error, "#{name} must be non-zero.") if decimal.zero?
58
+
59
+ decimal
60
+ end
61
+
62
+ def decimal_between(value, minimum:, maximum:, name:, error: ArgumentError)
63
+ decimal = decimal(value, name:)
64
+ raise(error, "#{name} must be between #{minimum} and #{maximum}, inclusive.") unless decimal.between?(minimum, maximum)
65
+
66
+ decimal
67
+ end
26
68
  end
27
69
  end
data/lib/finrb/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Finrb
4
- VERSION = '1.0.1'
4
+ VERSION = '1.2.0'
5
5
  public_constant :VERSION
6
6
  end
data/lib/finrb/yields.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'decimal'
4
4
  require_relative 'errors'
5
+ require_relative 'validation'
5
6
 
6
7
  module Finrb
7
8
  # Money-market yield and interest-rate conversion calculations.
@@ -14,9 +15,9 @@ module Finrb
14
15
  # @example
15
16
  # Finrb::Yields.bdy(d=1500,f=100000,t=120)
16
17
  def self.bdy(d:, f:, t:)
17
- d = Flt::DecNum(d.to_s)
18
- f = Flt::DecNum(f.to_s)
19
- t = Flt::DecNum(t.to_s)
18
+ d = Validation.decimal(d, name: 'dollar discount')
19
+ f = Validation.positive_decimal(f, name: 'face value', error: DomainError)
20
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
20
21
 
21
22
  (d * 360 / f / t)
22
23
  end
@@ -28,10 +29,12 @@ module Finrb
28
29
  # @example
29
30
  # Finrb::Yields.bdy2mmy(bdy=0.045,t=120)
30
31
  def self.bdy2mmy(bdy:, t:)
31
- bdy = Flt::DecNum(bdy.to_s)
32
- t = Flt::DecNum(t.to_s)
32
+ bdy = Validation.decimal(bdy, name: 'bank discount yield')
33
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
34
+ denominator = 360 - (t * bdy)
35
+ raise(DomainError, 'Bank discount yield and time to maturity must imply a positive purchase price.') unless denominator.positive?
33
36
 
34
- (bdy * 360 / (360 - (t * bdy)))
37
+ (bdy * 360 / denominator)
35
38
  end
36
39
 
37
40
  # Convert stated annual rate to the effective annual rate
@@ -44,10 +47,10 @@ module Finrb
44
47
  # @example
45
48
  # Finrb::Yields.ear(0.04,365)
46
49
  def self.ear(r:, m:)
47
- r = Flt::DecNum(r.to_s)
48
- m = Flt::DecNum(m.to_s)
50
+ r = Validation.decimal(r, name: 'stated annual rate')
51
+ m = Validation.positive_decimal(m, name: 'compounding periods', error: DomainError)
49
52
 
50
- ((((r / m) + 1)**m) - 1)
53
+ ((compounding_base(r, m)**m) - 1)
51
54
  end
52
55
 
53
56
  # Convert stated annual rate to the effective annual rate with continuous compounding
@@ -59,7 +62,7 @@ module Finrb
59
62
  # @example
60
63
  # Finrb::Yields.ear_continuous(0.03)
61
64
  def self.ear_continuous(r:)
62
- r = Flt::DecNum(r.to_s)
65
+ r = Validation.decimal(r, name: 'stated annual rate')
63
66
 
64
67
  (r.exp - 1)
65
68
  end
@@ -70,7 +73,7 @@ module Finrb
70
73
  # @example
71
74
  # Finrb::Yields.ear2bey(ear=0.08)
72
75
  def self.ear2bey(ear:)
73
- ear = Flt::DecNum(ear.to_s)
76
+ ear = Validation.decimal_at_least(ear, minimum: -1, name: 'effective annual rate', error: DomainError)
74
77
 
75
78
  (((ear + 1).sqrt - 1) * 2)
76
79
  end
@@ -82,8 +85,8 @@ module Finrb
82
85
  # @example
83
86
  # Finrb::Yields.ear2hpr(ear=0.05039,t=150)
84
87
  def self.ear2hpr(ear:, t:)
85
- ear = Flt::DecNum(ear.to_s)
86
- t = Flt::DecNum(t.to_s)
88
+ ear = Validation.decimal_at_least(ear, minimum: -1, name: 'effective annual rate', error: DomainError)
89
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
87
90
 
88
91
  (((ear + 1)**(t / 365)) - 1)
89
92
  end
@@ -124,18 +127,18 @@ module Finrb
124
127
  # # monthly proportional interest rate which is equivalent to a simple annual interest
125
128
  # Finrb::Yields.eir(r=0.05,p=12,type='p')
126
129
  def self.eir(r:, n: 1, p: 12, type: 'e')
127
- r = Flt::DecNum(r.to_s)
128
- n = Flt::DecNum(n.to_s)
129
- p = Flt::DecNum(p.to_s)
130
+ r = Validation.decimal(r, name: 'annual rate')
131
+ n = Validation.positive_decimal(n, name: 'source compounding periods', error: DomainError)
132
+ p = Validation.positive_decimal(p, name: 'target compounding periods', error: DomainError)
130
133
  type = type.to_s
131
134
 
132
135
  case type
133
136
  when 'e'
134
- eir = (((r / n) + 1)**(n / p)) - 1
137
+ eir = (compounding_base(r, n)**(n / p)) - 1
135
138
  when 'p'
136
139
  eir = r / p
137
140
  else
138
- raise(Error, "type must be 'e' or 'p'")
141
+ raise(ArgumentError, "conversion type must be 'e' (equivalent) or 'p' (proportional)")
139
142
  end
140
143
  eir
141
144
  end
@@ -147,8 +150,8 @@ module Finrb
147
150
  # @example
148
151
  # Finrb::Yields.hpr2bey(hpr=0.02,t=3)
149
152
  def self.hpr2bey(hpr:, t:)
150
- hpr = Flt::DecNum(hpr.to_s)
151
- t = Flt::DecNum(t.to_s)
153
+ hpr = Validation.decimal_at_least(hpr, minimum: -1, name: 'holding period return', error: DomainError)
154
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
152
155
 
153
156
  ((((hpr + 1)**(6 / t)) - 1) * 2)
154
157
  end
@@ -160,8 +163,8 @@ module Finrb
160
163
  # @example
161
164
  # Finrb::Yields.hpr2ear(hpr=0.015228,t=120)
162
165
  def self.hpr2ear(hpr:, t:)
163
- hpr = Flt::DecNum(hpr.to_s)
164
- t = Flt::DecNum(t.to_s)
166
+ hpr = Validation.decimal_at_least(hpr, minimum: -1, name: 'holding period return', error: DomainError)
167
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
165
168
 
166
169
  (((hpr + 1)**(365 / t)) - 1)
167
170
  end
@@ -173,8 +176,8 @@ module Finrb
173
176
  # @example
174
177
  # Finrb::Yields.hpr2mmy(hpr=0.01523,t=120)
175
178
  def self.hpr2mmy(hpr:, t:)
176
- hpr = Flt::DecNum(hpr.to_s)
177
- t = Flt::DecNum(t.to_s)
179
+ hpr = Validation.decimal(hpr, name: 'holding period return')
180
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
178
181
 
179
182
  (hpr * 360 / t)
180
183
  end
@@ -186,8 +189,8 @@ module Finrb
186
189
  # @example
187
190
  # Finrb::Yields.mmy2hpr(mmy=0.04898,t=150)
188
191
  def self.mmy2hpr(mmy:, t:)
189
- mmy = Flt::DecNum(mmy.to_s)
190
- t = Flt::DecNum(t.to_s)
192
+ mmy = Validation.decimal(mmy, name: 'money market yield')
193
+ t = Validation.positive_decimal(t, name: 'time to maturity', error: DomainError)
191
194
 
192
195
  (mmy * t / 360)
193
196
  end
@@ -199,10 +202,10 @@ module Finrb
199
202
  # @example
200
203
  # Finrb::Yields.r_continuous(r=0.03,m=4)
201
204
  def self.r_continuous(r:, m:)
202
- r = Flt::DecNum(r.to_s)
203
- m = Flt::DecNum(m.to_s)
205
+ r = Validation.decimal(r, name: 'nominal rate')
206
+ m = Validation.positive_decimal(m, name: 'compounding periods', error: DomainError)
204
207
 
205
- (m * ((r / m) + 1).log)
208
+ (m * compounding_base(r, m).log)
206
209
  end
207
210
 
208
211
  # Convert a given continuous compounded rate to a norminal rate
@@ -215,10 +218,18 @@ module Finrb
215
218
  # @example
216
219
  # Finrb::Yields.r_norminal(rc=0.03,m=4)
217
220
  def self.r_norminal(rc:, m:)
218
- rc = Flt::DecNum(rc.to_s)
219
- m = Flt::DecNum(m.to_s)
221
+ rc = Validation.decimal(rc, name: 'continuously compounded rate')
222
+ m = Validation.positive_decimal(m, name: 'compounding periods', error: DomainError)
220
223
 
221
224
  (m * ((rc / m).exp - 1))
222
225
  end
226
+
227
+ def self.compounding_base(rate, periods)
228
+ base = (rate / periods) + 1
229
+ raise(DomainError, 'The rate per compounding period must be greater than -1.') unless base.positive?
230
+
231
+ base
232
+ end
233
+ private_class_method :compounding_base
223
234
  end
224
235
  end
data/sig/finrb.rbs CHANGED
@@ -94,14 +94,40 @@ module Finrb
94
94
  end
95
95
 
96
96
  class Amortization
97
- def self.payment: (number, number, Integer) -> decimal
97
+ class Entry
98
+ def initialize: (period: Integer, opening_balance: number, payment: number, interest: number, principal: number, additional_payment: number, balloon_payment: number, interest_only: bool, closing_balance: number) -> void
99
+ def period: () -> Integer
100
+ def opening_balance: () -> decimal
101
+ def payment: () -> decimal
102
+ def interest: () -> decimal
103
+ def principal: () -> decimal
104
+ def additional_payment: () -> decimal
105
+ def balloon_payment: () -> decimal
106
+ def interest_only: () -> bool
107
+ def interest_only?: () -> bool
108
+ def closing_balance: () -> decimal
109
+ def ==: (untyped) -> bool
110
+ def eql?: (untyped) -> bool
111
+ def hash: () -> Integer
112
+ def to_h: () -> Hash[Symbol, Integer | decimal]
113
+ end
114
+
115
+ def self.payment: (number, number, Integer, ?balloon: number) -> decimal
98
116
 
99
- def initialize: (number, *Rate) ?{ (Transaction) -> number } -> void
117
+ def initialize: (number, *Rate, ?balloon: number, ?interest_only_periods: Integer, ?origination_fee: number, ?finance_origination_fee: bool) ?{ (Transaction) -> number } -> void
100
118
  def ==: (untyped) -> bool
101
119
  def balance: () -> decimal
120
+ def balloon: () -> decimal
121
+ def amount_financed: () -> decimal
122
+ def net_proceeds: () -> decimal
123
+ def origination_fee: () -> decimal
124
+ def finance_origination_fee: () -> bool
125
+ def finance_origination_fee?: () -> bool
126
+ def interest_only_periods: () -> Integer
102
127
  def payment: () -> decimal?
103
128
  def principal: () -> decimal
104
129
  def rates: () -> Array[Rate]
130
+ def schedule: () -> Array[Entry]
105
131
  def additional_payments: () -> Array[decimal]
106
132
  def duration: () -> Integer
107
133
  def inspect: () -> String
@@ -111,18 +137,20 @@ module Finrb
111
137
 
112
138
  module Cashflow
113
139
  def self.irr: (Enumerable[number], ?number) -> decimal
140
+ def self.mirr: (Enumerable[number], finance_rate: number, reinvestment_rate: number) -> decimal
114
141
  def self.npv: (Enumerable[number], number) -> decimal
115
142
  def self.xirr: (Enumerable[Transaction], ?number) -> Rate
116
143
  def self.xnpv: (Enumerable[Transaction], number) -> decimal
117
144
 
118
145
  def irr: (?number) -> decimal
146
+ def mirr: (finance_rate: number, reinvestment_rate: number) -> decimal
119
147
  def npv: (number) -> decimal
120
148
  def xirr: (?number) -> Rate
121
149
  def xnpv: (number) -> decimal
122
150
  end
123
151
 
124
152
  module TVM
125
- def self.discount_rate: (n: number, pv: number, fv: number, pmt: number, ?type: Integer, ?lower: number, ?upper: number) -> decimal
153
+ def self.discount_rate: (n: number, pv: number, fv: number, pmt: number, ?type: Integer, ?guess: number?, ?lower: number?, ?upper: number?) -> decimal
126
154
  def self.fv: (r: number, n: number, ?pv: number, ?pmt: number, ?type: Integer) -> decimal
127
155
  def self.fv_annuity: (r: number, n: number, pmt: number, ?type: Integer) -> decimal
128
156
  def self.fv_simple: (r: number, n: number, pv: number) -> decimal
@@ -141,7 +169,7 @@ module Finrb
141
169
  module Accounting
142
170
  type inventory_result = { cost_of_goods: decimal, ending_inventory: decimal }
143
171
 
144
- def self.cogs: (uinv: number, pinv: number, units: number | numbers, price: number | numbers, sinv: number, ?method: String) -> inventory_result
172
+ def self.cogs: (uinv: number, pinv: number, units: number | numbers | nil, price: number | numbers | nil, sinv: number, ?method: String | Symbol) -> inventory_result
145
173
  def self.ddb: (cost: number, rv: number, t: Integer) -> { t: Array[Integer], ddb: Array[decimal] }
146
174
  def self.slde: (cost: number, rv: number, t: number) -> decimal
147
175
  end
@@ -163,15 +191,22 @@ module Finrb
163
191
  end
164
192
 
165
193
  module Returns
194
+ def self.annualize_return: (rate: number, periods_per_year: Integer) -> decimal
195
+ def self.annualize_volatility: (volatility: number, periods_per_year: Integer) -> decimal
196
+ def self.cagr: (beginning_value: number, ending_value: number, periods: Integer) -> decimal
166
197
  def self.coefficient_variation: (sd: number, avg: number) -> decimal
167
198
  def self.geometric_mean: (r: number | numbers) -> decimal
168
199
  def self.harmonic_mean: (p: number | numbers) -> decimal
169
200
  def self.hpr: (ev: number, bv: number, ?cfr: number) -> decimal
201
+ def self.downside_deviation: (returns: number | numbers, ?target: number) -> decimal
202
+ def self.max_drawdown: (values: number | numbers) -> decimal
170
203
  def self.sampling_error: (sm: number, mu: number) -> decimal
171
204
  def self.sf_ratio: (rp: number, rl: number, sd: number) -> decimal
172
205
  def self.sharpe_ratio: (rp: number, rf: number, sd: number) -> decimal
206
+ def self.sortino_ratio: (returns: number | numbers, ?target: number, ?periods_per_year: Integer?) -> decimal
173
207
  def self.twrr: (ev: number | numbers, bv: number | numbers, cfr: number | numbers) -> decimal
174
208
  def self.wpr: (r: number | numbers, w: number | numbers) -> decimal
209
+ def self.volatility: (returns: number | numbers, ?sample: bool) -> decimal
175
210
  end
176
211
 
177
212
  module Yields
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: finrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nadir Cohen
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: benchmark-ips
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: bundler-audit
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -248,10 +262,12 @@ extra_rdoc_files:
248
262
  - README.md
249
263
  files:
250
264
  - CHANGELOG.md
265
+ - CONTRIBUTING.md
251
266
  - COPYING
252
267
  - COPYING.LESSER
253
268
  - NOTICE.md
254
269
  - README.md
270
+ - SECURITY.md
255
271
  - lib/finrb.rb
256
272
  - lib/finrb/accounting.rb
257
273
  - lib/finrb/amortization.rb