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,974 @@
1
+ """Multi-Signal Analysis Dashboard - Multi-tab interactive HTML dashboard.
2
+
3
+ This module provides the MultiSignalDashboard class for creating comprehensive,
4
+ self-contained HTML dashboards for multi-signal comparison and analysis.
5
+
6
+ The dashboard follows the Focus+Context pattern with 5 tabs:
7
+ 1. Summary - Key metrics cards, searchable/sortable table of all signals
8
+ 2. Distribution - IC ridge plot, ranking bar chart
9
+ 3. Correlation - Signal correlation cluster heatmap
10
+ 4. Efficiency - Pareto frontier scatter (IC IR vs Turnover)
11
+ 5. Comparison (optional) - Side-by-side tear sheets for selected signals
12
+
13
+ References
14
+ ----------
15
+ Tufte, E. (1983). "The Visual Display of Quantitative Information"
16
+ Few, S. (2012). "Show Me the Numbers"
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, Any, Literal
22
+
23
+ import polars as pl
24
+
25
+ from ml4t.diagnostic.visualization.dashboards import (
26
+ BaseDashboard,
27
+ DashboardSection,
28
+ )
29
+ from ml4t.diagnostic.visualization.signal.multi_signal_plots import (
30
+ plot_ic_ridge,
31
+ plot_pareto_frontier,
32
+ plot_signal_correlation_heatmap,
33
+ plot_signal_ranking_bar,
34
+ )
35
+
36
+ if TYPE_CHECKING:
37
+ from ml4t.diagnostic.results.multi_signal_results import (
38
+ ComparisonResult,
39
+ MultiSignalSummary,
40
+ )
41
+
42
+
43
+ class MultiSignalDashboard(BaseDashboard):
44
+ """Interactive multi-tab dashboard for multi-signal analysis results.
45
+
46
+ Creates a self-contained HTML dashboard with comprehensive visualizations
47
+ for analyzing and comparing 50-200 signals simultaneously.
48
+
49
+ The dashboard includes 5 tabs:
50
+
51
+ 1. **Summary**: Metric cards with FDR/FWER counts, searchable signal table
52
+ 2. **Distribution**: IC ridge plot showing IC ranges, ranking bar chart
53
+ 3. **Correlation**: Hierarchical cluster heatmap revealing redundant signals
54
+ 4. **Efficiency**: Pareto frontier scatter (IC IR vs Turnover trade-off)
55
+ 5. **Comparison** (optional): Side-by-side metrics for selected signals
56
+
57
+ Examples
58
+ --------
59
+ >>> from ml4t.diagnostic.evaluation import MultiSignalAnalysis
60
+ >>> from ml4t.diagnostic.visualization.signal import MultiSignalDashboard
61
+ >>>
62
+ >>> # Run multi-signal analysis
63
+ >>> analyzer = MultiSignalAnalysis(signals_dict, price_data)
64
+ >>> summary = analyzer.compute_summary()
65
+ >>> corr_matrix = analyzer.correlation_matrix()
66
+ >>>
67
+ >>> # Create and save dashboard
68
+ >>> dashboard = MultiSignalDashboard(title="Alpha Signal Comparison")
69
+ >>> dashboard.save(
70
+ ... "multi_signal_dashboard.html",
71
+ ... summary=summary,
72
+ ... correlation_matrix=corr_matrix
73
+ ... )
74
+
75
+ >>> # Dark theme with comparison
76
+ >>> comparison = analyzer.compare(selection="uncorrelated", n=5)
77
+ >>> dashboard = MultiSignalDashboard(title="Top Signals", theme="dark")
78
+ >>> html = dashboard.generate(
79
+ ... summary=summary,
80
+ ... correlation_matrix=corr_matrix,
81
+ ... comparison=comparison
82
+ ... )
83
+
84
+ Notes
85
+ -----
86
+ - Dashboard is self-contained HTML with embedded Plotly.js (via CDN)
87
+ - All visualizations are interactive (zoom, pan, hover)
88
+ - Works offline once loaded (all data embedded)
89
+
90
+ See Also
91
+ --------
92
+ MultiSignalAnalysis : Main multi-signal analysis class
93
+ MultiSignalSummary : Result container for multi-signal summary
94
+ ComparisonResult : Result container for signal comparison
95
+ """
96
+
97
+ def __init__(
98
+ self,
99
+ title: str = "Multi-Signal Analysis Dashboard",
100
+ theme: Literal["light", "dark"] = "light",
101
+ width: int | None = None,
102
+ height: int | None = None,
103
+ ):
104
+ """Initialize Multi-Signal Analysis Dashboard.
105
+
106
+ Parameters
107
+ ----------
108
+ title : str, default="Multi-Signal Analysis Dashboard"
109
+ Dashboard title displayed at top
110
+ theme : {'light', 'dark'}, default='light'
111
+ Visual theme for all plots and styling
112
+ width : int, optional
113
+ Dashboard width in pixels. If None, uses responsive width.
114
+ height : int, optional
115
+ Dashboard height in pixels. If None, uses auto height.
116
+ """
117
+ super().__init__(title, theme, width, height)
118
+
119
+ def generate(
120
+ self,
121
+ analysis_results: MultiSignalSummary,
122
+ correlation_matrix: pl.DataFrame | None = None,
123
+ comparison: ComparisonResult | None = None,
124
+ **_kwargs: Any,
125
+ ) -> str:
126
+ """Generate complete dashboard HTML.
127
+
128
+ Parameters
129
+ ----------
130
+ analysis_results : MultiSignalSummary
131
+ Results from MultiSignalAnalysis.compute_summary()
132
+ correlation_matrix : pl.DataFrame | None
133
+ Signal correlation matrix from MultiSignalAnalysis.correlation_matrix()
134
+ comparison : ComparisonResult | None
135
+ Optional comparison results for selected signals
136
+ **kwargs
137
+ Additional parameters (currently unused)
138
+
139
+ Returns
140
+ -------
141
+ str
142
+ Complete HTML document
143
+ """
144
+ # Clear any previous sections
145
+ self.sections: list[DashboardSection] = []
146
+
147
+ # Create tabbed layout
148
+ self._create_tabbed_layout(analysis_results, correlation_matrix, comparison)
149
+
150
+ # Compose final HTML
151
+ return self._compose_html()
152
+
153
+ def save(
154
+ self,
155
+ output_path: str,
156
+ analysis_results: MultiSignalSummary,
157
+ correlation_matrix: pl.DataFrame | None = None,
158
+ comparison: ComparisonResult | None = None,
159
+ **kwargs: Any,
160
+ ) -> str:
161
+ """Generate and save dashboard to file.
162
+
163
+ Parameters
164
+ ----------
165
+ output_path : str
166
+ Path for output HTML file
167
+ analysis_results : MultiSignalSummary
168
+ Results from MultiSignalAnalysis.compute_summary()
169
+ correlation_matrix : pl.DataFrame | None
170
+ Signal correlation matrix
171
+ comparison : ComparisonResult | None
172
+ Optional comparison results
173
+ **kwargs
174
+ Additional parameters passed to generate()
175
+
176
+ Returns
177
+ -------
178
+ str
179
+ Path to saved file
180
+ """
181
+ html = self.generate(
182
+ analysis_results,
183
+ correlation_matrix=correlation_matrix,
184
+ comparison=comparison,
185
+ **kwargs,
186
+ )
187
+
188
+ with open(output_path, "w", encoding="utf-8") as f:
189
+ f.write(html)
190
+
191
+ return output_path
192
+
193
+ # =========================================================================
194
+ # Tabbed Layout Methods
195
+ # =========================================================================
196
+
197
+ def _create_tabbed_layout(
198
+ self,
199
+ summary: MultiSignalSummary,
200
+ correlation_matrix: pl.DataFrame | None = None,
201
+ comparison: ComparisonResult | None = None,
202
+ ) -> None:
203
+ """Create tabbed dashboard layout."""
204
+ # Tab 1: Summary
205
+ self.sections.append(self._create_summary_tab(summary))
206
+
207
+ # Tab 2: Distribution
208
+ self.sections.append(self._create_distribution_tab(summary))
209
+
210
+ # Tab 3: Correlation (if matrix provided)
211
+ if correlation_matrix is not None:
212
+ self.sections.append(self._create_correlation_tab(correlation_matrix))
213
+
214
+ # Tab 4: Efficiency
215
+ self.sections.append(self._create_efficiency_tab(summary))
216
+
217
+ # Tab 5: Comparison (if provided)
218
+ if comparison is not None:
219
+ self.sections.append(self._create_comparison_tab(comparison, summary))
220
+
221
+ def _create_summary_tab(self, summary: MultiSignalSummary) -> DashboardSection:
222
+ """Create Summary tab with metric cards and signal table."""
223
+ content_parts = []
224
+
225
+ # Metric cards
226
+ content_parts.append(self._build_metric_cards(summary))
227
+
228
+ # Signal table (searchable/sortable)
229
+ content_parts.append(self._build_signal_table(summary))
230
+
231
+ return DashboardSection(
232
+ title="Summary",
233
+ description=(
234
+ "<p>Overview of all analyzed signals with multiple testing corrections. "
235
+ f"Analyzed <strong>{summary.n_signals}</strong> signals across periods "
236
+ f"<strong>{summary.periods}</strong>.</p>"
237
+ ),
238
+ content="\n".join(content_parts),
239
+ )
240
+
241
+ def _create_distribution_tab(self, summary: MultiSignalSummary) -> DashboardSection:
242
+ """Create Distribution tab with IC ridge and ranking plots."""
243
+ content_parts = []
244
+
245
+ # IC Ridge Plot
246
+ try:
247
+ fig_ridge = plot_ic_ridge(
248
+ summary,
249
+ max_signals=50,
250
+ sort_by="ic_mean" if "ic_mean" in summary.summary_data else "ic_ir",
251
+ theme=self.theme,
252
+ )
253
+ content_parts.append(
254
+ f'<div class="plot-container">'
255
+ f"{fig_ridge.to_html(full_html=False, include_plotlyjs='cdn')}"
256
+ f"</div>"
257
+ )
258
+ except Exception as e:
259
+ content_parts.append(f'<p class="error">Could not create IC ridge plot: {e}</p>')
260
+
261
+ # Ranking Bar Chart
262
+ try:
263
+ metric = "ic_ir" if "ic_ir" in summary.summary_data else "ic_mean"
264
+ fig_bar = plot_signal_ranking_bar(
265
+ summary,
266
+ metric=metric,
267
+ top_n=20,
268
+ theme=self.theme,
269
+ )
270
+ content_parts.append(
271
+ f'<div class="plot-container">'
272
+ f"{fig_bar.to_html(full_html=False, include_plotlyjs=False)}"
273
+ f"</div>"
274
+ )
275
+ except Exception as e:
276
+ content_parts.append(f'<p class="error">Could not create ranking plot: {e}</p>')
277
+
278
+ return DashboardSection(
279
+ title="Distribution",
280
+ description=(
281
+ "<p>IC distribution across signals. The ridge plot shows IC range "
282
+ "(5th-95th percentile) with mean indicated. Green indicates FDR-significant.</p>"
283
+ ),
284
+ content="\n".join(content_parts),
285
+ )
286
+
287
+ def _create_correlation_tab(self, correlation_matrix: pl.DataFrame) -> DashboardSection:
288
+ """Create Correlation tab with cluster heatmap."""
289
+ content_parts = []
290
+
291
+ # Insights about correlation structure
292
+ try:
293
+ corr_values = correlation_matrix.to_numpy()
294
+ n_signals = len(correlation_matrix.columns)
295
+
296
+ # Count high correlations (excluding diagonal)
297
+ high_corr_count = 0
298
+ for i in range(n_signals):
299
+ for j in range(i + 1, n_signals):
300
+ if abs(corr_values[i, j]) > 0.7:
301
+ high_corr_count += 1
302
+
303
+ total_pairs = n_signals * (n_signals - 1) // 2
304
+ pct_redundant = high_corr_count / total_pairs * 100 if total_pairs > 0 else 0
305
+
306
+ content_parts.append(
307
+ f'<div class="insights-panel">'
308
+ f"<h3>Correlation Analysis</h3>"
309
+ f"<ul>"
310
+ f"<li><strong>{n_signals}</strong> signals analyzed</li>"
311
+ f"<li><strong>{high_corr_count}</strong> pairs ({pct_redundant:.1f}%) have |correlation| > 0.7</li>"
312
+ f"<li>Highly correlated signals may represent the same underlying alpha</li>"
313
+ f"</ul>"
314
+ f"</div>"
315
+ )
316
+ except Exception:
317
+ pass
318
+
319
+ # Correlation heatmap
320
+ try:
321
+ fig_heatmap = plot_signal_correlation_heatmap(
322
+ correlation_matrix,
323
+ cluster=True,
324
+ max_signals=100,
325
+ theme=self.theme,
326
+ )
327
+ content_parts.append(
328
+ f'<div class="plot-container">'
329
+ f"{fig_heatmap.to_html(full_html=False, include_plotlyjs=False)}"
330
+ f"</div>"
331
+ )
332
+ except Exception as e:
333
+ content_parts.append(f'<p class="error">Could not create heatmap: {e}</p>')
334
+
335
+ return DashboardSection(
336
+ title="Correlation",
337
+ description=(
338
+ "<p>Signal correlation matrix with hierarchical clustering. "
339
+ "Clustered ordering reveals groups of similar signals - "
340
+ "the '100 signals = 3 unique bets' pattern.</p>"
341
+ ),
342
+ content="\n".join(content_parts),
343
+ )
344
+
345
+ def _create_efficiency_tab(self, summary: MultiSignalSummary) -> DashboardSection:
346
+ """Create Efficiency tab with Pareto frontier plot."""
347
+ content_parts = []
348
+
349
+ # Check required metrics
350
+ has_turnover = "turnover_mean" in summary.summary_data
351
+ has_ic_ir = "ic_ir" in summary.summary_data
352
+
353
+ if has_turnover and has_ic_ir:
354
+ try:
355
+ fig_pareto = plot_pareto_frontier(
356
+ summary,
357
+ x_metric="turnover_mean",
358
+ y_metric="ic_ir",
359
+ theme=self.theme,
360
+ )
361
+ content_parts.append(
362
+ f'<div class="plot-container">'
363
+ f"{fig_pareto.to_html(full_html=False, include_plotlyjs=False)}"
364
+ f"</div>"
365
+ )
366
+ except Exception as e:
367
+ content_parts.append(f'<p class="error">Could not create Pareto plot: {e}</p>')
368
+ else:
369
+ missing = []
370
+ if not has_turnover:
371
+ missing.append("turnover_mean")
372
+ if not has_ic_ir:
373
+ missing.append("ic_ir")
374
+ content_parts.append(
375
+ f'<p class="warning">Pareto frontier plot requires: {", ".join(missing)}</p>'
376
+ )
377
+
378
+ return DashboardSection(
379
+ title="Efficiency",
380
+ description=(
381
+ "<p>Pareto frontier showing trade-off between signal quality (IC IR) and "
382
+ "implementation cost (turnover). Signals on the frontier offer the best "
383
+ "risk-adjusted returns for their turnover level.</p>"
384
+ ),
385
+ content="\n".join(content_parts),
386
+ )
387
+
388
+ def _create_comparison_tab(
389
+ self,
390
+ comparison: ComparisonResult,
391
+ summary: MultiSignalSummary,
392
+ ) -> DashboardSection:
393
+ """Create Comparison tab for selected signals."""
394
+ content_parts = []
395
+
396
+ # Selection info
397
+ content_parts.append(
398
+ f'<div class="insights-panel">'
399
+ f"<h3>Selection Details</h3>"
400
+ f"<ul>"
401
+ f"<li>Method: <strong>{comparison.selection_method}</strong></li>"
402
+ f"<li>Signals selected: <strong>{len(comparison.signals)}</strong></li>"
403
+ f"</ul>"
404
+ f"<p>Selected signals: {', '.join(comparison.signals)}</p>"
405
+ f"</div>"
406
+ )
407
+
408
+ # Comparison table
409
+ content_parts.append(self._build_comparison_table(comparison, summary))
410
+
411
+ return DashboardSection(
412
+ title="Comparison",
413
+ description=(
414
+ "<p>Side-by-side comparison of selected signals. Signals were selected "
415
+ f'using the "<strong>{comparison.selection_method}</strong>" method.</p>'
416
+ ),
417
+ content="\n".join(content_parts),
418
+ )
419
+
420
+ # =========================================================================
421
+ # Helper Methods
422
+ # =========================================================================
423
+
424
+ def _build_metric_cards(self, summary: MultiSignalSummary) -> str:
425
+ """Build metric cards HTML."""
426
+ fdr_pct = summary.n_fdr_significant / summary.n_signals * 100
427
+ fwer_pct = summary.n_fwer_significant / summary.n_signals * 100
428
+
429
+ return f"""
430
+ <div class="metric-grid">
431
+ <div class="metric-card">
432
+ <div class="metric-label">Total Signals</div>
433
+ <div class="metric-value">{summary.n_signals}</div>
434
+ <div class="metric-sublabel">Periods: {summary.periods}</div>
435
+ </div>
436
+ <div class="metric-card">
437
+ <div class="metric-label">FDR Significant (α={summary.fdr_alpha:.0%})</div>
438
+ <div class="metric-value">{summary.n_fdr_significant}</div>
439
+ <div class="metric-sublabel">{fdr_pct:.1f}% of signals</div>
440
+ </div>
441
+ <div class="metric-card">
442
+ <div class="metric-label">FWER Significant (α={summary.fwer_alpha:.0%})</div>
443
+ <div class="metric-value">{summary.n_fwer_significant}</div>
444
+ <div class="metric-sublabel">{fwer_pct:.1f}% of signals</div>
445
+ </div>
446
+ </div>
447
+ """
448
+
449
+ def _build_signal_table(self, summary: MultiSignalSummary) -> str:
450
+ """Build searchable/sortable signal table HTML."""
451
+ df = summary.get_dataframe()
452
+
453
+ # Define columns to display (in order)
454
+ display_cols = ["signal_name"]
455
+
456
+ # Add metrics columns if available
457
+ for col in ["ic_mean", "ic_std", "ic_ir", "ic_t_stat", "ic_p_value"]:
458
+ if col in df.columns:
459
+ display_cols.append(col)
460
+
461
+ # Add turnover if available
462
+ if "turnover_mean" in df.columns:
463
+ display_cols.append("turnover_mean")
464
+
465
+ # Add significance flags
466
+ for col in ["fdr_significant", "fwer_significant"]:
467
+ if col in df.columns:
468
+ display_cols.append(col)
469
+
470
+ # Build header
471
+ header_cells = []
472
+ for col in display_cols:
473
+ nice_name = col.replace("_", " ").title()
474
+ header_cells.append(f"<th>{nice_name}</th>")
475
+
476
+ # Build rows
477
+ rows = []
478
+ for i in range(len(df)):
479
+ cells = []
480
+ for col in display_cols:
481
+ value = df[col][i]
482
+
483
+ # Format based on column type
484
+ if col == "signal_name":
485
+ cell_html = f"<td><strong>{value}</strong></td>"
486
+ elif "significant" in col:
487
+ badge_class = "badge-high" if value else "badge-low"
488
+ badge_text = "Yes" if value else "No"
489
+ cell_html = f'<td><span class="badge {badge_class}">{badge_text}</span></td>'
490
+ elif col == "ic_p_value":
491
+ cell_html = f"<td>{value:.4f}</td>"
492
+ elif isinstance(value, float):
493
+ cell_html = f"<td>{value:.4f}</td>"
494
+ else:
495
+ cell_html = f"<td>{value}</td>"
496
+
497
+ cells.append(cell_html)
498
+
499
+ rows.append(f"<tr>{''.join(cells)}</tr>")
500
+
501
+ return f"""
502
+ <input type="text" id="signal-search" placeholder="Search signals..."
503
+ onkeyup="filterSignalTable()">
504
+ <table class="feature-table" id="signal-table">
505
+ <thead>
506
+ <tr>{"".join(header_cells)}</tr>
507
+ </thead>
508
+ <tbody>
509
+ {"".join(rows)}
510
+ </tbody>
511
+ </table>
512
+ """
513
+
514
+ def _build_comparison_table(
515
+ self,
516
+ comparison: ComparisonResult,
517
+ summary: MultiSignalSummary,
518
+ ) -> str:
519
+ """Build comparison table for selected signals."""
520
+ summary_df = summary.get_dataframe()
521
+ rows = []
522
+
523
+ # Define columns for comparison
524
+ display_cols = ["signal_name"]
525
+ for col in ["ic_mean", "ic_ir", "turnover_mean", "fdr_significant"]:
526
+ if col in summary_df.columns:
527
+ display_cols.append(col)
528
+
529
+ # Header
530
+ header_cells = [f"<th>{col.replace('_', ' ').title()}</th>" for col in display_cols]
531
+
532
+ # Build rows for selected signals (maintaining order)
533
+ for signal_name in comparison.signals:
534
+ # Find row in summary
535
+ mask = summary_df["signal_name"] == signal_name
536
+ if not mask.any():
537
+ continue
538
+
539
+ signal_df = summary_df.filter(mask)
540
+
541
+ cells = []
542
+ for col in display_cols:
543
+ value = signal_df[col][0]
544
+
545
+ if col == "signal_name":
546
+ cell_html = f"<td><strong>{value}</strong></td>"
547
+ elif "significant" in col:
548
+ badge_class = "badge-high" if value else "badge-low"
549
+ badge_text = "Yes" if value else "No"
550
+ cell_html = f'<td><span class="badge {badge_class}">{badge_text}</span></td>'
551
+ elif isinstance(value, float):
552
+ cell_html = f"<td>{value:.4f}</td>"
553
+ else:
554
+ cell_html = f"<td>{value}</td>"
555
+
556
+ cells.append(cell_html)
557
+
558
+ rows.append(f"<tr>{''.join(cells)}</tr>")
559
+
560
+ return f"""
561
+ <h3>Selected Signals Comparison</h3>
562
+ <table class="feature-table">
563
+ <thead>
564
+ <tr>{"".join(header_cells)}</tr>
565
+ </thead>
566
+ <tbody>
567
+ {"".join(rows)}
568
+ </tbody>
569
+ </table>
570
+ """
571
+
572
+ # =========================================================================
573
+ # HTML Composition
574
+ # =========================================================================
575
+
576
+ def _compose_html(self) -> str:
577
+ """Compose final HTML document."""
578
+ return f"""
579
+ <!DOCTYPE html>
580
+ <html lang="en">
581
+ <head>
582
+ <meta charset="UTF-8">
583
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
584
+ <title>{self.title}</title>
585
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
586
+ {self._get_base_styles()}
587
+ </head>
588
+ <body>
589
+ {self._build_header()}
590
+ {self._build_navigation()}
591
+ {self._build_sections()}
592
+ {self._get_base_scripts()}
593
+ </body>
594
+ </html>
595
+ """
596
+
597
+ def _build_header(self) -> str:
598
+ """Build dashboard header HTML."""
599
+ return f"""
600
+ <div class="dashboard-header">
601
+ <h1>{self.title}</h1>
602
+ <p class="timestamp">Generated: {self.created_at.strftime("%Y-%m-%d %H:%M:%S")}</p>
603
+ </div>
604
+ """
605
+
606
+ def _build_navigation(self) -> str:
607
+ """Build tab navigation HTML."""
608
+ if len(self.sections) <= 1:
609
+ return ""
610
+
611
+ tabs_html = []
612
+ for i, section in enumerate(self.sections):
613
+ active_class = "active" if i == 0 else ""
614
+ tabs_html.append(
615
+ f'<button class="tab-button {active_class}" '
616
+ f"onclick=\"switchTab(event, 'section-{i}')\">"
617
+ f"{section.title}</button>"
618
+ )
619
+
620
+ return f"""
621
+ <div class="tab-navigation">
622
+ {"".join(tabs_html)}
623
+ </div>
624
+ """
625
+
626
+ def _build_sections(self) -> str:
627
+ """Build all dashboard sections HTML."""
628
+ sections_html = []
629
+
630
+ for i, section in enumerate(self.sections):
631
+ active_class = "active" if i == 0 else ""
632
+ sections_html.append(f"""
633
+ <div id="section-{i}" class="tab-content {active_class}">
634
+ <h2>{section.title}</h2>
635
+ <div class="section-description">{section.description}</div>
636
+ {section.content}
637
+ </div>
638
+ """)
639
+
640
+ return "".join(sections_html)
641
+
642
+ def _get_base_styles(self) -> str:
643
+ """Get base CSS styles for dashboard."""
644
+ bg_color = self.theme_config["plot_bgcolor"]
645
+ text_color = self.theme_config["font_color"]
646
+ border_color = "#555" if self.theme == "dark" else "#ddd"
647
+
648
+ return f"""
649
+ <style>
650
+ body {{
651
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
652
+ margin: 0;
653
+ padding: 20px;
654
+ background-color: {bg_color};
655
+ color: {text_color};
656
+ }}
657
+
658
+ .dashboard-header {{
659
+ text-align: center;
660
+ margin-bottom: 30px;
661
+ padding-bottom: 20px;
662
+ border-bottom: 2px solid {border_color};
663
+ }}
664
+
665
+ .dashboard-header h1 {{
666
+ margin: 0;
667
+ font-size: 2em;
668
+ font-weight: 600;
669
+ }}
670
+
671
+ .timestamp {{
672
+ margin: 10px 0 0 0;
673
+ font-size: 0.9em;
674
+ opacity: 0.7;
675
+ }}
676
+
677
+ .tab-navigation {{
678
+ display: flex;
679
+ gap: 5px;
680
+ margin-bottom: 20px;
681
+ border-bottom: 2px solid {border_color};
682
+ }}
683
+
684
+ .tab-button {{
685
+ padding: 12px 24px;
686
+ background: transparent;
687
+ border: none;
688
+ border-bottom: 3px solid transparent;
689
+ cursor: pointer;
690
+ font-size: 1em;
691
+ color: {text_color};
692
+ transition: all 0.3s ease;
693
+ }}
694
+
695
+ .tab-button:hover {{
696
+ background-color: {"rgba(255,255,255,0.05)" if self.theme == "dark" else "rgba(0,0,0,0.05)"};
697
+ }}
698
+
699
+ .tab-button.active {{
700
+ border-bottom-color: #1f77b4;
701
+ font-weight: 600;
702
+ }}
703
+
704
+ .tab-content {{
705
+ display: none;
706
+ animation: fadeIn 0.3s;
707
+ }}
708
+
709
+ .tab-content.active {{
710
+ display: block;
711
+ }}
712
+
713
+ @keyframes fadeIn {{
714
+ from {{ opacity: 0; }}
715
+ to {{ opacity: 1; }}
716
+ }}
717
+
718
+ .section-description {{
719
+ margin: 10px 0 20px 0;
720
+ padding: 15px;
721
+ background-color: {"rgba(255,255,255,0.05)" if self.theme == "dark" else "rgba(0,0,0,0.05)"};
722
+ border-left: 4px solid #1f77b4;
723
+ border-radius: 4px;
724
+ }}
725
+
726
+ .plot-container {{
727
+ margin: 20px 0;
728
+ }}
729
+
730
+ .insights-panel {{
731
+ margin: 30px 0;
732
+ padding: 20px;
733
+ background-color: {"rgba(100,150,255,0.1)" if self.theme == "dark" else "rgba(100,150,255,0.05)"};
734
+ border-radius: 8px;
735
+ border: 1px solid {border_color};
736
+ }}
737
+
738
+ .insights-panel h3 {{
739
+ margin-top: 0;
740
+ color: #1f77b4;
741
+ }}
742
+
743
+ .insights-panel ul {{
744
+ margin: 10px 0;
745
+ padding-left: 20px;
746
+ }}
747
+
748
+ .insights-panel li {{
749
+ margin: 8px 0;
750
+ line-height: 1.5;
751
+ }}
752
+
753
+ .metric-grid {{
754
+ display: grid;
755
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
756
+ gap: 15px;
757
+ margin: 20px 0;
758
+ }}
759
+
760
+ .metric-card {{
761
+ padding: 15px;
762
+ background-color: {"rgba(255,255,255,0.05)" if self.theme == "dark" else "rgba(0,0,0,0.05)"};
763
+ border-radius: 6px;
764
+ border: 1px solid {border_color};
765
+ }}
766
+
767
+ .metric-label {{
768
+ font-size: 0.85em;
769
+ opacity: 0.7;
770
+ margin-bottom: 5px;
771
+ }}
772
+
773
+ .metric-value {{
774
+ font-size: 1.5em;
775
+ font-weight: 600;
776
+ }}
777
+
778
+ .metric-sublabel {{
779
+ font-size: 0.75em;
780
+ opacity: 0.6;
781
+ margin-top: 5px;
782
+ }}
783
+
784
+ .feature-table {{
785
+ width: 100%;
786
+ border-collapse: collapse;
787
+ margin: 20px 0;
788
+ font-size: 0.95em;
789
+ }}
790
+
791
+ .feature-table thead {{
792
+ background-color: {"rgba(255,255,255,0.1)" if self.theme == "dark" else "rgba(0,0,0,0.1)"};
793
+ }}
794
+
795
+ .feature-table th {{
796
+ padding: 12px 15px;
797
+ text-align: left;
798
+ font-weight: 600;
799
+ border-bottom: 2px solid {border_color};
800
+ cursor: pointer;
801
+ }}
802
+
803
+ .feature-table th:hover {{
804
+ background-color: {"rgba(255,255,255,0.05)" if self.theme == "dark" else "rgba(0,0,0,0.05)"};
805
+ }}
806
+
807
+ .feature-table td {{
808
+ padding: 10px 15px;
809
+ border-bottom: 1px solid {border_color};
810
+ }}
811
+
812
+ .feature-table tbody tr:hover {{
813
+ background-color: {"rgba(255,255,255,0.05)" if self.theme == "dark" else "rgba(0,0,0,0.02)"};
814
+ }}
815
+
816
+ .badge {{
817
+ display: inline-block;
818
+ padding: 4px 10px;
819
+ border-radius: 12px;
820
+ font-size: 0.85em;
821
+ font-weight: 600;
822
+ }}
823
+
824
+ .badge-high {{
825
+ background-color: rgba(40, 167, 69, 0.2);
826
+ color: #28a745;
827
+ }}
828
+
829
+ .badge-low {{
830
+ background-color: rgba(220, 53, 69, 0.2);
831
+ color: #dc3545;
832
+ }}
833
+
834
+ #signal-search {{
835
+ width: 100%;
836
+ padding: 10px;
837
+ font-size: 16px;
838
+ border: 1px solid {border_color};
839
+ border-radius: 4px;
840
+ margin-bottom: 15px;
841
+ background-color: {bg_color};
842
+ color: {text_color};
843
+ }}
844
+
845
+ #signal-search:focus {{
846
+ outline: none;
847
+ border-color: #1f77b4;
848
+ box-shadow: 0 0 5px rgba(31, 119, 180, 0.3);
849
+ }}
850
+
851
+ .error {{
852
+ color: #dc3545;
853
+ padding: 10px;
854
+ background-color: rgba(220, 53, 69, 0.1);
855
+ border-radius: 4px;
856
+ }}
857
+
858
+ .warning {{
859
+ color: #ffc107;
860
+ padding: 10px;
861
+ background-color: rgba(255, 193, 7, 0.1);
862
+ border-radius: 4px;
863
+ }}
864
+ </style>
865
+ """
866
+
867
+ def _get_base_scripts(self) -> str:
868
+ """Get base JavaScript for interactivity."""
869
+ return """
870
+ <script>
871
+ function switchTab(event, sectionId) {
872
+ // Hide all tab contents
873
+ const contents = document.getElementsByClassName('tab-content');
874
+ for (let content of contents) {
875
+ content.classList.remove('active');
876
+ }
877
+
878
+ // Deactivate all tab buttons
879
+ const buttons = document.getElementsByClassName('tab-button');
880
+ for (let button of buttons) {
881
+ button.classList.remove('active');
882
+ }
883
+
884
+ // Show selected tab
885
+ document.getElementById(sectionId).classList.add('active');
886
+ event.currentTarget.classList.add('active');
887
+ }
888
+
889
+ // Signal table filtering
890
+ function filterSignalTable() {
891
+ const input = document.getElementById('signal-search');
892
+ if (!input) return;
893
+
894
+ const filter = input.value.toLowerCase();
895
+ const table = document.getElementById('signal-table');
896
+ if (!table) return;
897
+
898
+ const rows = table.getElementsByTagName('tbody')[0].getElementsByTagName('tr');
899
+
900
+ for (let row of rows) {
901
+ const signalName = row.cells[0].textContent.toLowerCase();
902
+ if (signalName.includes(filter)) {
903
+ row.style.display = '';
904
+ } else {
905
+ row.style.display = 'none';
906
+ }
907
+ }
908
+ }
909
+
910
+ // Table sorting functionality
911
+ document.addEventListener('DOMContentLoaded', function() {
912
+ const tables = document.querySelectorAll('.feature-table');
913
+
914
+ tables.forEach(table => {
915
+ const headers = table.querySelectorAll('thead th');
916
+ let sortDirection = {};
917
+
918
+ headers.forEach((header, colIndex) => {
919
+ header.addEventListener('click', function() {
920
+ const tbody = table.querySelector('tbody');
921
+ const rows = Array.from(tbody.querySelectorAll('tr'));
922
+
923
+ // Toggle sort direction
924
+ sortDirection[colIndex] = !sortDirection[colIndex];
925
+ const ascending = sortDirection[colIndex];
926
+
927
+ // Sort rows
928
+ rows.sort((a, b) => {
929
+ const aVal = a.cells[colIndex].textContent;
930
+ const bVal = b.cells[colIndex].textContent;
931
+
932
+ // Try numeric comparison first
933
+ const aNum = parseFloat(aVal);
934
+ const bNum = parseFloat(bVal);
935
+
936
+ if (!isNaN(aNum) && !isNaN(bNum)) {
937
+ return ascending ? aNum - bNum : bNum - aNum;
938
+ }
939
+
940
+ // Fall back to string comparison
941
+ return ascending
942
+ ? aVal.localeCompare(bVal)
943
+ : bVal.localeCompare(aVal);
944
+ });
945
+
946
+ // Re-append sorted rows
947
+ rows.forEach(row => tbody.appendChild(row));
948
+
949
+ // Update header indicators
950
+ headers.forEach(h => h.textContent = h.textContent.replace(/ ▲| ▼/g, ''));
951
+ header.textContent += ascending ? ' ▲' : ' ▼';
952
+ });
953
+ });
954
+ });
955
+ });
956
+
957
+ // Plotly responsive resizing
958
+ window.addEventListener('resize', function() {
959
+ const plots = document.querySelectorAll('.js-plotly-plot');
960
+ plots.forEach(plot => {
961
+ Plotly.Plots.resize(plot);
962
+ });
963
+ });
964
+ </script>
965
+ """
966
+
967
+
968
+ # =============================================================================
969
+ # Module Exports
970
+ # =============================================================================
971
+
972
+ __all__ = [
973
+ "MultiSignalDashboard",
974
+ ]