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.
@@ -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 'decimal'
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: 'effective', apy: 'effective', effective: 'effective', nominal: 'nominal' }.freeze
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 = Flt::DecNum.new(rate.to_s)
25
- periods = Flt::DecNum.new(periods.to_s)
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 = Flt::DecNum.new(rate.to_s)
44
- periods = Flt::DecNum.new(periods.to_s)
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.to_f / periods)) - 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)}=", Flt::DecNum.new(rate.to_s))
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
- attr_accessor :duration
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
- # (see #effective)
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
- effective
125
+ nominal
108
126
  end
109
127
 
110
- # (see #effective)
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 Flt::DecNum.new(input.to_s)
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.160755')
152
- # rate.monthly.round(6) #=> Flt::DecNum('0.013396')
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).round(15)
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
@@ -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