owlplanner 2025.5.3__py3-none-any.whl → 2025.5.12__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/rates.py CHANGED
@@ -35,7 +35,7 @@ import os
35
35
  import sys
36
36
  from datetime import date
37
37
 
38
- from owlplanner import logging
38
+ from owlplanner import mylogging as log
39
39
  from owlplanner import utils as u
40
40
 
41
41
  # All data goes from 1928 to 2024. Update the TO value when data
@@ -48,7 +48,7 @@ file = os.path.join(where, "data/rates.csv")
48
48
  try:
49
49
  df = pd.read_csv(file)
50
50
  except Exception as e:
51
- raise RuntimeError(f"Could not find rates data file: {e}")
51
+ raise RuntimeError(f"Could not find rates data file: {e}") from e
52
52
 
53
53
 
54
54
  # Annual rate of return (%) of S&P 500 since 1928, including dividends.
@@ -77,14 +77,17 @@ def getRatesDistributions(frm, to, mylog=None):
77
77
  the different rates. Function returns means and covariance matrix.
78
78
  """
79
79
  if mylog is None:
80
- mylog = logging.Logger()
80
+ mylog = log.Logger()
81
81
 
82
82
  # Convert years to index and check range.
83
83
  frm -= FROM
84
84
  to -= FROM
85
- assert 0 <= frm and frm <= len(SP500), 'Range "from" out of bounds.'
86
- assert 0 <= to and to <= len(SP500), 'Range "to" out of bounds.'
87
- assert frm <= to, '"from" must be smaller than "to".'
85
+ if not (0 <= frm and frm <= len(SP500)):
86
+ raise ValueError(f"Range 'from' {frm} out of bounds.")
87
+ if not (0 <= to and to <= len(SP500)):
88
+ raise ValueError(f"Range 'to' {to} out of bounds.")
89
+ if frm >= to:
90
+ raise ValueError(f'"from" {frm} must be smaller than "to" {to}.')
88
91
 
89
92
  series = {
90
93
  "SP500": SP500,
@@ -124,9 +127,12 @@ def historicalValue(amount, year):
124
127
  valued at the beginning of the year specified.
125
128
  """
126
129
  thisyear = date.today().year
127
- assert TO == thisyear - 1, f"Rates file needs to be updated to be current to {thisyear}."
128
- assert year >= FROM, f"Only data from {FROM} is available."
129
- assert year <= thisyear, f"Year must be < {thisyear} for historical data."
130
+ if TO != thisyear - 1:
131
+ raise RuntimeError(f"Rates file needs to be updated to be current to {thisyear}.")
132
+ if year < FROM:
133
+ raise ValueError(f"Only data from {FROM} is available.")
134
+ if year > thisyear:
135
+ raise ValueError(f"Year must be < {thisyear} for historical data.")
130
136
 
131
137
  span = thisyear - year
132
138
  ub = len(Inflation)
@@ -153,7 +159,7 @@ class Rates(object):
153
159
  Default constructor.
