finance_velocity 2.0.3
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 +7 -0
- data/.gitignore +8 -0
- data/.rbenv-version +1 -0
- data/.yardopts +1 -0
- data/COPYING +676 -0
- data/COPYING.LESSER +167 -0
- data/Gemfile +3 -0
- data/HISTORY +47 -0
- data/README.md +166 -0
- data/Rakefile +10 -0
- data/finance.gemspec +19 -0
- data/lib/finance/amortization.rb +201 -0
- data/lib/finance/cashflows.rb +127 -0
- data/lib/finance/decimal.rb +21 -0
- data/lib/finance/rates.rb +167 -0
- data/lib/finance/transaction.rb +122 -0
- data/lib/finance.rb +15 -0
- data/test/test_amortization.rb +171 -0
- data/test/test_cashflows.rb +32 -0
- data/test/test_helper.rb +15 -0
- data/test/test_rates.rb +72 -0
- metadata +127 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
require_relative 'decimal'
|
2
|
+
require_relative 'rates'
|
3
|
+
|
4
|
+
require 'bigdecimal'
|
5
|
+
require 'bigdecimal/newton'
|
6
|
+
include Newton
|
7
|
+
|
8
|
+
module Finance
|
9
|
+
# Provides methods for working with cash flows (collections of transactions)
|
10
|
+
# @api public
|
11
|
+
module Cashflow
|
12
|
+
# Base class for working with Newton's Method.
|
13
|
+
# @api private
|
14
|
+
class Function
|
15
|
+
values = {
|
16
|
+
eps: "1.0e-16",
|
17
|
+
one: "1.0",
|
18
|
+
two: "2.0",
|
19
|
+
ten: "10.0",
|
20
|
+
zero: "0.0"
|
21
|
+
}
|
22
|
+
|
23
|
+
values.each do |key, value|
|
24
|
+
define_method key do
|
25
|
+
BigDecimal value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(transactions, function)
|
30
|
+
@transactions = transactions
|
31
|
+
@function = function
|
32
|
+
end
|
33
|
+
|
34
|
+
def values(x)
|
35
|
+
value = @transactions.send(@function, Flt::DecNum.new(x[0].to_s))
|
36
|
+
[ BigDecimal(value.to_s) ]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# calculate the internal rate of return for a sequence of cash flows
|
41
|
+
# @return [DecNum] the internal rate of return
|
42
|
+
# @example
|
43
|
+
# [-4000,1200,1410,1875,1050].irr #=> 0.143
|
44
|
+
# @see http://en.wikipedia.org/wiki/Internal_rate_of_return
|
45
|
+
# @api public
|
46
|
+
def irr
|
47
|
+
# Make sure we have a valid sequence of cash flows.
|
48
|
+
positives, negatives = self.partition{ |i| i >= 0 }
|
49
|
+
if positives.empty? || negatives.empty?
|
50
|
+
raise ArgumentError, "Calculation does not converge."
|
51
|
+
end
|
52
|
+
|
53
|
+
func = Function.new(self, :npv)
|
54
|
+
rate = [ func.one ]
|
55
|
+
nlsolve( func, rate )
|
56
|
+
rate[0]
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing(name, *args, &block)
|
60
|
+
return self.inject(:+) if name.to_s == "sum"
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
# calculate the net present value of a sequence of cash flows
|
65
|
+
# @return [DecNum] the net present value
|
66
|
+
# @param [Numeric] rate the discount rate to be applied
|
67
|
+
# @example
|
68
|
+
# [-100.0, 60, 60, 60].npv(0.1) #=> 49.211
|
69
|
+
# @see http://en.wikipedia.org/wiki/Net_present_value
|
70
|
+
# @api public
|
71
|
+
def npv(rate)
|
72
|
+
self.collect! { |entry| Flt::DecNum.new(entry.to_s) }
|
73
|
+
|
74
|
+
rate, total = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(0.to_s)
|
75
|
+
self.each_with_index do |cashflow, index|
|
76
|
+
total += cashflow / (1 + rate) ** index
|
77
|
+
end
|
78
|
+
|
79
|
+
total
|
80
|
+
end
|
81
|
+
|
82
|
+
# calculate the internal rate of return for a sequence of cash flows with dates
|
83
|
+
# @return [Rate] the internal rate of return
|
84
|
+
# @example
|
85
|
+
# @transactions = []
|
86
|
+
# @transactions << Transaction.new(-1000, :date => Time.new(1985,01,01))
|
87
|
+
# @transactions << Transaction.new( 600, :date => Time.new(1990,01,01))
|
88
|
+
# @transactions << Transaction.new( 600, :date => Time.new(1995,01,01))
|
89
|
+
# @transactions.xirr(0.6) #=> Rate("0.024851", :apr, :compounds => :annually)
|
90
|
+
# @api public
|
91
|
+
def xirr(iterations=100)
|
92
|
+
# Make sure we have a valid sequence of cash flows.
|
93
|
+
positives, negatives = self.partition{ |t| t.amount >= 0 }
|
94
|
+
if positives.empty? || negatives.empty?
|
95
|
+
raise ArgumentError, "Calculation does not converge."
|
96
|
+
end
|
97
|
+
|
98
|
+
func = Function.new(self, :xnpv)
|
99
|
+
rate = [ func.one ]
|
100
|
+
nlsolve( func, rate )
|
101
|
+
Rate.new(rate[0], :apr, :compounds => :annually)
|
102
|
+
end
|
103
|
+
|
104
|
+
# calculate the net present value of a sequence of cash flows
|
105
|
+
# @return [DecNum]
|
106
|
+
# @example
|
107
|
+
# @transactions = []
|
108
|
+
# @transactions << Transaction.new(-1000, :date => Time.new(1985,01,01))
|
109
|
+
# @transactions << Transaction.new( 600, :date => Time.new(1990,01,01))
|
110
|
+
# @transactions << Transaction.new( 600, :date => Time.new(1995,01,01))
|
111
|
+
# @transactions.xnpv(0.6).round(2) #=> -937.41
|
112
|
+
# @api public
|
113
|
+
def xnpv(rate)
|
114
|
+
rate = Flt::DecNum.new(rate.to_s)
|
115
|
+
start = self[0].date
|
116
|
+
|
117
|
+
self.inject(0) do |sum, t|
|
118
|
+
n = t.amount / ( (1 + rate) ** ((t.date-start) / Flt::DecNum.new(31536000.to_s))) # 365 * 86400
|
119
|
+
sum + n
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class Array
|
126
|
+
include Finance::Cashflow
|
127
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'flt'
|
3
|
+
include Flt
|
4
|
+
|
5
|
+
DecNum.context.define_conversion_from(BigDecimal) do |x, context|
|
6
|
+
DecNum(x.to_s)
|
7
|
+
end
|
8
|
+
|
9
|
+
DecNum.context.define_conversion_to(BigDecimal) do |x|
|
10
|
+
BigDecimal(x.to_s)
|
11
|
+
end
|
12
|
+
|
13
|
+
class Numeric
|
14
|
+
def to_d
|
15
|
+
if self.instance_of? DecNum
|
16
|
+
self
|
17
|
+
else
|
18
|
+
DecNum self.to_s
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require_relative 'decimal'
|
2
|
+
|
3
|
+
module Finance
|
4
|
+
# the Rate class provides an interface for working with interest rates.
|
5
|
+
# {render:Rate#new}
|
6
|
+
# @api public
|
7
|
+
class Rate
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
# Accepted rate types
|
11
|
+
TYPES = { :apr => "effective",
|
12
|
+
:apy => "effective",
|
13
|
+
:effective => "effective",
|
14
|
+
:nominal => "nominal"
|
15
|
+
}
|
16
|
+
|
17
|
+
# @return [Integer] the duration for which the rate is valid, in months
|
18
|
+
# @api public
|
19
|
+
attr_accessor :duration
|
20
|
+
# @return [DecNum] the effective interest rate
|
21
|
+
# @api public
|
22
|
+
attr_reader :effective
|
23
|
+
# @return [DecNum] the nominal interest rate
|
24
|
+
# @api public
|
25
|
+
attr_reader :nominal
|
26
|
+
|
27
|
+
# compare two Rates, using the effective rate
|
28
|
+
# @return [Numeric] one of -1, 0, +1
|
29
|
+
# @param [Rate] rate the comparison Rate
|
30
|
+
# @example Which is better, a nominal rate of 15% compounded monthly, or 15.5% compounded semiannually?
|
31
|
+
# r1 = Rate.new(0.15, :nominal) #=> Rate.new(0.160755, :apr)
|
32
|
+
# r2 = Rate.new(0.155, :nominal, :compounds => :semiannually) #=> Rate.new(0.161006, :apr)
|
33
|
+
# r1 <=> r2 #=> -1
|
34
|
+
# @api public
|
35
|
+
def <=>(rate)
|
36
|
+
@effective <=> rate.effective
|
37
|
+
end
|
38
|
+
|
39
|
+
# (see #effective)
|
40
|
+
# @api public
|
41
|
+
def apr
|
42
|
+
self.effective
|
43
|
+
end
|
44
|
+
|
45
|
+
# (see #effective)
|
46
|
+
# @api public
|
47
|
+
def apy
|
48
|
+
self.effective
|
49
|
+
end
|
50
|
+
|
51
|
+
# a convenience method which sets the value of @periods
|
52
|
+
# @return none
|
53
|
+
# @param [Symbol, Numeric] input the compounding frequency
|
54
|
+
# @raise [ArgumentError] if input is not an accepted keyword or Numeric
|
55
|
+
# @api private
|
56
|
+
def compounds=(input)
|
57
|
+
@periods = case input
|
58
|
+
when :annually then Flt::DecNum.new(1)
|
59
|
+
when :continuously then Flt::DecNum.infinity
|
60
|
+
when :daily then Flt::DecNum.new(365)
|
61
|
+
when :monthly then Flt::DecNum.new(12)
|
62
|
+
when :quarterly then Flt::DecNum.new(4)
|
63
|
+
when :semiannually then Flt::DecNum.new(2)
|
64
|
+
when Numeric then Flt::DecNum.new(input.to_s)
|
65
|
+
else raise ArgumentError
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# set the effective interest rate
|
70
|
+
# @return none
|
71
|
+
# @param [DecNum] rate the effective interest rate
|
72
|
+
# @api private
|
73
|
+
def effective=(rate)
|
74
|
+
@effective = rate
|
75
|
+
@nominal = Rate.to_nominal(rate, @periods)
|
76
|
+
end
|
77
|
+
|
78
|
+
# create a new Rate instance
|
79
|
+
# @return [Rate]
|
80
|
+
# @param [Numeric] rate the decimal value of the interest rate
|
81
|
+
# @param [Symbol] type a valid {TYPES rate type}
|
82
|
+
# @param [optional, Hash] opts set optional attributes
|
83
|
+
# @option opts [String] :duration a time interval for which the rate is valid
|
84
|
+
# @option opts [String] :compounds (:monthly) the number of compounding periods per year
|
85
|
+
# @example create a 3.5% APR rate
|
86
|
+
# Rate.new(0.035, :apr) #=> Rate(0.035, :apr)
|
87
|
+
# @see http://en.wikipedia.org/wiki/Effective_interest_rate
|
88
|
+
# @see http://en.wikipedia.org/wiki/Nominal_interest_rate
|
89
|
+
# @api public
|
90
|
+
def initialize(rate, type, opts={})
|
91
|
+
# Default monthly compounding.
|
92
|
+
opts = { :compounds => :monthly }.merge opts
|
93
|
+
|
94
|
+
# Set optional attributes..
|
95
|
+
opts.each do |key, value|
|
96
|
+
send("#{key}=", value)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Set the rate in the proper way, based on the value of type.
|
100
|
+
begin
|
101
|
+
send("#{TYPES.fetch(type)}=", Flt::DecNum.new(rate.to_s))
|
102
|
+
rescue KeyError
|
103
|
+
raise ArgumentError, "type must be one of #{TYPES.keys.join(', ')}", caller
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def inspect
|
108
|
+
"Rate.new(#{self.apr.round(6)}, :apr)"
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return [DecNum] the monthly effective interest rate
|
112
|
+
# @example
|
113
|
+
# rate = Rate.new(0.15, :nominal)
|
114
|
+
# rate.apr.round(6) #=> DecNum('0.160755')
|
115
|
+
# rate.monthly.round(6) #=> DecNum('0.013396')
|
116
|
+
# @api public
|
117
|
+
def monthly
|
118
|
+
(self.effective / 12).round(15)
|
119
|
+
end
|
120
|
+
|
121
|
+
# set the nominal interest rate
|
122
|
+
# @return none
|
123
|
+
# @param [DecNum] rate the nominal interest rate
|
124
|
+
# @api private
|
125
|
+
def nominal=(rate)
|
126
|
+
@nominal = rate
|
127
|
+
@effective = Rate.to_effective(rate, @periods)
|
128
|
+
end
|
129
|
+
|
130
|
+
# convert a nominal interest rate to an effective interest rate
|
131
|
+
# @return [DecNum] the effective interest rate
|
132
|
+
# @param [Numeric] rate the nominal interest rate
|
133
|
+
# @param [Numeric] periods the number of compounding periods per year
|
134
|
+
# @example
|
135
|
+
# Rate.to_effective(0.05, 4) #=> DecNum('0.05095')
|
136
|
+
# @api public
|
137
|
+
def Rate.to_effective(rate, periods)
|
138
|
+
rate, periods = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(periods.to_s)
|
139
|
+
|
140
|
+
if periods.infinite?
|
141
|
+
rate.exp - 1
|
142
|
+
else
|
143
|
+
(1 + rate / periods) ** periods - 1
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# convert an effective interest rate to a nominal interest rate
|
148
|
+
# @return [DecNum] the nominal interest rate
|
149
|
+
# @param [Numeric] rate the effective interest rate
|
150
|
+
# @param [Numeric] periods the number of compounding periods per year
|
151
|
+
# @example
|
152
|
+
# Rate.to_nominal(0.06, 365) #=> DecNum('0.05827')
|
153
|
+
# @see http://www.miniwebtool.com/nominal-interest-rate-calculator/
|
154
|
+
# @api public
|
155
|
+
def Rate.to_nominal(rate, periods)
|
156
|
+
rate, periods = Flt::DecNum.new(rate.to_s), Flt::DecNum.new(periods.to_s)
|
157
|
+
|
158
|
+
if periods.infinite?
|
159
|
+
(rate + 1).log
|
160
|
+
else
|
161
|
+
periods * ((1 + rate) ** (1 / periods) - 1)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
private :compounds=, :effective=, :nominal=
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require_relative 'decimal'
|
2
|
+
|
3
|
+
module Finance
|
4
|
+
# the Transaction class provides a general interface for working with individual cash flows.
|
5
|
+
# @api public
|
6
|
+
class Transaction
|
7
|
+
# @return [DecNum] the cash value of the transaction
|
8
|
+
# @api public
|
9
|
+
attr_reader :amount
|
10
|
+
# @return [Integer] the period number of the transaction
|
11
|
+
# @note this attribute is mainly used in the case of mortgage amortization with no dates
|
12
|
+
# @api public
|
13
|
+
attr_accessor :period
|
14
|
+
# @return [Date] the date of the transaction
|
15
|
+
# @api public
|
16
|
+
attr_accessor :date
|
17
|
+
|
18
|
+
# Set the cash value of the transaction
|
19
|
+
# @return None
|
20
|
+
# @param [Numeric] value the cash value
|
21
|
+
# @example
|
22
|
+
# t = Transaction.new(500)
|
23
|
+
# t.amount = 750
|
24
|
+
# t.amount #=> 750
|
25
|
+
# @api public
|
26
|
+
def amount=(value)
|
27
|
+
@amount = Flt::DecNum.new(value.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [DecNum] the difference between the original transaction
|
31
|
+
# amount and the current amount
|
32
|
+
# @example
|
33
|
+
# t = Transaction.new(500)
|
34
|
+
# t.amount = 750
|
35
|
+
# t.difference #=> DecNum('250')
|
36
|
+
# @api public
|
37
|
+
def difference
|
38
|
+
@amount - @original
|
39
|
+
end
|
40
|
+
|
41
|
+
# create a new Transaction
|
42
|
+
# @return [Transaction]
|
43
|
+
# @param [Numeric] amount the cash value of the transaction
|
44
|
+
# @param [optional, Hash] opts sets optional attributes
|
45
|
+
# @option opts [String] :period the period number of the transaction
|
46
|
+
# @example a simple transaction
|
47
|
+
# t = Transaction.new(400)
|
48
|
+
# @example a transaction with a period number
|
49
|
+
# t = Transaction.new(400, :period => 3)
|
50
|
+
# @api public
|
51
|
+
def initialize(amount, opts={})
|
52
|
+
@amount = amount
|
53
|
+
@original = amount
|
54
|
+
|
55
|
+
# Set optional attributes..
|
56
|
+
opts.each do |key, value|
|
57
|
+
send("#{key}=", value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Boolean] whether or not the Transaction is an Interest transaction
|
62
|
+
# @example
|
63
|
+
# pmt = Payment.new(500)
|
64
|
+
# int = Interest.new(500)
|
65
|
+
# pmt.interest? #=> False
|
66
|
+
# int.interest? #=> True
|
67
|
+
# @api public
|
68
|
+
def interest?
|
69
|
+
self.instance_of? Interest
|
70
|
+
end
|
71
|
+
|
72
|
+
# @api public
|
73
|
+
def inspect
|
74
|
+
"Transaction(#{@amount})"
|
75
|
+
end
|
76
|
+
|
77
|
+
# Modify a Transaction's amount by passing a block
|
78
|
+
# @return none
|
79
|
+
# @note self is passed as the argument to the block. This makes any public attribute available.
|
80
|
+
# @example add $100 to a monthly payment
|
81
|
+
# pmt = Payment.new(-500)
|
82
|
+
# pmt.modify { |t| t.amount-100 }
|
83
|
+
# pmt.amount #=> -600
|
84
|
+
# @api public
|
85
|
+
def modify
|
86
|
+
@amount = yield(self)
|
87
|
+
end
|
88
|
+
|
89
|
+
# (see #amount)
|
90
|
+
# @deprecated Provided for backwards compatibility
|
91
|
+
def payment
|
92
|
+
@amount
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Boolean] whether or not the Transaction is a Payment transaction
|
96
|
+
# @example
|
97
|
+
# pmt = Payment.new(500)
|
98
|
+
# int = Interest.new(500)
|
99
|
+
# pmt.payment? #=> True
|
100
|
+
# int.payment? #=> False
|
101
|
+
# @api public
|
102
|
+
def payment?
|
103
|
+
self.instance_of? Payment
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Represent an interest charge as a Transaction
|
108
|
+
# @see Transaction
|
109
|
+
class Interest < Transaction
|
110
|
+
def inspect
|
111
|
+
"Interest(#{@amount})"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Represent a loan payment as a Transaction
|
116
|
+
# @see Transaction
|
117
|
+
class Payment < Transaction
|
118
|
+
def inspect
|
119
|
+
"Payment(#{@amount})"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/finance.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'finance/decimal'
|
2
|
+
require 'finance/cashflows'
|
3
|
+
|
4
|
+
# The *Finance* module adheres to the following conventions for
|
5
|
+
# financial calculations:
|
6
|
+
#
|
7
|
+
# * Positive values represent cash inflows (money received); negative
|
8
|
+
# values represent cash outflows (payments).
|
9
|
+
# * *principal* represents the outstanding balance of a loan or annuity.
|
10
|
+
# * *rate* represents the interest rate _per period_.
|
11
|
+
module Finance
|
12
|
+
autoload :Amortization, 'finance/amortization'
|
13
|
+
autoload :Rate, 'finance/rates'
|
14
|
+
autoload :Transaction, 'finance/transaction'
|
15
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
# @see http://tinyurl.com/6zroqvd for detailed calculations for the
|
4
|
+
# examples in these unit tests.
|
5
|
+
describe "Amortization" do
|
6
|
+
def ipmt(principal, rate, payment, period)
|
7
|
+
-(-rate*principal*(1+rate)**(period-1) - payment*((1+rate)**(period-1)-1)).round(2)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "amortization with a 0% rate" do
|
11
|
+
it "should not raise a divide-by-zero error" do
|
12
|
+
rate = Rate.new(0, :apr, :duration => 30 * 12)
|
13
|
+
Amortization.new(D(10000), rate) # should not raise an error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "a fixed-rate amortization of 200000 at 3.75% over 30 years" do
|
18
|
+
before(:all) do
|
19
|
+
@rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
20
|
+
@principal = D(200000)
|
21
|
+
@std = Amortization.new(@principal, @rate)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should have a principal of $200,000" do
|
25
|
+
assert_equal @principal, @std.principal
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should have a final balance of zero" do
|
29
|
+
assert @std.balance.zero?
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should have a duration of 360 months" do
|
33
|
+
assert_equal 360, @std.duration
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should have a monthly payment of $926.23" do
|
37
|
+
assert_equal D('-926.23'), @std.payment
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should have a final payment of $926.96 (due to rounding)" do
|
41
|
+
assert_equal D('-926.96'), @std.payments[-1]
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should have total payments of $333,443.53" do
|
45
|
+
assert_equal D('-333443.53'), @std.payments.sum
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should have interest charges which agree with the standard formula" do
|
49
|
+
0.upto 359 do |period|
|
50
|
+
assert_equal @std.interest[period], ipmt(@principal, @rate.monthly, @std.payment, period+1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should have total interest charges of $133,433.33" do
|
55
|
+
assert_equal D('133443.53'), @std.interest.sum
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "an adjustable rate amortization of 200000 starting at 3.75% and increasing by 1% every 3 years" do
|
60
|
+
before(:all) do
|
61
|
+
@rates = []
|
62
|
+
0.upto 9 do |adj|
|
63
|
+
@rates << Rate.new(0.0375 + (D('0.01') * adj), :apr, :duration => (3 * 12))
|
64
|
+
end
|
65
|
+
@principal = D(200000)
|
66
|
+
@arm = Amortization.new(@principal, *@rates)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should have a principal of $200,000" do
|
70
|
+
assert_equal @principal, @arm.principal
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should have a final balance of zero" do
|
74
|
+
assert @arm.balance.zero?
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should have a duration of 360 months" do
|
78
|
+
assert_equal 360, @arm.duration
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should not have a fixed monthly payment (since it changes)" do
|
82
|
+
assert_nil @arm.payment
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should have payments which increase every three years" do
|
86
|
+
values = %w{926.23 1033.73 1137.32 1235.39 1326.30 1408.27 1479.28 1537.03 1578.84 1601.66 }
|
87
|
+
values.collect!{ |v| -D(v) }
|
88
|
+
|
89
|
+
payments = []
|
90
|
+
values[0,9].each do |v|
|
91
|
+
36.times do
|
92
|
+
payments << v
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
35.times { payments << values[9] }
|
97
|
+
|
98
|
+
payments[0..-2].each_with_index do |payment, index|
|
99
|
+
assert_equal payment, @arm.payments[index]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should have a final payment of $1601.78 (due to rounding)" do
|
104
|
+
assert_equal D('-1601.78'), @arm.payments[-1]
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should have total payments of $47,505.92" do
|
108
|
+
assert_equal D('-477505.92'), @arm.payments.sum
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should have total interest charges of $277,505.92" do
|
112
|
+
assert_equal D('277505.92'), @arm.interest.sum
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "a fixed-rate amortization of 200000 at 3.75% over 30 years, where an additional 100 is paid each month" do
|
117
|
+
before(:all) do
|
118
|
+
@rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
119
|
+
@principal = D(200000)
|
120
|
+
@exp = Amortization.new(@principal, @rate){ |period| period.payment - 100 }
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should have a principal of $200,000" do
|
124
|
+
assert_equal @principal, @exp.principal
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should have a final balance of zero" do
|
128
|
+
assert @exp.balance.zero?
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should have a duration of 301 months" do
|
132
|
+
assert_equal 301, @exp.duration
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should have a monthly payment of $1026.23" do
|
136
|
+
assert_equal D('-1026.23'), @exp.payment
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should have a final payment of $1011.09" do
|
140
|
+
assert_equal D('-1011.09'), @exp.payments[-1]
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should have total payments of $308,880.09" do
|
144
|
+
assert_equal D('-308880.09'), @exp.payments.sum
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should have total additional payments of $30,084.86" do
|
148
|
+
assert_equal D('-30084.86'), @exp.additional_payments.sum
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should have total interest charges of $108880.09" do
|
152
|
+
assert_equal D('108880.09'), @exp.interest.sum
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe "Numeric Method" do
|
158
|
+
it 'works with simple invocation' do
|
159
|
+
rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
160
|
+
amt_method = 300000.amortize(rate)
|
161
|
+
amt_class = Amortization.new(300000, rate)
|
162
|
+
assert_equal amt_method, amt_class
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'works with block invocation' do
|
166
|
+
rate = Rate.new(0.0375, :apr, :duration => (30 * 12))
|
167
|
+
amt_method = 300000.amortize(rate){ |period| period.payment-300 }
|
168
|
+
amt_class = Amortization.new(300000, rate){ |period| period.payment-300 }
|
169
|
+
assert_equal amt_method, amt_class
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe "Cashflows" do
|
4
|
+
describe "an array of numeric cashflows" do
|
5
|
+
it "should have an Internal Rate of Return" do
|
6
|
+
assert_equal D("0.143"), [-4000,1200,1410,1875,1050].irr.round(3)
|
7
|
+
assert_raises(ArgumentError) { [10,20,30].irr }
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have a Net Present Value" do
|
11
|
+
assert_equal D("49.211"), [-100.0, 60, 60, 60].npv(0.1).round(3)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "an array of Transactions" do
|
16
|
+
before(:all) do
|
17
|
+
@xactions=[]
|
18
|
+
@xactions << Transaction.new(-1000, :date => Time.new(1985, 1, 1))
|
19
|
+
@xactions << Transaction.new( 600, :date => Time.new(1990, 1, 1))
|
20
|
+
@xactions << Transaction.new( 600, :date => Time.new(1995, 1, 1))
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should have an Internal Rate of Return" do
|
24
|
+
assert_equal D("0.024851"), @xactions.xirr.effective.round(6)
|
25
|
+
assert_raises(ArgumentError) { @xactions[1, 2].xirr }
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should have a Net Present Value" do
|
29
|
+
assert_equal D("-937.41"), @xactions.xnpv(0.6).round(2)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'minitest/spec'
|
3
|
+
|
4
|
+
require 'active_support/all'
|
5
|
+
|
6
|
+
require 'pry'
|
7
|
+
|
8
|
+
require 'flt'
|
9
|
+
require 'flt/d'
|
10
|
+
|
11
|
+
require_relative '../lib/finance/amortization.rb'
|
12
|
+
require_relative '../lib/finance/cashflows.rb'
|
13
|
+
require_relative '../lib/finance/rates.rb'
|
14
|
+
require_relative '../lib/finance/transaction.rb'
|
15
|
+
include Finance
|