rebalance 0.0.1
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/.gitignore +6 -0
- data/Gemfile +4 -0
- data/README.md +100 -0
- data/Rakefile +10 -0
- data/lib/rebalance.rb +4 -0
- data/lib/rebalance/account.rb +42 -0
- data/lib/rebalance/fund.rb +45 -0
- data/lib/rebalance/rebalancer.rb +306 -0
- data/lib/rebalance/target.rb +84 -0
- data/lib/rebalance/version.rb +3 -0
- data/rebalance.gemspec +26 -0
- data/spec/cassettes/price_lookup_for_98765.yml +40 -0
- data/spec/cassettes/price_lookup_for_VISVX.yml +42 -0
- data/spec/cassettes/price_lookup_for_VMMXX.yml +41 -0
- data/spec/rebalance/account_spec.rb +37 -0
- data/spec/rebalance/fund_spec.rb +28 -0
- data/spec/rebalance/rebalancer_spec.rb +285 -0
- data/spec/rebalance/target_spec.rb +138 -0
- data/spec/spec_helper.rb +46 -0
- metadata +116 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Rebalance::Target do
|
4
|
+
before do
|
5
|
+
@target = Rebalance::Target.new do
|
6
|
+
asset_class 30, 'Some Asset Class'
|
7
|
+
asset_class 20, 'Another Asset Class'
|
8
|
+
asset_class 50, 'Bonds'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'allows asset_class to be called' do
|
13
|
+
@target.asset_classes.size.must_equal 3
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'remembers the asset class description' do
|
17
|
+
@target.asset_classes['Some Asset Class'].must_equal 30
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'with account information' do
|
21
|
+
before do
|
22
|
+
@account = Rebalance::Account.new 'Test Account' do
|
23
|
+
fund 'ABCDE', 'Some Asset Class', 500, 10.00
|
24
|
+
fund 'FGHIJ', 'Some Asset Class', 300, 25.00
|
25
|
+
fund 'KLMNO', 'Another Asset Class', 75, 300
|
26
|
+
fund 'PQRST', 'Bonds', 35.5, 32.00
|
27
|
+
fund 'UVWXY', 'Bonds', 75, 5.50
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'calculates the target value for each asset class' do
|
32
|
+
target_values = @target.calculate_target_asset_class_values(@account)
|
33
|
+
|
34
|
+
expected_target_values = {
|
35
|
+
'Some Asset Class' => 10964.55,
|
36
|
+
'Another Asset Class' => 7309.70,
|
37
|
+
'Bonds' => 18274.25
|
38
|
+
}
|
39
|
+
|
40
|
+
target_values.must_equal expected_target_values
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'calculates the current value for each asset class' do
|
44
|
+
current_values = @target.calculate_current_asset_class_values(@account)
|
45
|
+
|
46
|
+
expected_current_values = {
|
47
|
+
'Some Asset Class' => 12500.00,
|
48
|
+
'Another Asset Class' => 22500.00,
|
49
|
+
'Bonds' => 1548.50
|
50
|
+
}
|
51
|
+
|
52
|
+
current_values.must_equal expected_current_values
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'with multiple accounts' do
|
57
|
+
before do
|
58
|
+
@wifes_roth = Rebalance::Account.new "Wife's Roth" do
|
59
|
+
fund 'ABCDE', 'Some Asset Class', 500, 10.00 # $5,000
|
60
|
+
fund 'FGHIJ', 'Some Asset Class', 300, 25.00 # $7,500
|
61
|
+
fund 'KLMNO', 'Another Asset Class', 75, 300 # $22,500
|
62
|
+
fund 'PQRST', 'Bonds', 35.5, 32.00 # $1,136
|
63
|
+
fund 'UVWXY', 'Bonds', 75, 5.50 # $412.50
|
64
|
+
end
|
65
|
+
|
66
|
+
@my_roth = Rebalance::Account.new 'My Roth' do
|
67
|
+
fund 'AAAAA', 'Cash', 150, 1.00 # $150
|
68
|
+
fund 'BBBBB', 'Some Asset Class', 10, 23.00 # $230
|
69
|
+
fund 'FGHIJ', 'Some Asset Class', 100, 25.00 # $2,500
|
70
|
+
end
|
71
|
+
|
72
|
+
@my_sep_ira = Rebalance::Account.new 'My SEP IRA' do
|
73
|
+
fund 'ZZZZZ', 'Bonds', 250, 20.25 # $5,062.50
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'calculates the total value of all accounts' do
|
78
|
+
@target.total_value_of_all_accounts(@wifes_roth, @my_roth, @my_sep_ira).must_equal 44491.00
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'calculates the target value for each asset class' do
|
82
|
+
target_values = @target.calculate_target_asset_class_values(@wifes_roth, @my_roth, @my_sep_ira)
|
83
|
+
|
84
|
+
expected_target_values = {
|
85
|
+
'Some Asset Class' => 13347.30,
|
86
|
+
'Another Asset Class' => 8898.20,
|
87
|
+
'Bonds' => 22245.50
|
88
|
+
}
|
89
|
+
|
90
|
+
target_values.must_equal expected_target_values
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'calculates the current value for each asset class' do
|
94
|
+
current_values = @target.calculate_current_asset_class_values(@wifes_roth, @my_roth, @my_sep_ira)
|
95
|
+
|
96
|
+
expected_current_values = {
|
97
|
+
'Some Asset Class' => 15230.00,
|
98
|
+
'Another Asset Class' => 22500.00,
|
99
|
+
'Bonds' => 6611.00,
|
100
|
+
'Cash' => 150.00
|
101
|
+
}
|
102
|
+
|
103
|
+
current_values.must_equal expected_current_values
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'calculates the current percentage for each asset class' do
|
107
|
+
current_values = @target.calculate_current_asset_class_percentages(@wifes_roth, @my_roth, @my_sep_ira)
|
108
|
+
|
109
|
+
expected_current_values = {
|
110
|
+
'Some Asset Class' => 34.2316,
|
111
|
+
'Another Asset Class' => 50.5720,
|
112
|
+
'Bonds' => 14.8592,
|
113
|
+
'Cash' => 0.3371
|
114
|
+
}
|
115
|
+
|
116
|
+
current_values.must_equal expected_current_values
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'calculates the asset class percentages across all accounts' do
|
120
|
+
expected_percentages = {
|
121
|
+
"Wife's Roth" => {
|
122
|
+
"Some Asset Class" => 28.0956,
|
123
|
+
"Another Asset Class" => 50.5720,
|
124
|
+
"Bonds" => 3.4805
|
125
|
+
},
|
126
|
+
"My Roth" => {
|
127
|
+
"Cash" => 0.3371,
|
128
|
+
"Some Asset Class" => 6.1361
|
129
|
+
},
|
130
|
+
"My SEP IRA" => {
|
131
|
+
"Bonds" => 11.3787
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
@target.asset_class_percentages_across_all_accounts(@wifes_roth, @my_roth, @my_sep_ira).must_equal expected_percentages
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
|
3
|
+
require 'rebalance/target'
|
4
|
+
require 'rebalance/account'
|
5
|
+
require 'rebalance/fund'
|
6
|
+
require 'rebalance/rebalancer'
|
7
|
+
|
8
|
+
require 'vcr'
|
9
|
+
|
10
|
+
VCR.config do |c|
|
11
|
+
c.cassette_library_dir = 'spec/cassettes'
|
12
|
+
c.stub_with :webmock
|
13
|
+
c.default_cassette_options = { :record => :new_episodes }
|
14
|
+
end
|
15
|
+
|
16
|
+
module MiniTest
|
17
|
+
module Assertions
|
18
|
+
def assert_rebalanced rebalance
|
19
|
+
target = rebalance.target
|
20
|
+
accounts = rebalance.accounts
|
21
|
+
|
22
|
+
target_values = target.calculate_target_asset_class_values(*accounts)
|
23
|
+
|
24
|
+
# Allow the rebalance to be off by half a percent of the total value of all accounts
|
25
|
+
acceptable_delta = target.total_value_of_all_accounts(*accounts) * 0.005
|
26
|
+
|
27
|
+
rebalance.calculate_rebalanced_asset_class_values.each do |asset_class, rebalanced_value|
|
28
|
+
if !target_values[asset_class].nil?
|
29
|
+
rebalanced_value.must_be_within_delta target_values[asset_class], acceptable_delta, "Failed for asset class: #{asset_class}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def assert_accounts_have_same_values_after_rebalance rebalance
|
35
|
+
rebalance.accounts.each do |account|
|
36
|
+
if rebalance.accounts.size > 1
|
37
|
+
values = rebalance.rebalanced_values[account.name].values
|
38
|
+
else
|
39
|
+
values = rebalance.rebalanced_values.values
|
40
|
+
end
|
41
|
+
rebalanced_total = values.inject{|sum,x| sum + x }
|
42
|
+
account.total_value.must_be_within_delta rebalanced_total, 0.10
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rebalance
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bryce Thornton
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ruport
|
16
|
+
requirement: &70357428827480 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70357428827480
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: json
|
27
|
+
requirement: &70357428826720 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70357428826720
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: vcr
|
38
|
+
requirement: &70357428826040 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70357428826040
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: webmock
|
49
|
+
requirement: &70357428825540 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70357428825540
|
58
|
+
description: Rebalances mutual fund accounts to match your target asset allocation
|
59
|
+
email:
|
60
|
+
- brycethornton@gmail.com
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- .gitignore
|
66
|
+
- Gemfile
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- lib/rebalance.rb
|
70
|
+
- lib/rebalance/account.rb
|
71
|
+
- lib/rebalance/fund.rb
|
72
|
+
- lib/rebalance/rebalancer.rb
|
73
|
+
- lib/rebalance/target.rb
|
74
|
+
- lib/rebalance/version.rb
|
75
|
+
- rebalance.gemspec
|
76
|
+
- spec/cassettes/price_lookup_for_98765.yml
|
77
|
+
- spec/cassettes/price_lookup_for_VISVX.yml
|
78
|
+
- spec/cassettes/price_lookup_for_VMMXX.yml
|
79
|
+
- spec/rebalance/account_spec.rb
|
80
|
+
- spec/rebalance/fund_spec.rb
|
81
|
+
- spec/rebalance/rebalancer_spec.rb
|
82
|
+
- spec/rebalance/target_spec.rb
|
83
|
+
- spec/spec_helper.rb
|
84
|
+
homepage: ''
|
85
|
+
licenses: []
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project: rebalance
|
104
|
+
rubygems_version: 1.8.10
|
105
|
+
signing_key:
|
106
|
+
specification_version: 3
|
107
|
+
summary: Target asset allocation rebalancer
|
108
|
+
test_files:
|
109
|
+
- spec/cassettes/price_lookup_for_98765.yml
|
110
|
+
- spec/cassettes/price_lookup_for_VISVX.yml
|
111
|
+
- spec/cassettes/price_lookup_for_VMMXX.yml
|
112
|
+
- spec/rebalance/account_spec.rb
|
113
|
+
- spec/rebalance/fund_spec.rb
|
114
|
+
- spec/rebalance/rebalancer_spec.rb
|
115
|
+
- spec/rebalance/target_spec.rb
|
116
|
+
- spec/spec_helper.rb
|