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,301 @@
1
+ """Risk visualization functions for portfolio analysis.
2
+
3
+ Interactive Plotly plots for risk metrics including:
4
+ - Rolling volatility
5
+ - Rolling Sharpe ratio
6
+ - Rolling beta
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ import plotly.graph_objects as go
14
+
15
+ from ml4t.diagnostic.visualization.core import (
16
+ create_base_figure,
17
+ get_theme_config,
18
+ validate_theme,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from ml4t.diagnostic.evaluation.portfolio_analysis import (
23
+ PortfolioAnalysis,
24
+ RollingMetricsResult,
25
+ )
26
+
27
+
28
+ def plot_rolling_volatility(
29
+ analysis: PortfolioAnalysis | None = None,
30
+ rolling_result: RollingMetricsResult | None = None,
31
+ windows: list[int] | None = None,
32
+ theme: str | None = None,
33
+ height: int = 400,
34
+ width: int | None = None,
35
+ ) -> go.Figure:
36
+ """Plot rolling annualized volatility.
37
+
38
+ Parameters
39
+ ----------
40
+ analysis : PortfolioAnalysis, optional
41
+ Portfolio analysis object (used if rolling_result not provided)
42
+ rolling_result : RollingMetricsResult, optional
43
+ Pre-computed rolling metrics
44
+ windows : list[int], optional
45
+ Rolling windows to plot. Default [21, 63, 252].
46
+ theme : str, optional
47
+ Plot theme
48
+ height : int, default 400
49
+ Figure height
50
+ width : int, optional
51
+ Figure width
52
+
53
+ Returns
54
+ -------
55
+ go.Figure
56
+ Interactive Plotly figure
57
+ """
58
+ theme = validate_theme(theme)
59
+ theme_config = get_theme_config(theme)
60
+
61
+ if windows is None:
62
+ windows = [21, 63, 252]
63
+
64
+ # Get rolling metrics
65
+ if rolling_result is None:
66
+ if analysis is None:
67
+ raise ValueError("Must provide either analysis or rolling_result")
68
+ rolling_result = analysis.compute_rolling_metrics(
69
+ windows=windows,
70
+ metrics=["volatility"],
71
+ )
72
+
73
+ fig = create_base_figure(
74
+ title="Rolling Volatility (Annualized)",
75
+ xaxis_title="Date",
76
+ yaxis_title="Volatility",
77
+ height=height,
78
+ width=width,
79
+ theme=theme,
80
+ )
81
+
82
+ dates = rolling_result.dates.to_list()
83
+
84
+ for i, window in enumerate(windows):
85
+ if window in rolling_result.volatility:
86
+ vol = rolling_result.volatility[window].to_numpy()
87
+ color = theme_config["colorway"][i % len(theme_config["colorway"])]
88
+
89
+ fig.add_trace(
90
+ go.Scatter(
91
+ x=dates,
92
+ y=vol,
93
+ mode="lines",
94
+ name=f"{window}d",
95
+ line={"color": color, "width": 1.5},
96
+ hovertemplate=f"{window}d Vol: %{{y:.2%}}<extra></extra>",
97
+ )
98
+ )
99
+
100
+ fig.update_layout(
101
+ legend={"yanchor": "top", "y": 0.99, "xanchor": "right", "x": 0.99},
102
+ hovermode="x unified",
103
+ yaxis={"tickformat": ".0%"},
104
+ )
105
+
106
+ return fig
107
+
108
+
109
+ def plot_rolling_sharpe(
110
+ analysis: PortfolioAnalysis | None = None,
111
+ rolling_result: RollingMetricsResult | None = None,
112
+ windows: list[int] | None = None,
113
+ theme: str | None = None,
114
+ height: int = 400,
115
+ width: int | None = None,
116
+ ) -> go.Figure:
117
+ """Plot rolling Sharpe ratio.
118
+
119
+ Parameters
120
+ ----------
121
+ analysis : PortfolioAnalysis, optional
122
+ Portfolio analysis object (used if rolling_result not provided)
123
+ rolling_result : RollingMetricsResult, optional
124
+ Pre-computed rolling metrics
125
+ windows : list[int], optional
126
+ Rolling windows to plot. Default [63, 126, 252].
127
+ theme : str, optional
128
+ Plot theme
129
+ height : int, default 400
130
+ Figure height
131
+ width : int, optional
132
+ Figure width
133
+
134
+ Returns
135
+ -------
136
+ go.Figure
137
+ Interactive Plotly figure
138
+ """
139
+ theme = validate_theme(theme)
140
+ theme_config = get_theme_config(theme)
141
+
142
+ if windows is None:
143
+ windows = [63, 126, 252]
144
+
145
+ # Get rolling metrics
146
+ if rolling_result is None:
147
+ if analysis is None:
148
+ raise ValueError("Must provide either analysis or rolling_result")
149
+ rolling_result = analysis.compute_rolling_metrics(
150
+ windows=windows,
151
+ metrics=["sharpe"],
152
+ )
153
+
154
+ fig = create_base_figure(
155
+ title="Rolling Sharpe Ratio",
156
+ xaxis_title="Date",
157
+ yaxis_title="Sharpe Ratio",
158
+ height=height,
159
+ width=width,
160
+ theme=theme,
161
+ )
162
+
163
+ dates = rolling_result.dates.to_list()
164
+
165
+ for i, window in enumerate(windows):
166
+ if window in rolling_result.sharpe:
167
+ sharpe = rolling_result.sharpe[window].to_numpy()
168
+ color = theme_config["colorway"][i % len(theme_config["colorway"])]
169
+
170
+ fig.add_trace(
171
+ go.Scatter(
172
+ x=dates,
173
+ y=sharpe,
174
+ mode="lines",
175
+ name=f"{window}d",
176
+ line={"color": color, "width": 1.5},
177
+ hovertemplate=f"{window}d Sharpe: %{{y:.2f}}<extra></extra>",
178
+ )
179
+ )
180
+
181
+ # Add reference lines
182
+ fig.add_hline(y=0, line_dash="solid", line_color="gray", line_width=1)
183
+ fig.add_hline(
184
+ y=1,
185
+ line_dash="dash",
186
+ line_color="green",
187
+ line_width=1,
188
+ annotation_text="Good (1.0)",
189
+ annotation_position="right",
190
+ )
191
+ fig.add_hline(
192
+ y=2,
193
+ line_dash="dash",
194
+ line_color="darkgreen",
195
+ line_width=1,
196
+ annotation_text="Excellent (2.0)",
197
+ annotation_position="right",
198
+ )
199
+
200
+ fig.update_layout(
201
+ legend={"yanchor": "top", "y": 0.99, "xanchor": "left", "x": 0.01},
202
+ hovermode="x unified",
203
+ )
204
+
205
+ return fig
206
+
207
+
208
+ def plot_rolling_beta(
209
+ analysis: PortfolioAnalysis | None = None,
210
+ rolling_result: RollingMetricsResult | None = None,
211
+ window: int = 126,
212
+ theme: str | None = None,
213
+ height: int = 400,
214
+ width: int | None = None,
215
+ ) -> go.Figure:
216
+ """Plot rolling beta vs benchmark.
217
+
218
+ Parameters
219
+ ----------
220
+ analysis : PortfolioAnalysis, optional
221
+ Portfolio analysis object (must have benchmark)
222
+ rolling_result : RollingMetricsResult, optional
223
+ Pre-computed rolling metrics
224
+ window : int, default 126
225
+ Rolling window size (6 months)
226
+ theme : str, optional
227
+ Plot theme
228
+ height : int, default 400
229
+ Figure height
230
+ width : int, optional
231
+ Figure width
232
+
233
+ Returns
234
+ -------
235
+ go.Figure
236
+ Interactive Plotly figure
237
+
238
+ Raises
239
+ ------
240
+ ValueError
241
+ If no benchmark data is available
242
+ """
243
+ theme = validate_theme(theme)
244
+ theme_config = get_theme_config(theme)
245
+
246
+ # Get rolling metrics
247
+ if rolling_result is None:
248
+ if analysis is None:
249
+ raise ValueError("Must provide either analysis or rolling_result")
250
+ if not analysis.has_benchmark:
251
+ raise ValueError("Benchmark data required for beta calculation")
252
+
253
+ rolling_result = analysis.compute_rolling_metrics(
254
+ windows=[window],
255
+ metrics=["beta"],
256
+ )
257
+
258
+ if not rolling_result.beta:
259
+ raise ValueError("No beta data available")
260
+
261
+ fig = create_base_figure(
262
+ title=f"Rolling Beta ({window}d)",
263
+ xaxis_title="Date",
264
+ yaxis_title="Beta",
265
+ height=height,
266
+ width=width,
267
+ theme=theme,
268
+ )
269
+
270
+ dates = rolling_result.dates.to_list()
271
+ beta = rolling_result.beta[window].to_numpy()
272
+
273
+ fig.add_trace(
274
+ go.Scatter(
275
+ x=dates,
276
+ y=beta,
277
+ mode="lines",
278
+ name="Beta",
279
+ line={"color": theme_config["colorway"][0], "width": 2},
280
+ fill="tozeroy",
281
+ fillcolor=f"rgba({int(theme_config['colorway'][0][1:3], 16)}, "
282
+ f"{int(theme_config['colorway'][0][3:5], 16)}, "
283
+ f"{int(theme_config['colorway'][0][5:7], 16)}, 0.2)",
284
+ hovertemplate="Date: %{x}<br>Beta: %{y:.2f}<extra></extra>",
285
+ )
286
+ )
287
+
288
+ # Reference lines
289
+ fig.add_hline(y=0, line_dash="solid", line_color="gray", line_width=1)
290
+ fig.add_hline(
291
+ y=1,
292
+ line_dash="dash",
293
+ line_color="orange",
294
+ line_width=1,
295
+ annotation_text="Market (1.0)",
296
+ annotation_position="right",
297
+ )
298
+
299
+ fig.update_layout(hovermode="x unified")
300
+
301
+ return fig