ml4t-diagnostic 0.1.0a1__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 (242) hide show
  1. ml4t/diagnostic/AGENT.md +25 -0
  2. ml4t/diagnostic/__init__.py +166 -0
  3. ml4t/diagnostic/backends/__init__.py +10 -0
  4. ml4t/diagnostic/backends/adapter.py +192 -0
  5. ml4t/diagnostic/backends/polars_backend.py +899 -0
  6. ml4t/diagnostic/caching/__init__.py +40 -0
  7. ml4t/diagnostic/caching/cache.py +331 -0
  8. ml4t/diagnostic/caching/decorators.py +131 -0
  9. ml4t/diagnostic/caching/smart_cache.py +339 -0
  10. ml4t/diagnostic/config/AGENT.md +24 -0
  11. ml4t/diagnostic/config/README.md +267 -0
  12. ml4t/diagnostic/config/__init__.py +219 -0
  13. ml4t/diagnostic/config/barrier_config.py +277 -0
  14. ml4t/diagnostic/config/base.py +301 -0
  15. ml4t/diagnostic/config/event_config.py +148 -0
  16. ml4t/diagnostic/config/feature_config.py +404 -0
  17. ml4t/diagnostic/config/multi_signal_config.py +55 -0
  18. ml4t/diagnostic/config/portfolio_config.py +215 -0
  19. ml4t/diagnostic/config/report_config.py +391 -0
  20. ml4t/diagnostic/config/sharpe_config.py +202 -0
  21. ml4t/diagnostic/config/signal_config.py +206 -0
  22. ml4t/diagnostic/config/trade_analysis_config.py +310 -0
  23. ml4t/diagnostic/config/validation.py +279 -0
  24. ml4t/diagnostic/core/__init__.py +29 -0
  25. ml4t/diagnostic/core/numba_utils.py +315 -0
  26. ml4t/diagnostic/core/purging.py +372 -0
  27. ml4t/diagnostic/core/sampling.py +471 -0
  28. ml4t/diagnostic/errors/__init__.py +205 -0
  29. ml4t/diagnostic/evaluation/AGENT.md +26 -0
  30. ml4t/diagnostic/evaluation/__init__.py +437 -0
  31. ml4t/diagnostic/evaluation/autocorrelation.py +531 -0
  32. ml4t/diagnostic/evaluation/barrier_analysis.py +1050 -0
  33. ml4t/diagnostic/evaluation/binary_metrics.py +910 -0
  34. ml4t/diagnostic/evaluation/dashboard.py +715 -0
  35. ml4t/diagnostic/evaluation/diagnostic_plots.py +1037 -0
  36. ml4t/diagnostic/evaluation/distribution/__init__.py +499 -0
  37. ml4t/diagnostic/evaluation/distribution/moments.py +299 -0
  38. ml4t/diagnostic/evaluation/distribution/tails.py +777 -0
  39. ml4t/diagnostic/evaluation/distribution/tests.py +470 -0
  40. ml4t/diagnostic/evaluation/drift/__init__.py +139 -0
  41. ml4t/diagnostic/evaluation/drift/analysis.py +432 -0
  42. ml4t/diagnostic/evaluation/drift/domain_classifier.py +517 -0
  43. ml4t/diagnostic/evaluation/drift/population_stability_index.py +310 -0
  44. ml4t/diagnostic/evaluation/drift/wasserstein.py +388 -0
  45. ml4t/diagnostic/evaluation/event_analysis.py +647 -0
  46. ml4t/diagnostic/evaluation/excursion.py +390 -0
  47. ml4t/diagnostic/evaluation/feature_diagnostics.py +873 -0
  48. ml4t/diagnostic/evaluation/feature_outcome.py +666 -0
  49. ml4t/diagnostic/evaluation/framework.py +935 -0
  50. ml4t/diagnostic/evaluation/metric_registry.py +255 -0
  51. ml4t/diagnostic/evaluation/metrics/AGENT.md +23 -0
  52. ml4t/diagnostic/evaluation/metrics/__init__.py +133 -0
  53. ml4t/diagnostic/evaluation/metrics/basic.py +160 -0
  54. ml4t/diagnostic/evaluation/metrics/conditional_ic.py +469 -0
  55. ml4t/diagnostic/evaluation/metrics/feature_outcome.py +475 -0
  56. ml4t/diagnostic/evaluation/metrics/ic_statistics.py +446 -0
  57. ml4t/diagnostic/evaluation/metrics/importance_analysis.py +338 -0
  58. ml4t/diagnostic/evaluation/metrics/importance_classical.py +375 -0
  59. ml4t/diagnostic/evaluation/metrics/importance_mda.py +371 -0
  60. ml4t/diagnostic/evaluation/metrics/importance_shap.py +715 -0
  61. ml4t/diagnostic/evaluation/metrics/information_coefficient.py +527 -0
  62. ml4t/diagnostic/evaluation/metrics/interactions.py +772 -0
  63. ml4t/diagnostic/evaluation/metrics/monotonicity.py +226 -0
  64. ml4t/diagnostic/evaluation/metrics/risk_adjusted.py +324 -0
  65. ml4t/diagnostic/evaluation/multi_signal.py +550 -0
  66. ml4t/diagnostic/evaluation/portfolio_analysis/__init__.py +83 -0
  67. ml4t/diagnostic/evaluation/portfolio_analysis/analysis.py +734 -0
  68. ml4t/diagnostic/evaluation/portfolio_analysis/metrics.py +589 -0
  69. ml4t/diagnostic/evaluation/portfolio_analysis/results.py +334 -0
  70. ml4t/diagnostic/evaluation/report_generation.py +824 -0
  71. ml4t/diagnostic/evaluation/signal_selector.py +452 -0
  72. ml4t/diagnostic/evaluation/stat_registry.py +139 -0
  73. ml4t/diagnostic/evaluation/stationarity/__init__.py +97 -0
  74. ml4t/diagnostic/evaluation/stationarity/analysis.py +518 -0
  75. ml4t/diagnostic/evaluation/stationarity/augmented_dickey_fuller.py +296 -0
  76. ml4t/diagnostic/evaluation/stationarity/kpss_test.py +308 -0
  77. ml4t/diagnostic/evaluation/stationarity/phillips_perron.py +365 -0
  78. ml4t/diagnostic/evaluation/stats/AGENT.md +43 -0
  79. ml4t/diagnostic/evaluation/stats/__init__.py +191 -0
  80. ml4t/diagnostic/evaluation/stats/backtest_overfitting.py +219 -0
  81. ml4t/diagnostic/evaluation/stats/bootstrap.py +228 -0
  82. ml4t/diagnostic/evaluation/stats/deflated_sharpe_ratio.py +591 -0
  83. ml4t/diagnostic/evaluation/stats/false_discovery_rate.py +295 -0
  84. ml4t/diagnostic/evaluation/stats/hac_standard_errors.py +108 -0
  85. ml4t/diagnostic/evaluation/stats/minimum_track_record.py +408 -0
  86. ml4t/diagnostic/evaluation/stats/moments.py +164 -0
  87. ml4t/diagnostic/evaluation/stats/rademacher_adjustment.py +436 -0
  88. ml4t/diagnostic/evaluation/stats/reality_check.py +155 -0
  89. ml4t/diagnostic/evaluation/stats/sharpe_inference.py +219 -0
  90. ml4t/diagnostic/evaluation/themes.py +330 -0
  91. ml4t/diagnostic/evaluation/threshold_analysis.py +957 -0
  92. ml4t/diagnostic/evaluation/trade_analysis.py +1136 -0
  93. ml4t/diagnostic/evaluation/trade_dashboard/__init__.py +32 -0
  94. ml4t/diagnostic/evaluation/trade_dashboard/app.py +315 -0
  95. ml4t/diagnostic/evaluation/trade_dashboard/export/__init__.py +18 -0
  96. ml4t/diagnostic/evaluation/trade_dashboard/export/csv.py +82 -0
  97. ml4t/diagnostic/evaluation/trade_dashboard/export/html.py +276 -0
  98. ml4t/diagnostic/evaluation/trade_dashboard/io.py +166 -0
  99. ml4t/diagnostic/evaluation/trade_dashboard/normalize.py +304 -0
  100. ml4t/diagnostic/evaluation/trade_dashboard/stats.py +386 -0
  101. ml4t/diagnostic/evaluation/trade_dashboard/style.py +79 -0
  102. ml4t/diagnostic/evaluation/trade_dashboard/tabs/__init__.py +21 -0
  103. ml4t/diagnostic/evaluation/trade_dashboard/tabs/patterns.py +354 -0
  104. ml4t/diagnostic/evaluation/trade_dashboard/tabs/shap_analysis.py +280 -0
  105. ml4t/diagnostic/evaluation/trade_dashboard/tabs/stat_validation.py +186 -0
  106. ml4t/diagnostic/evaluation/trade_dashboard/tabs/worst_trades.py +236 -0
  107. ml4t/diagnostic/evaluation/trade_dashboard/types.py +129 -0
  108. ml4t/diagnostic/evaluation/trade_shap/__init__.py +102 -0
  109. ml4t/diagnostic/evaluation/trade_shap/alignment.py +188 -0
  110. ml4t/diagnostic/evaluation/trade_shap/characterize.py +413 -0
  111. ml4t/diagnostic/evaluation/trade_shap/cluster.py +302 -0
  112. ml4t/diagnostic/evaluation/trade_shap/explain.py +208 -0
  113. ml4t/diagnostic/evaluation/trade_shap/hypotheses/__init__.py +23 -0
  114. ml4t/diagnostic/evaluation/trade_shap/hypotheses/generator.py +290 -0
  115. ml4t/diagnostic/evaluation/trade_shap/hypotheses/matcher.py +251 -0
  116. ml4t/diagnostic/evaluation/trade_shap/hypotheses/templates.yaml +467 -0
  117. ml4t/diagnostic/evaluation/trade_shap/models.py +386 -0
  118. ml4t/diagnostic/evaluation/trade_shap/normalize.py +116 -0
  119. ml4t/diagnostic/evaluation/trade_shap/pipeline.py +263 -0
  120. ml4t/diagnostic/evaluation/trade_shap_dashboard.py +283 -0
  121. ml4t/diagnostic/evaluation/trade_shap_diagnostics.py +588 -0
  122. ml4t/diagnostic/evaluation/validated_cv.py +535 -0
  123. ml4t/diagnostic/evaluation/visualization.py +1050 -0
  124. ml4t/diagnostic/evaluation/volatility/__init__.py +45 -0
  125. ml4t/diagnostic/evaluation/volatility/analysis.py +351 -0
  126. ml4t/diagnostic/evaluation/volatility/arch.py +258 -0
  127. ml4t/diagnostic/evaluation/volatility/garch.py +460 -0
  128. ml4t/diagnostic/integration/__init__.py +48 -0
  129. ml4t/diagnostic/integration/backtest_contract.py +671 -0
  130. ml4t/diagnostic/integration/data_contract.py +316 -0
  131. ml4t/diagnostic/integration/engineer_contract.py +226 -0
  132. ml4t/diagnostic/logging/__init__.py +77 -0
  133. ml4t/diagnostic/logging/logger.py +245 -0
  134. ml4t/diagnostic/logging/performance.py +234 -0
  135. ml4t/diagnostic/logging/progress.py +234 -0
  136. ml4t/diagnostic/logging/wandb.py +412 -0
  137. ml4t/diagnostic/metrics/__init__.py +9 -0
  138. ml4t/diagnostic/metrics/percentiles.py +128 -0
  139. ml4t/diagnostic/py.typed +1 -0
  140. ml4t/diagnostic/reporting/__init__.py +43 -0
  141. ml4t/diagnostic/reporting/base.py +130 -0
  142. ml4t/diagnostic/reporting/html_renderer.py +275 -0
  143. ml4t/diagnostic/reporting/json_renderer.py +51 -0
  144. ml4t/diagnostic/reporting/markdown_renderer.py +117 -0
  145. ml4t/diagnostic/results/AGENT.md +24 -0
  146. ml4t/diagnostic/results/__init__.py +105 -0
  147. ml4t/diagnostic/results/barrier_results/__init__.py +36 -0
  148. ml4t/diagnostic/results/barrier_results/hit_rate.py +304 -0
  149. ml4t/diagnostic/results/barrier_results/precision_recall.py +266 -0
  150. ml4t/diagnostic/results/barrier_results/profit_factor.py +297 -0
  151. ml4t/diagnostic/results/barrier_results/tearsheet.py +397 -0
  152. ml4t/diagnostic/results/barrier_results/time_to_target.py +305 -0
  153. ml4t/diagnostic/results/barrier_results/validation.py +38 -0
  154. ml4t/diagnostic/results/base.py +177 -0
  155. ml4t/diagnostic/results/event_results.py +349 -0
  156. ml4t/diagnostic/results/feature_results.py +787 -0
  157. ml4t/diagnostic/results/multi_signal_results.py +431 -0
  158. ml4t/diagnostic/results/portfolio_results.py +281 -0
  159. ml4t/diagnostic/results/sharpe_results.py +448 -0
  160. ml4t/diagnostic/results/signal_results/__init__.py +74 -0
  161. ml4t/diagnostic/results/signal_results/ic.py +581 -0
  162. ml4t/diagnostic/results/signal_results/irtc.py +110 -0
  163. ml4t/diagnostic/results/signal_results/quantile.py +392 -0
  164. ml4t/diagnostic/results/signal_results/tearsheet.py +456 -0
  165. ml4t/diagnostic/results/signal_results/turnover.py +213 -0
  166. ml4t/diagnostic/results/signal_results/validation.py +147 -0
  167. ml4t/diagnostic/signal/AGENT.md +17 -0
  168. ml4t/diagnostic/signal/__init__.py +69 -0
  169. ml4t/diagnostic/signal/_report.py +152 -0
  170. ml4t/diagnostic/signal/_utils.py +261 -0
  171. ml4t/diagnostic/signal/core.py +275 -0
  172. ml4t/diagnostic/signal/quantile.py +148 -0
  173. ml4t/diagnostic/signal/result.py +214 -0
  174. ml4t/diagnostic/signal/signal_ic.py +129 -0
  175. ml4t/diagnostic/signal/turnover.py +182 -0
  176. ml4t/diagnostic/splitters/AGENT.md +19 -0
  177. ml4t/diagnostic/splitters/__init__.py +36 -0
  178. ml4t/diagnostic/splitters/base.py +501 -0
  179. ml4t/diagnostic/splitters/calendar.py +421 -0
  180. ml4t/diagnostic/splitters/calendar_config.py +91 -0
  181. ml4t/diagnostic/splitters/combinatorial.py +1064 -0
  182. ml4t/diagnostic/splitters/config.py +322 -0
  183. ml4t/diagnostic/splitters/cpcv/__init__.py +57 -0
  184. ml4t/diagnostic/splitters/cpcv/combinations.py +119 -0
  185. ml4t/diagnostic/splitters/cpcv/partitioning.py +263 -0
  186. ml4t/diagnostic/splitters/cpcv/purge_engine.py +379 -0
  187. ml4t/diagnostic/splitters/cpcv/windows.py +190 -0
  188. ml4t/diagnostic/splitters/group_isolation.py +329 -0
  189. ml4t/diagnostic/splitters/persistence.py +316 -0
  190. ml4t/diagnostic/splitters/utils.py +207 -0
  191. ml4t/diagnostic/splitters/walk_forward.py +757 -0
  192. ml4t/diagnostic/utils/__init__.py +42 -0
  193. ml4t/diagnostic/utils/config.py +542 -0
  194. ml4t/diagnostic/utils/dependencies.py +318 -0
  195. ml4t/diagnostic/utils/sessions.py +127 -0
  196. ml4t/diagnostic/validation/__init__.py +54 -0
  197. ml4t/diagnostic/validation/dataframe.py +274 -0
  198. ml4t/diagnostic/validation/returns.py +280 -0
  199. ml4t/diagnostic/validation/timeseries.py +299 -0
  200. ml4t/diagnostic/visualization/AGENT.md +19 -0
  201. ml4t/diagnostic/visualization/__init__.py +223 -0
  202. ml4t/diagnostic/visualization/backtest/__init__.py +98 -0
  203. ml4t/diagnostic/visualization/backtest/cost_attribution.py +762 -0
  204. ml4t/diagnostic/visualization/backtest/executive_summary.py +895 -0
  205. ml4t/diagnostic/visualization/backtest/interactive_controls.py +673 -0
  206. ml4t/diagnostic/visualization/backtest/statistical_validity.py +874 -0
  207. ml4t/diagnostic/visualization/backtest/tearsheet.py +565 -0
  208. ml4t/diagnostic/visualization/backtest/template_system.py +373 -0
  209. ml4t/diagnostic/visualization/backtest/trade_plots.py +1172 -0
  210. ml4t/diagnostic/visualization/barrier_plots.py +782 -0
  211. ml4t/diagnostic/visualization/core.py +1060 -0
  212. ml4t/diagnostic/visualization/dashboards/__init__.py +36 -0
  213. ml4t/diagnostic/visualization/dashboards/base.py +582 -0
  214. ml4t/diagnostic/visualization/dashboards/importance.py +801 -0
  215. ml4t/diagnostic/visualization/dashboards/interaction.py +263 -0
  216. ml4t/diagnostic/visualization/dashboards.py +43 -0
  217. ml4t/diagnostic/visualization/data_extraction/__init__.py +48 -0
  218. ml4t/diagnostic/visualization/data_extraction/importance.py +649 -0
  219. ml4t/diagnostic/visualization/data_extraction/interaction.py +504 -0
  220. ml4t/diagnostic/visualization/data_extraction/types.py +113 -0
  221. ml4t/diagnostic/visualization/data_extraction/validation.py +66 -0
  222. ml4t/diagnostic/visualization/feature_plots.py +888 -0
  223. ml4t/diagnostic/visualization/interaction_plots.py +618 -0
  224. ml4t/diagnostic/visualization/portfolio/__init__.py +41 -0
  225. ml4t/diagnostic/visualization/portfolio/dashboard.py +514 -0
  226. ml4t/diagnostic/visualization/portfolio/drawdown_plots.py +341 -0
  227. ml4t/diagnostic/visualization/portfolio/returns_plots.py +487 -0
  228. ml4t/diagnostic/visualization/portfolio/risk_plots.py +301 -0
  229. ml4t/diagnostic/visualization/report_generation.py +1343 -0
  230. ml4t/diagnostic/visualization/signal/__init__.py +103 -0
  231. ml4t/diagnostic/visualization/signal/dashboard.py +911 -0
  232. ml4t/diagnostic/visualization/signal/event_plots.py +514 -0
  233. ml4t/diagnostic/visualization/signal/ic_plots.py +635 -0
  234. ml4t/diagnostic/visualization/signal/multi_signal_dashboard.py +974 -0
  235. ml4t/diagnostic/visualization/signal/multi_signal_plots.py +603 -0
  236. ml4t/diagnostic/visualization/signal/quantile_plots.py +625 -0
  237. ml4t/diagnostic/visualization/signal/turnover_plots.py +400 -0
  238. ml4t/diagnostic/visualization/trade_shap/__init__.py +90 -0
  239. ml4t_diagnostic-0.1.0a1.dist-info/METADATA +1044 -0
  240. ml4t_diagnostic-0.1.0a1.dist-info/RECORD +242 -0
  241. ml4t_diagnostic-0.1.0a1.dist-info/WHEEL +4 -0
  242. ml4t_diagnostic-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,341 @@
