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