owlplanner 2025.12.5__py3-none-any.whl → 2025.12.20__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/abcapi.py +6 -6
- owlplanner/config.py +22 -26
- owlplanner/data/awi.csv +75 -0
- owlplanner/data/bendpoints.csv +49 -0
- owlplanner/data/newawi.csv +75 -0
- owlplanner/debts.py +287 -0
- owlplanner/fixedassets.py +214 -0
- owlplanner/plan.py +316 -48
- owlplanner/plotting/plotly_backend.py +1 -1
- owlplanner/progress.py +50 -6
- owlplanner/rates.py +1 -1
- owlplanner/socialsecurity.py +126 -15
- owlplanner/tax2025.py +20 -0
- owlplanner/tax2026.py +61 -27
- owlplanner/timelists.py +127 -19
- owlplanner/utils.py +25 -1
- owlplanner/version.py +1 -1
- {owlplanner-2025.12.5.dist-info → owlplanner-2025.12.20.dist-info}/METADATA +41 -157
- owlplanner-2025.12.20.dist-info/RECORD +29 -0
- owlplanner-2025.12.5.dist-info/RECORD +0 -24
- {owlplanner-2025.12.5.dist-info → owlplanner-2025.12.20.dist-info}/WHEEL +0 -0
- {owlplanner-2025.12.5.dist-info → owlplanner-2025.12.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Owl/fixed_assets
|
|
4
|
+
|
|
5
|
+
This file contains functions for handling fixed assets and calculating
|
|
6
|
+
tax implications when they are sold or disposed of.
|
|
7
|
+
|
|
8
|
+
Copyright © 2025 - Martin-D. Lacasse
|
|
9
|
+
|
|
10
|
+
Disclaimers: This code is for educational purposes only and does not constitute financial advice.
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
######################################################################
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd # noqa: F401
|
|
17
|
+
from datetime import date
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Primary residence exclusion limits (2025 tax year)
|
|
21
|
+
RESIDENCE_EXCLUSION_SINGLE = 250000
|
|
22
|
+
RESIDENCE_EXCLUSION_MARRIED = 500000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def calculate_future_value(current_value, annual_rate, years):
|
|
26
|
+
"""
|
|
27
|
+
Calculate future value of an asset after a given number of years.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
-----------
|
|
31
|
+
current_value : float
|
|
32
|
+
Current value of the asset
|
|
33
|
+
annual_rate : float
|
|
34
|
+
Annual growth rate as a percentage
|
|
35
|
+
years : float
|
|
36
|
+
Number of years to grow
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
--------
|
|
40
|
+
float
|
|
41
|
+
Future value of the asset
|
|
42
|
+
"""
|
|
43
|
+
if years <= 0:
|
|
44
|
+
return current_value
|
|
45
|
+
|
|
46
|
+
growth_factor = (1 + annual_rate / 100.0) ** years
|
|
47
|
+
return current_value * growth_factor
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_fixed_assets_arrays(fixed_assets_df, N_n, thisyear=None, filing_status="single"):
|
|
51
|
+
"""
|
|
52
|
+
Process fixed_assets_df to provide three arrays of length N_n containing:
|
|
53
|
+
1) tax-free money, 2) ordinary income money, and 3) capital gains.
|
|
54
|
+
|
|
55
|
+
Parameters:
|
|
56
|
+
-----------
|
|
57
|
+
fixed_assets_df : pd.DataFrame
|
|
58
|
+
DataFrame with columns: name, type, basis, value, rate, yod, commission
|
|
59
|
+
N_n : int
|
|
60
|
+
Number of years in the plan (length of output arrays)
|
|
61
|
+
thisyear : int, optional
|
|
62
|
+
Starting year of the plan (defaults to date.today().year).
|
|
63
|
+
Array index 0 corresponds to thisyear, index 1 to thisyear+1, etc.
|
|
64
|
+
filing_status : str, optional
|
|
65
|
+
Filing status: "single" or "married" (defaults to "single").
|
|
66
|
+
Affects primary residence exclusion limits.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
--------
|
|
70
|
+
tuple of np.ndarray
|
|
71
|
+
Three arrays of length N_n:
|
|
72
|
+
- tax_free_n: Tax-free proceeds from asset sales
|
|
73
|
+
- ordinary_income_n: Ordinary income from asset sales (e.g., annuities)
|
|
74
|
+
- capital_gains_n: Capital gains from asset sales
|
|
75
|
+
"""
|
|
76
|
+
if thisyear is None:
|
|
77
|
+
thisyear = date.today().year
|
|
78
|
+
|
|
79
|
+
if fixed_assets_df is None or fixed_assets_df.empty:
|
|
80
|
+
return np.zeros(N_n), np.zeros(N_n), np.zeros(N_n)
|
|
81
|
+
|
|
82
|
+
tax_free_n = np.zeros(N_n)
|
|
83
|
+
ordinary_income_n = np.zeros(N_n)
|
|
84
|
+
capital_gains_n = np.zeros(N_n)
|
|
85
|
+
|
|
86
|
+
# Determine residence exclusion limit
|
|
87
|
+
if filing_status == "married":
|
|
88
|
+
residence_exclusion = RESIDENCE_EXCLUSION_MARRIED
|
|
89
|
+
else:
|
|
90
|
+
residence_exclusion = RESIDENCE_EXCLUSION_SINGLE
|
|
91
|
+
|
|
92
|
+
for _, asset in fixed_assets_df.iterrows():
|
|
93
|
+
asset_type = str(asset["type"]).lower()
|
|
94
|
+
basis = float(asset["basis"])
|
|
95
|
+
current_value = float(asset["value"])
|
|
96
|
+
annual_rate = float(asset["rate"])
|
|
97
|
+
yod = int(asset["yod"]) # Year of disposition
|
|
98
|
+
commission_pct = float(asset["commission"]) / 100.0
|
|
99
|
+
|
|
100
|
+
# Find the year index in the plan
|
|
101
|
+
if yod < thisyear or yod >= thisyear + N_n:
|
|
102
|
+
# Asset disposition is outside the plan horizon
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
n = yod - thisyear
|
|
106
|
+
|
|
107
|
+
# Calculate future value at disposition
|
|
108
|
+
years_to_disposition = yod - thisyear
|
|
109
|
+
future_value = calculate_future_value(current_value, annual_rate, years_to_disposition)
|
|
110
|
+
|
|
111
|
+
# Calculate proceeds after commission
|
|
112
|
+
commission_amount = future_value * commission_pct
|
|
113
|
+
proceeds = future_value - commission_amount
|
|
114
|
+
|
|
115
|
+
# Calculate gain (or loss)
|
|
116
|
+
gain = proceeds - basis
|
|
117
|
+
|
|
118
|
+
if asset_type == "fixed annuity":
|
|
119
|
+
# Annuities are taxed as ordinary income
|
|
120
|
+
if gain > 0:
|
|
121
|
+
ordinary_income_n[n] += gain
|
|
122
|
+
# Basis is returned tax-free (even if there's a loss)
|
|
123
|
+
tax_free_n[n] += basis
|
|
124
|
+
elif asset_type == "residence":
|
|
125
|
+
# Primary residence: exclusion up to $250k/$500k
|
|
126
|
+
if gain > 0:
|
|
127
|
+
taxable_gain = max(0, gain - residence_exclusion)
|
|
128
|
+
if taxable_gain > 0:
|
|
129
|
+
capital_gains_n[n] += taxable_gain
|
|
130
|
+
# Excluded gain is tax-free
|
|
131
|
+
tax_free_n[n] += basis + min(gain, residence_exclusion)
|
|
132
|
+
else:
|
|
133
|
+
# Loss or no gain: proceeds are tax-free
|
|
134
|
+
tax_free_n[n] += proceeds
|
|
135
|
+
elif asset_type in ["collectibles", "precious metals"]:
|
|
136
|
+
# Collectibles and precious metals: special capital gains treatment
|
|
137
|
+
# (28% max rate, but we just report as capital gains here)
|
|
138
|
+
if gain > 0:
|
|
139
|
+
capital_gains_n[n] += gain
|
|
140
|
+
tax_free_n[n] += basis
|
|
141
|
+
else:
|
|
142
|
+
# Loss: only proceeds are tax-free
|
|
143
|
+
tax_free_n[n] += proceeds
|
|
144
|
+
elif asset_type in ["real estate", "stocks"]:
|
|
145
|
+
# Real estate and stocks: standard capital gains
|
|
146
|
+
if gain > 0:
|
|
147
|
+
capital_gains_n[n] += gain
|
|
148
|
+
tax_free_n[n] += basis
|
|
149
|
+
else:
|
|
150
|
+
# Loss: only proceeds are tax-free
|
|
151
|
+
tax_free_n[n] += proceeds
|
|
152
|
+
else:
|
|
153
|
+
# Unknown type: treat as capital gains
|
|
154
|
+
if gain > 0:
|
|
155
|
+
capital_gains_n[n] += gain
|
|
156
|
+
tax_free_n[n] += basis
|
|
157
|
+
else:
|
|
158
|
+
# Loss: only proceeds are tax-free
|
|
159
|
+
tax_free_n[n] += proceeds
|
|
160
|
+
|
|
161
|
+
return tax_free_n, ordinary_income_n, capital_gains_n
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_fixed_assets_bequest_value(fixed_assets_df, N_n, thisyear=None):
|
|
165
|
+
"""
|
|
166
|
+
Calculate the total bequest value from fixed assets that have a yod
|
|
167
|
+
(year of disposition) past the end of the plan. These assets are assumed
|
|
168
|
+
to be liquidated at the end of the plan and added to the bequest.
|
|
169
|
+
|
|
170
|
+
Parameters:
|
|
171
|
+
-----------
|
|
172
|
+
fixed_assets_df : pd.DataFrame
|
|
173
|
+
DataFrame with columns: name, type, basis, value, rate, yod, commission
|
|
174
|
+
N_n : int
|
|
175
|
+
Number of years in the plan
|
|
176
|
+
thisyear : int, optional
|
|
177
|
+
Starting year of the plan (defaults to date.today().year)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
--------
|
|
181
|
+
float
|
|
182
|
+
Total proceeds (after commission) from assets liquidated at end of plan.
|
|
183
|
+
This represents the total value added to the bequest. No taxes are applied
|
|
184
|
+
as assets are assumed to pass to heirs with step-up in basis.
|
|
185
|
+
"""
|
|
186
|
+
if thisyear is None:
|
|
187
|
+
thisyear = date.today().year
|
|
188
|
+
|
|
189
|
+
if fixed_assets_df is None or fixed_assets_df.empty:
|
|
190
|
+
return 0.0
|
|
191
|
+
|
|
192
|
+
years_to_end = N_n # Years from start to last year of plan, inclusively
|
|
193
|
+
total_bequest_value = 0.0
|
|
194
|
+
|
|
195
|
+
for _, asset in fixed_assets_df.iterrows():
|
|
196
|
+
yod = int(asset["yod"]) # Year of disposition
|
|
197
|
+
|
|
198
|
+
# Only consider assets with yod past the end of the plan
|
|
199
|
+
if yod >= thisyear + N_n:
|
|
200
|
+
current_value = float(asset["value"])
|
|
201
|
+
annual_rate = float(asset["rate"])
|
|
202
|
+
commission_pct = float(asset["commission"]) / 100.0
|
|
203
|
+
|
|
204
|
+
# Calculate future value at the end of the plan
|
|
205
|
+
future_value = calculate_future_value(current_value, annual_rate, years_to_end)
|
|
206
|
+
|
|
207
|
+
# Calculate proceeds after commission
|
|
208
|
+
commission_amount = future_value * commission_pct
|
|
209
|
+
proceeds = future_value - commission_amount
|
|
210
|
+
|
|
211
|
+
# Add to total bequest value (full proceeds, no tax)
|
|
212
|
+
total_bequest_value += proceeds
|
|
213
|
+
|
|
214
|
+
return total_bequest_value
|