1
+ """Drawdown visualization functions for portfolio analysis.
2
+
3
+ Interactive Plotly plots for drawdown analysis including:
4
+ - Underwater (drawdown) curve
5
+ - Top drawdown periods
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ import numpy as np
13
+ import plotly.graph_objects as go
14
+ from plotly.subplots import make_subplots
15
+
16
+ from ml4t.diagnostic.visualization.core import (
17
+ create_base_figure,
18
+ get_theme_config,
19
+ validate_theme,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from ml4t.diagnostic.evaluation.portfolio_analysis import (
24
+ DrawdownResult,
25
+ PortfolioAnalysis,
26
+ )
27
+
28
+
29
+ def plot_drawdown_underwater(
30
+ analysis: PortfolioAnalysis | None = None,
31
+ drawdown_result: DrawdownResult | None = None,
32
+ theme: str | None = None,
33
+ height: int = 300,
34
+ width: int | None = None,
35
+ ) -> go.Figure:
36
+ """Plot underwater (drawdown) curve over time.
37
+
38
+ Parameters
39
+ ----------
40
+ analysis : PortfolioAnalysis, optional
41
+ Portfolio analysis object (used if drawdown_result not provided)
42
+ drawdown_result : DrawdownResult, optional
43
+ Pre-computed drawdown analysis
44
+ theme : str, optional
45
+ Plot theme
46
+ height : int, default 300
47
+ Figure height
48
+ width : int, optional
49
+ Figure width
50
+
51
+ Returns
52
+ -------
53
+ go.Figure
54
+ Interactive Plotly figure
55
+ """
56
+ theme = validate_theme(theme)
57
+ theme_config = get_theme_config(theme)
58
+
59
+ # Get drawdown data
60
+ if drawdown_result is None:
61
+ if analysis is None:
62
+ raise ValueError("Must provide either analysis or drawdown_result")
63
+ drawdown_result = analysis.compute_drawdown_analysis()
64
+
65
+ fig = create_base_figure(
66
+ title="Underwater (Drawdown) Curve",
67
+ xaxis_title="Date",
68
+ yaxis_title="Drawdown",
69
+ height=height,
70
+ width=width,
71
+ theme=theme,
72
+ )
73
+
74
+ dates = drawdown_result.dates.to_list()
75
+ underwater = drawdown_result.underwater_curve.to_numpy()
76
+
77
+ # Fill area below zero
78
+ fig.add_trace(
79
+ go.Scatter(
80
+ x=dates,
81
+ y=underwater,
82
+ mode="lines",
83
+ name="Drawdown",
84
+ line={"color": theme_config["colorway"][1], "width": 1},
85
+ fill="tozeroy",
86
+ fillcolor="rgba(231, 76, 60, 0.3)", # Semi-transparent red
87
+ hovertemplate="Date: %{x}<br>Drawdown: %{y:.2%}<extra></extra>",
88
+ )
89
+ )
90
+
91
+ # Mark max drawdown
92
+ max_dd_idx = np.argmin(underwater)
93
+ fig.add_trace(
94
+ go.Scatter(
95
+ x=[dates[max_dd_idx]],
96
+ y=[underwater[max_dd_idx]],
97
+ mode="markers",
98
+ name=f"Max DD: {underwater[max_dd_idx]:.1%}",
99
+ marker={"color": "darkred", "size": 10, "symbol": "circle"},
100
+ hovertemplate="Max Drawdown<br>Date: %{x}<br>DD: %{y:.2%}<extra></extra>",
101
+ )
102
+ )
103
+
104
+ fig.update_layout(
105
+ legend={"yanchor": "bottom", "y": 0.01, "xanchor": "right", "x": 0.99},
106
+ hovermode="x unified",
107
+ yaxis={"tickformat": ".0%"},
108
+ )
109
+
110
+ return fig
111
+
112
+
113
+ def plot_drawdown_periods(
114
+ analysis: PortfolioAnalysis | None = None,
115
+ drawdown_result: DrawdownResult | None = None,
116
+ top_n: int = 5,
117
+ theme: str | None = None,
118
+ height: int = 400,
119
+ width: int | None = None,
120
+ ) -> go.Figure:
121
+ """Plot top N drawdown periods with details.
122
+
123
+ Shows drawdown depth, duration, and recovery time for the
124
+ worst drawdown periods.
125
+
126
+ Parameters
127
+ ----------
128
+ analysis : PortfolioAnalysis, optional
129
+ Portfolio analysis object (used if drawdown_result not provided)
130
+ drawdown_result : DrawdownResult, optional
131
+ Pre-computed drawdown analysis
132
+ top_n : int, default 5
133
+ Number of top drawdowns to show
134
+ theme : str, optional
135
+ Plot theme
136
+ height : int, default 400
137
+ Figure height
138
+ width : int, optional
139
+ Figure width
140
+
141
+ Returns
142
+ -------
143
+ go.Figure
144
+ Interactive Plotly figure
145
+ """
146
+ theme = validate_theme(theme)
147
+ theme_config = get_theme_config(theme)
148
+
149
+ # Get drawdown data
150
+ if drawdown_result is None:
151
+ if analysis is None:
152
+ raise ValueError("Must provide either analysis or drawdown_result")
153
+ drawdown_result = analysis.compute_drawdown_analysis(top_n=top_n)
154
+
155
+ top_drawdowns = drawdown_result.top_drawdowns[:top_n]
156
+
157
+ if not top_drawdowns:
158
+ # No drawdowns found
159
+ fig = create_base_figure(
160
+ title="Top Drawdown Periods",
161
+ height=height,
162
+ width=width,
163
+ theme=theme,
164
+ )
165
+ fig.add_annotation(
166
+ text="No significant drawdowns found",
167
+ xref="paper",
168
+ yref="paper",
169
+ x=0.5,
170
+ y=0.5,
171
+ showarrow=False,
172
+ font={"size": 14},
173
+ )
174
+ return fig
175
+
176
+ # Create horizontal bar chart
177
+ depths = [abs(d.depth) for d in top_drawdowns]
178
+ labels = [f"#{i + 1}" for i in range(len(top_drawdowns))]
179
+
180
+ fig = go.Figure()
181
+
182
+ # Drawdown depth bars
183
+ fig.add_trace(
184
+ go.Bar(
185
+ y=labels,
186
+ x=depths,
187
+ orientation="h",
188
+ name="Drawdown Depth",
189
+ marker_color=theme_config["colorway"][1],
190
+ text=[f"{d:.1%}" for d in depths],
191
+ textposition="outside",
192
+ hovertemplate=(
193
+ "Drawdown: %{x:.2%}<br>"
194
+ "Peak: %{customdata[0]}<br>"
195
+ "Valley: %{customdata[1]}<br>"
196
+ "Recovery: %{customdata[2]}<br>"
197
+ "Duration: %{customdata[3]} days<extra></extra>"
198
+ ),
199
+ customdata=[
200
+ [
201
+ str(d.peak_date)[:10],
202
+ str(d.valley_date)[:10],
203
+ str(d.recovery_date)[:10] if d.recovery_date else "Not recovered",
204
+ d.duration_days,
205
+ ]
206
+ for d in top_drawdowns
207
+ ],
208
+ )
209
+ )
210
+
211
+ fig.update_layout(
212
+ title=f"Top {len(top_drawdowns)} Drawdown Periods",
213
+ xaxis_title="Drawdown Depth",
214
+ yaxis_title="",
215
+ height=height,
216
+ width=width,
217
+ xaxis={"tickformat": ".0%"},
218
+ yaxis={"autorange": "reversed"}, # #1 at top
219
+ showlegend=False,
220
+ )
221
+
222
+ return fig
223
+
224
+
225
+ def plot_drawdown_summary(
226
+ analysis: PortfolioAnalysis,
227
+ theme: str | None = None,
228
+ height: int = 600,
229
+ width: int | None = None,
230
+ ) -> go.Figure:
231
+ """Create combined drawdown summary with underwater curve and top periods.
232
+
233
+ Parameters
234
+ ----------
235
+ analysis : PortfolioAnalysis
236
+ Portfolio analysis object
237
+ theme : str, optional
238
+ Plot theme
239
+ height : int, default 600
240
+ Figure height
241
+ width : int, optional
242
+ Figure width
243
+
244
+ Returns
245
+ -------
246
+ go.Figure
247
+ Combined subplot figure
248
+ """
249
+ theme = validate_theme(theme)
250
+ theme_config = get_theme_config(theme)
251
+
252
+ drawdown_result = analysis.compute_drawdown_analysis(top_n=5)
253
+
254
+ fig = make_subplots(
255
+ rows=2,
256
+ cols=1,
257
+ row_heights=[0.6, 0.4],
258
+ subplot_titles=["Underwater Curve", "Top 5 Drawdowns"],
259
+ vertical_spacing=0.12,
260
+ )
261
+
262
+ # === Row 1: Underwater curve ===
263
+ dates = drawdown_result.dates.to_list()
264
+ underwater = drawdown_result.underwater_curve.to_numpy()
265
+
266
+ fig.add_trace(
267
+ go.Scatter(
268
+ x=dates,
269
+ y=underwater,
270
+ mode="lines",
271
+ name="Drawdown",
272
+ line={"color": theme_config["colorway"][1], "width": 1},
273
+ fill="tozeroy",
274
+ fillcolor="rgba(231, 76, 60, 0.3)",
275
+ hovertemplate="Date: %{x}<br>Drawdown: %{y:.2%}<extra></extra>",
276
+ ),
277
+ row=1,
278
+ col=1,
279
+ )
280
+
281
+ # Mark top drawdowns on the curve
282
+ for i, dd in enumerate(drawdown_result.top_drawdowns[:5]):
283
+ # Find valley date in dates list
284
+ try:
285
+ valley_idx = dates.index(dd.valley_date)
286
+ fig.add_trace(
287
+ go.Scatter(
288
+ x=[dates[valley_idx]],
289
+ y=[dd.depth],
290
+ mode="markers",
291
+ name=f"DD #{i + 1}",
292
+ marker={
293
+ "color": theme_config["colorway"][i % len(theme_config["colorway"])],
294
+ "size": 10,
295
+ "symbol": "circle",
296
+ },
297
+ showlegend=False,
298
+ ),
299
+ row=1,
300
+ col=1,
301
+ )
302
+ except (ValueError, IndexError):
303
+ pass
304
+
305
+ # === Row 2: Top drawdowns bar chart ===
306
+ top_drawdowns = drawdown_result.top_drawdowns[:5]
307
+ if top_drawdowns:
308
+ depths = [abs(d.depth) for d in top_drawdowns]
309
+ labels = [f"#{i + 1}: {str(d.valley_date)[:10]}" for i, d in enumerate(top_drawdowns)]
310
+
311
+ fig.add_trace(
312
+ go.Bar(
313
+ y=labels,
314
+ x=depths,
315
+ orientation="h",
316
+ marker_color=[
317
+ theme_config["colorway"][i % len(theme_config["colorway"])]
318
+ for i in range(len(top_drawdowns))
319
+ ],
320
+ text=[f"{d:.1%}" for d in depths],
321
+ textposition="outside",
322
+ showlegend=False,
323
+ hovertemplate="Drawdown: %{x:.2%}<extra></extra>",
324
+ ),
325
+ row=2,
326
+ col=1,
327
+ )
328
+
329
+ # Update layout
330
+ fig.update_layout(
331
+ title="Drawdown Analysis",
332
+ height=height,
333
+ width=width,
334
+ **theme_config["layout"],
335
+ )
336
+
337
+ fig.update_yaxes(tickformat=".0%", row=1, col=1)
338
+ fig.update_xaxes(tickformat=".0%", row=2, col=1)
339
+ fig.update_yaxes(autorange="reversed", row=2, col=1)
340
+
341
+ return fig