fortuneteller 0.1.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.
@@ -0,0 +1,121 @@
1
+ module FortuneTeller
2
+ # We extend `state` Hash with this module for readability
3
+ class State
4
+ attr_reader :date, :accounts, :cashflow, :from, :to
5
+
6
+ def self.cashflow_base
7
+ FortuneTeller::Cashflow.new(
8
+ pretax_gross: 0,
9
+ pretax_salary: 0,
10
+ pretax_savings_withdrawal: 0,
11
+ pretax_savings: 0,
12
+ pretax_savings_matched: 0,
13
+ pretax_adjusted: 0,
14
+ tax_withholding: 0,
15
+ take_home_pay: 0
16
+ )
17
+ end
18
+
19
+ def initialize(start_date:, previous: nil)
20
+ @from = start_date.dup
21
+ @date = start_date
22
+ @accounts = {}
23
+ unless previous.nil?
24
+ previous.accounts.each { |k, a| @accounts[k] = a.dup }
25
+ end
26
+ @cashflow = {
27
+ primary: Array.new(12) { self.class.cashflow_base },
28
+ partner: Array.new(12) { self.class.cashflow_base }
29
+ }
30
+ end
31
+
32
+ def add_account(key:, account:)
33
+ @accounts[key] = account.initial_state(start_date: @date)
34
+ end
35
+
36
+ def pass_time(to:)
37
+ @date = to
38
+ @to = to
39
+ @accounts.each_value { |a| a.pass_time(to: to) }
40
+ end
41
+
42
+ def apply_pretax_savings_withdrawal(date:, holder:, amount:, source:)
43
+ @accounts[source].debit(amount: amount, on: date)
44
+ c = FortuneTeller::Cashflow.new(pretax_gross: amount, pretax_savings_withdrawal: amount)
45
+ c.line_items[:pretax_adjusted] = amount
46
+ c.line_items[:tax_withholding] = 0
47
+ c.line_items[:take_home_pay] = amount
48
+ apply_cashflow(date: date, holder: holder, cashflow: c)
49
+ end
50
+
51
+ def apply_w2_income(date:, holder:, income:, account_credits:)
52
+ c = generate_w2_cashflow(date, income)
53
+ apply_cashflow(date: date, holder: holder, cashflow: c)
54
+ account_credits.each do |k, amount|
55
+ @accounts[k].credit(amount: amount, on: date)
56
+ end
57
+ end
58
+
59
+ def apply_ss_income(date:, holder:, income:)
60
+ c = generate_ss_cashflow(date, income)
61
+ apply_cashflow(date: date, holder: holder, cashflow: c)
62
+ end
63
+
64
+ def init_next
65
+ self.class.new(start_date: @date, previous: self)
66
+ end
67
+
68
+ def merged_cashflow(holder:)
69
+ @cashflow[holder].reduce(FortuneTeller::Cashflow.new, :merge!)
70
+ end
71
+
72
+ def as_json(_options = nil)
73
+ {
74
+ date: @date,
75
+ # cashflow: {
76
+ # primary: merged_cashflow(holder: :primary).as_json(options),
77
+ # partner: merged_cashflow(holder: :partner).as_json(options)
78
+ # },
79
+ accounts: @accounts.as_json
80
+ }
81
+ end
82
+
83
+ private
84
+
85
+ def generate_w2_cashflow(date, income)
86
+ c = FortuneTeller::Cashflow.new(
87
+ pretax_gross: (income[:wages] + income[:matched]),
88
+ pretax_salary: income[:wages],
89
+ pretax_savings: income[:saved],
90
+ pretax_savings_matched: income[:matched],
91
+ pretax_adjusted: (income[:wages] - income[:saved])
92
+ )
93
+ c.line_items[:tax_withholding] = calculate_w2_withholding(
94
+ date: date,
95
+ adjusted_income: c.line_items[:pretax_adjusted],
96
+ pay_period: income[:pay_period]
97
+ )
98
+ c.line_items[:take_home_pay] = c.line_items[:pretax_adjusted] - c.line_items[:tax_withholding]
99
+ c
100
+ end
101
+
102
+ def generate_ss_cashflow(date, income)
103
+ FortuneTeller::Cashflow.new(
104
+ pretax_gross: income[:ss],
105
+ pretax_ss: income[:ss],
106
+ pretax_adjusted: income[:ss],
107
+ tax_withholding: 0,
108
+ take_home_pay: income[:ss]
109
+ )
110
+ end
111
+
112
+ def calculate_w2_withholding(date:, adjusted_income:, pay_period:)
113
+ # Ideally, use state to determine w-4 allowances
114
+ (adjusted_income * 0.3).floor
115
+ end
116
+
117
+ def apply_cashflow(date:, holder:, cashflow:)
118
+ @cashflow[holder][(date.month - 1)].merge!(cashflow)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,14 @@
1
+ module FortuneTeller
2
+ # Provides base functionality to inheriting transform classes
3
+ class TransformBase
4
+ attr_reader :date, :holder
5
+ def initialize(date:, holder:)
6
+ @date = date
7
+ @holder = holder
8
+ end
9
+
10
+ def <=>(other)
11
+ date <=> other.date
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,59 @@
1
+ module FortuneTeller
2
+ # Base class for FortuneTeller objects that generate transforms
3
+ class TransformGenerator
4
+ attr_reader :holder, :start_date, :end_date
5
+
6
+ def initialize(holder: nil, start_date: nil, end_date: nil, **data)
7
+ @holder = holder
8
+ @start_date = start_date
9
+ @end_date = end_date
10
+ @scheduled = []
11
+ @data = MomentStruct.new data
12
+ end
13
+
14
+ def method_missing(name, *args)
15
+ if @data.respond_to? name
16
+ @data.send(name, *args)
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def respond_to_missing?(name, include_private = false)
23
+ @data.respond_to?(name) || super
24
+ end
25
+
26
+ def bounded_gen_transforms(from:, to:, plan:)
27
+ return [] if out_of_range?(from: from, to: to)
28
+ from = tighten_from(from)
29
+ to = tighten_to(to)
30
+ gen_transforms(from: from, to: to, plan: plan)
31
+ end
32
+
33
+ private
34
+
35
+ def state_reader
36
+ @data.to_reader
37
+ end
38
+
39
+ def states_in_range(from:, to:)
40
+ @data.read_range(from: from, to: to)
41
+ end
42
+
43
+ def out_of_range?(from:, to:)
44
+ return true if !@start_date.nil? && (to < @start_date)
45
+ return true if !@end_date.nil? && (from >= @end_date)
46
+ false
47
+ end
48
+
49
+ def tighten_from(from)
50
+ return from if @start_date.nil?
51
+ [from, @start_date].max
52
+ end
53
+
54
+ def tighten_to(to)
55
+ return to if @end_date.nil?
56
+ [to, @end_date].min
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,261 @@
1
+ module FortuneTeller
2
+ module Utils
3
+
4
+ # Calculates adjusted benefit from PIA
5
+ # Based on https://www.ssa.gov/oact/quickcalc/early_late.html 11/16/2017
6
+ class SocialSecurity
7
+ attr_accessor :pia
8
+
9
+ def initialize(dob:, start_month:)
10
+ @dob = dob
11
+ @adjusted_dob = (dob.day == 1 ? dob.yesterday : dob)
12
+ @start_month = start_month.at_beginning_of_month
13
+ end
14
+
15
+ def estimate_pia(current_salary:, annual_raise:)
16
+ year = Date.today.year
17
+ last_year = @start_month.year
18
+ salary_history = {year => current_salary}
19
+ ((@dob.year+18)..(year-1)).reverse_each do |y|
20
+ salary_history[y] = (salary_history[y+1]/annual_raise).floor
21
+ end
22
+ if(last_year > year)
23
+ ((year+1)..last_year).each do |y|
24
+ salary_history[y] = (salary_history[y-1]*annual_raise).floor
25
+ end
26
+ end
27
+ salaries = salary_history.map{|y, s| s*indexing_factors[y]}
28
+ aime = (salaries.sort.last(35).reduce(:+)/(35.0*12)).floor
29
+ puts "AIME #{aime}"
30
+
31
+ if aime > bend_points[1]
32
+ pia_62 = (0.9*bend_points[0]+0.32*(bend_points[1]-bend_points[0])+0.15*(aime-bend_points[1])).floor
33
+ elsif aime > bend_points[0]
34
+ pia_62 = (0.9*bend_points[0]+0.32*(aime-bend_points[0])).floor
35
+ else
36
+ pia_62 = (0.9*aime).floor
37
+ end
38
+
39
+ pia_adjusted = pia_62
40
+ ((@dob.year+63)..@start_month.year).each do |y|
41
+ pia_adjusted = (pia_adjusted*(100+COLA_1975_START[y-1-1975])/100.0).floor
42
+ end
43
+
44
+ @pia = pia_adjusted
45
+ end
46
+
47
+ def fra_pia=(fra_pia)
48
+ pia_adjusted = fra_pia
49
+ if @start_month.year < full_retirement_month.year
50
+ (@start_month.year..(full_retirement_month.year-1)).each do |y|
51
+ pia_adjusted = (pia_adjusted/((100+COLA_1975_START[y-1975])/100.0)).floor
52
+ end
53
+ elsif @start_month.year > full_retirement_month.year
54
+ puts "START GREATER"
55
+ ((full_retirement_month.year+1)..@start_month.year).each do |y|
56
+ puts "START: #{@start_month.year}, YEAR: #{y}"
57
+ puts "PIA ADJ = #{pia_adjusted}"
58
+ pia_adjusted = (pia_adjusted*(100+COLA_1975_START[y-1-1975])/100.0).floor
59
+ end
60
+ end
61
+ @pia = pia_adjusted
62
+ end
63
+
64
+ def calculate_benefit
65
+ frm = full_retirement_month
66
+ puts "FRM: #{frm}"
67
+ return @pia if @start_month == frm
68
+
69
+ if(@start_month < frm)
70
+ min_rm = min_retirement_month
71
+ raise bounds_error(start: @start_month, min: min_rm) if @start_month < min_rm
72
+ early_benefit( months: month_diff(@start_month, frm) )
73
+ else
74
+ max_rm = max_retirement_month
75
+ raise bounds_error(start: @start_month, max: max_rm) if @start_month > max_rm
76
+ late_benefit( months: month_diff(frm, @start_month) )
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ TRANSITION_YEARS = {
83
+ 1938 => [65, 2], 1939 => [65, 4], 1940 => [65, 6], 1941 => [65, 8],
84
+ 1942 => [65, 10], 1955 => [66, 2], 1956 => [66, 4], 1957 => [66, 6],
85
+ 1958 => [66, 8], 1959 => [66, 10],
86
+ }
87
+ DELAY_RATES = {
88
+ 1925 => (7/24.0), 1926 => (7/24.0), 1927 => (8/24.0), 1928 => (8/24.0),
89
+ 1929 => (9/24.0), 1930 => (9/24.0), 1931 => (10/24.0), 1932 => (10/24.0),
90
+ 1933 => (11/24.0), 1934 => (11/24.0), 1935 => (12/24.0), 1936 => (12/24.0),
91
+ 1937 => (13/24.0), 1938 => (13/24.0), 1939 => (14/24.0), 1940 => (14/24.0),
92
+ 1941 => (15/24.0), 1942 => (15/24.0),
93
+ }
94
+
95
+ AWI_1951_START = [
96
+ # Final (1951-2016)
97
+ # https://www.ssa.gov/oact/COLA/AWI.html
98
+ 279916, 297332, 313944, 315564, 330144, 353236, 364172, 367380, 385580,
99
+ 400712, 408676, 429140, 439664, 457632, 465872, 493836, 521344, 557176,
100
+ 589376, 618624, 649708, 713380, 758016, 803076, 863092, 922648, 977944,
101
+ 1055603, 1147946, 1251346, 1377310, 1453134, 1523924, 1613507, 1682251,
102
+ 1732182, 1842651, 1933404, 2009955, 2102798, 2181160, 2293542, 2313267,
103
+ 2375353, 2470566, 2591390, 2742600, 2886144, 3046984, 3215482, 3292192,
104
+ 3325209, 3406495, 3564855, 3695294, 3865141, 4040548, 4133497, 4071161,
105
+ 4167383, 4297961, 4432167, 4488816, 4648152, 4809863, 4866473,
106
+ # Estimates (2017-2070)
107
+ # https://www.ssa.gov/oact/TR/TRassum.html (see awi_projector)
108
+ 5056265, 5298965, 5537418, 5775526, 6018098, 6252803, 6484156, 6730553,
109
+ 6986314, 7251793, 7527361, 7813400, 8110309, 8418500, 8738403, 9070462,
110
+ 9415139, 9772914, 10144284, 10529766, 10929897, 11345233, 11776351,
111
+ 12223852, 12688358, 13170515, 13670994, 14190491, 14729729, 15289458,
112
+ 15870457, 16473534, 17099528, 17749310, 18423783, 19123886, 19850593,
113
+ 20604915, 21387901, 22200641, 23044265, 23919947, 24828904, 25772402,
114
+ 26751753, 27768319, 28823515, 29918808, 31055722, 32235839, 33460800,
115
+ 34732310, 36052137, 37422118
116
+ ]
117
+
118
+ COLA_1975_START = [
119
+ # Final (1975-2017)
120
+ # https://www.ssa.gov/oact/COLA/colaseries.html
121
+ 8.0, 6.4, 5.9, 6.5, 9.9, 14.3, 11.2, 7.4, 3.5, 3.5, 3.1, 1.3, 4.2,
122
+ 4.0, 4.7, 5.4, 3.7, 3.0, 2.6, 2.8, 2.6, 2.9, 2.1, 1.3, 2.5, 3.5,
123
+ 2.6, 1.4, 2.1, 2.7, 4.1, 3.3, 2.3, 5.8, 0.0, 0.0, 3.6, 1.7, 1.5,
124
+ 1.7, 0.0, 0.3, 2.0,
125
+ # Estimates (2018-2070)
126
+ # https://www.ssa.gov/oact/TR/TRassum.html
127
+ 3.1, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6,
128
+ 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6,
129
+ 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6,
130
+ 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6,
131
+ 2.6,
132
+ ]
133
+
134
+ def bend_points
135
+ return @bend_points unless @bend_points.nil?
136
+
137
+ age_62_year = @dob.year+62
138
+ age_62_index = AWI_1951_START[(age_62_year-1951)]
139
+ year_1979_index = AWI_1951_START[(1979-1951)]
140
+ year_1979_bends = [18000, 108500]
141
+ @bend_points = [
142
+ (year_1979_bends[0].to_f*(age_62_index/year_1979_index)).floor,
143
+ (year_1979_bends[1].to_f*(age_62_index/year_1979_index)).floor,
144
+ ]
145
+ end
146
+
147
+ def indexing_factors
148
+ return @indexing_factors unless @indexing_factors.nil?
149
+
150
+ age_60_year = @dob.year+60
151
+ age_60_index = AWI_1951_START[(age_60_year-1951)]
152
+ @indexing_factors = {}
153
+ (18..60).each do |age|
154
+ year = @dob.year+age
155
+ @indexing_factors[year] = age_60_index.to_f/AWI_1951_START[(year-1951)]
156
+ end
157
+ (61..70).each do |age|
158
+ year = @dob.year+age
159
+ @indexing_factors[year] = 1.0
160
+ end
161
+ @indexing_factors
162
+ end
163
+
164
+ def full_retirement_month
165
+ return @frm unless @frm.nil?
166
+
167
+ year = @adjusted_dob.year
168
+ frm = @adjusted_dob.at_beginning_of_month
169
+ if year <= 1938
170
+ @frm = frm.years_since(65)
171
+ elsif (year >= 1943 and year <= 1954)
172
+ @frm = frm.years_since(66)
173
+ elsif year >= 1960
174
+ @frm = frm.years_since(67)
175
+ else
176
+ t = TRANSITION_YEARS[year][0]
177
+ @frm = frm.years_since(t[0]).years_since(t[1])
178
+ end
179
+ end
180
+
181
+ def max_retirement_month
182
+ @adjusted_dob.at_beginning_of_month.years_since(70)
183
+ end
184
+
185
+ def min_retirement_month
186
+ if @adjusted_dob.day == 1
187
+ @adjusted_dob.years_since(62)
188
+ else
189
+ @adjusted_dob.at_beginning_of_month.years_since(62).months_since(1)
190
+ end
191
+ end
192
+
193
+ def early_benefit(months:)
194
+ if months <= 36
195
+ multiplier = 100.0 - ((5.0 * months) / 9.0)
196
+ else
197
+ multiplier = 100.0 - 20.0 - ((5.0 * months) / 12.0)
198
+ end
199
+ puts "EARLY PIA: #{(pia*multiplier/100.0).floor}"
200
+ (@pia*multiplier/100.0).floor
201
+ end
202
+
203
+ def late_benefit(months:)
204
+ year = @adjusted_dob.year
205
+ if year <= 1924
206
+ monthly = 6/24.0
207
+ elsif year <= 1942
208
+ monthly = DELAY_RATES[year]
209
+ else
210
+ monthly = 16.0/24.0
211
+ end
212
+ multiplier = 100.0 + (monthly*months)
213
+ puts "LATE PIA: #{(pia*multiplier/100.0).floor}"
214
+ (@pia*multiplier/100.0).floor
215
+ end
216
+
217
+ def month_diff(a, b)
218
+ (b.year * 12 + b.month) - (a.year * 12 + a.month)
219
+ end
220
+
221
+ def bounds_error(start:, min: nil, max: nil)
222
+ self.class::StartDateBounds.new(start: start, min: min, max: max)
223
+ end
224
+
225
+ def self.awi_projector
226
+ projected = []
227
+ projected_increases = {
228
+ # From https://www.ssa.gov/oact/TR/TRassum.html
229
+ 2017 => 3.9,
230
+ 2018 => 4.8,
231
+ 2019 => 4.5,
232
+ 2020 => 4.3,
233
+ 2021 => 4.2,
234
+ 2022 => 3.9,
235
+ 2023 => 3.7,
236
+ 2024 => 3.8,
237
+ 2025 => 3.8,
238
+ 2026 => 3.8
239
+ }
240
+ last_awi = AWI_1951_START[2016-1951]
241
+ (2017..2070).each do |year|
242
+ increase = projected_increases[[2026, year].min]
243
+ last_awi = (last_awi*((100+increase)/100.0)).floor
244
+ projected << last_awi
245
+ end
246
+ projected
247
+ end
248
+
249
+ class StartDateBounds < StandardError
250
+ def initialize(**kargs)
251
+ if not kargs[:max].nil?
252
+ super("Start #{kargs[:start]} is greater than max #{kargs[:max]}")
253
+ elsif not kargs[:min].nil?
254
+ super("Start #{kargs[:start]} is less than min #{kargs[:min]}")
255
+ end
256
+ end
257
+ end
258
+
259
+ end
260
+ end
261
+ end