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,911 @@
1
+ """Signal Analysis Dashboard - Multi-tab interactive HTML dashboard.
2
+
3
+ This module provides the SignalDashboard class for creating comprehensive,
4
+ self-contained HTML dashboards for signal/factor analysis results.
5
+
6
+ The dashboard follows the BaseDashboard pattern with 5 tabs:
7
+ 1. Summary - Key metrics cards, signal quality assessment
8
+ 2. IC Analysis - Information coefficient time series, distribution, heatmap
9
+ 3. Quantile Analysis - Returns by quantile, cumulative performance, spread
10
+ 4. Turnover - Signal stability, autocorrelation
11
+ 5. Events (optional) - Event study results if provided
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING, Any, Literal
17
+
18
+ from ml4t.diagnostic.visualization.dashboards import (
19
+ BaseDashboard,
20
+ DashboardSection,
21
+ )
22
+ from ml4t.diagnostic.visualization.signal.event_plots import (
23
+ plot_ar_distribution,
24
+ plot_caar,
25
+ plot_car_by_event,
26
+ plot_event_heatmap,
27
+ )
28
+ from ml4t.diagnostic.visualization.signal.ic_plots import (
29
+ plot_ic_heatmap,
30
+ plot_ic_histogram,
31
+ plot_ic_qq,
32
+ plot_ic_ts,
33
+ )
34
+ from ml4t.diagnostic.visualization.signal.quantile_plots import (
35
+ plot_cumulative_returns,
36
+ plot_quantile_returns_bar,
37
+ plot_quantile_returns_violin,
38
+ plot_spread_timeseries,
39
+ )
40
+ from ml4t.diagnostic.visualization.signal.turnover_plots import (
41
+ plot_autocorrelation,
42
+ plot_top_bottom_turnover,
43
+ )
44
+
45
+ if TYPE_CHECKING:
46
+ from ml4t.diagnostic.results.event_results import EventStudyResult
47
+ from ml4t.diagnostic.results.signal_results import SignalTearSheet
48
+
49
+
50
+ class SignalDashboard(BaseDashboard):
51
+ """Interactive multi-tab dashboard for signal analysis results.
52
+
53
+ Creates a self-contained HTML dashboard with comprehensive visualizations
54
+ of signal/factor analysis results. The dashboard includes 5 tabs:
55
+
56
+ 1. **Summary**: Key metrics at a glance, signal quality badges, insights
57
+ 2. **IC Analysis**: IC time series, histogram, Q-Q plot, monthly heatmap
58
+ 3. **Quantile Analysis**: Returns by quantile, cumulative, spread analysis
59
+ 4. **Turnover**: Signal stability, autocorrelation by lag
60
+ 5. **Events** (optional): Event study results if provided
61
+
62
+ Examples
63
+ --------
64
+ >>> from ml4t.diagnostic.evaluation import SignalAnalysis
65
+ >>> from ml4t.diagnostic.visualization.signal import SignalDashboard
66
+ >>>
67
+ >>> # Run signal analysis
68
+ >>> analyzer = SignalAnalysis(factor_data, price_data)
69
+ >>> tear_sheet = analyzer.create_tear_sheet()
70
+ >>>
71
+ >>> # Create and save dashboard
72
+ >>> dashboard = SignalDashboard(title="Momentum Factor Analysis")
73
+ >>> dashboard.save("momentum_dashboard.html", tear_sheet)
74
+
75
+ >>> # Dark theme with custom title
76
+ >>> dashboard = SignalDashboard(
77
+ ... title="Value Factor Analysis",
78
+ ... theme="dark"
79
+ ... )
80
+ >>> html = dashboard.generate(tear_sheet)
81
+
82
+ Notes
83
+ -----
84
+ - Dashboard is self-contained HTML with embedded Plotly.js (via CDN)
85
+ - All visualizations are interactive (zoom, pan, hover)
86
+ - Works offline once loaded (all data embedded)
87
+
88
+ See Also
89
+ --------
90
+ SignalAnalysis : Main signal analysis class
91
+ SignalTearSheet : Result container for signal analysis
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ title: str = "Signal Analysis Dashboard",
97
+ theme: Literal["light", "dark"] = "light",
98
+ width: int | None = None,
99
+ height: int | None = None,
100
+ ):
101
+ """Initialize Signal Analysis Dashboard.
102
+
103
+ Parameters
104
+ ----------
105
+ title : str, default="Signal Analysis Dashboard"
106
+ Dashboard title displayed at top
107
+ theme : {'light', 'dark'}, default='light'
108
+ Visual theme for all plots and styling
109
+ width : int, optional
110
+ Dashboard width in pixels. If None, uses responsive width.
111
+ height : int, optional
112
+ Dashboard height in pixels. If None, uses auto height.
113
+ """
114
+ super().__init__(title, theme, width, height)
115
+
116
+ def generate(
117
+ self,
118
+ analysis_results: SignalTearSheet,
119
+ include_events: bool = False,
120
+ event_analysis: Any | None = None,
121
+ **_kwargs: Any,
122
+ ) -> str:
123
+ """Generate complete dashboard HTML.
124
+
125
+ Parameters
126
+ ----------
127
+ analysis_results : SignalTearSheet
128
+ Results from SignalAnalysis.create_tear_sheet()
129
+ include_events : bool, default=False
130
+ Whether to include Events tab (requires event_analysis)
131
+ event_analysis : EventStudyResult, optional
132
+ Event study results to include in Events tab
133
+ **kwargs
134
+ Additional parameters (currently unused)
135
+
136
+ Returns
137
+ -------
138
+ str
139
+ Complete HTML document
140
+ """
141
+ # Clear any previous sections
142
+ self.sections: list[DashboardSection] = []
143
+
144
+ # Create tabbed layout
145
+ self._create_tabbed_layout(analysis_results, include_events, event_analysis)
146
+
147
+ # Compose final HTML
148
+ return self._compose_html()
149
+
150
+ def save(
151
+ self,
152
+ output_path: str,
153
+ analysis_results: SignalTearSheet,
154
+ include_events: bool = False,
155
+ event_analysis: Any | None = None,
156
+ **kwargs: Any,
157
+ ) -> str:
158
+ """Generate and save dashboard to file.
159
+
160
+ Parameters
161
+ ----------
162
+ output_path : str
163
+ Path for output HTML file
164
+ analysis_results : SignalTearSheet
165
+ Results from SignalAnalysis.create_tear_sheet()
166
+ include_events : bool, default=False
167
+ Whether to include Events tab
168
+ event_analysis : EventStudyResult, optional
169
+ Event study results for Events tab
170
+ **kwargs
171
+ Additional parameters passed to generate()
172
+
173
+ Returns
174
+ -------
175
+ str
176
+ Path to saved file
177
+ """
178
+ html = self.generate(
179
+ analysis_results,
180
+ include_events=include_events,
181
+ event_analysis=event_analysis,
182
+ **kwargs,
183
+ )
184
+
185
+ with open(output_path, "w", encoding="utf-8") as f:
186
+ f.write(html)
187
+
188
+ return output_path
189
+
190
+ # =========================================================================
191
+ # Tabbed Layout Methods
192
+ # =========================================================================
193
+
194
+ def _create_tabbed_layout(
195
+ self,
196
+ tear_sheet: SignalTearSheet,
197
+ include_events: bool = False,
198
+ event_analysis: Any | None = None,
199
+ ) -> None:
200
+ """Create tabbed dashboard layout."""
201
+ # Define tabs
202
+ tabs = [
203
+ ("summary", "Summary"),
204
+ ("ic", "IC Analysis"),
205
+ ("quantile", "Quantile Analysis"),
206
+ ("turnover", "Turnover"),
207
+ ]
208
+
209
+ # Add events tab if requested
210
+ if include_events and event_analysis is not None:
211
+ tabs.append(("events", "Events"))
212
+
213
+ # Build tab content
214
+ tab_contents = {
215
+ "summary": self._create_summary_tab(tear_sheet),
216
+ "ic": self._create_ic_tab(tear_sheet),
217
+ "quantile": self._create_quantile_tab(tear_sheet),
218
+ "turnover": self._create_turnover_tab(tear_sheet),
219
+ }
220
+
221
+ if include_events and event_analysis is not None:
222
+ tab_contents["events"] = self._create_events_tab(event_analysis)
223
+
224
+ # Build tab navigation buttons
225
+ tab_buttons = "".join(
226
+ [
227
+ f'<button class="tab-button{" active" if i == 0 else ""}" '
228
+ f"onclick=\"switchTab(event, '{tab_id}')\">{tab_name}</button>"
229
+ for i, (tab_id, tab_name) in enumerate(tabs)
230
+ ]
231
+ )
232
+
233
+ # Build tab content divs
234
+ tab_divs = "".join(
235
+ [
236
+ f'<div id="{tab_id}" class="tab-content{" active" if i == 0 else ""}">'
237
+ f"{tab_contents[tab_id]}</div>"
238
+ for i, (tab_id, _) in enumerate(tabs)
239
+ ]
240
+ )
241
+
242
+ # Compose tabbed layout
243
+ html_content = f"""
244
+ <div class="tab-navigation">
245
+ {tab_buttons}
246
+ </div>
247
+ {tab_divs}
248
+ """
249
+
250
+ # Create single section with all tabbed content
251
+ section = DashboardSection(
252
+ title="Signal Analysis",
253
+ description="",
254
+ content=html_content,
255
+ )
256
+ self.sections.append(section)
257
+
258
+ # =========================================================================
259
+ # Summary Tab
260
+ # =========================================================================
261
+
262
+ def _create_summary_tab(self, tear_sheet: SignalTearSheet) -> str:
263
+ """Create Summary tab with key metrics and insights."""
264
+ html_parts = ["<h2>Summary</h2>"]
265
+
266
+ # Metadata section
267
+ html_parts.append(f"""
268
+ <div class="metric-grid">
269
+ <div class="metric-card">
270
+ <div class="metric-label">Signal Name</div>
271
+ <div class="metric-value">{tear_sheet.signal_name}</div>
272
+ </div>
273
+ <div class="metric-card">
274
+ <div class="metric-label">Assets</div>
275
+ <div class="metric-value">{tear_sheet.n_assets:,}</div>
276
+ </div>
277
+ <div class="metric-card">
278
+ <div class="metric-label">Dates</div>
279
+ <div class="metric-value">{tear_sheet.n_dates:,}</div>
280
+ </div>
281
+ <div class="metric-card">
282
+ <div class="metric-label">Date Range</div>
283
+ <div class="metric-value" style="font-size: 1em;">
284
+ {tear_sheet.date_range[0][:10]}<br>to {tear_sheet.date_range[1][:10]}
285
+ </div>
286
+ </div>
287
+ </div>
288
+ """)
289
+
290
+ # IC Summary metrics
291
+ if tear_sheet.ic_analysis is not None:
292
+ ic = tear_sheet.ic_analysis
293
+ periods = list(ic.ic_mean.keys())
294
+ first_period = periods[0] if periods else "1D"
295
+
296
+ ic_mean = ic.ic_mean.get(first_period, 0)
297
+ ic_ir = ic.ic_ir.get(first_period, 0)
298
+ ic_positive = ic.ic_positive_pct.get(first_period, 0)
299
+ ic_t = ic.ic_t_stat.get(first_period, 0)
300
+
301
+ # Quality badge based on IC
302
+ if abs(ic_mean) > 0.05:
303
+ quality_badge = '<span class="badge badge-high">Strong</span>'
304
+ elif abs(ic_mean) > 0.02:
305
+ quality_badge = '<span class="badge badge-medium">Moderate</span>'
306
+ else:
307
+ quality_badge = '<span class="badge badge-low">Weak</span>'
308
+
309
+ html_parts.append(f"""
310
+ <h3>IC Metrics ({first_period})</h3>
311
+ <div class="metric-grid">
312
+ <div class="metric-card">
313
+ <div class="metric-label">Mean IC</div>
314
+ <div class="metric-value">{ic_mean:.4f}</div>
315
+ <div class="metric-sublabel">{quality_badge}</div>
316
+ </div>
317
+ <div class="metric-card">
318
+ <div class="metric-label">IC IR</div>
319
+ <div class="metric-value">{ic_ir:.3f}</div>
320
+ <div class="metric-sublabel">
321
+ {"Good" if ic_ir > 0.5 else "Moderate" if ic_ir > 0.2 else "Low"}
322
+ </div>
323
+ </div>
324
+ <div class="metric-card">
325
+ <div class="metric-label">IC Positive %</div>
326
+ <div class="metric-value">{ic_positive:.1%}</div>
327
+ </div>
328
+ <div class="metric-card">
329
+ <div class="metric-label">t-statistic</div>
330
+ <div class="metric-value">{ic_t:.2f}</div>
331
+ <div class="metric-sublabel">
332
+ {"Significant" if abs(ic_t) > 2 else "Not significant"}
333
+ </div>
334
+ </div>
335
+ </div>
336
+ """)
337
+
338
+ # RAS-adjusted IC if available
339
+ if ic.ras_adjusted_ic is not None and ic.ras_significant is not None:
340
+ ras_ic = ic.ras_adjusted_ic.get(first_period, 0)
341
+ ras_sig = ic.ras_significant.get(first_period, False)
342
+ sig_icon = "✓" if ras_sig else "✗"
343
+ sig_color = "#28a745" if ras_sig else "#dc3545"
344
+
345
+ html_parts.append(f"""
346
+ <div class="insights-panel">
347
+ <h3>RAS-Adjusted IC (Multiple Testing Correction)</h3>
348
+ <p><strong>Adjusted IC:</strong> {ras_ic:.4f}</p>
349
+ <p><strong>Significant:</strong>
350
+ <span style="color: {sig_color}; font-weight: bold;">{sig_icon}</span>
351
+ {"Signal passes multiple testing correction" if ras_sig else "Signal may be spurious"}
352
+ </p>
353
+ </div>
354
+ """)
355
+
356
+ # Quantile spread summary
357
+ if tear_sheet.quantile_analysis is not None:
358
+ qa = tear_sheet.quantile_analysis
359
+ periods = qa.periods
360
+ first_period = periods[0] if periods else "1D"
361
+
362
+ spread = qa.spread_mean.get(first_period, 0)
363
+ spread_t = qa.spread_t_stat.get(first_period, 0)
364
+ monotonic = qa.is_monotonic.get(first_period, False)
365
+
366
+ html_parts.append(f"""
367
+ <h3>Quantile Analysis ({first_period})</h3>
368
+ <div class="metric-grid">
369
+ <div class="metric-card">
370
+ <div class="metric-label">Spread (Top-Bottom)</div>
371
+ <div class="metric-value">{spread:.4%}</div>
372
+ </div>
373
+ <div class="metric-card">
374
+ <div class="metric-label">Spread t-stat</div>
375
+ <div class="metric-value">{spread_t:.2f}</div>
376
+ </div>
377
+ <div class="metric-card">
378
+ <div class="metric-label">Monotonic</div>
379
+ <div class="metric-value">{"Yes" if monotonic else "No"}</div>
380
+ </div>
381
+ </div>
382
+ """)
383
+
384
+ # Turnover summary
385
+ if tear_sheet.turnover_analysis is not None:
386
+ ta = tear_sheet.turnover_analysis
387
+ periods = list(ta.mean_turnover.keys())
388
+ first_period = periods[0] if periods else "1D"
389
+
390
+ turnover = ta.mean_turnover.get(first_period, 0)
391
+ half_life = ta.half_life.get(first_period)
392
+
393
+ html_parts.append(f"""
394
+ <h3>Turnover ({first_period})</h3>
395
+ <div class="metric-grid">
396
+ <div class="metric-card">
397
+ <div class="metric-label">Mean Turnover</div>
398
+ <div class="metric-value">{turnover:.1%}</div>
399
+ <div class="metric-sublabel">
400
+ {"High" if turnover > 0.3 else "Moderate" if turnover > 0.15 else "Low"}
401
+ </div>
402
+ </div>
403
+ <div class="metric-card">
404
+ <div class="metric-label">Signal Half-Life</div>
405
+ <div class="metric-value">
406
+ {f"{half_life:.1f}" if half_life else "N/A"}
407
+ </div>
408
+ <div class="metric-sublabel">periods</div>
409
+ </div>
410
+ </div>
411
+ """)
412
+
413
+ # IR_tc summary
414
+ if tear_sheet.ir_tc_analysis is not None:
415
+ ir_tc = tear_sheet.ir_tc_analysis
416
+ periods = list(ir_tc.ir_gross.keys())
417
+ first_period = periods[0] if periods else "1D"
418
+
419
+ ir_gross = ir_tc.ir_gross.get(first_period, 0)
420
+ ir_net = ir_tc.ir_tc.get(first_period, 0)
421
+ cost_drag = ir_tc.cost_drag.get(first_period, 0)
422
+
423
+ html_parts.append(f"""
424
+ <h3>Transaction Cost Impact ({first_period})</h3>
425
+ <div class="metric-grid">
426
+ <div class="metric-card">
427
+ <div class="metric-label">Gross IR</div>
428
+ <div class="metric-value">{ir_gross:.3f}</div>
429
+ </div>
430
+ <div class="metric-card">
431
+ <div class="metric-label">Net IR (after costs)</div>
432
+ <div class="metric-value">{ir_net:.3f}</div>
433
+ </div>
434
+ <div class="metric-card">
435
+ <div class="metric-label">Cost Drag</div>
436
+ <div class="metric-value">{cost_drag:.1%}</div>
437
+ </div>
438
+ </div>
439
+ """)
440
+
441
+ return "\n".join(html_parts)
442
+
443
+ # =========================================================================
444
+ # IC Analysis Tab
445
+ # =========================================================================
446
+
447
+ def _create_ic_tab(self, tear_sheet: SignalTearSheet) -> str:
448
+ """Create IC Analysis tab with all IC visualizations."""
449
+ html_parts = ["<h2>Information Coefficient Analysis</h2>"]
450
+
451
+ if tear_sheet.ic_analysis is None:
452
+ html_parts.append("<p>IC analysis not available.</p>")
453
+ return "\n".join(html_parts)
454
+
455
+ ic = tear_sheet.ic_analysis
456
+ theme_name = "dark" if self.theme == "dark" else "default"
457
+
458
+ # Period selector
459
+ periods = list(ic.ic_mean.keys())
460
+ html_parts.append(self._create_period_selector("ic", periods))
461
+
462
+ # IC Time Series
463
+ try:
464
+ fig_ts = plot_ic_ts(ic, period=periods[0], theme=theme_name)
465
+ html_parts.append('<div class="plot-container">')
466
+ html_parts.append(fig_ts.to_html(include_plotlyjs=False, full_html=False))
467
+ html_parts.append("</div>")
468
+ except Exception:
469
+ html_parts.append("<p>IC time series plot unavailable.</p>")
470
+
471
+ # Two-column layout for histogram and Q-Q
472
+ html_parts.append('<div style="display: flex; gap: 20px; flex-wrap: wrap;">')
473
+
474
+ # IC Histogram
475
+ try:
476
+ fig_hist = plot_ic_histogram(ic, period=periods[0], theme=theme_name)
477
+ html_parts.append('<div class="plot-container" style="flex: 1; min-width: 400px;">')
478
+ html_parts.append(fig_hist.to_html(include_plotlyjs=False, full_html=False))
479
+ html_parts.append("</div>")
480
+ except Exception:
481
+ html_parts.append("<p>IC histogram unavailable.</p>")
482
+
483
+ # IC Q-Q Plot
484
+ try:
485
+ fig_qq = plot_ic_qq(ic, period=periods[0], theme=theme_name)
486
+ html_parts.append('<div class="plot-container" style="flex: 1; min-width: 400px;">')
487
+ html_parts.append(fig_qq.to_html(include_plotlyjs=False, full_html=False))
488
+ html_parts.append("</div>")
489
+ except Exception:
490
+ html_parts.append("<p>IC Q-Q plot unavailable.</p>")
491
+
492
+ html_parts.append("</div>")
493
+
494
+ # IC Heatmap (monthly)
495
+ try:
496
+ fig_heatmap = plot_ic_heatmap(ic, period=periods[0], theme=theme_name)
497
+ html_parts.append('<div class="plot-container">')
498
+ html_parts.append(fig_heatmap.to_html(include_plotlyjs=False, full_html=False))
499
+ html_parts.append("</div>")
500
+ except Exception:
501
+ html_parts.append("<p>IC heatmap unavailable.</p>")
502
+
503
+ return "\n".join(html_parts)
504
+
505
+ # =========================================================================
506
+ # Quantile Analysis Tab
507
+ # =========================================================================
508
+
509
+ def _create_quantile_tab(self, tear_sheet: SignalTearSheet) -> str:
510
+ """Create Quantile Analysis tab."""
511
+ html_parts = ["<h2>Quantile Returns Analysis</h2>"]
512
+
513
+ if tear_sheet.quantile_analysis is None:
514
+ html_parts.append("<p>Quantile analysis not available.</p>")
515
+ return "\n".join(html_parts)
516
+
517
+ qa = tear_sheet.quantile_analysis
518
+ theme_name = "dark" if self.theme == "dark" else "default"
519
+
520
+ # Period selector
521
+ periods = qa.periods
522
+ html_parts.append(self._create_period_selector("quantile", periods))
523
+
524
+ # Quantile Returns Bar
525
+ try:
526
+ fig_bar = plot_quantile_returns_bar(qa, period=periods[0], theme=theme_name)
527
+ html_parts.append('<div class="plot-container">')
528
+ html_parts.append(fig_bar.to_html(include_plotlyjs=False, full_html=False))
529
+ html_parts.append("</div>")
530
+ except Exception:
531
+ html_parts.append("<p>Quantile returns bar chart unavailable.</p>")
532
+
533
+ # Two-column layout
534
+ html_parts.append('<div style="display: flex; gap: 20px; flex-wrap: wrap;">')
535
+
536
+ # Quantile Returns Violin
537
+ try:
538
+ fig_violin = plot_quantile_returns_violin(qa, period=periods[0], theme=theme_name)
539
+ html_parts.append('<div class="plot-container" style="flex: 1; min-width: 400px;">')
540
+ html_parts.append(fig_violin.to_html(include_plotlyjs=False, full_html=False))
541
+ html_parts.append("</div>")
542
+ except Exception:
543
+ html_parts.append("<p>Quantile violin plot unavailable.</p>")
544
+
545
+ # Spread Time Series
546
+ try:
547
+ fig_spread = plot_spread_timeseries(qa, period=periods[0], theme=theme_name)
548
+ html_parts.append('<div class="plot-container" style="flex: 1; min-width: 400px;">')
549
+ html_parts.append(fig_spread.to_html(include_plotlyjs=False, full_html=False))
550
+ html_parts.append("</div>")
551
+ except Exception:
552
+ html_parts.append("<p>Spread time series unavailable.</p>")
553
+
554
+ html_parts.append("</div>")
555
+
556
+ # Cumulative Returns
557
+ if qa.cumulative_returns is not None:
558
+ try:
559
+ fig_cum = plot_cumulative_returns(qa, period=periods[0], theme=theme_name)
560
+ html_parts.append('<div class="plot-container">')
561
+ html_parts.append(fig_cum.to_html(include_plotlyjs=False, full_html=False))
562
+ html_parts.append("</div>")
563
+ except Exception:
564
+ html_parts.append("<p>Cumulative returns plot unavailable.</p>")
565
+
566
+ return "\n".join(html_parts)
567
+
568
+ # =========================================================================
569
+ # Turnover Tab
570
+ # =========================================================================
571
+
572
+ def _create_turnover_tab(self, tear_sheet: SignalTearSheet) -> str:
573
+ """Create Turnover tab."""
574
+ html_parts = ["<h2>Signal Turnover Analysis</h2>"]
575
+
576
+ if tear_sheet.turnover_analysis is None:
577
+ html_parts.append("<p>Turnover analysis not available.</p>")
578
+ return "\n".join(html_parts)
579
+
580
+ ta = tear_sheet.turnover_analysis
581
+ theme_name = "dark" if self.theme == "dark" else "default"
582
+
583
+ # Two-column layout
584
+ html_parts.append('<div style="display: flex; gap: 20px; flex-wrap: wrap;">')
585
+
586
+ # Top/Bottom Turnover
587
+ try:
588
+ fig_turnover = plot_top_bottom_turnover(ta, theme=theme_name)
589
+ html_parts.append('<div class="plot-container" style="flex: 1; min-width: 400px;">')
590
+ html_parts.append(fig_turnover.to_html(include_plotlyjs=False, full_html=False))
591
+ html_parts.append("</div>")
592
+ except Exception:
593
+ html_parts.append("<p>Turnover chart unavailable.</p>")
594
+
595
+ # Autocorrelation
596
+ periods = list(ta.autocorrelation.keys())
597
+ if periods:
598
+ try:
599
+ fig_ac = plot_autocorrelation(ta, period=periods[0], theme=theme_name)
600
+ html_parts.append('<div class="plot-container" style="flex: 1; min-width: 400px;">')
601
+ html_parts.append(fig_ac.to_html(include_plotlyjs=False, full_html=False))
602
+ html_parts.append("</div>")
603
+ except Exception:
604
+ html_parts.append("<p>Autocorrelation plot unavailable.</p>")
605
+
606
+ html_parts.append("</div>")
607
+
608
+ # Summary table
609
+ html_parts.append(self._create_turnover_summary_table(ta))
610
+
611
+ return "\n".join(html_parts)
612
+
613
+ def _create_turnover_summary_table(self, ta: Any) -> str:
614
+ """Create turnover summary table."""
615
+ periods = list(ta.mean_turnover.keys())
616
+
617
+ rows = []
618
+ for period in periods:
619
+ mean_to = ta.mean_turnover.get(period, 0)
620
+ top_to = ta.top_quantile_turnover.get(period, 0)
621
+ bottom_to = ta.bottom_quantile_turnover.get(period, 0)
622
+ mean_ac = ta.mean_autocorrelation.get(period, 0)
623
+ half_life = ta.half_life.get(period)
624
+
625
+ rows.append(f"""
626
+ <tr>
627
+ <td>{period}</td>
628
+ <td>{mean_to:.1%}</td>
629
+ <td>{top_to:.1%}</td>
630
+ <td>{bottom_to:.1%}</td>
631
+ <td>{mean_ac:.4f}</td>
632
+ <td>{f"{half_life:.1f}" if half_life else "N/A"}</td>
633
+ </tr>
634
+ """)
635
+
636
+ return f"""
637
+ <h3>Turnover Summary</h3>
638
+ <table class="feature-table">
639
+ <thead>
640
+ <tr>
641
+ <th>Period</th>
642
+ <th>Mean Turnover</th>
643
+ <th>Top Quantile</th>
644
+ <th>Bottom Quantile</th>
645
+ <th>Mean AC</th>
646
+ <th>Half-Life</th>
647
+ </tr>
648
+ </thead>
649
+ <tbody>
650
+ {"".join(rows)}
651
+ </tbody>
652
+ </table>
653
+ """
654
+
655
+ # =========================================================================
656
+ # Events Tab - Event Study Analysis
657
+ # =========================================================================
658
+
659
+ def _create_events_tab(self, event_analysis: EventStudyResult) -> str:
660
+ """Create Events tab for event study results.
661
+
662
+ Displays comprehensive event study analysis including:
663
+ - Summary metrics (CAAR, significance, n events)
664
+ - CAAR time series with confidence bands
665
+ - Event drift heatmap
666
+ - AR distribution on event day
667
+ - CAR by event bar chart
668
+
669
+ Parameters
670
+ ----------
671
+ event_analysis : EventStudyResult
672
+ Complete event study results.
673
+
674
+ Returns
675
+ -------
676
+ str
677
+ HTML content for the Events tab.
678
+ """
679
+ html_parts = ["<h2>Event Study Analysis</h2>"]
680
+ theme_name = "dark" if self.theme == "dark" else "default"
681
+
682
+ # Summary metrics section
683
+ sig_status = "Significant" if event_analysis.is_significant else "Not Significant"
684
+ sig_color = "#28a745" if event_analysis.is_significant else "#dc3545"
685
+
686
+ html_parts.append(f"""
687
+ <div class="metric-grid">
688
+ <div class="metric-card">
689
+ <div class="metric-label">Events Analyzed</div>
690
+ <div class="metric-value">{event_analysis.n_events}</div>
691
+ </div>
692
+ <div class="metric-card">
693
+ <div class="metric-label">Event Window</div>
694
+ <div class="metric-value">
695
+ [{event_analysis.event_window[0]}, {event_analysis.event_window[1]}]
696
+ </div>
697
+ </div>
698
+ <div class="metric-card">
699
+ <div class="metric-label">Model</div>
700
+ <div class="metric-value">{event_analysis.model_name.replace("_", " ").title()}</div>
701
+ </div>
702
+ <div class="metric-card">
703
+ <div class="metric-label">Final CAAR</div>
704
+ <div class="metric-value">{event_analysis.final_caar:+.4f}</div>
705
+ <div class="metric-sublabel">{event_analysis.final_caar * 100:+.2f}%</div>
706
+ </div>
707
+ </div>
708
+ """)
709
+
710
+ # Event day AAR and significance
711
+ html_parts.append(f"""
712
+ <div class="metric-grid">
713
+ <div class="metric-card">
714
+ <div class="metric-label">Event Day AAR (t=0)</div>
715
+ <div class="metric-value">{event_analysis.event_day_aar:+.4f}</div>
716
+ <div class="metric-sublabel">{event_analysis.event_day_aar * 100:+.2f}%</div>
717
+ </div>
718
+ <div class="metric-card">
719
+ <div class="metric-label">Test</div>
720
+ <div class="metric-value">{event_analysis.test_name.replace("_", " ").title()}</div>
721
+ </div>
722
+ <div class="metric-card">
723
+ <div class="metric-label">Test Statistic</div>
724
+ <div class="metric-value">{event_analysis.test_statistic:.3f}</div>
725
+ </div>
726
+ <div class="metric-card">
727
+ <div class="metric-label">P-value</div>
728
+ <div class="metric-value">{event_analysis.p_value:.4f}</div>
729
+ <div class="metric-sublabel" style="color: {sig_color};">{sig_status}</div>
730
+ </div>
731
+ </div>
732
+ """)
733
+
734
+ # CAAR plot with confidence bands
735
+ try:
736
+ fig_caar = plot_caar(
737
+ event_analysis,
738
+ show_confidence=True,
739
+ show_aar_bars=True,
740
+ theme=theme_name,
741
+ )
742
+ html_parts.append('<div class="plot-container">')
743
+ html_parts.append(fig_caar.to_html(include_plotlyjs=False, full_html=False))
744
+ html_parts.append("</div>")
745
+ except Exception as e:
746
+ html_parts.append(f"<p>CAAR plot unavailable: {e}</p>")
747
+
748
+ # Two-column layout for heatmap and distribution
749
+ html_parts.append('<div style="display: flex; gap: 20px; flex-wrap: wrap;">')
750
+
751
+ # Event heatmap (if individual results available)
752
+ if (
753
+ event_analysis.individual_results is not None
754
+ and len(event_analysis.individual_results) > 0
755
+ ):
756
+ try:
757
+ # Limit to 30 events for readability
758
+ ar_results = event_analysis.individual_results[:30]
759
+ fig_heatmap = plot_event_heatmap(ar_results, theme=theme_name)
760
+ html_parts.append('<div class="plot-container" style="flex: 1; min-width: 500px;">')
761
+ html_parts.append(fig_heatmap.to_html(include_plotlyjs=False, full_html=False))
762
+ html_parts.append("</div>")
763
+ except Exception as e:
764
+ html_parts.append(f"<p>Event heatmap unavailable: {e}</p>")
765
+
766
+ # AR distribution on event day
767
+ try:
768
+ fig_dist = plot_ar_distribution(
769
+ event_analysis,
770
+ day=0,
771
+ show_kde=True,
772
+ theme=theme_name,
773
+ )
774
+ html_parts.append('<div class="plot-container" style="flex: 1; min-width: 400px;">')
775
+ html_parts.append(fig_dist.to_html(include_plotlyjs=False, full_html=False))
776
+ html_parts.append("</div>")
777
+ except Exception as e:
778
+ html_parts.append(f"<p>AR distribution plot unavailable: {e}</p>")
779
+
780
+ html_parts.append("</div>") # Close flex container
781
+
782
+ # CAR by event bar chart (top 20 by magnitude)
783
+ if (
784
+ event_analysis.individual_results is not None
785
+ and len(event_analysis.individual_results) > 0
786
+ ):
787
+ try:
788
+ fig_car = plot_car_by_event(
789
+ event_analysis.individual_results,
790
+ sort_by="car",
791
+ top_n=min(20, len(event_analysis.individual_results)),
792
+ theme=theme_name,
793
+ )
794
+ html_parts.append('<div class="plot-container">')
795
+ html_parts.append(fig_car.to_html(include_plotlyjs=False, full_html=False))
796
+ html_parts.append("</div>")
797
+ except Exception as e:
798
+ html_parts.append(f"<p>CAR by event chart unavailable: {e}</p>")
799
+
800
+ # Events table
801
+ if (
802
+ event_analysis.individual_results is not None
803
+ and len(event_analysis.individual_results) > 0
804
+ ):
805
+ html_parts.append(self._create_events_table(event_analysis))
806
+
807
+ return "\n".join(html_parts)
808
+
809
+ def _create_events_table(self, event_analysis: EventStudyResult) -> str:
810
+ """Create table summarizing individual event results.
811
+
812
+ Parameters
813
+ ----------
814
+ event_analysis : EventStudyResult
815
+ Event study results with individual event data.
816
+
817
+ Returns
818
+ -------
819
+ str
820
+ HTML table of event results.
821
+ """
822
+ if event_analysis.individual_results is None:
823
+ return ""
824
+
825
+ # Sort by CAR magnitude (descending)
826
+ sorted_results = sorted(
827
+ event_analysis.individual_results,
828
+ key=lambda x: abs(x.car),
829
+ reverse=True,
830
+ )
831
+
832
+ rows = []
833
+ for r in sorted_results[:20]: # Limit to top 20
834
+ car_color = "#28a745" if r.car >= 0 else "#dc3545"
835
+ ar_day0 = r.ar_by_day.get(0, 0.0)
836
+ beta_str = f"{r.estimation_beta:.2f}" if r.estimation_beta is not None else "N/A"
837
+ rows.append(f"""
838
+ <tr>
839
+ <td>{r.event_id}</td>
840
+ <td>{r.asset}</td>
841
+ <td>{r.event_date[:10] if len(r.event_date) >= 10 else r.event_date}</td>
842
+ <td style="color: {car_color};">{r.car:+.4f}</td>
843
+ <td>{ar_day0:+.4f}</td>
844
+ <td>{beta_str}</td>
845
+ </tr>
846
+ """)
847
+
848
+ n_shown = min(20, len(event_analysis.individual_results))
849
+ n_total = len(event_analysis.individual_results)
850
+ table_title = f"Individual Event Results (Top {n_shown} of {n_total} by |CAR|)"
851
+
852
+ return f"""
853
+ <h3>{table_title}</h3>
854
+ <table class="feature-table">
855
+ <thead>
856
+ <tr>
857
+ <th>Event ID</th>
858
+ <th>Asset</th>
859
+ <th>Event Date</th>
860
+ <th>CAR</th>
861
+ <th>AR (t=0)</th>
862
+ <th>Beta</th>
863
+ </tr>
864
+ </thead>
865
+ <tbody>
866
+ {"".join(rows)}
867
+ </tbody>
868
+ </table>
869
+ """
870
+
871
+ # =========================================================================
872
+ # Helper Methods
873
+ # =========================================================================
874
+
875
+ def _create_period_selector(self, tab_id: str, periods: list[str]) -> str:
876
+ """Create period selector dropdown (for future JS interactivity)."""
877
+ if len(periods) <= 1:
878
+ return ""
879
+
880
+ options = "".join([f'<option value="{p}">{p}</option>' for p in periods])
881
+ return f"""
882
+ <div class="period-selector" style="margin-bottom: 15px;">
883
+ <label for="{tab_id}-period">Period: </label>
884
+ <select id="{tab_id}-period" style="padding: 5px;">
885
+ {options}
886
+ </select>
887
+ <span style="font-size: 0.85em; color: #666; margin-left: 10px;">
888
+ (Changing period will update in future version)
889
+ </span>
890
+ </div>
891
+ """
892
+
893
+ def _compose_html(self) -> str:
894
+ """Compose complete HTML document."""
895
+ return f"""
896
+ <!DOCTYPE html>
897
+ <html lang="en">
898
+ <head>
899
+ <meta charset="UTF-8">
900
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
901
+ <title>{self.title}</title>
902
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
903
+ {self._get_base_styles()}
904
+ </head>
905
+ <body>
906
+ {self._build_header()}
907
+ {self._build_sections()}
908
+ {self._get_base_scripts()}
909
+ </body>
910
+ </html>
911
+ """