finrb 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/finrb/ratios.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
  # Financial-statement, leverage, and per-share ratios.
@@ -25,9 +26,9 @@ module Finrb
25
26
  # @example
26
27
  # Finrb::Ratios.cash_ratio(cash=3000,ms=2000,cl=2000)
27
28
  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)
29
+ cash = Validation.decimal(cash, name: 'cash')
30
+ ms = Validation.decimal(ms, name: 'marketable securities')
31
+ cl = Validation.non_zero_decimal(cl, name: 'current liabilities', error: DomainError)
31
32
 
32
33
  ((cash + ms) / cl)
33
34
  end
@@ -39,8 +40,8 @@ module Finrb
39
40
  # @example
40
41
  # Finrb::Ratios.current_ratio(ca=8000,cl=2000)
41
42
  def self.current_ratio(ca:, cl:)
42
- ca = Flt::DecNum(ca.to_s)
43
- cl = Flt::DecNum(cl.to_s)
43
+ ca = Validation.decimal(ca, name: 'current assets')
44
+ cl = Validation.non_zero_decimal(cl, name: 'current liabilities', error: DomainError)
44
45
 
45
46
  (ca / cl)
46
47
  end
@@ -52,8 +53,8 @@ module Finrb
52
53
  # @example
53
54
  # Finrb::Ratios.debt_ratio(td=6000,ta=20000)
54
55
  def self.debt_ratio(td:, ta:)
55
- td = Flt::DecNum(td.to_s)
56
- ta = Flt::DecNum(ta.to_s)
56
+ td = Validation.decimal(td, name: 'total debt')
57
+ ta = Validation.non_zero_decimal(ta, name: 'total assets', error: DomainError)
57
58
 
58
59
  (td / ta)
59
60
  end
@@ -81,15 +82,15 @@ module Finrb
81
82
  # @example
82
83
  # Finrb::Ratios.diluted_eps(ni=115600,pd=10000,cpd=10000,cdi=42000,tax=0.4,w=200000,cps=40000,cds=60000,iss=2500)
83
84
  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)
85
+ ni = Validation.decimal(ni, name: 'net income')
86
+ pd = Validation.decimal(pd, name: 'preferred dividends')
87
+ w = Validation.positive_decimal(w, name: 'weighted average common shares', error: DomainError)
88
+ cpd = Validation.non_negative_decimal(cpd, name: 'convertible preferred dividends')
89
+ cdi = Validation.non_negative_decimal(cdi, name: 'convertible debt interest')
90
+ tax = Validation.decimal_between(tax, minimum: 0, maximum: 1, name: 'tax rate')
91
+ cps = Validation.non_negative_decimal(cps, name: 'convertible preferred shares')
92
+ cds = Validation.non_negative_decimal(cds, name: 'convertible debt shares')
93
+ iss = Validation.non_negative_decimal(iss, name: 'incremental option shares')
93
94
 
94
95
  basic = (ni - pd) / w
95
96
  diluted = (ni - pd + cpd + (cdi * (1 - tax))) / (w + cps + cds + iss)
@@ -105,9 +106,9 @@ module Finrb
105
106
  # @example
106
107
  # Finrb::Ratios.eps(ni=10000,pd=1000,w=11000)
107
108
  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)
109
+ ni = Validation.decimal(ni, name: 'net income')
110
+ pd = Validation.decimal(pd, name: 'preferred dividends')
111
+ w = Validation.positive_decimal(w, name: 'weighted average common shares', error: DomainError)
111
112
 
112
113
  ((ni - pd) / w)
113
114
  end
@@ -119,8 +120,8 @@ module Finrb
119
120
  # @example
120
121
  # Finrb::Ratios.financial_leverage(te=16000,ta=20000)
121
122
  def self.financial_leverage(te:, ta:)
122
- te = Flt::DecNum(te.to_s)
123
- ta = Flt::DecNum(ta.to_s)
123
+ te = Validation.non_zero_decimal(te, name: 'total equity', error: DomainError)
124
+ ta = Validation.decimal(ta, name: 'total assets')
124
125
 
125
126
  (ta / te)
126
127
  end
@@ -132,8 +133,8 @@ module Finrb
132
133
  # @example
133
134
  # Finrb::Ratios.gpm(gp=1000,rv=20000)
