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.
Files changed (38) hide show
  1. owlplanner/In Discussion #58, the case of Kim and Sam.md +307 -0
  2. owlplanner/__init__.py +20 -1
  3. owlplanner/abcapi.py +24 -23
  4. owlplanner/cli/README.md +50 -0
  5. owlplanner/cli/_main.py +52 -0
  6. owlplanner/cli/cli_logging.py +56 -0
  7. owlplanner/cli/cmd_list.py +83 -0
  8. owlplanner/cli/cmd_run.py +86 -0
  9. owlplanner/config.py +315 -136
  10. owlplanner/data/__init__.py +21 -0
  11. owlplanner/data/awi.csv +75 -0
  12. owlplanner/data/bendpoints.csv +49 -0
  13. owlplanner/data/newawi.csv +75 -0
  14. owlplanner/data/rates.csv +99 -98
  15. owlplanner/debts.py +315 -0
  16. owlplanner/fixedassets.py +288 -0
  17. owlplanner/mylogging.py +157 -25
  18. owlplanner/plan.py +1044 -332
  19. owlplanner/plotting/__init__.py +16 -3
  20. owlplanner/plotting/base.py +17 -3
  21. owlplanner/plotting/factory.py +16 -3
  22. owlplanner/plotting/matplotlib_backend.py +30 -7
  23. owlplanner/plotting/plotly_backend.py +33 -10
  24. owlplanner/progress.py +66 -9
  25. owlplanner/rates.py +366 -361
  26. owlplanner/socialsecurity.py +142 -22
  27. owlplanner/tax2026.py +170 -57
  28. owlplanner/timelists.py +316 -32
  29. owlplanner/utils.py +204 -5
  30. owlplanner/version.py +20 -1
  31. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/METADATA +50 -158
  32. owlplanner-2026.1.26.dist-info/RECORD +36 -0
  33. owlplanner-2026.1.26.dist-info/entry_points.txt +2 -0
  34. owlplanner-2026.1.26.dist-info/licenses/AUTHORS +15 -0
  35. owlplanner/tax2025.py +0 -339
  36. owlplanner-2025.12.5.dist-info/RECORD +0 -24
  37. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/WHEEL +0 -0
  38. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/licenses/LICENSE +0 -0
