owlplanner 2025.12.5__py3-none-any.whl → 2026.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- owlplanner/In Discussion #58, the case of Kim and Sam.md +307 -0
- owlplanner/__init__.py +20 -1
- owlplanner/abcapi.py +24 -23
- owlplanner/cli/README.md +50 -0
- owlplanner/cli/_main.py +52 -0
- owlplanner/cli/cli_logging.py +56 -0
- owlplanner/cli/cmd_list.py +83 -0
- owlplanner/cli/cmd_run.py +86 -0
- owlplanner/config.py +315 -136
- owlplanner/data/__init__.py +21 -0
- owlplanner/data/awi.csv +75 -0
- owlplanner/data/bendpoints.csv +49 -0
- owlplanner/data/newawi.csv +75 -0
- owlplanner/data/rates.csv +99 -98
- owlplanner/debts.py +315 -0
- owlplanner/fixedassets.py +288 -0
- owlplanner/mylogging.py +157 -25
- owlplanner/plan.py +1044 -332
- owlplanner/plotting/__init__.py +16 -3
- owlplanner/plotting/base.py +17 -3
- owlplanner/plotting/factory.py +16 -3
- owlplanner/plotting/matplotlib_backend.py +30 -7
- owlplanner/plotting/plotly_backend.py +33 -10
- owlplanner/progress.py +66 -9
- owlplanner/rates.py +366 -361
- owlplanner/socialsecurity.py +142 -22
- owlplanner/tax2026.py +170 -57
- owlplanner/timelists.py +316 -32
- owlplanner/utils.py +204 -5
- owlplanner/version.py +20 -1
- {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/METADATA +50 -158
- owlplanner-2026.1.26.dist-info/RECORD +36 -0
- owlplanner-2026.1.26.dist-info/entry_points.txt +2 -0
- owlplanner-2026.1.26.dist-info/licenses/AUTHORS +15 -0
- owlplanner/tax2025.py +0 -339
- owlplanner-2025.12.5.dist-info/RECORD +0 -24
- {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/WHEEL +0 -0
- {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: owlplanner
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2026.1.26
|
|
4
4
|
Summary: Owl - Optimal Wealth Lab: 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
|
|
@@ -683,6 +683,7 @@ License: GNU GENERAL PUBLIC LICENSE
|
|
|
683
683
|
the library. If this is what you want to do, use the GNU Lesser General
|
|
684
684
|
Public License instead of this License. But first, please read
|
|
685
685
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
686
|
+
License-File: AUTHORS
|
|
686
687
|
License-File: LICENSE
|
|
687
688
|
Classifier: Development Status :: 5 - Production/Stable
|
|
688
689
|
Classifier: Intended Audience :: End Users/Desktop
|
|
@@ -691,7 +692,10 @@ Classifier: Operating System :: OS Independent
|
|
|
691
692
|
Classifier: Programming Language :: Python :: 3
|
|
692
693
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
693
694
|
Requires-Python: >=3.10
|
|
695
|
+
Requires-Dist: click>=8.3.1
|
|
694
696
|
Requires-Dist: highspy
|
|
697
|
+
Requires-Dist: jupyter>=1.1.1
|
|
698
|
+
Requires-Dist: loguru>=0.7.3
|
|
695
699
|
Requires-Dist: matplotlib
|
|
696
700
|
Requires-Dist: numpy
|
|
697
701
|
Requires-Dist: odfpy
|
|
@@ -699,6 +703,7 @@ Requires-Dist: openpyxl
|
|
|
699
703
|
Requires-Dist: pandas
|
|
700
704
|
Requires-Dist: plotly>=6.3
|
|
701
705
|
Requires-Dist: pulp
|
|
706
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
702
707
|
Requires-Dist: scipy
|
|
703
708
|
Requires-Dist: seaborn
|
|
704
709
|
Requires-Dist: streamlit
|
|
@@ -710,7 +715,7 @@ Description-Content-Type: text/markdown
|
|
|
710
715
|
|
|
711
716
|
## A retirement exploration tool based on linear programming
|
|
712
717
|
|
|
713
|
-
<img align=right src="
|
|
718
|
+
<img align="right" src="papers/images/owl.png" width="250">
|
|
714
719
|
|
|
715
720
|
-------------------------------------------------------------------------------------
|
|
716
721
|
|
|
@@ -722,6 +727,10 @@ Users can select varying return rates to perform historical back testing,
|
|
|
722
727
|
stochastic rates for performing Monte Carlo analyses,
|
|
723
728
|
or fixed rates either derived from historical averages, or set by the user.
|
|
724
729
|
|
|
730
|
+
Owl is designed for US retirees as it considers US federal tax laws,
|
|
731
|
+
Medicare premiums, rules for 401k including required minimum distributions,
|
|
732
|
+
maturation rules for Roth accounts and conversions, social security rules, etc.
|
|
733
|
+
|
|
725
734
|
There are three ways to run Owl:
|
|
726
735
|
|
|
727
736
|
- **Streamlit Hub:** Run Owl remotely as hosted on the Streamlit Community Server at
|
|
@@ -733,154 +742,6 @@ Follow these [instructions](docker/README.md) for using this option.
|
|
|
733
742
|
- **Self-hosting:** Run Owl locally on your computer using Python code and libraries.
|
|
734
743
|
Follow these [instructions](INSTALL.md) to install from the source code and self-host on your own computer.
|
|
735
744
|
|
|
736
|
-
-------------------------------------------------------------------------------------
|
|
737
|
-
## Overview
|
|
738
|
-
This package is a modeling framework for exploring the sensitivity of retirement financial decisions.
|
|
739
|
-
Strictly speaking, it is not a planning tool, but more an environment for exploring *what if* scenarios.
|
|
740
|
-
It provides different realizations of a financial strategy through the rigorous
|
|
741
|
-
mathematical optimization of relevant decision variables. Two major objective goals can be set: either
|
|
742
|
-
maximize net spending, or after-tax bequest under various constraints.
|
|
743
|
-
Look at the *Capabilities* section below for more detail.
|
|
744
|
-
|
|
745
|
-
One can certainly have a savings plan, but due to the volatility of financial investments,
|
|
746
|
-
it is impossible to have a certain asset earnings plan. This does not mean one cannot make decisions.
|
|
747
|
-
These decisions need to be guided with an understanding of the sensitivity of the parameters.
|
|
748
|
-
This is exactly where this tool fits in. Given your savings capabilities and spending desires,
|
|
749
|
-
it can generate different future realizations of
|
|
750
|
-
your strategy under different market assumptions, helping to better understand your financial situation.
|
|
751
|
-
|
|
752
|
-
-------------------------------------------------------------------------------------
|
|
753
|
-
## Purpose and vision
|
|
754
|
-
One goal of Owl is to provide a free and open-source ecosystem that has cutting-edge optimization capabilities,
|
|
755
|
-
allowing for the next generation of Python-literate retirees to experiment with their own financial future
|
|
756
|
-
while providing a codebase where they can learn and contribute. At the same time, an intuitive and easy-to-use
|
|
757
|
-
user interface based on Streamlit allows a broad set of users to benefit from the application as it only requires basic financial knowledge.
|
|
758
|
-
|
|
759
|
-
There are and were
|
|
760
|
-
good retirement optimizers in the recent past, but the vast majority of them are either proprietary platforms
|
|
761
|
-
collecting your data, or academic papers that share the results without really sharing the details of
|
|
762
|
-
the underlying mathematical models.
|
|
763
|
-
The algorithms in Owl rely on the open-source HiGHS linear programming solver but they have also been ported and tested on
|
|
764
|
-
other platforms such as Mosek and COIN-OR. The complete formulation and
|
|
765
|
-
detailed description of the underlying
|
|
766
|
-
mathematical model can be found [here](https://github.com/mdlacasse/Owl/blob/main/docs/owl.pdf).
|
|
767
|
-
|
|
768
|
-
It is anticipated that most end users will use Owl through the graphical interface
|
|
769
|
-
either at [owlplanner.streamlit.app](https://owlplanner.streamlit.app)
|
|
770
|
-
or [installed](INSTALL.md) on their own computer.
|
|
771
|
-
The underlying Python package can also be used directly through Python scripts or Jupyter Notebooks
|
|
772
|
-
as described [here](USER_GUIDE.md).
|
|
773
|
-
|
|
774
|
-
Not every retirement decision strategy can be framed as an easy-to-solve optimization problem.
|
|
775
|
-
In particular, if one is interested in comparing different withdrawal strategies,
|
|
776
|
-
[FI Calc](https://ficalc.app) is an elegant application that addresses this need.
|
|
777
|
-
If, however, you also want to optimize spending, bequest, and Roth conversions, with
|
|
778
|
-
an approach also considering Medicare and federal income tax over the next few years,
|
|
779
|
-
then Owl is definitely a tool that can help guide your decisions.
|
|
780
|
-
|
|
781
|
-
--------------------------------------------------------------------------------------
|
|
782
|
-
## Capabilities
|
|
783
|
-
Owl can optimize for either maximum net spending under the constraint of a given bequest (which can be zero),
|
|
784
|
-
or maximize the after-tax value of a bequest under the constraint of a desired net spending profile,
|
|
785
|
-
and under the assumption of a heirs marginal tax rate.
|
|
786
|
-
Roth conversions are also considered, subject to an optional maximum conversion amount,
|
|
787
|
-
and optimized to suit the goals of the selected objective function.
|
|
788
|
-
All calculations are indexed for inflation, which is either provided as a fixed rate,
|
|
789
|
-
or through historical values, as are all other rates used for the calculations.
|
|
790
|
-
These rates can be used for backtesting different scenarios by choosing
|
|
791
|
-
*historical* rates, or by choosing *historical average* rates over a historical year range,
|
|
792
|
-
or what I coined "*histochastic*" rates which are
|
|
793
|
-
generated using the statistical distribution of observed historical rates.
|
|
794
|
-
|
|
795
|
-
Portfolios available for experimenting include assets from the S&P 500, Corporate Bonds Baa, Treasury 10-y Notes,
|
|
796
|
-
and cash assets assumed to just follow inflation which is represented by the Consumer Price Index.
|
|
797
|
-
Other asset classes can easily be added, but would add complexity while only providing diminishing insights.
|
|
798
|
-
Historical data used are from
|
|
799
|
-
[Aswath Damodaran](https://pages.stern.nyu.edu/~adamodar/) at the Stern School of Business.
|
|
800
|
-
Asset allocations are selected for the duration of the plan, and these can glide linearly
|
|
801
|
-
or along a configurable s-curve over the lifespan of the individual.
|
|
802
|
-
|
|
803
|
-
Spending profiles are adjusted for inflation, and so are all other indexable quantities. Proflies can be
|
|
804
|
-
flat or follow a *smile* curve which is also adjustable through three simple parameters.
|
|
805
|
-
|
|
806
|
-
Available rates are from 1928 to last year and can be used to test historical performance.
|
|
807
|
-
Fixed rates can also be provided, as well as *histochastic* rates, which are generated using
|
|
808
|
-
the statistical characteristics (means and covariance matrix) of
|
|
809
|
-
a selected historical year range. Pure *stochastic* rates can also be generated
|
|
810
|
-
if the user provides means, volatility (expressed as standard deviation), and optionally
|
|
811
|
-
the correlations between the different assets return rates provided as a matrix, or a list of
|
|
812
|
-
the off-diagonal elements (see documentation for details).
|
|
813
|
-
Average rates calculated over a historical data period can also be chosen.
|
|
814
|
-
|
|
815
|
-
Monte Carlo simulations capabilities are included and provide a probability of success and a histogram of
|
|
816
|
-
outcomes. These simulations can be used for either determining the probability distribution of the
|
|
817
|
-
maximum net spending amount under
|
|
818
|
-
the constraint of a desired bequest, or the probability distribution of the maximum
|
|
819
|
-
bequest under the constraint of a desired net spending amount. Unlike discrete-event
|
|
820
|
-
simulators, Owl uses an optimization algorithm for every new scenario, which results in more
|
|
821
|
-
calculations being performed. As a result, the number of cases to be considered should be kept
|
|
822
|
-
to a reasonable number. For a few hundred cases, a few minutes of calculations can provide very good estimates
|
|
823
|
-
and reliable probability distributions.
|
|
824
|
-
|
|
825
|
-
Optimizing each solution is more representative than event-base simulators
|
|
826
|
-
in the sense that optimal solutions
|
|
827
|
-
will naturally adjust to the return scenarios being considered.
|
|
828
|
-
This is more realistic as retirees would certainly re-evaluate
|
|
829
|
-
their expectations under severe market drops or gains.
|
|
830
|
-
This optimal approach provides a net benefit over event-based simulators,
|
|
831
|
-
which maintain a distribution strategy either fixed, or within guardrails for capturing the
|
|
832
|
-
retirees' reactions to the market.
|
|
833
|
-
|
|
834
|
-
Basic input parameters can be entered through the user interface
|
|
835
|
-
while optional additional time series can be read from
|
|
836
|
-
an Excel spreadsheet that contains future wages, contributions
|
|
837
|
-
to savings accounts, and planned *big-ticket items* such as the purchase of a lake house,
|
|
838
|
-
the sale of a boat, large gifts, or inheritance.
|
|
839
|
-
|
|
840
|
-
Three types of savings accounts are considered: taxable, tax-deferred, and tax-free,
|
|
841
|
-
which are all tracked separately for married individuals. Asset transition to the surviving spouse
|
|
842
|
-
is done according to beneficiary fractions for each type of savings account.
|
|
843
|
-
Tax status covers married filing jointly and single, depending on the number of individuals reported.
|
|
844
|
-
|
|
845
|
-
Maturation rules for Roth contributions and conversions are implemented as constraints
|
|
846
|
-
limiting withdrawal amounts to cover Roth account balances for 5 years after the events.
|
|
847
|
-
Medicare and IRMAA calculations are performed through a self-consistent loop on cash flow constraints.
|
|
848
|
-
They can also be optimized explicitly as an option, but this choice can lead to longer calculations
|
|
849
|
-
due to the use of the many additional binary variables required by the formulation.
|
|
850
|
-
Future Medicare and IRMAA values are simple projections of current values with the assumed inflation rates.
|
|
851
|
-
|
|
852
|
-
Owl has a basic social security calculator that determines the actual benefits based on the individual's
|
|
853
|
-
primary insurance amount (PIA), full retirement age (FRA), and claiming age. Both
|
|
854
|
-
spousal's benefits and survivor's benefits are calculated for non-complex cases.
|
|
855
|
-
|
|
856
|
-
### Limitations
|
|
857
|
-
Owl is work in progress. At the current time:
|
|
858
|
-
- Only the US federal income tax is considered (and minimized through the optimization algorithm).
|
|
859
|
-
Head of household filing status has not been added but can easily be.
|
|
860
|
-
- Required minimum distributions are calculated, but tables for spouses more than 10 years apart are not included.
|
|
861
|
-
These cases are detected and will generate an error message.
|
|
862
|
-
- Current version has no optimization of asset allocations between individuals and/or types of savings accounts.
|
|
863
|
-
If there is interest, that could be added in the future.
|
|
864
|
-
- 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).
|
|
865
|
-
- When Medicare calculations are done through a self-consistent loop,
|
|
866
|
-
the Medicare premiums are calculated after an initial solution is generated,
|
|
867
|
-
and then a new solution is re-generated with these premiums as a constraint.
|
|
868
|
-
In some situations, when the income (MAGI) is near an IRMAA bracket, oscillatory solutions can arise.
|
|
869
|
-
While the solutions generated are very close to one another, Owl will pick the smallest solution
|
|
870
|
-
for being conservative. While sometimes computationally costly,
|
|
871
|
-
a comparison with a full Medicare optimization should always be performed.
|
|
872
|
-
- Part D is not included in the IRMAA calculations. Only Part B is taken into account,
|
|
873
|
-
which is considerably more significant.
|
|
874
|
-
- Future tax brackets are pure speculations derived from the little we know now and projected to the next 30 years.
|
|
875
|
-
Your guesses are as good as mine.
|
|
876
|
-
|
|
877
|
-
The solution from an optimization algorithm has only two states: feasible and infeasible.
|
|
878
|
-
Therefore, unlike event-driven simulators that can tell you that your distribution strategy runs
|
|
879
|
-
out of money in year 20, an optimization-based solver can only tell you that a solution does or does not
|
|
880
|
-
exist for the plan being considered. Examples of infeasible solutions include requesting a bequeathed
|
|
881
|
-
estate value too large for the savings assets to support, even with zero net spending basis,
|
|
882
|
-
or maximizing the bequest subject to a net spending basis that is already too large for the savings
|
|
883
|
-
assets to support, even with no estate being left.
|
|
884
745
|
|
|
885
746
|
---------------------------------------------------------------
|
|
886
747
|
## Documentation
|
|
@@ -892,16 +753,47 @@ assets to support, even with no estate being left.
|
|
|
892
753
|
---------------------------------------------------------------------
|
|
893
754
|
|
|
894
755
|
## Credits
|
|
895
|
-
-
|
|
896
|
-
- Image from [freepik](https://freepik.com)
|
|
897
|
-
- Optimization solver from [HiGHS](https://highs.dev)
|
|
898
|
-
- Streamlit Community Cloud [Streamlit](https://streamlit.io)
|
|
899
|
-
- Contributors: Josh (noimjosh@gmail.com) for Docker image code,
|
|
900
|
-
kg333 for fixing an error in Docker's instructions,
|
|
901
|
-
Dale Seng (sengsational) for great insights and suggestions,
|
|
756
|
+
- Contributors:
|
|
902
757
|
Robert E. Anderson (NH-RedAnt) for bug fixes and suggestions,
|
|
903
758
|
Clark Jefcoat (hubcity) for fruitful interactions,
|
|
904
|
-
|
|
759
|
+
kg333 for fixing an error in Docker's instructions,
|
|
760
|
+
John Leonard (jleonard99) for great suggestions, website, and more to come,
|
|
761
|
+
Benjamin Quinn (blquinn) for improvements and bug fixes,
|
|
762
|
+
Dale Seng (sengsational) for great insights, testing, and suggestions,
|
|
763
|
+
Josh Williams (noimjosh) for Docker image code,
|
|
764
|
+
Gene Wood (gene1wood) for improvements and bug fixes.
|
|
765
|
+
- Greg Grothaus for developing [ssa.tools](https://ssa.tools) and providing an integration with Owl.
|
|
766
|
+
- Owl image is from [freepik](https://freepik.com).
|
|
767
|
+
- Historical rates are from [Aswath Damodaran](https://pages.stern.nyu.edu/~adamodar/).
|
|
768
|
+
- Linear programming optimization solvers are from
|
|
769
|
+
[HiGHS](https://highs.dev) and [PuLP](https://coin-or.github.io/pulp/).
|
|
770
|
+
It can also run on [MOSEK](https://mosek.com) if available on your computer.
|
|
771
|
+
- Owl planner relies on the following [Python](https://python.org) packages:
|
|
772
|
+
- [highspy](https://highs.dev),
|
|
773
|
+
[loguru](https://github.com/Delgan/loguru),
|
|
774
|
+
[Matplotlib](https://matplotlib.org),
|
|
775
|
+
[Numpy](https://numpy.org),
|
|
776
|
+
[odfpy](https://https://pypi.org/project/odfpy),
|
|
777
|
+
[openpyxl](https://openpyxl.readthedocs.io),
|
|
778
|
+
[Pandas](https://pandas.pydata.org),
|
|
779
|
+
[Plotly](https://plotly.com),
|
|
780
|
+
[PuLP](https://coin-or.github.io/pulp),
|
|
781
|
+
[Scipy](https://scipy.org),
|
|
782
|
+
[Seaborn](https://seaborn.pydata.org),
|
|
783
|
+
[toml](https://toml.io),
|
|
784
|
+
and [Streamlit](https://streamlit.io) for the front-end.
|
|
785
|
+
|
|
786
|
+
## Bugs and Feature Requests
|
|
787
|
+
Please submit bugs and feature requests through
|
|
788
|
+
[GitHub](https://github.com/mdlacasse/owl/issues) if you have a GitHub account
|
|
789
|
+
or directly by [email](mailto:martin.d.lacasse@gmail.com).
|
|
790
|
+
Or just drop me a line to report your experience with the tool.
|
|
791
|
+
|
|
792
|
+
## Privacy
|
|
793
|
+
This app does not store or forward any information. All data entered is lost
|
|
794
|
+
after a session is closed. However, you can choose to download selected parts of your
|
|
795
|
+
own data to your computer before closing the session. These data will be stored strictly on
|
|
796
|
+
your computer and can be used to reproduce a case at a later time.
|
|
905
797
|
|
|
906
798
|
---------------------------------------------------------------------
|
|
907
799
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"owlplanner/In Discussion #58, the case of Kim and Sam.md",sha256=ah5BgkP4Bo04SZ2-Ory8PBeoFRVd7YNUBktYtjp42v0,25193
|
|
2
|
+
owlplanner/__init__.py,sha256=X9u8PrfeTyfjuksYhx0eNrIACgX5Tg3Le7pz8qKkJXk,1341
|
|
3
|
+
owlplanner/abcapi.py,sha256=5yWuiu5eBVO46V5vklO_zfCCrFZX9guRZ9XEemeJ878,7229
|
|
4
|
+
owlplanner/config.py,sha256=-q6TYMagUJGv1ige6VvWPFtZ5ZWkT9Yh-HQrnnWDHfU,19834
|
|
5
|
+
owlplanner/debts.py,sha256=c-eLynjOFryYisae51Po6Iuh46_d_AGOUbOz3z96ZJY,9850
|
|
6
|
+
owlplanner/fixedassets.py,sha256=ItFeXjIdhMVNyXY0xBW6P9ODRajVg1kDRaid9-Eu_Ww,11510
|
|
7
|
+
owlplanner/mylogging.py,sha256=amQA0qWPsXMoJkYYF3nMaVtCDZMWxXJ1hbJS0TkMvf8,8269
|
|
8
|
+
owlplanner/plan.py,sha256=VWrKDGl3UgNB6GQzfw7hQzVNJW_BcupH0I2zP9yKtsw,152509
|
|
9
|
+
owlplanner/progress.py,sha256=ayqoRUMn6dtIxHDYaeX1mu7W2rAhkBYAi4Y2Y_NtdYA,2521
|
|
10
|
+
owlplanner/rates.py,sha256=W-cXEbJl7L9cPEhQbtz4bf_JAkChoCfDSFoZHDUZeys,14393
|
|
11
|
+
owlplanner/socialsecurity.py,sha256=4vn76dUZ2P2fMKyy4eu2ramGNvPQymKzsNFZvwbFWOc,7595
|
|
12
|
+
owlplanner/tax2026.py,sha256=-aHOVv5pErXY82PK5dQBjqoAqYc74w4C5zqGODboUBw,15659
|
|
13
|
+
owlplanner/timelists.py,sha256=4dBZVCgNIx0RwUfimp_k-m9KhRFj6BHX8Hon1vb8oo8,15032
|
|
14
|
+
owlplanner/utils.py,sha256=l2-3HmZutFlEfJUp7gFblY3PBNYm7HiPPCGl0wFkVmA,9461
|
|
15
|
+
owlplanner/version.py,sha256=ah-vzrsjzBTIMnscgEdl8z7Y1yu4uXQ5B3og10cS2y4,747
|
|
16
|
+
owlplanner/cli/README.md,sha256=R-jwwiX5ZW6gCKaRPWVCgwKVoN2MKozrLAkKvfUT4vA,2292
|
|
17
|
+
owlplanner/cli/_main.py,sha256=qAy_2oCLaa_EhvfGFnuh_sVXkCcgp70WMSL7_QKH5XI,1497
|
|
18
|
+
owlplanner/cli/cli_logging.py,sha256=CTBaAMpJIG7ge9Gz44YykBiea3hMxdAXHyiLj1UyvDU,1690
|
|
19
|
+
owlplanner/cli/cmd_list.py,sha256=lOpS_5LGR1IpJLOaHQOrwm1EtbGwv0tfIcZZ8QObmMM,2584
|
|
20
|
+
owlplanner/cli/cmd_run.py,sha256=aLy-X6x_3MSO_yBQ1R4MWKzczfQgWuRQUsNTrVUaWj8,3009
|
|
21
|
+
owlplanner/data/__init__.py,sha256=jpbWHd9n4F06pyfQj1kI3HQ3uiWXfZCjb5BYINOr9_M,850
|
|
22
|
+
owlplanner/data/awi.csv,sha256=w9YwPEqhxqo1Czumh3ihLXLPtk8kJQgqWKClMysOq-c,1625
|
|
23
|
+
owlplanner/data/bendpoints.csv,sha256=h0a7_XqTuyHWe5ZlS3mjIcAWqOMG_93GoiETS-1xxUY,746
|
|
24
|
+
owlplanner/data/newawi.csv,sha256=w9YwPEqhxqo1Czumh3ihLXLPtk8kJQgqWKClMysOq-c,1625
|
|
25
|
+
owlplanner/data/rates.csv,sha256=6Pr5cXFunZKRzB32--kWzhuRnTQMNFfZaww0QF1SIag,3898
|
|
26
|
+
owlplanner/plotting/__init__.py,sha256=UrKO_zHTxZ-Wb2sQbyk2vNHe20Owzt6M1KJ2M6Z5GGw,944
|
|
27
|
+
owlplanner/plotting/base.py,sha256=70-J5AWnHtWqrehVhCNojnTdfNT6PnEpxBKzDh3R57o,3298
|
|
28
|
+
owlplanner/plotting/factory.py,sha256=sVZ0uAuGKBVVh1qkF2IS7xNZGUPIQRIfqgUt2s2Ncjk,1694
|
|
29
|
+
owlplanner/plotting/matplotlib_backend.py,sha256=AILNOhXi_gsR__i1hKoHaEHvMUqQMyLeRAGCoCBfQBY,19222
|
|
30
|
+
owlplanner/plotting/plotly_backend.py,sha256=OqMJRxAhpwIbKSV-Od1-q-tzOs66McZPbKD32be2qx0,34523
|
|
31
|
+
owlplanner-2026.1.26.dist-info/METADATA,sha256=ZHGDl0MzxLspxcYtBCoxp8q1oQKnEZ1qH_gjzInxw1Q,46494
|
|
32
|
+
owlplanner-2026.1.26.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
33
|
+
owlplanner-2026.1.26.dist-info/entry_points.txt,sha256=m2Ql-6VQbotl65wiqx35d3TIh-Tm6Wdjrz3hNdqZKKg,52
|
|
34
|
+
owlplanner-2026.1.26.dist-info/licenses/AUTHORS,sha256=KD6uW6iEOHf51J8DEJ6az-gowFv3NWIpW4zRgqxwuAA,464
|
|
35
|
+
owlplanner-2026.1.26.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
36
|
+
owlplanner-2026.1.26.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# This is the list of Owlplanner's significant contributors.
|
|
2
|
+
#
|
|
3
|
+
# This does not necessarily list everyone who has contributed code.
|
|
4
|
+
# To see the full list of contributors, see the revision history in
|
|
5
|
+
# source control.
|
|
6
|
+
Martin-D. Lacasse (mdlacasse, original author)
|
|
7
|
+
Robert E. Anderson (NH-RedAnt)
|
|
8
|
+
Clark Jefcoat (hubcity)
|
|
9
|
+
kg333
|
|
10
|
+
John Leonard (jleonard99)
|
|
11
|
+
Benjamin Quinn (blquinn)
|
|
12
|
+
Dale Seng (sengsational)
|
|
13
|
+
Josh Williams (noimjosh)
|
|
14
|
+
Gene Wood (gene1wood)
|
|
15
|
+
|
owlplanner/tax2025.py
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
Owl/tax2025
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
A retirement planner using linear programming optimization.
|
|
7
|
-
|
|
8
|
-
See companion document for a complete explanation and description
|
|
9
|
-
of all variables and parameters.
|
|
10
|
-
|
|
11
|
-
Module to handle all tax calculations.
|
|
12
|
-
|
|
13
|
-
Copyright © 2024 - Martin-D. Lacasse
|
|
14
|
-
|
|
15
|
-
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
import numpy as np
|
|
20
|
-
from datetime import date
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
##############################################################################
|
|
24
|
-
# Prepare the data.
|
|
25
|
-
|
|
26
|
-
taxBracketNames = ["10%", "12/15%", "22/25%", "24/28%", "32/33%", "35%", "37/40%"]
|
|
27
|
-
|
|
28
|
-
rates_OBBBA = np.array([0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.370])
|
|
29
|
-
rates_preTCJA = np.array([0.10, 0.15, 0.25, 0.28, 0.33, 0.35, 0.396])
|
|
30
|
-
|
|
31
|
-
###############################################################################
|
|
32
|
-
# Start of section where rates need to be actualized every year.
|
|
33
|
-
###############################################################################
|
|
34
|
-
# Single [0] and married filing jointly [1].
|
|
35
|
-
|
|
36
|
-
# These are 2025 current.
|
|
37
|
-
taxBrackets_OBBBA = np.array(
|
|
38
|
-
[
|
|
39
|
-
[11925, 48475, 103350, 197300, 250525, 626350, 9999999],
|
|
40
|
-
[23850, 96950, 206700, 394600, 501050, 751600, 9999999],
|
|
41
|
-
]
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
irmaaBrackets = np.array(
|
|
45
|
-
[
|
|
46
|
-
[0, 106000, 133000, 167000, 200000, 500000],
|
|
47
|
-
[0, 212000, 266000, 334000, 400000, 750000],
|
|
48
|
-
]
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
# Index [0] stores the standard Medicare part B premium.
|
|
52
|
-
# Following values are incremental IRMAA part B monthly fees.
|
|
53
|
-
irmaaFees = 12 * np.array([185.00, 74.00, 111.00, 110.90, 111.00, 37.00])
|
|
54
|
-
|
|
55
|
-
# Make projection for pre-TCJA using 2017 to current year.
|
|
56
|
-
# taxBrackets_2017 = np.array(
|
|
57
|
-
# [ [9325, 37950, 91900, 191650, 416700, 418400, 9999999],
|
|
58
|
-
# [18650, 75900, 153100, 233350, 416700, 470700, 9999999],
|
|
59
|
-
# ])
|
|
60
|
-
#
|
|
61
|
-
# stdDeduction_2017 = [6350, 12700]
|
|
62
|
-
#
|
|
63
|
-
# For 2025, I used a 30.5% adjustment from 2017, rounded to closest 50.
|
|
64
|
-
#
|
|
65
|
-
# These are speculated.
|
|
66
|
-
taxBrackets_preTCJA = np.array(
|
|
67
|
-
[
|
|
68
|
-
[12150, 49550, 119950, 250200, 544000, 546200, 9999999], # Single
|
|
69
|
-
[24350, 99100, 199850, 304600, 543950, 614450, 9999999], # MFJ
|
|
70
|
-
]
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# These are 2025 current.
|
|
74
|
-
stdDeduction_OBBBA = np.array([15750, 31500]) # Single, MFJ
|
|
75
|
-
# These are speculated (adjusted for inflation).
|
|
76
|
-
stdDeduction_preTCJA = np.array([8300, 16600]) # Single, MFJ
|
|
77
|
-
|
|
78
|
-
# These are current (adjusted for inflation) per individual.
|
|
79
|
-
extra65Deduction = np.array([2000, 1600]) # Single, MFJ
|
|
80
|
-
|
|
81
|
-
# Thresholds setting capital gains brackets 0%, 15%, 20% (adjusted for inflation).
|
|
82
|
-
capGainRates = np.array(
|
|
83
|
-
[
|
|
84
|
-
[48350, 533400],
|
|
85
|
-
[96700, 600050],
|
|
86
|
-
]
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
# Thresholds for net investment income tax (not adjusted for inflation).
|
|
90
|
-
niitThreshold = np.array([200000, 250000])
|
|
91
|
-
niitRate = 0.038
|
|
92
|
-
|
|
93
|
-
# Thresholds for 65+ bonus for circumventing tax on social security.
|
|
94
|
-
bonusThreshold = np.array([75000, 150000])
|
|
95
|
-
|
|
96
|
-
###############################################################################
|
|
97
|
-
# End of section where rates need to be actualized every year.
|
|
98
|
-
###############################################################################
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def mediVals(yobs, horizons, gamma_n, Nn, Nq):
|
|
102
|
-
"""
|
|
103
|
-
Return tuple (nm, L, C) of year index when Medicare starts and vectors L, and C
|
|
104
|
-
defining end points of constant piecewise linear functions representing IRMAA fees.
|
|
105
|
-
"""
|
|
106
|
-
thisyear = date.today().year
|
|
107
|
-
assert Nq == len(irmaaFees), f"Inconsistent value of Nq: {Nq}."
|
|
108
|
-
assert Nq == len(irmaaBrackets[0]), "Inconsistent IRMAA brackets array."
|
|
109
|
-
Ni = len(yobs)
|
|
110
|
-
# What index year will Medicare start? 65 - age.
|
|
111
|
-
nm = 65 - (thisyear - yobs)
|
|
112
|
-
nm = np.min(nm)
|
|
113
|
-
# Has it already started?
|
|
114
|
-
nm = max(0, nm)
|
|
115
|
-
Nmed = Nn - nm
|
|
116
|
-
|
|
117
|
-
L = np.zeros((Nmed, Nq-1))
|
|
118
|
-
C = np.zeros((Nmed, Nq))
|
|
119
|
-
|
|
120
|
-
# Year starts at offset nm in the plan.
|
|
121
|
-
for nn in range(Nmed):
|
|
122
|
-
imed = 0
|
|
123
|
-
n = nm + nn
|
|
124
|
-
if thisyear + n - yobs[0] >= 65 and n < horizons[0]:
|
|
125
|
-
imed += 1
|
|
126
|
-
if Ni == 2 and thisyear + n - yobs[1] >= 65 and n < horizons[1]:
|
|
127
|
-
imed += 1
|
|
128
|
-
if imed:
|
|
129
|
-
status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
|
|
130
|
-
L[nn] = gamma_n[n] * irmaaBrackets[status][1:]
|
|
131
|
-
C[nn] = imed * gamma_n[n] * irmaaFees
|
|
132
|
-
else:
|
|
133
|
-
raise RuntimeError("mediVals: This should never happen.")
|
|
134
|
-
|
|
135
|
-
return nm, L, C
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def capitalGainTaxRate(Ni, magi_n, gamma_n, nd, Nn):
|
|
139
|
-
"""
|
|
140
|
-
Return an array of decimal rates for capital gains.
|
|
141
|
-
Parameter nd is the index year of first passing of a spouse, if applicable,
|
|
142
|
-
nd == Nn for single individuals.
|
|
143
|
-
"""
|
|
144
|
-
status = Ni - 1
|
|
145
|
-
cgRate_n = np.zeros(Nn)
|
|
146
|
-
|
|
147
|
-
for n in range(Nn):
|
|
148
|
-
if status and n == nd:
|
|
149
|
-
status -= 1
|
|
150
|
-
|
|
151
|
-
if magi_n[n] > gamma_n[n] * capGainRates[status][1]:
|
|
152
|
-
cgRate_n[n] = 0.20
|
|
153
|
-
elif magi_n[n] > gamma_n[n] * capGainRates[status][0]:
|
|
154
|
-
cgRate_n[n] = 0.15
|
|
155
|
-
|
|
156
|
-
return cgRate_n
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def mediCosts(yobs, horizons, magi, prevmagi, gamma_n, Nn):
|
|
160
|
-
"""
|
|
161
|
-
Compute Medicare costs directly.
|
|
162
|
-
"""
|
|
163
|
-
thisyear = date.today().year
|
|
164
|
-
Ni = len(yobs)
|
|
165
|
-
costs = np.zeros(Nn)
|
|
166
|
-
for n in range(Nn):
|
|
167
|
-
status = 0 if Ni == 1 else 1 if n < horizons[0] and n < horizons[1] else 0
|
|
168
|
-
for i in range(Ni):
|
|
169
|
-
if thisyear + n - yobs[i] >= 65 and n < horizons[i]:
|
|
170
|
-
# Start with the (inflation-adjusted) basic Medicare part B premium.
|
|
171
|
-
costs[n] += gamma_n[n] * irmaaFees[0]
|
|
172
|
-
if n < 2:
|
|
173
|
-
mymagi = prevmagi[n]
|
|
174
|
-
else:
|
|
175
|
-
mymagi = magi[n - 2]
|
|
176
|
-
for q in range(1, 6):
|
|
177
|
-
if mymagi > gamma_n[n] * irmaaBrackets[status][q]:
|
|
178
|
-
costs[n] += gamma_n[n] * irmaaFees[q]
|
|
179
|
-
|
|
180
|
-
return costs
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def taxParams(yobs, i_d, n_d, N_n, gamma_n, MAGI_n, yOBBBA=2099):
|
|
184
|
-
"""
|
|
185
|
-
Input is year of birth, index of shortest-lived individual,
|
|
186
|
-
lifespan of shortest-lived individual, total number of years
|
|
187
|
-
in the plan, and the year that preTCJA rates might come back.
|
|
188
|
-
|
|
189
|
-
It returns 3 time series:
|
|
190
|
-
1) Standard deductions at year n (sigma_n).
|
|
191
|
-
2) Tax rate in year n (theta_tn)
|
|
192
|
-
3) Delta from top to bottom of tax brackets (Delta_tn)
|
|
193
|
-
This is pure speculation on future values.
|
|
194
|
-
Returned values are not indexed for inflation.
|
|
195
|
-
"""
|
|
196
|
-
# Compute the deltas in-place between brackets, starting from the end.
|
|
197
|
-
deltaBrackets_OBBBA = np.array(taxBrackets_OBBBA)
|
|
198
|
-
deltaBrackets_preTCJA = np.array(taxBrackets_preTCJA)
|
|
199
|
-
for t in range(6, 0, -1):
|
|
200
|
-
for i in range(2):
|
|
201
|
-
deltaBrackets_OBBBA[i, t] -= deltaBrackets_OBBBA[i, t - 1]
|
|
202
|
-
deltaBrackets_preTCJA[i, t] -= deltaBrackets_preTCJA[i, t - 1]
|
|
203
|
-
|
|
204
|
-
# Prepare the 3 arrays to return - use transpose for easy slicing.
|
|
205
|
-
sigmaBar = np.zeros((N_n))
|
|
206
|
-
Delta = np.zeros((N_n, 7))
|
|
207
|
-
theta = np.zeros((N_n, 7))
|
|
208
|
-
|
|
209
|
-
filingStatus = len(yobs) - 1
|
|
210
|
-
souls = list(range(len(yobs)))
|
|
211
|
-
thisyear = date.today().year
|
|
212
|
-
|
|
213
|
-
for n in range(N_n):
|
|
214
|
-
# First check if shortest-lived individual is still with us.
|
|
215
|
-
if n == n_d:
|
|
216
|
-
souls.remove(i_d)
|
|
217
|
-
filingStatus -= 1
|
|
218
|
-
|
|
219
|
-
if thisyear + n < yOBBBA:
|
|
220
|
-
sigmaBar[n] = stdDeduction_OBBBA[filingStatus] * gamma_n[n]
|
|
221
|
-
Delta[n, :] = deltaBrackets_OBBBA[filingStatus, :]
|
|
222
|
-
else:
|
|
223
|
-
sigmaBar[n] = stdDeduction_preTCJA[filingStatus] * gamma_n[n]
|
|
224
|
-
Delta[n, :] = deltaBrackets_preTCJA[filingStatus, :]
|
|
225
|
-
|
|
226
|
-
# Add 65+ additional exemption(s) and "bonus" phasing out.
|
|
227
|
-
for i in souls:
|
|
228
|
-
if thisyear + n - yobs[i] >= 65:
|
|
229
|
-
sigmaBar[n] += extra65Deduction[filingStatus] * gamma_n[n]
|
|
230
|
-
if thisyear + n <= 2028:
|
|
231
|
-
sigmaBar[n] += 6000 * max(0, 1 - 0.06*max(0, MAGI_n[n] - bonusThreshold[filingStatus]))
|
|
232
|
-
|
|
233
|
-
# Fill in future tax rates for year n.
|
|
234
|
-
if thisyear + n < yOBBBA:
|
|
235
|
-
theta[n, :] = rates_OBBBA[:]
|
|
236
|
-
else:
|
|
237
|
-
theta[n, :] = rates_preTCJA[:]
|
|
238
|
-
|
|
239
|
-
Delta = Delta.transpose()
|
|
240
|
-
theta = theta.transpose()
|
|
241
|
-
|
|
242
|
-
# Return series unadjusted for inflation, except for sigmaBar, in STD order.
|
|
243
|
-
return sigmaBar, theta, Delta
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def taxBrackets(N_i, n_d, N_n, yOBBBA=2099):
|
|
247
|
-
"""
|
|
248
|
-
Return dictionary containing future tax brackets
|
|
249
|
-
unadjusted for inflation for plotting.
|
|
250
|
-
"""
|
|
251
|
-
if not (0 < N_i <= 2):
|
|
252
|
-
raise ValueError(f"Cannot process {N_i} individuals.")
|
|
253
|
-
|
|
254
|
-
n_d = min(n_d, N_n)
|
|
255
|
-
status = N_i - 1
|
|
256
|
-
|
|
257
|
-
# Number of years left in OBBBA from this year.
|
|
258
|
-
thisyear = date.today().year
|
|
259
|
-
if yOBBBA < thisyear:
|
|
260
|
-
raise ValueError(f"Expiration year {yOBBBA} cannot be in the past.")
|
|
261
|
-
|
|
262
|
-
ytc = yOBBBA - thisyear
|
|
263
|
-
|
|
264
|
-
data = {}
|
|
265
|
-
for t in range(len(taxBracketNames) - 1):
|
|
266
|
-
array = np.zeros(N_n)
|
|
267
|
-
for n in range(N_n):
|
|
268
|
-
stat = status if n < n_d else 0
|
|
269
|
-
array[n] = taxBrackets_OBBBA[stat][t] if n < ytc else taxBrackets_preTCJA[stat][t]
|
|
270
|
-
|
|
271
|
-
data[taxBracketNames[t]] = array
|
|
272
|
-
|
|
273
|
-
return data
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def rho_in(yobs, N_n):
|
|
277
|
-
"""
|
|
278
|
-
Return Required Minimum Distribution fractions for each individual.
|
|
279
|
-
This implementation does not support spouses with more than
|
|
280
|
-
10-year difference.
|
|
281
|
-
It starts at age 73 until it goes to 75 in 2033.
|
|
282
|
-
"""
|
|
283
|
-
# Notice that table starts at age 72.
|
|
284
|
-
rmdTable = [
|
|
285
|
-
27.4,
|
|
286
|
-
26.5,
|
|
287
|
-
25.5,
|
|
288
|
-
24.6,
|
|
289
|
-
23.7,
|
|
290
|
-
22.9,
|
|
291
|
-
22.0,
|
|
292
|
-
21.1,
|
|
293
|
-
20.2,
|
|
294
|
-
19.4,
|
|
295
|
-
18.5,
|
|
296
|
-
17.7,
|
|
297
|
-
16.8,
|
|
298
|
-
16.0,
|
|
299
|
-
15.2,
|
|
300
|
-
14.4,
|
|
301
|
-
13.7,
|
|
302
|
-
12.9,
|
|
303
|
-
12.2,
|
|
304
|
-
11.5,
|
|
305
|
-
10.8,
|
|
306
|
-
10.1,
|
|
307
|
-
9.5,
|
|
308
|
-
8.9,
|
|
309
|
-
8.4,
|
|
310
|
-
7.8,
|
|
311
|
-
7.3,
|
|
312
|
-
6.8,
|
|
313
|
-
6.4,
|
|
314
|
-
6.0,
|
|
315
|
-
5.6,
|
|
316
|
-
5.2,
|
|
317
|
-
4.9,
|
|
318
|
-
4.6,
|
|
319
|
-
]
|
|
320
|
-
|
|
321
|
-
N_i = len(yobs)
|
|
322
|
-
if N_i == 2 and abs(yobs[0] - yobs[1]) > 10:
|
|
323
|
-
raise RuntimeError("RMD: Unsupported age difference of more than 10 years.")
|
|
324
|
-
|
|
325
|
-
rho = np.zeros((N_i, N_n))
|
|
326
|
-
thisyear = date.today().year
|
|
327
|
-
for i in range(N_i):
|
|
328
|
-
agenow = thisyear - yobs[i]
|
|
329
|
-
# Account for increase of RMD age between 2023 and 2032.
|
|
330
|
-
yrmd = 70 if yobs[i] < 1949 else 72 if 1949 <= yobs[i] <= 1950 else 73 if 1951 <= yobs[i] <= 1959 else 75
|
|
331
|
-
for n in range(N_n):
|
|
332
|
-
yage = agenow + n
|
|
333
|
-
|
|
334
|
-
if yage < yrmd:
|
|
335
|
-
pass # rho[i][n] = 0
|
|
336
|
-
else:
|
|
337
|
-
rho[i][n] = 1.0 / rmdTable[yage - 72]
|
|
338
|
-
|
|
339
|
-
return rho
|