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.
- ml4t/diagnostic/AGENT.md +25 -0
- ml4t/diagnostic/__init__.py +166 -0
- ml4t/diagnostic/backends/__init__.py +10 -0
- ml4t/diagnostic/backends/adapter.py +192 -0
- ml4t/diagnostic/backends/polars_backend.py +899 -0
- ml4t/diagnostic/caching/__init__.py +40 -0
- ml4t/diagnostic/caching/cache.py +331 -0
- ml4t/diagnostic/caching/decorators.py +131 -0
- ml4t/diagnostic/caching/smart_cache.py +339 -0
- ml4t/diagnostic/config/AGENT.md +24 -0
- ml4t/diagnostic/config/README.md +267 -0
- ml4t/diagnostic/config/__init__.py +219 -0
- ml4t/diagnostic/config/barrier_config.py +277 -0
- ml4t/diagnostic/config/base.py +301 -0
- ml4t/diagnostic/config/event_config.py +148 -0
- ml4t/diagnostic/config/feature_config.py +404 -0
- ml4t/diagnostic/config/multi_signal_config.py +55 -0
- ml4t/diagnostic/config/portfolio_config.py +215 -0
- ml4t/diagnostic/config/report_config.py +391 -0
- ml4t/diagnostic/config/sharpe_config.py +202 -0
- ml4t/diagnostic/config/signal_config.py +206 -0
- ml4t/diagnostic/config/trade_analysis_config.py +310 -0
- ml4t/diagnostic/config/validation.py +279 -0
- ml4t/diagnostic/core/__init__.py +29 -0
- ml4t/diagnostic/core/numba_utils.py +315 -0
- ml4t/diagnostic/core/purging.py +372 -0
- ml4t/diagnostic/core/sampling.py +471 -0
- ml4t/diagnostic/errors/__init__.py +205 -0
- ml4t/diagnostic/evaluation/AGENT.md +26 -0
- ml4t/diagnostic/evaluation/__init__.py +437 -0
- ml4t/diagnostic/evaluation/autocorrelation.py +531 -0
- ml4t/diagnostic/evaluation/barrier_analysis.py +1050 -0
- ml4t/diagnostic/evaluation/binary_metrics.py +910 -0
- ml4t/diagnostic/evaluation/dashboard.py +715 -0
- ml4t/diagnostic/evaluation/diagnostic_plots.py +1037 -0
- ml4t/diagnostic/evaluation/distribution/__init__.py +499 -0
- ml4t/diagnostic/evaluation/distribution/moments.py +299 -0
- ml4t/diagnostic/evaluation/distribution/tails.py +777 -0
- ml4t/diagnostic/evaluation/distribution/tests.py +470 -0
- ml4t/diagnostic/evaluation/drift/__init__.py +139 -0
- ml4t/diagnostic/evaluation/drift/analysis.py +432 -0
- ml4t/diagnostic/evaluation/drift/domain_classifier.py +517 -0
- ml4t/diagnostic/evaluation/drift/population_stability_index.py +310 -0
- ml4t/diagnostic/evaluation/drift/wasserstein.py +388 -0
- ml4t/diagnostic/evaluation/event_analysis.py +647 -0
- ml4t/diagnostic/evaluation/excursion.py +390 -0
- ml4t/diagnostic/evaluation/feature_diagnostics.py +873 -0
- ml4t/diagnostic/evaluation/feature_outcome.py +666 -0
- ml4t/diagnostic/evaluation/framework.py +935 -0
- ml4t/diagnostic/evaluation/metric_registry.py +255 -0
- ml4t/diagnostic/evaluation/metrics/AGENT.md +23 -0
- ml4t/diagnostic/evaluation/metrics/__init__.py +133 -0
- ml4t/diagnostic/evaluation/metrics/basic.py +160 -0
- ml4t/diagnostic/evaluation/metrics/conditional_ic.py +469 -0
- ml4t/diagnostic/evaluation/metrics/feature_outcome.py +475 -0
- ml4t/diagnostic/evaluation/metrics/ic_statistics.py +446 -0
- ml4t/diagnostic/evaluation/metrics/importance_analysis.py +338 -0
- ml4t/diagnostic/evaluation/metrics/importance_classical.py +375 -0
- ml4t/diagnostic/evaluation/metrics/importance_mda.py +371 -0
- ml4t/diagnostic/evaluation/metrics/importance_shap.py +715 -0
- ml4t/diagnostic/evaluation/metrics/information_coefficient.py +527 -0
- ml4t/diagnostic/evaluation/metrics/interactions.py +772 -0
- ml4t/diagnostic/evaluation/metrics/monotonicity.py +226 -0
- ml4t/diagnostic/evaluation/metrics/risk_adjusted.py +324 -0
- ml4t/diagnostic/evaluation/multi_signal.py +550 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/__init__.py +83 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/analysis.py +734 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/metrics.py +589 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/results.py +334 -0
- ml4t/diagnostic/evaluation/report_generation.py +824 -0
- ml4t/diagnostic/evaluation/signal_selector.py +452 -0
- ml4t/diagnostic/evaluation/stat_registry.py +139 -0
- ml4t/diagnostic/evaluation/stationarity/__init__.py +97 -0
- ml4t/diagnostic/evaluation/stationarity/analysis.py +518 -0
- ml4t/diagnostic/evaluation/stationarity/augmented_dickey_fuller.py +296 -0
- ml4t/diagnostic/evaluation/stationarity/kpss_test.py +308 -0
- ml4t/diagnostic/evaluation/stationarity/phillips_perron.py +365 -0
- ml4t/diagnostic/evaluation/stats/AGENT.md +43 -0
- ml4t/diagnostic/evaluation/stats/__init__.py +191 -0
- ml4t/diagnostic/evaluation/stats/backtest_overfitting.py +219 -0
- ml4t/diagnostic/evaluation/stats/bootstrap.py +228 -0
- ml4t/diagnostic/evaluation/stats/deflated_sharpe_ratio.py +591 -0
- ml4t/diagnostic/evaluation/stats/false_discovery_rate.py +295 -0
- ml4t/diagnostic/evaluation/stats/hac_standard_errors.py +108 -0
- ml4t/diagnostic/evaluation/stats/minimum_track_record.py +408 -0
- ml4t/diagnostic/evaluation/stats/moments.py +164 -0
- ml4t/diagnostic/evaluation/stats/rademacher_adjustment.py +436 -0
- ml4t/diagnostic/evaluation/stats/reality_check.py +155 -0
- ml4t/diagnostic/evaluation/stats/sharpe_inference.py +219 -0
- ml4t/diagnostic/evaluation/themes.py +330 -0
- ml4t/diagnostic/evaluation/threshold_analysis.py +957 -0
- ml4t/diagnostic/evaluation/trade_analysis.py +1136 -0
- ml4t/diagnostic/evaluation/trade_dashboard/__init__.py +32 -0
- ml4t/diagnostic/evaluation/trade_dashboard/app.py +315 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/__init__.py +18 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/csv.py +82 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/html.py +276 -0
- ml4t/diagnostic/evaluation/trade_dashboard/io.py +166 -0
- ml4t/diagnostic/evaluation/trade_dashboard/normalize.py +304 -0
- ml4t/diagnostic/evaluation/trade_dashboard/stats.py +386 -0
- ml4t/diagnostic/evaluation/trade_dashboard/style.py +79 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/__init__.py +21 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/patterns.py +354 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/shap_analysis.py +280 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/stat_validation.py +186 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/worst_trades.py +236 -0
- ml4t/diagnostic/evaluation/trade_dashboard/types.py +129 -0
- ml4t/diagnostic/evaluation/trade_shap/__init__.py +102 -0
- ml4t/diagnostic/evaluation/trade_shap/alignment.py +188 -0
- ml4t/diagnostic/evaluation/trade_shap/characterize.py +413 -0
- ml4t/diagnostic/evaluation/trade_shap/cluster.py +302 -0
- ml4t/diagnostic/evaluation/trade_shap/explain.py +208 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/__init__.py +23 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/generator.py +290 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/matcher.py +251 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/templates.yaml +467 -0
- ml4t/diagnostic/evaluation/trade_shap/models.py +386 -0
- ml4t/diagnostic/evaluation/trade_shap/normalize.py +116 -0
- ml4t/diagnostic/evaluation/trade_shap/pipeline.py +263 -0
- ml4t/diagnostic/evaluation/trade_shap_dashboard.py +283 -0
- ml4t/diagnostic/evaluation/trade_shap_diagnostics.py +588 -0
- ml4t/diagnostic/evaluation/validated_cv.py +535 -0
- ml4t/diagnostic/evaluation/visualization.py +1050 -0
- ml4t/diagnostic/evaluation/volatility/__init__.py +45 -0
- ml4t/diagnostic/evaluation/volatility/analysis.py +351 -0
- ml4t/diagnostic/evaluation/volatility/arch.py +258 -0
- ml4t/diagnostic/evaluation/volatility/garch.py +460 -0
- ml4t/diagnostic/integration/__init__.py +48 -0
- ml4t/diagnostic/integration/backtest_contract.py +671 -0
- ml4t/diagnostic/integration/data_contract.py +316 -0
- ml4t/diagnostic/integration/engineer_contract.py +226 -0
- ml4t/diagnostic/logging/__init__.py +77 -0
- ml4t/diagnostic/logging/logger.py +245 -0
- ml4t/diagnostic/logging/performance.py +234 -0
- ml4t/diagnostic/logging/progress.py +234 -0
- ml4t/diagnostic/logging/wandb.py +412 -0
- ml4t/diagnostic/metrics/__init__.py +9 -0
- ml4t/diagnostic/metrics/percentiles.py +128 -0
- ml4t/diagnostic/py.typed +1 -0
- ml4t/diagnostic/reporting/__init__.py +43 -0
- ml4t/diagnostic/reporting/base.py +130 -0
- ml4t/diagnostic/reporting/html_renderer.py +275 -0
- ml4t/diagnostic/reporting/json_renderer.py +51 -0
- ml4t/diagnostic/reporting/markdown_renderer.py +117 -0
- ml4t/diagnostic/results/AGENT.md +24 -0
- ml4t/diagnostic/results/__init__.py +105 -0
- ml4t/diagnostic/results/barrier_results/__init__.py +36 -0
- ml4t/diagnostic/results/barrier_results/hit_rate.py +304 -0
- ml4t/diagnostic/results/barrier_results/precision_recall.py +266 -0
- ml4t/diagnostic/results/barrier_results/profit_factor.py +297 -0
- ml4t/diagnostic/results/barrier_results/tearsheet.py +397 -0
- ml4t/diagnostic/results/barrier_results/time_to_target.py +305 -0
- ml4t/diagnostic/results/barrier_results/validation.py +38 -0
- ml4t/diagnostic/results/base.py +177 -0
- ml4t/diagnostic/results/event_results.py +349 -0
- ml4t/diagnostic/results/feature_results.py +787 -0
- ml4t/diagnostic/results/multi_signal_results.py +431 -0
- ml4t/diagnostic/results/portfolio_results.py +281 -0
- ml4t/diagnostic/results/sharpe_results.py +448 -0
- ml4t/diagnostic/results/signal_results/__init__.py +74 -0
- ml4t/diagnostic/results/signal_results/ic.py +581 -0
- ml4t/diagnostic/results/signal_results/irtc.py +110 -0
- ml4t/diagnostic/results/signal_results/quantile.py +392 -0
- ml4t/diagnostic/results/signal_results/tearsheet.py +456 -0
- ml4t/diagnostic/results/signal_results/turnover.py +213 -0
- ml4t/diagnostic/results/signal_results/validation.py +147 -0
- ml4t/diagnostic/signal/AGENT.md +17 -0
- ml4t/diagnostic/signal/__init__.py +69 -0
- ml4t/diagnostic/signal/_report.py +152 -0
- ml4t/diagnostic/signal/_utils.py +261 -0
- ml4t/diagnostic/signal/core.py +275 -0
- ml4t/diagnostic/signal/quantile.py +148 -0
- ml4t/diagnostic/signal/result.py +214 -0
- ml4t/diagnostic/signal/signal_ic.py +129 -0
- ml4t/diagnostic/signal/turnover.py +182 -0
- ml4t/diagnostic/splitters/AGENT.md +19 -0
- ml4t/diagnostic/splitters/__init__.py +36 -0
- ml4t/diagnostic/splitters/base.py +501 -0
- ml4t/diagnostic/splitters/calendar.py +421 -0
- ml4t/diagnostic/splitters/calendar_config.py +91 -0
- ml4t/diagnostic/splitters/combinatorial.py +1064 -0
- ml4t/diagnostic/splitters/config.py +322 -0
- ml4t/diagnostic/splitters/cpcv/__init__.py +57 -0
- ml4t/diagnostic/splitters/cpcv/combinations.py +119 -0
- ml4t/diagnostic/splitters/cpcv/partitioning.py +263 -0
- ml4t/diagnostic/splitters/cpcv/purge_engine.py +379 -0
- ml4t/diagnostic/splitters/cpcv/windows.py +190 -0
- ml4t/diagnostic/splitters/group_isolation.py +329 -0
- ml4t/diagnostic/splitters/persistence.py +316 -0
- ml4t/diagnostic/splitters/utils.py +207 -0
- ml4t/diagnostic/splitters/walk_forward.py +757 -0
- ml4t/diagnostic/utils/__init__.py +42 -0
- ml4t/diagnostic/utils/config.py +542 -0
- ml4t/diagnostic/utils/dependencies.py +318 -0
- ml4t/diagnostic/utils/sessions.py +127 -0
- ml4t/diagnostic/validation/__init__.py +54 -0
- ml4t/diagnostic/validation/dataframe.py +274 -0
- ml4t/diagnostic/validation/returns.py +280 -0
- ml4t/diagnostic/validation/timeseries.py +299 -0
- ml4t/diagnostic/visualization/AGENT.md +19 -0
- ml4t/diagnostic/visualization/__init__.py +223 -0
- ml4t/diagnostic/visualization/backtest/__init__.py +98 -0
- ml4t/diagnostic/visualization/backtest/cost_attribution.py +762 -0
- ml4t/diagnostic/visualization/backtest/executive_summary.py +895 -0
- ml4t/diagnostic/visualization/backtest/interactive_controls.py +673 -0
- ml4t/diagnostic/visualization/backtest/statistical_validity.py +874 -0
- ml4t/diagnostic/visualization/backtest/tearsheet.py +565 -0
- ml4t/diagnostic/visualization/backtest/template_system.py +373 -0
- ml4t/diagnostic/visualization/backtest/trade_plots.py +1172 -0
- ml4t/diagnostic/visualization/barrier_plots.py +782 -0
- ml4t/diagnostic/visualization/core.py +1060 -0
- ml4t/diagnostic/visualization/dashboards/__init__.py +36 -0
- ml4t/diagnostic/visualization/dashboards/base.py +582 -0
- ml4t/diagnostic/visualization/dashboards/importance.py +801 -0
- ml4t/diagnostic/visualization/dashboards/interaction.py +263 -0
- ml4t/diagnostic/visualization/dashboards.py +43 -0
- ml4t/diagnostic/visualization/data_extraction/__init__.py +48 -0
- ml4t/diagnostic/visualization/data_extraction/importance.py +649 -0
- ml4t/diagnostic/visualization/data_extraction/interaction.py +504 -0
- ml4t/diagnostic/visualization/data_extraction/types.py +113 -0
- ml4t/diagnostic/visualization/data_extraction/validation.py +66 -0
- ml4t/diagnostic/visualization/feature_plots.py +888 -0
- ml4t/diagnostic/visualization/interaction_plots.py +618 -0
- ml4t/diagnostic/visualization/portfolio/__init__.py +41 -0
- ml4t/diagnostic/visualization/portfolio/dashboard.py +514 -0
- ml4t/diagnostic/visualization/portfolio/drawdown_plots.py +341 -0
- ml4t/diagnostic/visualization/portfolio/returns_plots.py +487 -0
- ml4t/diagnostic/visualization/portfolio/risk_plots.py +301 -0
- ml4t/diagnostic/visualization/report_generation.py +1343 -0
- ml4t/diagnostic/visualization/signal/__init__.py +103 -0
- ml4t/diagnostic/visualization/signal/dashboard.py +911 -0
- ml4t/diagnostic/visualization/signal/event_plots.py +514 -0
- ml4t/diagnostic/visualization/signal/ic_plots.py +635 -0
- ml4t/diagnostic/visualization/signal/multi_signal_dashboard.py +974 -0
- ml4t/diagnostic/visualization/signal/multi_signal_plots.py +603 -0
- ml4t/diagnostic/visualization/signal/quantile_plots.py +625 -0
- ml4t/diagnostic/visualization/signal/turnover_plots.py +400 -0
- ml4t/diagnostic/visualization/trade_shap/__init__.py +90 -0
- ml4t_diagnostic-0.1.0a1.dist-info/METADATA +1044 -0
- ml4t_diagnostic-0.1.0a1.dist-info/RECORD +242 -0
- ml4t_diagnostic-0.1.0a1.dist-info/WHEEL +4 -0
- 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
|
+
]
|