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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/CONTRIBUTING.md +89 -0
- data/README.md +58 -2
- data/SECURITY.md +41 -0
- data/lib/finrb/accounting.rb +93 -74
- data/lib/finrb/amortization.rb +145 -44
- data/lib/finrb/cashflows.rb +32 -9
- data/lib/finrb/config.rb +5 -5
- data/lib/finrb/rates.rb +3 -21
- data/lib/finrb/ratios.rb +41 -40
- data/lib/finrb/returns.rb +138 -39
- data/lib/finrb/transaction.rb +0 -11
- data/lib/finrb/tvm.rb +154 -52
- data/lib/finrb/validation.rb +42 -0
- data/lib/finrb/version.rb +1 -1
- data/lib/finrb/yields.rb +42 -31
- data/sig/finrb.rbs +39 -4
- metadata +17 -1
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
33
|
+
rate = periodic_rate(r)
|
|
34
|
+
periods = period_count(n)
|
|
35
|
+
payment_type(type)
|
|
23
36
|
|
|
24
|
-
fv_simple(r
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
cashflows =
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
(
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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 / ((
|
|
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
|
-
|
|
71
|
-
|
|
102
|
+
rate = periodic_rate(r)
|
|
103
|
+
periods = period_count(n)
|
|
104
|
+
payment_type(type)
|
|
72
105
|
|
|
73
|
-
pv_simple(r
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
(
|
|
126
|
+
(payment / (rate - growth)) * ((rate + 1)**payment_timing) * -1
|
|
89
127
|
end
|
|
90
128
|
|
|
91
129
|
def pv_simple(r:, n:, fv:)
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
pv_simple(r
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
179
|
+
def period_count(value)
|
|
180
|
+
Validation.non_negative_decimal(value, name: 'period count', error: DomainError)
|
|
113
181
|
end
|
|
114
|
-
private_class_method :
|
|
182
|
+
private_class_method :period_count
|
|
115
183
|
|
|
116
|
-
def
|
|
117
|
-
|
|
184
|
+
def positive_period_count(value)
|
|
185
|
+
Validation.positive_decimal(value, name: 'period count', error: DomainError)
|
|
118
186
|
end
|
|
119
|
-
private_class_method :
|
|
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
|
-
|
|
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 :
|
|
226
|
+
private_class_method :zero_rate_periods
|
|
125
227
|
end
|
|
126
228
|
end
|
data/lib/finrb/validation.rb
CHANGED
|
@@ -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
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 =
|
|
18
|
-
f =
|
|
19
|
-
t =
|
|
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 =
|
|
32
|
-
t =
|
|
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 /
|
|
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 =
|
|
48
|
-
m =
|
|
50
|
+
r = Validation.decimal(r, name: 'stated annual rate')
|
|
51
|
+
m = Validation.positive_decimal(m, name: 'compounding periods', error: DomainError)
|
|
49
52
|
|
|
50
|
-
(((
|
|
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 =
|
|
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 =
|
|
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 =
|
|
86
|
-
t =
|
|
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 =
|
|
128
|
-
n =
|
|
129
|
-
p =
|
|
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 = ((
|
|
137
|
+
eir = (compounding_base(r, n)**(n / p)) - 1
|
|
135
138
|
when 'p'
|
|
136
139
|
eir = r / p
|
|
137
140
|
else
|
|
138
|
-
raise(
|
|
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 =
|
|
151
|
-
t =
|
|
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 =
|
|
164
|
-
t =
|
|
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 =
|
|
177
|
-
t =
|
|
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 =
|
|
190
|
-
t =
|
|
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 =
|
|
203
|
-
m =
|
|
205
|
+
r = Validation.decimal(r, name: 'nominal rate')
|
|
206
|
+
m = Validation.positive_decimal(m, name: 'compounding periods', error: DomainError)
|
|
204
207
|
|
|
205
|
-
(m * (
|
|
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 =
|
|
219
|
-
m =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|