134
135
  def self.gpm(gp:, rv:)
135
- gp = Flt::DecNum(gp.to_s)
136
- rv = Flt::DecNum(rv.to_s)
136
+ gp = Validation.decimal(gp, name: 'gross profit')
137
+ rv = Validation.non_zero_decimal(rv, name: 'revenue', error: DomainError)
137
138
 
138
139
  (gp / rv)
139
140
  end
@@ -146,14 +147,14 @@ module Finrb
146
147
  # @example
147
148
  # Finrb::Ratios.iss(amp=20,ep=15,n=10000)
148
149
  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)
150
+ amp = Validation.positive_decimal(amp, name: 'average market price', error: DomainError)
151
+ ep = Validation.non_negative_decimal(ep, name: 'exercise price')
152
+ n = Validation.non_negative_decimal(n, name: 'option shares')
152
153
 
153
154
  if amp > ep
154
155
  ((amp - ep) * n / amp)
155
156
  else
156
- raise(Error, 'amp must larger than ep')
157
+ raise(DomainError, 'Average market price must be greater than exercise price.')
157
158
  end
158
159
  end
159
160
 
@@ -164,8 +165,8 @@ module Finrb
164
165
  # @example
165
166
  # Finrb::Ratios.lt_d2e(ltd=8000,te=20000)
166
167
  def self.lt_d2e(ltd:, te:)
167
- ltd = Flt::DecNum(ltd.to_s)
168
- te = Flt::DecNum(te.to_s)
168
+ ltd = Validation.decimal(ltd, name: 'long-term debt')
169
+ te = Validation.non_zero_decimal(te, name: 'total equity', error: DomainError)
169
170
 
170
171
  (ltd / te)
171
172
  end
@@ -177,8 +178,8 @@ module Finrb
177
178
  # @example
178
179
  # Finrb::Ratios.npm(ni=8000,rv=20000)
179
180
  def self.npm(ni:, rv:)
180
- ni = Flt::DecNum(ni.to_s)
181
- rv = Flt::DecNum(rv.to_s)
181
+ ni = Validation.decimal(ni, name: 'net income')
182
+ rv = Validation.non_zero_decimal(rv, name: 'revenue', error: DomainError)
182
183
 
183
184
  (ni / rv)
184
185
  end
@@ -192,10 +193,10 @@ module Finrb
192
193
  # @example
193
194
  # Finrb::Ratios.quick_ratio(cash=3000,ms=2000,rc=1000,cl=2000)
194
195
  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)
196
+ cash = Validation.decimal(cash, name: 'cash')
197
+ ms = Validation.decimal(ms, name: 'marketable securities')
198
+ rc = Validation.decimal(rc, name: 'receivables')
199
+ cl = Validation.non_zero_decimal(cl, name: 'current liabilities', error: DomainError)
199
200
 
200
201
  ((cash + ms + rc) / cl)
201
202
  end
@@ -207,8 +208,8 @@ module Finrb
207
208
  # @example
208
209
  # Finrb::Ratios.total_d2e(td=6000,te=20000)
209
210
  def self.total_d2e(td:, te:)
210
- td = Flt::DecNum(td.to_s)
211
- te = Flt::DecNum(te.to_s)
211
+ td = Validation.decimal(td, name: 'total debt')
212
+ te = Validation.non_zero_decimal(te, name: 'total equity', error: DomainError)
212
213
 
213
214
  (td / te)
214
215
  end
@@ -223,8 +224,8 @@ module Finrb
223
224
  # @example
224
225
  # s=[11000,4400,-3000];m=[12,9,4];Finrb::Ratios.was(ns=s,nm=m)
225
226
  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) }
227
+ ns = wrap_array(ns).map { |value| Validation.decimal(value, name: 'share change') }
228
+ nm = wrap_array(nm).map { |value| Validation.decimal_between(value, minimum: 0, maximum: 12, name: 'months outstanding') }
228
229
 
229
230
  m = ns.size
230
231
  n = nm.size
@@ -234,7 +235,7 @@ module Finrb
234
235
  sum += (ns[i] * nm[i])
235
236
  end
236
237
  else
237
- raise(Error, 'length of ns and nm must be equal')
238
+ raise(ArgumentError, 'Share changes and months outstanding must have equal lengths.')
238
239
  end
239
240
  sum /= 12
