owlplanner 2025.12.20__py3-none-any.whl → 2026.2.2__py3-none-any.whl
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.
- owlplanner/__init__.py +20 -1
- owlplanner/abcapi.py +18 -17
- owlplanner/cli/README.md +50 -0
- owlplanner/cli/_main.py +52 -0
- owlplanner/cli/cli_logging.py +56 -0
- owlplanner/cli/cmd_list.py +83 -0
- owlplanner/cli/cmd_run.py +86 -0
- owlplanner/config.py +315 -118
- owlplanner/data/__init__.py +21 -0
- owlplanner/data/rates.csv +99 -98
- owlplanner/debts.py +36 -8
- owlplanner/fixedassets.py +95 -21
- owlplanner/mylogging.py +157 -25
- owlplanner/plan.py +938 -390
- owlplanner/plotting/__init__.py +16 -3
- owlplanner/plotting/base.py +17 -3
- owlplanner/plotting/factory.py +16 -3
- owlplanner/plotting/matplotlib_backend.py +30 -7
- owlplanner/plotting/plotly_backend.py +32 -9
- owlplanner/progress.py +16 -3
- owlplanner/rates.py +50 -34
- owlplanner/socialsecurity.py +28 -19
- owlplanner/tax2026.py +119 -38
- owlplanner/timelists.py +194 -18
- owlplanner/utils.py +179 -4
- owlplanner/version.py +20 -1
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/METADATA +11 -3
- owlplanner-2026.2.2.dist-info/RECORD +35 -0
- owlplanner-2026.2.2.dist-info/entry_points.txt +2 -0
- owlplanner-2026.2.2.dist-info/licenses/AUTHORS +15 -0
- owlplanner/tax2025.py +0 -359
- owlplanner-2025.12.20.dist-info/RECORD +0 -29
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/WHEEL +0 -0
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/licenses/LICENSE +0 -0
owlplanner/plotting/__init__.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Plotting backends for Owl.
|
|
2
|
+
Plotting backends package for Owl retirement planner.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This package provides a factory pattern for creating plot backends (matplotlib,
|
|
5
|
+
plotly) for visualizing retirement planning results.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
8
|
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
8
21
|
"""
|
|
9
22
|
|
|
10
23
|
from .factory import PlotFactory
|
owlplanner/plotting/base.py
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Abstract base classes for plot backends.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This module defines the abstract base class interface that all plotting
|
|
5
|
+
backends must implement for consistent plotting functionality across
|
|
6
|
+
different visualization libraries.
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
9
|
|
|
10
|
+
This program is free software: you can redistribute it and/or modify
|
|
11
|
+
it under the terms of the GNU General Public License as published by
|
|
12
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
13
|
+
(at your option) any later version.
|
|
14
|
+
|
|
15
|
+
This program is distributed in the hope that it will be useful,
|
|
16
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
18
|
+
GNU General Public License for more details.
|
|
19
|
+
|
|
20
|
+
You should have received a copy of the GNU General Public License
|
|
21
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
8
22
|
"""
|
|
9
23
|
|
|
10
24
|
from abc import ABC, abstractmethod
|
owlplanner/plotting/factory.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Factory for creating plot
|
|
2
|
+
Factory for creating plot backend instances.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This module provides a factory class to create plot backends (matplotlib or
|
|
5
|
+
plotly) based on the specified backend type.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
8
|
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
8
21
|
"""
|
|
9
22
|
|
|
10
23
|
from .base import PlotBackend
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Matplotlib implementation
|
|
2
|
+
Matplotlib backend implementation for plotting retirement planning results.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This module provides the Matplotlib-based implementation of the plot backend
|
|
5
|
+
interface for creating static visualizations of retirement planning data.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
8
|
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
8
21
|
"""
|
|
9
22
|
|
|
10
23
|
import numpy as np
|
|
@@ -65,10 +78,20 @@ class MatplotlibBackend(PlotBackend):
|
|
|
65
78
|
"""Core function for stacked plots."""
|
|
66
79
|
nonzeroSeries = {}
|
|
67
80
|
for sname in snames:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
source_data = series[sname]
|
|
82
|
+
# Check if this is a household-level source (shape (1, N_n) when N_i > 1)
|
|
83
|
+
is_household = source_data.shape[0] == 1 and len(inames) > 1
|
|
84
|
+
if is_household:
|
|
85
|
+
# Show household total once without individual name
|
|
86
|
+
tmp = source_data[0]
|
|
87
|
+
if abs(sum(tmp)) > 1.0: # Use abs for debts
|
|
88
|
+
nonzeroSeries[sname] = tmp
|
|
89
|
+
else:
|
|
90
|
+
# Show per individual
|
|
91
|
+
for i in irange:
|
|
92
|
+
tmp = source_data[i]
|
|
93
|
+
if abs(sum(tmp)) > 1.0: # Use abs for debts
|
|
94
|
+
nonzeroSeries[sname + " " + inames[i]] = tmp
|
|
72
95
|
|
|
73
96
|
if len(nonzeroSeries) == 0:
|
|
74
97
|
return None, None
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Plotly implementation
|
|
2
|
+
Plotly backend implementation for plotting retirement planning results.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This module provides the Plotly-based implementation of the plot backend
|
|
5
|
+
interface for creating interactive visualizations of retirement planning data.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
8
|
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
8
21
|
"""
|
|
9
22
|
|
|
10
23
|
import numpy as np
|
|
@@ -749,7 +762,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
749
762
|
|
|
750
763
|
# Add each individual's data as a separate series
|
|
751
764
|
for i in range(len(inames)):
|
|
752
|
-
if np.sum(values[i]) > 1.0: # Only show non-zero series
|
|
765
|
+
if np.abs(np.sum(values[i])) > 1.0: # Only show non-zero series (use abs for debts)
|
|
753
766
|
stack_data.append((values[i], f"{namek} {inames[i]}"))
|
|
754
767
|
|
|
755
768
|
# Add stacked area traces
|
|
@@ -887,7 +900,7 @@ class PlotlyBackend(PlotBackend):
|
|
|
887
900
|
for sname in savings:
|
|
888
901
|
for i in range(len(inames)):
|
|
889
902
|
data = savings[sname][i] / 1000
|
|
890
|
-
if np.sum(data) > 1.0e-3: # Only show non-zero series
|
|
903
|
+
if np.abs(np.sum(data)) > 1.0e-3: # Only show non-zero series (use abs for debts)
|
|
891
904
|
nonzero_series[f"{sname} {inames[i]}"] = data
|
|
892
905
|
|
|
893
906
|
# Add stacked area traces for each account type
|
|
@@ -942,10 +955,20 @@ class PlotlyBackend(PlotBackend):
|
|
|
942
955
|
# Filter out zero series and create individual series names
|
|
943
956
|
nonzero_series = {}
|
|
944
957
|
for sname in sources:
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
958
|
+
source_data = sources[sname]
|
|
959
|
+
# Check if this is a household-level source (shape (1, N_n) when N_i > 1)
|
|
960
|
+
is_household = source_data.shape[0] == 1 and len(inames) > 1
|
|
961
|
+
if is_household:
|
|
962
|
+
# Show household total once without individual name
|
|
963
|
+
data = source_data[0] / 1000
|
|
964
|
+
if np.abs(np.sum(data)) > 1.0e-3: # Only show non-zero series (use abs for debts)
|
|
965
|
+
nonzero_series[sname] = data
|
|
966
|
+
else:
|
|
967
|
+
# Show per individual
|
|
968
|
+
for i in range(len(inames)):
|
|
969
|
+
data = source_data[i] / 1000
|
|
970
|
+
if np.abs(np.sum(data)) > 1.0e-3: # Only show non-zero series (use abs for debts)
|
|
971
|
+
nonzero_series[f"{sname} {inames[i]}"] = data
|
|
949
972
|
|
|
950
973
|
# Add stacked area traces for each source type
|
|
951
974
|
for source_name, data in nonzero_series.items():
|
owlplanner/progress.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Progress indicator for long-running operations.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
This module provides a simple progress indicator class that displays
|
|
5
|
+
progress as a percentage on a single line that updates in place.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
8
|
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
8
21
|
"""
|
|
9
22
|
|
|
10
23
|
from typing import Optional
|
owlplanner/rates.py
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
|
+
Historical and statistical rate of return data for asset classes.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
This module provides historical annual rates of return for different asset
|
|
5
|
+
classes: S&P500, Baa corporate bonds, real estate, 3-mo T-Bills, 10-year Treasury
|
|
6
|
+
notes, and inflation as measured by CPI from 1928 to present. Values were
|
|
7
|
+
extracted from NYU's Stern School of business historical returns data.
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
of
|
|
11
|
+
This program is free software: you can redistribute it and/or modify
|
|
12
|
+
it under the terms of the GNU General Public License as published by
|
|
13
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
14
|
+
(at your option) any later version.
|
|
10
15
|
|
|
11
|
-
This
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Values were extracted from NYU's Stern School of business:
|
|
17
|
-
https://pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/histretSP.html
|
|
18
|
-
from references therein.
|
|
19
|
-
|
|
20
|
-
Rate lists will need to be updated with values for current year.
|
|
21
|
-
When doing so, the TO bound defined below will need to be adjusted
|
|
22
|
-
to the last current data year.
|
|
23
|
-
|
|
24
|
-
Copyright © 2024 - Martin-D. Lacasse
|
|
25
|
-
|
|
26
|
-
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
16
|
+
This program is distributed in the hope that it will be useful,
|
|
17
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
+
GNU General Public License for more details.
|
|
27
20
|
|
|
21
|
+
You should have received a copy of the GNU General Public License
|
|
22
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
28
23
|
"""
|
|
29
24
|
|
|
30
25
|
###################################################################
|
|
@@ -36,10 +31,21 @@ import sys
|
|
|
36
31
|
from owlplanner import mylogging as log
|
|
37
32
|
from owlplanner import utils as u
|
|
38
33
|
|
|
39
|
-
# All data goes from 1928 to
|
|
34
|
+
# All data goes from 1928 to 2025. Update the TO value when data
|
|
40
35
|
# becomes available for subsequent years.
|
|
41
36
|
FROM = 1928
|
|
42
|
-
TO =
|
|
37
|
+
TO = 2025
|
|
38
|
+
|
|
39
|
+
# Rate methods that use the same rate every year (reverse/roll are no-ops).
|
|
40
|
+
CONSTANT_RATE_METHODS = (
|
|
41
|
+
"default", "optimistic", "conservative", "user",
|
|
42
|
+
"historical average", "mean",
|
|
43
|
+
)
|
|
44
|
+
# Rate methods that produce deterministic series (no regeneration needed).
|
|
45
|
+
RATE_METHODS_NO_REGEN = (
|
|
46
|
+
"default", "optimistic", "conservative", "user",
|
|
47
|
+
"historical average", "historical",
|
|
48
|
+
)
|
|
43
49
|
|
|
44
50
|
where = os.path.dirname(sys.modules["owlplanner"].__file__)
|
|
45
51
|
file = os.path.join(where, "data/rates.csv")
|
|
@@ -55,8 +61,8 @@ SP500 = df["S&P 500"]
|
|
|
55
61
|
# Annual rate of return (%) of Baa Corporate Bonds since 1928.
|
|
56
62
|
BondsBaa = df["Bonds Baa"]
|
|
57
63
|
|
|
58
|
-
# Annual rate of return (%) of
|
|
59
|
-
|
|
64
|
+
# Annual rate of return (%) of Real Estate since 1928.
|
|
65
|
+
RealEstate = df["real estate"]
|
|
60
66
|
|
|
61
67
|
# Annual rate of return (%) for 10-y Treasury notes since 1928.
|
|
62
68
|
TNotes = df["TNotes"]
|
|
@@ -101,8 +107,8 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
101
107
|
stdev = df.std()
|
|
102
108
|
covar = df.cov()
|
|
103
109
|
|
|
104
|
-
mylog.
|
|
105
|
-
mylog.
|
|
110
|
+
mylog.vprint("means: (%)\n", means)
|
|
111
|
+
mylog.vprint("standard deviation: (%)\n", stdev)
|
|
106
112
|
|
|
107
113
|
# Convert to NumPy array and from percent to decimal.
|
|
108
114
|
means = np.array(means) / 100.0
|
|
@@ -114,7 +120,7 @@ def getRatesDistributions(frm, to, mylog=None):
|
|
|
114
120
|
# Fold round-off errors in proper bounds.
|
|
115
121
|
corr[corr > 1] = 1
|
|
116
122
|
corr[corr < -1] = -1
|
|
117
|
-
mylog.
|
|
123
|
+
mylog.vprint("correlation matrix: \n\t\t%s" % str(corr).replace("\n", "\n\t\t"))
|
|
118
124
|
|
|
119
125
|
return means, stdev, corr, covar
|
|
120
126
|
|
|
@@ -131,15 +137,25 @@ class Rates(object):
|
|
|
131
137
|
then ``mySeries = r.genSeries()``
|
|
132
138
|
"""
|
|
133
139
|
|
|
134
|
-
def __init__(self, mylog=None):
|
|
140
|
+
def __init__(self, mylog=None, seed=None):
|
|
135
141
|
"""
|
|
136
142
|
Default constructor.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
mylog: Logger instance (optional)
|
|
146
|
+
seed: Random seed for reproducible stochastic rates (optional)
|
|
137
147
|
"""
|
|
138
148
|
if mylog is None:
|
|
139
149
|
self.mylog = log.Logger()
|
|
140
150
|
else:
|
|
141
151
|
self.mylog = mylog
|
|
142
152
|
|
|
153
|
+
# Store seed for stochastic rate generation
|
|
154
|
+
# Always use a Generator instance for thread safety and modern API
|
|
155
|
+
# If seed is None, default_rng() will use entropy/current time
|
|
156
|
+
self._seed = seed
|
|
157
|
+
self._rng = np.random.default_rng(seed)
|
|
158
|
+
|
|
143
159
|
# Default rates are average over last 30 years.
|
|
144
160
|
self._defRates = np.array([0.1101, 0.0736, 0.0503, 0.0251])
|
|
145
161
|
|
|
@@ -335,18 +351,18 @@ class Rates(object):
|
|
|
335
351
|
|
|
336
352
|
def _histRates(self, n):
|
|
337
353
|
"""
|
|
338
|
-
Return
|
|
354
|
+
Return an array of 4 values representing the historical rates
|
|
339
355
|
of stock, Corporate Baa bonds, Treasury notes, and inflation,
|
|
340
356
|
respectively.
|
|
341
357
|
"""
|
|
342
358
|
hrates = np.array([SP500[n], BondsBaa[n], TNotes[n], Inflation[n]])
|
|
343
359
|
|
|
344
|
-
# Convert from percent to decimal.
|
|
360
|
+
# Historical rates are stored in percent. Convert from percent to decimal.
|
|
345
361
|
return hrates / 100
|
|
346
362
|
|
|
347
363
|
def _stochRates(self, n):
|
|
348
364
|
"""
|
|
349
|
-
Return
|
|
365
|
+
Return an array of 4 values representing the historical rates
|
|
350
366
|
of stock, Corporate Baa bonds, Treasury notes, and inflation,
|
|
351
367
|
respectively. Values are pulled from normal distributions
|
|
352
368
|
having the same characteristics as the historical data for
|
|
@@ -354,8 +370,8 @@ class Rates(object):
|
|
|
354
370
|
|
|
355
371
|
But these variables need to be looked at together
|
|
356
372
|
through multivariate analysis. Code below accounts for
|
|
357
|
-
covariance between stocks, bonds, and inflation.
|
|
373
|
+
covariance between stocks, corp bonds, t-notes, and inflation.
|
|
358
374
|
"""
|
|
359
|
-
srates =
|
|
375
|
+
srates = self._rng.multivariate_normal(self.means, self.covar)
|
|
360
376
|
|
|
361
377
|
return srates
|
owlplanner/socialsecurity.py
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
"""
|
|
2
|
+
Social Security benefit calculation rules and utilities.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
This module implements Social Security rules including full retirement age
|
|
5
|
+
calculations, benefit computations, and related retirement planning functions.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
7
8
|
|
|
8
|
-
This
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
13
18
|
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
14
21
|
"""
|
|
15
22
|
|
|
16
23
|
import numpy as np
|
|
@@ -91,7 +98,7 @@ def getSpousalBenefits(pias):
|
|
|
91
98
|
return benefits
|
|
92
99
|
|
|
93
100
|
|
|
94
|
-
def getSelfFactor(fra, convage,
|
|
101
|
+
def getSelfFactor(fra, convage, bornOnFirstDays):
|
|
95
102
|
"""
|
|
96
103
|
Return the reduction/increase factor to multiply PIA based on claiming age.
|
|
97
104
|
|
|
@@ -102,7 +109,8 @@ def getSelfFactor(fra, convage, bornOnFirst):
|
|
|
102
109
|
- After FRA: Benefits are increased by 8% per year (up to 132% at age 70)
|
|
103
110
|
|
|
104
111
|
The function automatically adjusts for Social Security age if the birthday is on
|
|
105
|
-
the
|
|
112
|
+
the 1st or 2nd day of the month (adds 1/12 year to conventional age), consistent
|
|
113
|
+
with SSA rules that treat both days the same for age calculation purposes.
|
|
106
114
|
|
|
107
115
|
Parameters
|
|
108
116
|
----------
|
|
@@ -111,8 +119,8 @@ def getSelfFactor(fra, convage, bornOnFirst):
|
|
|
111
119
|
convage : float
|
|
112
120
|
Conventional age when benefits start, in years (can be fractional with 1/12 increments).
|
|
113
121
|
Must be between 62 and 70 inclusive.
|
|
114
|
-
|
|
115
|
-
True if birthday is on the
|
|
122
|
+
bornOnFirstDays : bool
|
|
123
|
+
True if birthday is on the 1st or 2nd day of the month, False otherwise.
|
|
116
124
|
If True, the function adds 1/12 year to convert to Social Security age.
|
|
117
125
|
|
|
118
126
|
Returns
|
|
@@ -131,8 +139,8 @@ def getSelfFactor(fra, convage, bornOnFirst):
|
|
|
131
139
|
if convage < 62 or convage > 70:
|
|
132
140
|
raise ValueError(f"Age {convage} out of range.")
|
|
133
141
|
|
|
134
|
-
# Add a month to conventional age if born on the
|
|
135
|
-
offset = 0 if not
|
|
142
|
+
# Add a month to conventional age if born on the 1st or 2nd (SSA treats both the same).
|
|
143
|
+
offset = 0 if not bornOnFirstDays else 1/12
|
|
136
144
|
ssage = convage + offset
|
|
137
145
|
|
|
138
146
|
diff = fra - ssage
|
|
@@ -146,7 +154,7 @@ def getSelfFactor(fra, convage, bornOnFirst):
|
|
|
146
154
|
return .8 - 0.05 * (diff - 3)
|
|
147
155
|
|
|
148
156
|
|
|
149
|
-
def getSpousalFactor(fra, convage,
|
|
157
|
+
def getSpousalFactor(fra, convage, bornOnFirstDays):
|
|
150
158
|
"""
|
|
151
159
|
Return the reduction factor to multiply spousal benefits based on claiming age.
|
|
152
160
|
|
|
@@ -156,7 +164,8 @@ def getSpousalFactor(fra, convage, bornOnFirst):
|
|
|
156
164
|
- At or after FRA: Full spousal benefit (50% of spouse's PIA, no increase for delay)
|
|
157
165
|
|
|
158
166
|
The function automatically adjusts for Social Security age if the birthday is on
|
|
159
|
-
the
|
|
167
|
+
the 1st or 2nd day of the month (adds 1/12 year to conventional age), consistent
|
|
168
|
+
with SSA rules that treat both days the same for age calculation purposes.
|
|
160
169
|
|
|
161
170
|
Parameters
|
|
162
171
|
----------
|
|
@@ -165,8 +174,8 @@ def getSpousalFactor(fra, convage, bornOnFirst):
|
|
|
165
174
|
convage : float
|
|
166
175
|
Conventional age when benefits start, in years (can be fractional with 1/12 increments).
|
|
167
176
|
Must be at least 62 (no maximum, but no increase beyond FRA).
|
|
168
|
-
|
|
169
|
-
True if birthday is on the
|
|
177
|
+
bornOnFirstDays : bool
|
|
178
|
+
True if birthday is on the 1st or 2nd day of the month, False otherwise.
|
|
170
179
|
If True, the function adds 1/12 year to convert to Social Security age.
|
|
171
180
|
|
|
172
181
|
Returns
|
|
@@ -185,8 +194,8 @@ def getSpousalFactor(fra, convage, bornOnFirst):
|
|
|
185
194
|
if convage < 62:
|
|
186
195
|
raise ValueError(f"Age {convage} out of range.")
|
|
187
196
|
|
|
188
|
-
# Add a month to conventional age if born on the
|
|
189
|
-
offset = 0 if not
|
|
197
|
+
# Add a month to conventional age if born on the 1st or 2nd (SSA treats both the same).
|
|
198
|
+
offset = 0 if not bornOnFirstDays else 1/12
|
|
190
199
|
ssage = convage + offset
|
|
191
200
|
|
|
192
201
|
diff = fra - ssage
|