154
160
  """
155
161
  if mylog is None:
156
- self.mylog = logging.Logger()
162
+ self.mylog = log.Logger()
157
163
  else:
158
164
  self.mylog = mylog
159
165
 
@@ -223,18 +229,24 @@ class Rates(object):
223
229
  self.mylog.vprint("Using conservative fixed rates values:", *[u.pc(k) for k in self.means])
224
230
  self._setFixedRates(self._conservRates)
225
231
  elif method == "user":
226
- assert values is not None, "Fixed values must be provided with the user option."
227
- assert len(values) == Nk, f"Values must have {Nk} items."
232
+ if values is None:
233
+ raise ValueError("Fixed values must be provided with the user option.")
234
+ if len(values) != Nk:
235
+ raise ValueError(f"Values must have {Nk} items.")
228
236
  self.means = np.array(values, dtype=float)
229
237
  # Convert percent to decimal for storing.
230
238
  self.means /= 100.0
231
239
  self.mylog.vprint("Setting rates using fixed user values:", *[u.pc(k) for k in self.means])
232
240
  self._setFixedRates(self.means)
233
241
  elif method == "stochastic":
234
- assert values is not None, "Mean values must be provided with the stochastic option."
235
- assert stdev is not None, "Standard deviations must be provided with the stochastic option."
236
- assert len(values) == Nk, f"Values must have {Nk} items."
237
- assert len(stdev) == Nk, f"stdev must have {Nk} items."
242
+ if values is None:
243
+ raise ValueError("Mean values must be provided with the stochastic option.")
244
+ if stdev is None:
245
+ raise ValueError("Standard deviations must be provided with the stochastic option.")
246
+ if len(values) != Nk:
247
+ raise ValueError(f"Values must have {Nk} items.")
248
+ if len(stdev) != Nk:
249
+ raise ValueError(f"stdev must have {Nk} items.")
238
250
  self.means = np.array(values, dtype=float)
239
251
  self.stdev = np.array(stdev, dtype=float)
240
252
  # Convert percent to decimal for storing.
@@ -262,7 +274,8 @@ class Rates(object):
262
274
  raise RuntimeError(f"Unable to process correlation shape of {corrarr.shape}.")
263
275
 
264
276
  self.corr = corrarr
265
- assert np.array_equal(self.corr, self.corr.T), "Correlation matrix must be symmetric."
277
+ if not np.array_equal(self.corr, self.corr.T):
278
+ raise ValueError("Correlation matrix must be symmetric.")
266
279
  # Now build covariance matrix from stdev and correlation matrix.
267
280
  # Multiply each row by a vector element-wise. Then columns.
268
281
  covar = self.corr * self.stdev
@@ -273,10 +286,14 @@ class Rates(object):
273
286
  self.mylog.vprint("\t and correlation matrix:\n\t\t", str(self.corr).replace("\n", "\n\t\t"))
274
287
  else:
275
288
  # Then methods relying on historical data range.
276
- assert frm is not None, "From year must be provided with this option."
277
- assert FROM <= frm and frm <= TO, f"Lower range 'frm={frm}' out of bounds."
278
- assert FROM <= to and to <= TO, f"Upper range 'to={to}' out of bounds."
279
- assert frm < to, "Unacceptable range."
289
+ if frm is None:
290
+ raise ValueError("From year must be provided with this option.")
291
+ if not (FROM <= frm <= TO):
292
+ raise ValueError(f"Lower range 'frm={frm}' out of bounds.")
293
+ if not (FROM <= to <= TO):
294
+ raise ValueError(f"Upper range 'to={to}' out of bounds.")
295
+ if not (frm < to):
296
+ raise ValueError("Unacceptable range.")
280
297
  self.frm = frm
281
298
  self.to = to
282
299
 
@@ -300,7 +317,8 @@ class Rates(object):
300
317
 
301
318
  def _setFixedRates(self, rates):
302
319
  Nk = len(self._defRates)
303
- assert len(rates) == Nk, f"Rate list provided must have {Nk} entries."
320
+ if len(rates) != Nk:
321
+ raise ValueError(f"Rate list provided must have {Nk} entries.")
304
322
  self._myRates = np.array(rates)
305
323
  self._rateMethod = self._fixedRates
306
324
 
@@ -364,49 +382,3 @@ class Rates(object):
364
382
  srates = np.random.multivariate_normal(self.means, self.covar)
365
383
 
366
384
  return srates
367
-
368
-
369
- def showRatesDistributions(frm=FROM, to=TO):
370
- """
371
- Plot histograms of the rates distributions.
372
- """
373
- import matplotlib.pyplot as plt
374
-
375
- title = "Rates from " + str(frm) + " to " + str(to)
376
- # Bring year values to indices.
377
- frm -= FROM
378
- to -= FROM
379
-
380
- nbins = int((to - frm) / 4)
381
- fig, ax = plt.subplots(1, 4, sharey=True, sharex=True, tight_layout=True)
382
-
383
- dat0 = np.array(SP500[frm:to])
384
- dat1 = np.array(BondsBaa[frm:to])
385
- dat2 = np.array(TNotes[frm:to])
386
- dat3 = np.array(Inflation[frm:to])
387
-
388
- fig.suptitle(title)
389
- ax[0].set_title("S&P500")
390
- label = "<>: " + u.pc(np.mean(dat0), 2, 1)
391
- ax[0].hist(dat0, bins=nbins, label=label)
392
- ax[0].legend(loc="upper left", fontsize=8, framealpha=0.7)
393
-
394
- ax[1].set_title("BondsBaa")
395
- label = "<>: " + u.pc(np.mean(dat1), 2, 1)
396
- ax[1].hist(dat1, bins=nbins, label=label)
397
- ax[1].legend(loc="upper left", fontsize=8, framealpha=0.7)
398
-
399
- ax[2].set_title("TNotes")
400
- label = "<>: " + u.pc(np.mean(dat2), 2, 1)
401
- ax[2].hist(dat1, bins=nbins, label=label)
402
- ax[2].legend(loc="upper left", fontsize=8, framealpha=0.7)
403
-
404
- ax[3].set_title("Inflation")
405
- label = "<>: " + u.pc(np.mean(dat3), 2, 1)
406
- ax[3].hist(dat3, bins=nbins, label=label)
407
- ax[3].legend(loc="upper left", fontsize=8, framealpha=0.7)
408
-
409
- plt.show()
410
-
411
- # return fig, ax
412
- return None
owlplanner/tax2025.py CHANGED
@@ -172,7 +172,8 @@ def taxBrackets(N_i, n_d, N_n, y_TCJA):
172
172
  Return dictionary containing future tax brackets
173
173
  unadjusted for inflation for plotting.
174
174
  """
