owlplanner 2025.8.1__tar.gz → 2025.11.2__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.8.1 → owlplanner-2025.11.2}/INSTALL.md +1 -1
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/PKG-INFO +20 -14
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/README.md +19 -13
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/USER_GUIDE.md +14 -14
- owlplanner-2025.8.1/docker/Dockerfile.build → owlplanner-2025.11.2/docker/Dockerfile.bare +2 -8
- owlplanner-2025.8.1/docker/Dockerfile.run → owlplanner-2025.11.2/docker/Dockerfile.static +2 -8
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docker/README.md +23 -11
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/examples/case_drawdowncalc-comparison-1.toml +2 -1
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/pyproject.toml +1 -1
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/requirements.txt +2 -2
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/config.py +4 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/plan.py +11 -12
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/tax2025.py +2 -2
- owlplanner-2025.8.1/mediopt/tax2025.py → owlplanner-2025.11.2/src/owlplanner/tax2026.py +106 -61
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/timelists.py +1 -1
- owlplanner-2025.11.2/src/owlplanner/version.py +1 -0
- owlplanner-2025.11.2/ui/.owlbridge.py.swo +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/About_Owl.py +5 -2
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Asset_Allocation.py +19 -19
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Create_Case.py +19 -18
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Current_Assets.py +13 -13
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Documentation.py +14 -18
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Fixed_Income.py +13 -12
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Graphs.py +2 -2
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Historical_Range.py +12 -12
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Logs.py +1 -1
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Monte_Carlo.py +4 -4
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Optimization_Parameters.py +42 -37
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Output_Files.py +4 -4
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Quick_Start.py +12 -6
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Rates_Selection.py +45 -45
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Settings.py +5 -11
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Wages_and_Contributions.py +17 -17
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/Worksheets.py +1 -1
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/main.py +1 -1
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/owlbridge.py +92 -92
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/sskeys.py +105 -75
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/tomlexamples.py +2 -2
- owlplanner-2025.8.1/examples/joe.xlsx +0 -0
- owlplanner-2025.8.1/examples/john+sally.xlsx +0 -0
- owlplanner-2025.8.1/examples/jon+jane.xlsx +0 -0
- owlplanner-2025.8.1/examples/kim+sam.xlsx +0 -0
- owlplanner-2025.8.1/examples/template.xlsx +0 -0
- owlplanner-2025.8.1/examples.new/case_drawdowncalc-comparison-1.toml +0 -56
- owlplanner-2025.8.1/examples.new/case_jack+jill.toml +0 -57
- owlplanner-2025.8.1/examples.new/case_joe.toml +0 -54
- owlplanner-2025.8.1/examples.new/case_john+sally.toml +0 -53
- owlplanner-2025.8.1/examples.new/case_jon+jane.toml +0 -56
- owlplanner-2025.8.1/examples.new/case_kim+sam-bequest.toml +0 -56
- owlplanner-2025.8.1/examples.new/case_kim+sam-spending.toml +0 -56
- owlplanner-2025.8.1/examples.new/jack+jill.xlsx +0 -0
- owlplanner-2025.8.1/mediopt/owl.pdf +0 -0
- owlplanner-2025.8.1/mediopt/owl.tex +0 -1524
- owlplanner-2025.8.1/mediopt/owl.tex.merged +0 -1524
- owlplanner-2025.8.1/mediopt/piecewiseConstant.png +0 -0
- owlplanner-2025.8.1/mediopt/plan.py +0 -3077
- owlplanner-2025.8.1/src/owlplanner/version.py +0 -1
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/.devcontainer/devcontainer.json +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/.flake8 +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/.gitattributes +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/.github/workflows/github-actions-runtests.yml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/.gitignore +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/.streamlit/config.toml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/.streamlit/fullconfig.toml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/LICENSE +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/RELEASE_NOTES.md +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docker/buildentrypoint.sh +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docker/docker-compose.yml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docker/runentrypoint.sh +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/AD-taxDef.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/AD-taxFree.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/AD-taxable.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/Hist_Bequest.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/Hist_Spending.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/MC-tutorial2a.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/MC-tutorial2b.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/OwlUI.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/allocations.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/owl.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/piecewiseConstant.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/profile.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/ratesCorrelations.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/ratesPlot.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/savingsPlot.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/sourcesPlot.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/spendingPlot.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/taxIncomePlot.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/images/taxesPlot.png +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/owl.pdf +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/docs/owl.tex +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/examples/case_jack+jill.toml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/examples/case_joe.toml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/examples/case_john+sally.toml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/examples/case_jon+jane.toml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/examples/case_kim+sam-bequest.toml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/examples/case_kim+sam-spending.toml +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/examples/jack+jill.xlsx +0 -0
- {owlplanner-2025.8.1/examples.new → owlplanner-2025.11.2/examples}/joe.xlsx +0 -0
- {owlplanner-2025.8.1/examples.new → owlplanner-2025.11.2/examples}/john+sally.xlsx +0 -0
- {owlplanner-2025.8.1/examples.new → owlplanner-2025.11.2/examples}/jon+jane.xlsx +0 -0
- {owlplanner-2025.8.1/examples.new → owlplanner-2025.11.2/examples}/kim+sam.xlsx +0 -0
- {owlplanner-2025.8.1/examples.new → owlplanner-2025.11.2/examples}/template.xlsx +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/notebooks/john+sally.ipynb +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/notebooks/kim+sam.ipynb +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/notebooks/template.ipynb +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/notebooks/tutorial_1.ipynb +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/notebooks/tutorial_2.ipynb +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/notebooks/tutorial_3.ipynb +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/owlplanner.cmd +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/owlplanner.sh +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/pytest.ini +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/__init__.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/abcapi.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/data/__init__.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/data/rates.csv +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/mylogging.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/plotting/__init__.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/plotting/base.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/plotting/factory.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/plotting/matplotlib_backend.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/plotting/plotly_backend.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/progress.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/rates.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/src/owlplanner/utils.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/tests/test_logger.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/tests/test_regressions.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/tests/test_repro.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/tests/test_toml_cases.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/tests/test_ui_asset_allocation.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/tests/test_ui_compare_summaries.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/tests/test_ui_sskeys.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/tests/test_units.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/README.md +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/__init__.py +0 -0
- {owlplanner-2025.8.1 → owlplanner-2025.11.2}/ui/progress.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## A retirement exploration tool based on linear programming
|
|
4
4
|
|
|
5
|
-
<img align=right src="https://
|
|
5
|
+
<img align=right src="https://github.com/mdlacasse/Owl/blob/main/docs/images/owl.png?raw=true" width="250">
|
|
6
6
|
|
|
7
7
|
------------------------------------------------------------------------------------
|
|
8
8
|
### About
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.11.2
|
|
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
|
|
@@ -709,7 +709,7 @@ Description-Content-Type: text/markdown
|
|
|
709
709
|
|
|
710
710
|
## A retirement exploration tool based on linear programming
|
|
711
711
|
|
|
712
|
-
<img align=right src="https://
|
|
712
|
+
<img align=right src="https://github.com/mdlacasse/Owl/blob/main/docs/images/owl.png?raw=true" width="250">
|
|
713
713
|
|
|
714
714
|
-------------------------------------------------------------------------------------
|
|
715
715
|
|
|
@@ -739,12 +739,12 @@ Strictly speaking, it is not a planning tool, but more an environment for explor
|
|
|
739
739
|
It provides different realizations of a financial strategy through the rigorous
|
|
740
740
|
mathematical optimization of relevant decision variables. Two major objective goals can be set: either
|
|
741
741
|
maximize net spending, or after-tax bequest under various constraints.
|
|
742
|
-
Look at
|
|
742
|
+
Look at the *Capabilities* section below for more detail.
|
|
743
743
|
|
|
744
744
|
One can certainly have a savings plan, but due to the volatility of financial investments,
|
|
745
745
|
it is impossible to have a certain asset earnings plan. This does not mean one cannot make decisions.
|
|
746
746
|
These decisions need to be guided with an understanding of the sensitivity of the parameters.
|
|
747
|
-
This is exactly where this tool fits
|
|
747
|
+
This is exactly where this tool fits in. Given your savings capabilities and spending desires,
|
|
748
748
|
it can generate different future realizations of
|
|
749
749
|
your strategy under different market assumptions, helping to better understand your financial situation.
|
|
750
750
|
|
|
@@ -758,7 +758,7 @@ collecting your data, or academic papers that share the results without really s
|
|
|
758
758
|
the underlying mathematical models.
|
|
759
759
|
The algorithms in Owl rely on the open-source HiGHS linear programming solver. The complete formulation and
|
|
760
760
|
detailed description of the underlying
|
|
761
|
-
mathematical model can be found [here](https://
|
|
761
|
+
mathematical model can be found [here](https://github.com/mdlacasse/Owl/blob/main/docs/owl.pdf).
|
|
762
762
|
|
|
763
763
|
It is anticipated that most end users will use Owl through the graphical interface
|
|
764
764
|
either at [owlplanner.streamlit.app](https://owlplanner.streamlit.app)
|
|
@@ -768,7 +768,7 @@ as described [here](USER_GUIDE.md).
|
|
|
768
768
|
|
|
769
769
|
Not every retirement decision strategy can be framed as an easy-to-solve optimization problem.
|
|
770
770
|
In particular, if one is interested in comparing different withdrawal strategies,
|
|
771
|
-
[FI Calc](ficalc.app) is an elegant application that addresses this need.
|
|
771
|
+
[FI Calc](https://ficalc.app) is an elegant application that addresses this need.
|
|
772
772
|
If, however, you also want to optimize spending, bequest, and Roth conversions, with
|
|
773
773
|
an approach also considering Medicare and federal income tax over the next few years,
|
|
774
774
|
then Owl is definitely a tool that can help guide your decisions.
|
|
@@ -840,7 +840,9 @@ Tax status covers married filing jointly and single, depending on the number of
|
|
|
840
840
|
Maturation rules for Roth contributions and conversions are implemented as constraints
|
|
841
841
|
limiting withdrawal amounts to cover Roth account balances for 5 years after the events.
|
|
842
842
|
Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
|
|
843
|
-
|
|
843
|
+
They can also be optimized explicitly as an option, but this choice can lead to longer calculations
|
|
844
|
+
due to the use of the many additional binary variables required by the formulation.
|
|
845
|
+
Future Medicare and IRMAA values are simple projections of current values with the assumed inflation rates.
|
|
844
846
|
|
|
845
847
|
### Limitations
|
|
846
848
|
Owl is work in progress. At the current time:
|
|
@@ -851,15 +853,16 @@ These cases are detected and will generate an error message.
|
|
|
851
853
|
- Social security rule for surviving spouse assumes that benefits were taken at full retirement age.
|
|
852
854
|
- Current version has no optimization of asset allocations between individuals and/or types of savings accounts.
|
|
853
855
|
If there is interest, that could be added in the future.
|
|
854
|
-
- In the current implementation, social securiy is always taxed at 85
|
|
855
|
-
- Medicare calculations are done through a self-consistent loop
|
|
856
|
-
|
|
856
|
+
- In the current implementation, social securiy is always taxed at 85%, assuming that your taxable income will be larger than 34 k$ (single) or 44 k$ (married filing jointly).
|
|
857
|
+
- When Medicare calculations are done through a self-consistent loop,
|
|
858
|
+
the Medicare premiums are calculated after an initial solution is generated,
|
|
857
859
|
and then a new solution is re-generated with these premiums as a constraint.
|
|
858
860
|
In some situations, when the income (MAGI) is near an IRMAA bracket, oscillatory solutions can arise.
|
|
859
861
|
While the solutions generated are very close to one another, Owl will pick the smallest solution
|
|
860
|
-
for being conservative.
|
|
861
|
-
|
|
862
|
-
|
|
862
|
+
for being conservative. While sometimes computationally costly,
|
|
863
|
+
a comparison with a full Medicare optimization should always be performed.
|
|
864
|
+
- Part D is not included in the IRMAA calculations. Only Part B is taken into account,
|
|
865
|
+
which is considerably more significant.
|
|
863
866
|
- Future tax brackets are pure speculations derived from the little we know now and projected to the next 30 years.
|
|
864
867
|
Your guesses are as good as mine.
|
|
865
868
|
|
|
@@ -886,8 +889,11 @@ assets to support, even with no estate being left.
|
|
|
886
889
|
- Optimization solver from [HiGHS](https://highs.dev)
|
|
887
890
|
- Streamlit Community Cloud [Streamlit](https://streamlit.io)
|
|
888
891
|
- Contributors: Josh (noimjosh@gmail.com) for Docker image code,
|
|
892
|
+
kg333 for fixing an error in Docker's instructions,
|
|
889
893
|
Dale Seng (sengsational) for great insights and suggestions,
|
|
890
|
-
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions,
|
|
894
|
+
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions,
|
|
895
|
+
Clark Jefcoat (hubcity) for fruitful interactions,
|
|
896
|
+
Benjamin Quinn (blquinn) and Gene Wood (gene1wood) for improvements and bug fixes.
|
|
891
897
|
|
|
892
898
|
---------------------------------------------------------------------
|
|
893
899
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
## A retirement exploration tool based on linear programming
|
|
5
5
|
|
|
6
|
-
<img align=right src="https://
|
|
6
|
+
<img align=right src="https://github.com/mdlacasse/Owl/blob/main/docs/images/owl.png?raw=true" width="250">
|
|
7
7
|
|
|
8
8
|
-------------------------------------------------------------------------------------
|
|
9
9
|
|
|
@@ -33,12 +33,12 @@ Strictly speaking, it is not a planning tool, but more an environment for explor
|
|
|
33
33
|
It provides different realizations of a financial strategy through the rigorous
|
|
34
34
|
mathematical optimization of relevant decision variables. Two major objective goals can be set: either
|
|
35
35
|
maximize net spending, or after-tax bequest under various constraints.
|
|
36
|
-
Look at
|
|
36
|
+
Look at the *Capabilities* section below for more detail.
|
|
37
37
|
|
|
38
38
|
One can certainly have a savings plan, but due to the volatility of financial investments,
|
|
39
39
|
it is impossible to have a certain asset earnings plan. This does not mean one cannot make decisions.
|
|
40
40
|
These decisions need to be guided with an understanding of the sensitivity of the parameters.
|
|
41
|
-
This is exactly where this tool fits
|
|
41
|
+
This is exactly where this tool fits in. Given your savings capabilities and spending desires,
|
|
42
42
|
it can generate different future realizations of
|
|
43
43
|
your strategy under different market assumptions, helping to better understand your financial situation.
|
|
44
44
|
|
|
@@ -52,7 +52,7 @@ collecting your data, or academic papers that share the results without really s
|
|
|
52
52
|
the underlying mathematical models.
|
|
53
53
|
The algorithms in Owl rely on the open-source HiGHS linear programming solver. The complete formulation and
|
|
54
54
|
detailed description of the underlying
|
|
55
|
-
mathematical model can be found [here](https://
|
|
55
|
+
mathematical model can be found [here](https://github.com/mdlacasse/Owl/blob/main/docs/owl.pdf).
|
|
56
56
|
|
|
57
57
|
It is anticipated that most end users will use Owl through the graphical interface
|
|
58
58
|
either at [owlplanner.streamlit.app](https://owlplanner.streamlit.app)
|
|
@@ -62,7 +62,7 @@ as described [here](USER_GUIDE.md).
|
|
|
62
62
|
|
|
63
63
|
Not every retirement decision strategy can be framed as an easy-to-solve optimization problem.
|
|
64
64
|
In particular, if one is interested in comparing different withdrawal strategies,
|
|
65
|
-
[FI Calc](ficalc.app) is an elegant application that addresses this need.
|
|
65
|
+
[FI Calc](https://ficalc.app) is an elegant application that addresses this need.
|
|
66
66
|
If, however, you also want to optimize spending, bequest, and Roth conversions, with
|
|
67
67
|
an approach also considering Medicare and federal income tax over the next few years,
|
|
68
68
|
then Owl is definitely a tool that can help guide your decisions.
|
|
@@ -134,7 +134,9 @@ Tax status covers married filing jointly and single, depending on the number of
|
|
|
134
134
|
Maturation rules for Roth contributions and conversions are implemented as constraints
|
|
135
135
|
limiting withdrawal amounts to cover Roth account balances for 5 years after the events.
|
|
136
136
|
Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
|
|
137
|
-
|
|
137
|
+
They can also be optimized explicitly as an option, but this choice can lead to longer calculations
|
|
138
|
+
due to the use of the many additional binary variables required by the formulation.
|
|
139
|
+
Future Medicare and IRMAA values are simple projections of current values with the assumed inflation rates.
|
|
138
140
|
|
|
139
141
|
### Limitations
|
|
140
142
|
Owl is work in progress. At the current time:
|
|
@@ -145,15 +147,16 @@ These cases are detected and will generate an error message.
|
|
|
145
147
|
- Social security rule for surviving spouse assumes that benefits were taken at full retirement age.
|
|
146
148
|
- Current version has no optimization of asset allocations between individuals and/or types of savings accounts.
|
|
147
149
|
If there is interest, that could be added in the future.
|
|
148
|
-
- In the current implementation, social securiy is always taxed at 85
|
|
149
|
-
- Medicare calculations are done through a self-consistent loop
|
|
150
|
-
|
|
150
|
+
- In the current implementation, social securiy is always taxed at 85%, assuming that your taxable income will be larger than 34 k$ (single) or 44 k$ (married filing jointly).
|
|
151
|
+
- When Medicare calculations are done through a self-consistent loop,
|
|
152
|
+
the Medicare premiums are calculated after an initial solution is generated,
|
|
151
153
|
and then a new solution is re-generated with these premiums as a constraint.
|
|
152
154
|
In some situations, when the income (MAGI) is near an IRMAA bracket, oscillatory solutions can arise.
|
|
153
155
|
While the solutions generated are very close to one another, Owl will pick the smallest solution
|
|
154
|
-
for being conservative.
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
for being conservative. While sometimes computationally costly,
|
|
157
|
+
a comparison with a full Medicare optimization should always be performed.
|
|
158
|
+
- Part D is not included in the IRMAA calculations. Only Part B is taken into account,
|
|
159
|
+
which is considerably more significant.
|
|
157
160
|
- Future tax brackets are pure speculations derived from the little we know now and projected to the next 30 years.
|
|
158
161
|
Your guesses are as good as mine.
|
|
159
162
|
|
|
@@ -180,8 +183,11 @@ assets to support, even with no estate being left.
|
|
|
180
183
|
- Optimization solver from [HiGHS](https://highs.dev)
|
|
181
184
|
- Streamlit Community Cloud [Streamlit](https://streamlit.io)
|
|
182
185
|
- Contributors: Josh (noimjosh@gmail.com) for Docker image code,
|
|
186
|
+
kg333 for fixing an error in Docker's instructions,
|
|
183
187
|
Dale Seng (sengsational) for great insights and suggestions,
|
|
184
|
-
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions,
|
|
188
|
+
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions,
|
|
189
|
+
Clark Jefcoat (hubcity) for fruitful interactions,
|
|
190
|
+
Benjamin Quinn (blquinn) and Gene Wood (gene1wood) for improvements and bug fixes.
|
|
185
191
|
|
|
186
192
|
---------------------------------------------------------------------
|
|
187
193
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## A retirement exploration tool based on linear programming
|
|
4
4
|
|
|
5
|
-
<img align=right src="https://
|
|
5
|
+
<img align=right src="https://github.com/mdlacasse/Owl/blob/main/docs/images/owl.png?raw=true" width="250">
|
|
6
6
|
|
|
7
7
|
------------------------------------------------------------------------------------
|
|
8
8
|
### About
|
|
@@ -64,7 +64,7 @@ the boundaries of tax brackets.
|
|
|
64
64
|
```python
|
|
65
65
|
plan.showGrossIncome(value='nominal')
|
|
66
66
|
```
|
|
67
|
-
<img src="https://
|
|
67
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/taxIncomePlot.png?raw=true" width="75%">
|
|
68
68
|
|
|
69
69
|
The optimal spending profile is shown in the next plot (in today's dollars). Notice the drop
|
|
70
70
|
(recall we selected 60% survivor needs) at the passing of the first spouse.
|
|
@@ -72,26 +72,26 @@ The optimal spending profile is shown in the next plot (in today's dollars). Not
|
|
|
72
72
|
plan.showProfile('today')
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
<img src="https://
|
|
75
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/spendingPlot.png?raw=true" width="75%">
|
|
76
76
|
|
|
77
77
|
The following plot shows the account balances in nominal value for all savings accounts owned by Jack and Jill.
|
|
78
78
|
It was generated using
|
|
79
79
|
```python
|
|
80
80
|
plan.showAccounts(value='nominal')
|
|
81
81
|
```
|
|
82
|
-
<img src="https://
|
|
82
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/savingsPlot.png?raw=true" width="75%">
|
|
83
83
|
|
|
84
84
|
while this plot shows the complex cash flow from all sources, which was generated with
|
|
85
85
|
```python
|
|
86
86
|
plan.showSources(value='nominal')
|
|
87
87
|
```
|
|
88
|
-
<img src="https://
|
|
88
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/sourcesPlot.png?raw=true" width="75%">
|
|
89
89
|
|
|
90
90
|
For taxes, the following call will display Medicare premiums (including Part B IRMAA fees) and federal income tax
|
|
91
91
|
```python
|
|
92
92
|
plan.showTaxes(value='nominal')
|
|
93
93
|
```
|
|
94
|
-
<img src="https://
|
|
94
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/taxesPlot.png?raw=true" width="75%">
|
|
95
95
|
|
|
96
96
|
For the case at hand, recall that asset allocations were selected above through
|
|
97
97
|
|
|
@@ -103,9 +103,9 @@ Assets distribution in all accounts in today's $ over time can be displayed from
|
|
|
103
103
|
```python
|
|
104
104
|
plan.showAssetDistribution(value='today')
|
|
105
105
|
```
|
|
106
|
-
<img src="https://
|
|
107
|
-
<img src="https://
|
|
108
|
-
<img src="https://
|
|
106
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/AD-taxable.png?raw=true" width="75%">
|
|
107
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/AD-taxDef.png?raw=true" width="75%">
|
|
108
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/AD-taxFree.png?raw=true" width="75%">
|
|
109
109
|
|
|
110
110
|
These plots are irregular because we used historical rates from 1969. The volatility of
|
|
111
111
|
the rates offers Roth conversion benefits which are exploited by the optimizer.
|
|
@@ -113,7 +113,7 @@ The rates used can be displayed by:
|
|
|
113
113
|
```python
|
|
114
114
|
plan.showRates()
|
|
115
115
|
```
|
|
116
|
-
<img src="https://
|
|
116
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/ratesPlot.png?raw=true" width="75%">
|
|
117
117
|
|
|
118
118
|
Values between brackets <> are the average values and volatility over the selected period.
|
|
119
119
|
|
|
@@ -121,7 +121,7 @@ For the statisticians, rates distributions and correlations between them can be
|
|
|
121
121
|
```python
|
|
122
122
|
plan.showRatesCorrelations()
|
|
123
123
|
```
|
|
124
|
-
<img src="https://
|
|
124
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/ratesCorrelations.png?raw=true" width="75%">
|
|
125
125
|
|
|
126
126
|
A short text summary of the outcome of the optimization can be displayed through using:
|
|
127
127
|
```python
|
|
@@ -169,13 +169,13 @@ by selecting *stochastic* rates and using
|
|
|
169
169
|
```
|
|
170
170
|
plan.runMC('maxSpending', ...)
|
|
171
171
|
```
|
|
172
|
-
<img src="https://
|
|
172
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/MC-tutorial2a.png?raw=true" width="75%">
|
|
173
173
|
|
|
174
174
|
Similarly, the next one was generated using
|
|
175
175
|
```
|
|
176
176
|
plan.runMC('maxBequest', ...)
|
|
177
177
|
```
|
|
178
|
-
<img src="https://
|
|
178
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/MC-tutorial2b.png?raw=true" width="75%">
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
See tutorial notebooks [1](https://github.com/mdlacasse/Owl/blob/main/notebooks/tutorial_1.ipynb),
|
|
@@ -195,7 +195,7 @@ The simplest way to get started with Owl is to use the `streamlit` browser-based
|
|
|
195
195
|
that is started by the `owlplanner.cmd` script, which will start a user interface on your own browser.
|
|
196
196
|
Here is a screenshot of one of the multiple tabs of the interface:
|
|
197
197
|
|
|
198
|
-
<img src="https://
|
|
198
|
+
<img src="https://github.com/mdlacasse/Owl/blob/main/docs/images/OwlUI.png?raw=true" width="100%">
|
|
199
199
|
|
|
200
200
|
Alternatively, one can prefer using Owl from Jupyter notebooks. For that purpose, the `examples` directory
|
|
201
201
|
contains many files as a tutorial. The Jupyter Notebook interface is a browser-based application
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
# docker/Dockerfile
|
|
2
2
|
|
|
3
|
-
FROM python:3.13-
|
|
3
|
+
FROM python:3.13-alpine
|
|
4
4
|
|
|
5
5
|
WORKDIR /app
|
|
6
6
|
|
|
7
|
-
RUN
|
|
8
|
-
apt-get upgrade; \
|
|
9
|
-
apt-get install -y --no-install-recommends \
|
|
10
|
-
curl \
|
|
11
|
-
git \
|
|
12
|
-
; \
|
|
13
|
-
rm -rf /var/lib/apt/lists/*
|
|
7
|
+
RUN apk update; apk upgrade; apk add curl git bash
|
|
14
8
|
|
|
15
9
|
COPY buildentrypoint.sh /usr/bin/entrypoint.sh
|
|
16
10
|
COPY runentrypoint.sh /usr/bin/runentrypoint.sh
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
# docker/Dockerfile
|
|
2
2
|
|
|
3
|
-
FROM python:3.13-
|
|
3
|
+
FROM python:3.13-alpine
|
|
4
4
|
|
|
5
5
|
WORKDIR /app
|
|
6
6
|
|
|
7
|
-
RUN
|
|
8
|
-
apt-get upgrade; \
|
|
9
|
-
apt-get install -y --no-install-recommends \
|
|
10
|
-
curl \
|
|
11
|
-
git \
|
|
12
|
-
; \
|
|
13
|
-
rm -rf /var/lib/apt/lists/*
|
|
7
|
+
RUN apk update; apk upgrade; apk add bash curl git
|
|
14
8
|
|
|
15
9
|
COPY runentrypoint.sh /usr/bin/entrypoint.sh
|
|
16
10
|
|
|
@@ -14,37 +14,49 @@ Using this approach only requires downloading the Docker image from
|
|
|
14
14
|
the [Docker Hub](http://hub.docker.com) and having the [Docker](http://docker.com)
|
|
15
15
|
application installed on your computer.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
There are two versions of the Docker image: one that has been provisioned
|
|
18
|
+
with all the necessary Python modules
|
|
19
|
+
named 'owldocker.static' and one bare image that self-installs the Owl application from
|
|
20
|
+
GitHub at runtime named 'owldocker.bare'.
|
|
21
|
+
The 'static' version, while being larger than the 'bare' version (1.6 GB vs 96 MB),
|
|
22
|
+
will start much faster as all the required code is contained in the image. However,
|
|
23
|
+
the version of Owl is fixed at the time when the container was built. Conversely,
|
|
24
|
+
the 'bare' version will dynamically clone Owl from GitHub and download/install all its requirements.
|
|
25
|
+
The resulting Owl version is the one available from GitHub at the time when the container is launched.
|
|
26
|
+
This leads to a slower start, but this approach guarantees to have Owl's latest version.
|
|
27
|
+
|
|
28
|
+
For downloading the Docker image from the command line (select one from 'static' or 'bare'):
|
|
29
|
+
|
|
18
30
|
```
|
|
19
|
-
docker pull owlplanner/owldocker
|
|
31
|
+
docker pull owlplanner/owldocker.{static or bare}
|
|
20
32
|
```
|
|
21
33
|
Then the container can be started (and stopped) from the command line:
|
|
22
34
|
```
|
|
23
|
-
docker run -p 8501:8501 --rm owlplanner/owldocker
|
|
35
|
+
docker run -p 8501:8501 --rm owlplanner/owldocker.{static or bare}
|
|
24
36
|
```
|
|
25
37
|
|
|
26
38
|
Just point your browser to http://localhost:8501 to access the Owl user interface.
|
|
27
39
|
Owl will run locally and safely through a container on your computer.
|
|
28
40
|
|
|
29
41
|
One can also use the Docker Desktop graphical user interface for performing the same steps.
|
|
30
|
-
The image *owlplanner/owldocker* can be searched for and downloaded in the
|
|
42
|
+
The image *owlplanner/owldocker.{static or bare}* can be searched for and downloaded in the
|
|
31
43
|
*Docker Hub* section of Docker Desktop. Then, on the *Images* section,
|
|
32
44
|
click on the run icon for the image, and use host port 8501 to map to container port 8501.
|
|
33
45
|
|
|
34
46
|
------------------------------------------------------------------------------------
|
|
35
|
-
### Building the docker
|
|
47
|
+
### Building the docker images
|
|
36
48
|
This approach requires cloning the Owl package from GitHub,
|
|
37
49
|
and having both Python and Docker installed on your computer.
|
|
38
50
|
|
|
39
|
-
##### Docker
|
|
51
|
+
##### Building the Docker images
|
|
40
52
|
There are two images you can create. One builds Owl at run time, while
|
|
41
53
|
the other builds it statically in the image.
|
|
42
54
|
These two approaches trade startup time for image space
|
|
43
|
-
(~
|
|
55
|
+
(~96 MB vs. 1.5 GB, ~22 vs. 200 sec).
|
|
44
56
|
First build the Docker image from the `docker` directory:
|
|
45
57
|
```shell
|
|
46
58
|
cd docker
|
|
47
|
-
docker build --no-cache -f Dockerfile.{
|
|
59
|
+
docker build --no-cache -f Dockerfile.{static or bare} -t owlplanner/owldocker.{static or bare}:latest .
|
|
48
60
|
```
|
|
49
61
|
|
|
50
62
|
#### Running the container
|
|
@@ -52,7 +64,7 @@ The container can be run directly from the command line,
|
|
|
52
64
|
with the desired port mapping.
|
|
53
65
|
|
|
54
66
|
```shell
|
|
55
|
-
docker run -p 8501:8501 --rm owldocker
|
|
67
|
+
docker run -p 8501:8501 --rm owlplanner/owldocker.{static or bare}
|
|
56
68
|
```
|
|
57
69
|
|
|
58
70
|
#### Running with docker-compose
|
|
@@ -64,7 +76,7 @@ The compose file maps the host-side port to the container-side port.
|
|
|
64
76
|
```yml
|
|
65
77
|
services:
|
|
66
78
|
owl:
|
|
67
|
-
image: owldocker
|
|
79
|
+
image: owldocker.{static or bare}
|
|
68
80
|
restart: always
|
|
69
81
|
ports:
|
|
70
82
|
- 8501:8501
|
|
@@ -75,4 +87,4 @@ As before, just point your browser to http://localhost:8501 to access the Owl us
|
|
|
75
87
|
------------------------------------------------------------------------------------
|
|
76
88
|
|
|
77
89
|
#### Credits
|
|
78
|
-
Josh (noimjosh@gmail.com)
|
|
90
|
+
Josh (noimjosh@gmail.com), kg333 (matthew@kyengineer.com)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"Plan Name" = "drawdowncalc-comparison-1"
|
|
2
|
-
Description = "This is a case involving a single individual. Case is used for comparing with @hugcity's DrawdownCalc. For max bequest, it should yield $90,
|
|
2
|
+
Description = "This is a case involving a single individual. Case is used for comparing with @hugcity's DrawdownCalc. For max bequest, it should yield $90,882, while an $80k net spending should leave $678,625 as a bequest."
|
|
3
3
|
|
|
4
4
|
["Basic Info"]
|
|
5
5
|
Status = "single"
|
|
@@ -51,6 +51,7 @@ startRothConversions = 2025
|
|
|
51
51
|
withSCLoop = false
|
|
52
52
|
solver = "HiGHS"
|
|
53
53
|
spendingSlack = 0
|
|
54
|
+
withMedicare = "None"
|
|
54
55
|
|
|
55
56
|
[Results]
|
|
56
57
|
"Default plots" = "today"
|
|
@@ -296,6 +296,10 @@ def readConfig(file, *, verbose=True, logstreams=None, readContributions=True):
|
|
|
296
296
|
# Solver Options.
|
|
297
297
|
p.solverOptions = diconf["Solver Options"]
|
|
298
298
|
|
|
299
|
+
# Address legacy case files.
|
|
300
|
+
if diconf["Solver Options"].get("withMedicare", None) is True:
|
|
301
|
+
p.solverOptions["withMedicare"] = "loop"
|
|
302
|
+
|
|
299
303
|
# Check consistency of noRothConversions.
|
|
300
304
|
name = p.solverOptions.get("noRothConversions", "None")
|
|
301
305
|
if name != "None" and name not in p.inames:
|
|
@@ -1021,7 +1021,7 @@ class Plan(object):
|
|
|
1021
1021
|
Refer to companion document for explanations.
|
|
1022
1022
|
All binary variables must be lumped at the end of the vector.
|
|
1023
1023
|
"""
|
|
1024
|
-
medi = options.get("
|
|
1024
|
+
medi = options.get("withMedicare", "loop") == "optimize"
|
|
1025
1025
|
|
|
1026
1026
|
# Stack all variables in a single block vector with all binary variables at the end.
|
|
1027
1027
|
C = {}
|
|
@@ -1223,8 +1223,8 @@ class Plan(object):
|
|
|
1223
1223
|
|
|
1224
1224
|
for i in range(self.N_i):
|
|
1225
1225
|
for j in range(self.N_j):
|
|
1226
|
-
backTau = 1
|
|
1227
|
-
rhs = self.beta_ij[i, j]
|
|
1226
|
+
backTau = 1 + yearSpent * np.sum(self.tau_kn[:, 0] * self.alpha_ijkn[i, j, :, 0])
|
|
1227
|
+
rhs = self.beta_ij[i, j] / backTau
|
|
1228
1228
|
self.B.setRange(_q3(self.C["b"], i, j, 0, self.N_i, self.N_j, self.N_n + 1), rhs, rhs)
|
|
1229
1229
|
|
|
1230
1230
|
def _add_surplus_deposit_linking(self):
|
|
@@ -1386,7 +1386,7 @@ class Plan(object):
|
|
|
1386
1386
|
self.B.setRange(_q3(self.C["zx"], i, n, 1, self.N_i, self.N_n, self.N_zx), 0, 0)
|
|
1387
1387
|
|
|
1388
1388
|
def _configure_Medicare_binary_variables(self, options):
|
|
1389
|
-
if
|
|
1389
|
+
if options.get("withMedicare", "loop") != "optimize":
|
|
1390
1390
|
return
|
|
1391
1391
|
|
|
1392
1392
|
bigM = options.get("bigM", 5e6)
|
|
@@ -1444,7 +1444,7 @@ class Plan(object):
|
|
|
1444
1444
|
self.A.addRow(row2, -np.inf, rhs2)
|
|
1445
1445
|
|
|
1446
1446
|
def _add_Medicare_costs(self, options):
|
|
1447
|
-
if
|
|
1447
|
+
if options.get("withMedicare", "loop") != "optimize":
|
|
1448
1448
|
return
|
|
1449
1449
|
|
|
1450
1450
|
for n in range(self.nm):
|
|
@@ -1627,7 +1627,7 @@ class Plan(object):
|
|
|
1627
1627
|
"netSpending",
|
|
1628
1628
|
"noRothConversions",
|
|
1629
1629
|
"oppCostX",
|
|
1630
|
-
"
|
|
1630
|
+
"withMedicare",
|
|
1631
1631
|
"previousMAGIs",
|
|
1632
1632
|
"solver",
|
|
1633
1633
|
"spendingSlack",
|
|
@@ -1635,7 +1635,6 @@ class Plan(object):
|
|
|
1635
1635
|
"units",
|
|
1636
1636
|
"xorConstraints",
|
|
1637
1637
|
"withSCLoop",
|
|
1638
|
-
"withMedicare", # Ignore keyword.
|
|
1639
1638
|
]
|
|
1640
1639
|
# We might modify options if required.
|
|
1641
1640
|
options = {} if options is None else options
|
|
@@ -1713,7 +1712,7 @@ class Plan(object):
|
|
|
1713
1712
|
"""
|
|
1714
1713
|
Self-consistent loop, regardless of solver.
|
|
1715
1714
|
"""
|
|
1716
|
-
|
|
1715
|
+
includeMedicare = options.get("withMedicare", "loop") == "loop"
|
|
1717
1716
|
withSCLoop = options.get("withSCLoop", True)
|
|
1718
1717
|
|
|
1719
1718
|
if objective == "maxSpending":
|
|
@@ -1724,7 +1723,7 @@ class Plan(object):
|
|
|
1724
1723
|
it = 0
|
|
1725
1724
|
old_x = np.zeros(self.nvars)
|
|
1726
1725
|
old_objfns = [np.inf]
|
|
1727
|
-
self._computeNLstuff(None,
|
|
1726
|
+
self._computeNLstuff(None, includeMedicare)
|
|
1728
1727
|
while True:
|
|
1729
1728
|
objfn, xx, solverSuccess, solverMsg = solverMethod(objective, options)
|
|
1730
1729
|
|
|
@@ -1735,7 +1734,7 @@ class Plan(object):
|
|
|
1735
1734
|
if not withSCLoop:
|
|
1736
1735
|
break
|
|
1737
1736
|
|
|
1738
|
-
self._computeNLstuff(xx,
|
|
1737
|
+
self._computeNLstuff(xx, includeMedicare)
|
|
1739
1738
|
|
|
1740
1739
|
delta = xx - old_x
|
|
1741
1740
|
absSolDiff = np.sum(np.abs(delta), axis=0)/100
|
|
@@ -1955,7 +1954,7 @@ class Plan(object):
|
|
|
1955
1954
|
|
|
1956
1955
|
return J_n
|
|
1957
1956
|
|
|
1958
|
-
def _computeNLstuff(self, x,
|
|
1957
|
+
def _computeNLstuff(self, x, includeMedicare):
|
|
1959
1958
|
"""
|
|
1960
1959
|
Compute MAGI, Medicare costs, long-term capital gain tax rate, and
|
|
1961
1960
|
net investment income tax (NIIT).
|
|
@@ -1972,7 +1971,7 @@ class Plan(object):
|
|
|
1972
1971
|
self.J_n = self._computeNIIT(self.MAGI_n, self.I_n, self.Q_n)
|
|
1973
1972
|
self.psi_n = tx.capitalGainTaxRate(self.N_i, self.MAGI_n, self.gamma_n[:-1], self.n_d, self.N_n)
|
|
1974
1973
|
# Compute Medicare through self-consistent loop.
|
|
1975
|
-
if
|
|
1974
|
+
if includeMedicare:
|
|
1976
1975
|
self.M_n = tx.mediCosts(self.yobs, self.horizons, self.MAGI_n, self.prevMAGI, self.gamma_n[:-1], self.N_n)
|
|
1977
1976
|
|
|
1978
1977
|
return None
|
|
@@ -70,7 +70,7 @@ taxBrackets_preTCJA = np.array(
|
|
|
70
70
|
]
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
-
# These are 2025 current
|
|
73
|
+
# These are 2025 current.
|
|
74
74
|
stdDeduction_OBBBA = np.array([15750, 31500]) # Single, MFJ
|
|
75
75
|
# These are speculated (adjusted for inflation).
|
|
76
76
|
stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
|
|
@@ -78,7 +78,7 @@ stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
|
|
|
78
78
|
# These are current (adjusted for inflation) per individual.
|
|
79
79
|
extra65Deduction = np.array([2000, 1600]) # Single, MFJ
|
|
80
80
|
|
|
81
|
-
# Thresholds
|
|
81
|
+
# Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
|
|
82
82
|
capGainRates = np.array(
|
|
83
83
|
[
|
|
84
84
|
[48350, 533400],
|