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.
@@ -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