175
- assert 0 < N_i and N_i <= 2, f"Cannot process {N_i} individuals."
175
+ if not (0 < N_i <= 2):
176
+ raise ValueError(f"Cannot process {N_i} individuals.")
176
177
  n_d = min(n_d, N_n)
177
178
  status = N_i - 1
178
179
 
owlplanner/timelists.py CHANGED
@@ -55,7 +55,7 @@ def read(finput, inames, horizons, mylog):
55
55
  try:
56
56
  dfDict = pd.read_excel(finput, sheet_name=None)
57
57
  except Exception as e:
58
- raise Exception(f"Could not read file {finput}: {e}.")
58
+ raise Exception(f"Could not read file {finput}: {e}.") from e
59
59
  streamName = f"file '{finput}'"
60
60
 
61
61
  timeLists = condition(dfDict, inames, horizons, mylog)
@@ -83,7 +83,7 @@ def condition(dfDict, inames, horizons, mylog):
83
83
  df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
84
84
  for col in df.columns:
85
85
  if col == "" or col not in timeHorizonItems:
86
- df.drop(col, axis=1)
86
+ df.drop(col, axis=1, inplace=True)
87
87
 
88
88
  for item in timeHorizonItems:
89
89
  if item not in df.columns:
@@ -114,12 +114,7 @@ def condition(dfDict, inames, horizons, mylog):
114
114
 
115
115
  if df["year"].iloc[-1] != endyear - 1:
116
116
  raise ValueError(
117
- "Time horizon for",
118
- iname,
119
- "is too short.\n\tIt should end in",
120
- endyear,
121
- "but ends in",
122
- df["year"].iloc[-1],
117
+ f"Time horizon for {iname} too short.\n\tIt should end in {endyear}, not {df['year'].iloc[-1]}"
123
118
  )
124
119
 
125
120
  return timeLists
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.05.03"
1
+ __version__ = "2025.05.12"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.5.3
3
+ Version: 2025.5.12
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
@@ -695,6 +695,7 @@ Requires-Dist: matplotlib
695
695
  Requires-Dist: numpy
696
696
  Requires-Dist: openpyxl
697
697
  Requires-Dist: pandas
698
+ Requires-Dist: plotly
698
699
  Requires-Dist: scipy
699
700
  Requires-Dist: seaborn
700
701
  Requires-Dist: streamlit
@@ -711,15 +712,17 @@ Description-Content-Type: text/markdown
711
712
  -------------------------------------------------------------------------------------
712
713
 
713
714
  ### TL;DR
714
- Owl is a retirement planning tool that uses a linear programming optimization algorithm
715
- to provide guidance on retirement decisions, including Roth conversions.
715
+ Owl is a financial retirement planning tool that uses a linear programming
716
+ optimization algorithm to provide guidance on retirement decisions
717
+ such as contributions, withdrawals, Roth conversions, and more.
716
718
  Users can select varying return rates to perform historical back testing,
717
719
  stochastic rates for performing Monte Carlo analyses,
718
720
  or fixed rates either derived from historical averages, or set by the user.
719
721
 
720
722
  There are a few ways to run Owl:
721
723
 