240
241
  sum
data/lib/finrb/returns.rb CHANGED
@@ -29,12 +29,9 @@ module Finrb
29
29
  # @param periods [Integer] number of equal annual periods
30
30
  # @return [Flt::DecNum] compound growth rate per period
31
31
  def self.cagr(beginning_value:, ending_value:, periods:)
32
- beginning_value = Validation.decimal(beginning_value, name: 'beginning_value')
33
- ending_value = Validation.decimal(ending_value, name: 'ending_value')
34
- periods = Validation.positive_integer(periods, name: 'periods')
35
-
36
- raise(ArgumentError, 'beginning_value must be greater than zero.') unless beginning_value.positive?
37
- raise(ArgumentError, 'ending_value must be greater than or equal to zero.') if ending_value.negative?
32
+ beginning_value = Validation.positive_decimal(beginning_value, name: 'beginning value')
33
+ ending_value = Validation.non_negative_decimal(ending_value, name: 'ending value')
34
+ periods = Validation.positive_integer(periods, name: 'period count')
38
35
 
39
36
  ((ending_value / beginning_value)**(Flt::DecNum(1) / periods)) - 1
40
37
  end
@@ -49,18 +46,16 @@ module Finrb
49
46
 
50
47
  # Compound a periodic return into an annual return.
51
48
  def self.annualize_return(rate:, periods_per_year:)
52
- rate = Validation.decimal(rate, name: 'rate')
53
- periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods_per_year')
54
- raise(ArgumentError, 'rate must be greater than or equal to -1.') if rate < -1
49
+ rate = Validation.decimal_at_least(rate, minimum: -1, name: 'periodic rate')
50
+ periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods per year')
55
51
 
56
52
  ((rate + 1)**periods_per_year) - 1
57
53
  end
58
54
 
59
55
  # Scale periodic volatility by the square root of periods per year.
60
56
  def self.annualize_volatility(volatility:, periods_per_year:)
61
- volatility = Validation.decimal(volatility, name: 'volatility')
62
- periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods_per_year')
63
- raise(ArgumentError, 'volatility must be greater than or equal to zero.') if volatility.negative?
57
+ volatility = Validation.non_negative_decimal(volatility, name: 'volatility')
58
+ periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods per year')
64
59
 
65
60
  volatility * (Flt::DecNum(periods_per_year)**Flt::DecNum('0.5'))
66
61
  end
@@ -103,7 +98,7 @@ module Finrb
103
98
  ratio = ((returns.sum / returns.size) - target) / downside
104
99
  return ratio if periods_per_year.nil?
105
100
 
106
- periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods_per_year')
101
+ periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods per year')
107
102
  ratio * (Flt::DecNum(periods_per_year)**Flt::DecNum('0.5'))
108
103
  end
109
104
 
@@ -126,8 +121,9 @@ module Finrb
126
121
  # @example
127
122
  # Finrb::Returns.coefficient_variation(sd=0.15,avg=0.39)
128
123
  def self.coefficient_variation(sd:, avg:)
129
- sd = Flt::DecNum(sd.to_s)
130
- avg = Flt::DecNum(avg.to_s)
124
+ sd = Validation.non_negative_decimal(sd, name: 'standard deviation')
125
+ avg = Validation.decimal(avg, name: 'average')
126
+ raise(DomainError, 'Average must be non-zero.') if avg.zero?
131
127
 
132
128
  (sd / avg)
133
129
  end
@@ -138,10 +134,13 @@ module Finrb
138
134
  # @example
139
135
  # Finrb::Returns.geometric_mean(r=[-0.0934, 0.2345, 0.0892])
140
136
  def self.geometric_mean(r:)
141
- r = wrap_array(r).map { |value| Flt::DecNum(value.to_s) }
137
+ returns = risk_values(r, name: 'return')
138
+ returns.each do |value|
139
+ raise(DomainError, 'Returns must be greater than or equal to -1.') if value < -1
140
+ end
142
141
 
143
- rs = r.map { |value| value + 1 }
144
- ((rs.reduce(:*)**(Flt::DecNum(1) / rs.size)) - 1)
142
+ growth_factors = returns.map { |value| value + 1 }
143
+ ((growth_factors.reduce(:*)**(Flt::DecNum(1) / growth_factors.size)) - 1)
145
144
  end
