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,487 @@
1
+ """Returns visualization functions for portfolio analysis.
2
+
3
+ Interactive Plotly plots for return analysis including:
4
+ - Cumulative returns
5
+ - Rolling returns
6
+ - Annual returns bar charts
7
+ - Monthly returns heatmap
8
+ - Returns distribution
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ import numpy as np
16
+ import plotly.graph_objects as go
17
+
18
+ from ml4t.diagnostic.visualization.core import (
19
+ create_base_figure,
20
+ get_theme_config,
21
+ validate_theme,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from ml4t.diagnostic.evaluation.portfolio_analysis import (
26
+ PortfolioAnalysis,
27
+ RollingMetricsResult,
28
+ )
29
+
30
+
31
+ def plot_cumulative_returns(
32
+ analysis: PortfolioAnalysis,
33
+ theme: str | None = None,
34
+ show_benchmark: bool = True,
35
+ log_scale: bool = False,
36
+ height: int = 500,
37
+ width: int | None = None,
38
+ ) -> go.Figure:
39
+ """Plot cumulative returns over time.
40
+
41
+ Parameters
42
+ ----------
43
+ analysis : PortfolioAnalysis
44
+ Portfolio analysis object with returns data
45
+ theme : str, optional
46
+ Plot theme ("default", "dark", "print", "presentation")
47
+ show_benchmark : bool, default True
48
+ Show benchmark returns if available
49
+ log_scale : bool, default False
50
+ Use log scale for y-axis
51
+ height : int, default 500
52
+ Figure height in pixels
53
+ width : int, optional
54
+ Figure width in pixels
55
+
56
+ Returns
57
+ -------
58
+ go.Figure
59
+ Interactive Plotly figure
60
+ """
61
+ theme = validate_theme(theme)
62
+ theme_config = get_theme_config(theme)
63
+
64
+ fig = create_base_figure(
65
+ title="Cumulative Returns",
66
+ xaxis_title="Date",
67
+ yaxis_title="Cumulative Return",
68
+ height=height,
69
+ width=width,
70
+ theme=theme,
71
+ )
72
+
73
+ # Compute cumulative returns
74
+ cum_returns = (1 + analysis.returns).cumprod()
75
+ dates = analysis.dates.to_list()
76
+
77
+ # Strategy line
78
+ fig.add_trace(
79
+ go.Scatter(
80
+ x=dates,
81
+ y=cum_returns,
82
+ mode="lines",
83
+ name="Strategy",
84
+ line={"color": theme_config["colorway"][0], "width": 2},
85
+ hovertemplate="Date: %{x}<br>Return: %{y:.2%}<extra></extra>",
86
+ )
87
+ )
88
+
89
+ # Benchmark line
90
+ if show_benchmark and analysis.has_benchmark and analysis.benchmark is not None:
91
+ bench_cum = (1 + analysis.benchmark).cumprod()
92
+ fig.add_trace(
93
+ go.Scatter(
94
+ x=dates,
95
+ y=bench_cum,
96
+ mode="lines",
97
+ name="Benchmark",
98
+ line={"color": theme_config["colorway"][1], "width": 2, "dash": "dash"},
99
+ hovertemplate="Date: %{x}<br>Return: %{y:.2%}<extra></extra>",
100
+ )
101
+ )
102
+
103
+ if log_scale:
104
+ fig.update_yaxes(type="log")
105
+
106
+ fig.update_layout(
107
+ legend={"yanchor": "top", "y": 0.99, "xanchor": "left", "x": 0.01},
108
+ hovermode="x unified",
109
+ )
110
+
111
+ return fig
112
+
113
+
114
+ def plot_rolling_returns(
115
+ analysis: PortfolioAnalysis | None = None,
116
+ rolling_result: RollingMetricsResult | None = None,
117
+ windows: list[int] | None = None,
118
+ theme: str | None = None,
119
+ height: int = 500,
120
+ width: int | None = None,
121
+ ) -> go.Figure:
122
+ """Plot rolling returns for multiple windows.
123
+
124
+ Parameters
125
+ ----------
126
+ analysis : PortfolioAnalysis, optional
127
+ Portfolio analysis object (used if rolling_result not provided)
128
+ rolling_result : RollingMetricsResult, optional
129
+ Pre-computed rolling metrics
130
+ windows : list[int], optional
131
+ Rolling windows to plot. Default [21, 63, 252].
132
+ theme : str, optional
133
+ Plot theme
134
+ height : int, default 500
135
+ Figure height
136
+ width : int, optional
137
+ Figure width
138
+
139
+ Returns
140
+ -------
141
+ go.Figure
142
+ Interactive Plotly figure
143
+ """
144
+ theme = validate_theme(theme)
145
+ theme_config = get_theme_config(theme)
146
+
147
+ if windows is None:
148
+ windows = [21, 63, 252]
149
+
150
+ # Get rolling metrics
151
+ if rolling_result is None:
152
+ if analysis is None:
153
+ raise ValueError("Must provide either analysis or rolling_result")
154
+ rolling_result = analysis.compute_rolling_metrics(
155
+ windows=windows,
156
+ metrics=["returns"],
157
+ )
158
+
159
+ fig = create_base_figure(
160
+ title="Rolling Returns",
161
+ xaxis_title="Date",
162
+ yaxis_title="Rolling Return",
163
+ height=height,
164
+ width=width,
165
+ theme=theme,
166
+ )
167
+
168
+ dates = rolling_result.dates.to_list()
169
+
170
+ for i, window in enumerate(windows):
171
+ if window in rolling_result.returns:
172
+ returns = rolling_result.returns[window].to_numpy()
173
+ color = theme_config["colorway"][i % len(theme_config["colorway"])]
174
+
175
+ fig.add_trace(
176
+ go.Scatter(
177
+ x=dates,
178
+ y=returns,
179
+ mode="lines",
180
+ name=f"{window}d",
181
+ line={"color": color, "width": 1.5},
182
+ hovertemplate=f"{window}d Return: %{{y:.2%}}<extra></extra>",
183
+ )
184
+ )
185
+
186
+ # Add zero line
187
+ fig.add_hline(y=0, line_dash="dash", line_color="gray", line_width=1)
188
+
189
+ fig.update_layout(
190
+ legend={"yanchor": "top", "y": 0.99, "xanchor": "right", "x": 0.99},
191
+ hovermode="x unified",
192
+ )
193
+
194
+ return fig
195
+
196
+
197
+ def plot_annual_returns_bar(
198
+ analysis: PortfolioAnalysis,
199
+ theme: str | None = None,
200
+ show_benchmark: bool = True,
201
+ height: int = 400,
202
+ width: int | None = None,
203
+ ) -> go.Figure:
204
+ """Plot annual returns as bar chart.
205
+
206
+ Parameters
207
+ ----------
208
+ analysis : PortfolioAnalysis
209
+ Portfolio analysis object
210
+ theme : str, optional
211
+ Plot theme
212
+ show_benchmark : bool, default True
213
+ Show benchmark returns if available
214
+ height : int, default 400
215
+ Figure height
216
+ width : int, optional
217
+ Figure width
218
+
219
+ Returns
220
+ -------
221
+ go.Figure
222
+ Interactive Plotly figure
223
+ """
224
+ theme = validate_theme(theme)
225
+ theme_config = get_theme_config(theme)
226
+
227
+ # Compute annual returns
228
+ annual = analysis.compute_annual_returns()
229
+ years = annual["year"].to_list()
230
+ returns = annual["annual_return"].to_numpy()
231
+
232
+ fig = create_base_figure(
233
+ title="Annual Returns",
234
+ xaxis_title="Year",
235
+ yaxis_title="Return",
236
+ height=height,
237
+ width=width,
238
+ theme=theme,
239
+ )
240
+
241
+ # Color bars based on positive/negative
242
+ colors = [
243
+ theme_config["colorway"][2] if r > 0 else theme_config["colorway"][1] for r in returns
244
+ ]
245
+
246
+ fig.add_trace(
247
+ go.Bar(
248
+ x=years,
249
+ y=returns,
250
+ name="Strategy",
251
+ marker_color=colors,
252
+ hovertemplate="Year: %{x}<br>Return: %{y:.2%}<extra></extra>",
253
+ )
254
+ )
255
+
256
+ # Add benchmark if available
257
+ if show_benchmark and analysis.has_benchmark:
258
+ # Compute benchmark annual returns
259
+ import polars as pl
260
+
261
+ bench_df = (
262
+ pl.DataFrame(
263
+ {
264
+ "date": analysis.dates,
265
+ "return": analysis.benchmark,
266
+ }
267
+ )
268
+ .with_columns(
269
+ [
270
+ pl.col("date").dt.year().alias("year"),
271
+ ]
272
+ )
273
+ .group_by("year")
274
+ .agg((1 + pl.col("return")).product().alias("annual_return") - 1)
275
+ .sort("year")
276
+ )
277
+
278
+ fig.add_trace(
279
+ go.Scatter(
280
+ x=bench_df["year"].to_list(),
281
+ y=bench_df["annual_return"].to_numpy(),
282
+ mode="lines+markers",
283
+ name="Benchmark",
284
+ line={"color": theme_config["colorway"][1], "width": 2},
285
+ marker={"size": 8},
286
+ hovertemplate="Year: %{x}<br>Return: %{y:.2%}<extra></extra>",
287
+ )
288
+ )
289
+
290
+ fig.add_hline(y=0, line_dash="solid", line_color="gray", line_width=1)
291
+
292
+ fig.update_layout(
293
+ legend={"yanchor": "top", "y": 0.99, "xanchor": "right", "x": 0.99},
294
+ bargap=0.3,
295
+ )
296
+
297
+ return fig
298
+
299
+
300
+ def plot_monthly_returns_heatmap(
301
+ analysis: PortfolioAnalysis,
302
+ theme: str | None = None,
303
+ height: int = 400,
304
+ width: int | None = None,
305
+ ) -> go.Figure:
306
+ """Plot monthly returns as a year x month heatmap.
307
+
308
+ Parameters
309
+ ----------
310
+ analysis : PortfolioAnalysis
311
+ Portfolio analysis object
312
+ theme : str, optional
313
+ Plot theme
314
+ height : int, default 400
315
+ Figure height
316
+ width : int, optional
317
+ Figure width
318
+
319
+ Returns
320
+ -------
321
+ go.Figure
322
+ Interactive Plotly figure
323
+ """
324
+ theme = validate_theme(theme)
325
+
326
+ # Get monthly returns matrix
327
+ matrix = analysis.get_monthly_returns_matrix()
328
+
329
+ years = matrix["year"].to_list()
330
+ months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
331
+
332
+ # Extract values (columns 1-12 are the months)
333
+ z_values = []
334
+ for row in matrix.iter_rows():
335
+ z_values.append([row[i] if row[i] is not None else np.nan for i in range(1, 13)])
336
+
337
+ z_array = np.array(z_values)
338
+
339
+ # Color scale: red for negative, green for positive
340
+ colorscale = [
341
+ [0.0, "#d73027"],
342
+ [0.25, "#fc8d59"],
343
+ [0.5, "#ffffff"],
344
+ [0.75, "#91cf60"],
345
+ [1.0, "#1a9850"],
346
+ ]
347
+
348
+ # Find symmetric range for color scale
349
+ max_abs = np.nanmax(np.abs(z_array))
350
+ if np.isnan(max_abs):
351
+ max_abs = 0.1
352
+
353
+ fig = go.Figure(
354
+ data=go.Heatmap(
355
+ z=z_array,
356
+ x=months,
357
+ y=years,
358
+ colorscale=colorscale,
359
+ zmin=-max_abs,
360
+ zmax=max_abs,
361
+ text=[[f"{v:.1%}" if not np.isnan(v) else "" for v in row] for row in z_array],
362
+ texttemplate="%{text}",
363
+ textfont={"size": 10},
364
+ hovertemplate="Year: %{y}<br>Month: %{x}<br>Return: %{z:.2%}<extra></extra>",
365
+ colorbar={
366
+ "title": "Return",
367
+ "tickformat": ".0%",
368
+ },
369
+ )
370
+ )
371
+
372
+ fig.update_layout(
373
+ title="Monthly Returns",
374
+ xaxis_title="Month",
375
+ yaxis_title="Year",
376
+ height=height,
377
+ width=width,
378
+ yaxis={"autorange": "reversed"}, # Most recent year at top
379
+ )
380
+
381
+ return fig
382
+
383
+
384
+ def plot_returns_distribution(
385
+ analysis: PortfolioAnalysis,
386
+ theme: str | None = None,
387
+ bins: int = 50,
388
+ show_normal: bool = True,
389
+ height: int = 400,
390
+ width: int | None = None,
391
+ ) -> go.Figure:
392
+ """Plot returns distribution histogram with optional normal fit.
393
+
394
+ Parameters
395
+ ----------
396
+ analysis : PortfolioAnalysis
397
+ Portfolio analysis object
398
+ theme : str, optional
399
+ Plot theme
400
+ bins : int, default 50
401
+ Number of histogram bins
402
+ show_normal : bool, default True
403
+ Overlay normal distribution fit
404
+ height : int, default 400
405
+ Figure height
406
+ width : int, optional
407
+ Figure width
408
+
409
+ Returns
410
+ -------
411
+ go.Figure
412
+ Interactive Plotly figure
413
+ """
414
+ theme = validate_theme(theme)
415
+ theme_config = get_theme_config(theme)
416
+
417
+ returns = analysis.returns
418
+ clean_returns = returns[~np.isnan(returns)]
419
+
420
+ fig = create_base_figure(
421
+ title="Returns Distribution",
422
+ xaxis_title="Daily Return",
423
+ yaxis_title="Frequency",
424
+ height=height,
425
+ width=width,
426
+ theme=theme,
427
+ )
428
+
429
+ # Histogram
430
+ fig.add_trace(
431
+ go.Histogram(
432
+ x=clean_returns,
433
+ nbinsx=bins,
434
+ name="Returns",
435
+ marker_color=theme_config["colorway"][0],
436
+ opacity=0.7,
437
+ histnorm="probability density",
438
+ hovertemplate="Return: %{x:.2%}<br>Density: %{y:.4f}<extra></extra>",
439
+ )
440
+ )
441
+
442
+ # Normal fit
443
+ if show_normal:
444
+ from scipy import stats as sp_stats
445
+
446
+ mean = np.mean(clean_returns)
447
+ std = np.std(clean_returns, ddof=1)
448
+
449
+ x_range = np.linspace(min(clean_returns), max(clean_returns), 100)
450
+ y_normal = sp_stats.norm.pdf(x_range, mean, std)
451
+
452
+ fig.add_trace(
453
+ go.Scatter(
454
+ x=x_range,
455
+ y=y_normal,
456
+ mode="lines",
457
+ name=f"Normal (μ={mean:.4f}, σ={std:.4f})",
458
+ line={"color": theme_config["colorway"][1], "width": 2, "dash": "dash"},
459
+ )
460
+ )
461
+
462
+ # Add VaR lines
463
+ var_95 = np.percentile(clean_returns, 5)
464
+ var_99 = np.percentile(clean_returns, 1)
465
+
466
+ fig.add_vline(
467
+ x=var_95,
468
+ line_dash="dot",
469
+ line_color="orange",
470
+ annotation_text=f"VaR 95%: {var_95:.2%}",
471
+ annotation_position="top",
472
+ )
473
+
474
+ fig.add_vline(
475
+ x=var_99,
476
+ line_dash="dot",
477
+ line_color="red",
478
+ annotation_text=f"VaR 99%: {var_99:.2%}",
479
+ annotation_position="bottom",
480
+ )
481
+
482
+ fig.update_layout(
483
+ legend={"yanchor": "top", "y": 0.99, "xanchor": "right", "x": 0.99},
484
+ bargap=0.05,
485
+ )
486
+
487
+ return fig