owlplanner 2025.12.20__py3-none-any.whl → 2026.2.2__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.
@@ -1,10 +1,23 @@
1
1
  """
2
- Plotting backends for Owl.
2
+ Plotting backends package for Owl retirement planner.
3
3
 
4
- Copyright © 2025 - Martin-D. Lacasse
4
+ This package provides a factory pattern for creating plot backends (matplotlib,
5
+ plotly) for visualizing retirement planning results.
5
6
 
6
- Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
8
 
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
8
21
  """
9
22
 
10
23
  from .factory import PlotFactory
@@ -1,10 +1,24 @@
1
1
  """
2
- Base classes for plot backends.
2
+ Abstract base classes for plot backends.
3
3
 
4
- Copyright &copy; 2025 - Martin-D. Lacasse
4
+ This module defines the abstract base class interface that all plotting
5
+ backends must implement for consistent plotting functionality across
6
+ different visualization libraries.
5
7
 
6
- Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
8
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
9
 
10
+ This program is free software: you can redistribute it and/or modify
11
+ it under the terms of the GNU General Public License as published by
12
+ the Free Software Foundation, either version 3 of the License, or
13
+ (at your option) any later version.
14
+
15
+ This program is distributed in the hope that it will be useful,
16
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ GNU General Public License for more details.
19
+
20
+ You should have received a copy of the GNU General Public License
21
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
8
22
  """
9
23
 
10
24
  from abc import ABC, abstractmethod
@@ -1,10 +1,23 @@
1
1
  """
2
- Factory for creating plot backends.
2
+ Factory for creating plot backend instances.
3
3
 
4
- Copyright &copy; 2025 - Martin-D. Lacasse
4
+ This module provides a factory class to create plot backends (matplotlib or
5
+ plotly) based on the specified backend type.
5
6
 
6
- Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
8
 
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
8
21
  """
9
22
 
10
23
  from .base import PlotBackend
@@ -1,10 +1,23 @@
1
1
  """
2
- Matplotlib implementation of plot backend.
2
+ Matplotlib backend implementation for plotting retirement planning results.
3
3
 
4
- Copyright &copy; 2025 - Martin-D. Lacasse
4
+ This module provides the Matplotlib-based implementation of the plot backend
5
+ interface for creating static visualizations of retirement planning data.
5
6
 
6
- Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
8
 
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
8
21
  """
9
22
 
10
23
  import numpy as np
@@ -65,10 +78,20 @@ class MatplotlibBackend(PlotBackend):
65
78
  """Core function for stacked plots."""
66
79
  nonzeroSeries = {}
67
80
  for sname in snames:
68
- for i in irange:
69
- tmp = series[sname][i]
70
- if sum(tmp) > 1.0:
71
- nonzeroSeries[sname + " " + inames[i]] = tmp
81
+ source_data = series[sname]
82
+ # Check if this is a household-level source (shape (1, N_n) when N_i > 1)
83
+ is_household = source_data.shape[0] == 1 and len(inames) > 1
84
+ if is_household:
85
+ # Show household total once without individual name
86
+ tmp = source_data[0]
87
+ if abs(sum(tmp)) > 1.0: # Use abs for debts
88
+ nonzeroSeries[sname] = tmp
89
+ else:
90
+ # Show per individual
91
+ for i in irange:
92
+ tmp = source_data[i]
93
+ if abs(sum(tmp)) > 1.0: # Use abs for debts
94
+ nonzeroSeries[sname + " " + inames[i]] = tmp
72
95
 
73
96
  if len(nonzeroSeries) == 0:
74
97
  return None, None
@@ -1,10 +1,23 @@
1
1
  """
2
- Plotly implementation of plot backend.
2
+ Plotly backend implementation for plotting retirement planning results.
3
3
 
4
- Copyright &copy; 2025 - Martin-D. Lacasse
4
+ This module provides the Plotly-based implementation of the plot backend
5
+ interface for creating interactive visualizations of retirement planning data.
5
6
 
6
- Disclaimers: This code is for educatonal purposes only and does not constitute financial advice.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
8
 
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
8
21
  """
9
22
 
10
23
  import numpy as np
@@ -749,7 +762,7 @@ class PlotlyBackend(PlotBackend):
749
762
 
750
763
  # Add each individual's data as a separate series
751
764
  for i in range(len(inames)):
752
- if np.sum(values[i]) > 1.0: # Only show non-zero series
765
+ if np.abs(np.sum(values[i])) > 1.0: # Only show non-zero series (use abs for debts)
753
766
  stack_data.append((values[i], f"{namek} {inames[i]}"))
754
767
 
755
768
  # Add stacked area traces
@@ -887,7 +900,7 @@ class PlotlyBackend(PlotBackend):
887
900
  for sname in savings:
888
901
  for i in range(len(inames)):