722
- - Run Owl directly on the Streamlit Community Server at [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
724
+ - Run Owl directly on the Streamlit Community Server at
725
+ [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
723
726
 
724
727
  - Run locally on your computer using a Docker image.
725
728
  Follow these [instructions](docker/README.md) for this option.
@@ -729,7 +732,7 @@ Follow these [instructions](INSTALL.md) to install Owl from the source code and
729
732
 
730
733
  -------------------------------------------------------------------------------------
731
734
  ## Overview
732
- This package is a retirement modeling framework for exploring the sensitivity of retirement financial decisions.
735
+ This package is a modeling framework for exploring the sensitivity of retirement financial decisions.
733
736
  Strictly speaking, it is not a planning tool, but more an environment for exploring *what if* scenarios.
734
737
  It provides different realizations of a financial strategy through the rigorous
735
738
  mathematical optimization of relevant decision variables. Two major objective goals can be set: either
@@ -0,0 +1,22 @@
1
+ owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
2
+ owlplanner/abcapi.py,sha256=m0vtoEzz9HJV7fOK_d7OnK7ha2Qbf7wLLPCJ9YZzR1k,6851
3
+ owlplanner/config.py,sha256=qIWzj3Tbz_jhhFqIkaMzXzgWQBN4Uk2km_VIMZSh910,12559
4
+ owlplanner/mylogging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
5
+ owlplanner/plan.py,sha256=_Oq2UomWzteYVlT6zJC_A2zXO3YVlXG9xgUMcQfWBJg,106742
6
+ owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
7
+ owlplanner/rates.py,sha256=MiaibxJY82JGpAhGyF2BJTm5-rmVAUuG8KLApVQhjvU,14816
8
+ owlplanner/tax2025.py,sha256=wmlZpYeeGNrbyn5g7wOFqhWbggppodtHqc-ex5XRooI,7850
9
+ owlplanner/timelists.py,sha256=wNYnJqxJ6QqE6jHh5lfFqYngfw5wUFrI15LSsM5ae8s,3949
10
+ owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
11
+ owlplanner/version.py,sha256=5ZqN4YDTyjz76bfj2w5OuF_20x5Fau01NRLJ8nXtxx8,28
12
+ owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
+ owlplanner/plotting/__init__.py,sha256=VnF6ui78YrTrg1dA6hBIdI02ahzEaHVR3ZEdDe_i880,103
15
+ owlplanner/plotting/base.py,sha256=1bU6iM1pGIE-l9p0GuulX4gRK_7ds96784Wb5oVUUR0,2449
16
+ owlplanner/plotting/factory.py,sha256=i1k8m_ISnJw06f_JWlMvOQ7Q0PgV_BoLm05uLwFPvOQ,883
17
+ owlplanner/plotting/matplotlib_backend.py,sha256=-M4Am7N0D8Nfv_tKNA1TneFYU_DuW_ZsoUBHRQWD_ok,17887
18
+ owlplanner/plotting/plotly_backend.py,sha256=LUmeWYAA_F1vdl-GPehzprVx3oFWehGmc1Oir-kNJ_w,33093
19
+ owlplanner-2025.5.12.dist-info/METADATA,sha256=w0R8yDGSwrRGCouu2Hbe0Cx4tvQInuwm6VcvHcjFN9Q,53983
20
+ owlplanner-2025.5.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ owlplanner-2025.5.12.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
22
+ owlplanner-2025.5.12.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- owlplanner/__init__.py,sha256=QqrdT0Qks20osBTg7h0vJHAxpP9lL7DA99xb0nYbtw4,254
2
- owlplanner/abcapi.py,sha256=Lt8OUgbrfOPzAw0HyxyT2wT-IXI3d9Zo26MwyqdX56Y,6617
3
- owlplanner/config.py,sha256=F6GS3n02VeFX0GCVeM4J7Ra0in4N632W6TZIXk7Yj2w,12519
4
- owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
5
- owlplanner/plan.py,sha256=mKSfHqFdG8P9Dlse-VbTnWSEgO2ZDcqAfK3gHYNEIho,119112
6
- owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
7
- owlplanner/rates.py,sha256=gJaoe-gJqWCQV5qVLlHp-Yn9TSJs-PJzeTbOwMCbqWs,15682
8
- owlplanner/tax2025.py,sha256=JDBtFFAf2bWtKUMuE3W5F0nBhYaKBjmdJj0iayM2iGA,7829
9
- owlplanner/timelists.py,sha256=tYieZU67FT6TCcQQis36JaXGI7dT6NqD7RvdEjgJL4M,4026
10
- owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
11
- owlplanner/version.py,sha256=NZ889ANsXH5BzjEmfYGvqRdWan3eQSUgEGwPnMds764,28
12
- owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
- owlplanner-2025.5.3.dist-info/METADATA,sha256=eGnBmMVTpNCvwzG153HRjN1rzzrD0zu1hQn1tV0SroA,53926
15
- owlplanner-2025.5.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- owlplanner-2025.5.3.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
17
- owlplanner-2025.5.3.dist-info/RECORD,,
File without changes