146
145
 
147
146
  # harmonic mean, average price
@@ -149,9 +148,10 @@ module Finrb
149
148
  # @example
150
149
  # Finrb::Returns.harmonic_mean(p=[8,9,10])
151
150
  def self.harmonic_mean(p:)
152
- p = wrap_array(p).map { |value| Flt::DecNum(value.to_s) }
151
+ prices = risk_values(p, name: 'price')
152
+ raise(DomainError, 'Prices must be greater than zero.') unless prices.all?(&:positive?)
153
153
 
154
- (Flt::DecNum(1) / (p.sum { |val| Flt::DecNum(1) / val } / p.size))
154
+ (Flt::DecNum(1) / (prices.sum { |price| Flt::DecNum(1) / price } / prices.size))
155
155
  end
156
156
 
157
157
  # Computing HPR, the holding period return
@@ -162,9 +162,9 @@ module Finrb
162
162
  # @example
163
163
  # Finrb::Returns.hpr(ev=33,bv=30,cfr=0.5)
164
164
  def self.hpr(ev:, bv:, cfr: 0)
165
- ev = Flt::DecNum(ev.to_s)
166
- bv = Flt::DecNum(bv.to_s)
167
- cfr = Flt::DecNum(cfr.to_s)
165
+ ev = Validation.decimal(ev, name: 'ending value')
166
+ bv = Validation.positive_decimal(bv, name: 'beginning value', error: DomainError)
167
+ cfr = Validation.decimal(cfr, name: 'cashflow received')
168
168
 
169
169
  ((ev - bv + cfr) / bv)
170
170
  end
@@ -176,8 +176,8 @@ module Finrb
176
176
  # @example
177
177
  # Finrb::Returns.sampling_error(sm=0.45, mu=0.5)
178
178
  def self.sampling_error(sm:, mu:)
179
- sm = Flt::DecNum(sm.to_s)
180
- mu = Flt::DecNum(mu.to_s)
179
+ sm = Validation.decimal(sm, name: 'sample mean')
180
+ mu = Validation.decimal(mu, name: 'population mean')
181
181
 
182
182
  (sm - mu)
183
183
  end
@@ -190,9 +190,9 @@ module Finrb
190
190
  # @example
191
191
  # Finrb::Returns.sf_ratio(rp=0.09,rl=0.03,sd=0.12)
192
192
  def self.sf_ratio(rp:, rl:, sd:)
193
- rp = Flt::DecNum(rp.to_s)
194
- rl = Flt::DecNum(rl.to_s)
195
- sd = Flt::DecNum(sd.to_s)
193
+ rp = Validation.decimal(rp, name: 'portfolio return')
194
+ rl = Validation.decimal(rl, name: 'threshold return')
195
+ sd = Validation.positive_decimal(sd, name: 'standard deviation', error: DomainError)
196
196
 
197
197
  ((rp - rl) / sd)
198
198
  end
@@ -205,9 +205,9 @@ module Finrb
205
205
  # @example
206
206
  # Finrb::Returns.sharpe_ratio(rp=0.038,rf=0.015,sd=0.07)
207
207
  def self.sharpe_ratio(rp:, rf:, sd:)
208
- rp = Flt::DecNum(rp.to_s)
209
- rf = Flt::DecNum(rf.to_s)
210
- sd = Flt::DecNum(sd.to_s)
208
+ rp = Validation.decimal(rp, name: 'portfolio return')
209
+ rf = Validation.decimal(rf, name: 'risk-free return')
210
+ sd = Validation.positive_decimal(sd, name: 'standard deviation', error: DomainError)
211
211
 
212
212
  ((rp - rf) / sd)
213
213
  end
@@ -220,22 +220,20 @@ module Finrb
220
220
  # @example
221
221
  # Finrb::Returns.twrr(ev=[120,260],bv=[100,240],cfr=[2,4])
222
222
  def self.twrr(ev:, bv:, cfr:)