889
902
  data = savings[sname][i] / 1000
890
- if np.sum(data) > 1.0e-3: # Only show non-zero series
903
+ if np.abs(np.sum(data)) > 1.0e-3: # Only show non-zero series (use abs for debts)
891
904
  nonzero_series[f"{sname} {inames[i]}"] = data
892
905
 
893
906
  # Add stacked area traces for each account type
@@ -942,10 +955,20 @@ class PlotlyBackend(PlotBackend):
942
955
  # Filter out zero series and create individual series names
943
956
  nonzero_series = {}
944
957
  for sname in sources:
945
- for i in range(len(inames)):
946
- data = sources[sname][i] / 1000
947
- if np.sum(data) > 1.0e-3: # Only show non-zero series
948
- nonzero_series[f"{sname} {inames[i]}"] = data
958
+ source_data = sources[sname]
959
+ # Check if this is a household-level source (shape (1, N_n) when N_i > 1)
960
+ is_household = source_data.shape[0] == 1 and len(inames) > 1
961
+ if is_household:
962
+ # Show household total once without individual name
963
+ data = source_data[0] / 1000
964
+ if np.abs(np.sum(data)) > 1.0e-3: # Only show non-zero series (use abs for debts)
965
+ nonzero_series[sname] = data
966
+ else:
967
+ # Show per individual
968
+ for i in range(len(inames)):
969
+ data = source_data[i] / 1000
970
+ if np.abs(np.sum(data)) > 1.0e-3: # Only show non-zero series (use abs for debts)
971
+ nonzero_series[f"{sname} {inames[i]}"] = data
949
972
 
950
973
  # Add stacked area traces for each source type
951
974
  for source_name, data in nonzero_series.items():
owlplanner/progress.py CHANGED
@@ -1,10 +1,23 @@
1
1
  """
2
- A simple object to display progress.
2
+ Progress indicator for long-running operations.
3
3
 
4
- Copyright &copy; 2024 - Martin-D. Lacasse
4
+ This module provides a simple progress indicator class that displays
5
+ progress as a percentage on a single line that updates in place.
5
6
 
6
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
8
 
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
8
21
  """
9
22
 
10
23
  from typing import Optional
owlplanner/rates.py CHANGED
@@ -1,30 +1,25 @@
1
1
  """
2
+ Historical and statistical rate of return data for asset classes.
2
3
 
3
- Owl/rates
4
- ---
4
+ This module provides historical annual rates of return for different asset
5
+ classes: S&P500, Baa corporate bonds, real estate, 3-mo T-Bills, 10-year Treasury
6
+ notes, and inflation as measured by CPI from 1928 to present. Values were
7
+ extracted from NYU's Stern School of business historical returns data.
5
8
 
6
- A retirement planner using linear programming optimization.
9
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
10
 
8
- See companion document for a complete explanation and description
9
- of all variables and parameters.
11
+ This program is free software: you can redistribute it and/or modify
12
+ it under the terms of the GNU General Public License as published by
13
+ the Free Software Foundation, either version 3 of the License, or
14
+ (at your option) any later version.
10
15
 
11
- This class provides the historical annual rate of returns for different
12
- classes of assets: S&P500, Aaa and Baa corporate bonds, 3-mo T-Bills,
13
- 10-year Treasury notes, and inflation as measured by CPI all from
14
- 1928 until now.
15
-
16
- Values were extracted from NYU's Stern School of business:
17
- https://pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/histretSP.html
18
- from references therein.
19
-
20
- Rate lists will need to be updated with values for current year.
21
- When doing so, the TO bound defined below will need to be adjusted
22
- to the last current data year.
23
-
24
- Copyright &copy; 2024 - Martin-D. Lacasse
25
-
26
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
16
+ This program is distributed in the hope that it will be useful,
17
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ GNU General Public License for more details.
27
20
 
21
+ You should have received a copy of the GNU General Public License
22
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
28
23
  """
29
24
 
30
25
  ###################################################################
@@ -36,10 +31,21 @@ import sys
36
31
  from owlplanner import mylogging as log
37
32
  from owlplanner import utils as u
38
33
 
39
- # All data goes from 1928 to 2024. Update the TO value when data
34
+ # All data goes from 1928 to 2025. Update the TO value when data
40
35
  # becomes available for subsequent years.
41
36
  FROM = 1928
42
- TO = 2024
37
+ TO = 2025
38
+
39
+ # Rate methods that use the same rate every year (reverse/roll are no-ops).
40
+ CONSTANT_RATE_METHODS = (
41
+ "default", "optimistic", "conservative", "user",
42
+ "historical average", "mean",
43
+ )
44
+ # Rate methods that produce deterministic series (no regeneration needed).
45
+ RATE_METHODS_NO_REGEN = (
46
+ "default", "optimistic", "conservative", "user",
47
+ "historical average", "historical",
48
+ )
43
49
 
