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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'flt'
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
|
|
6
|
+
module Finrb
|
|
7
|
+
module Numerical
|
|
8
|
+
# Locates the nearest sign-changing rate interval around a caller's guess.
|
|
9
|
+
# Search happens in log(1 + rate) space, which covers the entire financial
|
|
10
|
+
# domain rate > -1 without stepping across its singular boundary.
|
|
11
|
+
class RateSearch
|
|
12
|
+
DEFAULT_STEP = '0.125'
|
|
13
|
+
DEFAULT_MAX_STEPS = 256
|
|
14
|
+
private_constant :DEFAULT_STEP, :DEFAULT_MAX_STEPS
|
|
15
|
+
|
|
16
|
+
def initialize(step: DEFAULT_STEP, max_steps: DEFAULT_MAX_STEPS)
|
|
17
|
+
@step = decimal(step)
|
|
18
|
+
@max_steps = Integer(max_steps)
|
|
19
|
+
|
|
20
|
+
raise(ArgumentError, 'Search step must be positive.') unless @step.positive?
|
|
21
|
+
raise(ArgumentError, 'Maximum search steps must be positive.') unless @max_steps.positive?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bracket(function, guess:)
|
|
25
|
+
guess = decimal(guess)
|
|
26
|
+
raise(DomainError, 'Rate guess must be greater than -1.') if guess <= -1
|
|
27
|
+
|
|
28
|
+
center_coordinate = (guess + 1).ln
|
|
29
|
+
center = [guess, evaluate(function, guess)]
|
|
30
|
+
return [guess, guess] if center.last.zero?
|
|
31
|
+
|
|
32
|
+
left = center
|
|
33
|
+
right = center
|
|
34
|
+
|
|
35
|
+
1.upto(@max_steps) do |distance|
|
|
36
|
+
next_left = point(function, center_coordinate - (@step * distance))
|
|
37
|
+
next_right = point(function, center_coordinate + (@step * distance))
|
|
38
|
+
candidates = []
|
|
39
|
+
candidates << [next_left.first, left.first] if opposite_signs?(next_left.last, left.last)
|
|
40
|
+
candidates << [right.first, next_right.first] if opposite_signs?(right.last, next_right.last)
|
|
41
|
+
return nearest(candidates, guess) unless candidates.empty?
|
|
42
|
+
|
|
43
|
+
left = next_left
|
|
44
|
+
right = next_right
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
raise(ConvergenceError, "Could not bracket a root near guess #{guess}.")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def point(function, coordinate)
|
|
53
|
+
rate = coordinate.exp - 1
|
|
54
|
+
[rate, evaluate(function, rate)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def evaluate(function, rate)
|
|
58
|
+
result = decimal(function.call(rate))
|
|
59
|
+
raise(DomainError, "Rate function returned a non-finite value at #{rate}.") unless result.finite?
|
|
60
|
+
|
|
61
|
+
result
|
|
62
|
+
rescue Flt::Num::Exception, FloatDomainError, Math::DomainError, ZeroDivisionError => e
|
|
63
|
+
raise(DomainError, "Rate function is undefined at #{rate}: #{e.message}", e.backtrace)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def nearest(candidates, guess)
|
|
67
|
+
candidates.min_by { |lower, upper| (((lower + upper) / 2) - guess).abs }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def opposite_signs?(left, right)
|
|
71
|
+
left.negative? != right.negative?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def decimal(value)
|
|
75
|
+
Flt::DecNum.new(value.to_s)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'validation'
|
|
4
|
+
|
|
5
|
+
module Finrb
|
|
6
|
+
# Explicit quantization policy for values rounded during calculations.
|
|
7
|
+
# General calculations otherwise retain the active Flt::DecNum context.
|
|
8
|
+
module Precision
|
|
9
|
+
MONEY_PLACES = 2
|
|
10
|
+
RATE_PLACES = 15
|
|
11
|
+
ROUNDING_MODE = :half_up
|
|
12
|
+
public_constant :MONEY_PLACES, :RATE_PLACES, :ROUNDING_MODE
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def money(value)
|
|
17
|
+
round(value, places: MONEY_PLACES)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def rate(value)
|
|
21
|
+
round(value, places: RATE_PLACES)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def round(value, places:)
|
|
25
|
+
Validation.decimal(value, name: 'value').round(places, rounding: ROUNDING_MODE)
|
|
26
|
+
end
|
|
27
|
+
private_class_method :round
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/finrb/rates.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'precision'
|
|
4
|
+
require_relative 'validation'
|
|
4
5
|
|
|
5
6
|
module Finrb
|
|
6
7
|
# the Rate class provides an interface for working with interest rates.
|
|
@@ -10,9 +11,20 @@ module Finrb
|
|
|
10
11
|
include Comparable
|
|
11
12
|
|
|
12
13
|
# Accepted rate types
|
|
13
|
-
TYPES = { apr: '
|
|
14
|
+
TYPES = { apr: 'nominal', apy: 'effective', effective: 'effective', nominal: 'nominal' }.freeze
|
|
14
15
|
public_constant :TYPES
|
|
15
16
|
|
|
17
|
+
def self.compounding_periods(value)
|
|
18
|
+
infinite = value.infinite? if value.respond_to?(:infinite?)
|
|
19
|
+
return Flt::DecNum.infinity if [true, 1].include?(infinite)
|
|
20
|
+
|
|
21
|
+
periods = Validation.decimal(value, name: 'compounding periods')
|
|
22
|
+
raise(ArgumentError, 'compounding periods must be positive.') unless periods.positive?
|
|
23
|
+
|
|
24
|
+
periods
|
|
25
|
+
end
|
|
26
|
+
private_class_method :compounding_periods
|
|
27
|
+
|
|
16
28
|
# convert a nominal interest rate to an effective interest rate
|
|
17
29
|
# @return [Flt::DecNum] the effective interest rate
|
|
18
30
|
# @param [Numeric] rate the nominal interest rate
|
|
@@ -21,8 +33,8 @@ module Finrb
|
|
|
21
33
|
# Rate.to_effective(0.05, 4) #=> Flt::DecNum('0.05095')
|
|
22
34
|
# @api public
|
|
23
35
|
def self.to_effective(rate, periods)
|
|
24
|
-
rate =
|
|
25
|
-
periods =
|
|
36
|
+
rate = Validation.decimal(rate, name: 'rate')
|
|
37
|
+
periods = compounding_periods(periods)
|
|
26
38
|
|
|
27
39
|
if periods.infinite?
|
|
28
40
|
rate.exp - 1
|
|
@@ -40,13 +52,15 @@ module Finrb
|
|
|
40
52
|
# @see https://www.miniwebtool.com/nominal-interest-rate-calculator/
|
|
41
53
|
# @api public
|
|
42
54
|
def self.to_nominal(rate, periods)
|
|
43
|
-
rate =
|
|
44
|
-
|
|
55
|
+
rate = Validation.decimal(rate, name: 'rate')
|
|
56
|
+
raise(ArgumentError, 'effective rate must be greater than -1.') if rate <= -1
|
|
57
|
+
|
|
58
|
+
periods = compounding_periods(periods)
|
|
45
59
|
|
|
46
60
|
if periods.infinite?
|
|
47
61
|
(rate + 1).log
|
|
48
62
|
else
|
|
49
|
-
periods * (((rate + 1)**(1
|
|
63
|
+
periods * (((rate + 1)**(Flt::DecNum.new(1) / periods)) - 1)
|
|
50
64
|
end
|
|
51
65
|
end
|
|
52
66
|
|
|
@@ -63,6 +77,9 @@ module Finrb
|
|
|
63
77
|
# @see https://en.wikipedia.org/wiki/Nominal_interest_rate
|
|
64
78
|
# @api public
|
|
65
79
|
def initialize(rate, type, opts = {})
|
|
80
|
+
raise(ArgumentError, 'options must be a Hash.') unless opts.is_a?(Hash)
|
|
81
|
+
raise(ArgumentError, 'options may only contain compounds and duration.') unless (opts.keys - %i[compounds duration]).empty?
|
|
82
|
+
|
|
66
83
|
# Default monthly compounding.
|
|
67
84
|
opts = { compounds: :monthly }.merge(opts)
|
|
68
85
|
|
|
@@ -73,7 +90,7 @@ module Finrb
|
|
|
73
90
|
|
|
74
91
|
# Set the rate in the proper way, based on the value of type.
|
|
75
92
|
begin
|
|
76
|
-
__send__(:"#{TYPES.fetch(type)}=",
|
|
93
|
+
__send__(:"#{TYPES.fetch(type)}=", Validation.decimal(rate, name: 'rate'))
|
|
77
94
|
rescue KeyError
|
|
78
95
|
raise(ArgumentError, "type must be one of #{TYPES.keys.join(', ')}", caller)
|
|
79
96
|
end
|
|
@@ -81,7 +98,7 @@ module Finrb
|
|
|
81
98
|
|
|
82
99
|
# @return [Integer] the duration for which the rate is valid, in months
|
|
83
100
|
# @api public
|
|
84
|
-
|
|
101
|
+
attr_reader :duration
|
|
85
102
|
# @return [Flt::DecNum] the effective interest rate
|
|
86
103
|
# @api public
|
|
87
104
|
attr_reader :effective
|
|
@@ -101,13 +118,15 @@ module Finrb
|
|
|
101
118
|
@effective <=> other.effective
|
|
102
119
|
end
|
|
103
120
|
|
|
104
|
-
#
|
|
121
|
+
# Return the nominal annual percentage rate for the configured compounding frequency.
|
|
122
|
+
# @return [Flt::DecNum] the nominal annual percentage rate
|
|
105
123
|
# @api public
|
|
106
124
|
def apr
|
|
107
|
-
|
|
125
|
+
nominal
|
|
108
126
|
end
|
|
109
127
|
|
|
110
|
-
#
|
|
128
|
+
# Return the effective annual percentage yield.
|
|
129
|
+
# @return [Flt::DecNum] the effective annual percentage yield
|
|
111
130
|
# @api public
|
|
112
131
|
def apy
|
|
113
132
|
effective
|
|
@@ -127,16 +146,22 @@ module Finrb
|
|
|
127
146
|
when :monthly then Flt::DecNum.new(12)
|
|
128
147
|
when :quarterly then Flt::DecNum.new(4)
|
|
129
148
|
when :semiannually then Flt::DecNum.new(2)
|
|
130
|
-
when Numeric then
|
|
131
|
-
else raise(ArgumentError)
|
|
149
|
+
when Numeric then self.class.__send__(:compounding_periods, input)
|
|
150
|
+
else raise(ArgumentError, 'compounds must be a known frequency or a positive number.')
|
|
132
151
|
end
|
|
133
152
|
end
|
|
134
153
|
|
|
154
|
+
def duration=(value)
|
|
155
|
+
@duration = Validation.positive_integer(value, name: 'duration')
|
|
156
|
+
end
|
|
157
|
+
|
|
135
158
|
# set the effective interest rate
|
|
136
159
|
# @return none
|
|
137
160
|
# @param [Flt::DecNum] rate the effective interest rate
|
|
138
161
|
# @api private
|
|
139
162
|
def effective=(rate)
|
|
163
|
+
raise(ArgumentError, 'effective rate must be greater than -1.') if rate <= -1
|
|
164
|
+
|
|
140
165
|
@effective = rate
|
|
141
166
|
@nominal = Rate.to_nominal(rate, @periods)
|
|
142
167
|
end
|
|
@@ -145,14 +170,15 @@ module Finrb
|
|
|
145
170
|
"Rate.new(#{apr.round(6)}, :apr)"
|
|
146
171
|
end
|
|
147
172
|
|
|
148
|
-
# @return [Flt::DecNum] the monthly effective interest rate
|
|
173
|
+
# @return [Flt::DecNum] the equivalent monthly effective interest rate
|
|
149
174
|
# @example
|
|
150
175
|
# rate = Rate.new(0.15, :nominal)
|
|
151
|
-
# rate.apr.round(6) #=> Flt::DecNum('0.
|
|
152
|
-
# rate.
|
|
176
|
+
# rate.apr.round(6) #=> Flt::DecNum('0.15')
|
|
177
|
+
# rate.apy.round(6) #=> Flt::DecNum('0.160755')
|
|
178
|
+
# rate.monthly.round(6) #=> Flt::DecNum('0.0125')
|
|
153
179
|
# @api public
|
|
154
180
|
def monthly
|
|
155
|
-
(effective / 12)
|
|
181
|
+
@monthly ||= Precision.rate(Rate.to_nominal(effective, 12) / 12)
|
|
156
182
|
end
|
|
157
183
|
|
|
158
184
|
# set the nominal interest rate
|
|
@@ -160,6 +186,8 @@ module Finrb
|
|
|
160
186
|
# @param [Flt::DecNum] rate the nominal interest rate
|
|
161
187
|
# @api private
|
|
162
188
|
def nominal=(rate)
|
|
189
|
+
raise(ArgumentError, 'nominal rate must keep every compounded period greater than -100%.') if !@periods.infinite? && rate <= -@periods
|
|
190
|
+
|
|
163
191
|
@nominal = rate
|
|
164
192
|
@effective = Rate.to_effective(rate, @periods)
|
|
165
193
|
end
|
data/lib/finrb/ratios.rb
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'decimal'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
|
|
6
|
+
module Finrb
|
|
7
|
+
# Financial-statement, leverage, and per-share ratios.
|
|
8
|
+
module Ratios
|
|
9
|
+
def self.wrap_array(object)
|
|
10
|
+
if object.nil?
|
|
11
|
+
[]
|
|
12
|
+
elsif object.respond_to?(:to_ary)
|
|
13
|
+
object.to_ary || [object]
|
|
14
|
+
else
|
|
15
|
+
[object]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
private_class_method :wrap_array
|
|
19
|
+
|
|
20
|
+
# cash ratio -- Liquidity ratios measure the firm's ability to satisfy its short-term obligations as they come due.
|
|
21
|
+
#
|
|
22
|
+
# @param cash cash
|
|
23
|
+
# @param ms marketable securities
|
|
24
|
+
# @param cl current liabilities
|
|
25
|
+
# @example
|
|
26
|
+
# Finrb::Ratios.cash_ratio(cash=3000,ms=2000,cl=2000)
|
|
27
|
+
def self.cash_ratio(cash:, ms:, cl:)
|
|
28
|
+
cash = Flt::DecNum(cash.to_s)
|
|
29
|
+
ms = Flt::DecNum(ms.to_s)
|
|
30
|
+
cl = Flt::DecNum(cl.to_s)
|
|
31
|
+
|
|
32
|
+
((cash + ms) / cl)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# current ratio -- Liquidity ratios measure the firm's ability to satisfy its short-term obligations as they come due.
|
|
36
|
+
#
|
|
37
|
+
# @param ca current assets
|
|
38
|
+
# @param cl current liabilities
|
|
39
|
+
# @example
|
|
40
|
+
# Finrb::Ratios.current_ratio(ca=8000,cl=2000)
|
|
41
|
+
def self.current_ratio(ca:, cl:)
|
|
42
|
+
ca = Flt::DecNum(ca.to_s)
|
|
43
|
+
cl = Flt::DecNum(cl.to_s)
|
|
44
|
+
|
|
45
|
+
(ca / cl)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# debt ratio -- Solvency ratios measure the firm's ability to satisfy its long-term obligations.
|
|
49
|
+
#
|
|
50
|
+
# @param td total debt
|
|
51
|
+
# @param ta total assets
|
|
52
|
+
# @example
|
|
53
|
+
# Finrb::Ratios.debt_ratio(td=6000,ta=20000)
|
|
54
|
+
def self.debt_ratio(td:, ta:)
|
|
55
|
+
td = Flt::DecNum(td.to_s)
|
|
56
|
+
ta = Flt::DecNum(ta.to_s)
|
|
57
|
+
|
|
58
|
+
(td / ta)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# diluted Earnings Per Share
|
|
62
|
+
#
|
|
63
|
+
# @param ni net income
|
|
64
|
+
# @param pd preferred dividends
|
|
65
|
+
# @param cpd dividends on convertible preferred stock
|
|
66
|
+
# @param cdi interest on convertible debt
|
|
67
|
+
# @param tax tax rate
|
|
68
|
+
# @param w weighted average number of common shares outstanding
|
|
69
|
+
# @param cps shares from conversion of convertible preferred stock
|
|
70
|
+
# @param cds shares from conversion of convertible debt
|
|
71
|
+
# @param iss shares issuable from stock options
|
|
72
|
+
# @example
|
|
73
|
+
# Finrb::Ratios.diluted_eps(ni=115600,pd=10000,cdi=42000,tax=0.4,w=200000,cds=60000)
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# Finrb::Ratios.diluted_eps(ni=115600,pd=10000,cpd=10000,w=200000,cps=40000)
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# Finrb::Ratios.diluted_eps(ni=115600,pd=10000,w=200000,iss=2500)
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# Finrb::Ratios.diluted_eps(ni=115600,pd=10000,cpd=10000,cdi=42000,tax=0.4,w=200000,cps=40000,cds=60000,iss=2500)
|
|
83
|
+
def self.diluted_eps(ni:, pd:, w:, cpd: 0, cdi: 0, tax: 0, cps: 0, cds: 0, iss: 0)
|
|
84
|
+
ni = Flt::DecNum(ni.to_s)
|
|
85
|
+
pd = Flt::DecNum(pd.to_s)
|
|
86
|
+
w = Flt::DecNum(w.to_s)
|
|
87
|
+
cpd = Flt::DecNum(cpd.to_s)
|
|
88
|
+
cdi = Flt::DecNum(cdi.to_s)
|
|
89
|
+
tax = Flt::DecNum(tax.to_s)
|
|
90
|
+
cps = Flt::DecNum(cps.to_s)
|
|
91
|
+
cds = Flt::DecNum(cds.to_s)
|
|
92
|
+
iss = Flt::DecNum(iss.to_s)
|
|
93
|
+
|
|
94
|
+
basic = (ni - pd) / w
|
|
95
|
+
diluted = (ni - pd + cpd + (cdi * (1 - tax))) / (w + cps + cds + iss)
|
|
96
|
+
diluted = (ni - pd + cpd) / (w + cps + iss) if diluted > basic
|
|
97
|
+
diluted
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Basic Earnings Per Share
|
|
101
|
+
#
|
|
102
|
+
# @param ni net income
|
|
103
|
+
# @param pd preferred dividends
|
|
104
|
+
# @param w weighted average number of common shares outstanding
|
|
105
|
+
# @example
|
|
106
|
+
# Finrb::Ratios.eps(ni=10000,pd=1000,w=11000)
|
|
107
|
+
def self.eps(ni:, pd:, w:)
|
|
108
|
+
ni = Flt::DecNum(ni.to_s)
|
|
109
|
+
pd = Flt::DecNum(pd.to_s)
|
|
110
|
+
w = Flt::DecNum(w.to_s)
|
|
111
|
+
|
|
112
|
+
((ni - pd) / w)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# financial leverage -- Solvency ratios measure the firm's ability to satisfy its long-term obligations.
|
|
116
|
+
#
|
|
117
|
+
# @param te total equity
|
|
118
|
+
# @param ta total assets
|
|
119
|
+
# @example
|
|
120
|
+
# Finrb::Ratios.financial_leverage(te=16000,ta=20000)
|
|
121
|
+
def self.financial_leverage(te:, ta:)
|
|
122
|
+
te = Flt::DecNum(te.to_s)
|
|
123
|
+
ta = Flt::DecNum(ta.to_s)
|
|
124
|
+
|
|
125
|
+
(ta / te)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# gross profit margin -- Evaluate a company's financial performance
|
|
129
|
+
#
|
|
130
|
+
# @param gp gross profit, equal to revenue minus cost of goods sold (cogs)
|
|
131
|
+
# @param rv revenue (sales)
|
|
132
|
+
# @example
|
|
133
|
+
# Finrb::Ratios.gpm(gp=1000,rv=20000)
|
|
134
|
+
def self.gpm(gp:, rv:)
|
|
135
|
+
gp = Flt::DecNum(gp.to_s)
|
|
136
|
+
rv = Flt::DecNum(rv.to_s)
|
|
137
|
+
|
|
138
|
+
(gp / rv)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# calculate the net increase in common shares from the potential exercise of stock options or warrants
|
|
142
|
+
#
|
|
143
|
+
# @param amp average market price over the year
|
|
144
|
+
# @param ep exercise price of the options or warrants
|
|
145
|
+
# @param n number of common shares that the options and warrants can be convened into
|
|
146
|
+
# @example
|
|
147
|
+
# Finrb::Ratios.iss(amp=20,ep=15,n=10000)
|
|
148
|
+
def self.iss(amp:, ep:, n:)
|
|
149
|
+
amp = Flt::DecNum(amp.to_s)
|
|
150
|
+
ep = Flt::DecNum(ep.to_s)
|
|
151
|
+
n = Flt::DecNum(n.to_s)
|
|
152
|
+
|
|
153
|
+
if amp > ep
|
|
154
|
+
((amp - ep) * n / amp)
|
|
155
|
+
else
|
|
156
|
+
raise(Error, 'amp must larger than ep')
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# long-term debt-to-equity -- Solvency ratios measure the firm's ability to satisfy its long-term obligations.
|
|
161
|
+
#
|
|
162
|
+
# @param ltd long-term debt
|
|
163
|
+
# @param te total equity
|
|
164
|
+
# @example
|
|
165
|
+
# Finrb::Ratios.lt_d2e(ltd=8000,te=20000)
|
|
166
|
+
def self.lt_d2e(ltd:, te:)
|
|
167
|
+
ltd = Flt::DecNum(ltd.to_s)
|
|
168
|
+
te = Flt::DecNum(te.to_s)
|
|
169
|
+
|
|
170
|
+
(ltd / te)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# net profit margin -- Evaluate a company's financial performance
|
|
174
|
+
#
|
|
175
|
+
# @param ni net income
|
|
176
|
+
# @param rv revenue (sales)
|
|
177
|
+
# @example
|
|
178
|
+
# Finrb::Ratios.npm(ni=8000,rv=20000)
|
|
179
|
+
def self.npm(ni:, rv:)
|
|
180
|
+
ni = Flt::DecNum(ni.to_s)
|
|
181
|
+
rv = Flt::DecNum(rv.to_s)
|
|
182
|
+
|
|
183
|
+
(ni / rv)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# quick ratio -- Liquidity ratios measure the firm's ability to satisfy its short-term obligations as they come due.
|
|
187
|
+
#
|
|
188
|
+
# @param cash cash
|
|
189
|
+
# @param ms marketable securities
|
|
190
|
+
# @param rc receivables
|
|
191
|
+
# @param cl current liabilities
|
|
192
|
+
# @example
|
|
193
|
+
# Finrb::Ratios.quick_ratio(cash=3000,ms=2000,rc=1000,cl=2000)
|
|
194
|
+
def self.quick_ratio(cash:, ms:, rc:, cl:)
|
|
195
|
+
cash = Flt::DecNum(cash.to_s)
|
|
196
|
+
ms = Flt::DecNum(ms.to_s)
|
|
197
|
+
rc = Flt::DecNum(rc.to_s)
|
|
198
|
+
cl = Flt::DecNum(cl.to_s)
|
|
199
|
+
|
|
200
|
+
((cash + ms + rc) / cl)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# total debt-to-equity -- Solvency ratios measure the firm's ability to satisfy its long-term obligations.
|
|
204
|
+
#
|
|
205
|
+
# @param td total debt
|
|
206
|
+
# @param te total equity
|
|
207
|
+
# @example
|
|
208
|
+
# Finrb::Ratios.total_d2e(td=6000,te=20000)
|
|
209
|
+
def self.total_d2e(td:, te:)
|
|
210
|
+
td = Flt::DecNum(td.to_s)
|
|
211
|
+
te = Flt::DecNum(te.to_s)
|
|
212
|
+
|
|
213
|
+
(td / te)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# calculate weighted average shares -- weighted average number of common shares
|
|
217
|
+
#
|
|
218
|
+
# @param ns n x 1 vector vector of number of shares
|
|
219
|
+
# @param nm n x 1 vector vector of number of months relate to ns
|
|
220
|
+
# @example
|
|
221
|
+
# s=[10000,2000];m=[12,6];Finrb::Ratios.was(ns=s,nm=m)
|
|
222
|
+
#
|
|
223
|
+
# @example
|
|
224
|
+
# s=[11000,4400,-3000];m=[12,9,4];Finrb::Ratios.was(ns=s,nm=m)
|
|
225
|
+
def self.was(ns:, nm:)
|
|
226
|
+
ns = wrap_array(ns).map { |value| Flt::DecNum(value.to_s) }
|
|
227
|
+
nm = wrap_array(nm).map { |value| Flt::DecNum(value.to_s) }
|
|
228
|
+
|
|
229
|
+
m = ns.size
|
|
230
|
+
n = nm.size
|
|
231
|
+
sum = 0
|
|
232
|
+
if m == n
|
|
233
|
+
(0...m).each do |i|
|
|
234
|
+
sum += (ns[i] * nm[i])
|
|
235
|
+
end
|
|
236
|
+
else
|
|
237
|
+
raise(Error, 'length of ns and nm must be equal')
|
|
238
|
+
end
|
|
239
|
+
sum /= 12
|
|
240
|
+
sum
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'decimal'
|
|
4
|
+
require_relative 'errors'
|
|
5
|
+
|
|
6
|
+
module Finrb
|
|
7
|
+
# Investment return and risk-adjusted performance calculations.
|
|
8
|
+
module Returns
|
|
9
|
+
def self.wrap_array(object)
|
|
10
|
+
if object.nil?
|
|
11
|
+
[]
|
|
12
|
+
elsif object.respond_to?(:to_ary)
|
|
13
|
+
object.to_ary || [object]
|
|
14
|
+
else
|
|
15
|
+
[object]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
private_class_method :wrap_array
|
|
19
|
+
|
|
20
|
+
# Computing Coefficient of variation
|
|
21
|
+
#
|
|
22
|
+
# @param sd standard deviation
|
|
23
|
+
# @param avg average value
|
|
24
|
+
# @example
|
|
25
|
+
# Finrb::Returns.coefficient_variation(sd=0.15,avg=0.39)
|
|
26
|
+
def self.coefficient_variation(sd:, avg:)
|
|
27
|
+
sd = Flt::DecNum(sd.to_s)
|
|
28
|
+
avg = Flt::DecNum(avg.to_s)
|
|
29
|
+
|
|
30
|
+
(sd / avg)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Geometric mean return
|
|
34
|
+
#
|
|
35
|
+
# @param r returns over multiple periods
|
|
36
|
+
# @example
|
|
37
|
+
# Finrb::Returns.geometric_mean(r=[-0.0934, 0.2345, 0.0892])
|
|
38
|
+
def self.geometric_mean(r:)
|
|
39
|
+
r = wrap_array(r).map { |value| Flt::DecNum(value.to_s) }
|
|
40
|
+
|
|
41
|
+
rs = r.map { |value| value + 1 }
|
|
42
|
+
((rs.reduce(:*)**(Flt::DecNum(1) / rs.size)) - 1)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# harmonic mean, average price
|
|
46
|
+
# @param p price over multiple periods
|
|
47
|
+
# @example
|
|
48
|
+
# Finrb::Returns.harmonic_mean(p=[8,9,10])
|
|
49
|
+
def self.harmonic_mean(p:)
|
|
50
|
+
p = wrap_array(p).map { |value| Flt::DecNum(value.to_s) }
|
|
51
|
+
|
|
52
|
+
(Flt::DecNum(1) / (p.sum { |val| Flt::DecNum(1) / val } / p.size))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Computing HPR, the holding period return
|
|
56
|
+
#
|
|
57
|
+
# @param ev ending value
|
|
58
|
+
# @param bv beginning value
|
|
59
|
+
# @param cfr cash flow received
|
|
60
|
+
# @example
|
|
61
|
+
# Finrb::Returns.hpr(ev=33,bv=30,cfr=0.5)
|
|
62
|
+
def self.hpr(ev:, bv:, cfr: 0)
|
|
63
|
+
ev = Flt::DecNum(ev.to_s)
|
|
64
|
+
bv = Flt::DecNum(bv.to_s)
|
|
65
|
+
cfr = Flt::DecNum(cfr.to_s)
|
|
66
|
+
|
|
67
|
+
((ev - bv + cfr) / bv)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Computing Sampling error
|
|
71
|
+
#
|
|
72
|
+
# @param sm sample mean
|
|
73
|
+
# @param mu population mean
|
|
74
|
+
# @example
|
|
75
|
+
# Finrb::Returns.sampling_error(sm=0.45, mu=0.5)
|
|
76
|
+
def self.sampling_error(sm:, mu:)
|
|
77
|
+
sm = Flt::DecNum(sm.to_s)
|
|
78
|
+
mu = Flt::DecNum(mu.to_s)
|
|
79
|
+
|
|
80
|
+
(sm - mu)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Computing Roy's safety-first ratio
|
|
84
|
+
#
|
|
85
|
+
# @param rp portfolio return
|
|
86
|
+
# @param rl threshold level return
|
|
87
|
+
# @param sd standard deviation of portfolio retwns
|
|
88
|
+
# @example
|
|
89
|
+
# Finrb::Returns.sf_ratio(rp=0.09,rl=0.03,sd=0.12)
|
|
90
|
+
def self.sf_ratio(rp:, rl:, sd:)
|
|
91
|
+
rp = Flt::DecNum(rp.to_s)
|
|
92
|
+
rl = Flt::DecNum(rl.to_s)
|
|
93
|
+
sd = Flt::DecNum(sd.to_s)
|
|
94
|
+
|
|
95
|
+
((rp - rl) / sd)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Computing Sharpe Ratio
|
|
99
|
+
#
|
|
100
|
+
# @param rp portfolio return
|
|
101
|
+
# @param rf risk-free return
|
|
102
|
+
# @param sd standard deviation of portfolio retwns
|
|
103
|
+
# @example
|
|
104
|
+
# Finrb::Returns.sharpe_ratio(rp=0.038,rf=0.015,sd=0.07)
|
|
105
|
+
def self.sharpe_ratio(rp:, rf:, sd:)
|
|
106
|
+
rp = Flt::DecNum(rp.to_s)
|
|
107
|
+
rf = Flt::DecNum(rf.to_s)
|
|
108
|
+
sd = Flt::DecNum(sd.to_s)
|
|
109
|
+
|
|
110
|
+
((rp - rf) / sd)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Computing TWRR, the time-weighted rate of return
|
|
114
|
+
#
|
|
115
|
+
# @param ev ordered ending value list
|
|
116
|
+
# @param bv ordered beginning value list
|
|
117
|
+
# @param cfr ordered cash flow received list
|
|
118
|
+
# @example
|
|
119
|
+
# Finrb::Returns.twrr(ev=[120,260],bv=[100,240],cfr=[2,4])
|
|
120
|
+
def self.twrr(ev:, bv:, cfr:)
|
|
121
|
+
ev = wrap_array(ev).map { |value| Flt::DecNum(value.to_s) }
|
|
122
|
+
bv = wrap_array(bv).map { |value| Flt::DecNum(value.to_s) }
|
|
123
|
+
cfr = wrap_array(cfr).map { |value| Flt::DecNum(value.to_s) }
|
|
124
|
+
|
|
125
|
+
r = ev.size
|
|
126
|
+
s = bv.size
|
|
127
|
+
t = cfr.size
|
|
128
|
+
wr = Flt::DecNum(1)
|
|
129
|
+
if r != s || r != t || s != t
|
|
130
|
+
raise(Error, 'Different number of values!')
|
|
131
|
+
else
|
|
132
|
+
(0...r).each do |i|
|
|
133
|
+
wr *= (Finrb::Returns.hpr(ev: ev[i], bv: bv[i], cfr: cfr[i]) + 1)
|
|
134
|
+
end
|
|
135
|
+
((wr**(Flt::DecNum(1) / r)) - 1)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Weighted mean as a portfolio return
|
|
140
|
+
#
|
|
141
|
+
# @param r returns of the individual assets in the portfolio
|
|
142
|
+
# @param w corresponding weights associated with each of the individual assets
|
|
143
|
+
# @example
|
|
144
|
+
# Finrb::Returns.wpr(r=[0.12, 0.07, 0.03],w=[0.5,0.4,0.1])
|
|
145
|
+
def self.wpr(r:, w:)
|
|
146
|
+
r = wrap_array(r).map { |value| Flt::DecNum(value.to_s) }
|
|
147
|
+
w = wrap_array(w).map { |value| Flt::DecNum(value.to_s) }
|
|
148
|
+
|
|
149
|
+
# TODO: need to change
|
|
150
|
+
puts('sum of weights is NOT equal to 1!') if w.sum != 1
|
|
151
|
+
|
|
152
|
+
r.zip(w).sum { |arr| arr.reduce(:*) }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|