223
- ev = wrap_array(ev).map { |value| Flt::DecNum(value.to_s) }
224
- bv = wrap_array(bv).map { |value| Flt::DecNum(value.to_s) }
225
- cfr = wrap_array(cfr).map { |value| Flt::DecNum(value.to_s) }
226
-
227
- r = ev.size
228
- s = bv.size
229
- t = cfr.size
230
- wr = Flt::DecNum(1)
231
- if r != s || r != t || s != t
232
- raise(Error, 'Different number of values!')
233
- else
234
- (0...r).each do |i|
235
- wr *= (Finrb::Returns.hpr(ev: ev[i], bv: bv[i], cfr: cfr[i]) + 1)
223
+ ending_values = risk_values(ev, name: 'ending value')
224
+ beginning_values = risk_values(bv, name: 'beginning value')
225
+ cashflows_received = risk_values(cfr, name: 'cashflow received')
226
+ sizes = [ending_values.size, beginning_values.size, cashflows_received.size]
227
+ raise(ArgumentError, 'Ending values, beginning values, and cashflows received must have equal lengths.') unless sizes.uniq.one?
228
+
229
+ wealth_relative =
230
+ ending_values.each_index.reduce(Flt::DecNum(1)) do |product, index|
231
+ period_growth = hpr(ev: ending_values[index], bv: beginning_values[index], cfr: cashflows_received[index]) + 1
232
+ raise(DomainError, 'Each subperiod wealth relative must be greater than or equal to zero.') if period_growth.negative?
233
+
234
+ product * period_growth
236
235
  end
237
- ((wr**(Flt::DecNum(1) / r)) - 1)
238
- end
236
+ (wealth_relative**(Flt::DecNum(1) / ending_values.size)) - 1
239
237
  end
240
238
 
241
239
  # Weighted mean as a portfolio return
@@ -245,13 +243,12 @@ module Finrb
245
243
  # @example
246
244
  # Finrb::Returns.wpr(r=[0.12, 0.07, 0.03],w=[0.5,0.4,0.1])
247
245
  def self.wpr(r:, w:)
248
- r = wrap_array(r).map { |value| Flt::DecNum(value.to_s) }
249
- w = wrap_array(w).map { |value| Flt::DecNum(value.to_s) }
250
-
251
- # TODO: need to change
252
- puts('sum of weights is NOT equal to 1!') if w.sum != 1
246
+ returns = risk_values(r, name: 'return')
247
+ weights = risk_values(w, name: 'weight')
248
+ raise(ArgumentError, 'Returns and weights must have equal lengths.') unless returns.size == weights.size
249
+ raise(ArgumentError, 'Weights must sum to 1.') unless weights.sum == 1
253
250
 
254
- r.zip(w).sum { |arr| arr.reduce(:*) }
251
+ returns.zip(weights).sum { |rate, weight| rate * weight }
255
252
  end
256
253
  end
257
254
  end
data/lib/finrb/tvm.rb CHANGED
@@ -4,123 +4,225 @@ require_relative 'config'
4
4
  require_relative 'decimal'
5
5
  require_relative 'errors'
6
6
  require_relative 'numerical/brent'
7
+ require_relative 'numerical/rate_search'
8
+ require_relative 'validation'
7
9
 
8
10
  module Finrb
9
11
  # Time-value-of-money calculations for periodic rates and cashflows.
10
12
  module TVM
11
13
  module_function
12
14
 
13
- def discount_rate(n:, pv:, fv:, pmt:, type: 0, lower: 0.0001, upper: 100)
14
- n, pv, fv, pmt, type, lower, upper = decimals(n, pv, fv, pmt, type, lower, upper)
15
+ UNSET_BOUND = Object.new.freeze
16
+ private_constant :UNSET_BOUND
17
+ INPUT_NAMES = { fv: 'future value', g: 'growth rate', guess: 'rate guess', lower: 'lower rate bound', n: 'period count', pmt: 'payment', pv: 'present value', r: 'periodic rate', upper: 'upper rate bound' }.freeze
18
+ private_constant :INPUT_NAMES
19
+
20
+ def discount_rate(n:, pv:, fv:, pmt:, type: 0, guess: nil, lower: UNSET_BOUND, upper: UNSET_BOUND)
21
+ n = period_count(n)
22
+ pv, fv, pmt = decimal_inputs(pv:, fv:, pmt:).values
23
+ type = payment_type(type)
15
24
  function = ->(rate) { fv_simple(r: rate, n:, pv:) + fv_annuity(r: rate, n:, pmt:, type:) - fv }
16
25
 
