finrb 0.1.12 → 1.0.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 +24 -0
- data/NOTICE.md +20 -0
- data/README.md +172 -84
- data/lib/finrb/accounting.rb +180 -0
- data/lib/finrb/amortization.rb +28 -23
- data/lib/finrb/cashflows.rb +101 -66
- data/lib/finrb/config.rb +53 -4
- data/lib/finrb/core_ext/array.rb +6 -0
- data/lib/finrb/core_ext/numeric.rb +12 -0
- data/lib/finrb/core_ext.rb +11 -0
- data/lib/finrb/decimal.rb +0 -10
- data/lib/finrb/errors.rb +8 -0
- data/lib/finrb/numerical/brent.rb +135 -0
- data/lib/finrb/numerical/rate_search.rb +79 -0
- data/lib/finrb/precision.rb +29 -0
- data/lib/finrb/rates.rb +46 -18
- data/lib/finrb/ratios.rb +243 -0
- data/lib/finrb/returns.rb +155 -0
- data/lib/finrb/transaction.rb +23 -7
- data/lib/finrb/tvm.rb +126 -0
- data/lib/finrb/validation.rb +27 -0
- data/lib/finrb/version.rb +6 -0
- data/lib/finrb/yields.rb +224 -0
- data/lib/finrb.rb +9 -4
- data/sig/finrb.rbs +204 -0
- metadata +49 -29
- data/lib/finrb/utils.rb +0 -1094
data/lib/finrb/transaction.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'validation'
|
|
4
4
|
|
|
5
5
|
module Finrb
|
|
6
6
|
# the Transaction class provides a general interface for working with individual cash flows.
|
|
@@ -12,10 +12,10 @@ module Finrb
|
|
|
12
12
|
# @return [Integer] the period number of the transaction
|
|
13
13
|
# @note this attribute is mainly used in the case of mortgage amortization with no dates
|
|
14
14
|
# @api public
|
|
15
|
-
|
|
15
|
+
attr_reader :period
|
|
16
16
|
# @return [Date] the date of the transaction
|
|
17
17
|
# @api public
|
|
18
|
-
|
|
18
|
+
attr_reader :date
|
|
19
19
|
|
|
20
20
|
# create a new Transaction
|
|
21
21
|
# @return [Transaction]
|
|
@@ -28,8 +28,11 @@ module Finrb
|
|
|
28
28
|
# t = Transaction.new(400, :period => 3)
|
|
29
29
|
# @api public
|
|
30
30
|
def initialize(amount, opts = {})
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
raise(ArgumentError, 'options must be a Hash.') unless opts.is_a?(Hash)
|
|
32
|
+
raise(ArgumentError, 'options may only contain date and period.') unless (opts.keys - %i[date period]).empty?
|
|
33
|
+
|
|
34
|
+
self.amount = amount
|
|
35
|
+
@original = @amount
|
|
33
36
|
|
|
34
37
|
# Set optional attributes..
|
|
35
38
|
opts.each do |key, value|
|
|
@@ -46,7 +49,20 @@ module Finrb
|
|
|
46
49
|
# t.amount #=> 750
|
|
47
50
|
# @api public
|
|
48
51
|
def amount=(value)
|
|
49
|
-
@amount =
|
|
52
|
+
@amount = Validation.decimal(value, name: 'amount')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def date=(value)
|
|
56
|
+
raise(ArgumentError, 'date must respond to to_date.') unless value.nil? || value.respond_to?(:to_date)
|
|
57
|
+
|
|
58
|
+
@date = value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def period=(value)
|
|
62
|
+
valid = value.nil? || (value.is_a?(Integer) && !value.negative?)
|
|
63
|
+
raise(ArgumentError, 'period must be a non-negative integer.') unless valid
|
|
64
|
+
|
|
65
|
+
@period = value
|
|
50
66
|
end
|
|
51
67
|
|
|
52
68
|
# @return [Flt::DecNum] the difference between the original transaction
|
|
@@ -85,7 +101,7 @@ module Finrb
|
|
|
85
101
|
# pmt.amount #=> -600
|
|
86
102
|
# @api public
|
|
87
103
|
def modify
|
|
88
|
-
|
|
104
|
+
self.amount = yield(self)
|
|
89
105
|
end
|
|
90
106
|
|
|
91
107
|
# (see #amount)
|
data/lib/finrb/tvm.rb
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'config'
|
|
4
|
+
require_relative 'decimal'
|
|
5
|
+
require_relative 'errors'
|
|
6
|
+
require_relative 'numerical/brent'
|
|
7
|
+
|
|
8
|
+
module Finrb
|
|
9
|
+
# Time-value-of-money calculations for periodic rates and cashflows.
|
|
10
|
+
module TVM
|
|
11
|
+
module_function
|
|
12
|
+
|
|
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
|
+
function = ->(rate) { fv_simple(r: rate, n:, pv:) + fv_annuity(r: rate, n:, pmt:, type:) - fv }
|
|
16
|
+
|
|
17
|
+
Numerical::Brent.new(tolerance: Finrb.config.eps).solve(function, lower:, upper:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
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)
|
|
23
|
+
|
|
24
|
+
fv_simple(r:, n:, pv:) + fv_annuity(r:, n:, pmt:, type:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def fv_annuity(r:, n:, pmt:, type: 0)
|
|
28
|
+
r, n, pmt, type = decimals(r, n, pmt, type)
|
|
29
|
+
validate_payment_type!(type)
|
|
30
|
+
|
|
31
|
+
(pmt / r * (((r + 1)**n) - 1)) * ((r + 1)**type) * -1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fv_simple(r:, n:, pv:)
|
|
35
|
+
r, n, pv = decimals(r, n, pv)
|
|
36
|
+
(pv * ((r + 1)**n)) * -1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fv_uneven(r:, cf:)
|
|
40
|
+
r = Flt::DecNum(r.to_s)
|
|
41
|
+
cashflows = array(cf).map { |value| Flt::DecNum(value.to_s) }
|
|
42
|
+
|
|
43
|
+
cashflows.each_with_index.sum do |cashflow, index|
|
|
44
|
+
fv_simple(r:, n: cashflows.size - index - 1, pv: cashflow)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
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)
|
|
51
|
+
|
|
52
|
+
numerator = ((fv * r) - (pmt * ((r + 1)**type))) * -1
|
|
53
|
+
denominator = (pv * r) + (pmt * ((r + 1)**type))
|
|
54
|
+
(numerator / denominator).log / (r + 1).log
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
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
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
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)
|
|
65
|
+
|
|
66
|
+
(pv + (fv / ((r + 1)**n))) * r / (1 - (Flt::DecNum(1) / ((r + 1)**n))) * -1 * ((r + 1)**(type * -1))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
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)
|
|
72
|
+
|
|
73
|
+
pv_simple(r:, n:, fv:) + pv_annuity(r:, n:, pmt:, type:)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def pv_annuity(r:, n:, pmt:, type: 0)
|
|
77
|
+
r, n, pmt, type = decimals(r, n, pmt, type)
|
|
78
|
+
validate_payment_type!(type)
|
|
79
|
+
|
|
80
|
+
(pmt / r * (1 - (Flt::DecNum(1) / ((r + 1)**n)))) * ((r + 1)**type) * -1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
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
|
|
87
|
+
|
|
88
|
+
(pmt / (r - g)) * ((r + 1)**type) * -1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def pv_simple(r:, n:, fv:)
|
|
92
|
+
r, n, fv = decimals(r, n, fv)
|
|
93
|
+
(fv / ((r + 1)**n)) * -1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
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)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def r_perpetuity(pmt:, pv:)
|
|
104
|
+
pmt, pv = decimals(pmt, pv)
|
|
105
|
+
pmt * -1 / pv
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def array(value)
|
|
109
|
+
return [] if value.nil?
|
|
110
|
+
return value.to_ary || [value] if value.respond_to?(:to_ary)
|
|
111
|
+
|
|
112
|
+
[value]
|
|
113
|
+
end
|
|
114
|
+
private_class_method :array
|
|
115
|
+
|
|
116
|
+
def decimals(*values)
|
|
117
|
+
values.map { |value| Flt::DecNum(value.to_s) }
|
|
118
|
+
end
|
|
119
|
+
private_class_method :decimals
|
|
120
|
+
|
|
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)
|
|
123
|
+
end
|
|
124
|
+
private_class_method :validate_payment_type!
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'decimal'
|
|
4
|
+
|
|
5
|
+
module Finrb
|
|
6
|
+
# Shared validation and decimal coercion for public financial inputs.
|
|
7
|
+
module Validation
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def decimal(value, name:)
|
|
11
|
+
raise(ArgumentError, "#{name} must be numeric.") unless value.is_a?(Numeric)
|
|
12
|
+
|
|
13
|
+
decimal = value.is_a?(Flt::DecNum) ? value : Flt::DecNum.new(value.to_s)
|
|
14
|
+
raise(ArgumentError, "#{name} must be finite.") unless decimal.finite?
|
|
15
|
+
|
|
16
|
+
decimal
|
|
17
|
+
rescue Flt::Num::Exception, FloatDomainError, Math::DomainError => e
|
|
18
|
+
raise(ArgumentError, "#{name} must be a finite numeric value.", e.backtrace)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def positive_integer(value, name:)
|
|
22
|
+
raise(ArgumentError, "#{name} must be a positive integer.") unless value.is_a?(Integer) && value.positive?
|
|
23
|
+
|
|
24
|
+
value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/finrb/yields.rb
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'decimal'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
|
|
6
|
+
module Finrb
|
|
7
|
+
# Money-market yield and interest-rate conversion calculations.
|
|
8
|
+
module Yields
|
|
9
|
+
# Computing bank discount yield (BDY) for a T-bill
|
|
10
|
+
#
|
|
11
|
+
# @param d the dollar discount, which is equal to the difference between the face value of the bill and the purchase price
|
|
12
|
+
# @param f the face value (par value) of the bill
|
|
13
|
+
# @param t number of days remaining until maturity
|
|
14
|
+
# @example
|
|
15
|
+
# Finrb::Yields.bdy(d=1500,f=100000,t=120)
|
|
16
|
+
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)
|
|
20
|
+
|
|
21
|
+
(d * 360 / f / t)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Computing money market yield (MMY) for a T-bill
|
|
25
|
+
#
|
|
26
|
+
# @param bdy bank discount yield
|
|
27
|
+
# @param t number of days remaining until maturity
|
|
28
|
+
# @example
|
|
29
|
+
# Finrb::Yields.bdy2mmy(bdy=0.045,t=120)
|
|
30
|
+
def self.bdy2mmy(bdy:, t:)
|
|
31
|
+
bdy = Flt::DecNum(bdy.to_s)
|
|
32
|
+
t = Flt::DecNum(t.to_s)
|
|
33
|
+
|
|
34
|
+
(bdy * 360 / (360 - (t * bdy)))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Convert stated annual rate to the effective annual rate
|
|
38
|
+
#
|
|
39
|
+
# @param r stated annual rate
|
|
40
|
+
# @param m number of compounding periods per year
|
|
41
|
+
# @example
|
|
42
|
+
# Finrb::Yields.ear(r=0.12,m=12)
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# Finrb::Yields.ear(0.04,365)
|
|
46
|
+
def self.ear(r:, m:)
|
|
47
|
+
r = Flt::DecNum(r.to_s)
|
|
48
|
+
m = Flt::DecNum(m.to_s)
|
|
49
|
+
|
|
50
|
+
((((r / m) + 1)**m) - 1)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Convert stated annual rate to the effective annual rate with continuous compounding
|
|
54
|
+
#
|
|
55
|
+
# @param r stated annual rate
|
|
56
|
+
# @example
|
|
57
|
+
# Finrb::Yields.ear_continuous(r=0.1)
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# Finrb::Yields.ear_continuous(0.03)
|
|
61
|
+
def self.ear_continuous(r:)
|
|
62
|
+
r = Flt::DecNum(r.to_s)
|
|
63
|
+
|
|
64
|
+
(r.exp - 1)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# bond-equivalent yield (BEY), 2 x the semiannual discount rate
|
|
68
|
+
#
|
|
69
|
+
# @param ear effective annual rate
|
|
70
|
+
# @example
|
|
71
|
+
# Finrb::Yields.ear2bey(ear=0.08)
|
|
72
|
+
def self.ear2bey(ear:)
|
|
73
|
+
ear = Flt::DecNum(ear.to_s)
|
|
74
|
+
|
|
75
|
+
(((ear + 1).sqrt - 1) * 2)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Computing HPR, the holding period return
|
|
79
|
+
#
|
|
80
|
+
# @param ear effective annual rate
|
|
81
|
+
# @param t number of days remaining until maturity
|
|
82
|
+
# @example
|
|
83
|
+
# Finrb::Yields.ear2hpr(ear=0.05039,t=150)
|
|
84
|
+
def self.ear2hpr(ear:, t:)
|
|
85
|
+
ear = Flt::DecNum(ear.to_s)
|
|
86
|
+
t = Flt::DecNum(t.to_s)
|
|
87
|
+
|
|
88
|
+
(((ear + 1)**(t / 365)) - 1)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Equivalent/proportional Interest Rates
|
|
92
|
+
# @note An interest rate to be applied n times p.a. can be converted to an equivalent rate to be applied p times p.a.
|
|
93
|
+
# @param r interest rate to be applied n times per year (r is annual rate!)
|
|
94
|
+
# @param n times that the interest rate r were compounded per year
|
|
95
|
+
# @param p times that the equivalent rate were compounded per year
|
|
96
|
+
# @param type equivalent interest rates ('e',default) or proportional interest rates ('p')
|
|
97
|
+
# @example
|
|
98
|
+
# # monthly interest rat equivalent to 5% compounded per year
|
|
99
|
+
# Finrb::Yields.eir(r=0.05,n=1,p=12)
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# # monthly interest rat equivalent to 5% compounded per half year
|
|
103
|
+
# Finrb::Yields.eir(r=0.05,n=2,p=12)
|
|
104
|
+
#
|
|
105
|
+
# @example
|
|
106
|
+
# # monthly interest rat equivalent to 5% compounded per quarter
|
|
107
|
+
# Finrb::Yields.eir(r=0.05,n=4,p=12)
|
|
108
|
+
#
|
|
109
|
+
# @example
|
|
110
|
+
# # annual interest rate equivalent to 5% compounded per month
|
|
111
|
+
# Finrb::Yields.eir(r=0.05,n=12,p=1)
|
|
112
|
+
# # this is equivalent to
|
|
113
|
+
# Finrb::Yields.ear(r=0.05,m=12)
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# # quarter interest rate equivalent to 5% compounded per year
|
|
117
|
+
# Finrb::Yields.eir(r=0.05,n=1,p=4)
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# # quarter interest rate equivalent to 5% compounded per month
|
|
121
|
+
# Finrb::Yields.eir(r=0.05,n=12,p=4)
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# # monthly proportional interest rate which is equivalent to a simple annual interest
|
|
125
|
+
# Finrb::Yields.eir(r=0.05,p=12,type='p')
|
|
126
|
+
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
|
+
type = type.to_s
|
|
131
|
+
|
|
132
|
+
case type
|
|
133
|
+
when 'e'
|
|
134
|
+
eir = (((r / n) + 1)**(n / p)) - 1
|
|
135
|
+
when 'p'
|
|
136
|
+
eir = r / p
|
|
137
|
+
else
|
|
138
|
+
raise(Error, "type must be 'e' or 'p'")
|
|
139
|
+
end
|
|
140
|
+
eir
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# bond-equivalent yield (BEY), 2 x the semiannual discount rate
|
|
144
|
+
#
|
|
145
|
+
# @param hpr holding period return
|
|
146
|
+
# @param t number of month remaining until maturity
|
|
147
|
+
# @example
|
|
148
|
+
# Finrb::Yields.hpr2bey(hpr=0.02,t=3)
|
|
149
|
+
def self.hpr2bey(hpr:, t:)
|
|
150
|
+
hpr = Flt::DecNum(hpr.to_s)
|
|
151
|
+
t = Flt::DecNum(t.to_s)
|
|
152
|
+
|
|
153
|
+
((((hpr + 1)**(6 / t)) - 1) * 2)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Convert holding period return to the effective annual rate
|
|
157
|
+
#
|
|
158
|
+
# @param hpr holding period return
|
|
159
|
+
# @param t number of days remaining until maturity
|
|
160
|
+
# @example
|
|
161
|
+
# Finrb::Yields.hpr2ear(hpr=0.015228,t=120)
|
|
162
|
+
def self.hpr2ear(hpr:, t:)
|
|
163
|
+
hpr = Flt::DecNum(hpr.to_s)
|
|
164
|
+
t = Flt::DecNum(t.to_s)
|
|
165
|
+
|
|
166
|
+
(((hpr + 1)**(365 / t)) - 1)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Computing money market yield (MMY) for a T-bill
|
|
170
|
+
#
|
|
171
|
+
# @param hpr holding period return
|
|
172
|
+
# @param t number of days remaining until maturity
|
|
173
|
+
# @example
|
|
174
|
+
# Finrb::Yields.hpr2mmy(hpr=0.01523,t=120)
|
|
175
|
+
def self.hpr2mmy(hpr:, t:)
|
|
176
|
+
hpr = Flt::DecNum(hpr.to_s)
|
|
177
|
+
t = Flt::DecNum(t.to_s)
|
|
178
|
+
|
|
179
|
+
(hpr * 360 / t)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Computing HPR, the holding period return
|
|
183
|
+
#
|
|
184
|
+
# @param mmy money market yield
|
|
185
|
+
# @param t number of days remaining until maturity
|
|
186
|
+
# @example
|
|
187
|
+
# Finrb::Yields.mmy2hpr(mmy=0.04898,t=150)
|
|
188
|
+
def self.mmy2hpr(mmy:, t:)
|
|
189
|
+
mmy = Flt::DecNum(mmy.to_s)
|
|
190
|
+
t = Flt::DecNum(t.to_s)
|
|
191
|
+
|
|
192
|
+
(mmy * t / 360)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Convert a given norminal rate to a continuous compounded rate
|
|
196
|
+
#
|
|
197
|
+
# @param r norminal rate
|
|
198
|
+
# @param m number of times compounded each year
|
|
199
|
+
# @example
|
|
200
|
+
# Finrb::Yields.r_continuous(r=0.03,m=4)
|
|
201
|
+
def self.r_continuous(r:, m:)
|
|
202
|
+
r = Flt::DecNum(r.to_s)
|
|
203
|
+
m = Flt::DecNum(m.to_s)
|
|
204
|
+
|
|
205
|
+
(m * ((r / m) + 1).log)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Convert a given continuous compounded rate to a norminal rate
|
|
209
|
+
#
|
|
210
|
+
# @param rc continuous compounded rate
|
|
211
|
+
# @param m number of desired times compounded each year
|
|
212
|
+
# @example
|
|
213
|
+
# Finrb::Yields.r_norminal(0.03,1)
|
|
214
|
+
#
|
|
215
|
+
# @example
|
|
216
|
+
# Finrb::Yields.r_norminal(rc=0.03,m=4)
|
|
217
|
+
def self.r_norminal(rc:, m:)
|
|
218
|
+
rc = Flt::DecNum(rc.to_s)
|
|
219
|
+
m = Flt::DecNum(m.to_s)
|
|
220
|
+
|
|
221
|
+
(m * ((rc / m).exp - 1))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
data/lib/finrb.rb
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'finrb/accounting'
|
|
4
4
|
require 'finrb/cashflows'
|
|
5
5
|
require 'finrb/config'
|
|
6
6
|
require 'finrb/decimal'
|
|
7
|
-
require 'finrb/
|
|
7
|
+
require 'finrb/errors'
|
|
8
|
+
require 'finrb/precision'
|
|
9
|
+
require 'finrb/ratios'
|
|
10
|
+
require 'finrb/returns'
|
|
11
|
+
require 'finrb/tvm'
|
|
12
|
+
require 'finrb/version'
|
|
13
|
+
require 'finrb/yields'
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
FinrbError = Finrb::Error
|
|
10
16
|
|
|
11
17
|
# The *Finrb* module adheres to the following conventions for
|
|
12
18
|
# financial calculations:
|
|
@@ -19,5 +25,4 @@ module Finrb
|
|
|
19
25
|
autoload :Amortization, 'finrb/amortization'
|
|
20
26
|
autoload :Rate, 'finrb/rates'
|
|
21
27
|
autoload :Transaction, 'finrb/transaction'
|
|
22
|
-
autoload :Utils, 'finrb/utils'
|
|
23
28
|
end
|
data/sig/finrb.rbs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
module Flt
|
|
2
|
+
class DecNum < Numeric
|
|
3
|
+
end
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module Finrb
|
|
7
|
+
VERSION: String
|
|
8
|
+
|
|
9
|
+
type number = Numeric
|
|
10
|
+
type decimal = Flt::DecNum
|
|
11
|
+
type numbers = Array[number] | Enumerable[number]
|
|
12
|
+
|
|
13
|
+
interface _Configuration
|
|
14
|
+
def eps: () -> decimal
|
|
15
|
+
def guess: () -> decimal
|
|
16
|
+
def business_days: () -> bool
|
|
17
|
+
def periodic_compound: () -> bool
|
|
18
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
interface _ConfigurationBuilder
|
|
22
|
+
def eps: () -> untyped
|
|
23
|
+
def eps=: (untyped) -> untyped
|
|
24
|
+
def guess: () -> untyped
|
|
25
|
+
def guess=: (untyped) -> untyped
|
|
26
|
+
def business_days: () -> untyped
|
|
27
|
+
def business_days=: (untyped) -> untyped
|
|
28
|
+
def periodic_compound: () -> untyped
|
|
29
|
+
def periodic_compound=: (untyped) -> untyped
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.config: () -> _Configuration
|
|
33
|
+
def self.configure: () { (_ConfigurationBuilder) -> void } -> _Configuration
|
|
34
|
+
def self.with_config: [A] (**untyped) { () -> A } -> A
|
|
35
|
+
|
|
36
|
+
class Error < StandardError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class ConvergenceError < Error
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class DomainError < Error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class InvalidCashflowError < Error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module Precision
|
|
49
|
+
def self.money: (number) -> decimal
|
|
50
|
+
def self.rate: (number) -> decimal
|
|
51
|
+
def self.round: (number, places: Integer) -> decimal
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class Rate
|
|
55
|
+
include Comparable
|
|
56
|
+
|
|
57
|
+
TYPES: Hash[Symbol, String]
|
|
58
|
+
|
|
59
|
+
def self.to_effective: (number, number) -> decimal
|
|
60
|
+
def self.to_nominal: (number, number) -> decimal
|
|
61
|
+
|
|
62
|
+
def initialize: (number, Symbol, ?Hash[Symbol, untyped]) -> void
|
|
63
|
+
def duration: () -> Integer?
|
|
64
|
+
def effective: () -> decimal
|
|
65
|
+
def nominal: () -> decimal
|
|
66
|
+
def <=>: (Rate) -> Integer?
|
|
67
|
+
def apr: () -> decimal
|
|
68
|
+
def apy: () -> decimal
|
|
69
|
+
def duration=: (Integer) -> Integer
|
|
70
|
+
def inspect: () -> String
|
|
71
|
+
def monthly: () -> decimal
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class Transaction
|
|
75
|
+
def initialize: (number, ?Hash[Symbol, untyped]) -> void
|
|
76
|
+
def amount: () -> decimal
|
|
77
|
+
def amount=: (number) -> number
|
|
78
|
+
def period: () -> Integer?
|
|
79
|
+
def period=: (Integer?) -> Integer?
|
|
80
|
+
def date: () -> untyped
|
|
81
|
+
def date=: (untyped) -> untyped
|
|
82
|
+
def difference: () -> decimal
|
|
83
|
+
def interest?: () -> bool
|
|
84
|
+
def inspect: () -> String
|
|
85
|
+
def modify: () { (Transaction) -> number } -> number
|
|
86
|
+
def payment: () -> decimal
|
|
87
|
+
def payment?: () -> bool
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Interest < Transaction
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class Payment < Transaction
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class Amortization
|
|
97
|
+
def self.payment: (number, number, Integer) -> decimal
|
|
98
|
+
|
|
99
|
+
def initialize: (number, *Rate) ?{ (Transaction) -> number } -> void
|
|
100
|
+
def ==: (untyped) -> bool
|
|
101
|
+
def balance: () -> decimal
|
|
102
|
+
def payment: () -> decimal?
|
|
103
|
+
def principal: () -> decimal
|
|
104
|
+
def rates: () -> Array[Rate]
|
|
105
|
+
def additional_payments: () -> Array[decimal]
|
|
106
|
+
def duration: () -> Integer
|
|
107
|
+
def inspect: () -> String
|
|
108
|
+
def interest: () -> Array[decimal]
|
|
109
|
+
def payments: () -> Array[decimal]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
module Cashflow
|
|
113
|
+
def self.irr: (Enumerable[number], ?number) -> decimal
|
|
114
|
+
def self.npv: (Enumerable[number], number) -> decimal
|
|
115
|
+
def self.xirr: (Enumerable[Transaction], ?number) -> Rate
|
|
116
|
+
def self.xnpv: (Enumerable[Transaction], number) -> decimal
|
|
117
|
+
|
|
118
|
+
def irr: (?number) -> decimal
|
|
119
|
+
def npv: (number) -> decimal
|
|
120
|
+
def xirr: (?number) -> Rate
|
|
121
|
+
def xnpv: (number) -> decimal
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
module TVM
|
|
125
|
+
def self.discount_rate: (n: number, pv: number, fv: number, pmt: number, ?type: Integer, ?lower: number, ?upper: number) -> decimal
|
|
126
|
+
def self.fv: (r: number, n: number, ?pv: number, ?pmt: number, ?type: Integer) -> decimal
|
|
127
|
+
def self.fv_annuity: (r: number, n: number, pmt: number, ?type: Integer) -> decimal
|
|
128
|
+
def self.fv_simple: (r: number, n: number, pv: number) -> decimal
|
|
129
|
+
def self.fv_uneven: (r: number, cf: number | numbers) -> decimal
|
|
130
|
+
def self.n_period: (r: number, pv: number, fv: number, pmt: number, ?type: Integer) -> decimal
|
|
131
|
+
def self.npv: (r: number, cf: number | numbers) -> decimal
|
|
132
|
+
def self.pmt: (r: number, n: number, pv: number, fv: number, ?type: Integer) -> decimal
|
|
133
|
+
def self.pv: (r: number, n: number, ?fv: number, ?pmt: number, ?type: Integer) -> decimal
|
|
134
|
+
def self.pv_annuity: (r: number, n: number, pmt: number, ?type: Integer) -> decimal
|
|
135
|
+
def self.pv_perpetuity: (r: number, pmt: number, ?g: number, ?type: Integer) -> decimal
|
|
136
|
+
def self.pv_simple: (r: number, n: number, fv: number) -> decimal
|
|
137
|
+
def self.pv_uneven: (r: number, cf: number | numbers) -> decimal
|
|
138
|
+
def self.r_perpetuity: (pmt: number, pv: number) -> decimal
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
module Accounting
|
|
142
|
+
type inventory_result = { cost_of_goods: decimal, ending_inventory: decimal }
|
|
143
|
+
|
|
144
|
+
def self.cogs: (uinv: number, pinv: number, units: number | numbers, price: number | numbers, sinv: number, ?method: String) -> inventory_result
|
|
145
|
+
def self.ddb: (cost: number, rv: number, t: Integer) -> { t: Array[Integer], ddb: Array[decimal] }
|
|
146
|
+
def self.slde: (cost: number, rv: number, t: number) -> decimal
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
module Ratios
|
|
150
|
+
def self.cash_ratio: (cash: number, ms: number, cl: number) -> decimal
|
|
151
|
+
def self.current_ratio: (ca: number, cl: number) -> decimal
|
|
152
|
+
def self.debt_ratio: (td: number, ta: number) -> decimal
|
|
153
|
+
def self.diluted_eps: (ni: number, pd: number, w: number, ?cpd: number, ?cdi: number, ?tax: number, ?cps: number, ?cds: number, ?iss: number) -> decimal
|
|
154
|
+
def self.eps: (ni: number, pd: number, w: number) -> decimal
|
|
155
|
+
def self.financial_leverage: (te: number, ta: number) -> decimal
|
|
156
|
+
def self.gpm: (gp: number, rv: number) -> decimal
|
|
157
|
+
def self.iss: (amp: number, ep: number, n: number) -> decimal
|
|
158
|
+
def self.lt_d2e: (ltd: number, te: number) -> decimal
|
|
159
|
+
def self.npm: (ni: number, rv: number) -> decimal
|
|
160
|
+
def self.quick_ratio: (cash: number, ms: number, rc: number, cl: number) -> decimal
|
|
161
|
+
def self.total_d2e: (td: number, te: number) -> decimal
|
|
162
|
+
def self.was: (ns: number | numbers | nil, nm: number | numbers | nil) -> (decimal | Integer)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
module Returns
|
|
166
|
+
def self.coefficient_variation: (sd: number, avg: number) -> decimal
|
|
167
|
+
def self.geometric_mean: (r: number | numbers) -> decimal
|
|
168
|
+
def self.harmonic_mean: (p: number | numbers) -> decimal
|
|
169
|
+
def self.hpr: (ev: number, bv: number, ?cfr: number) -> decimal
|
|
170
|
+
def self.sampling_error: (sm: number, mu: number) -> decimal
|
|
171
|
+
def self.sf_ratio: (rp: number, rl: number, sd: number) -> decimal
|
|
172
|
+
def self.sharpe_ratio: (rp: number, rf: number, sd: number) -> decimal
|
|
173
|
+
def self.twrr: (ev: number | numbers, bv: number | numbers, cfr: number | numbers) -> decimal
|
|
174
|
+
def self.wpr: (r: number | numbers, w: number | numbers) -> decimal
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
module Yields
|
|
178
|
+
def self.bdy: (d: number, f: number, t: number) -> decimal
|
|
179
|
+
def self.bdy2mmy: (bdy: number, t: number) -> decimal
|
|
180
|
+
def self.ear: (r: number, m: number) -> decimal
|
|
181
|
+
def self.ear_continuous: (r: number) -> decimal
|
|
182
|
+
def self.ear2bey: (ear: number) -> decimal
|
|
183
|
+
def self.ear2hpr: (ear: number, t: number) -> decimal
|
|
184
|
+
def self.eir: (r: number, ?n: number, ?p: number, ?type: String) -> decimal
|
|
185
|
+
def self.hpr2bey: (hpr: number, t: number) -> decimal
|
|
186
|
+
def self.hpr2ear: (hpr: number, t: number) -> decimal
|
|
187
|
+
def self.hpr2mmy: (hpr: number, t: number) -> decimal
|
|
188
|
+
def self.mmy2hpr: (mmy: number, t: number) -> decimal
|
|
189
|
+
def self.r_continuous: (r: number, m: number) -> decimal
|
|
190
|
+
def self.r_norminal: (rc: number, m: number) -> decimal
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
module Numerical
|
|
194
|
+
class Brent
|
|
195
|
+
def initialize: (tolerance: number, ?relative_tolerance: number, ?max_iterations: Integer) -> void
|
|
196
|
+
def solve: (^(decimal) -> number, lower: number, upper: number) -> decimal
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class RateSearch
|
|
200
|
+
def initialize: (?step: number, ?max_steps: Integer) -> void
|
|
201
|
+
def bracket: (^(decimal) -> number, guess: number) -> [decimal, decimal]
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|