owlplanner/debts.py ADDED
@@ -0,0 +1,315 @@
1
+ """
2
+ Debt management and calculation module.
3
+
4
+ This module provides functions for handling debts including mortgage calculations,
5
+ loan amortization, and debt-related financial planning.
6
+
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
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/>.
21
+ """
22
+
23
+ ######################################################################
24
+ import numpy as np
25
+ import pandas as pd # noqa: F401
26
+ from datetime import date
27
+
28
+ from . import utils as u
29
+
30
+
31
+ def calculate_monthly_payment(principal, annual_rate, term_years):
32
+ """
33
+ Calculate monthly payment for an amortizing loan. This is a constant payment amount for a fixed-rate loan.
34
+ monthly payment = principal * (monthly_rate * (1 + monthly_rate)**num_payments) /
35
+ ((1 + monthly_rate)**num_payments - 1)
36
+ where monthly_rate = annual_rate / 100.0 / 12.0 and num_payments = term_years * 12.0.
37
+
38
+ This formula is derived from the formula for the present value of an annuity.
39
+
40
+ Parameters:
41
+ -----------
42
+ principal : float
43
+ Original loan amount
44
+ annual_rate : float
45
+ Annual interest rate as a percentage (e.g., 4.5 for 4.5%)
46
+ term_years : int
47
+ Loan term in years
48
+
49
+ Returns:
50
+ --------
51
+ float
52
+ Monthly payment amount
53
+ """
54
+ if term_years <= 0 or annual_rate < 0 or principal <= 0:
55
+ return 0.0
56
+
57
+ monthly_rate = annual_rate / 100.0 / 12.0
58
+ num_payments = term_years * 12
59
+ fac = (1 + monthly_rate)**num_payments
60
+
61
+ if monthly_rate == 0:
62
+ return principal / num_payments
63
+
64
+ payment = principal * (monthly_rate * fac) / (fac - 1)
65
+
66
+ return payment
67
+
68
+
69
+ def calculate_annual_payment(principal, annual_rate, term_years):
70
+ """
71
+ Calculate annual payment for an amortizing loan.
72
+
73
+ Parameters:
74
+ -----------
75
+ principal : float
76
+ Original loan amount
77
+ annual_rate : float
78
+ Annual interest rate as a percentage
79
+ term_years : int
80
+ Loan term in years
81
+
82
+ Returns:
83
+ --------
84
+ float
85
+ Annual payment amount
86
+ """
87
+ return 12 * calculate_monthly_payment(principal, annual_rate, term_years)
88
+
89
+
90
+ def calculate_remaining_balance(principal, annual_rate, term_years, years_elapsed):
91
+ """
92
+ Calculate remaining balance on a loan after a given number of years.
93
+
94
+ Parameters:
95
+ -----------
96
+ principal : float
97
+ Original loan amount
98
+ annual_rate : float
99
+ Annual interest rate as a percentage
100
+ term_years : int
101
+ Original loan term in years
102
+ years_elapsed : float
103
+ Number of years since loan origination
104
+
105
+ Returns:
106
+ --------
107
+ float
108
+ Remaining balance
109
+ """
110
+ if years_elapsed <= 0:
111
+ return principal
112
+
113
+ if years_elapsed >= term_years:
114
+ return 0.0
115
+
116
+ monthly_rate = annual_rate / 100.0 / 12.0
117
+ fac = 1 + monthly_rate
118
+ num_payments = term_years * 12
119
+ payments_made = int(years_elapsed * 12)
120
+
121
+ if monthly_rate == 0:
122
+ return principal * (1 - payments_made / num_payments)
123
+
124
+ remaining = principal * (fac**num_payments - fac**payments_made) / (fac**num_payments - 1)
125
+
126
+ return max(0.0, remaining)
127
+
128
+
129
+ def get_debt_payments_for_year(debts_df, year):
130
+ """
131
+ Calculate total debt payments (principal + interest) for a given year.
132
+
133
+ Parameters:
134
+ -----------
135
+ debts_df : pd.DataFrame
136
+ DataFrame with columns: name, type, year, term, amount, rate
137
+ year : int
138
+ Year for which to calculate payments
139
+
140
+ Returns:
141
+ --------
142
+ float
143
+ Total annual debt payments for the year
144
+ """
145
+ if u.is_dataframe_empty(debts_df):
146
+ return 0.0
147
+
148
+ total_payments = 0.0
149
+
150
+ for _, debt in debts_df.iterrows():
151
+ # Skip if active column exists and is False (treat NaN/None as True)
152
+ if not u.is_row_active(debt):
153
+ continue
154
+
155
+ start_year = int(debt["year"])
156
+ term = int(debt["term"])
157
+ end_year = start_year + term
158
+
159
+ # Check if loan is active in this year
160
+ if start_year <= year < end_year:
161
+ principal = float(debt["amount"])
162
+ rate = float(debt["rate"])
163
+
164
+ # Calculate annual payment (payment amount is constant for fixed-rate loans)
165
+ annual_payment = calculate_annual_payment(principal, rate, term)
166
+ total_payments += annual_payment
167
+
168
+ return total_payments
169
+
170
+
171
+ def get_debt_balances_for_year(debts_df, year):
172
+ """
173
+ Calculate total remaining debt balances at the end of a given year.
174
+
175
+ Parameters:
176
+ -----------
177
+ debts_df : pd.DataFrame
178
+ DataFrame with columns: name, type, year, term, amount, rate
179
+ year : int
180
+ Year for which to calculate balances
181
+
182
+ Returns:
183
+ --------
184
+ float
185
+ Total remaining debt balances at end of year
186
+ """
187
+ if u.is_dataframe_empty(debts_df):
188
+ return 0.0
189
+
190
+ total_balance = 0.0
191
+
192
+ for _, debt in debts_df.iterrows():
193
+ # Skip if active column exists and is False (treat NaN/None as True)
194
+ if not u.is_row_active(debt):
195
+ continue
196
+
197
+ start_year = int(debt["year"])
198
+ term = int(debt["term"])
199
+ end_year = start_year + term
200
+
201
+ # Check if loan is active at end of this year
202
+ if start_year <= year < end_year:
203
+ principal = float(debt["amount"])
204
+ rate = float(debt["rate"])
205
+
206
+ # Calculate remaining balance at end of year
207
+ years_elapsed = year - start_year + 1
208
+ remaining_balance = calculate_remaining_balance(principal, rate, term, years_elapsed)
209
+ total_balance += remaining_balance
210
+
211
+ return total_balance
212
+
213
+
214
+ def get_debt_payments_array(debts_df, N_n, thisyear=None):
215
+ """
216
+ Process debts_df to provide a single array of length N_n containing
217
+ all annual payments made for each year of the plan.
218
+
219
+ Parameters:
220
+ -----------
221
+ debts_df : pd.DataFrame
222
+ DataFrame with columns: name, type, year, term, amount, rate
223
+ N_n : int
224
+ Number of years in the plan (length of output array)
225
+ thisyear : int, optional
226
+ Starting year of the plan (defaults to date.today().year).
227
+ Array index 0 corresponds to thisyear, index 1 to thisyear+1, etc.
228
+
229
+ Returns:
230
+ --------
231
+ np.ndarray
232
+ Array of length N_n with annual debt payments for each year.
233
+ payments_n[0] = payments for thisyear,
234
+ payments_n[1] = payments for thisyear+1, etc.
235
+ """
236
+ if thisyear is None:
237
+ thisyear = date.today().year
238
+
239
+ if u.is_dataframe_empty(debts_df):
240
+ return np.zeros(N_n)
241
+
242
+ payments_n = np.zeros(N_n)
243
+
244
+ for _, debt in debts_df.iterrows():
245
+ # Skip if active column exists and is False (treat NaN/None as True)
246
+ if not u.is_row_active(debt):
247
+ continue
248
+
249
+ start_year = int(debt["year"])
250
+ term = int(debt["term"])
251
+ end_year = start_year + term
252
+ principal = float(debt["amount"])
253
+ rate = float(debt["rate"])
254
+
255
+ # Calculate annual payment (payment amount is constant for fixed-rate loans)
256
+ annual_payment = calculate_annual_payment(principal, rate, term)
257
+
258
+ # Add payments for each year the loan is active within the plan horizon
259
+ for n in range(N_n):
260
+ year = thisyear + n
261
+ if start_year <= year < end_year:
262
+ payments_n[n] += annual_payment
263
+
264
+ return payments_n
265
+
266
+
267
+ def get_remaining_debt_balance(debts_df, N_n, thisyear=None):
268
+ """
269
+ Calculate total remaining debt balance at the end of the plan horizon.
270
+ Returns the sum of all remaining balances for loans that haven't been
271
+ fully paid off by the end of the plan.
272
+
273
+ Parameters:
274
+ -----------
275
+ debts_df : pd.DataFrame
276
+ DataFrame with columns: name, type, year, term, amount, rate
277
+ N_n : int
278
+ Number of years in the plan
279
+ thisyear : int, optional
280
+ Starting year of the plan (defaults to date.today().year)
281
+
282
+ Returns:
283
+ --------
284
+ float
285
+ Total remaining debt balance at the end of the plan horizon.
286
+ Returns 0.0 if all loans are paid off or if no loans are active.
287
+ """
288
+ if thisyear is None:
289
+ thisyear = date.today().year
290
+
291
+ if u.is_dataframe_empty(debts_df):
292
+ return 0.0
293
+
294
+ end_year = thisyear + N_n - 1
295
+ total_balance = 0.0
296
+
297
+ for _, debt in debts_df.iterrows():
298
+ # Skip if active column exists and is False (treat NaN/None as True)
299
+ if not u.is_row_active(debt):
300
+ continue
301
+
302
+ start_year = int(debt["year"])
303
+ term = int(debt["term"])
304
+ loan_end_year = start_year + term
305
+ principal = float(debt["amount"])
306
+ rate = float(debt["rate"])
307
+
308
+ # Check if loan is still active at the end of the plan
309
+ if start_year <= end_year < loan_end_year:
310
+ # Calculate remaining balance at end of plan horizon
311
+ years_elapsed = end_year - start_year + 1
312
+ remaining_balance = calculate_remaining_balance(principal, rate, term, years_elapsed)
313
+ total_balance += remaining_balance
314
+
315
+ return total_balance
@@ -0,0 +1,288 @@
1
+ """
2
+ Fixed assets management and tax calculation module.
3
+
4
+ This module provides functions for handling fixed assets (such as real estate)
5
+ and calculating tax implications when they are sold or disposed of, including
6
+ primary residence exclusion rules.
7
+
8
+ Copyright (C) 2025-2026 The Owlplanner Authors
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/>.
22
+ """
23
+
24
+ ######################################################################
25
+ import numpy as np
26
+ import pandas as pd # noqa: F401
27
+ from datetime import date
28
+
29
+ from . import utils as u
30
+
31
+
32
+ # Primary residence exclusion limits (2025 tax year)
33
+ RESIDENCE_EXCLUSION_SINGLE = 250000
34
+ RESIDENCE_EXCLUSION_MARRIED = 500000
35
+
36
+
37
+ def calculate_future_value(current_value, annual_rate, years):
38
+ """
39
+ Calculate future value of an asset after a given number of years.
40
+
41
+ Parameters:
42
+ -----------
43
+ current_value : float
44
+ Current value of the asset
45
+ annual_rate : float
46
+ Annual growth rate as a percentage
47
+ years : float
48
+ Number of years to grow
49
+
50
+ Returns:
51
+ --------
52
+ float
53
+ Future value of the asset
54
+ """
55
+ if years <= 0:
56
+ return current_value
57
+
58
+ growth_factor = (1 + annual_rate / 100.0) ** years
59
+ return current_value * growth_factor
60
+
61
+
62
+ def get_fixed_assets_arrays(fixed_assets_df, N_n, thisyear=None, filing_status="single"):
63
+ """
64
+ Process fixed_assets_df to provide three arrays of length N_n containing:
65
+ 1) tax-free money, 2) ordinary income money, and 3) capital gains.
66
+
67
+ Parameters:
68
+ -----------
69
+ fixed_assets_df : pd.DataFrame
70
+ DataFrame with columns: name, type, year, basis, value, rate, yod, commission
71
+ where 'year' is the reference year (this year or after). Basis and
72
+ value are in reference-year dollars.
73
+ N_n : int
74
+ Number of years in the plan (length of output arrays)
75
+ thisyear : int, optional
76
+ Starting year of the plan (defaults to date.today().year).
77
+ Array index 0 corresponds to thisyear, index 1 to thisyear+1, etc.
78
+ filing_status : str, optional
79
+ Filing status: "single" or "married" (defaults to "single").
80
+ Affects primary residence exclusion limits.
81
+
82
+ Returns:
83
+ --------
84
+ tuple of np.ndarray
85
+ Three arrays of length N_n:
86
+ - tax_free_n: Tax-free proceeds from asset sales
87
+ - ordinary_income_n: Ordinary income from asset sales (e.g., annuities)
88
+ - capital_gains_n: Capital gains from asset sales
89
+ """
90
+ if thisyear is None:
91
+ thisyear = date.today().year
92
+
93
+ if u.is_dataframe_empty(fixed_assets_df):
94
+ return np.zeros(N_n), np.zeros(N_n), np.zeros(N_n)
95
+
96
+ tax_free_n = np.zeros(N_n)
97
+ ordinary_income_n = np.zeros(N_n)
98
+ capital_gains_n = np.zeros(N_n)
99
+
100
+ # Determine residence exclusion limit
101
+ if filing_status == "married":
102
+ residence_exclusion = RESIDENCE_EXCLUSION_MARRIED
103
+ else:
104
+ residence_exclusion = RESIDENCE_EXCLUSION_SINGLE
105
+
106
+ for _, asset in fixed_assets_df.iterrows():
107
+ # Skip if active column exists and is False (treat NaN/None as True)
108
+ if not u.is_row_active(asset):
109
+ continue
110
+
111
+ asset_type = str(asset["type"]).lower()
112
+ basis = float(asset["basis"])
113
+ value_at_reference = float(asset["value"]) # Value at reference year
114
+ annual_rate = float(asset["rate"])
115
+ # Get reference year, defaulting to thisyear for backward compatibility
116
+ if "year" in asset.index and not pd.isna(asset["year"]):
117
+ reference_year = int(asset["year"])
118
+ else:
119
+ reference_year = thisyear
120
+ yod = int(asset["yod"]) # Year of disposition
121
+ commission_pct = float(asset["commission"]) / 100.0
122
+
123
+ end_year = thisyear + N_n - 1 # Last year of the plan
124
+
125
+ # Skip if asset reference year is after the plan ends
126
+ if reference_year > end_year:
127
+ continue
128
+
129
+ # Account for negative or null yod with reference to end of plan
130
+ if yod <= 0:
131
+ yod = end_year + yod + 1
132
+
133
+ # Skip if disposition is before reference year (invalid)
134
+ if yod < reference_year:
135
+ continue
136
+
137
+ # Skip if disposition is before the plan starts
138
+ if yod < thisyear:
139
+ continue
140
+
141
+ # Only process assets disposed during the plan (yod <= end_year)
142
+ # IMPORTANT: Assets with yod > end_year are NOT processed here to avoid double counting.
143
+ # They are handled separately in get_fixed_assets_bequest_value().
144
+ if yod > end_year:
145
+ continue
146
+
147
+ # Disposition at beginning of yod (within plan duration)
148
+ n = yod - thisyear
149
+ # Asset assessed at beginning of reference_year, disposed at beginning of yod
150
+ # Growth period: from start of reference_year to start of yod = (yod - reference_year) years
151
+ years_from_reference_to_disposition = yod - reference_year
152
+
153
+ # Calculate future value at disposition
154
+ future_value = calculate_future_value(value_at_reference, annual_rate, years_from_reference_to_disposition)
155
+
156
+ # Calculate proceeds after commission
157
+ commission_amount = future_value * commission_pct
158
+ proceeds = future_value - commission_amount
159
+
160
+ # Calculate gain (or loss)
161
+ gain = proceeds - basis
162
+
163
+ if asset_type == "fixed annuity":
164
+ # Annuities are taxed as ordinary income
165
+ if gain > 0:
166
+ ordinary_income_n[n] += gain
167
+ # Basis is returned tax-free (even if there's a loss)
168
+ tax_free_n[n] += basis
169
+ elif asset_type == "residence":
170
+ # Primary residence: exclusion up to $250k/$500k
171
+ if gain > 0:
172
+ taxable_gain = max(0, gain - residence_exclusion)
173
+ if taxable_gain > 0:
174
+ capital_gains_n[n] += taxable_gain
175
+ # Excluded gain is tax-free
176
+ tax_free_n[n] += basis + min(gain, residence_exclusion)
177
+ else:
178
+ # Loss or no gain: proceeds are tax-free
179
+ tax_free_n[n] += proceeds
180
+ elif asset_type in ["collectibles", "precious metals"]:
181
+ # Collectibles and precious metals: special capital gains treatment
182
+ # (28% max rate, but we just report as capital gains here)
183
+ if gain > 0:
184
+ capital_gains_n[n] += gain
185
+ tax_free_n[n] += basis
186
+ else:
187
+ # Loss: only proceeds are tax-free
188
+ tax_free_n[n] += proceeds
189
+ elif asset_type in ["real estate", "stocks"]:
190
+ # Real estate and stocks: standard capital gains
191
+ if gain > 0:
192
+ capital_gains_n[n] += gain
193
+ tax_free_n[n] += basis
194
+ else:
195
+ # Loss: only proceeds are tax-free
196
+ tax_free_n[n] += proceeds
197
+ else:
198
+ # Unknown type: treat as capital gains
199
+ if gain > 0:
200
+ capital_gains_n[n] += gain
201
+ tax_free_n[n] += basis
202
+ else:
203
+ # Loss: only proceeds are tax-free
204
+ tax_free_n[n] += proceeds
205
+
206
+ return tax_free_n, ordinary_income_n, capital_gains_n
207
+
208
+
209
+ def get_fixed_assets_bequest_value(fixed_assets_df, N_n, thisyear=None):
210
+ """
211
+ Calculate the total bequest value from fixed assets that have a yod
212
+ (year of disposition) past the end of the plan. These assets are assumed
213
+ to be liquidated at the end of the plan and added to the bequest.
214
+
215
+ Parameters:
216
+ -----------
217
+ fixed_assets_df : pd.DataFrame
218
+ DataFrame with columns: name, type, year, basis, value, rate, yod, commission
219
+ where 'year' is the reference year (this year or after). Basis and
220
+ value are in reference-year dollars.
221
+ N_n : int
222
+ Number of years in the plan
223
+ thisyear : int, optional
224
+ Starting year of the plan (defaults to date.today().year)
225
+
226
+ Returns:
227
+ --------
228
+ float
229
+ Total proceeds (after commission) from assets liquidated at end of plan.
230
+ This represents the total value added to the bequest. No taxes are applied
231
+ as assets are assumed to pass to heirs with step-up in basis.
232
+ """
233
+ if thisyear is None:
234
+ thisyear = date.today().year
235
+
236
+ if u.is_dataframe_empty(fixed_assets_df):
237
+ return 0.0
238
+
239
+ end_year = thisyear + N_n - 1 # Last year of the plan
240
+ total_bequest_value = 0.0
241
+
242
+ for _, asset in fixed_assets_df.iterrows():
243
+ # Skip if active column exists and is False (treat NaN/None as True)
244
+ if not u.is_row_active(asset):
245
+ continue
246
+
247
+ # Get reference year, defaulting to thisyear for backward compatibility
248
+ if "year" in asset.index and not pd.isna(asset["year"]):
249
+ reference_year = int(asset["year"])
250
+ else:
251
+ reference_year = thisyear
252
+ yod = int(asset["yod"]) # Year of disposition
253
+
254
+ # Skip if asset reference year is after the plan ends
255
+ if reference_year > end_year:
256
+ continue
257
+
258
+ # Account for negative or null yod with reference to end of plan
259
+ if yod <= 0:
260
+ yod = end_year + yod + 1
261
+
262
+ # Skip if disposition is before reference year (invalid)
263
+ if yod < reference_year:
264
+ continue
265
+
266
+ # Only consider assets with yod past the end of the plan (not disposed during the plan)
267
+ # IMPORTANT: Assets with yod <= end_year are NOT processed here to avoid double counting.
268
+ # They are handled separately in get_fixed_assets_arrays() where they are disposed during the plan.
269
+ # These assets (yod > end_year) are assumed to be liquidated at the end of the plan and added to the bequest
270
+ if yod > end_year:
271
+ value_at_reference = float(asset["value"]) # Value at reference year
272
+ annual_rate = float(asset["rate"])
273
+ commission_pct = float(asset["commission"]) / 100.0
274
+
275
+ # Calculate future value at the end of the plan
276
+ # Asset assessed at beginning of reference_year, liquidated at end of end_year
277
+ # Growth period: from start of reference_year to end of end_year = (end_year - reference_year + 1) years
278
+ years_from_reference_to_end = end_year - reference_year + 1
279
+ future_value = calculate_future_value(value_at_reference, annual_rate, years_from_reference_to_end)
280
+
281
+ # Calculate proceeds after commission
282
+ commission_amount = future_value * commission_pct
283
+ proceeds = future_value - commission_amount
284
+
285
+ # Add to total bequest value (full proceeds, no tax - step-up in basis for heirs)
286
+ total_bequest_value += proceeds
287
+
288
+ return total_bequest_value