fundedness 0.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.

Potentially problematic release.


This version of fundedness might be problematic. Click here for more details.

Files changed (43) hide show
  1. fundedness/__init__.py +71 -0
  2. fundedness/allocation/__init__.py +20 -0
  3. fundedness/allocation/base.py +32 -0
  4. fundedness/allocation/constant.py +25 -0
  5. fundedness/allocation/glidepath.py +111 -0
  6. fundedness/allocation/merton_optimal.py +220 -0
  7. fundedness/cefr.py +241 -0
  8. fundedness/liabilities.py +221 -0
  9. fundedness/liquidity.py +49 -0
  10. fundedness/merton.py +289 -0
  11. fundedness/models/__init__.py +35 -0
  12. fundedness/models/assets.py +148 -0
  13. fundedness/models/household.py +153 -0
  14. fundedness/models/liabilities.py +99 -0
  15. fundedness/models/market.py +188 -0
  16. fundedness/models/simulation.py +80 -0
  17. fundedness/models/tax.py +125 -0
  18. fundedness/models/utility.py +154 -0
  19. fundedness/optimize.py +473 -0
  20. fundedness/policies.py +204 -0
  21. fundedness/risk.py +72 -0
  22. fundedness/simulate.py +559 -0
  23. fundedness/viz/__init__.py +33 -0
  24. fundedness/viz/colors.py +110 -0
  25. fundedness/viz/comparison.py +294 -0
  26. fundedness/viz/fan_chart.py +193 -0
  27. fundedness/viz/histogram.py +225 -0
  28. fundedness/viz/optimal.py +542 -0
  29. fundedness/viz/survival.py +230 -0
  30. fundedness/viz/tornado.py +236 -0
  31. fundedness/viz/waterfall.py +203 -0
  32. fundedness/withdrawals/__init__.py +27 -0
  33. fundedness/withdrawals/base.py +116 -0
  34. fundedness/withdrawals/comparison.py +230 -0
  35. fundedness/withdrawals/fixed_swr.py +174 -0
  36. fundedness/withdrawals/guardrails.py +136 -0
  37. fundedness/withdrawals/merton_optimal.py +286 -0
  38. fundedness/withdrawals/rmd_style.py +203 -0
  39. fundedness/withdrawals/vpw.py +136 -0
  40. fundedness-0.2.2.dist-info/METADATA +299 -0
  41. fundedness-0.2.2.dist-info/RECORD +43 -0
  42. fundedness-0.2.2.dist-info/WHEEL +4 -0
  43. fundedness-0.2.2.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