finance 0.2.0 → 1.0.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.
- data/COPYING +2 -0
- data/COPYING.LESSER +2 -0
- data/HISTORY +26 -0
- data/README +24 -22
- data/lib/finance.rb +5 -53
- data/lib/finance/amortization.rb +196 -0
- data/lib/finance/cashflows.rb +55 -0
- data/lib/finance/decimal.rb +12 -0
- data/lib/finance/interval.rb +19 -0
- data/lib/finance/rates.rb +167 -0
- data/lib/finance/transaction.rb +119 -0
- data/test/test_amortization.rb +114 -114
- data/test/test_cashflows.rb +12 -10
- data/test/test_interval.rb +18 -0
- data/test/test_rates.rb +69 -22
- metadata +19 -12
- data/lib/amortization.rb +0 -160
- data/lib/cashflows.rb +0 -33
- data/lib/rates.rb +0 -93
- data/lib/timespan.rb +0 -10
- data/test/test_timespan.rb +0 -12
data/test/test_cashflows.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
-
|
2
|
-
require 'test/unit'
|
1
|
+
require_relative '../lib/finance/cashflows.rb'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
8
|
-
end
|
3
|
+
require 'flt/d'
|
4
|
+
require 'minitest/unit'
|
5
|
+
require 'shoulda'
|
9
6
|
|
10
|
-
class
|
11
|
-
|
12
|
-
|
7
|
+
class TestCashflows < Test::Unit::TestCase
|
8
|
+
context "an array of cashflows" do
|
9
|
+
should "have an Internal Rate of Return" do
|
10
|
+
assert_equal D("0.143"), [-4000,1200,1410,1875,1050].irr.round(3)
|
11
|
+
end
|
12
|
+
should "have a Net Present Value" do
|
13
|
+
assert_equal D("49.211"), [-100.0, 60, 60, 60].npv(0.1).round(3)
|
14
|
+
end
|
13
15
|
end
|
14
16
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative '../lib/finance/interval.rb'
|
2
|
+
|
3
|
+
require 'minitest/unit'
|
4
|
+
require 'shoulda'
|
5
|
+
|
6
|
+
class TestInterval < Test::Unit::TestCase
|
7
|
+
context "a time interval" do
|
8
|
+
context "can be created from an integer" do
|
9
|
+
should "convert an integer into months" do
|
10
|
+
assert_equal 360, 360.months
|
11
|
+
end
|
12
|
+
|
13
|
+
should "convert an integer into years" do
|
14
|
+
assert_equal 360, 30.years
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/test/test_rates.rb
CHANGED
@@ -1,31 +1,78 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require_relative '../lib/finance/rates.rb'
|
2
|
+
include Finance
|
3
|
+
|
4
|
+
require 'flt'
|
3
5
|
require 'flt/d'
|
4
|
-
require '
|
6
|
+
require 'minitest/unit'
|
7
|
+
require 'shoulda'
|
5
8
|
|
6
9
|
class TestRates < Test::Unit::TestCase
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
10
|
+
context "an interest rate" do
|
11
|
+
context "can compound with different periods" do
|
12
|
+
should "compound monthly by default" do
|
13
|
+
rate = Rate.new(0.15, :nominal)
|
14
|
+
assert_equal D('0.16075'), rate.effective.round(5)
|
15
|
+
end
|
11
16
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
17
|
+
should "compound annually" do
|
18
|
+
rate = Rate.new(0.15, :nominal, :compounds => :annually)
|
19
|
+
assert_equal D('0.15'), rate.effective
|
20
|
+
end
|
21
|
+
|
22
|
+
should "compound continuously" do
|
23
|
+
rate = Rate.new(0.15, :nominal, :compounds => :continuously)
|
24
|
+
assert_equal D('0.16183'), rate.effective.round(5)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "compound daily" do
|
28
|
+
rate = Rate.new(0.15, :nominal, :compounds => :daily)
|
29
|
+
assert_equal D('0.16180'), rate.effective.round(5)
|
30
|
+
end
|
31
|
+
|
32
|
+
should "compound quarterly" do
|
33
|
+
rate = Rate.new(0.15, :nominal, :compounds => :quarterly)
|
34
|
+
assert_equal D('0.15865'), rate.effective.round(5)
|
35
|
+
end
|
36
|
+
|
37
|
+
should "compound semiannually" do
|
38
|
+
rate = Rate.new(0.15, :nominal, :compounds => :semiannually)
|
39
|
+
assert_equal D('0.15563'), rate.effective.round(5)
|
40
|
+
end
|
41
|
+
|
42
|
+
should "accept a numerical value as the compounding frequency per year" do
|
43
|
+
rate = Rate.new(0.15, :nominal, :compounds => 7)
|
44
|
+
assert_equal D('0.15999'), rate.effective.round(5)
|
45
|
+
end
|
46
|
+
|
47
|
+
should "raise an exception if an unknown string is given" do
|
48
|
+
assert_raises(ArgumentError){ Rate.new(0.15, :nominal, :compounds => :quickly) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
should "accept a duration if given" do
|
53
|
+
rate = Rate.new(0.0375, :effective, :duration => 360)
|
54
|
+
assert_equal 360, rate.duration
|
55
|
+
end
|
56
|
+
|
57
|
+
should "be comparable to other interest rates" do
|
58
|
+
r1 = Rate.new(0.15, :nominal)
|
59
|
+
r2 = Rate.new(0.16, :nominal)
|
60
|
+
assert_equal( 1, r2 <=> r1)
|
61
|
+
assert_equal(-1, r1 <=> r2)
|
62
|
+
end
|
16
63
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
64
|
+
should "convert to a monthly value" do
|
65
|
+
rate = Rate.new(0.0375, :effective)
|
66
|
+
assert_equal D('0.003125'), rate.monthly
|
67
|
+
end
|
21
68
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
69
|
+
should "convert effective interest rates to nominal" do
|
70
|
+
assert_equal D('0.03687'), Rate.to_nominal(D('0.0375'), 12).round(5)
|
71
|
+
assert_equal D('0.03681'), Rate.to_nominal(D('0.0375'), Flt::DecNum.infinity).round(5)
|
72
|
+
end
|
26
73
|
|
27
|
-
|
28
|
-
|
29
|
-
|
74
|
+
should "raise an exception if an unknown value is given for :type" do
|
75
|
+
assert_raises(ArgumentError){ Rate.new(0.0375, :foo) }
|
76
|
+
end
|
30
77
|
end
|
31
78
|
end
|
metadata
CHANGED
@@ -4,10 +4,10 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
- 2
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 1.0.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Bill Kranec
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-07-20 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: flt
|
@@ -41,19 +41,25 @@ extensions: []
|
|
41
41
|
|
42
42
|
extra_rdoc_files:
|
43
43
|
- README
|
44
|
+
- COPYING
|
45
|
+
- COPYING.LESSER
|
46
|
+
- HISTORY
|
44
47
|
files:
|
45
48
|
- README
|
46
49
|
- COPYING
|
47
50
|
- COPYING.LESSER
|
48
|
-
-
|
49
|
-
- lib/
|
51
|
+
- HISTORY
|
52
|
+
- lib/finance/amortization.rb
|
53
|
+
- lib/finance/cashflows.rb
|
54
|
+
- lib/finance/decimal.rb
|
55
|
+
- lib/finance/interval.rb
|
56
|
+
- lib/finance/rates.rb
|
57
|
+
- lib/finance/transaction.rb
|
50
58
|
- lib/finance.rb
|
51
|
-
- lib/rates.rb
|
52
|
-
- lib/timespan.rb
|
53
59
|
- test/test_amortization.rb
|
54
60
|
- test/test_cashflows.rb
|
61
|
+
- test/test_interval.rb
|
55
62
|
- test/test_rates.rb
|
56
|
-
- test/test_timespan.rb
|
57
63
|
homepage: https://rubygems.org/gems/finance
|
58
64
|
licenses: []
|
59
65
|
|
@@ -67,10 +73,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
73
|
requirements:
|
68
74
|
- - ">="
|
69
75
|
- !ruby/object:Gem::Version
|
70
|
-
hash:
|
76
|
+
hash: 29
|
71
77
|
segments:
|
72
|
-
-
|
73
|
-
|
78
|
+
- 1
|
79
|
+
- 9
|
80
|
+
version: "1.9"
|
74
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
82
|
none: false
|
76
83
|
requirements:
|
@@ -86,6 +93,6 @@ rubyforge_project:
|
|
86
93
|
rubygems_version: 1.8.5
|
87
94
|
signing_key:
|
88
95
|
specification_version: 3
|
89
|
-
summary: a library for financial
|
96
|
+
summary: a library for financial modelling in Ruby.
|
90
97
|
test_files: []
|
91
98
|
|
data/lib/amortization.rb
DELETED
@@ -1,160 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'cashflows'
|
3
|
-
require 'flt'
|
4
|
-
|
5
|
-
class Period
|
6
|
-
attr_accessor :payment
|
7
|
-
attr_accessor :principal
|
8
|
-
attr_accessor :rate
|
9
|
-
|
10
|
-
def additional_payment
|
11
|
-
@payment-@original_payment
|
12
|
-
end
|
13
|
-
|
14
|
-
# Return the remaining balance at the end of the period.
|
15
|
-
def balance
|
16
|
-
@principal + @payment + self.interest
|
17
|
-
end
|
18
|
-
|
19
|
-
def initialize(principal, rate, payment)
|
20
|
-
@principal = principal
|
21
|
-
@rate = rate
|
22
|
-
@payment = payment
|
23
|
-
@original_payment = payment
|
24
|
-
end
|
25
|
-
|
26
|
-
# Return the interest charged for the period.
|
27
|
-
def interest
|
28
|
-
(@principal * @rate).round(2)
|
29
|
-
end
|
30
|
-
|
31
|
-
def modify_payment(&modifier)
|
32
|
-
value = modifier.call(self)
|
33
|
-
|
34
|
-
# There's a chance that the block does not return a decimal.
|
35
|
-
unless value.class == Flt::DecNum
|
36
|
-
value = Flt::DecNum value.to_s
|
37
|
-
end
|
38
|
-
|
39
|
-
self.payment = value
|
40
|
-
end
|
41
|
-
|
42
|
-
def payment=(value)
|
43
|
-
total = @principal + self.interest
|
44
|
-
if value.abs > total
|
45
|
-
@payment = -total
|
46
|
-
else
|
47
|
-
@payment = value
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class Amortization
|
53
|
-
attr_accessor :balance
|
54
|
-
attr_accessor :block
|
55
|
-
attr_accessor :rate_duration
|
56
|
-
attr_accessor :payment
|
57
|
-
attr_accessor :periods
|
58
|
-
attr_accessor :principal
|
59
|
-
attr_accessor :rates
|
60
|
-
|
61
|
-
def ==(amortization)
|
62
|
-
self.principal == amortization.principal and self.rates == amortization.rates and self.payments == amortization.payments
|
63
|
-
end
|
64
|
-
|
65
|
-
def additional_payments
|
66
|
-
@periods.collect{ |period| period.additional_payment }
|
67
|
-
end
|
68
|
-
|
69
|
-
def amortize(rate)
|
70
|
-
# For the purposes of calculating a payment, the relevant time
|
71
|
-
# period is the remaining number of periods in the loan, _not_ the
|
72
|
-
# duration of the rate itself.
|
73
|
-
duration = @rate_duration - @periods.length
|
74
|
-
payment = Amortization.payment @balance, rate.monthly, duration
|
75
|
-
|
76
|
-
rate.duration.times do
|
77
|
-
# Do this first in case the balance is zero already.
|
78
|
-
if @balance.zero?
|
79
|
-
break
|
80
|
-
end
|
81
|
-
|
82
|
-
period = Period.new(@balance, rate.monthly, payment)
|
83
|
-
|
84
|
-
if @block
|
85
|
-
period.modify_payment(&@block)
|
86
|
-
end
|
87
|
-
|
88
|
-
@periods << period
|
89
|
-
@balance = period.balance
|
90
|
-
end
|
91
|
-
|
92
|
-
if @rates.length == 1
|
93
|
-
@payment = self.payments[0]
|
94
|
-
else
|
95
|
-
@payment = nil
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# Compute the amortization of the principal.
|
100
|
-
def compute
|
101
|
-
@balance = @principal
|
102
|
-
@periods = []
|
103
|
-
|
104
|
-
@rates.each do |rate|
|
105
|
-
amortize(rate)
|
106
|
-
end
|
107
|
-
|
108
|
-
# Add any remaining balance due to rounding error to the last payment.
|
109
|
-
unless @balance.zero?
|
110
|
-
@periods[-1].payment -= @balance
|
111
|
-
@balance = 0
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def duration
|
116
|
-
@periods.length
|
117
|
-
end
|
118
|
-
|
119
|
-
def initialize(principal, *rates, &block)
|
120
|
-
@principal = principal
|
121
|
-
@rates = rates
|
122
|
-
@rate_duration = (rates.collect { |r| r.duration }).sum
|
123
|
-
@block = block
|
124
|
-
|
125
|
-
compute
|
126
|
-
end
|
127
|
-
|
128
|
-
def inspect
|
129
|
-
"Amortization.new(#{@principal}"
|
130
|
-
end
|
131
|
-
|
132
|
-
# Return an Array with the amount of interest charged in each period.
|
133
|
-
def interest
|
134
|
-
@periods.collect { |period| period.interest }
|
135
|
-
end
|
136
|
-
|
137
|
-
# Return the periodic payment due on a loan, based on the
|
138
|
-
#{http://en.wikipedia.org/wiki/Amortization_calculator amortization process}.
|
139
|
-
def Amortization.payment(balance, rate, periods)
|
140
|
-
-(balance * (rate + (rate / ((1 + rate) ** periods - 1)))).round(2)
|
141
|
-
end
|
142
|
-
|
143
|
-
# Return an array with the payment amount for each period.
|
144
|
-
def payments
|
145
|
-
@periods.collect { |period| period.payment }
|
146
|
-
end
|
147
|
-
|
148
|
-
# "Pretty print" a text amortization table.
|
149
|
-
def pprint
|
150
|
-
@periods.each_with_index do |p, i|
|
151
|
-
puts "%03d $%9s %8s $%7s $%7s $%9s" % [i, p.principal, p.rate, p.payment, p.interest, p.balance]
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
class Numeric
|
157
|
-
def amortize(rate, &block)
|
158
|
-
amortization = Amortization.new(self, rate, &block)
|
159
|
-
end
|
160
|
-
end
|
data/lib/cashflows.rb
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
module Cashflow
|
2
|
-
# Return the {http://en.wikipedia.org/wiki/Internal_rate_of_return
|
3
|
-
# internal rate of return} for a given sequence of cashflows.
|
4
|
-
def irr(iterations=100)
|
5
|
-
rate = 1.0
|
6
|
-
investment = self[0]
|
7
|
-
for i in 1..iterations+1
|
8
|
-
rate = rate * (1 - self.npv(rate) / investment)
|
9
|
-
end
|
10
|
-
rate
|
11
|
-
end
|
12
|
-
|
13
|
-
# Return the {http://en.wikipedia.org/wiki/Net_present_value net present value} of a sequence of cash flows given
|
14
|
-
# the discount rate _rate_.
|
15
|
-
def npv(rate)
|
16
|
-
total = 0.0
|
17
|
-
self.each_with_index do |cashflow, index|
|
18
|
-
total = total + cashflow / (1+rate) ** index
|
19
|
-
end
|
20
|
-
total
|
21
|
-
end
|
22
|
-
|
23
|
-
def sum
|
24
|
-
self.inject(:+)
|
25
|
-
end
|
26
|
-
|
27
|
-
def xirr
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
class Array
|
32
|
-
include Cashflow
|
33
|
-
end
|
data/lib/rates.rb
DELETED
@@ -1,93 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'flt'
|
3
|
-
|
4
|
-
class Rate
|
5
|
-
attr_accessor :duration
|
6
|
-
attr_accessor :periods
|
7
|
-
attr_accessor :nominal
|
8
|
-
|
9
|
-
def ==(rate)
|
10
|
-
self.nominal == rate.nominal and self.periods == rate.periods
|
11
|
-
end
|
12
|
-
|
13
|
-
# Alias method for *effective*.
|
14
|
-
def apr=(apr)
|
15
|
-
self.effective = apr
|
16
|
-
end
|
17
|
-
|
18
|
-
def apr
|
19
|
-
self.effective
|
20
|
-
end
|
21
|
-
|
22
|
-
# Alias method for *effective*.
|
23
|
-
def apy=(apy)
|
24
|
-
self.effective = apy
|
25
|
-
end
|
26
|
-
|
27
|
-
def apy
|
28
|
-
self.effective
|
29
|
-
end
|
30
|
-
|
31
|
-
def effective=(rate)
|
32
|
-
unless @periods == Flt::DecNum.infinity
|
33
|
-
@nominal = @periods * ((1 + rate) ** (1 / @periods) - 1)
|
34
|
-
else
|
35
|
-
@nominal = Math.log(rate + 1)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def effective
|
40
|
-
unless @periods == Flt::DecNum.infinity
|
41
|
-
(1 + @nominal / @periods) ** @periods - 1
|
42
|
-
else
|
43
|
-
@nominal.exp - 1
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def initialize(rate, type, opts={})
|
48
|
-
# Make sure the rate is a decimal.
|
49
|
-
unless rate.class == Flt::DecNum
|
50
|
-
rate = Flt::DecNum rate.to_s
|
51
|
-
end
|
52
|
-
|
53
|
-
# Set the compounding interval.
|
54
|
-
compounding = opts.fetch(:compounds, :monthly)
|
55
|
-
|
56
|
-
translate = {
|
57
|
-
:annually => Flt::DecNum(1),
|
58
|
-
:continuously => Flt::DecNum.infinity,
|
59
|
-
:daily => Flt::DecNum(365),
|
60
|
-
:monthly => Flt::DecNum(12),
|
61
|
-
:quarterly => Flt::DecNum(4),
|
62
|
-
:semiannually => Flt::DecNum(2)
|
63
|
-
}
|
64
|
-
|
65
|
-
if translate.has_key? compounding
|
66
|
-
@periods = translate.fetch compounding
|
67
|
-
elsif compounding.kind_of? Numeric
|
68
|
-
@periods = Flt::DecNum compounding.to_s
|
69
|
-
end
|
70
|
-
|
71
|
-
# Set the rate in the proper way, based on the value of :type:.
|
72
|
-
if %w{apr apy effective}.include? type.to_s
|
73
|
-
self.effective = rate
|
74
|
-
else
|
75
|
-
@nominal = rate
|
76
|
-
end
|
77
|
-
|
78
|
-
# Set the remainder of the attributes provided in :opts:.
|
79
|
-
opts.each do |key, value|
|
80
|
-
unless key == :compounds
|
81
|
-
send("#{key}=", value)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def inspect
|
87
|
-
"Rate.new(#{self.apr.round(6)}, :apr)"
|
88
|
-
end
|
89
|
-
|
90
|
-
def monthly
|
91
|
-
(self.effective / 12).round(15)
|
92
|
-
end
|
93
|
-
end
|