17
- Numerical::Brent.new(tolerance: Finrb.config.eps).solve(function, lower:, upper:)
26
+ bounds = rate_bounds(function, guess:, lower:, upper:)
27
+ return bounds.first if bounds.first == bounds.last
28
+
29
+ Numerical::Brent.new(tolerance: Finrb.config.eps).solve(function, lower: bounds.first, upper: bounds.last)
18
30
  end
19
31
 
20
32
  def fv(r:, n:, pv: 0, pmt: 0, type: 0)
21
- r, n, pv, pmt, type = decimals(r, n, pv, pmt, type)
22
- validate_payment_type!(type)
33
+ rate = periodic_rate(r)
34
+ periods = period_count(n)
35
+ payment_type(type)
23
36
 
24
- fv_simple(r:, n:, pv:) + fv_annuity(r:, n:, pmt:, type:)
37
+ fv_simple(r: rate, n: periods, pv:) + fv_annuity(r: rate, n: periods, pmt:, type:)
25
38
  end
26
39
 
27
40
  def fv_annuity(r:, n:, pmt:, type: 0)
28
- r, n, pmt, type = decimals(r, n, pmt, type)
29
- validate_payment_type!(type)
41
+ rate = periodic_rate(r)
42
+ periods = period_count(n)
43
+ payment = Validation.decimal(pmt, name: 'payment')
44
+ payment_timing = payment_type(type)
45
+ return -payment * periods if rate.zero?
30
46
 
31
- (pmt / r * (((r + 1)**n) - 1)) * ((r + 1)**type) * -1
47
+ (payment / rate * (((rate + 1)**periods) - 1)) * ((rate + 1)**payment_timing) * -1
32
48
  end
33
49
 
34
50
  def fv_simple(r:, n:, pv:)
35
- r, n, pv = decimals(r, n, pv)
36
- (pv * ((r + 1)**n)) * -1
51
+ rate = periodic_rate(r)
52
+ periods = period_count(n)
53
+ present_value = Validation.decimal(pv, name: 'present value')
54
+ (present_value * ((rate + 1)**periods)) * -1
37
55
  end
38
56
 
39
57
  def fv_uneven(r:, cf:)
40
- r = Flt::DecNum(r.to_s)
41
- cashflows = array(cf).map { |value| Flt::DecNum(value.to_s) }
58
+ rate = periodic_rate(r)
59
+ cashflows = cashflow_values(cf)
42
60
 
43
61
  cashflows.each_with_index.sum do |cashflow, index|
44
- fv_simple(r:, n: cashflows.size - index - 1, pv: cashflow)
62
+ fv_simple(r: rate, n: cashflows.size - index - 1, pv: cashflow)
45
63
  end
46
64
  end
47
65
 
48
66
  def n_period(r:, pv:, fv:, pmt:, type: 0)
49
- r, pv, fv, pmt, type = decimals(r, pv, fv, pmt, type)
50
- validate_payment_type!(type)
67
+ rate = periodic_rate(r)
68
+ values = decimal_inputs(pv:, fv:, pmt:)
69
+ payment_timing = payment_type(type)
70
+
71
+ return zero_rate_periods(**values) if rate.zero?
72
+
73
+ numerator = ((values[:fv] * rate) - (values[:pmt] * ((rate + 1)**payment_timing))) * -1
74
+ denominator = (values[:pv] * rate) + (values[:pmt] * ((rate + 1)**payment_timing))
75
+ periods = (numerator / denominator).log / (rate + 1).log
76
+ raise(DomainError, 'Inputs do not produce a finite non-negative period count.') unless periods.finite? && !periods.negative?
51
77
 
52
- numerator = ((fv * r) - (pmt * ((r + 1)**type))) * -1
53
- denominator = (pv * r) + (pmt * ((r + 1)**type))
54
- (numerator / denominator).log / (r + 1).log
78
+ periods
79
+ rescue Flt::Num::Exception, Math::DomainError, ZeroDivisionError => e
80
+ raise(DomainError, "Inputs do not produce a real period count: #{e.message}", e.backtrace)
55
81
  end
56
82
 
57
83
  def npv(r:, cf:)
