doughnut 0.0.3 → 0.0.4
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 +4 -4
- data/Rakefile +1 -0
- data/doughnut.gemspec +1 -0
- data/lib/doughnut.rb +8 -3
- data/lib/doughnut/monte_carlo/population_factory.rb +31 -0
- data/lib/doughnut/monte_carlo/population_tester.rb +0 -0
- data/lib/doughnut/monte_carlo/predicted_return.rb +19 -0
- data/lib/doughnut/{expenses.rb → retirement_calculator/expenses.rb} +9 -9
- data/lib/doughnut/{income.rb → retirement_calculator/income.rb} +2 -10
- data/lib/doughnut/{retirement_calculator.rb → retirement_calculator/retirement_calculator.rb} +5 -1
- data/lib/doughnut/tasks/retirement.rake +14 -0
- data/lib/doughnut/version.rb +1 -1
- data/spec/monte_carlo/population_factory_spec.rb +121 -0
- data/spec/monte_carlo/population_tester.rb +14 -0
- data/spec/monte_carlo/predicted_return_spec.rb +78 -0
- data/spec/{expenses_spec.rb → retirement_calculator/expenses_spec.rb} +0 -0
- data/spec/{income_spec.rb → retirement_calculator/income_spec.rb} +0 -0
- data/spec/retirement_calculator/retirement_calculator_spec.rb +142 -0
- metadata +35 -11
- data/spec/retirement_calculator_spec.rb +0 -75
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0dcdc95522dc60f4014b3d243542e9312b09521
|
4
|
+
data.tar.gz: 5b25a23ebadd05e66344b8d1ed110d62ba15a87b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23b34df6870d38f36362481b8543b6773700c2dd1710dcef05f73d29c8db10944d0474db4efc2356a6f3004cbd997736084b80735a807c63d25261467c289116
|
7
|
+
data.tar.gz: d80e2c1cff77910bca4634d8d5e5979fdfb2fdd73b3bcb75fe24f4272f02fa4b2eaf9186357e83a09038eca7e455ce573d088fb6e1bf81f0decaf650b8d70535
|
data/Rakefile
CHANGED
data/doughnut.gemspec
CHANGED
data/lib/doughnut.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
-
require "doughnut/
|
2
|
-
require "doughnut/
|
3
|
-
require "doughnut/
|
1
|
+
require "doughnut/monte_carlo/population_factory"
|
2
|
+
require "doughnut/monte_carlo/population_tester"
|
3
|
+
require "doughnut/monte_carlo/predicted_return"
|
4
|
+
|
5
|
+
require "doughnut/retirement_calculator/expenses"
|
6
|
+
require "doughnut/retirement_calculator/income"
|
7
|
+
require "doughnut/retirement_calculator/retirement_calculator"
|
8
|
+
|
4
9
|
require "doughnut/version"
|
5
10
|
|
6
11
|
module Doughnut
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Doughnut
|
2
|
+
|
3
|
+
class PopulationFactory
|
4
|
+
|
5
|
+
def initialize(list_of_assets)
|
6
|
+
@num_genomes = 10
|
7
|
+
@list_of_assets = list_of_assets
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_random_population
|
11
|
+
Array.new(@num_genomes, random_genome)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def random_genome
|
17
|
+
output = []
|
18
|
+
@list_of_assets.each_with_index do |asset, indx|
|
19
|
+
output << asset.merge!({fraction: gene_fractions[indx]})
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def gene_fractions
|
24
|
+
fracs = Array.new(@list_of_assets.length, rand)
|
25
|
+
s = fracs.sum
|
26
|
+
fracs.map { |x| x/s }
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
File without changes
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "rubystats"
|
2
|
+
|
3
|
+
module Doughnut
|
4
|
+
|
5
|
+
class PredictedReturn
|
6
|
+
|
7
|
+
def initialize(avg_return = 0, standard_deviation = 0)
|
8
|
+
@avg_return = avg_return
|
9
|
+
@standard_deviation = standard_deviation
|
10
|
+
end
|
11
|
+
|
12
|
+
def return(num_data_points = 1)
|
13
|
+
return @avg_return if @standard_deviation == 0
|
14
|
+
Rubystats::NormalDistribution.new(@avg_return, @standard_deviation).rng
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -9,14 +9,6 @@ module Doughnut
|
|
9
9
|
@inflation_rate = 0.0322
|
10
10
|
end
|
11
11
|
|
12
|
-
def future_expenses(death_date, discount_rate)
|
13
|
-
output = []
|
14
|
-
(Date.today..death_date).each do |mydate|
|
15
|
-
output << present_value(mydate, discount_rate) if is_last_day(mydate)
|
16
|
-
end
|
17
|
-
output
|
18
|
-
end
|
19
|
-
|
20
12
|
def total_expenses(death_date, discount_rate)
|
21
13
|
output = 0
|
22
14
|
future_expenses(death_date, discount_rate).each do |h|
|
@@ -25,9 +17,17 @@ module Doughnut
|
|
25
17
|
output
|
26
18
|
end
|
27
19
|
|
20
|
+
def future_expenses(death_date, discount_rate)
|
21
|
+
output = []
|
22
|
+
(Date.today..death_date).each do |mydate|
|
23
|
+
output << present_value(mydate, discount_rate) if is_last_day_of_month(mydate)
|
24
|
+
end
|
25
|
+
output
|
26
|
+
end
|
27
|
+
|
28
28
|
private
|
29
29
|
|
30
|
-
def
|
30
|
+
def is_last_day_of_month(mydate)
|
31
31
|
mydate.month != mydate.next_day.month
|
32
32
|
end
|
33
33
|
|
@@ -12,22 +12,14 @@ module Doughnut
|
|
12
12
|
def future_income(death_date, discount_rate)
|
13
13
|
output = []
|
14
14
|
(Date.today..death_date).each do |mydate|
|
15
|
-
output << present_value(mydate, discount_rate) if
|
16
|
-
end
|
17
|
-
output
|
18
|
-
end
|
19
|
-
|
20
|
-
def total_income(death_date, discount_rate)
|
21
|
-
output = 0
|
22
|
-
future_expenses(death_date, discount_rate).each do |h|
|
23
|
-
output += h[:income]
|
15
|
+
output << present_value(mydate, discount_rate) if is_last_day_of_month(mydate)
|
24
16
|
end
|
25
17
|
output
|
26
18
|
end
|
27
19
|
|
28
20
|
private
|
29
21
|
|
30
|
-
def
|
22
|
+
def is_last_day_of_month(mydate)
|
31
23
|
mydate.month != mydate.next_day.month
|
32
24
|
end
|
33
25
|
|
data/lib/doughnut/{retirement_calculator.rb → retirement_calculator/retirement_calculator.rb}
RENAMED
@@ -3,15 +3,19 @@ module Doughnut
|
|
3
3
|
class RetirementCalculator
|
4
4
|
|
5
5
|
attr_accessor :death_date, :portfolio_return
|
6
|
+
attr_accessor :current_net_worth
|
6
7
|
|
7
|
-
def initialize
|
8
|
+
def initialize(net_worth = 207000)
|
8
9
|
@death_date = Date.new(2067,7,19)
|
10
|
+
@current_net_worth = net_worth
|
9
11
|
@portfolio_return = 0.1
|
10
12
|
end
|
11
13
|
|
12
14
|
def retirement_date(total_expenses, monthly_incomes)
|
13
15
|
return Date.today if total_expenses == 0
|
16
|
+
return Date.today if total_expenses < @current_net_worth
|
14
17
|
return @death_date if monthly_incomes.length == 0
|
18
|
+
monthly_incomes.insert( 0, {date: Date.today, income: @current_net_worth} )
|
15
19
|
running_income = 0
|
16
20
|
monthly_incomes.each do |h|
|
17
21
|
running_income += h[:income]
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "doughnut/retirement_calculator/expenses"
|
2
|
+
require "doughnut/retirement_calculator/income"
|
3
|
+
require "doughnut/retirement_calculator/retirement_calculator"
|
4
|
+
|
5
|
+
task :calculate_retirement do
|
6
|
+
e = Expenses.new
|
7
|
+
i = Income.new
|
8
|
+
r = RetirementCalculator.new
|
9
|
+
death_date = r.death_date
|
10
|
+
discount_rate = r.portfolio_return
|
11
|
+
e_tot = e.total_expenses(death_date, discount_rate)
|
12
|
+
i_tot = i.future_income(death_date, discount_rate)
|
13
|
+
puts "#{r.retirement_date(e_tot, i_tot)}"
|
14
|
+
end
|
data/lib/doughnut/version.rb
CHANGED
@@ -0,0 +1,121 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Doughnut
|
4
|
+
|
5
|
+
describe PopulationFactory do
|
6
|
+
|
7
|
+
describe "building a population of random genomes" do
|
8
|
+
|
9
|
+
context "one input" do
|
10
|
+
|
11
|
+
before(:each) do
|
12
|
+
@population = PopulationFactory.new( [{name: "X", avg_return: 1, standard_deviation: 5}] ).build_random_population
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns ten genomes" do
|
16
|
+
expect(@population.length).to eq 10
|
17
|
+
end
|
18
|
+
|
19
|
+
it "each genome has a length equal to one" do
|
20
|
+
@population.each do |genome|
|
21
|
+
expect(genome.length).to eq 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "each gene in a given genome has a name" do
|
26
|
+
@population.each do |genome|
|
27
|
+
genome.each do |gene|
|
28
|
+
expect(gene[:name]).not_to be nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "each gene in a given genome has an average return" do
|
34
|
+
@population.each do |genome|
|
35
|
+
genome.each do |gene|
|
36
|
+
expect(gene[:avg_return]).to eq 1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "each gene in a given genome has a standard deviation" do
|
42
|
+
@population.each do |genome|
|
43
|
+
genome.each do |gene|
|
44
|
+
expect(gene[:standard_deviation]).to eq 5
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "each gene in a given genome has a fraction of one" do
|
50
|
+
@population.each do |genome|
|
51
|
+
genome.each do |gene|
|
52
|
+
expect(gene[:fraction]).to eq 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
context "multiple inputs" do
|
60
|
+
|
61
|
+
before(:each) do
|
62
|
+
@population = PopulationFactory.new( [{name: "X", avg_return: 1, standard_deviation: 5},{name: "Y", avg_return: 3, standard_deviation: 7}] ).build_random_population
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns ten genomes" do
|
66
|
+
expect(@population.length).to eq 10
|
67
|
+
end
|
68
|
+
|
69
|
+
it "each genome has a length greater than one" do
|
70
|
+
@population.each do |genome|
|
71
|
+
expect(genome.length).to eq 2
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "each gene in a given genome has a name" do
|
76
|
+
@population.each do |genome|
|
77
|
+
genome.each do |gene|
|
78
|
+
expect(gene[:name]).not_to be nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it "each gene in a given genome has an average return" do
|
84
|
+
@population.each do |genome|
|
85
|
+
genome.each do |gene|
|
86
|
+
expect(gene[:avg_return] == 1 || gene[:avg_return] == 3).to be true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it "each gene in a given genome has a standard deviation" do
|
92
|
+
@population.each do |genome|
|
93
|
+
genome.each do |gene|
|
94
|
+
expect(gene[:standard_deviation] == 5 || gene[:standard_deviation] == 7).to be true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "each fraction is NOT equal to one" do
|
100
|
+
@population.each do |genome|
|
101
|
+
genome.each do |gene|
|
102
|
+
expect(gene[:fraction]).not_to eq 1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it "the sum of all fractions equals one" do
|
108
|
+
@population.each do |genome|
|
109
|
+
sum = 0
|
110
|
+
genome.each { |gene| sum += gene[:fraction] }
|
111
|
+
expect(sum).to eq 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "descriptive_statistics"
|
3
|
+
|
4
|
+
module Doughnut
|
5
|
+
|
6
|
+
describe PredictedReturn do
|
7
|
+
|
8
|
+
describe "#return" do
|
9
|
+
|
10
|
+
context "boundary conditions" do
|
11
|
+
|
12
|
+
it "gives zero predicted return if there are no inputs" do
|
13
|
+
expect(PredictedReturn.new.return).to eq 0
|
14
|
+
end
|
15
|
+
|
16
|
+
it "gives zero predicted return if both average return and standard deviation are zero" do
|
17
|
+
expect(PredictedReturn.new(0,0).return).to eq 0
|
18
|
+
end
|
19
|
+
|
20
|
+
it "gives the average return if standard deviation is zero" do
|
21
|
+
expect(PredictedReturn.new(0.5,0).return).to eq 0.5
|
22
|
+
end
|
23
|
+
|
24
|
+
it "gives a range of returns if the standard deviation is zero" do
|
25
|
+
expect(PredictedReturn.new(0,0.0001).return).to be > -1
|
26
|
+
expect(PredictedReturn.new(0,0.0001).return).to be < 1
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
context "Gaussian outputs" do
|
32
|
+
|
33
|
+
before(:each) do
|
34
|
+
@simulation = PredictedReturn.new(0.4, 0.1)
|
35
|
+
@thin_sim = PredictedReturn.new(0.8, 0.00000000001)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "the data points should be unique" do
|
39
|
+
output = []
|
40
|
+
50.times { output << @simulation.return }
|
41
|
+
expect(output.uniq.length).to eq 50
|
42
|
+
end
|
43
|
+
|
44
|
+
it "the data points should have the given mean" do
|
45
|
+
output = []
|
46
|
+
50.times { output << @simulation.return }
|
47
|
+
expect(output.mean).to be > 0.25
|
48
|
+
expect(output.mean).to be < 0.45
|
49
|
+
end
|
50
|
+
|
51
|
+
it "the data points should have the given standard deviation" do
|
52
|
+
output = []
|
53
|
+
50.times { output << @simulation.return }
|
54
|
+
expect(output.standard_deviation).to be > 0.0
|
55
|
+
expect(output.standard_deviation).to be < 0.2
|
56
|
+
end
|
57
|
+
|
58
|
+
it "creates data points near the average return if the standard deviation is small" do
|
59
|
+
output = []
|
60
|
+
50.times { output << @thin_sim.return }
|
61
|
+
expect(output.mean).to be > 0.7999
|
62
|
+
expect(output.mean).to be < 0.8001
|
63
|
+
end
|
64
|
+
|
65
|
+
it "creates closely clustered data points if the standard deviation is small" do
|
66
|
+
output = []
|
67
|
+
50.times { output << @thin_sim.return }
|
68
|
+
expect(output.standard_deviation).to be > 0.0000
|
69
|
+
expect(output.standard_deviation).to be < 0.0002
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
File without changes
|
File without changes
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Doughnut
|
4
|
+
|
5
|
+
describe RetirementCalculator do
|
6
|
+
|
7
|
+
describe "parameters" do
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
@retire = RetirementCalculator.new
|
11
|
+
end
|
12
|
+
|
13
|
+
context "defaults" do
|
14
|
+
|
15
|
+
it "returns a death date of 80 years old" do
|
16
|
+
expect(@retire.death_date).to eq Date.new(2067,7,19)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns average portfolio return of 10%" do
|
20
|
+
expect(@retire.portfolio_return).to eq 0.1
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns a current net worth of 207000" do
|
24
|
+
expect(@retire.current_net_worth).to eq 207000
|
25
|
+
end
|
26
|
+
|
27
|
+
it "updates the death date" do
|
28
|
+
@retire.death_date = Date.new(2001,3,7)
|
29
|
+
expect(@retire.death_date).to eq Date.new(2001,3,7)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "updates the average portfolio return" do
|
33
|
+
@retire.portfolio_return = 0.99
|
34
|
+
expect(@retire.portfolio_return).to eq 0.99
|
35
|
+
end
|
36
|
+
|
37
|
+
it "updates the current net worth" do
|
38
|
+
@retire.current_net_worth = 100
|
39
|
+
expect(@retire.current_net_worth).to eq 100
|
40
|
+
end
|
41
|
+
|
42
|
+
it "sets the net worth on initialization" do
|
43
|
+
worth = RetirementCalculator.new(200).current_net_worth
|
44
|
+
expect(worth).to eq 200
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "retirement date with no initial net worth" do
|
52
|
+
|
53
|
+
before(:each) do
|
54
|
+
@retire = RetirementCalculator.new(0)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "returns today's date if there are no monthly expenses or incomes" do
|
58
|
+
expect(@retire.retirement_date(0,[])).to eq Date.today
|
59
|
+
end
|
60
|
+
|
61
|
+
it "returns today's date if there are no monthly expenses but positive incomes" do
|
62
|
+
expect(@retire.retirement_date(0, [{date: Date.new(2012,1,2), income: 100}])).to eq Date.today
|
63
|
+
end
|
64
|
+
|
65
|
+
it "returns the death date if there is no monthly income" do
|
66
|
+
expect(@retire.retirement_date(1,[])).to eq Date.new(2067,7,19)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "returns the date of earned income if PV(income) > PV(expenses)" do
|
70
|
+
params = [{:date => Date.new(2040,1,2), :income => 2000}]
|
71
|
+
expect(@retire.retirement_date(1000, params)).to eq Date.new(2040,1,2)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "returns the date of earned income if PV(income) = PV(expenses)" do
|
75
|
+
params = [{:date => Date.new(2041,2,8), :income => 1000}]
|
76
|
+
expect(@retire.retirement_date(1000, params)).to eq Date.new(2041,2,8)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "returns the date of earned income if PV(income) > PV(expenses) for multiple incomes" do
|
80
|
+
params = [{:date => Date.new(2040,1,2), :income => 999}, {:date => Date.new(2049,7,8), :income => 1000}]
|
81
|
+
expect(@retire.retirement_date(1000, params)).to eq Date.new(2049,7,8)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "returns the date of earned income if PV(income) = PV(expenses) for multiple incomes" do
|
85
|
+
params = [{:date => Date.new(2040,1,2), :income => 999}, {:date => Date.new(2049,7,8), :income => 1}]
|
86
|
+
expect(@retire.retirement_date(1000, params)).to eq Date.new(2049,7,8)
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "retirement date with non-zero initial net worth" do
|
92
|
+
|
93
|
+
before(:each) do
|
94
|
+
@retire = RetirementCalculator.new(1000)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "returns today's date if there are no monthly expenses or incomes" do
|
98
|
+
expect(@retire.retirement_date(0,[])).to eq Date.today
|
99
|
+
end
|
100
|
+
|
101
|
+
it "returns today's date if there are no monthly expenses but positive incomes" do
|
102
|
+
expect(@retire.retirement_date(0, [{date: Date.new(2011,1,2), income: 100}])).to eq Date.today
|
103
|
+
end
|
104
|
+
|
105
|
+
it "returns today's if there is no monthly income, but net worth exceeds expenses" do
|
106
|
+
expect(@retire.retirement_date(800,[])).to eq Date.today
|
107
|
+
end
|
108
|
+
|
109
|
+
it "returns the death date if there is no monthly income and net worth is less than expenses" do
|
110
|
+
expect(@retire.retirement_date(2000,[])).to eq Date.new(2067,7,19)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "returns the date of earned income if current_net_worth + PV(income) > PV(expenses) (test 1)" do
|
114
|
+
params = [{:date => Date.new(2040,1,2), :income => 2000}]
|
115
|
+
expect(@retire.retirement_date(1100, params)).to eq Date.new(2040,1,2)
|
116
|
+
end
|
117
|
+
|
118
|
+
it "returns the date of earned income if current_net_worth + PV(income) > PV(expenses) (test 2)" do
|
119
|
+
params = [{:date => Date.new(2032,1,5), :income => 980}]
|
120
|
+
expect(@retire.retirement_date(1500, params)).to eq Date.new(2032,1,5)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "returns the date of earned income if current_net_worth + PV(income) = PV(expenses)" do
|
124
|
+
params = [{:date => Date.new(2021,8,8), :income => 1000}]
|
125
|
+
expect(@retire.retirement_date(2000, params)).to eq Date.new(2021,8,8)
|
126
|
+
end
|
127
|
+
|
128
|
+
it "returns the date of earned income if current_net_worth + PV(income) > PV(expenses) for multiple incomes" do
|
129
|
+
params = [{:date => Date.new(2040,1,2), :income => 999}, {:date => Date.new(2049,7,8), :income => 1000}]
|
130
|
+
expect(@retire.retirement_date(2500, params)).to eq Date.new(2049,7,8)
|
131
|
+
end
|
132
|
+
|
133
|
+
it "returns the date of earned income if current_net_worth + PV(income) = PV(expenses) for multiple incomes" do
|
134
|
+
params = [{:date => Date.new(2040,1,2), :income => 999}, {:date => Date.new(2049,7,8), :income => 1}]
|
135
|
+
expect(@retire.retirement_date(2000, params)).to eq Date.new(2049,7,8)
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: doughnut
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyrus Vandrevala
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-06-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubystats
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
description: ''
|
70
84
|
email:
|
71
85
|
- cyrus.vandrevala@gmail.com
|
@@ -80,13 +94,20 @@ files:
|
|
80
94
|
- Rakefile
|
81
95
|
- doughnut.gemspec
|
82
96
|
- lib/doughnut.rb
|
83
|
-
- lib/doughnut/
|
84
|
-
- lib/doughnut/
|
85
|
-
- lib/doughnut/
|
97
|
+
- lib/doughnut/monte_carlo/population_factory.rb
|
98
|
+
- lib/doughnut/monte_carlo/population_tester.rb
|
99
|
+
- lib/doughnut/monte_carlo/predicted_return.rb
|
100
|
+
- lib/doughnut/retirement_calculator/expenses.rb
|
101
|
+
- lib/doughnut/retirement_calculator/income.rb
|
102
|
+
- lib/doughnut/retirement_calculator/retirement_calculator.rb
|
103
|
+
- lib/doughnut/tasks/retirement.rake
|
86
104
|
- lib/doughnut/version.rb
|
87
|
-
- spec/
|
88
|
-
- spec/
|
89
|
-
- spec/
|
105
|
+
- spec/monte_carlo/population_factory_spec.rb
|
106
|
+
- spec/monte_carlo/population_tester.rb
|
107
|
+
- spec/monte_carlo/predicted_return_spec.rb
|
108
|
+
- spec/retirement_calculator/expenses_spec.rb
|
109
|
+
- spec/retirement_calculator/income_spec.rb
|
110
|
+
- spec/retirement_calculator/retirement_calculator_spec.rb
|
90
111
|
- spec/spec_helper.rb
|
91
112
|
homepage: ''
|
92
113
|
licenses:
|
@@ -113,7 +134,10 @@ signing_key:
|
|
113
134
|
specification_version: 4
|
114
135
|
summary: Generate statistics for a personal portfolio.
|
115
136
|
test_files:
|
116
|
-
- spec/
|
117
|
-
- spec/
|
118
|
-
- spec/
|
137
|
+
- spec/monte_carlo/population_factory_spec.rb
|
138
|
+
- spec/monte_carlo/population_tester.rb
|
139
|
+
- spec/monte_carlo/predicted_return_spec.rb
|
140
|
+
- spec/retirement_calculator/expenses_spec.rb
|
141
|
+
- spec/retirement_calculator/income_spec.rb
|
142
|
+
- spec/retirement_calculator/retirement_calculator_spec.rb
|
119
143
|
- spec/spec_helper.rb
|
@@ -1,75 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
module Doughnut
|
4
|
-
|
5
|
-
describe RetirementCalculator do
|
6
|
-
|
7
|
-
before(:each) do
|
8
|
-
@retire = RetirementCalculator.new
|
9
|
-
end
|
10
|
-
|
11
|
-
describe "parameters" do
|
12
|
-
|
13
|
-
context "defaults" do
|
14
|
-
|
15
|
-
it "returns a death date of 80 years old" do
|
16
|
-
expect(@retire.death_date).to eq Date.new(2067,7,19)
|
17
|
-
end
|
18
|
-
|
19
|
-
it "returns average portfolio return of 10%" do
|
20
|
-
expect(@retire.portfolio_return).to eq 0.1
|
21
|
-
end
|
22
|
-
|
23
|
-
it "updates the death date" do
|
24
|
-
@retire.death_date = Date.new(2001,3,7)
|
25
|
-
expect(@retire.death_date).to eq Date.new(2001,3,7)
|
26
|
-
end
|
27
|
-
|
28
|
-
it "updates the average portfolio return" do
|
29
|
-
@retire.portfolio_return = 0.99
|
30
|
-
expect(@retire.portfolio_return).to eq 0.99
|
31
|
-
end
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
end
|
36
|
-
|
37
|
-
describe "retirement date" do
|
38
|
-
|
39
|
-
it "returns today's date if there are no monthly expenses or incomes" do
|
40
|
-
expect(@retire.retirement_date(0,[])).to eq Date.today
|
41
|
-
end
|
42
|
-
|
43
|
-
it "returns today's date if there are no monthly expenses but positive incomes" do
|
44
|
-
expect(@retire.retirement_date(0, [{date: Date.new(2012,1,2), income: 100}])).to eq Date.today
|
45
|
-
end
|
46
|
-
|
47
|
-
it "returns the death date if there is no monthly income" do
|
48
|
-
expect(@retire.retirement_date(1,[])).to eq Date.new(2067,7,19)
|
49
|
-
end
|
50
|
-
|
51
|
-
it "returns the date of earned income if PV(income) > PV(expenses)" do
|
52
|
-
params = [{:date => Date.new(2040,1,2), :income => 2000}]
|
53
|
-
expect(@retire.retirement_date(1000, params)).to eq Date.new(2040,1,2)
|
54
|
-
end
|
55
|
-
|
56
|
-
it "returns the date of earned income if PV(income) = PV(expenses)" do
|
57
|
-
params = [{:date => Date.new(2041,2,8), :income => 1000}]
|
58
|
-
expect(@retire.retirement_date(1000, params)).to eq Date.new(2041,2,8)
|
59
|
-
end
|
60
|
-
|
61
|
-
it "returns the date of earned income if PV(income) > PV(expenses) for multiple incomes" do
|
62
|
-
params = [{:date => Date.new(2040,1,2), :income => 999}, {:date => Date.new(2049,7,8), :income => 1000}]
|
63
|
-
expect(@retire.retirement_date(1000, params)).to eq Date.new(2049,7,8)
|
64
|
-
end
|
65
|
-
|
66
|
-
it "returns the date of earned income if PV(income) = PV(expenses) for multiple incomes" do
|
67
|
-
params = [{:date => Date.new(2040,1,2), :income => 999}, {:date => Date.new(2049,7,8), :income => 1}]
|
68
|
-
expect(@retire.retirement_date(1000, params)).to eq Date.new(2049,7,8)
|
69
|
-
end
|
70
|
-
|
71
|
-
end
|
72
|
-
|
73
|
-
end
|
74
|
-
|
75
|
-
end
|