finrb 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -0
- data/CONTRIBUTING.md +89 -0
- data/README.md +58 -2
- data/SECURITY.md +41 -0
- data/lib/finrb/accounting.rb +93 -74
- data/lib/finrb/amortization.rb +145 -44
- data/lib/finrb/cashflows.rb +32 -9
- data/lib/finrb/config.rb +5 -5
- data/lib/finrb/rates.rb +3 -21
- data/lib/finrb/ratios.rb +41 -40
- data/lib/finrb/returns.rb +138 -39
- data/lib/finrb/transaction.rb +0 -11
- data/lib/finrb/tvm.rb +154 -52
- data/lib/finrb/validation.rb +42 -0
- data/lib/finrb/version.rb +1 -1
- data/lib/finrb/yields.rb +42 -31
- data/sig/finrb.rbs +39 -4
- metadata +17 -1
data/lib/finrb/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 =
|
|
29
|
-
ms =
|
|
30
|
-
cl =
|
|
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 =
|
|
43
|
-
cl =
|
|
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 =
|
|
56
|
-
ta =
|
|
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 =
|
|
85
|
-
pd =
|
|
86
|
-
w =
|
|
87
|
-
cpd =
|
|
88
|
-
cdi =
|
|
89
|
-
tax =
|
|
90
|
-
cps =
|
|
91
|
-
cds =
|
|
92
|
-
iss =
|
|
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 =
|
|
109
|
-
pd =
|
|
110
|
-
w =
|
|
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 =
|
|
123
|
-
ta =
|
|
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 =
|
|
136
|
-
rv =
|
|
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 =
|
|
150
|
-
ep =
|
|
151
|
-
n =
|
|
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(
|
|
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 =
|
|
168
|
-
te =
|
|
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 =
|
|
181
|
-
rv =
|
|
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 =
|
|
196
|
-
ms =
|
|
197
|
-
rc =
|
|
198
|
-
cl =
|
|
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 =
|
|
211
|
-
te =
|
|
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|
|
|
227
|
-
nm = wrap_array(nm).map { |value|
|
|
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(
|
|
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
|
@@ -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
|
# Investment return and risk-adjusted performance calculations.
|
|
@@ -17,6 +18,102 @@ module Finrb
|
|
|
17
18
|
end
|
|
18
19
|
private_class_method :wrap_array
|
|
19
20
|
|
|
21
|
+
# Compound annual growth rate over a positive number of periods.
|
|
22
|
+
#
|
|
23
|
+
# Beginning value must be positive. Ending value may be zero, representing
|
|
24
|
+
# a total loss, but cannot be negative because a fractional growth root
|
|
25
|
+
# would not have a generally meaningful real-valued result.
|
|
26
|
+
#
|
|
27
|
+
# @param beginning_value [Numeric] value at the start of the measurement
|
|
28
|
+
# @param ending_value [Numeric] value at the end of the measurement
|
|
29
|
+
# @param periods [Integer] number of equal annual periods
|
|
30
|
+
# @return [Flt::DecNum] compound growth rate per period
|
|
31
|
+
def self.cagr(beginning_value:, ending_value:, periods:)
|
|
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')
|
|
35
|
+
|
|
36
|
+
((ending_value / beginning_value)**(Flt::DecNum(1) / periods)) - 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.risk_values(values, name:)
|
|
40
|
+
values = wrap_array(values)
|
|
41
|
+
raise(ArgumentError, "#{name} cannot be empty.") if values.empty?
|
|
42
|
+
|
|
43
|
+
values.map { |value| Validation.decimal(value, name:) }
|
|
44
|
+
end
|
|
45
|
+
private_class_method :risk_values
|
|
46
|
+
|
|
47
|
+
# Compound a periodic return into an annual return.
|
|
48
|
+
def self.annualize_return(rate:, periods_per_year:)
|
|
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')
|
|
51
|
+
|
|
52
|
+
((rate + 1)**periods_per_year) - 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Scale periodic volatility by the square root of periods per year.
|
|
56
|
+
def self.annualize_volatility(volatility:, periods_per_year:)
|
|
57
|
+
volatility = Validation.non_negative_decimal(volatility, name: 'volatility')
|
|
58
|
+
periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods per year')
|
|
59
|
+
|
|
60
|
+
volatility * (Flt::DecNum(periods_per_year)**Flt::DecNum('0.5'))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Standard deviation of periodic returns. Sample volatility uses n - 1;
|
|
64
|
+
# population volatility uses n.
|
|
65
|
+
def self.volatility(returns:, sample: true)
|
|
66
|
+
raise(ArgumentError, 'sample must be true or false.') unless [true, false].include?(sample)
|
|
67
|
+
|
|
68
|
+
returns = risk_values(returns, name: 'return')
|
|
69
|
+
raise(ArgumentError, 'sample volatility requires at least two returns.') if sample && returns.size < 2
|
|
70
|
+
|
|
71
|
+
mean = returns.sum / returns.size
|
|
72
|
+
denominator = sample ? returns.size - 1 : returns.size
|
|
73
|
+
variance = returns.sum { |value| (value - mean)**2 } / denominator
|
|
74
|
+
variance**Flt::DecNum('0.5')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Root-mean-square return shortfall below a target return. The denominator
|
|
78
|
+
# includes every observation, including returns at or above the target.
|
|
79
|
+
def self.downside_deviation(returns:, target: 0)
|
|
80
|
+
returns = risk_values(returns, name: 'return')
|
|
81
|
+
target = Validation.decimal(target, name: 'target')
|
|
82
|
+
squared_shortfalls =
|
|
83
|
+
returns.sum do |value|
|
|
84
|
+
shortfall = [value - target, Flt::DecNum(0)].min
|
|
85
|
+
shortfall**2
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
(squared_shortfalls / returns.size)**Flt::DecNum('0.5')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Sortino ratio using arithmetic mean excess return and downside deviation.
|
|
92
|
+
def self.sortino_ratio(returns:, target: 0, periods_per_year: nil)
|
|
93
|
+
returns = risk_values(returns, name: 'return')
|
|
94
|
+
target = Validation.decimal(target, name: 'target')
|
|
95
|
+
downside = downside_deviation(returns:, target:)
|
|
96
|
+
raise(ArgumentError, 'downside deviation must be greater than zero.') if downside.zero?
|
|
97
|
+
|
|
98
|
+
ratio = ((returns.sum / returns.size) - target) / downside
|
|
99
|
+
return ratio if periods_per_year.nil?
|
|
100
|
+
|
|
101
|
+
periods_per_year = Validation.positive_integer(periods_per_year, name: 'periods per year')
|
|
102
|
+
ratio * (Flt::DecNum(periods_per_year)**Flt::DecNum('0.5'))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Largest peak-to-trough decline as a non-negative fraction.
|
|
106
|
+
def self.max_drawdown(values:)
|
|
107
|
+
values = risk_values(values, name: 'value')
|
|
108
|
+
raise(ArgumentError, 'values must be greater than zero.') unless values.all?(&:positive?)
|
|
109
|
+
|
|
110
|
+
peak = values.first
|
|
111
|
+
values.reduce(Flt::DecNum(0)) do |maximum, value|
|
|
112
|
+
peak = value if value > peak
|
|
113
|
+
[maximum, (peak - value) / peak].max
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
20
117
|
# Computing Coefficient of variation
|
|
21
118
|
#
|
|
22
119
|
# @param sd standard deviation
|
|
@@ -24,8 +121,9 @@ module Finrb
|
|
|
24
121
|
# @example
|
|
25
122
|
# Finrb::Returns.coefficient_variation(sd=0.15,avg=0.39)
|
|
26
123
|
def self.coefficient_variation(sd:, avg:)
|
|
27
|
-
sd =
|
|
28
|
-
avg =
|
|
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?
|
|
29
127
|
|
|
30
128
|
(sd / avg)
|
|
31
129
|
end
|
|
@@ -36,10 +134,13 @@ module Finrb
|
|
|
36
134
|
# @example
|
|
37
135
|
# Finrb::Returns.geometric_mean(r=[-0.0934, 0.2345, 0.0892])
|
|
38
136
|
def self.geometric_mean(r:)
|
|
39
|
-
|
|
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
|
|
40
141
|
|
|
41
|
-
|
|
42
|
-
((
|
|
142
|
+
growth_factors = returns.map { |value| value + 1 }
|
|
143
|
+
((growth_factors.reduce(:*)**(Flt::DecNum(1) / growth_factors.size)) - 1)
|
|
43
144
|
end
|
|
44
145
|
|
|
45
146
|
# harmonic mean, average price
|
|
@@ -47,9 +148,10 @@ module Finrb
|
|
|
47
148
|
# @example
|
|
48
149
|
# Finrb::Returns.harmonic_mean(p=[8,9,10])
|
|
49
150
|
def self.harmonic_mean(p:)
|
|
50
|
-
|
|
151
|
+
prices = risk_values(p, name: 'price')
|
|
152
|
+
raise(DomainError, 'Prices must be greater than zero.') unless prices.all?(&:positive?)
|
|
51
153
|
|
|
52
|
-
(Flt::DecNum(1) / (
|
|
154
|
+
(Flt::DecNum(1) / (prices.sum { |price| Flt::DecNum(1) / price } / prices.size))
|
|
53
155
|
end
|
|
54
156
|
|
|
55
157
|
# Computing HPR, the holding period return
|
|
@@ -60,9 +162,9 @@ module Finrb
|
|
|
60
162
|
# @example
|
|
61
163
|
# Finrb::Returns.hpr(ev=33,bv=30,cfr=0.5)
|
|
62
164
|
def self.hpr(ev:, bv:, cfr: 0)
|
|
63
|
-
ev =
|
|
64
|
-
bv =
|
|
65
|
-
cfr =
|
|
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')
|
|
66
168
|
|
|
67
169
|
((ev - bv + cfr) / bv)
|
|
68
170
|
end
|
|
@@ -74,8 +176,8 @@ module Finrb
|
|
|
74
176
|
# @example
|
|
75
177
|
# Finrb::Returns.sampling_error(sm=0.45, mu=0.5)
|
|
76
178
|
def self.sampling_error(sm:, mu:)
|
|
77
|
-
sm =
|
|
78
|
-
mu =
|
|
179
|
+
sm = Validation.decimal(sm, name: 'sample mean')
|
|
180
|
+
mu = Validation.decimal(mu, name: 'population mean')
|
|
79
181
|
|
|
80
182
|
(sm - mu)
|
|
81
183
|
end
|
|
@@ -88,9 +190,9 @@ module Finrb
|
|
|
88
190
|
# @example
|
|
89
191
|
# Finrb::Returns.sf_ratio(rp=0.09,rl=0.03,sd=0.12)
|
|
90
192
|
def self.sf_ratio(rp:, rl:, sd:)
|
|
91
|
-
rp =
|
|
92
|
-
rl =
|
|
93
|
-
sd =
|
|
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)
|
|
94
196
|
|
|
95
197
|
((rp - rl) / sd)
|
|
96
198
|
end
|
|
@@ -103,9 +205,9 @@ module Finrb
|
|
|
103
205
|
# @example
|
|
104
206
|
# Finrb::Returns.sharpe_ratio(rp=0.038,rf=0.015,sd=0.07)
|
|
105
207
|
def self.sharpe_ratio(rp:, rf:, sd:)
|
|
106
|
-
rp =
|
|
107
|
-
rf =
|
|
108
|
-
sd =
|
|
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)
|
|
109
211
|
|
|
110
212
|
((rp - rf) / sd)
|
|
111
213
|
end
|
|
@@ -118,22 +220,20 @@ module Finrb
|
|
|
118
220
|
# @example
|
|
119
221
|
# Finrb::Returns.twrr(ev=[120,260],bv=[100,240],cfr=[2,4])
|
|
120
222
|
def self.twrr(ev:, bv:, cfr:)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
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
|
|
134
235
|
end
|
|
135
|
-
|
|
136
|
-
end
|
|
236
|
+
(wealth_relative**(Flt::DecNum(1) / ending_values.size)) - 1
|
|
137
237
|
end
|
|
138
238
|
|
|
139
239
|
# Weighted mean as a portfolio return
|
|
@@ -143,13 +243,12 @@ module Finrb
|
|
|
143
243
|
# @example
|
|
144
244
|
# Finrb::Returns.wpr(r=[0.12, 0.07, 0.03],w=[0.5,0.4,0.1])
|
|
145
245
|
def self.wpr(r:, w:)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
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
|
|
151
250
|
|
|
152
|
-
|
|
251
|
+
returns.zip(weights).sum { |rate, weight| rate * weight }
|
|
153
252
|
end
|
|
154
253
|
end
|
|
155
254
|
end
|
data/lib/finrb/transaction.rb
CHANGED
|
@@ -4,17 +4,13 @@ require_relative 'validation'
|
|
|
4
4
|
|
|
5
5
|
module Finrb
|
|
6
6
|
# the Transaction class provides a general interface for working with individual cash flows.
|
|
7
|
-
# @api public
|
|
8
7
|
class Transaction
|
|
9
8
|
# @return [Flt::DecNum] the cash value of the transaction
|
|
10
|
-
# @api public
|
|
11
9
|
attr_reader :amount
|
|
12
10
|
# @return [Integer] the period number of the transaction
|
|
13
11
|
# @note this attribute is mainly used in the case of mortgage amortization with no dates
|
|
14
|
-
# @api public
|
|
15
12
|
attr_reader :period
|
|
16
13
|
# @return [Date] the date of the transaction
|
|
17
|
-
# @api public
|
|
18
14
|
attr_reader :date
|
|
19
15
|
|
|
20
16
|
# create a new Transaction
|
|
@@ -26,7 +22,6 @@ module Finrb
|
|
|
26
22
|
# t = Transaction.new(400)
|
|
27
23
|
# @example a transaction with a period number
|
|
28
24
|
# t = Transaction.new(400, :period => 3)
|
|
29
|
-
# @api public
|
|
30
25
|
def initialize(amount, opts = {})
|
|
31
26
|
raise(ArgumentError, 'options must be a Hash.') unless opts.is_a?(Hash)
|
|
32
27
|
raise(ArgumentError, 'options may only contain date and period.') unless (opts.keys - %i[date period]).empty?
|
|
@@ -47,7 +42,6 @@ module Finrb
|
|
|
47
42
|
# t = Transaction.new(500)
|
|
48
43
|
# t.amount = 750
|
|
49
44
|
# t.amount #=> 750
|
|
50
|
-
# @api public
|
|
51
45
|
def amount=(value)
|
|
52
46
|
@amount = Validation.decimal(value, name: 'amount')
|
|
53
47
|
end
|
|
@@ -71,7 +65,6 @@ module Finrb
|
|
|
71
65
|
# t = Transaction.new(500)
|
|
72
66
|
# t.amount = 750
|
|
73
67
|
# t.difference #=> Flt::DecNum('250')
|
|
74
|
-
# @api public
|
|
75
68
|
def difference
|
|
76
69
|
@amount - @original
|
|
77
70
|
end
|
|
@@ -82,12 +75,10 @@ module Finrb
|
|
|
82
75
|
# int = Interest.new(500)
|
|
83
76
|
# pmt.interest? #=> False
|
|
84
77
|
# int.interest? #=> True
|
|
85
|
-
# @api public
|
|
86
78
|
def interest?
|
|
87
79
|
instance_of?(Interest)
|
|
88
80
|
end
|
|
89
81
|
|
|
90
|
-
# @api public
|
|
91
82
|
def inspect
|
|
92
83
|
"Transaction(#{@amount.round(2)}, date: #{@date})"
|
|
93
84
|
end
|
|
@@ -99,7 +90,6 @@ module Finrb
|
|
|
99
90
|
# pmt = Payment.new(-500)
|
|
100
91
|
# pmt.modify { |t| t.amount-100 }
|
|
101
92
|
# pmt.amount #=> -600
|
|
102
|
-
# @api public
|
|
103
93
|
def modify
|
|
104
94
|
self.amount = yield(self)
|
|
105
95
|
end
|
|
@@ -116,7 +106,6 @@ module Finrb
|
|
|
116
106
|
# int = Interest.new(500)
|
|
117
107
|
# pmt.payment? #=> True
|
|
118
108
|
# int.payment? #=> False
|
|
119
|
-
# @api public
|
|
120
109
|
def payment?
|
|
121
110
|
instance_of?(Payment)
|
|
122
111
|
end
|