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,565 @@
1
+ """Unified backtest tearsheet generation.
2
+
3
+ The main entry point for generating comprehensive backtest reports.
4
+ Combines all visualization modules into a single, publication-quality
5
+ HTML document.
6
+
7
+ This is the primary interface users should use:
8
+ from ml4t.diagnostic.visualization.backtest import generate_backtest_tearsheet
9
+
10
+ html = generate_backtest_tearsheet(
11
+ backtest_result,
12
+ template="full",
13
+ theme="default",
14
+ output_path="report.html",
15
+ )
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING, Any, Literal
23
+
24
+ import numpy as np
25
+
26
+ from .template_system import (
27
+ HTML_TEMPLATE,
28
+ TEARSHEET_CSS,
29
+ TearsheetTemplate,
30
+ get_template,
31
+ )
32
+
33
+ if TYPE_CHECKING:
34
+ import plotly.graph_objects as go
35
+ import polars as pl
36
+
37
+
38
+ def generate_backtest_tearsheet(
39
+ trades: pl.DataFrame | None = None,
40
+ returns: pl.Series | np.ndarray | None = None,
41
+ equity_curve: pl.DataFrame | None = None,
42
+ metrics: dict[str, Any] | None = None,
43
+ output_path: str | Path | None = None,
44
+ template: Literal["quant_trader", "hedge_fund", "risk_manager", "full"] = "full",
45
+ theme: Literal["default", "dark", "print", "presentation"] = "default",
46
+ title: str = "Backtest Analysis Report",
47
+ subtitle: str = "",
48
+ benchmark_returns: pl.Series | np.ndarray | None = None,
49
+ n_trials: int | None = None,
50
+ interactive: bool = True,
51
+ include_plotlyjs: bool = True,
52
+ ) -> str:
53
+ """Generate a comprehensive backtest tearsheet.
54
+
55
+ This is the main entry point for creating publication-quality backtest
56
+ reports. It combines all visualization modules into a single HTML document.
57
+
58
+ Parameters
59
+ ----------
60
+ trades : pl.DataFrame, optional
61
+ Trade records with columns like: symbol, entry_time, exit_time,
62
+ pnl, gross_pnl, net_pnl, mfe, mae, exit_reason, duration, size
63
+ returns : pl.Series or np.ndarray, optional
64
+ Daily returns series for portfolio-level analysis
65
+ equity_curve : pl.DataFrame, optional
66
+ Equity curve with date and equity columns
67
+ metrics : dict, optional
68
+ Pre-computed metrics dict with keys like:
69
+ - sharpe, cagr, max_drawdown, win_rate, profit_factor
70
+ - dsr_probability, min_trl, etc. for statistical validity
71
+ output_path : str or Path, optional
72
+ If provided, save HTML to this path
73
+ template : {"quant_trader", "hedge_fund", "risk_manager", "full"}
74
+ Template persona to use (determines which sections are shown)
75
+ theme : {"default", "dark", "print", "presentation"}
76
+ Visual theme for the charts
77
+ title : str
78
+ Report title
79
+ subtitle : str
80
+ Report subtitle (e.g., strategy name, date range)
81
+ benchmark_returns : pl.Series or np.ndarray, optional
82
+ Benchmark returns for comparison
83
+ n_trials : int, optional
84
+ Number of trials for DSR calculation
85
+ interactive : bool
86
+ Whether charts should be interactive (vs static images)
87
+ include_plotlyjs : bool
88
+ Whether to include Plotly.js (set False if already loaded)
89
+
90
+ Returns
91
+ -------
92
+ str
93
+ HTML string of the complete tearsheet
94
+
95
+ Examples
96
+ --------
97
+ >>> from ml4t.diagnostic.visualization.backtest import generate_backtest_tearsheet
98
+ >>>
99
+ >>> # From trades DataFrame
100
+ >>> html = generate_backtest_tearsheet(
101
+ ... trades=my_trades,
102
+ ... metrics={"sharpe": 1.5, "max_drawdown": -0.15},
103
+ ... template="quant_trader",
104
+ ... output_path="strategy_report.html",
105
+ ... )
106
+ >>>
107
+ >>> # From returns series
108
+ >>> html = generate_backtest_tearsheet(
109
+ ... returns=daily_returns,
110
+ ... template="risk_manager",
111
+ ... n_trials=100, # For DSR
112
+ ... )
113
+ """
114
+ # Get template
115
+ tmpl = get_template(template)
116
+
117
+ # Generate sections HTML
118
+ sections_html = _generate_sections(
119
+ tmpl,
120
+ trades=trades,
121
+ returns=returns,
122
+ equity_curve=equity_curve,
123
+ metrics=metrics,
124
+ benchmark_returns=benchmark_returns,
125
+ n_trials=n_trials,
126
+ theme=theme,
127
+ interactive=interactive,
128
+ )
129
+
130
+ # Generate full HTML - conditionally include Plotly JS
131
+ if include_plotlyjs:
132
+ css = TEARSHEET_CSS
133
+ else:
134
+ css = TEARSHEET_CSS.replace(
135
+ '<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>',
136
+ "",
137
+ )
138
+
139
+ html = HTML_TEMPLATE.format(
140
+ theme=theme if theme == "dark" else "light",
141
+ title=title,
142
+ subtitle=subtitle or f"Template: {template}",
143
+ timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
144
+ css=css,
145
+ sections_html=sections_html,
146
+ )
147
+
148
+ # Save if path provided
149
+ if output_path:
150
+ output_path = Path(output_path)
151
+ output_path.parent.mkdir(parents=True, exist_ok=True)
152
+ output_path.write_text(html)
153
+
154
+ return html
155
+
156
+
157
+ def _generate_sections(
158
+ template: TearsheetTemplate,
159
+ trades: pl.DataFrame | None = None,
160
+ returns: pl.Series | np.ndarray | None = None,
161
+ equity_curve: pl.DataFrame | None = None,
162
+ metrics: dict[str, Any] | None = None,
163
+ benchmark_returns: pl.Series | np.ndarray | None = None,
164
+ n_trials: int | None = None,
165
+ theme: str = "default",
166
+ interactive: bool = True,
167
+ ) -> str:
168
+ """Generate HTML for all enabled sections."""
169
+ sections_html = []
170
+
171
+ for section in template.get_enabled_sections():
172
+ section_html = _generate_section(
173
+ section.name,
174
+ section.title,
175
+ trades=trades,
176
+ returns=returns,
177
+ equity_curve=equity_curve,
178
+ metrics=metrics,
179
+ benchmark_returns=benchmark_returns,
180
+ n_trials=n_trials,
181
+ theme=theme,
182
+ interactive=interactive,
183
+ )
184
+ if section_html:
185
+ sections_html.append(section_html)
186
+
187
+ return "\n".join(sections_html)
188
+
189
+
190
+ def _generate_section(
191
+ section_name: str,
192
+ section_title: str,
193
+ trades: pl.DataFrame | None = None,
194
+ returns: pl.Series | np.ndarray | None = None,
195
+ equity_curve: pl.DataFrame | None = None,
196
+ metrics: dict[str, Any] | None = None,
197
+ benchmark_returns: pl.Series | np.ndarray | None = None,
198
+ n_trials: int | None = None,
199
+ theme: str = "default",
200
+ interactive: bool = True,
201
+ ) -> str | None:
202
+ """Generate HTML for a single section."""
203
+ try:
204
+ fig = _create_section_figure(
205
+ section_name,
206
+ trades=trades,
207
+ returns=returns,
208
+ equity_curve=equity_curve,
209
+ metrics=metrics,
210
+ benchmark_returns=benchmark_returns,
211
+ n_trials=n_trials,
212
+ theme=theme,
213
+ )
214
+
215
+ if fig is None:
216
+ return None
217
+
218
+ # Convert figure to HTML
219
+ if interactive:
220
+ fig_html = fig.to_html(full_html=False, include_plotlyjs=False)
221
+ else:
222
+ fig_html = fig.to_html(full_html=False, include_plotlyjs=False)
223
+
224
+ return f"""
225
+ <section class="section">
226
+ <h2 class="section-title">{section_title}</h2>
227
+ <div class="chart-container">
228
+ {fig_html}
229
+ </div>
230
+ </section>
231
+ """
232
+
233
+ except Exception as e:
234
+ # Log error but don't fail the whole report
235
+ return f"""
236
+ <section class="section">
237
+ <h2 class="section-title">{section_title}</h2>
238
+ <div class="chart-container">
239
+ <p style="color: #999;">Section unavailable: {str(e)}</p>
240
+ </div>
241
+ </section>
242
+ """
243
+
244
+
245
+ def _create_section_figure(
246
+ section_name: str,
247
+ trades: pl.DataFrame | None = None,
248
+ returns: pl.Series | np.ndarray | None = None,
249
+ equity_curve: pl.DataFrame | None = None,
250
+ metrics: dict[str, Any] | None = None,
251
+ benchmark_returns: pl.Series | np.ndarray | None = None,
252
+ n_trials: int | None = None,
253
+ theme: str = "default",
254
+ ) -> go.Figure | None:
255
+ """Create the Plotly figure for a specific section."""
256
+
257
+ metrics = metrics or {}
258
+
259
+ # Executive Summary sections
260
+ if section_name == "executive_summary":
261
+ if not metrics:
262
+ return None
263
+ from .executive_summary import create_executive_summary
264
+
265
+ return create_executive_summary(metrics, theme=theme)
266
+
267
+ if section_name == "key_insights":
268
+ if not metrics:
269
+ return None
270
+ from .executive_summary import create_key_insights
271
+
272
+ insights = create_key_insights(metrics)
273
+ # Create a simple text figure for insights
274
+ import plotly.graph_objects as go
275
+
276
+ fig = go.Figure()
277
+ insight_text = "<br>".join([f"• [{i.category.upper()}] {i.message}" for i in insights])
278
+ fig.add_annotation(
279
+ text=insight_text or "No insights available",
280
+ xref="paper",
281
+ yref="paper",
282
+ x=0.5,
283
+ y=0.5,
284
+ showarrow=False,
285
+ font={"size": 14},
286
+ align="left",
287
+ )
288
+ fig.update_layout(
289
+ height=max(150, len(insights) * 40 + 50),
290
+ xaxis={"visible": False},
291
+ yaxis={"visible": False},
292
+ )
293
+ return fig
294
+
295
+ # Trade Analysis sections
296
+ if section_name == "mfe_mae":
297
+ if trades is None:
298
+ return None
299
+ from .trade_plots import plot_mfe_mae_scatter
300
+
301
+ return plot_mfe_mae_scatter(trades, theme=theme)
302
+
303
+ if section_name == "exit_reasons":
304
+ if trades is None:
305
+ return None
306
+ from .trade_plots import plot_exit_reason_breakdown
307
+
308
+ return plot_exit_reason_breakdown(trades, theme=theme)
309
+
310
+ if section_name == "trade_waterfall":
311
+ if trades is None:
312
+ return None
313
+ from .trade_plots import plot_trade_waterfall
314
+
315
+ return plot_trade_waterfall(trades, theme=theme)
316
+
317
+ if section_name == "duration":
318
+ if trades is None:
319
+ return None
320
+ from .trade_plots import plot_trade_duration_distribution
321
+
322
+ return plot_trade_duration_distribution(trades, theme=theme)
323
+
324
+ if section_name == "consecutive":
325
+ if trades is None:
326
+ return None
327
+ from .trade_plots import plot_consecutive_analysis
328
+
329
+ return plot_consecutive_analysis(trades, theme=theme)
330
+
331
+ if section_name == "size_return":
332
+ if trades is None:
333
+ return None
334
+ from .trade_plots import plot_trade_size_vs_return
335
+
336
+ return plot_trade_size_vs_return(trades, theme=theme)
337
+
338
+ # Cost Attribution sections
339
+ if section_name == "cost_waterfall":
340
+ gross_pnl = metrics.get("gross_pnl")
341
+ commission = metrics.get("commission", 0)
342
+ slippage = metrics.get("slippage", 0)
343
+ if gross_pnl is None:
344
+ return None
345
+ from .cost_attribution import plot_cost_waterfall
346
+
347
+ return plot_cost_waterfall(
348
+ gross_pnl=gross_pnl,
349
+ commission=commission,
350
+ slippage=slippage,
351
+ theme=theme,
352
+ )
353
+
354
+ if section_name == "cost_sensitivity":
355
+ if returns is None:
356
+ return None
357
+ from .cost_attribution import plot_cost_sensitivity
358
+
359
+ return plot_cost_sensitivity(returns, theme=theme)
360
+
361
+ if section_name == "cost_by_asset":
362
+ if trades is None:
363
+ return None
364
+ from .cost_attribution import plot_cost_by_asset
365
+
366
+ return plot_cost_by_asset(trades, theme=theme)
367
+
368
+ # Statistical Validity sections
369
+ if section_name == "statistical_summary":
370
+ from .statistical_validity import plot_statistical_summary_card
371
+
372
+ return plot_statistical_summary_card(metrics, theme=theme)
373
+
374
+ if section_name == "dsr_gauge":
375
+ dsr_prob = metrics.get("dsr_probability")
376
+ sharpe = metrics.get("sharpe")
377
+ if dsr_prob is None or sharpe is None:
378
+ return None
379
+ from .statistical_validity import plot_dsr_gauge
380
+
381
+ return plot_dsr_gauge(
382
+ dsr_probability=dsr_prob,
383
+ observed_sharpe=sharpe,
384
+ expected_max_sharpe=metrics.get("expected_max_sharpe"),
385
+ n_trials=n_trials,
386
+ theme=theme,
387
+ )
388
+
389
+ if section_name == "confidence_intervals":
390
+ # Build CI dict from metrics
391
+ ci_metrics = {}
392
+ for key in ["sharpe", "cagr", "max_drawdown"]:
393
+ if key in metrics:
394
+ ci_metrics[key] = {
395
+ "point": metrics[key],
396
+ "lower_95": metrics.get(f"{key}_lower_95", metrics[key] * 0.7),
397
+ "upper_95": metrics.get(f"{key}_upper_95", metrics[key] * 1.3),
398
+ }
399
+ if not ci_metrics:
400
+ return None
401
+ from .statistical_validity import plot_confidence_intervals
402
+
403
+ return plot_confidence_intervals(ci_metrics, theme=theme)
404
+
405
+ if section_name == "min_trl":
406
+ sharpe = metrics.get("sharpe")
407
+ periods = metrics.get("n_periods", metrics.get("n_observations"))
408
+ if sharpe is None or periods is None:
409
+ return None
410
+ from .statistical_validity import plot_minimum_track_record
411
+
412
+ return plot_minimum_track_record(
413
+ observed_sharpe=sharpe,
414
+ current_periods=periods,
415
+ theme=theme,
416
+ )
417
+
418
+ if section_name == "ras_analysis":
419
+ original_ic = metrics.get("original_ic")
420
+ adjusted_ic = metrics.get("ras_adjusted_ic")
421
+ rademacher = metrics.get("rademacher_complexity")
422
+ if original_ic is None or adjusted_ic is None or rademacher is None:
423
+ return None
424
+ from .statistical_validity import plot_ras_analysis
425
+
426
+ return plot_ras_analysis(
427
+ original_ic=float(original_ic),
428
+ adjusted_ic=float(adjusted_ic),
429
+ rademacher_complexity=float(rademacher),
430
+ theme=theme,
431
+ )
432
+
433
+ # Portfolio-level sections (use existing portfolio viz if available)
434
+ if section_name in (
435
+ "equity_curve",
436
+ "drawdowns",
437
+ "monthly_returns",
438
+ "annual_returns",
439
+ "rolling_metrics",
440
+ ):
441
+ # These would integrate with existing portfolio visualization
442
+ # For now, return None to skip
443
+ return None
444
+
445
+ if section_name in ("distribution", "tail_risk"):
446
+ # These would integrate with existing statistical visualization
447
+ return None
448
+
449
+ # Unknown section
450
+ return None
451
+
452
+
453
+ class BacktestTearsheet:
454
+ """Object-oriented interface for building tearsheets incrementally.
455
+
456
+ Provides a fluent API for customizing tearsheet content before generation.
457
+
458
+ Examples
459
+ --------
460
+ >>> tearsheet = BacktestTearsheet(template="quant_trader")
461
+ >>> tearsheet.add_trades(my_trades)
462
+ >>> tearsheet.add_metrics({"sharpe": 1.5, "max_drawdown": -0.15})
463
+ >>> tearsheet.enable_section("dsr_gauge")
464
+ >>> html = tearsheet.generate()
465
+ """
466
+
467
+ def __init__(
468
+ self,
469
+ template: Literal["quant_trader", "hedge_fund", "risk_manager", "full"] = "full",
470
+ theme: Literal["default", "dark", "print", "presentation"] = "default",
471
+ title: str = "Backtest Analysis Report",
472
+ ):
473
+ """Initialize tearsheet builder."""
474
+ self.template = get_template(template)
475
+ self.theme = theme
476
+ self.title = title
477
+ self.subtitle = ""
478
+
479
+ # Data
480
+ self.trades: pl.DataFrame | None = None
481
+ self.returns: pl.Series | np.ndarray | None = None
482
+ self.equity_curve: pl.DataFrame | None = None
483
+ self.metrics: dict[str, Any] = {}
484
+ self.benchmark_returns: pl.Series | np.ndarray | None = None
485
+ self.n_trials: int | None = None
486
+
487
+ def add_trades(self, trades: pl.DataFrame) -> BacktestTearsheet:
488
+ """Add trade records to the tearsheet."""
489
+ self.trades = trades
490
+ return self
491
+
492
+ def add_returns(self, returns: pl.Series | np.ndarray) -> BacktestTearsheet:
493
+ """Add daily returns series."""
494
+ self.returns = returns
495
+ return self
496
+
497
+ def add_equity_curve(self, equity: pl.DataFrame) -> BacktestTearsheet:
498
+ """Add equity curve DataFrame."""
499
+ self.equity_curve = equity
500
+ return self
501
+
502
+ def add_metrics(self, metrics: dict[str, Any]) -> BacktestTearsheet:
503
+ """Add or update metrics dictionary."""
504
+ self.metrics.update(metrics)
505
+ return self
506
+
507
+ def add_benchmark(self, returns: pl.Series | np.ndarray) -> BacktestTearsheet:
508
+ """Add benchmark returns for comparison."""
509
+ self.benchmark_returns = returns
510
+ return self
511
+
512
+ def set_n_trials(self, n: int) -> BacktestTearsheet:
513
+ """Set number of trials for DSR calculation."""
514
+ self.n_trials = n
515
+ return self
516
+
517
+ def set_title(self, title: str, subtitle: str = "") -> BacktestTearsheet:
518
+ """Set report title and subtitle."""
519
+ self.title = title
520
+ self.subtitle = subtitle
521
+ return self
522
+
523
+ def enable_section(self, name: str) -> BacktestTearsheet:
524
+ """Enable a section by name."""
525
+ self.template.enable_section(name)
526
+ return self
527
+
528
+ def disable_section(self, name: str) -> BacktestTearsheet:
529
+ """Disable a section by name."""
530
+ self.template.disable_section(name)
531
+ return self
532
+
533
+ def generate(
534
+ self,
535
+ output_path: str | Path | None = None,
536
+ interactive: bool = True,
537
+ ) -> str:
538
+ """Generate the tearsheet HTML.
539
+
540
+ Parameters
541
+ ----------
542
+ output_path : str or Path, optional
543
+ If provided, save HTML to this path
544
+ interactive : bool
545
+ Whether charts should be interactive
546
+
547
+ Returns
548
+ -------
549
+ str
550
+ HTML string of the complete tearsheet
551
+ """
552
+ return generate_backtest_tearsheet(
553
+ trades=self.trades,
554
+ returns=self.returns,
555
+ equity_curve=self.equity_curve,
556
+ metrics=self.metrics,
557
+ output_path=output_path,
558
+ template=self.template.name, # type: ignore
559
+ theme=self.theme,
560
+ title=self.title,
561
+ subtitle=self.subtitle,
562
+ benchmark_returns=self.benchmark_returns,
563
+ n_trials=self.n_trials,
564
+ interactive=interactive,
565
+ )