financial_calculator 2.1.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +7 -23
- data/financial_calculator.gemspec +3 -2
- data/lib/financial_calculator.rb +10 -6
- data/lib/financial_calculator/amortization.rb +0 -2
- data/lib/financial_calculator/irr.rb +100 -0
- data/lib/financial_calculator/npv.rb +44 -0
- data/lib/financial_calculator/pv.rb +85 -0
- data/lib/financial_calculator/rates.rb +0 -2
- data/lib/financial_calculator/transaction.rb +0 -2
- data/lib/financial_calculator/version.rb +1 -1
- data/lib/financial_calculator/xirr.rb +124 -0
- data/lib/financial_calculator/xnpv.rb +91 -0
- data/spec/irr_spec.rb +44 -0
- data/spec/npv_spec.rb +39 -0
- data/spec/pv_spec.rb +127 -0
- data/spec/spec_helper.rb +10 -9
- data/spec/xirr_spec.rb +73 -0
- data/spec/xnpv_spec.rb +81 -0
- metadata +33 -10
- data/HISTORY +0 -47
- data/lib/financial_calculator/cashflows.rb +0 -127
- data/lib/financial_calculator/decimal.rb +0 -21
- data/spec/cashflows_spec.rb +0 -66
- data/spec/decimal_spec.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f0d684ffcf8cd34cdad09576834da2057e6879b45af1484123474b6e151baad
|
4
|
+
data.tar.gz: eccae432dc2cca2470cf1023871c0d9cb58620b080715c8ed00c818a62e4d2b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5715ae679ff1cc465004bf136989bdee35ce1d60c3c1a21cf6daac07ec4ecbe71f1db8f04ff26d8b3e57a525394e7508601c96be5c5923e443b6504014c94325
|
7
|
+
data.tar.gz: 449cdc6640b282aea1e34a3de372717986232474310101bcf4097ecbbfac6021087aa26d1b4b429ab017239f40bd337c993f91675f3f7f9823920a1243c7bc70
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
5
|
+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
|
9
|
+
## [3.0.0] - 2018-05-12
|
10
|
+
### Added
|
11
|
+
- New class for Present Value calculation (FinancialCalculator::PV)
|
12
|
+
|
13
|
+
### Removed
|
14
|
+
- Monkey patched cashflow methods on Array
|
15
|
+
- Fork relationship with Ruby [finance](https://github.com/marksweston/finance) gem
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
- Calculations for IRR
|
19
|
+
- IRR, XIRR, NPV, and XNPV now belong to their own classes
|
20
|
+
- Method for calculating IRR. The finance gem used Newton's method, this gem now uses the Secant method.
|
21
|
+
- Significant changes now captured in README.md instead of HISTORY
|
22
|
+
|
23
|
+
## [2.1.0]
|
24
|
+
### Added
|
25
|
+
- Code coverage and build status badges to README
|
26
|
+
|
27
|
+
### Removed
|
28
|
+
|
29
|
+
### Changed
|
30
|
+
- Gem name from finance to financial_calculator
|
31
|
+
- Replaced minitest with rspec
|
data/README.md
CHANGED
@@ -10,10 +10,7 @@ A library for financial modelling in Ruby.
|
|
10
10
|
|
11
11
|
## IMPORTANT CHANGES
|
12
12
|
|
13
|
-
This
|
14
|
-
The fork was made at version 2.0 of the original library. The first release of
|
15
|
-
this library will not introduce any feature changes, it will only update the tests
|
16
|
-
and documentation.
|
13
|
+
This project started as a fork of the [finance](https://github.com/marksweston/finance) gem. The fork occured at version 2.0 of the finance library, and the first version of this gem is version 2.1. Version 2.1 is designed to be a drop in replacement for the finance gem, having only updated tests and documentation. However, starting with version 3.0, several significant changes have been made to the strucuture of the library as well as the underlying calculations.
|
17
14
|
|
18
15
|
Since there are many additional calculations that can be included in this gem, I am
|
19
16
|
working on developing a roadmap of which ones I will be adding and in what order. However,
|
@@ -109,25 +106,12 @@ You can also increase your payment to a specific amount:
|
|
109
106
|
|
110
107
|
>> extra_payments_2 = 250000.amortize(rate){ -1500 }
|
111
108
|
|
112
|
-
##
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
table. I thought I could create a better interface for this type of
|
119
|
-
work in Ruby, and since I couldn't find an existing resource for these
|
120
|
-
tools, I am hoping to save other folks some time by releasing what I
|
121
|
-
have as a gem.
|
122
|
-
|
123
|
-
More broadly, I believe there are many calculations that are necessary
|
124
|
-
for the effective management of personal finances, but are difficult
|
125
|
-
(or impossible) to do with spreadsheets or other existing open source
|
126
|
-
tools. My hope is that the `finance` library will grow to provide a set
|
127
|
-
of open, tested tools to fill this gap.
|
128
|
-
|
129
|
-
If you have used `finance` and find it useful, I would enjoy hearing
|
130
|
-
about it!
|
109
|
+
## SUPPORTED STANDARD CALCUALTIONS
|
110
|
+
- PV
|
111
|
+
- NPV
|
112
|
+
- XNPV
|
113
|
+
- IRR
|
114
|
+
- XIRR
|
131
115
|
|
132
116
|
## FEATURES
|
133
117
|
|
@@ -24,11 +24,12 @@ SPEC = Gem::Specification.new do |s|
|
|
24
24
|
s.add_development_dependency 'rake'
|
25
25
|
s.add_development_dependency 'rspec', '>= 3.4.0'
|
26
26
|
s.add_development_dependency 'simplecov', '>= 0.16.0'
|
27
|
+
s.add_development_dependency 'yard'
|
27
28
|
|
28
29
|
s.files = `git ls-files`.split("\n")
|
29
30
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
30
31
|
s.require_paths = ['lib']
|
31
32
|
|
32
33
|
s.has_rdoc = true
|
33
|
-
s.extra_rdoc_files = ['README.md', 'COPYING', 'COPYING.LESSER', '
|
34
|
-
end
|
34
|
+
s.extra_rdoc_files = ['README.md', 'COPYING', 'COPYING.LESSER', 'CHANGELOG.md']
|
35
|
+
end
|
data/lib/financial_calculator.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
|
1
|
+
require 'flt'
|
2
|
+
require 'flt/d'
|
4
3
|
# The *FinancialCalculator* module adheres to the following conventions for
|
5
4
|
# financial calculations:
|
6
5
|
#
|
@@ -9,7 +8,12 @@ require 'financial_calculator/cashflows'
|
|
9
8
|
# * *principal* represents the outstanding balance of a loan or annuity.
|
10
9
|
# * *rate* represents the interest rate _per period_.
|
11
10
|
module FinancialCalculator
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
require 'financial_calculator/amortization'
|
12
|
+
require 'financial_calculator/rates'
|
13
|
+
require 'financial_calculator/transaction'
|
14
|
+
require 'financial_calculator/irr'
|
15
|
+
require 'financial_calculator/pv'
|
16
|
+
require 'financial_calculator/npv'
|
17
|
+
require 'financial_calculator/xnpv'
|
18
|
+
require 'financial_calculator/xirr'
|
15
19
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module FinancialCalculator
|
2
|
+
class Irr
|
3
|
+
# @return [Numeric] The internal rate of return
|
4
|
+
# @example
|
5
|
+
# FinancialCalculator::Irr.new([-123400, 36200, 54800, 48100]).result.round(4) #=> Flt::DecNum('0.0596')
|
6
|
+
attr_reader :result
|
7
|
+
|
8
|
+
# The number of iterations to perform while attempting to minimize the root.
|
9
|
+
NUM_ITERATIONS = 1_000
|
10
|
+
|
11
|
+
# Creates a new Irr instance
|
12
|
+
# @param [Array<Numeric>] values An array of cash flows.
|
13
|
+
# @param [Numeric] r_1 An optional first guess to use for the secant method.
|
14
|
+
# @param [Numeric] r_2 An optional second guess to use for the secant method.
|
15
|
+
# @return [FinancialClaculator::Irr] An Irr instance
|
16
|
+
# @see http://en.wikipedia.org/wiki/Internal_rate_of_return
|
17
|
+
# @api public
|
18
|
+
def initialize(values, r_1 = nil, r_2 = nil)
|
19
|
+
unless (values[0].positive? ^ values[1].positive?) & values[0].nonzero?
|
20
|
+
raise ArgumentError.new('The values do not converge')
|
21
|
+
end
|
22
|
+
@eps = 1e-7
|
23
|
+
@values = values.map { |val| Flt::DecNum(val.to_s) }
|
24
|
+
|
25
|
+
r_1 ||= initial_r_1
|
26
|
+
r_2 ||= initial_r_2
|
27
|
+
|
28
|
+
@result = solve(@values, r_1, r_2)
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"IRR(#{@result})"
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Perform a single iteration
|
38
|
+
# @param [Array<Numeric>] values
|
39
|
+
# @param [DecNum] r_1
|
40
|
+
# @param [DecNum] r_2
|
41
|
+
# @return [DecNum] A rate of return that is closer to the actual internal rate of return
|
42
|
+
# than the previous iteration
|
43
|
+
def iterate(values, r_1, r_2)
|
44
|
+
fn_1 = Npv.new(r_1, values).result
|
45
|
+
fn_2 = Npv.new(r_2, values).result
|
46
|
+
|
47
|
+
r_1 - (fn_1 * (r_1 - r_2)) / (fn_1 - fn_2)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Solve the IRR
|
51
|
+
# @param [Array<Numeric>] values
|
52
|
+
# @param [DecNum] r_1
|
53
|
+
# @param [DecNum] r_2
|
54
|
+
# @return [DecNum] The internal rate of return
|
55
|
+
def solve(values, r_1, r_2)
|
56
|
+
NUM_ITERATIONS.times do
|
57
|
+
break if r_1.infinite? || converged?(r_1, r_2)
|
58
|
+
r_2, r_1 = [r_1, iterate(values, r_1, r_2)]
|
59
|
+
end
|
60
|
+
|
61
|
+
r_1.infinite? ? r_2 : r_1
|
62
|
+
end
|
63
|
+
|
64
|
+
# Default first guess for use in the secant method
|
65
|
+
# @see https://en.wikipedia.org/wiki/Internal_rate_of_return#Numerical_solution_for_single_outflow_and_multiple_inflows
|
66
|
+
def initial_r_1
|
67
|
+
@initial_r_1 ||= cap_a_over_abs_cap_c_0 ** (2 / Flt::DecNum(@values.length.to_s)) - 1
|
68
|
+
end
|
69
|
+
|
70
|
+
# Default second guess for use in the secant method
|
71
|
+
# @see https://en.wikipedia.org/wiki/Internal_rate_of_return#Numerical_solution_for_single_outflow_and_multiple_inflows
|
72
|
+
def initial_r_2
|
73
|
+
(1 + initial_r_1) ** p - 1
|
74
|
+
end
|
75
|
+
|
76
|
+
def cap_a_over_abs_cap_c_0
|
77
|
+
cap_a / abs_c_0
|
78
|
+
end
|
79
|
+
|
80
|
+
def cap_a
|
81
|
+
@cap_a ||= @values[1..-1].sum
|
82
|
+
end
|
83
|
+
|
84
|
+
def abs_c_0
|
85
|
+
@values[0].abs
|
86
|
+
end
|
87
|
+
|
88
|
+
def p
|
89
|
+
Flt::DecNum(Math.log(cap_a_over_abs_cap_c_0).to_s) / Flt::DecNum(Math.log(cap_a / npv_1_in(initial_r_1)).to_s)
|
90
|
+
end
|
91
|
+
|
92
|
+
def npv_1_in(rate)
|
93
|
+
@flows ||= Npv.new(rate, [0] + @values[1..-1].flatten).result
|
94
|
+
end
|
95
|
+
|
96
|
+
def converged?(r_1, r_2)
|
97
|
+
(r_1 - r_2).abs < @eps
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module FinancialCalculator
|
2
|
+
# Calculates the present value of a series of equally spaced cashflows of unequal amounts
|
3
|
+
class Npv
|
4
|
+
|
5
|
+
# @return [Numeric] The rates used for calculating the present value
|
6
|
+
# @api public
|
7
|
+
attr_reader :rate
|
8
|
+
|
9
|
+
# @return [Arrray<Numeric>] An array of cashflow amounts
|
10
|
+
# @api public
|
11
|
+
attr_reader :cashflows
|
12
|
+
|
13
|
+
# @return [DecNum] Result of the XNPV calculation
|
14
|
+
# @api public
|
15
|
+
attr_reader :result
|
16
|
+
|
17
|
+
# Create a new net present value calculation
|
18
|
+
# @see https://en.wikipedia.org/wiki/Net_present_value
|
19
|
+
# @param [Numeric] rate The discount (interest) rate
|
20
|
+
# @param [Array<Numeric>] cashflows An array of cashflows
|
21
|
+
# @return [FinancialCalculator::Npv] An instance of a Net Present Value calculation
|
22
|
+
def initialize(rate, cashflows)
|
23
|
+
raise ArgumentError.new("Rate must be a Numeric. Got #{rate.class} instead") unless rate.is_a? Numeric
|
24
|
+
|
25
|
+
@rate = rate
|
26
|
+
@cashflows = cashflows
|
27
|
+
@result = solve(rate, cashflows)
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"NPV(#{result})"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def solve(rate, cashflows)
|
37
|
+
cashflows.each_with_index.reduce(0) do |total, (payment, index)|
|
38
|
+
total += payment / (1 + rate) ** (index + 1)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
NetPresentValue = Npv
|
44
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module FinancialCalculator
|
2
|
+
# Calculate the future value of a series of equal of payments
|
3
|
+
class Pv
|
4
|
+
|
5
|
+
# @return [Numeric] The discount rate used in the calculation
|
6
|
+
attr_reader :rate
|
7
|
+
|
8
|
+
# @return [Numeric] The number of periodic payments
|
9
|
+
attr_reader :num_periods
|
10
|
+
|
11
|
+
# @return [Numeric] The amount of the periodic payment
|
12
|
+
attr_reader :payment
|
13
|
+
|
14
|
+
# @return [Numeric] The value remaining after the final payment has been made
|
15
|
+
attr_reader :future_value
|
16
|
+
|
17
|
+
# @return [Numeric] The result of the present value calculation
|
18
|
+
attr_reader :result
|
19
|
+
|
20
|
+
# Create a new present value calculation
|
21
|
+
#
|
22
|
+
# @see https://en.wikipedia.org/wiki/Present_value
|
23
|
+
# @example
|
24
|
+
# FinancialCalculator::Pv.new(0.02, 10, -100) #=> PresentValue(898.2585006242236)
|
25
|
+
# @param [Numeric] rate The discount (interest) rate to use
|
26
|
+
# @param [Numeric] num_periods The number of periodic payments
|
27
|
+
# @param [Numeric] payment The amount of the periodic payment
|
28
|
+
# @param [Numeric] future_value The value remaining after the final payment has been made
|
29
|
+
# @param [Boolean] pay_at_beginning Whether the payments are made at the beginning or end of each period
|
30
|
+
# @return [FinancialCalculator::Pv] A Pv object
|
31
|
+
# @raise [ArgumentError] Raises an ArgumentError if num_periods is less than 0 or a required numeric
|
32
|
+
# field is given a non-numeric value
|
33
|
+
def initialize(rate, num_periods, payment, future_value = 0, pay_at_beginning = false)
|
34
|
+
validate_numerics(rate: rate, num_periods: num_periods, payment: payment, future_value: future_value)
|
35
|
+
|
36
|
+
if num_periods < 0
|
37
|
+
raise ArgumentError.new('Cannot calculate present value with negative periods. Use future value instead.')
|
38
|
+
end
|
39
|
+
|
40
|
+
@rate = Flt::DecNum(rate.to_s)
|
41
|
+
@num_periods = Flt::DecNum(num_periods.to_s)
|
42
|
+
@payment = Flt::DecNum(payment.to_s)
|
43
|
+
@future_value = Flt::DecNum(future_value.to_s)
|
44
|
+
@pay_at_beginning = pay_at_beginning
|
45
|
+
@result = solve(@rate, @num_periods, @payment, @future_value, pay_at_beginning)
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
"PV(#{result})"
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Boolean] Whether the payments are made at the beginning of each period
|
53
|
+
def pays_at_beginning?
|
54
|
+
@pay_at_beginning
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def solve(rate, num_periods, payment, future_value, pay_at_beginning)
|
60
|
+
start_period = pay_at_beginning ? 0 : 1
|
61
|
+
end_period = pay_at_beginning ? num_periods - 1 : num_periods
|
62
|
+
|
63
|
+
present_value = (start_period..end_period.abs).reduce(0) do |total, t|
|
64
|
+
total += discount(payment, rate, t)
|
65
|
+
end
|
66
|
+
|
67
|
+
-(present_value + discount(future_value, rate, num_periods))
|
68
|
+
end
|
69
|
+
|
70
|
+
def discount(amount, rate, periods)
|
71
|
+
return 0 if amount.zero?
|
72
|
+
amount / (1 + rate) ** periods
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_numerics(numerics)
|
76
|
+
numerics.each do |key, value|
|
77
|
+
unless value.is_a? Numeric
|
78
|
+
raise ArgumentError.new("#{key} must be a type of Numeric. Got #{value.class} instead.")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
PresentValue = Pv
|
85
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module FinancialCalculator
|
2
|
+
class Xirr
|
3
|
+
# @return [DecNum] The internal rate of return
|
4
|
+
# @example
|
5
|
+
# transactions = []
|
6
|
+
# transactions << Transaction.new(-1000, :date => Date.new(1985,01,01))
|
7
|
+
# transactions << Transaction.new( 600, :date => Date.new(1990,01,01))
|
8
|
+
# transactions << Transaction.new( 600, :date => Date.new(1995,01,01))
|
9
|
+
# Xirr.with_transactions(transactions).result #=> 0.0249
|
10
|
+
attr_reader :result
|
11
|
+
|
12
|
+
# The number of iterations to perform while attempting to minimize the root.
|
13
|
+
NUM_ITERATIONS = 1_000
|
14
|
+
|
15
|
+
# Factory method for creating an Xirr instance from an array of transactions
|
16
|
+
# @param [Array<Transaction>] transactions An array of transactions
|
17
|
+
# @param [Numeric] /_1 An optional first guess to use when calculating the secant method
|
18
|
+
# @param [Numeric] r_2 An optional second guess to use when calculating the secant method
|
19
|
+
# @return [FinancialCalculator::Xirr] An Xirr instance
|
20
|
+
# @api public
|
21
|
+
def self.with_transactions(transactions, r_1 = nil, r_2 = nil)
|
22
|
+
unless transactions.all? { |t| t.is_a? Transaction }
|
23
|
+
raise ArgumentError.new("Argument \"transactions\" must be an array of Transaction")
|
24
|
+
end
|
25
|
+
|
26
|
+
cashflows = transactions.map(&:amount)
|
27
|
+
dates = transactions.map(&:date)
|
28
|
+
|
29
|
+
self.new(cashflows, dates, r_1, r_2)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a new Xirr instance
|
33
|
+
# @param [Array<Numeric>] cashflows An array of cash flows.
|
34
|
+
# @param [Numeric] r_1 An optional first guess to use for the secant method.
|
35
|
+
# @param [Numeric] r_2 An optional second guess to use for the secant method.
|
36
|
+
# @return [FinancialClaculator::Xirr] An Xirr instance
|
37
|
+
# @see http://en.wikipedia.org/wiki/Internal_rate_of_return
|
38
|
+
# @api public
|
39
|
+
def initialize(cashflows, dates, r_1 = nil, r_2 = nil)
|
40
|
+
unless (cashflows[0].positive? ^ cashflows[1].positive?) & cashflows[0].nonzero?
|
41
|
+
raise ArgumentError.new('The cashflows do not converge')
|
42
|
+
end
|
43
|
+
@eps = 1e-7
|
44
|
+
@cashflows = cashflows.map { |val| Flt::DecNum(val.to_s) }
|
45
|
+
@dates = dates
|
46
|
+
|
47
|
+
r_1 = r_1 ? Flt::DecNum(r_1.to_s) : initial_r_1
|
48
|
+
r_2 = r_2 ? Flt::DecNum(r_2.to_s) : initial_r_2
|
49
|
+
|
50
|
+
@result = solve(@cashflows, @dates, r_1, r_2)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [String]
|
54
|
+
# @api public
|
55
|
+
def inspect
|
56
|
+
"XIRR(#{@result})"
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Perform a single iteration
|
62
|
+
# @param [Array<Numeric>] cashflows
|
63
|
+
# @param [Numeric] r_1
|
64
|
+
# @param [Numeric] r_2
|
65
|
+
# @return [Numeric] A rate of return that is closer to the actual internal rate of return
|
66
|
+
# than the previous iteration
|
67
|
+
def iterate(cashflows, dates, r_1, r_2)
|
68
|
+
fn_1 = Xnpv.new(r_1, cashflows, dates).result
|
69
|
+
fn_2 = Xnpv.new(r_2, cashflows, dates).result
|
70
|
+
|
71
|
+
r_1 - (fn_1 * (r_1 - r_2)) / (fn_1 - fn_2)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Solve the Xirr
|
75
|
+
# @param [Array<Numeric>] cashflows
|
76
|
+
# @param [Numeric] r_1
|
77
|
+
# @param [Numeric] r_2
|
78
|
+
# @return [DecNum] The internal rate of return
|
79
|
+
def solve(cashflows, dates, r_1, r_2)
|
80
|
+
NUM_ITERATIONS.times do
|
81
|
+
break if r_1.infinite? || converged?(r_1, r_2)
|
82
|
+
r_2, r_1 = [r_1, iterate(cashflows, dates, r_1, r_2)]
|
83
|
+
end
|
84
|
+
|
85
|
+
r_1.infinite? ? r_2 : r_1
|
86
|
+
end
|
87
|
+
|
88
|
+
# Default first guess for use in the secant method
|
89
|
+
# @see https://en.wikipedia.org/wiki/Internal_rate_of_return#Numerical_solution_for_single_outflow_and_multiple_inflows
|
90
|
+
def initial_r_1
|
91
|
+
@initial_r_1 ||= cap_a_over_abs_cap_c_0 ** (2 / Flt::DecNum(@cashflows.length.to_s)) - 1
|
92
|
+
end
|
93
|
+
|
94
|
+
# Default second guess for use in the secant method
|
95
|
+
# @see https://en.wikipedia.org/wiki/Internal_rate_of_return#Numerical_solution_for_single_outflow_and_multiple_inflows
|
96
|
+
def initial_r_2
|
97
|
+
(1 + initial_r_1) ** p - 1
|
98
|
+
end
|
99
|
+
|
100
|
+
def cap_a_over_abs_cap_c_0
|
101
|
+
cap_a / abs_c_0
|
102
|
+
end
|
103
|
+
|
104
|
+
def cap_a
|
105
|
+
@cap_a ||= @cashflows[1..-1].sum
|
106
|
+
end
|
107
|
+
|
108
|
+
def abs_c_0
|
109
|
+
@cashflows[0].abs
|
110
|
+
end
|
111
|
+
|
112
|
+
def p
|
113
|
+
Flt::DecNum(Math.log(cap_a_over_abs_cap_c_0).to_s) / Flt::DecNum(Math.log(cap_a / npv_1_in(initial_r_1)).to_s)
|
114
|
+
end
|
115
|
+
|
116
|
+
def npv_1_in(rate)
|
117
|
+
@flows ||= Xnpv.new(rate, [0] + @cashflows[1..-1].flatten, @dates).result
|
118
|
+
end
|
119
|
+
|
120
|
+
def converged?(r_1, r_2)
|
121
|
+
(r_1 - r_2).abs < @eps
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|