58
- cashflows = array(cf).map { |value| Flt::DecNum(value.to_s) }
59
- (pv_uneven(r:, cf: cashflows.drop(1)) * -1) + cashflows.first
84
+ rate = periodic_rate(r)
85
+ cashflows = cashflow_values(cf)
86
+ return cashflows.first if cashflows.one?
87
+
88
+ (pv_uneven(r: rate, cf: cashflows.drop(1)) * -1) + cashflows.first
60
89
  end
61
90
 
62
91
  def pmt(r:, n:, pv:, fv:, type: 0)
63
- r, n, pv, fv, type = decimals(r, n, pv, fv, type)
64
- validate_payment_type!(type)
92
+ rate = periodic_rate(r)
93
+ periods = positive_period_count(n)
94
+ values = decimal_inputs(pv:, fv:)
95
+ payment_timing = payment_type(type)
96
+ return -(values[:pv] + values[:fv]) / periods if rate.zero?
65
97
 
66
- (pv + (fv / ((r + 1)**n))) * r / (1 - (Flt::DecNum(1) / ((r + 1)**n))) * -1 * ((r + 1)**(type * -1))
98
+ (values[:pv] + (values[:fv] / ((rate + 1)**periods))) * rate / (1 - (Flt::DecNum(1) / ((rate + 1)**periods))) * -1 * ((rate + 1)**(payment_timing * -1))
67
99
  end
68
100
 
69
101
  def pv(r:, n:, fv: 0, pmt: 0, type: 0)
70
- r, n, fv, pmt, type = decimals(r, n, fv, pmt, type)
71
- validate_payment_type!(type)
102
+ rate = periodic_rate(r)
103
+ periods = period_count(n)
104
+ payment_type(type)
72
105
 
73
- pv_simple(r:, n:, fv:) + pv_annuity(r:, n:, pmt:, type:)
106
+ pv_simple(r: rate, n: periods, fv:) + pv_annuity(r: rate, n: periods, pmt:, type:)
74
107
  end
75
108
 
76
109
  def pv_annuity(r:, n:, pmt:, type: 0)
77
- r, n, pmt, type = decimals(r, n, pmt, type)
78
- validate_payment_type!(type)
110
+ rate = periodic_rate(r)
111
+ periods = period_count(n)
112
+ payment = Validation.decimal(pmt, name: 'payment')
113
+ payment_timing = payment_type(type)
114
+ return -payment * periods if rate.zero?
79
115
 
80
- (pmt / r * (1 - (Flt::DecNum(1) / ((r + 1)**n)))) * ((r + 1)**type) * -1
116
+ (payment / rate * (1 - (Flt::DecNum(1) / ((rate + 1)**periods)))) * ((rate + 1)**payment_timing) * -1
81
117
  end
82
118
 
83
119
  def pv_perpetuity(r:, pmt:, g: 0, type: 0)
84
- r, pmt, g, type = decimals(r, pmt, g, type)
85
- validate_payment_type!(type)
86
- raise(Error, 'Error: g is not smaller than r!') if g >= r
120
+ rate = periodic_rate(r)
121
+ payment = Validation.decimal(pmt, name: 'payment')
122
+ growth = periodic_rate(g, name: :g)
123
+ payment_timing = payment_type(type)
124
+ raise(DomainError, 'Growth rate must be smaller than the discount rate.') if growth >= rate
87
125
 
88
- (pmt / (r - g)) * ((r + 1)**type) * -1
126
+ (payment / (rate - growth)) * ((rate + 1)**payment_timing) * -1
89
127
  end
90
128
 
91
129
  def pv_simple(r:, n:, fv:)
92
- r, n, fv = decimals(r, n, fv)
93
- (fv / ((r + 1)**n)) * -1
130
+ rate = periodic_rate(r)
131
+ periods = period_count(n)
132
+ future_value = Validation.decimal(fv, name: 'future value')
133
+ (future_value / ((rate + 1)**periods)) * -1
94
134
  end
95
135
 
96
136
  def pv_uneven(r:, cf:)
97
- r = Flt::DecNum(r.to_s)
98
- array(cf).each_with_index.sum do |cashflow, index|
99
- pv_simple(r:, n: index + 1, fv: cashflow)
137
+ rate = periodic_rate(r)
138
+ cashflow_values(cf).each_with_index.sum do |cashflow, index|
139
+ pv_simple(r: rate, n: index + 1, fv: cashflow)
100
140
  end
101
141
  end
102
142
 
103
143
  def r_perpetuity(pmt:, pv:)
