finance_velocity 2.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|