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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89354ba8e6670b27b4848b7ba3e1ab400b79b847177d9f31cac035b0993dc2c9
4
- data.tar.gz: 972ceb6f53f670b63896732364fa4b1291d51d85316779a28a517fa9f13d8deb
3
+ metadata.gz: 5f0d684ffcf8cd34cdad09576834da2057e6879b45af1484123474b6e151baad
4
+ data.tar.gz: eccae432dc2cca2470cf1023871c0d9cb58620b080715c8ed00c818a62e4d2b4
5
5
  SHA512:
6
- metadata.gz: 1ef6f4907275ece9ea20f046394a10c62484ac991b5b3a8bc69d9a7b7b8dee6fa2ee470a316b6f9348a5fa5b36b99e315fab4ec67c003b6e2639e35d5049a203
7
- data.tar.gz: e02169325a2c0f08e3f7379b9944cdf5374b7f055338a41e7862d6321a942d69d4f9e9d24943fc833e7d5a22d2532fb035b387d6ca5a842fea855458caf42a57
6
+ metadata.gz: 5715ae679ff1cc465004bf136989bdee35ce1d60c3c1a21cf6daac07ec4ecbe71f1db8f04ff26d8b3e57a525394e7508601c96be5c5923e443b6504014c94325
7
+ data.tar.gz: 449cdc6640b282aea1e34a3de372717986232474310101bcf4097ecbbfac6021087aa26d1b4b429ab017239f40bd337c993f91675f3f7f9823920a1243c7bc70
@@ -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 is a fork of the [finance](https://github.com/marksweston/finance) library.
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
- ## ABOUT
113
-
114
- I started developing `finance` while analyzing mortgages as a personal
115
- project. Spreadsheets have convenient formulas for doing this type of
116
- work, until you want to do something semi-complex (like ARMs or extra
117
- payments), at which point you need to create your own amortization
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', 'HISTORY']
34
- end
34
+ s.extra_rdoc_files = ['README.md', 'COPYING', 'COPYING.LESSER', 'CHANGELOG.md']
35
+ end
@@ -1,6 +1,5 @@
1
- require 'financial_calculator/decimal'
2
- require 'financial_calculator/cashflows'
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
- autoload :Amortization, 'financial_calculator/amortization'
13
- autoload :Rate, 'financial_calculator/rates'
14
- autoload :Transaction, 'financial_calculator/transaction'
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
@@ -1,5 +1,3 @@
1
- require_relative 'cashflows'
2
- require_relative 'decimal'
3
1
  require_relative 'transaction'
4
2
 
5
3
  module FinancialCalculator
@@ -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
@@ -1,5 +1,3 @@
1
- require_relative 'decimal'
2
-
3
1
  module FinancialCalculator
4
2
  # the Rate class provides an interface for working with interest rates.
5
3
  # {render:Rate#new}
@@ -1,5 +1,3 @@
1
- require_relative 'decimal'
2
-
3
1
  module FinancialCalculator
4
2
  # the Transaction class provides a general interface for working with individual cash flows.
5
3
  # @api public
@@ -1,3 +1,3 @@
1
1
  module FinancialCalculator
2
- VERSION = '2.1.0'
2
+ VERSION = '3.0.0'
3
3
  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