Skip to content

Commit

Permalink
add test on clone
Browse files Browse the repository at this point in the history
  • Loading branch information
mdlacasse committed Dec 11, 2024
1 parent 126df26 commit 37dac35
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 23 deletions.
26 changes: 12 additions & 14 deletions examples/tutorial_1.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,7 @@
"\n",
"or just Google life expectancy calculator.\n",
"\n",
"There are two values needed for couples. Single individuals just enter one value in each list between square brackets `[ ]`. For couples, always keep the same order in the pair of values when entering the data.\n",
"\n",
"When first creating a plan, the default values used for assets allocation ratios and rates of return will be reported. These values are listed to make you aware that these values would be used if no other choice is entered. We will cover how to do this in the next sections."
"There are two values needed for couples. Single individuals just enter one value in each list between square brackets `[ ]`. For couples, always keep the same order in the pair of values when entering the data.\n"
]
},
{
Expand Down Expand Up @@ -183,7 +181,7 @@
"id": "6ea4c5dc-f88d-47c5-a1ec-3474d023bf23",
"metadata": {},
"source": [
"Jack has \\\\$90 k in his taxable account, \\\\$600 k in his 401k, and $50 k in his Roth 401k and \\\\$20 k in Roth IRAs. Jill has \\\\$60 k in her savings bank account, \\\\$150 k in a 403b, and \\\\$40 k in Roth IRAs in which she contributed over the years. Units for \\\\$ are in thousands by default. You can use the `units` optional keyword to indicate that the numbers are entered in other units, such as dollars (`1`) or millions (`M`). Notice that being in Python, arithmetic can be used while entering numbers."
"Jack has \\\\$90.5 k in his taxable account, \\\\$600.2 k in his 401k, and $50 k in his Roth 401k and \\\\$20.6 k in Roth IRAs. Jill has \\\\$60 k in her savings bank account, \\\\$150 k in a 403b, and \\\\$40.8 k in Roth IRAs in which she contributed over the years. Units for \\\\$ are in thousands by default. You can use the `units` optional keyword to indicate that the numbers are entered in other units, such as dollars (`1`) or millions (`M`). Notice that being in Python, arithmetic can be used while entering numbers."
]
},
{
Expand All @@ -194,9 +192,9 @@
"outputs": [],
"source": [
"plan.setAccountBalances(\n",
" taxable=[90, 60],\n",
" taxDeferred=[600, 150],\n",
" taxFree=[50 + 20, 40],\n",
" taxable=[90.5, 60],\n",
" taxDeferred=[600.2, 150],\n",
" taxFree=[50 + 20.6, 40.8],\n",
")"
]
},
Expand Down Expand Up @@ -388,7 +386,7 @@
"\n",
"The desired spending defined here is the minimum annual **net** spending income (i.e., after paying federal income tax and Medicare) that one would like to receive starting at her/his \"retirement age\" (we provided a loose definition of the term *retirement age* above). This desired spending must be adjusted for inflation and can follow an additional adjustment called a *smile* profile. A *smile* profile accounts for the fact that your spending capacity will modulate during retirement as you go from the so-called gogo years to the no-go years. A *flat* profile, on the other hand, will keep the same value, which will only be adjusted for inflation. More realistically, the income could also be modulated by the performance of assets, reducing withdrawals in market down years. Some of these strategies (first proposed by Guyton and Klinger) could be implemented in Owl.\n",
"\n",
"These profiles are multipliers to a spending amount to be set, or obtained from an optimization. For example, a flat profile for a single individual would be unity ($1$) for the whole duration of the plan. In contrast, a smile profile would start at a value larger than $1$ and then decrease over the years to a value smaller than unity, then to go above unity toward the end of the plan. The actual spending amount will be determined either by optimization (for `maxSpending`) or be provided by the user (for `maxBequest`). The final spending amount is a *basis* multiplied by the profile. These values are the same for a flat profile, but will differ for a *smile* profile.\n",
"These profiles are multipliers to a spending amount to be set, or obtained from an optimization. For example, a flat profile for a single individual would be unity ($1$) for the whole duration of the plan. In contrast, a smile profile would start at a value larger than $1$ and then decrease over the years to a value smaller than unity, then to go above unity toward the end of the plan. The actual spending amount will be determined either by optimization (for `maxSpending`) or be provided by the user (for `maxBequest`). The final spending amount is a *basis* multiplied by the profile. The basis and the net spending amount are the same for a flat profile, but will differ for a *smile* profile.\n",
"\n",
"A second optional argument can be provided to specify the fraction of spending left to the surviving spouse. The default is 60%.\n",
"\n",
Expand Down Expand Up @@ -565,9 +563,9 @@
"metadata": {},
"source": [
"##### Show historical rate distribution\n",
"Since Owl has the historical rates available, one can also display their histograms using a simple function call. This is done with `owl.showRatesDistributions()`. The symbol '<>' means average.\n",
"Since Owl has the historical rates available, one can also display their histograms using a simple function call. This is done with `owl.showRatesDistributions()`.\n",
"\n",
"Given the standard deviation of each histogram, the risk/benefit between stocks and bonds is clear. Let's look at the rates distribution over the 30-year period running from 1969 to 1999. Notice how high is the average inflation."
"Given the standard deviation of each histogram, the risk/benefit between stocks and bonds is clear. Let's look at the rates distribution over the 30-year period running from 1969 to 1999. Notice how high is the average inflation. The symbol '<>' means everage."
]
},
{
Expand All @@ -585,7 +583,7 @@
"id": "66f25114",
"metadata": {},
"source": [
"When *stochastic* or *histochastic* rates are used, the distributions and correlations betwen the different rates can be displayed by using the following method, (which can optionally share the same percentage range across all investments). The diagonal represents a histogram of obsesrved values, the upper diagonal the values themselves, and the lower diagonal a representation of the kernel distribution estimates (KDEs)."
"When *stochastic* or *histochastic* rates are used, the distributions and correlations betwen the different rates can be displayed by using the following method, (which can optionally share the same percentage range across all investments). The diagonal represents a histogram of observed values, the upper diagonal the values themselves, and the lower diagonal a representation of the kernel distribution estimates (KDEs)."
]
},
{
Expand Down Expand Up @@ -721,7 +719,7 @@
"\n",
" plan.summary()\n",
" \n",
"which returns the value of the assets in today's \\\\$ at the last year of the scenario, assuming (in this case) a 30\\% tax burden on the taxable portion of the bequeathed estate (read tax-deferred savings accounts). The `summary()` function returns two values: the total post-tax value of all savings account in today's dollars and the cumulative inflation rate between today and the last day of the realization."
"which returns the value of the assets in today's \\\\$ at the last year of the scenario, assuming (in this case) a 30\\% tax burden on the taxable portion of the bequeathed estate (read tax-deferred savings accounts). The `summary()` function returns multiple values. These include the total post-tax value of all savings account in today's dollars, the cumulative inflation rate between today and the last day of the realization and much more."
]
},
{
Expand Down Expand Up @@ -763,7 +761,7 @@
"metadata": {},
"source": [
"#### Show net spending compared to target spending over the years\n",
"This graph shows how the actual net spending generated by the plan realization matches the inflation-adjusted net spending profile specified. Note the 40% drop in target spending as one spouse passes. This drop is configurable through the `setProfile()` method. Default value is 60%."
"This graph shows how the actual net spending generated by the plan realization matches the inflation-adjusted net spending profile specified. Note the 40% drop in target spending as one spouse passes. This drop is configurable through the `setProfile()` method."
]
},
{
Expand Down Expand Up @@ -889,7 +887,7 @@
"id": "9231c810-31db-45aa-98f1-6632f717d375",
"metadata": {},
"source": [
"Here, this call will create an excel workbook with one spreadsheet (tab) for Jack and one for Jill. \n",
"Here, this call will create an excel workbook with multiple spreadsheets (tab) representing the data for Jack and Jill. \n",
"\n",
"Open the file in Excel to see what it looks like. "
]
Expand Down
10 changes: 5 additions & 5 deletions examples/tutorial_2.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@
"outputs": [],
"source": [
"plan.setAccountBalances(\n",
" taxable=[90, 60],\n",
" taxDeferred=[600, 150],\n",
" taxFree=[50 + 20, 40],\n",
" taxable=[90.5, 60],\n",
" taxDeferred=[600.2, 150],\n",
" taxFree=[50 + 20.6, 40.8],\n",
")"
]
},
Expand Down Expand Up @@ -178,8 +178,8 @@
"\n",
" plan.setRates('histochastic', 1970, 2023)\n",
"\n",
"This example will extract the statistical features of annual returns which happened from 1970 to 2023 inclusively. Using this method, the means and covariances are all set under the hood, and the user has nothing more to specify. \n",
"The means and covariance matrix calculated from this time series are then used to generate a series with similar characteristics. Note that the approach used assumes that the rates follow a normal distribution which is a known incorrect approximation. This implies that the distribution of events in the tails might be slightly incorrect but for the time being, there is no known distribution that will capture the tails more accurately. The mean values however, are more robust to our choice of distribution. \n",
"As called, this method will extract the statistical features of annual returns which happened from 1970 to 2023 inclusively. Using this approach, the means and covariances are all set under the hood, and the user has nothing more to specify. \n",
"The means and covariance matrix calculated from this time series are then used to generate a series with similar characteristics. Note that the mathematical model used assumes that the rates follow a normal distribution which is a known incorrect approximation. This implies that the distribution of events in the tails might be slightly incorrect but for the time being, there is no known distribution that will capture the tails more accurately. The mean values however, are more robust to our choice of distribution, due to a averaging effect known as the central limit theorem. \n",
"\n",
"##### The *stochastic* method\n",
"The means and covariance matrix can also be explicitly specified by the user. Let's look at a specific example:\n",
Expand Down
8 changes: 4 additions & 4 deletions examples/tutorial_3.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@
"outputs": [],
"source": [
"plan.setAccountBalances(\n",
" taxable=[90, 60],\n",
" taxDeferred=[600, 150],\n",
" taxFree=[50 + 20, 40],\n",
" taxable=[90.5, 60],\n",
" taxDeferred=[600.2, 150],\n",
" taxFree=[50 + 20.6, 40.8],\n",
")"
]
},
Expand Down Expand Up @@ -318,7 +318,7 @@
"id": "8553d877-c44c-43f0-a015-9055f7d76e25",
"metadata": {},
"source": [
"*Partial* is the post-tax partial bequest left to individuals others than Jill at the passing of Jack, in that case, Jack's children. The range of values obtained in solutions is also reported as well as the number of solutions with 0 as an answer. The amount that Jack leaves to his children can vary from \\\\$0 to about \\\\$1 M, and shows the largest variability, explaining the large difference between the median and the average. Mean values are indicated by $\\bar{x}$. What strikes here the the number of solutions which have Jack's account depleted. we will discuss these cases.\n",
"*Partial* is the post-tax partial bequest left to individuals others than Jill at the passing of Jack, in that case, Jack's children. The range of values obtained in solutions is also reported as well as the number of solutions with 0 as an answer. The amount that Jack leaves to his children can vary from \\\\$0 to about \\\\$1 M, and shows the largest variability, explaining the large difference between the median and the average. Mean values are indicated by $\\bar{x}$. What strikes here the the number of solutions which have Jack's account depleted. We will discuss these cases.\n",
"\n",
"Almost all cases are successful at leaving a \\\\$200k final bequest, but the net spending required for achieving this constraint can lead to small values in some scenarios. However, the median value $M$ for net spending is about \\\\$88k, very close to the \\\\$90k desired by Jack and Jill. Recall that we are using a more conservative return of 8%, with a volatility of 17% for the S&P 500. It is therefore natural that the number we find here is slightly lower than what the historical returns of the market have been observed to provide. Some of these scenarios will yield more than the median, but some others will require a net spending of less than \\\\$50k. All these scenarios make a probability of close to 100\\% for leaving a \\\\$200k bequest at Jill's passing as requested. This is in addition to a median of about \\\\$20k left at Jack's passing. We can see that the mean value left by Jack to his children is around \\\\$130k while the median is much lower, due to many scenarios depleting Jack's account at his passing. What is to note is that several scenarios deplete Jack's account to reduce the partial bequest in view of maximizing the net spending. This is the action of the optimization working to maximize its objective function, which is the net spending in this particular case. That is, the optimizer will select scenarios where the net spending will be increased at the expense of decreasing the partial bequest. What can potentially still hold money in Jack's account and allow to provide a partial bequest is the restriction imposed by satisfying a certain spending profile which limits the depletion of the accounts. That is, more gains can be realized in certain scenarios that the ability to spend.\n",
"\n",
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ exclude = [
"build",
"dist",
"examples",
"*.ipynb",
]
extend-exclude = ["*.ipynb"]

[tool.ruff.lint]
# 1. Enable flake8-bugbear (`B`) rules, in addition to the defaults.
Expand Down
28 changes: 28 additions & 0 deletions tests/test_repro.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,31 @@ def test_config():
assert p2.basis == pytest.approx(80000, abs=0.5)
assert p2.bequest == pytest.approx(606235, abs=0.5)
os.remove(filename)


def test_clone1():
name = 'testclone1'
p = createJackAndJillPlan(name)
p.setRates('historical', 1969)
p.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
assert p.basis == pytest.approx(80000, abs=0.5)
assert p.bequest == pytest.approx(606235, abs=0.5)
name2= 'testclone2'
p2 = owl.clone(p, name2)
p2.solve('maxBequest', options={'maxRothConversion': 100, 'netSpending': 80})
assert p2.basis == pytest.approx(80000, abs=0.5)
assert p2.bequest == pytest.approx(606235, abs=0.5)


def test_clone2():
name = 'testclone1'
p = createJackAndJillPlan(name)
p.setRates('historical', 1969)
p.solve('maxSpending', options={'maxRothConversion': 100, 'bequest': 0})
assert p.basis == pytest.approx(92543, abs=0.5)
assert p.bequest == pytest.approx(0, abs=0.5)
name2= 'testclone2'
p2 = owl.clone(p, name2)
p2.solve('maxSpending', options={'maxRothConversion': 100, 'bequest': 0})
assert p2.basis == pytest.approx(92543, abs=0.5)
assert p2.bequest == pytest.approx(0, abs=0.5)

0 comments on commit 37dac35

Please sign in to comment.