44
50
  where = os.path.dirname(sys.modules["owlplanner"].__file__)
45
51
  file = os.path.join(where, "data/rates.csv")
@@ -55,8 +61,8 @@ SP500 = df["S&P 500"]
55
61
  # Annual rate of return (%) of Baa Corporate Bonds since 1928.
56
62
  BondsBaa = df["Bonds Baa"]
57
63
 
58
- # Annual rate of return (%) of Aaa Corporate Bonds since 1928.
59
- BondsAaa = df["Bonds Aaa"]
64
+ # Annual rate of return (%) of Real Estate since 1928.
65
+ RealEstate = df["real estate"]
60
66
 
61
67
  # Annual rate of return (%) for 10-y Treasury notes since 1928.
62
68
  TNotes = df["TNotes"]
@@ -101,8 +107,8 @@ def getRatesDistributions(frm, to, mylog=None):
101
107
  stdev = df.std()
102
108
  covar = df.cov()
103
109
 
104
- mylog.print("means: (%)\n", means)
105
- mylog.print("standard deviation: (%)\n", stdev)
110
+ mylog.vprint("means: (%)\n", means)
111
+ mylog.vprint("standard deviation: (%)\n", stdev)
106
112
 
107
113
  # Convert to NumPy array and from percent to decimal.
108
114
  means = np.array(means) / 100.0
@@ -114,7 +120,7 @@ def getRatesDistributions(frm, to, mylog=None):
114
120
  # Fold round-off errors in proper bounds.
115
121
  corr[corr > 1] = 1
116
122
  corr[corr < -1] = -1
117
- mylog.print("correlation matrix: \n\t\t%s" % str(corr).replace("\n", "\n\t\t"))
123
+ mylog.vprint("correlation matrix: \n\t\t%s" % str(corr).replace("\n", "\n\t\t"))
118
124
 
119
125
  return means, stdev, corr, covar
120
126
 
@@ -131,15 +137,25 @@ class Rates(object):
131
137
  then ``mySeries = r.genSeries()``
132
138
  """
133
139
 
134
- def __init__(self, mylog=None):
140
+ def __init__(self, mylog=None, seed=None):
135
141
  """
136
142
  Default constructor.
143
+
144
+ Args:
145
+ mylog: Logger instance (optional)
146
+ seed: Random seed for reproducible stochastic rates (optional)
137
147
  """
138
148
  if mylog is None:
139
149
  self.mylog = log.Logger()
140
150
  else:
141
151
  self.mylog = mylog
142
152
 
153
+ # Store seed for stochastic rate generation
154
+ # Always use a Generator instance for thread safety and modern API
155
+ # If seed is None, default_rng() will use entropy/current time
156
+ self._seed = seed
157
+ self._rng = np.random.default_rng(seed)
158
+
143
159
  # Default rates are average over last 30 years.
144
160
  self._defRates = np.array([0.1101, 0.0736, 0.0503, 0.0251])
145
161
 
@@ -335,18 +351,18 @@ class Rates(object):
335
351
 
336
352
  def _histRates(self, n):
337
353
  """
338
- Return a list of 4 values representing the historical rates
354
+ Return an array of 4 values representing the historical rates
339
355
  of stock, Corporate Baa bonds, Treasury notes, and inflation,
340
356
  respectively.
341
357
  """
342
358
  hrates = np.array([SP500[n], BondsBaa[n], TNotes[n], Inflation[n]])
343
359
 
344
- # Convert from percent to decimal.
360
+ # Historical rates are stored in percent. Convert from percent to decimal.
345
361
  return hrates / 100
346
362
 
347
363
  def _stochRates(self, n):
348
364
  """
349
- Return a list of 4 values representing the historical rates
365
+ Return an array of 4 values representing the historical rates
350
366
  of stock, Corporate Baa bonds, Treasury notes, and inflation,
351
367
  respectively. Values are pulled from normal distributions
352
368
  having the same characteristics as the historical data for
@@ -354,8 +370,8 @@ class Rates(object):
354
370
 
355
371
  But these variables need to be looked at together
356
372
  through multivariate analysis. Code below accounts for
357
- covariance between stocks, bonds, and inflation.
373
+ covariance between stocks, corp bonds, t-notes, and inflation.
358
374
  """
359
- srates = np.random.multivariate_normal(self.means, self.covar)
375
+ srates = self._rng.multivariate_normal(self.means, self.covar)
360
376
 
361
377
  return srates
@@ -1,16 +1,23 @@
1
1
  """
2
+ Social Security benefit calculation rules and utilities.
2
3
 
3
- Owl/socialsecurity
4
- --------
4
+ This module implements Social Security rules including full retirement age
5
+ calculations, benefit computations, and related retirement planning functions.
5
6
 
