owlplanner 2025.2.25__tar.gz → 2025.3.7__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.25 → owlplanner-2025.3.7}/.github/workflows/github-actions-runtests.yml +1 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/INSTALL.md +4 -3
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/PKG-INFO +9 -3
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/README.md +8 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/case_jack+jill.toml +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/case_john+sally.toml +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/case_jon+jane.toml +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/pyproject.toml +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/plan.py +53 -26
- owlplanner-2025.3.7/src/owlplanner/version.py +1 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Asset_Allocation.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Assets.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Create_Case.py +19 -8
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Documentation.py +34 -16
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Fixed_Income.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Graphs.py +3 -3
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Historical_Range.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Logs.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Monte_Carlo.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Optimization_Parameters.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Output_Files.py +9 -7
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Quick_Start.py +6 -6
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Rates_Selection.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Wages_And_Contributions.py +9 -7
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Worksheets.py +1 -2
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/owlbridge.py +10 -11
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/plots.py +2 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/requirements.txt +1 -1
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/sskeys.py +64 -28
- owlplanner-2025.3.7/ui/tomlexamples.py +16 -0
- owlplanner-2025.2.25/src/owlplanner/version.py +0 -1
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/.flake8 +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/.gitattributes +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/.gitignore +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/LICENSE +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/USER_GUIDE.md +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docker/Dockerfile +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docker/README.md +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docker/fastentrypoint.sh +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/allocations.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/owl.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/profile.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/owl.pdf +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/docs/owl.tex +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/case_joe.toml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/joe.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/john+sally.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/jon+jane.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/examples/template.xlsx +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/owlplanner.cmd +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/owlplanner.sh +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/requirements.txt +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/config.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/logging.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/tax2025.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/timelists.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/tests/test_logger.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/tests/test_regressions.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/tests/test_repro.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/tests/test_units.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ttt.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/About_Owl.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/README.md +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/Settings.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/main+fonts.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/main.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/progress.py +0 -0
- {owlplanner-2025.2.25 → owlplanner-2025.3.7}/ui/style.css +0 -0
|
@@ -14,7 +14,7 @@ or, if one prefers to have everything on their own computer,
|
|
|
14
14
|
to install and run a Docker image as described in these [instructions](docker/README.md).
|
|
15
15
|
|
|
16
16
|
### Requirements
|
|
17
|
-
You will need Python and `pip` installed on your computer for
|
|
17
|
+
You will need Python and `pip` installed on your computer for completing the installation.
|
|
18
18
|
|
|
19
19
|
### Installation steps for developers
|
|
20
20
|
These instructions are command-line instructions.
|
|
@@ -29,7 +29,8 @@ From the top directory of the source code run:
|
|
|
29
29
|
python -m build
|
|
30
30
|
pip install -e .
|
|
31
31
|
```
|
|
32
|
-
The -e instructs
|
|
32
|
+
The -e instructs `pip` to install in *editable* mode and use the live version
|
|
33
|
+
in the current directory tree.
|
|
33
34
|
|
|
34
35
|
### Running the streamlit frontend
|
|
35
36
|
Running the Owl user interface locally from Windows:
|
|
@@ -42,7 +43,7 @@ Running the Owl user interface locally from Linux or MacOS:
|
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
### Publishing a version (for reference only)
|
|
45
|
-
Run checks before
|
|
46
|
+
Run checks before all commits:
|
|
46
47
|
```
|
|
47
48
|
flake8 ui src tests
|
|
48
49
|
pytest
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.3.7
|
|
4
4
|
Summary: Owl: Retirement planner with great wisdom
|
|
5
5
|
Project-URL: HomePage, https://github.com/mdlacasse/owl
|
|
6
6
|
Project-URL: Repository, https://github.com/mdlacasse/owl
|
|
@@ -711,7 +711,13 @@ Description-Content-Type: text/markdown
|
|
|
711
711
|
-------------------------------------------------------------------------------------
|
|
712
712
|
|
|
713
713
|
### TL;DR
|
|
714
|
-
Owl is a planning tool that uses a linear programming optimization algorithm
|
|
714
|
+
Owl is a retirement planning tool that uses a linear programming optimization algorithm
|
|
715
|
+
to provide guidance on retirement decisions, including Roth conversions.
|
|
716
|
+
Users can select varying return rates to perform historical back testing,
|
|
717
|
+
stochastic rates for performing Monte Carlo analyses,
|
|
718
|
+
or fixed rates either derived from historical averages, or set by the user.
|
|
719
|
+
|
|
720
|
+
There are a few ways to run Owl:
|
|
715
721
|
|
|
716
722
|
- Run Owl directly on the Streamlit Community Server at [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
|
|
717
723
|
|
|
@@ -868,7 +874,7 @@ assets to support, even with no estate being left.
|
|
|
868
874
|
---------------------------------------------------------------
|
|
869
875
|
## Documentation
|
|
870
876
|
|
|
871
|
-
- Documentation for the app user interface is available from the interface itself.
|
|
877
|
+
- Documentation for the app user interface is available from the interface [itself](https://owlplanner.streamlit.app/Documentation).
|
|
872
878
|
- Installation guide and software requirements can be found [here](INSTALL.md).
|
|
873
879
|
- User guide for the underlying Python package as used in a Jupyter notebook can be found [here](USER_GUIDE.md).
|
|
874
880
|
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
-------------------------------------------------------------------------------------
|
|
9
9
|
|
|
10
10
|
### TL;DR
|
|
11
|
-
Owl is a planning tool that uses a linear programming optimization algorithm
|
|
11
|
+
Owl is a retirement planning tool that uses a linear programming optimization algorithm
|
|
12
|
+
to provide guidance on retirement decisions, including Roth conversions.
|
|
13
|
+
Users can select varying return rates to perform historical back testing,
|
|
14
|
+
stochastic rates for performing Monte Carlo analyses,
|
|
15
|
+
or fixed rates either derived from historical averages, or set by the user.
|
|
16
|
+
|
|
17
|
+
There are a few ways to run Owl:
|
|
12
18
|
|
|
13
19
|
- Run Owl directly on the Streamlit Community Server at [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
|
|
14
20
|
|
|
@@ -165,7 +171,7 @@ assets to support, even with no estate being left.
|
|
|
165
171
|
---------------------------------------------------------------
|
|
166
172
|
## Documentation
|
|
167
173
|
|
|
168
|
-
- Documentation for the app user interface is available from the interface itself.
|
|
174
|
+
- Documentation for the app user interface is available from the interface [itself](https://owlplanner.streamlit.app/Documentation).
|
|
169
175
|
- Installation guide and software requirements can be found [here](INSTALL.md).
|
|
170
176
|
- User guide for the underlying Python package as used in a Jupyter notebook can be found [here](USER_GUIDE.md).
|
|
171
177
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "jack+jill"
|
|
2
|
-
Description = "This example
|
|
2
|
+
Description = "This example aims to demonstrate some of Owl's capabilities. Jack and Jill are a married couple a few years from retirement. A wages and contributions file called 'jack+jill.xlsx' is associated with this case. This case uses the historical rate sequence of 1969 as a test case for guiding spending amounts from a near worst-case historical scenario. This case also demonstrates that the optimal strategy for Roth conversions does not necessarily involve surfing a tax bracket."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "married"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "john+sally"
|
|
2
|
-
Description = "This example reproduces the case of John and Sally, discussed by Eric Sajdak. This case can be used to compare the heuristic strategy of surfing a tax bracket for performing Roth conversions to a solution optimized by linear programming. The former is a good approach when one assumes fixed rates. When rates are varying, and the market drops, an optimized solution shows that it is sometime advantageous to convert above the target tax bracket. File 'john+sally.xlsx' contains wages and contributions associated with this case. Run this case with different rates to see the
|
|
2
|
+
Description = "This example reproduces the case of John and Sally, discussed by Eric Sajdak. This case can be used to compare the heuristic strategy of surfing a tax bracket for performing Roth conversions to a solution optimized by linear programming. The former is a good approach when one assumes fixed rates. When rates are varying, and the market drops, an optimized solution shows that it is sometime advantageous to convert above the target tax bracket. File 'john+sally.xlsx' contains wages and contributions associated with this case. Run this case with different rates to see the effects on Roth conversions."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "married"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "Jon+Jane"
|
|
2
|
-
Description = "This case
|
|
2
|
+
Description = "This case reproduces a similar case discussed a while back on i-orp. It involves Jon and Jane close to retirement, and assumes a base case of optimistic returns of 10% with an inflation of 3.5%. A wages and contributions file called 'jon+jane.xlsx' is associated with this case."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "married"
|
|
@@ -1968,8 +1968,8 @@ class Plan(object):
|
|
|
1968
1968
|
self.dist_in = self.w_ijn[:, 1, :] - self.rmd_in
|
|
1969
1969
|
self.dist_in[self.dist_in < 0] = 0
|
|
1970
1970
|
self.G_n = np.sum(self.F_tn, axis=0)
|
|
1971
|
-
T_tn = self.F_tn * self.theta_tn
|
|
1972
|
-
self.T_n = np.sum(T_tn, axis=0)
|
|
1971
|
+
self.T_tn = self.F_tn * self.theta_tn
|
|
1972
|
+
self.T_n = np.sum(self.T_tn, axis=0)
|
|
1973
1973
|
|
|
1974
1974
|
tau_0 = np.array(self.tau_kn[0, :])
|
|
1975
1975
|
tau_0[tau_0 < 0] = 0
|
|
@@ -2062,6 +2062,12 @@ class Plan(object):
|
|
|
2062
2062
|
|
|
2063
2063
|
return mylist
|
|
2064
2064
|
|
|
2065
|
+
def summaryDf(self):
|
|
2066
|
+
"""
|
|
2067
|
+
Return summary as a dataframe.
|
|
2068
|
+
"""
|
|
2069
|
+
return pd.DataFrame(self.summaryDic(), index=[self._name])
|
|
2070
|
+
|
|
2065
2071
|
def summaryString(self):
|
|
2066
2072
|
"""
|
|
2067
2073
|
Return summary as a string.
|
|
@@ -2081,29 +2087,40 @@ class Plan(object):
|
|
|
2081
2087
|
dic = {}
|
|
2082
2088
|
# Results
|
|
2083
2089
|
dic["Plan name"] = self._name
|
|
2084
|
-
dic[
|
|
2085
|
-
dic[f"Net
|
|
2090
|
+
dic["Net yearly spending basis"] = u.d(self.g_n[0] / self.xi_n[0])
|
|
2091
|
+
dic[f"Net spending for year {now}"] = u.d(self.g_n[0] / self.yearFracLeft)
|
|
2086
2092
|
dic[f"Net spending remaining in year {now}"] = u.d(self.g_n[0])
|
|
2087
2093
|
|
|
2088
2094
|
totIncome = np.sum(self.g_n, axis=0)
|
|
2089
2095
|
totIncomeNow = np.sum(self.g_n / self.gamma_n[:-1], axis=0)
|
|
2090
|
-
dic[
|
|
2096
|
+
dic["Total net spending"] = f"{u.d(totIncomeNow)}"
|
|
2097
|
+
dic["- Total net spending (nominal)"] = f"{u.d(totIncome)}"
|
|
2091
2098
|
|
|
2092
2099
|
totRoth = np.sum(self.x_in, axis=(0, 1))
|
|
2093
2100
|
totRothNow = np.sum(np.sum(self.x_in, axis=0) / self.gamma_n[:-1], axis=0)
|
|
2094
|
-
dic[
|
|
2101
|
+
dic["Total Roth conversions"] = f"{u.d(totRothNow)}"
|
|
2102
|
+
dic["- Total Roth conversions (nominal)"] = f"{u.d(totRoth)}"
|
|
2095
2103
|
|
|
2096
2104
|
taxPaid = np.sum(self.T_n, axis=0)
|
|
2097
2105
|
taxPaidNow = np.sum(self.T_n / self.gamma_n[:-1], axis=0)
|
|
2098
|
-
dic[
|
|
2106
|
+
dic["Total income tax paid on ordinary income"] = f"{u.d(taxPaidNow)}"
|
|
2107
|
+
dic["- Total income tax paid on ordinary income (nominal)"] = f"{u.d(taxPaid)}"
|
|
2108
|
+
for t in range(self.N_t):
|
|
2109
|
+
taxPaid = np.sum(self.T_tn[t], axis=0)
|
|
2110
|
+
taxPaidNow = np.sum(self.T_tn[t] / self.gamma_n[:-1], axis=0)
|
|
2111
|
+
tname = tx.taxBracketNames[t]
|
|
2112
|
+
dic[f"-- Subtotal in tax bracket {tname}"] = f"{u.d(taxPaidNow)}"
|
|
2113
|
+
dic[f"--- Subtotal in tax bracket {tname} (nominal)"] = f"{u.d(taxPaid)}"
|
|
2099
2114
|
|
|
2100
2115
|
taxPaid = np.sum(self.U_n, axis=0)
|
|
2101
2116
|
taxPaidNow = np.sum(self.U_n / self.gamma_n[:-1], axis=0)
|
|
2102
|
-
dic[
|
|
2117
|
+
dic["Total tax paid on gains and dividends"] = f"{u.d(taxPaidNow)}"
|
|
2118
|
+
dic["- Total tax paid on gains and dividends (nominal)"] = f"{u.d(taxPaid)}"
|
|
2103
2119
|
|
|
2104
2120
|
taxPaid = np.sum(self.M_n, axis=0)
|
|
2105
2121
|
taxPaidNow = np.sum(self.M_n / self.gamma_n[:-1], axis=0)
|
|
2106
|
-
dic[
|
|
2122
|
+
dic["Total Medicare premiums paid"] = f"{u.d(taxPaidNow)}"
|
|
2123
|
+
dic["- Total Medicare premiums paid (nominal)"] = f"{u.d(taxPaid)}"
|
|
2107
2124
|
|
|
2108
2125
|
if self.N_i == 2 and self.n_d < self.N_n:
|
|
2109
2126
|
p_j = self.partialEstate_j * (1 - self.phi_j)
|
|
@@ -2117,30 +2134,40 @@ class Plan(object):
|
|
|
2117
2134
|
totSpousalNow = totSpousal / self.gamma_n[nx + 1]
|
|
2118
2135
|
iname_s = self.inames[self.i_s]
|
|
2119
2136
|
iname_d = self.inames[self.i_d]
|
|
2120
|
-
dic[f"
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
dic[f"
|
|
2124
|
-
f"{u.d(
|
|
2125
|
-
dic[
|
|
2126
|
-
f"
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2137
|
+
dic[f"Sum of spousal transfer to {iname_s} in year {ynx}"] = (f"{u.d(totSpousalNow)}")
|
|
2138
|
+
dic[f"- Sum of spousal transfer to {iname_s} in year {ynx} (nominal)"] = (
|
|
2139
|
+
f"{u.d(totSpousal)}")
|
|
2140
|
+
dic[f"-- Spousal transfer to {iname_s} in year {ynx} - taxable (nominal)"] = (
|
|
2141
|
+
f"{u.d(q_j[0])}")
|
|
2142
|
+
dic[f"-- Spousal transfer to {iname_s} in year {ynx} - tax-def (nominal)"] = (
|
|
2143
|
+
f"{u.d(q_j[1])}")
|
|
2144
|
+
dic[f"-- Spousal transfer to {iname_s} in year {ynx} - tax-free (nominal)"] = (
|
|
2145
|
+
f"{u.d(q_j[2])}")
|
|
2146
|
+
|
|
2147
|
+
dic[f"Sum of post-tax non-spousal bequests from {iname_d} in year {ynx}"] = (
|
|
2148
|
+
f"{u.d(totOthersNow)}")
|
|
2149
|
+
dic[f"- Sum of post-tax non-spousal bequests from {iname_d} in year {ynx} (nominal)"] = (
|
|
2150
|
+
f"{u.d(totOthers)}")
|
|
2151
|
+
dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - taxable (nominal)"] = (
|
|
2152
|
+
f"{u.d(p_j[0])}")
|
|
2153
|
+
dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-def (nominal)"] = (
|
|
2154
|
+
f"{u.d(p_j[1])}")
|
|
2155
|
+
dic[f"-- Post-tax non-spousal bequests from {iname_d} in year {ynx} - tax-free (nominal)"] = (
|
|
2156
|
+
f"{u.d(p_j[2])}")
|
|
2131
2157
|
|
|
2132
2158
|
estate = np.sum(self.b_ijn[:, :, self.N_n], axis=0)
|
|
2133
2159
|
estate[1] *= 1 - self.nu
|
|
2134
2160
|
lastyear = self.year_n[-1]
|
|
2135
|
-
dic[f"Post-tax account values at the end of final plan year {lastyear} (nominal)"] = (
|
|
2136
|
-
f"taxable: {u.d(estate[0])} tax-def: {u.d(estate[1])} tax-free: {u.d(estate[2])}")
|
|
2137
|
-
|
|
2138
2161
|
totEstate = np.sum(estate)
|
|
2139
2162
|
totEstateNow = totEstate / self.gamma_n[-1]
|
|
2140
|
-
dic[f"Total estate value at the end of
|
|
2141
|
-
|
|
2163
|
+
dic[f"Total estate value at the end of {lastyear}"] = (f"{u.d(totEstateNow)}")
|
|
2164
|
+
dic[f"- Total estate value at the end of {lastyear} (nominal)"] = (f"{u.d(totEstate)}")
|
|
2165
|
+
dic[f"-- Post-tax account value at the end of {lastyear} - taxable (nominal)"] = (f"{u.d(estate[0])}")
|
|
2166
|
+
dic[f"-- Post-tax account value at the end of {lastyear} - tax-def (nominal)"] = (f"{u.d(estate[1])}")
|
|
2167
|
+
dic[f"-- Post-tax account value at the end of {lastyear} - tax-free (nominal)"] = (f"{u.d(estate[2])}")
|
|
2168
|
+
|
|
2142
2169
|
dic["Plan starting date"] = str(self.startDate)
|
|
2143
|
-
dic["Cumulative inflation factor from start date to end of
|
|
2170
|
+
dic[f"Cumulative inflation factor from start date to end of {lastyear}"] = (f"{self.gamma_n[-1]:.2f}")
|
|
2144
2171
|
for i in range(self.N_i):
|
|
2145
2172
|
dic[f"{self.inames[i]:>12}'s {self.horizons[i]:02}-year life horizon"] = (
|
|
2146
2173
|
f"{now} -> {now + self.horizons[i] - 1}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2025.03.07"
|
|
@@ -81,8 +81,7 @@ def checkAllAllocs():
|
|
|
81
81
|
return result
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
ret = kz.titleBar("
|
|
85
|
-
kz.caseHeader("Asset Allocation")
|
|
84
|
+
ret = kz.titleBar("Asset Allocation")
|
|
86
85
|
|
|
87
86
|
if ret is None or kz.caseHasNoPlan():
|
|
88
87
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from datetime import date
|
|
2
|
+
from io import StringIO
|
|
2
3
|
import streamlit as st
|
|
3
4
|
|
|
4
5
|
import sskeys as kz
|
|
5
6
|
import owlbridge as owb
|
|
7
|
+
import tomlexamples as tomlex
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
caseChoices = kz.allCaseNames()
|
|
9
|
-
ret = kz.titleBar("
|
|
10
|
-
kz.caseHeader("Create Case")
|
|
11
|
+
ret = kz.titleBar("Create Case", caseChoices)
|
|
11
12
|
|
|
12
13
|
if ret == kz.newCase:
|
|
13
14
|
st.info("#### Starting a new case from scratch.\n\n" "A name for the scenario must first be provided.")
|
|
@@ -23,12 +24,22 @@ elif ret == kz.loadCaseFile:
|
|
|
23
24
|
# "<a href="Documentation" target="_self">Documentation</a>", unsafe_allow_html=True)
|
|
24
25
|
st.info(
|
|
25
26
|
"#### Starting a case from a *case* parameter file.\n\n"
|
|
26
|
-
"
|
|
27
|
-
"Alternatively, select `New Case...` to start a case from scratch
|
|
27
|
+
"Upload your own case or select one from multiple examples."
|
|
28
|
+
" Alternatively, you can select `New Case...` in the top selector box to start a case from scratch.\n\n"
|
|
29
|
+
"Look at the :material/help: [Documentation](Documentation) for more details."
|
|
28
30
|
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
st.write("#### Upload your own case file")
|
|
32
|
+
file = st.file_uploader("Upload *case* parameter file...", key="_confile", type=["toml"])
|
|
33
|
+
if file is not None:
|
|
34
|
+
mystringio = StringIO(file.read().decode("utf-8"))
|
|
35
|
+
if kz.createCaseFromFile(mystringio):
|
|
36
|
+
st.rerun()
|
|
37
|
+
|
|
38
|
+
st.write("#### Load a case example")
|
|
39
|
+
case = st.selectbox("Examples available from GitHub", tomlex.cases, index=None, placeholder="Select a case")
|
|
40
|
+
if case is not None:
|
|
41
|
+
mystringio = tomlex.loadExample(case)
|
|
42
|
+
if kz.createCaseFromFile(mystringio):
|
|
32
43
|
st.rerun()
|
|
33
44
|
else:
|
|
34
45
|
helpmsg = "Case name can be changed by editing it directly."
|
|
@@ -110,7 +121,7 @@ else:
|
|
|
110
121
|
st.divider()
|
|
111
122
|
cantcreate = kz.isIncomplete() or diz1
|
|
112
123
|
if not cantcreate and kz.getKey("plan") is None:
|
|
113
|
-
st.info("Plan needs to be created once
|
|
124
|
+
st.info("Plan needs to be created once desired changes are completed.")
|
|
114
125
|
|
|
115
126
|
cantmodify = kz.currentCaseName() == kz.newCase or kz.currentCaseName() == kz.loadCaseFile
|
|
116
127
|
cantcopy = cantmodify or kz.caseHasNoPlan()
|
|
@@ -34,16 +34,28 @@ formulation of the optimization problem can be found
|
|
|
34
34
|
|
|
35
35
|
--------------------------------------------------------------------------------------
|
|
36
36
|
### Getting started with the user interface
|
|
37
|
-
Functions of each page are described below in the same order as they appear in the sidebar.
|
|
37
|
+
Functions of each page are described below in the same order as they appear in the left sidebar.
|
|
38
38
|
Typically, pages would be accessed in order, starting from the top.
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
The `Case selector` box at the top of the page allows to select an existing case
|
|
40
41
|
or create a new one from scratch, or from a *case* parameter file, which
|
|
41
42
|
would then populate all parameter values.
|
|
42
43
|
This box is present in all pages except those in the **Resources** section
|
|
43
|
-
and allows to compare different scenarios.
|
|
44
|
+
and allows to access and compare different scenarios.
|
|
45
|
+
|
|
46
|
+
A typical workflow for exploring different scenarios involves starting with a base
|
|
47
|
+
case and then duplicating/creating derived scenarios with slight changes in the parameters,
|
|
48
|
+
which are configured in the **Case Setup** section. The comparison between the
|
|
49
|
+
different resulting outcomes is shown on the [Output Files](#output-files) page.
|
|
50
|
+
|
|
51
|
+
Owl uses a year as the standard time unit. All values are therefore entered and
|
|
52
|
+
reported as yearly values. These include wages, income, rates, social security, etc.
|
|
53
|
+
Dollar values are typically entered in thousands, unless in tables, where they
|
|
54
|
+
are entered and reported in unit dollars.
|
|
44
55
|
|
|
45
56
|
There are four sections in the user interface:
|
|
46
57
|
**Case Setup**, **Single Scenario**, **Multiple Scenarios**, and **Resources**.
|
|
58
|
+
The sections below follow the same logical order.
|
|
47
59
|
|
|
48
60
|
-------------------------------------------------
|
|
49
61
|
### :orange[Case Setup]
|
|
@@ -51,7 +63,7 @@ This section contains the steps for creating and configuring case scenarios.
|
|
|
51
63
|
|
|
52
64
|
#### Create Case
|
|
53
65
|
This page is where every new scenario begins.
|
|
54
|
-
It controls the creation of scenarios as the `
|
|
66
|
+
It controls the creation of scenarios as the `Case selector` drop-down menu contains
|
|
55
67
|
two additional items when this page is open:
|
|
56
68
|
one to create new cases, and the other to create cases from a *case* parameter file.
|
|
57
69
|
This page also allows you to duplicate and/or rename scenarios, as well as deleting them.
|
|
@@ -65,7 +77,7 @@ When duplicating a scenario, make sure to visit all pages in the **Case Setup**
|
|
|
65
77
|
and verify that all parameters are as intended.
|
|
66
78
|
|
|
67
79
|
##### Initializing the life parameters for the realization
|
|
68
|
-
Start with the `
|
|
80
|
+
Start with the `Case selector` box and choose one of `New case...` or `Upload case file...`.
|
|
69
81
|
|
|
70
82
|
If `Upload case file...` is selected, a *case* file must be uploaded.
|
|
71
83
|
These files end with the *.toml* extension, are human readable (and therefore editable),
|
|
@@ -307,22 +319,28 @@ The first line of the *Sources* worksheets are the most important
|
|
|
307
319
|
as these lines are the only ones that are actionable.
|
|
308
320
|
|
|
309
321
|
#### Output Files
|
|
310
|
-
This page
|
|
311
|
-
First it shows a synopsis of the computed scenario by
|
|
322
|
+
This page allows to compare cases and save files for future use.
|
|
323
|
+
First, it shows a synopsis of the computed scenario by
|
|
312
324
|
displaying sums of income, bequest, and spending values over the duration of the plan.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
The
|
|
318
|
-
|
|
325
|
+
If multiple cases were configured and run (most likely through duplication and
|
|
326
|
+
modifying the configuration), they will be compared in that panel provided they were made
|
|
327
|
+
for the same individuals. Column on the left shows the values for the selected case
|
|
328
|
+
while those on the right will show the differences.
|
|
329
|
+
The contents of the synopsis can be downloaded as a plain text file by
|
|
330
|
+
clicking the button below it.
|
|
319
331
|
|
|
320
332
|
Another section called `Excel workbooks` allows
|
|
321
|
-
to save the contents of the tables on the corresponding page
|
|
333
|
+
to save the contents of the tables on the corresponding page as an Excel workbook.
|
|
334
|
+
These data are displayed on the *Worksheets* and the *Wages and Contributions* pages.
|
|
335
|
+
|
|
336
|
+
Similarly, all parameters used to generate the case are collected in *toml* format and displayed.
|
|
337
|
+
The `Download case file...` button allows to save the parameters of the selected scenario
|
|
338
|
+
to a *case* file.
|
|
322
339
|
|
|
323
|
-
With the case parameter and the wages and contributions
|
|
340
|
+
With the case parameter file and the wages and contributions worksheet,
|
|
324
341
|
the same case can be reproduced at a later time by uploading
|
|
325
|
-
them through the widgets on the `Create Case` and `Wages and Contributions` pages
|
|
342
|
+
them through the widgets on the `Create Case` and `Wages and Contributions` pages,
|
|
343
|
+
respectively.
|
|
326
344
|
|
|
327
345
|
--------------------------------------------------------------------------------------
|
|
328
346
|
### :orange[Multiple Scenarios]
|
|
@@ -36,8 +36,7 @@ def getToggleInput(i, key, thing):
|
|
|
36
36
|
st.toggle(thing, on_change=kz.setpull, value=defval, args=[nkey], key="_" + nkey)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
ret = kz.titleBar("
|
|
40
|
-
kz.caseHeader("Fixed Income")
|
|
39
|
+
ret = kz.titleBar("Fixed Income")
|
|
41
40
|
|
|
42
41
|
if ret is None or kz.caseHasNoPlan():
|
|
43
42
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -4,8 +4,7 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
ret = kz.titleBar("
|
|
8
|
-
kz.caseHeader("Graphs")
|
|
7
|
+
ret = kz.titleBar("Graphs")
|
|
9
8
|
|
|
10
9
|
if ret is None or kz.caseHasNoPlan():
|
|
11
10
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -21,9 +20,10 @@ else:
|
|
|
21
20
|
ret = kz.getRadio("Dollar amounts in plots", choices, "plots", callback=owb.setDefaultPlots)
|
|
22
21
|
|
|
23
22
|
with col2:
|
|
23
|
+
helpmsg = "Click on button if graphs are not all showing."
|
|
24
24
|
st.button(
|
|
25
25
|
"Re-run single case",
|
|
26
|
-
help=
|
|
26
|
+
help=helpmsg,
|
|
27
27
|
on_click=owb.runPlan,
|
|
28
28
|
disabled=kz.caseIsNotRunReady(),
|
|
29
29
|
)
|
|
@@ -4,8 +4,7 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
ret = kz.titleBar("
|
|
8
|
-
kz.caseHeader("Historical Range")
|
|
7
|
+
ret = kz.titleBar("Historical Range")
|
|
9
8
|
|
|
10
9
|
if ret is None or kz.caseHasNoPlan():
|
|
11
10
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -16,8 +16,7 @@ def initProfile():
|
|
|
16
16
|
owb.setProfile(profileChoices[0], False)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
ret = kz.titleBar("
|
|
20
|
-
kz.caseHeader("Optimization Parameters")
|
|
19
|
+
ret = kz.titleBar("Optimization Parameters")
|
|
21
20
|
|
|
22
21
|
if ret is None or kz.caseHasNoPlan():
|
|
23
22
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -3,8 +3,7 @@ import streamlit as st
|
|
|
3
3
|
import sskeys as kz
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
|
-
ret = kz.titleBar("
|
|
7
|
-
kz.caseHeader("Output Files")
|
|
6
|
+
ret = kz.titleBar("Output Files")
|
|
8
7
|
|
|
9
8
|
if ret is None or kz.caseHasNoPlan():
|
|
10
9
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -16,12 +15,14 @@ else:
|
|
|
16
15
|
st.info("Case status is currently '%s'." % kz.getKey("caseStatus"))
|
|
17
16
|
else:
|
|
18
17
|
caseName = kz.getKey("name")
|
|
19
|
-
|
|
20
|
-
if
|
|
18
|
+
df = kz.compareSummaries()
|
|
19
|
+
if df is not None:
|
|
21
20
|
st.write("#### Synopsis")
|
|
22
|
-
st.
|
|
21
|
+
st.dataframe(df[1:], use_container_width=True)
|
|
22
|
+
st.caption("Values are in today's \\$ unless marked otherwise.")
|
|
23
23
|
st.download_button(
|
|
24
|
-
"Download synopsis", data=
|
|
24
|
+
"Download synopsis", data=df[1:].to_string(), file_name=f"Synopsis_{caseName}.txt",
|
|
25
|
+
mime="text/plain;charset=UTF-8"
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
st.divider()
|
|
@@ -54,5 +55,6 @@ else:
|
|
|
54
55
|
st.code(lines, language="toml")
|
|
55
56
|
|
|
56
57
|
st.download_button(
|
|
57
|
-
"Download case parameter file", data=lines,
|
|
58
|
+
"Download case parameter file", data=lines,
|
|
59
|
+
file_name=f"case_{caseName}.toml", mime="application/toml"
|
|
58
60
|
)
|
|
@@ -36,7 +36,8 @@ of Jack and Jill provided here as an example:
|
|
|
36
36
|
- Wages and contributions file named
|
|
37
37
|
[jack+jill.xlsx](https://raw.github.com/mdlacasse/Owl/main/examples/jack+jill.xlsx)
|
|
38
38
|
in Excel format.
|
|
39
|
-
1) Navigate to the **Create Case** page and
|
|
39
|
+
1) Navigate to the **Create Case** page and select the case of *jack+jill* among the GitHub examples.
|
|
40
|
+
Alternatively, you can drag and drop the case parameter file
|
|
40
41
|
you just downloaded (*case_jack+jill.toml*).
|
|
41
42
|
1) Navigate to the **Wages and Contributions** page and
|
|
42
43
|
drag and drop the wages and contributions file you downloaded (*jack+jill.xlsx*).
|
|
@@ -48,14 +49,13 @@ experiment with different parameters.
|
|
|
48
49
|
For creating your own cases, you can start
|
|
49
50
|
from scratch by selecting `New Case...` in the selection box while on the **Create Case** page,
|
|
50
51
|
and fill in the information needed on each page in the `Case Setup` section.
|
|
51
|
-
Once a case has been fully parameterized and successfully optimized,
|
|
52
|
-
its parameters can be saved by using the `Download case file...` button on the `Output Files` page.
|
|
53
|
-
|
|
54
52
|
Alternatively, you can duplicate any existing case by using
|
|
55
53
|
the `Duplicate case` button, and then edit its values to fit your situation.
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
Once a case has been fully parameterized and successfully optimized,
|
|
56
|
+
its parameters can be saved by using the `Download case file...` button on the `Output Files` page.
|
|
57
|
+
Multiple cases can coexist and can be called and compared using the `Case selector` box
|
|
58
|
+
at the top of the page.
|
|
59
59
|
|
|
60
60
|
More information can be found on the :material/help: **[Documentation](Documentation)**
|
|
61
61
|
page located in the **Resources** section.
|
|
@@ -47,8 +47,7 @@ kz.initKey("rateType", rateChoices[0])
|
|
|
47
47
|
kz.initKey("fixedType", fixedChoices[0])
|
|
48
48
|
kz.initKey("varyingType", varyingChoices[0])
|
|
49
49
|
|
|
50
|
-
ret = kz.titleBar("
|
|
51
|
-
kz.caseHeader("Rates Selection")
|
|
50
|
+
ret = kz.titleBar("Rates Selection")
|
|
52
51
|
|
|
53
52
|
if ret is None or kz.caseHasNoPlan():
|
|
54
53
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -4,15 +4,15 @@ import sskeys as kz
|
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
# Refresh Wages and Contributions tables as time window likely to change.
|
|
7
8
|
def resetTimeLists():
|
|
8
|
-
if not kz.getKey("duplicate"):
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# if not kz.getKey("duplicate"):
|
|
10
|
+
tlists = owb.resetContributions()
|
|
11
|
+
for i, iname in enumerate(tlists):
|
|
12
|
+
kz.setKey("timeList" + str(i), tlists[iname])
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
ret = kz.titleBar("
|
|
15
|
-
kz.caseHeader("Wages and Contributions")
|
|
15
|
+
ret = kz.titleBar("Wages and Contributions")
|
|
16
16
|
|
|
17
17
|
if ret is None or kz.caseHasNoPlan():
|
|
18
18
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -38,11 +38,12 @@ else:
|
|
|
38
38
|
" that has not yet been uploaded."
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
st.write("#### Upload a *Wages and Contributions* file")
|
|
41
42
|
kz.initKey("_xlsx", 0)
|
|
42
43
|
stTimeLists = st.file_uploader(
|
|
43
44
|
"Upload values from a wages and contributions file...",
|
|
44
45
|
key="_stTimeLists" + str(kz.getKey("_xlsx")),
|
|
45
|
-
type=["xlsx"],
|
|
46
|
+
type=["xlsx", "ods"],
|
|
46
47
|
)
|
|
47
48
|
if stTimeLists is not None:
|
|
48
49
|
if owb.readContributions(stTimeLists):
|
|
@@ -51,6 +52,7 @@ else:
|
|
|
51
52
|
kz.storeKey("_xlsx", kz.getKey("_xlsx") + 1)
|
|
52
53
|
st.rerun()
|
|
53
54
|
|
|
55
|
+
st.divider()
|
|
54
56
|
for i in range(n):
|
|
55
57
|
st.write("##### " + kz.getKey("iname" + str(i)) + "'s timetable")
|
|
56
58
|
colfor = {"year": st.column_config.NumberColumn(None, format="%d")}
|
|
@@ -3,8 +3,7 @@ import streamlit as st
|
|
|
3
3
|
import sskeys as kz
|
|
4
4
|
import owlbridge as owb
|
|
5
5
|
|
|
6
|
-
ret = kz.titleBar("
|
|
7
|
-
kz.caseHeader("Worksheets")
|
|
6
|
+
ret = kz.titleBar("Worksheets")
|
|
8
7
|
|
|
9
8
|
if ret is None or kz.caseHasNoPlan():
|
|
10
9
|
st.info("Case(s) must be first created before running this page.")
|
|
@@ -122,15 +122,15 @@ def runPlan(plan):
|
|
|
122
122
|
except Exception as e:
|
|
123
123
|
st.error(f"Solution failed: {e}")
|
|
124
124
|
kz.storeKey("caseStatus", "exception")
|
|
125
|
-
kz.storeKey("
|
|
125
|
+
kz.storeKey("summaryDf", None)
|
|
126
126
|
return
|
|
127
127
|
|
|
128
128
|
kz.storeKey("caseStatus", plan.caseStatus)
|
|
129
129
|
if plan.caseStatus == "solved":
|
|
130
|
-
kz.storeKey("
|
|
130
|
+
kz.storeKey("summaryDf", plan.summaryDf())
|
|
131
131
|
kz.storeKey("casetoml", getCaseString().getvalue())
|
|
132
132
|
else:
|
|
133
|
-
kz.storeKey("
|
|
133
|
+
kz.storeKey("summaryDf", None)
|
|
134
134
|
kz.storeKey("casetoml", "")
|
|
135
135
|
|
|
136
136
|
|
|
@@ -381,7 +381,7 @@ def _setAllocationRatios(plan):
|
|
|
381
381
|
def plotSingleResults(plan):
|
|
382
382
|
c = 0
|
|
383
383
|
n = 3
|
|
384
|
-
cols = st.columns(n, gap="
|
|
384
|
+
cols = st.columns(n, gap="small")
|
|
385
385
|
fig = plan.showRates(figure=True)
|
|
386
386
|
if fig:
|
|
387
387
|
cols[c].write("##### Annual Rates")
|
|
@@ -400,7 +400,7 @@ def plotSingleResults(plan):
|
|
|
400
400
|
cols[c].pyplot(fig)
|
|
401
401
|
c = (c + 1) % n
|
|
402
402
|
|
|
403
|
-
cols = st.columns(n, gap="
|
|
403
|
+
cols = st.columns(n, gap="small")
|
|
404
404
|
fig = plan.showSources(figure=True)
|
|
405
405
|
if fig:
|
|
406
406
|
cols[c].write("##### Raw Income Sources")
|
|
@@ -544,17 +544,16 @@ def saveCaseFile(plan):
|
|
|
544
544
|
return BytesIO(encoded_data)
|
|
545
545
|
|
|
546
546
|
|
|
547
|
-
def createCaseFromFile(
|
|
548
|
-
|
|
547
|
+
def createCaseFromFile(strio):
|
|
548
|
+
logstrio = StringIO()
|
|
549
549
|
try:
|
|
550
|
-
|
|
551
|
-
plan = owl.readConfig(mystringio, logstreams=[strio], readContributions=False)
|
|
550
|
+
plan = owl.readConfig(strio, logstreams=[logstrio], readContributions=False)
|
|
552
551
|
except Exception as e:
|
|
553
552
|
st.error(f"Failed to parse case file: {e}")
|
|
554
553
|
return "", {}
|
|
555
554
|
|
|
556
555
|
name, mydic = genDic(plan)
|
|
557
|
-
mydic["logs"] =
|
|
556
|
+
mydic["logs"] = logstrio
|
|
558
557
|
|
|
559
558
|
return name, mydic
|
|
560
559
|
|
|
@@ -565,7 +564,7 @@ def genDic(plan):
|
|
|
565
564
|
dic["plan"] = plan
|
|
566
565
|
dic["name"] = plan._name
|
|
567
566
|
dic["description"] = plan._description
|
|
568
|
-
dic["
|
|
567
|
+
dic["summaryDf"] = None
|
|
569
568
|
dic["casetoml"] = ""
|
|
570
569
|
dic["caseStatus"] = "new"
|
|
571
570
|
dic["status"] = ["unknown", "single", "married"][plan.N_i]
|
|
@@ -3,6 +3,7 @@ Module for storing keys in Streamlit session state.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import streamlit as st
|
|
6
|
+
import pandas as pd
|
|
6
7
|
import copy
|
|
7
8
|
import re
|
|
8
9
|
|
|
@@ -22,8 +23,8 @@ def init():
|
|
|
22
23
|
ss = st.session_state
|
|
23
24
|
if "cases" not in ss:
|
|
24
25
|
ss.cases = {
|
|
25
|
-
newCase: {"iname0": "", "status": "unkown", "caseStatus": "new"
|
|
26
|
-
loadCaseFile: {"iname0": "", "status": "unkown", "caseStatus": "new"
|
|
26
|
+
newCase: {"iname0": "", "status": "unkown", "caseStatus": "new"},
|
|
27
|
+
loadCaseFile: {"iname0": "", "status": "unkown", "caseStatus": "new"},
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
# Variable for storing name of current case.
|
|
@@ -151,21 +152,6 @@ def caseIsNotMCReady():
|
|
|
151
152
|
return caseIsNotRunReady() or getKey("rateType") != "varying" or "tochastic" not in getKey("varyingType")
|
|
152
153
|
|
|
153
154
|
|
|
154
|
-
def titleBar(nkey, choices=None):
|
|
155
|
-
if choices is None:
|
|
156
|
-
choices = onlyCaseNames()
|
|
157
|
-
helpmsg = "Select an existing case, or create a new one from scratch or from a *case* parameter file."
|
|
158
|
-
return st.sidebar.selectbox(
|
|
159
|
-
"Select case",
|
|
160
|
-
choices,
|
|
161
|
-
help=helpmsg,
|
|
162
|
-
index=getIndex(currentCaseName(), choices),
|
|
163
|
-
key="_" + nkey,
|
|
164
|
-
on_change=switchToCase,
|
|
165
|
-
args=[nkey],
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
155
|
def currentCaseDic() -> dict:
|
|
170
156
|
return ss.cases[ss.currentCase]
|
|
171
157
|
|
|
@@ -192,16 +178,17 @@ def duplicateCase():
|
|
|
192
178
|
ss.cases[dupname] = copy.deepcopy(ss.cases[ss.currentCase])
|
|
193
179
|
ss.cases[ss.currentCase]["plan"] = currentPlan
|
|
194
180
|
ss.cases[dupname]["name"] = dupname
|
|
195
|
-
ss.cases[dupname]["
|
|
181
|
+
ss.cases[dupname]["summaryDf"] = None
|
|
196
182
|
ss.cases[dupname]["duplicate"] = True
|
|
197
183
|
refreshCase(ss.cases[dupname])
|
|
198
184
|
ss.currentCase = dupname
|
|
185
|
+
st.toast("Case duplicated except for Wages and Contributions tables.")
|
|
199
186
|
|
|
200
187
|
|
|
201
|
-
def createCaseFromFile(
|
|
188
|
+
def createCaseFromFile(strio):
|
|
202
189
|
import owlbridge as owb
|
|
203
190
|
|
|
204
|
-
name, dic = owb.createCaseFromFile(
|
|
191
|
+
name, dic = owb.createCaseFromFile(strio)
|
|
205
192
|
if name == "":
|
|
206
193
|
return False
|
|
207
194
|
elif name in ss.cases:
|
|
@@ -228,7 +215,7 @@ def createNewCase(case):
|
|
|
228
215
|
st.error(f"Case name '{casename}' already exists.")
|
|
229
216
|
return
|
|
230
217
|
|
|
231
|
-
ss.cases[casename] = {"name": casename, "caseStatus": "unknown", "
|
|
218
|
+
ss.cases[casename] = {"name": casename, "caseStatus": "unknown", "logs": None}
|
|
232
219
|
setCurrentCase(ss._newcase)
|
|
233
220
|
|
|
234
221
|
|
|
@@ -237,10 +224,11 @@ def renameCase(key):
|
|
|
237
224
|
return
|
|
238
225
|
newname = ss["_" + key]
|
|
239
226
|
plan = getKey("plan")
|
|
240
|
-
if plan:
|
|
241
|
-
plan.rename(newname)
|
|
242
227
|
ss.cases[newname] = ss.cases.pop(ss.currentCase)
|
|
243
228
|
ss.cases[newname]["name"] = newname
|
|
229
|
+
if plan:
|
|
230
|
+
plan.rename(newname)
|
|
231
|
+
ss.cases[newname]["caseStatus"] = "modified"
|
|
244
232
|
setCurrentCase(newname)
|
|
245
233
|
|
|
246
234
|
|
|
@@ -272,7 +260,7 @@ def storepull(key):
|
|
|
272
260
|
def setKey(key, val):
|
|
273
261
|
ss.cases[ss.currentCase][key] = val
|
|
274
262
|
ss.cases[ss.currentCase]["caseStatus"] = "modified"
|
|
275
|
-
|
|
263
|
+
ss.cases[ss.currentCase]["summaryDf"] = None
|
|
276
264
|
return val
|
|
277
265
|
|
|
278
266
|
|
|
@@ -325,6 +313,34 @@ def getAccountBalances(ni):
|
|
|
325
313
|
return bal
|
|
326
314
|
|
|
327
315
|
|
|
316
|
+
def compareSummaries():
|
|
317
|
+
df = getKey("summaryDf")
|
|
318
|
+
if df is None:
|
|
319
|
+
return None
|
|
320
|
+
for case in onlyCaseNames():
|
|
321
|
+
if case == currentCaseName():
|
|
322
|
+
continue
|
|
323
|
+
odf = ss.cases[case]["summaryDf"]
|
|
324
|
+
if odf is None or set(odf.columns) != set(df.columns):
|
|
325
|
+
continue
|
|
326
|
+
df = pd.concat([df, odf])
|
|
327
|
+
|
|
328
|
+
if df.shape[0] > 1:
|
|
329
|
+
# Unroll to subtract $tring representation of numbers.
|
|
330
|
+
for col in range(1, df.shape[1] - 5):
|
|
331
|
+
strval = df.iloc[0, col]
|
|
332
|
+
if isinstance(strval, str) and strval[0] == "$":
|
|
333
|
+
f0val = float(strval[1:].replace(",", ""))
|
|
334
|
+
for row in range(1, df.shape[0]):
|
|
335
|
+
fnval = float(df.iloc[row, col][1:].replace(",", ""))
|
|
336
|
+
diff = fnval - f0val
|
|
337
|
+
sign = "\u2191" if diff >= 0 else "\u2193"
|
|
338
|
+
sign = "\u2192" if diff == 0 else sign
|
|
339
|
+
df.iloc[row, col] = f"{sign} ${abs(diff):,.0f}"
|
|
340
|
+
|
|
341
|
+
return df.transpose()
|
|
342
|
+
|
|
343
|
+
|
|
328
344
|
def getSolveParameters():
|
|
329
345
|
maximize = getKey("objective")
|
|
330
346
|
if maximize is None:
|
|
@@ -488,8 +504,28 @@ def orangeDivider():
|
|
|
488
504
|
st.html("<style> hr {border-color: orange;}</style><hr>")
|
|
489
505
|
|
|
490
506
|
|
|
491
|
-
def
|
|
492
|
-
# st.html("<div style=
|
|
493
|
-
|
|
494
|
-
|
|
507
|
+
def titleBar(txt, choices=None):
|
|
508
|
+
# st.html(f"<div style='text-align: left;color: orange;font-style: italic;'>{currentCaseName()}</div>")
|
|
509
|
+
if choices is None:
|
|
510
|
+
choices = onlyCaseNames()
|
|
511
|
+
helpmsg = "Select an existing case."
|
|
512
|
+
else:
|
|
513
|
+
helpmsg = "Select an existing case, or create a new one from scratch or from a *case* parameter file."
|
|
514
|
+
|
|
515
|
+
col1, col2 = st.columns(2, gap="large")
|
|
516
|
+
with col1:
|
|
517
|
+
st.write("## " + txt)
|
|
518
|
+
with col2:
|
|
519
|
+
nkey = txt
|
|
520
|
+
ret = st.selectbox(
|
|
521
|
+
"Case selector",
|
|
522
|
+
choices,
|
|
523
|
+
help=helpmsg,
|
|
524
|
+
index=getIndex(currentCaseName(), choices),
|
|
525
|
+
key="_" + nkey,
|
|
526
|
+
on_change=switchToCase,
|
|
527
|
+
args=[nkey],
|
|
528
|
+
)
|
|
529
|
+
|
|
495
530
|
orangeDivider()
|
|
531
|
+
return ret
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import streamlit as st
|
|
3
|
+
from io import StringIO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
cases = ["jack+jill", "joe", "john+sally", "jon+jane", "kim+sam-bequest", "kim+sam-spending"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def loadExample(case):
|
|
10
|
+
url = f"https://raw.github.com/mdlacasse/owl/main/examples/case_{case}.toml"
|
|
11
|
+
response = requests.get(url)
|
|
12
|
+
if response.status_code == 200:
|
|
13
|
+
return StringIO(response.text)
|
|
14
|
+
else:
|
|
15
|
+
st.error(f"Failed to load case parameter file from GitHub: {response.status_code}.")
|
|
16
|
+
return None
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2025.02.25"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|