owlplanner 2025.2.8__tar.gz → 2025.2.10__tar.gz
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-2025.2.8 → owlplanner-2025.2.10}/PKG-INFO +1 -1
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/owl.tex +1 -1
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/case_jack+jill.toml +2 -2
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/case_joe.toml +2 -2
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/case_john+sally.toml +2 -2
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/case_kim+sam-bequest.toml +2 -2
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/case_kim+sam-spending.toml +2 -2
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/pyproject.toml +1 -1
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/abcapi.py +15 -15
- owlplanner-2025.2.10/src/owlplanner/config.py +299 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/logging.py +13 -13
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/plan.py +632 -673
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/progress.py +2 -2
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/rates.py +74 -74
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/tax2025.py +3 -3
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/timelists.py +33 -31
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/utils.py +9 -9
- owlplanner-2025.2.10/src/owlplanner/version.py +1 -0
- owlplanner-2025.2.10/test.py +18 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/About_Owl.py +5 -3
- owlplanner-2025.2.10/ui/Asset_Allocation.py +142 -0
- owlplanner-2025.2.10/ui/Assets.py +63 -0
- owlplanner-2025.2.10/ui/Create_Case.py +119 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/Documentation.py +21 -13
- owlplanner-2025.2.10/ui/Fixed_Income.py +68 -0
- owlplanner-2025.2.10/ui/Graphs.py +35 -0
- owlplanner-2025.2.10/ui/Historical_Range.py +55 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/Logs.py +3 -3
- owlplanner-2025.2.10/ui/Monte_Carlo.py +31 -0
- owlplanner-2025.2.10/ui/Optimization_Parameters.py +120 -0
- owlplanner-2025.2.10/ui/Output_Files.py +58 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/Quick_Start.py +8 -6
- owlplanner-2025.2.10/ui/Rates_Selection.py +228 -0
- owlplanner-2025.2.10/ui/Settings.py +23 -0
- owlplanner-2025.2.10/ui/Wages_And_Contributions.py +67 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/Worksheets.py +3 -3
- owlplanner-2025.2.10/ui/main.py +52 -0
- owlplanner-2025.2.10/ui/owlbridge.py +682 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/plots.py +3 -3
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/progress.py +2 -2
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/requirements.txt +1 -1
- owlplanner-2025.2.10/ui/sskeys.py +480 -0
- owlplanner-2025.2.8/src/owlplanner/config.py +0 -294
- owlplanner-2025.2.8/src/owlplanner/version.py +0 -1
- owlplanner-2025.2.8/ui/Asset_Allocation.py +0 -135
- owlplanner-2025.2.8/ui/Assets.py +0 -58
- owlplanner-2025.2.8/ui/Create_Case.py +0 -94
- owlplanner-2025.2.8/ui/Fixed_Income.py +0 -60
- owlplanner-2025.2.8/ui/Graphs.py +0 -32
- owlplanner-2025.2.8/ui/Historical_Range.py +0 -43
- owlplanner-2025.2.8/ui/Monte_Carlo.py +0 -30
- owlplanner-2025.2.8/ui/Optimization_Parameters.py +0 -114
- owlplanner-2025.2.8/ui/Output_Files.py +0 -57
- owlplanner-2025.2.8/ui/Rates_Selection.py +0 -214
- owlplanner-2025.2.8/ui/Settings.py +0 -17
- owlplanner-2025.2.8/ui/Wages_And_Contributions.py +0 -57
- owlplanner-2025.2.8/ui/main.py +0 -44
- owlplanner-2025.2.8/ui/owlbridge.py +0 -677
- owlplanner-2025.2.8/ui/sskeys.py +0 -446
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/.flake8 +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/.gitignore +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/INSTALL.md +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/LICENSE +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/README.md +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/allocations.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/owl.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/profile.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/docs/owl.pdf +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/joe.xlsx +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/examples/template.xlsx +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/owlplanner.cmd +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/requirements.txt +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/tests/test_logger.py +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/tests/test_regressions.py +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/tests/test_repro.py +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/tests/test_units.py +0 -0
- {owlplanner-2025.2.8 → owlplanner-2025.2.10}/ui/README.md +0 -0
|
@@ -205,7 +205,7 @@ Parameter values are either set by the user, historical data, or by the tax code
|
|
|
205
205
|
\item [$\Gamma_{tn}$]
|
|
206
206
|
Bound for Federal income tax bracket. We define $\Gamma_{(-1)n} := 0$, so that
|
|
207
207
|
$\Gamma_{0n}$ is the upper bound for the 10\% tax bracket in year $n$. As the filing status
|
|
208
|
-
|
|
208
|
+
can change for couples, and so can the tax code, $\Gamma_{tn}$ will be changing over $n$.
|
|
209
209
|
\item [$\Delta_{tn}$]
|
|
210
210
|
Difference between upper bound $\Gamma_t$ and lower bound $\Gamma_{t-1}$
|
|
211
211
|
of a federal income tax bracket,
|
|
@@ -24,7 +24,7 @@ Names = [ "Jack", "Jill",]
|
|
|
24
24
|
"Social security amounts" = [ 28.4, 19.7,]
|
|
25
25
|
"Social security ages" = [ 70, 62,]
|
|
26
26
|
|
|
27
|
-
["
|
|
27
|
+
["Rates Selection"]
|
|
28
28
|
"Heirs rate on tax-deferred estate" = 30.0
|
|
29
29
|
"Long-term capital gain tax rate" = 15.0
|
|
30
30
|
"Dividend tax rate" = 2.0
|
|
@@ -32,7 +32,7 @@ Method = "historical"
|
|
|
32
32
|
From = 1969
|
|
33
33
|
To = 2002
|
|
34
34
|
|
|
35
|
-
["Asset
|
|
35
|
+
["Asset Allocation"]
|
|
36
36
|
"Interpolation method" = "s-curve"
|
|
37
37
|
"Interpolation center" = 15
|
|
38
38
|
"Interpolation width" = 5
|
|
@@ -22,7 +22,7 @@ Names = [ "Joe",]
|
|
|
22
22
|
"Social security amounts" = [ 28.4,]
|
|
23
23
|
"Social security ages" = [ 67,]
|
|
24
24
|
|
|
25
|
-
["
|
|
25
|
+
["Rates Selection"]
|
|
26
26
|
"Heirs rate on tax-deferred estate" = 30.0
|
|
27
27
|
"Long-term capital gain tax rate" = 15.0
|
|
28
28
|
"Dividend tax rate" = 2.0
|
|
@@ -30,7 +30,7 @@ Method = "historical average"
|
|
|
30
30
|
From = 1969
|
|
31
31
|
To = 2002
|
|
32
32
|
|
|
33
|
-
["Asset
|
|
33
|
+
["Asset Allocation"]
|
|
34
34
|
"Interpolation method" = "s-curve"
|
|
35
35
|
"Interpolation center" = 15
|
|
36
36
|
"Interpolation width" = 5
|
|
@@ -24,7 +24,7 @@ Names = [ "John", "Sally",]
|
|
|
24
24
|
"Social security amounts" = [ 36.0, 21.6,]
|
|
25
25
|
"Social security ages" = [ 67, 67,]
|
|
26
26
|
|
|
27
|
-
["
|
|
27
|
+
["Rates Selection"]
|
|
28
28
|
"Heirs rate on tax-deferred estate" = 30.0
|
|
29
29
|
"Long-term capital gain tax rate" = 15.0
|
|
30
30
|
"Dividend tax rate" = 2.0
|
|
@@ -32,7 +32,7 @@ Method = "historical average"
|
|
|
32
32
|
From = 1990
|
|
33
33
|
To = 2023
|
|
34
34
|
|
|
35
|
-
["Asset
|
|
35
|
+
["Asset Allocation"]
|
|
36
36
|
"Interpolation method" = "linear"
|
|
37
37
|
"Interpolation center" = 15
|
|
38
38
|
"Interpolation width" = 5
|
|
@@ -24,7 +24,7 @@ Names = [ "Kim", "Sam",]
|
|
|
24
24
|
"Social security amounts" = [ 45.0, 25.0,]
|
|
25
25
|
"Social security ages" = [ 70, 68,]
|
|
26
26
|
|
|
27
|
-
["
|
|
27
|
+
["Rates Selection"]
|
|
28
28
|
"Heirs rate on tax-deferred estate" = 33.0
|
|
29
29
|
"Long-term capital gain tax rate" = 15.0
|
|
30
30
|
"Dividend tax rate" = 2.0
|
|
@@ -32,7 +32,7 @@ Method = "conservative"
|
|
|
32
32
|
From = 1922
|
|
33
33
|
To = 2023
|
|
34
34
|
|
|
35
|
-
["Asset
|
|
35
|
+
["Asset Allocation"]
|
|
36
36
|
"Interpolation method" = "s-curve"
|
|
37
37
|
"Interpolation center" = 15
|
|
38
38
|
"Interpolation width" = 5
|
|
@@ -24,7 +24,7 @@ Names = [ "Kim", "Sam",]
|
|
|
24
24
|
"Social security amounts" = [ 45.0, 25.0,]
|
|
25
25
|
"Social security ages" = [ 70, 68,]
|
|
26
26
|
|
|
27
|
-
["
|
|
27
|
+
["Rates Selection"]
|
|
28
28
|
"Heirs rate on tax-deferred estate" = 33.0
|
|
29
29
|
"Long-term capital gain tax rate" = 15.0
|
|
30
30
|
"Dividend tax rate" = 2.0
|
|
@@ -32,7 +32,7 @@ Method = "conservative"
|
|
|
32
32
|
From = 1922
|
|
33
33
|
To = 2023
|
|
34
34
|
|
|
35
|
-
["Asset
|
|
35
|
+
["Asset Allocation"]
|
|
36
36
|
"Interpolation method" = "s-curve"
|
|
37
37
|
"Interpolation center" = 15
|
|
38
38
|
"Interpolation width" = 5
|
|
@@ -43,7 +43,7 @@ class Row(object):
|
|
|
43
43
|
"""
|
|
44
44
|
Add an element at index ``ind`` of value ``val`` to the row.
|
|
45
45
|
"""
|
|
46
|
-
assert 0 <= ind and ind < self.nvars,
|
|
46
|
+
assert 0 <= ind and ind < self.nvars, f"Index {ind} out of range."
|
|
47
47
|
self.ind.append(ind)
|
|
48
48
|
self.val.append(val)
|
|
49
49
|
|
|
@@ -96,11 +96,11 @@ class ConstraintMatrix(object):
|
|
|
96
96
|
self.lb.append(lb)
|
|
97
97
|
self.ub.append(ub)
|
|
98
98
|
if lb == ub:
|
|
99
|
-
self.key.append(
|
|
99
|
+
self.key.append("fx")
|
|
100
100
|
elif ub == np.inf:
|
|
101
|
-
self.key.append(
|
|
101
|
+
self.key.append("lo")
|
|
102
102
|
else:
|
|
103
|
-
self.key.append(
|
|
103
|
+
self.key.append("ra")
|
|
104
104
|
self.ncons += 1
|
|
105
105
|
|
|
106
106
|
def addNewRow(self, rowDic, lb, ub):
|
|
@@ -154,39 +154,39 @@ class Bounds(object):
|
|
|
154
154
|
self.integrality = []
|
|
155
155
|
|
|
156
156
|
def setBinary(self, ii):
|
|
157
|
-
assert 0 <= ii and ii < self.nvars,
|
|
157
|
+
assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
|
|
158
158
|
self.ind.append(ii)
|
|
159
159
|
self.lb.append(0)
|
|
160
160
|
self.ub.append(1)
|
|
161
|
-
self.key.append(
|
|
161
|
+
self.key.append("ra")
|
|
162
162
|
self.integrality.append(ii)
|
|
163
163
|
|
|
164
164
|
def set0_Ub(self, ii, ub):
|
|
165
|
-
assert 0 <= ii and ii < self.nvars,
|
|
165
|
+
assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
|
|
166
166
|
self.ind.append(ii)
|
|
167
167
|
self.lb.append(0)
|
|
168
168
|
self.ub.append(ub)
|
|
169
|
-
self.key.append(
|
|
169
|
+
self.key.append("ra")
|
|
170
170
|
|
|
171
171
|
def setLb_Inf(self, ii, lb):
|
|
172
|
-
assert 0 <= ii and ii < self.nvars,
|
|
172
|
+
assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
|
|
173
173
|
self.ind.append(ii)
|
|
174
174
|
self.lb.append(lb)
|
|
175
175
|
self.ub.append(np.inf)
|
|
176
|
-
self.key.append(
|
|
176
|
+
self.key.append("lo")
|
|
177
177
|
|
|
178
178
|
def setRange(self, ii, lb, ub):
|
|
179
|
-
assert 0 <= ii and ii < self.nvars,
|
|
179
|
+
assert 0 <= ii and ii < self.nvars, f"Index {ii} out of range."
|
|
180
180
|
self.ind.append(ii)
|
|
181
181
|
self.lb.append(lb)
|
|
182
182
|
self.ub.append(ub)
|
|
183
183
|
if lb == ub:
|
|
184
|
-
self.key.append(
|
|
184
|
+
self.key.append("fx")
|
|
185
185
|
else:
|
|
186
|
-
self.key.append(
|
|
186
|
+
self.key.append("ra")
|
|
187
187
|
|
|
188
188
|
def keys(self):
|
|
189
|
-
keys = [
|
|
189
|
+
keys = ["lo"] * self.nvars
|
|
190
190
|
for ii in range(len(self.ind)):
|
|
191
191
|
keys[self.ind[ii]] = self.key[ii]
|
|
192
192
|
|
|
@@ -223,7 +223,7 @@ class Objective(object):
|
|
|
223
223
|
self.val = []
|
|
224
224
|
|
|
225
225
|
def setElem(self, ind, val):
|
|
226
|
-
assert 0 <= ind and ind < self.nvars,
|
|
226
|
+
assert 0 <= ind and ind < self.nvars, f"Index {ind} out of range."
|
|
227
227
|
self.ind.append(ind)
|
|
228
228
|
self.val.append(val)
|
|
229
229
|
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Owl/conftoml
|
|
4
|
+
|
|
5
|
+
This file contains utility functions to save case parameters.
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 -- Martin-D. Lacasse
|
|
8
|
+
|
|
9
|
+
Disclaimer: This program comes with no guarantee. Use at your own risk.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import toml as toml
|
|
13
|
+
from io import StringIO, BytesIO
|
|
14
|
+
import numpy as np
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
from owlplanner import plan
|
|
18
|
+
from owlplanner import logging
|
|
19
|
+
from owlplanner.rates import FROM, TO
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def saveConfig(plan, file, mylog):
|
|
23
|
+
"""
|
|
24
|
+
Save case parameters and return a dictionary containing all parameters.
|
|
25
|
+
"""
|
|
26
|
+
# np.set_printoptions(legacy='1.21')
|
|
27
|
+
accountTypes = ["taxable", "tax-deferred", "tax-free"]
|
|
28
|
+
|
|
29
|
+
diconf = {}
|
|
30
|
+
diconf["Plan Name"] = plan._name
|
|
31
|
+
|
|
32
|
+
# Basic Info.
|
|
33
|
+
diconf["Basic Info"] = {
|
|
34
|
+
"Status": ["unknown", "single", "married"][plan.N_i],
|
|
35
|
+
"Names": plan.inames,
|
|
36
|
+
"Birth year": plan.yobs.tolist(),
|
|
37
|
+
"Life expectancy": plan.expectancy.tolist(),
|
|
38
|
+
"Start date": plan.startDate,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Assets.
|
|
42
|
+
diconf["Assets"] = {}
|
|
43
|
+
for j in range(plan.N_j):
|
|
44
|
+
amounts = plan.beta_ij[:, j] / 1000
|
|
45
|
+
diconf["Assets"][f"{accountTypes[j]} savings balances"] = amounts.tolist()
|
|
46
|
+
if plan.N_i == 2:
|
|
47
|
+
diconf["Assets"]["Beneficiary fractions"] = plan.phi_j.tolist()
|
|
48
|
+
diconf["Assets"]["Spousal surplus deposit fraction"] = plan.eta
|
|
49
|
+
|
|
50
|
+
# Wages and Contributions.
|
|
51
|
+
diconf["Wages and Contributions"] = {"Contributions file name": plan.timeListsFileName}
|
|
52
|
+
|
|
53
|
+
# Fixed Income.
|
|
54
|
+
diconf["Fixed Income"] = {
|
|
55
|
+
"Pension amounts": (plan.pensionAmounts / 1000).tolist(),
|
|
56
|
+
"Pension ages": plan.pensionAges.tolist(),
|
|
57
|
+
"Pension indexed": plan.pensionIndexed,
|
|
58
|
+
"Social security amounts": (plan.ssecAmounts / 1000).tolist(),
|
|
59
|
+
"Social security ages": plan.ssecAges.tolist(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Rates Selection.
|
|
63
|
+
diconf["Rates Selection"] = {
|
|
64
|
+
"Heirs rate on tax-deferred estate": float(100 * plan.nu),
|
|
65
|
+
"Long-term capital gain tax rate": float(100 * plan.psi),
|
|
66
|
+
"Dividend tax rate": float(100 * plan.mu),
|
|
67
|
+
"Method": plan.rateMethod,
|
|
68
|
+
}
|
|
69
|
+
if plan.rateMethod in ["user", "stochastic"]:
|
|
70
|
+
diconf["Rates Selection"]["Values"] = (100 * plan.rateValues).tolist()
|
|
71
|
+
if plan.rateMethod in ["stochastic"]:
|
|
72
|
+
diconf["Rates Selection"]["Standard deviations"] = (100 * plan.rateStdev).tolist()
|
|
73
|
+
diconf["Rates Selection"]["Correlations"] = plan.rateCorr.tolist()
|
|
74
|
+
if plan.rateMethod in ["historical average", "historical", "histochastic"]:
|
|
75
|
+
diconf["Rates Selection"]["From"] = int(plan.rateFrm)
|
|
76
|
+
diconf["Rates Selection"]["To"] = int(plan.rateTo)
|
|
77
|
+
else:
|
|
78
|
+
diconf["Rates Selection"]["From"] = int(FROM)
|
|
79
|
+
diconf["Rates Selection"]["To"] = int(TO)
|
|
80
|
+
|
|
81
|
+
# Asset Allocation.
|
|
82
|
+
diconf["Asset Allocation"] = {
|
|
83
|
+
"Interpolation method": plan.interpMethod,
|
|
84
|
+
"Interpolation center": float(plan.interpCenter),
|
|
85
|
+
"Interpolation width": float(plan.interpWidth),
|
|
86
|
+
"Type": plan.ARCoord,
|
|
87
|
+
}
|
|
88
|
+
if plan.ARCoord == "account":
|
|
89
|
+
for accType in accountTypes:
|
|
90
|
+
diconf["Asset Allocation"][accType] = plan.boundsAR[accType]
|
|
91
|
+
else:
|
|
92
|
+
diconf["Asset Allocation"]["generic"] = plan.boundsAR["generic"]
|
|
93
|
+
|
|
94
|
+
# Optimization Parameters.
|
|
95
|
+
diconf["Optimization Parameters"] = {
|
|
96
|
+
"Spending profile": plan.spendingProfile,
|
|
97
|
+
"Surviving spouse spending percent": int(100 * plan.chi),
|
|
98
|
+
}
|
|
99
|
+
if plan.spendingProfile == "smile":
|
|
100
|
+
diconf["Optimization Parameters"]["Smile dip"] = int(plan.smileDip)
|
|
101
|
+
diconf["Optimization Parameters"]["Smile increase"] = int(plan.smileIncrease)
|
|
102
|
+
diconf["Optimization Parameters"]["Smile delay"] = int(plan.smileDelay)
|
|
103
|
+
|
|
104
|
+
diconf["Optimization Parameters"]["Objective"] = plan.objective
|
|
105
|
+
diconf["Solver Options"] = plan.solverOptions
|
|
106
|
+
|
|
107
|
+
# Results.
|
|
108
|
+
diconf["Results"] = {"Default plots": plan.defaultPlots}
|
|
109
|
+
|
|
110
|
+
if isinstance(file, str):
|
|
111
|
+
filename = file
|
|
112
|
+
if not file.endswith(".toml"):
|
|
113
|
+
filename = filename + ".toml"
|
|
114
|
+
if not filename.startswith("case_"):
|
|
115
|
+
filename = "case_" + filename
|
|
116
|
+
mylog.vprint(f"Saving plan case file as '{filename}'.")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
with open(filename, "w") as casefile:
|
|
120
|
+
toml.dump(diconf, casefile, encoder=toml.TomlNumpyEncoder())
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise RuntimeError(f"Failed to save case file {filename}: {e}")
|
|
123
|
+
elif isinstance(file, StringIO):
|
|
124
|
+
try:
|
|
125
|
+
string = toml.dumps(diconf, encoder=toml.TomlNumpyEncoder())
|
|
126
|
+
file.write(string)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise RuntimeError(f"Failed to save case to StringIO: {e}")
|
|
129
|
+
elif file is None:
|
|
130
|
+
pass
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError(f"Argument {type(file)} has unknown type")
|
|
133
|
+
|
|
134
|
+
return diconf
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
138
|
+
"""
|
|
139
|
+
Read plan parameters from case file *basename*.toml.
|
|
140
|
+
A new plan is created and returned.
|
|
141
|
+
Argument file can be a filename, a file, or a stringIO.
|
|
142
|
+
"""
|
|
143
|
+
mylog = logging.Logger(verbose, logstreams)
|
|
144
|
+
|
|
145
|
+
accountTypes = ["taxable", "tax-deferred", "tax-free"]
|
|
146
|
+
|
|
147
|
+
dirname = ""
|
|
148
|
+
if isinstance(file, str):
|
|
149
|
+
filename = file
|
|
150
|
+
dirname = os.path.dirname(filename)
|
|
151
|
+
if not filename.endswith(".toml"):
|
|
152
|
+
filename = filename + ".toml"
|
|
153
|
+
|
|
154
|
+
mylog.vprint(f"Reading plan from case file '{filename}'.")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
with open(filename, "r") as f:
|
|
158
|
+
diconf = toml.load(f)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise FileNotFoundError(f"File {filename} not found: {e}")
|
|
161
|
+
elif isinstance(file, BytesIO):
|
|
162
|
+
try:
|
|
163
|
+
string = file.getvalue().decode("utf-8")
|
|
164
|
+
diconf = toml.loads(string)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
raise RuntimeError(f"Cannot read from BytesIO: {e}")
|
|
167
|
+
elif isinstance(file, StringIO):
|
|
168
|
+
try:
|
|
169
|
+
string = file.getvalue()
|
|
170
|
+
diconf = toml.loads(string)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
raise RuntimeError(f"Cannot read from StringIO: {e}")
|
|
173
|
+
else:
|
|
174
|
+
raise ValueError(f"Type {type(file)} not a valid type")
|
|
175
|
+
|
|
176
|
+
# Basic Info.
|
|
177
|
+
name = diconf["Plan Name"]
|
|
178
|
+
inames = diconf["Basic Info"]["Names"]
|
|
179
|
+
# status = diconf['Basic Info']['Status']
|
|
180
|
+
yobs = diconf["Basic Info"]["Birth year"]
|
|
181
|
+
expectancy = diconf["Basic Info"]["Life expectancy"]
|
|
182
|
+
startDate = diconf["Basic Info"]["Start date"]
|
|
183
|
+
icount = len(yobs)
|
|
184
|
+
s = ["", "s"][icount - 1]
|
|
185
|
+
mylog.vprint(f"Plan for {icount} individual{s}: {inames}.")
|
|
186
|
+
p = plan.Plan(inames, yobs, expectancy, name, startDate=startDate, verbose=True, logstreams=logstreams)
|
|
187
|
+
|
|
188
|
+
# Assets.
|
|
189
|
+
balances = {}
|
|
190
|
+
for acc in accountTypes:
|
|
191
|
+
balances[acc] = diconf["Assets"][f"{acc} savings balances"]
|
|
192
|
+
p.setAccountBalances(
|
|
193
|
+
taxable=balances["taxable"], taxDeferred=balances["tax-deferred"], taxFree=balances["tax-free"]
|
|
194
|
+
)
|
|
195
|
+
if icount == 2:
|
|
196
|
+
phi_j = diconf["Assets"]["Beneficiary fractions"]
|
|
197
|
+
p.setBeneficiaryFractions(phi_j)
|
|
198
|
+
eta = diconf["Assets"]["Spousal surplus deposit fraction"]
|
|
199
|
+
p.setSpousalDepositFraction(eta)
|
|
200
|
+
|
|
201
|
+
# Wages and Contributions.
|
|
202
|
+
timeListsFileName = diconf["Wages and Contributions"]["Contributions file name"]
|
|
203
|
+
if timeListsFileName != "None":
|
|
204
|
+
if readContributions:
|
|
205
|
+
if os.path.exists(timeListsFileName):
|
|
206
|
+
myfile = timeListsFileName
|
|
207
|
+
elif dirname != "" and os.path.exists(dirname + "/" + timeListsFileName):
|
|
208
|
+
myfile = dirname + "/" + timeListsFileName
|
|
209
|
+
else:
|
|
210
|
+
raise FileNotFoundError(f"File '{timeListsFileName}' not found.")
|
|
211
|
+
p.readContributions(myfile)
|
|
212
|
+
else:
|
|
213
|
+
p.timeListsFileName = timeListsFileName
|
|
214
|
+
mylog.vprint(f"Ignoring to read contributions file {timeListsFileName}.")
|
|
215
|
+
|
|
216
|
+
# Fixed Income.
|
|
217
|
+
ssecAmounts = np.array(diconf["Fixed Income"]["Social security amounts"], dtype=np.float32)
|
|
218
|
+
ssecAges = np.array(diconf["Fixed Income"]["Social security ages"], dtype=np.int32)
|
|
219
|
+
p.setSocialSecurity(ssecAmounts, ssecAges)
|
|
220
|
+
pensionAmounts = np.array(diconf["Fixed Income"]["Pension amounts"], dtype=np.float32)
|
|
221
|
+
pensionAges = np.array(diconf["Fixed Income"]["Pension ages"], dtype=np.int32)
|
|
222
|
+
pensionIndexed = diconf["Fixed Income"]["Pension indexed"]
|
|
223
|
+
p.setPension(pensionAmounts, pensionAges, pensionIndexed)
|
|
224
|
+
|
|
225
|
+
# Rates Selection.
|
|
226
|
+
p.setDividendRate(float(diconf["Rates Selection"]["Dividend tax rate"]))
|
|
227
|
+
p.setLongTermCapitalTaxRate(float(diconf["Rates Selection"]["Long-term capital gain tax rate"]))
|
|
228
|
+
p.setHeirsTaxRate(float(diconf["Rates Selection"]["Heirs rate on tax-deferred estate"]))
|
|
229
|
+
|
|
230
|
+
frm = None
|
|
231
|
+
to = None
|
|
232
|
+
rateValues = None
|
|
233
|
+
stdev = None
|
|
234
|
+
rateCorr = None
|
|
235
|
+
rateMethod = diconf["Rates Selection"]["Method"]
|
|
236
|
+
if rateMethod in ["historical average", "historical", "histochastic"]:
|
|
237
|
+
frm = diconf["Rates Selection"]["From"]
|
|
238
|
+
if not isinstance(frm, int):
|
|
239
|
+
frm = int(frm)
|
|
240
|
+
to = int(diconf["Rates Selection"]["To"])
|
|
241
|
+
if not isinstance(to, int):
|
|
242
|
+
to = int(to)
|
|
243
|
+
if rateMethod in ["user", "stochastic"]:
|
|
244
|
+
rateValues = np.array(diconf["Rates Selection"]["Values"], dtype=np.float32)
|
|
245
|
+
if rateMethod in ["stochastic"]:
|
|
246
|
+
stdev = np.array(diconf["Rates Selection"]["Standard deviations"], dtype=np.float32)
|
|
247
|
+
rateCorr = np.array(diconf["Rates Selection"]["Correlations"], dtype=np.float32)
|
|
248
|
+
p.setRates(rateMethod, frm, to, rateValues, stdev, rateCorr)
|
|
249
|
+
|
|
250
|
+
# Asset Allocation.
|
|
251
|
+
boundsAR = {}
|
|
252
|
+
p.setInterpolationMethod(
|
|
253
|
+
diconf["Asset Allocation"]["Interpolation method"],
|
|
254
|
+
float(diconf["Asset Allocation"]["Interpolation center"]),
|
|
255
|
+
float(diconf["Asset Allocation"]["Interpolation width"]),
|
|
256
|
+
)
|
|
257
|
+
allocType = diconf["Asset Allocation"]["Type"]
|
|
258
|
+
if allocType == "account":
|
|
259
|
+
for aType in accountTypes:
|
|
260
|
+
boundsAR[aType] = np.array(diconf["Asset Allocation"][aType], dtype=np.float32)
|
|
261
|
+
|
|
262
|
+
p.setAllocationRatios(
|
|
263
|
+
allocType,
|
|
264
|
+
taxable=boundsAR["taxable"],
|
|
265
|
+
taxDeferred=boundsAR["tax-deferred"],
|
|
266
|
+
taxFree=boundsAR["tax-free"],
|
|
267
|
+
)
|
|
268
|
+
elif allocType == "individual" or allocType == "spouses":
|
|
269
|
+
boundsAR["generic"] = np.array(diconf["Asset Allocation"]["generic"], dtype=np.float32)
|
|
270
|
+
p.setAllocationRatios(
|
|
271
|
+
allocType,
|
|
272
|
+
generic=boundsAR["generic"],
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
raise ValueError(f"Unknown asset allocation type {allocType}.")
|
|
276
|
+
|
|
277
|
+
# Optimization Parameters.
|
|
278
|
+
p.objective = diconf["Optimization Parameters"]["Objective"]
|
|
279
|
+
|
|
280
|
+
profile = diconf["Optimization Parameters"]["Spending profile"]
|
|
281
|
+
survivor = int(diconf["Optimization Parameters"]["Surviving spouse spending percent"])
|
|
282
|
+
if profile == "smile":
|
|
283
|
+
dip = int(diconf["Optimization Parameters"]["Smile dip"])
|
|
284
|
+
increase = int(diconf["Optimization Parameters"]["Smile increase"])
|
|
285
|
+
delay = int(diconf["Optimization Parameters"]["Smile delay"])
|
|
286
|
+
else:
|
|
287
|
+
dip = 15
|
|
288
|
+
increase = 12
|
|
289
|
+
delay = 0
|
|
290
|
+
|
|
291
|
+
p.setSpendingProfile(profile, survivor, dip, increase, delay)
|
|
292
|
+
|
|
293
|
+
# Solver Options.
|
|
294
|
+
p.solverOptions = diconf["Solver Options"]
|
|
295
|
+
|
|
296
|
+
# Results.
|
|
297
|
+
p.setDefaultPlots(diconf["Results"]["Default plots"])
|
|
298
|
+
|
|
299
|
+
return p
|
|
@@ -19,15 +19,15 @@ class Logger(object):
|
|
|
19
19
|
self._prevState = self._verbose
|
|
20
20
|
if logstreams is None or logstreams == [] or len(logstreams) > 2:
|
|
21
21
|
self._logstreams = [sys.stdout, sys.stderr]
|
|
22
|
-
self.vprint(
|
|
22
|
+
self.vprint("Using stdout and stderr as stream loggers.")
|
|
23
23
|
elif len(logstreams) == 2:
|
|
24
24
|
self._logstreams = logstreams
|
|
25
|
-
self.vprint(
|
|
25
|
+
self.vprint("Using logstreams as stream loggers.")
|
|
26
26
|
elif len(logstreams) == 1:
|
|
27
|
-
self._logstreams = 2*logstreams
|
|
28
|
-
self.vprint(
|
|
27
|
+
self._logstreams = 2 * logstreams
|
|
28
|
+
self.vprint("Using logstream as stream logger.")
|
|
29
29
|
else:
|
|
30
|
-
raise ValueError(
|
|
30
|
+
raise ValueError(f"Log streams {logstreams} must be a list.")
|
|
31
31
|
|
|
32
32
|
def setVerbose(self, verbose=True):
|
|
33
33
|
"""
|
|
@@ -36,7 +36,7 @@ class Logger(object):
|
|
|
36
36
|
"""
|
|
37
37
|
self._prevState = self._verbose
|
|
38
38
|
self._verbose = verbose
|
|
39
|
-
self.vprint(
|
|
39
|
+
self.vprint("Setting verbose to", verbose)
|
|
40
40
|
|
|
41
41
|
return self._prevState
|
|
42
42
|
|
|
@@ -51,9 +51,9 @@ class Logger(object):
|
|
|
51
51
|
Unconditional printing regardless of the value of the verbose variable
|
|
52
52
|
previously set.
|
|
53
53
|
"""
|
|
54
|
-
if
|
|
54
|
+
if "file" not in kwargs:
|
|
55
55
|
file = self._logstreams[0]
|
|
56
|
-
kwargs[
|
|
56
|
+
kwargs["file"] = file
|
|
57
57
|
|
|
58
58
|
print(*args, **kwargs)
|
|
59
59
|
file.flush()
|
|
@@ -71,14 +71,14 @@ class Logger(object):
|
|
|
71
71
|
Print message and exit. Use to print error messages on stderr.
|
|
72
72
|
The exit() used throws an exception in an interactive environment.
|
|
73
73
|
"""
|
|
74
|
-
if
|
|
74
|
+
if "file" not in kwargs:
|
|
75
75
|
file = self._logstreams[1]
|
|
76
|
-
kwargs[
|
|
76
|
+
kwargs["file"] = file
|
|
77
77
|
|
|
78
78
|
if self._verbose:
|
|
79
|
-
print(
|
|
80
|
-
print(
|
|
79
|
+
print("ERROR:", *args, **kwargs)
|
|
80
|
+
print("Exiting...")
|
|
81
81
|
file.flush()
|
|
82
82
|
|
|
83
|
-
raise Exception(
|
|
83
|
+
raise Exception("Fatal error.")
|
|
84
84
|
# sys.exit(-1)
|