fundedness 0.2.4__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.
- fundedness/__init__.py +71 -0
- fundedness/allocation/__init__.py +20 -0
- fundedness/allocation/base.py +32 -0
- fundedness/allocation/constant.py +25 -0
- fundedness/allocation/glidepath.py +111 -0
- fundedness/allocation/merton_optimal.py +220 -0
- fundedness/cefr.py +241 -0
- fundedness/liabilities.py +221 -0
- fundedness/liquidity.py +49 -0
- fundedness/merton.py +289 -0
- fundedness/models/__init__.py +35 -0
- fundedness/models/assets.py +148 -0
- fundedness/models/household.py +153 -0
- fundedness/models/liabilities.py +99 -0
- fundedness/models/market.py +199 -0
- fundedness/models/simulation.py +80 -0
- fundedness/models/tax.py +125 -0
- fundedness/models/utility.py +154 -0
- fundedness/optimize.py +473 -0
- fundedness/policies.py +204 -0
- fundedness/risk.py +72 -0
- fundedness/simulate.py +595 -0
- fundedness/viz/__init__.py +33 -0
- fundedness/viz/colors.py +110 -0
- fundedness/viz/comparison.py +294 -0
- fundedness/viz/fan_chart.py +193 -0
- fundedness/viz/histogram.py +225 -0
- fundedness/viz/optimal.py +542 -0
- fundedness/viz/survival.py +230 -0
- fundedness/viz/tornado.py +236 -0
- fundedness/viz/waterfall.py +203 -0
- fundedness/withdrawals/__init__.py +27 -0
- fundedness/withdrawals/base.py +116 -0
- fundedness/withdrawals/comparison.py +230 -0
- fundedness/withdrawals/fixed_swr.py +174 -0
- fundedness/withdrawals/guardrails.py +136 -0
- fundedness/withdrawals/merton_optimal.py +286 -0
- fundedness/withdrawals/rmd_style.py +203 -0
- fundedness/withdrawals/vpw.py +136 -0
- fundedness-0.2.4.dist-info/METADATA +300 -0
- fundedness-0.2.4.dist-info/RECORD +43 -0
- fundedness-0.2.4.dist-info/WHEEL +4 -0
- fundedness-0.2.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Histogram visualizations for time-to-event distributions."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import plotly.graph_objects as go
|
|
5
|
+
|
|
6
|
+
from fundedness.viz.colors import COLORS, get_plotly_layout_defaults
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_time_distribution_histogram(
|
|
10
|
+
time_to_event: np.ndarray,
|
|
11
|
+
event_name: str = "Ruin",
|
|
12
|
+
planning_horizon: int | None = None,
|
|
13
|
+
percentiles_to_show: list[int] | None = None,
|
|
14
|
+
title: str | None = None,
|
|
15
|
+
height: int = 400,
|
|
16
|
+
width: int | None = None,
|
|
17
|
+
) -> go.Figure:
|
|
18
|
+
"""Create a histogram of time-to-event distribution.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
time_to_event: Array of time values (years until event, inf for no event)
|
|
22
|
+
event_name: Name of the event (e.g., "Ruin", "Floor Breach")
|
|
23
|
+
planning_horizon: Maximum planning horizon (for x-axis)
|
|
24
|
+
percentiles_to_show: Percentiles to mark (e.g., [10, 50, 90])
|
|
25
|
+
title: Chart title (auto-generated if None)
|
|
26
|
+
height: Chart height in pixels
|
|
27
|
+
width: Chart width in pixels
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Plotly Figure object
|
|
31
|
+
"""
|
|
32
|
+
# Filter out infinite values (no event occurred)
|
|
33
|
+
finite_times = time_to_event[np.isfinite(time_to_event)]
|
|
34
|
+
never_occurred_count = np.sum(~np.isfinite(time_to_event))
|
|
35
|
+
total_count = len(time_to_event)
|
|
36
|
+
|
|
37
|
+
if title is None:
|
|
38
|
+
title = f"Time to {event_name} Distribution"
|
|
39
|
+
|
|
40
|
+
fig = go.Figure()
|
|
41
|
+
|
|
42
|
+
if len(finite_times) > 0:
|
|
43
|
+
# Determine bins
|
|
44
|
+
max_time = planning_horizon or int(np.ceil(finite_times.max()))
|
|
45
|
+
bins = np.arange(0, max_time + 2, 1)
|
|
46
|
+
|
|
47
|
+
# Create histogram
|
|
48
|
+
fig.add_trace(
|
|
49
|
+
go.Histogram(
|
|
50
|
+
x=finite_times,
|
|
51
|
+
xbins={"start": 0, "end": max_time + 1, "size": 1},
|
|
52
|
+
marker_color=COLORS["danger_primary"],
|
|
53
|
+
opacity=0.7,
|
|
54
|
+
name=f"Years to {event_name}",
|
|
55
|
+
hovertemplate=(
|
|
56
|
+
"<b>Year %{x}</b><br>"
|
|
57
|
+
"Count: %{y}<br>"
|
|
58
|
+
"<extra></extra>"
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Add percentile lines
|
|
64
|
+
if percentiles_to_show:
|
|
65
|
+
percentile_colors = {
|
|
66
|
+
10: COLORS["danger_secondary"],
|
|
67
|
+
25: COLORS["warning_secondary"],
|
|
68
|
+
50: COLORS["wealth_primary"],
|
|
69
|
+
75: COLORS["success_secondary"],
|
|
70
|
+
90: COLORS["success_primary"],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for pct in percentiles_to_show:
|
|
74
|
+
value = np.percentile(finite_times, pct)
|
|
75
|
+
color = percentile_colors.get(pct, COLORS["neutral_primary"])
|
|
76
|
+
fig.add_vline(
|
|
77
|
+
x=value,
|
|
78
|
+
line_dash="dash",
|
|
79
|
+
line_color=color,
|
|
80
|
+
line_width=2,
|
|
81
|
+
annotation_text=f"P{pct}: {value:.1f}y",
|
|
82
|
+
annotation_position="top",
|
|
83
|
+
annotation_font_color=color,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Add annotation for "never occurred" count
|
|
87
|
+
if never_occurred_count > 0:
|
|
88
|
+
never_pct = never_occurred_count / total_count * 100
|
|
89
|
+
fig.add_annotation(
|
|
90
|
+
x=0.98,
|
|
91
|
+
y=0.98,
|
|
92
|
+
xref="paper",
|
|
93
|
+
yref="paper",
|
|
94
|
+
text=f"Never {event_name.lower()}ed: {never_pct:.1f}%<br>({never_occurred_count:,} paths)",
|
|
95
|
+
showarrow=False,
|
|
96
|
+
font={"size": 12, "color": COLORS["success_primary"]},
|
|
97
|
+
bgcolor=COLORS["background"],
|
|
98
|
+
bordercolor=COLORS["success_primary"],
|
|
99
|
+
borderwidth=1,
|
|
100
|
+
borderpad=6,
|
|
101
|
+
align="right",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Apply layout
|
|
105
|
+
layout = get_plotly_layout_defaults()
|
|
106
|
+
layout.update({
|
|
107
|
+
"title": {"text": title},
|
|
108
|
+
"height": height,
|
|
109
|
+
"xaxis": {
|
|
110
|
+
"title": f"Years to {event_name}",
|
|
111
|
+
"gridcolor": COLORS["neutral_light"],
|
|
112
|
+
"dtick": 5,
|
|
113
|
+
},
|
|
114
|
+
"yaxis": {
|
|
115
|
+
"title": "Number of Paths",
|
|
116
|
+
"gridcolor": COLORS["neutral_light"],
|
|
117
|
+
},
|
|
118
|
+
"showlegend": False,
|
|
119
|
+
"bargap": 0.1,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if width:
|
|
123
|
+
layout["width"] = width
|
|
124
|
+
|
|
125
|
+
fig.update_layout(**layout)
|
|
126
|
+
|
|
127
|
+
return fig
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def create_outcome_distribution_histogram(
|
|
131
|
+
terminal_values: np.ndarray,
|
|
132
|
+
initial_value: float | None = None,
|
|
133
|
+
target_value: float | None = None,
|
|
134
|
+
title: str = "Terminal Wealth Distribution",
|
|
135
|
+
height: int = 400,
|
|
136
|
+
width: int | None = None,
|
|
137
|
+
) -> go.Figure:
|
|
138
|
+
"""Create a histogram of terminal portfolio values.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
terminal_values: Array of terminal portfolio values
|
|
142
|
+
initial_value: Starting portfolio value (for reference)
|
|
143
|
+
target_value: Target terminal value (for reference)
|
|
144
|
+
title: Chart title
|
|
145
|
+
height: Chart height in pixels
|
|
146
|
+
width: Chart width in pixels
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Plotly Figure object
|
|
150
|
+
"""
|
|
151
|
+
fig = go.Figure()
|
|
152
|
+
|
|
153
|
+
# Create histogram with log-scale bins for wealth distribution
|
|
154
|
+
fig.add_trace(
|
|
155
|
+
go.Histogram(
|
|
156
|
+
x=terminal_values,
|
|
157
|
+
nbinsx=50,
|
|
158
|
+
marker_color=COLORS["wealth_primary"],
|
|
159
|
+
opacity=0.7,
|
|
160
|
+
name="Terminal Value",
|
|
161
|
+
hovertemplate=(
|
|
162
|
+
"<b>Value Range</b><br>"
|
|
163
|
+
"$%{x:,.0f}<br>"
|
|
164
|
+
"Count: %{y}<br>"
|
|
165
|
+
"<extra></extra>"
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Add reference lines
|
|
171
|
+
if initial_value is not None:
|
|
172
|
+
fig.add_vline(
|
|
173
|
+
x=initial_value,
|
|
174
|
+
line_dash="dash",
|
|
175
|
+
line_color=COLORS["neutral_primary"],
|
|
176
|
+
line_width=2,
|
|
177
|
+
annotation_text=f"Initial: ${initial_value:,.0f}",
|
|
178
|
+
annotation_position="top",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if target_value is not None:
|
|
182
|
+
fig.add_vline(
|
|
183
|
+
x=target_value,
|
|
184
|
+
line_dash="dot",
|
|
185
|
+
line_color=COLORS["success_primary"],
|
|
186
|
+
line_width=2,
|
|
187
|
+
annotation_text=f"Target: ${target_value:,.0f}",
|
|
188
|
+
annotation_position="top",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Add median line
|
|
192
|
+
median_value = np.median(terminal_values)
|
|
193
|
+
fig.add_vline(
|
|
194
|
+
x=median_value,
|
|
195
|
+
line_dash="solid",
|
|
196
|
+
line_color=COLORS["wealth_secondary"],
|
|
197
|
+
line_width=2,
|
|
198
|
+
annotation_text=f"Median: ${median_value:,.0f}",
|
|
199
|
+
annotation_position="bottom",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Apply layout
|
|
203
|
+
layout = get_plotly_layout_defaults()
|
|
204
|
+
layout.update({
|
|
205
|
+
"title": {"text": title},
|
|
206
|
+
"height": height,
|
|
207
|
+
"xaxis": {
|
|
208
|
+
"title": "Portfolio Value ($)",
|
|
209
|
+
"tickformat": "$,.0f",
|
|
210
|
+
"gridcolor": COLORS["neutral_light"],
|
|
211
|
+
},
|
|
212
|
+
"yaxis": {
|
|
213
|
+
"title": "Number of Paths",
|
|
214
|
+
"gridcolor": COLORS["neutral_light"],
|
|
215
|
+
},
|
|
216
|
+
"showlegend": False,
|
|
217
|
+
"bargap": 0.05,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
if width:
|
|
221
|
+
layout["width"] = width
|
|
222
|
+
|
|
223
|
+
fig.update_layout(**layout)
|
|
224
|
+
|
|
225
|
+
return fig
|