104
- pmt, pv = decimals(pmt, pv)
105
- pmt * -1 / pv
144
+ payment = Validation.decimal(pmt, name: 'payment')
145
+ present_value = Validation.decimal(pv, name: 'present value')
146
+ raise(DomainError, 'Present value must be non-zero.') if present_value.zero?
147
+
148
+ payment * -1 / present_value
106
149
  end
107
150
 
108
- def array(value)
109
- return [] if value.nil?
110
- return value.to_ary || [value] if value.respond_to?(:to_ary)
151
+ def cashflow_values(value)
152
+ values =
153
+ if value.nil?
154
+ []
155
+ elsif value.respond_to?(:to_ary)
156
+ value.to_ary || [value]
157
+ else
158
+ [value]
159
+ end
160
+ raise(ArgumentError, 'cashflows cannot be empty.') if values.empty?
161
+
162
+ values.map { |cashflow| Validation.decimal(cashflow, name: 'cashflow') }
163
+ end
164
+ private_class_method :cashflow_values
165
+
166
+ def decimal_inputs(**values)
167
+ values.to_h { |name, value| [name, Validation.decimal(value, name: input_name(name))] }
168
+ end
169
+ private_class_method :decimal_inputs
170
+
171
+ def payment_type(value)
172
+ value = Validation.decimal(value, name: 'payment timing type')
173
+ raise(ArgumentError, 'payment timing type must be 0 (end) or 1 (beginning).') unless [Flt::DecNum(0), Flt::DecNum(1)].include?(value)
174
+
175
+ value
176
+ end
177
+ private_class_method :payment_type
111
178
 
112
- [value]
179
+ def period_count(value)
180
+ Validation.non_negative_decimal(value, name: 'period count', error: DomainError)
113
181
  end
114
- private_class_method :array
182
+ private_class_method :period_count
115
183
 
116
- def decimals(*values)
117
- values.map { |value| Flt::DecNum(value.to_s) }
184
+ def positive_period_count(value)
185
+ Validation.positive_decimal(value, name: 'period count', error: DomainError)
118
186
  end
119
- private_class_method :decimals
187
+ private_class_method :positive_period_count
188
+
189
+ def periodic_rate(value, name: :r)
190
+ Validation.decimal_greater_than(value, minimum: -1, name: input_name(name), error: DomainError)
191
+ end
192
+ private_class_method :periodic_rate
193
+
194
+ def input_name(name)
195
+ INPUT_NAMES.fetch(name, name.to_s.tr('_', ' '))
196
+ end
197
+ private_class_method :input_name
198
+
199
+ def rate_bounds(function, guess:, lower:, upper:)
200
+ return searched_rate_bounds(function, guess) if lower.equal?(UNSET_BOUND) && upper.equal?(UNSET_BOUND)
201
+
202
+ lower = '0.0001' if lower.equal?(UNSET_BOUND)
203
+ upper = 100 if upper.equal?(UNSET_BOUND)
204
+ lower = periodic_rate(lower, name: :lower)
205
+ upper = periodic_rate(upper, name: :upper)
206
+ raise(ArgumentError, 'lower rate bound must be less than upper rate bound.') if lower >= upper
207
+
208
+ [lower, upper]
209
+ end
210
+ private_class_method :rate_bounds
211
+
212
+ def searched_rate_bounds(function, guess)
213
+ guess = Finrb.config.guess if guess.nil?
214
+ Numerical::RateSearch.new.bracket(function, guess: periodic_rate(guess, name: :guess))
215
+ end
216
+ private_class_method :searched_rate_bounds
217
+
218
+ def zero_rate_periods(pv:, fv:, pmt:)
219
+ raise(DomainError, 'pmt must be non-zero when solving periods at a zero rate.') if pmt.zero?
220
+
221
+ periods = (-pv - fv) / pmt
222
+ raise(DomainError, 'Inputs do not produce a non-negative period count.') if periods.negative?
120
223
 
121
- def validate_payment_type!(type)
122
- raise(Error, 'Error: type should be 0 or 1!') unless [Flt::DecNum(0), Flt::DecNum(1)].include?(type)
224
+ periods
123
225
  end
124
- private_class_method :validate_payment_type!
226
+ private_class_method :zero_rate_periods
125
227
  end
126
228
  end