6
- A retirement planner using linear programming optimization.
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
7
8
 
8
- This file contains the rules related to social security.
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
9
13
 
10
- Copyright &copy; 2025 - Martin-D. Lacasse
11
-
12
- Disclaimers: This code is for educational purposes only and does not constitute financial advice.
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
13
18
 
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
14
21
  """
15
22
 
16
23
  import numpy as np
@@ -91,7 +98,7 @@ def getSpousalBenefits(pias):
91
98
  return benefits
92
99
 
93
100
 
94
- def getSelfFactor(fra, convage, bornOnFirst):
101
+ def getSelfFactor(fra, convage, bornOnFirstDays):
95
102
  """
96
103
  Return the reduction/increase factor to multiply PIA based on claiming age.
97
104
 
@@ -102,7 +109,8 @@ def getSelfFactor(fra, convage, bornOnFirst):
102
109
  - After FRA: Benefits are increased by 8% per year (up to 132% at age 70)
103
110
 
104
111
  The function automatically adjusts for Social Security age if the birthday is on
105
- the first day of the month (adds 1/12 year to conventional age).
112
+ the 1st or 2nd day of the month (adds 1/12 year to conventional age), consistent
113
+ with SSA rules that treat both days the same for age calculation purposes.
106
114
 
107
115
  Parameters
108
116
  ----------
@@ -111,8 +119,8 @@ def getSelfFactor(fra, convage, bornOnFirst):
111
119
  convage : float
112
120
  Conventional age when benefits start, in years (can be fractional with 1/12 increments).
113
121
  Must be between 62 and 70 inclusive.
114
- bornOnFirst : bool
115
- True if birthday is on the first day of the month, False otherwise.
122
+ bornOnFirstDays : bool
123
+ True if birthday is on the 1st or 2nd day of the month, False otherwise.
116
124
  If True, the function adds 1/12 year to convert to Social Security age.
117
125
 
118
126
  Returns
@@ -131,8 +139,8 @@ def getSelfFactor(fra, convage, bornOnFirst):
131
139
  if convage < 62 or convage > 70:
132
140
  raise ValueError(f"Age {convage} out of range.")
133
141
 
134
- # Add a month to conventional age if born on the first.
135
- offset = 0 if not bornOnFirst else 1/12
142
+ # Add a month to conventional age if born on the 1st or 2nd (SSA treats both the same).
143
+ offset = 0 if not bornOnFirstDays else 1/12
136
144
  ssage = convage + offset
137
145
 
138
146
  diff = fra - ssage
@@ -146,7 +154,7 @@ def getSelfFactor(fra, convage, bornOnFirst):
146
154
  return .8 - 0.05 * (diff - 3)
147
155
 
148
156
 
149
- def getSpousalFactor(fra, convage, bornOnFirst):
157
+ def getSpousalFactor(fra, convage, bornOnFirstDays):
150
158
  """
151
159
  Return the reduction factor to multiply spousal benefits based on claiming age.
152
160
 
@@ -156,7 +164,8 @@ def getSpousalFactor(fra, convage, bornOnFirst):
156
164
  - At or after FRA: Full spousal benefit (50% of spouse's PIA, no increase for delay)
157
165
 
158
166
  The function automatically adjusts for Social Security age if the birthday is on
159
- the first day of the month (adds 1/12 year to conventional age).
167
+ the 1st or 2nd day of the month (adds 1/12 year to conventional age), consistent
168
+ with SSA rules that treat both days the same for age calculation purposes.
160
169
 
161
170
  Parameters
162
171
  ----------
@@ -165,8 +174,8 @@ def getSpousalFactor(fra, convage, bornOnFirst):
165
174
  convage : float
166
175
  Conventional age when benefits start, in years (can be fractional with 1/12 increments).
167
176
  Must be at least 62 (no maximum, but no increase beyond FRA).
168
- bornOnFirst : bool
169
- True if birthday is on the first day of the month, False otherwise.
177
+ bornOnFirstDays : bool
178
+ True if birthday is on the 1st or 2nd day of the month, False otherwise.
170
179
  If True, the function adds 1/12 year to convert to Social Security age.
171
180
 
172
181
  Returns
@@ -185,8 +194,8 @@ def getSpousalFactor(fra, convage, bornOnFirst):
185
194
  if convage < 62:
186
195
  raise ValueError(f"Age {convage} out of range.")
187
196
 
188
- # Add a month to conventional age if born on the first.
189
- offset = 0 if not bornOnFirst else 1/12
197
+ # Add a month to conventional age if born on the 1st or 2nd (SSA treats both the same).
198
+ offset = 0 if not bornOnFirstDays else 1/12
190
199
  ssage = convage + offset
191
200
 
192
201
  diff = fra - ssage