ml4t-diagnostic 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. ml4t/diagnostic/AGENT.md +25 -0
  2. ml4t/diagnostic/__init__.py +166 -0
  3. ml4t/diagnostic/backends/__init__.py +10 -0
  4. ml4t/diagnostic/backends/adapter.py +192 -0
  5. ml4t/diagnostic/backends/polars_backend.py +899 -0
  6. ml4t/diagnostic/caching/__init__.py +40 -0
  7. ml4t/diagnostic/caching/cache.py +331 -0
  8. ml4t/diagnostic/caching/decorators.py +131 -0
  9. ml4t/diagnostic/caching/smart_cache.py +339 -0
  10. ml4t/diagnostic/config/AGENT.md +24 -0
  11. ml4t/diagnostic/config/README.md +267 -0
  12. ml4t/diagnostic/config/__init__.py +219 -0
  13. ml4t/diagnostic/config/barrier_config.py +277 -0
  14. ml4t/diagnostic/config/base.py +301 -0
  15. ml4t/diagnostic/config/event_config.py +148 -0
  16. ml4t/diagnostic/config/feature_config.py +404 -0
  17. ml4t/diagnostic/config/multi_signal_config.py +55 -0
  18. ml4t/diagnostic/config/portfolio_config.py +215 -0
  19. ml4t/diagnostic/config/report_config.py +391 -0
  20. ml4t/diagnostic/config/sharpe_config.py +202 -0
  21. ml4t/diagnostic/config/signal_config.py +206 -0
  22. ml4t/diagnostic/config/trade_analysis_config.py +310 -0
  23. ml4t/diagnostic/config/validation.py +279 -0
  24. ml4t/diagnostic/core/__init__.py +29 -0
  25. ml4t/diagnostic/core/numba_utils.py +315 -0
  26. ml4t/diagnostic/core/purging.py +372 -0
  27. ml4t/diagnostic/core/sampling.py +471 -0
  28. ml4t/diagnostic/errors/__init__.py +205 -0
  29. ml4t/diagnostic/evaluation/AGENT.md +26 -0
  30. ml4t/diagnostic/evaluation/__init__.py +437 -0
  31. ml4t/diagnostic/evaluation/autocorrelation.py +531 -0
  32. ml4t/diagnostic/evaluation/barrier_analysis.py +1050 -0
  33. ml4t/diagnostic/evaluation/binary_metrics.py +910 -0
  34. ml4t/diagnostic/evaluation/dashboard.py +715 -0
  35. ml4t/diagnostic/evaluation/diagnostic_plots.py +1037 -0
  36. ml4t/diagnostic/evaluation/distribution/__init__.py +499 -0
  37. ml4t/diagnostic/evaluation/distribution/moments.py +299 -0
  38. ml4t/diagnostic/evaluation/distribution/tails.py +777 -0
  39. ml4t/diagnostic/evaluation/distribution/tests.py +470 -0
  40. ml4t/diagnostic/evaluation/drift/__init__.py +139 -0
  41. ml4t/diagnostic/evaluation/drift/analysis.py +432 -0
  42. ml4t/diagnostic/evaluation/drift/domain_classifier.py +517 -0
  43. ml4t/diagnostic/evaluation/drift/population_stability_index.py +310 -0
  44. ml4t/diagnostic/evaluation/drift/wasserstein.py +388 -0
  45. ml4t/diagnostic/evaluation/event_analysis.py +647 -0
  46. ml4t/diagnostic/evaluation/excursion.py +390 -0
  47. ml4t/diagnostic/evaluation/feature_diagnostics.py +873 -0
  48. ml4t/diagnostic/evaluation/feature_outcome.py +666 -0
  49. ml4t/diagnostic/evaluation/framework.py +935 -0
  50. ml4t/diagnostic/evaluation/metric_registry.py +255 -0
  51. ml4t/diagnostic/evaluation/metrics/AGENT.md +23 -0
  52. ml4t/diagnostic/evaluation/metrics/__init__.py +133 -0
  53. ml4t/diagnostic/evaluation/metrics/basic.py +160 -0
  54. ml4t/diagnostic/evaluation/metrics/conditional_ic.py +469 -0
  55. ml4t/diagnostic/evaluation/metrics/feature_outcome.py +475 -0
  56. ml4t/diagnostic/evaluation/metrics/ic_statistics.py +446 -0
  57. ml4t/diagnostic/evaluation/metrics/importance_analysis.py +338 -0
  58. ml4t/diagnostic/evaluation/metrics/importance_classical.py +375 -0
  59. ml4t/diagnostic/evaluation/metrics/importance_mda.py +371 -0
  60. ml4t/diagnostic/evaluation/metrics/importance_shap.py +715 -0
  61. ml4t/diagnostic/evaluation/metrics/information_coefficient.py +527 -0
  62. ml4t/diagnostic/evaluation/metrics/interactions.py +772 -0
  63. ml4t/diagnostic/evaluation/metrics/monotonicity.py +226 -0
  64. ml4t/diagnostic/evaluation/metrics/risk_adjusted.py +324 -0
  65. ml4t/diagnostic/evaluation/multi_signal.py +550 -0
  66. ml4t/diagnostic/evaluation/portfolio_analysis/__init__.py +83 -0
  67. ml4t/diagnostic/evaluation/portfolio_analysis/analysis.py +734 -0
  68. ml4t/diagnostic/evaluation/portfolio_analysis/metrics.py +589 -0
  69. ml4t/diagnostic/evaluation/portfolio_analysis/results.py +334 -0
  70. ml4t/diagnostic/evaluation/report_generation.py +824 -0
  71. ml4t/diagnostic/evaluation/signal_selector.py +452 -0
  72. ml4t/diagnostic/evaluation/stat_registry.py +139 -0
  73. ml4t/diagnostic/evaluation/stationarity/__init__.py +97 -0
  74. ml4t/diagnostic/evaluation/stationarity/analysis.py +518 -0
  75. ml4t/diagnostic/evaluation/stationarity/augmented_dickey_fuller.py +296 -0
  76. ml4t/diagnostic/evaluation/stationarity/kpss_test.py +308 -0
  77. ml4t/diagnostic/evaluation/stationarity/phillips_perron.py +365 -0
  78. ml4t/diagnostic/evaluation/stats/AGENT.md +43 -0
  79. ml4t/diagnostic/evaluation/stats/__init__.py +191 -0
  80. ml4t/diagnostic/evaluation/stats/backtest_overfitting.py +219 -0
  81. ml4t/diagnostic/evaluation/stats/bootstrap.py +228 -0
  82. ml4t/diagnostic/evaluation/stats/deflated_sharpe_ratio.py +591 -0
  83. ml4t/diagnostic/evaluation/stats/false_discovery_rate.py +295 -0
  84. ml4t/diagnostic/evaluation/stats/hac_standard_errors.py +108 -0
  85. ml4t/diagnostic/evaluation/stats/minimum_track_record.py +408 -0
  86. ml4t/diagnostic/evaluation/stats/moments.py +164 -0
  87. ml4t/diagnostic/evaluation/stats/rademacher_adjustment.py +436 -0
  88. ml4t/diagnostic/evaluation/stats/reality_check.py +155 -0
  89. ml4t/diagnostic/evaluation/stats/sharpe_inference.py +219 -0
  90. ml4t/diagnostic/evaluation/themes.py +330 -0
  91. ml4t/diagnostic/evaluation/threshold_analysis.py +957 -0
  92. ml4t/diagnostic/evaluation/trade_analysis.py +1136 -0
  93. ml4t/diagnostic/evaluation/trade_dashboard/__init__.py +32 -0
  94. ml4t/diagnostic/evaluation/trade_dashboard/app.py +315 -0
  95. ml4t/diagnostic/evaluation/trade_dashboard/export/__init__.py +18 -0
  96. ml4t/diagnostic/evaluation/trade_dashboard/export/csv.py +82 -0
  97. ml4t/diagnostic/evaluation/trade_dashboard/export/html.py +276 -0
  98. ml4t/diagnostic/evaluation/trade_dashboard/io.py +166 -0
  99. ml4t/diagnostic/evaluation/trade_dashboard/normalize.py +304 -0
  100. ml4t/diagnostic/evaluation/trade_dashboard/stats.py +386 -0
  101. ml4t/diagnostic/evaluation/trade_dashboard/style.py +79 -0
  102. ml4t/diagnostic/evaluation/trade_dashboard/tabs/__init__.py +21 -0
  103. ml4t/diagnostic/evaluation/trade_dashboard/tabs/patterns.py +354 -0
  104. ml4t/diagnostic/evaluation/trade_dashboard/tabs/shap_analysis.py +280 -0
  105. ml4t/diagnostic/evaluation/trade_dashboard/tabs/stat_validation.py +186 -0
  106. ml4t/diagnostic/evaluation/trade_dashboard/tabs/worst_trades.py +236 -0
  107. ml4t/diagnostic/evaluation/trade_dashboard/types.py +129 -0
  108. ml4t/diagnostic/evaluation/trade_shap/__init__.py +102 -0
  109. ml4t/diagnostic/evaluation/trade_shap/alignment.py +188 -0
  110. ml4t/diagnostic/evaluation/trade_shap/characterize.py +413 -0
  111. ml4t/diagnostic/evaluation/trade_shap/cluster.py +302 -0
  112. ml4t/diagnostic/evaluation/trade_shap/explain.py +208 -0
  113. ml4t/diagnostic/evaluation/trade_shap/hypotheses/__init__.py +23 -0
  114. ml4t/diagnostic/evaluation/trade_shap/hypotheses/generator.py +290 -0
  115. ml4t/diagnostic/evaluation/trade_shap/hypotheses/matcher.py +251 -0
  116. ml4t/diagnostic/evaluation/trade_shap/hypotheses/templates.yaml +467 -0
  117. ml4t/diagnostic/evaluation/trade_shap/models.py +386 -0
  118. ml4t/diagnostic/evaluation/trade_shap/normalize.py +116 -0
  119. ml4t/diagnostic/evaluation/trade_shap/pipeline.py +263 -0
  120. ml4t/diagnostic/evaluation/trade_shap_dashboard.py +283 -0
  121. ml4t/diagnostic/evaluation/trade_shap_diagnostics.py +588 -0
  122. ml4t/diagnostic/evaluation/validated_cv.py +535 -0
  123. ml4t/diagnostic/evaluation/visualization.py +1050 -0
  124. ml4t/diagnostic/evaluation/volatility/__init__.py +45 -0
  125. ml4t/diagnostic/evaluation/volatility/analysis.py +351 -0
  126. ml4t/diagnostic/evaluation/volatility/arch.py +258 -0
  127. ml4t/diagnostic/evaluation/volatility/garch.py +460 -0
  128. ml4t/diagnostic/integration/__init__.py +48 -0
  129. ml4t/diagnostic/integration/backtest_contract.py +671 -0
  130. ml4t/diagnostic/integration/data_contract.py +316 -0
  131. ml4t/diagnostic/integration/engineer_contract.py +226 -0
  132. ml4t/diagnostic/logging/__init__.py +77 -0
  133. ml4t/diagnostic/logging/logger.py +245 -0
  134. ml4t/diagnostic/logging/performance.py +234 -0
  135. ml4t/diagnostic/logging/progress.py +234 -0
  136. ml4t/diagnostic/logging/wandb.py +412 -0
  137. ml4t/diagnostic/metrics/__init__.py +9 -0
  138. ml4t/diagnostic/metrics/percentiles.py +128 -0
  139. ml4t/diagnostic/py.typed +1 -0
  140. ml4t/diagnostic/reporting/__init__.py +43 -0
  141. ml4t/diagnostic/reporting/base.py +130 -0
  142. ml4t/diagnostic/reporting/html_renderer.py +275 -0
  143. ml4t/diagnostic/reporting/json_renderer.py +51 -0
  144. ml4t/diagnostic/reporting/markdown_renderer.py +117 -0
  145. ml4t/diagnostic/results/AGENT.md +24 -0
  146. ml4t/diagnostic/results/__init__.py +105 -0
  147. ml4t/diagnostic/results/barrier_results/__init__.py +36 -0
  148. ml4t/diagnostic/results/barrier_results/hit_rate.py +304 -0
  149. ml4t/diagnostic/results/barrier_results/precision_recall.py +266 -0
  150. ml4t/diagnostic/results/barrier_results/profit_factor.py +297 -0
  151. ml4t/diagnostic/results/barrier_results/tearsheet.py +397 -0
  152. ml4t/diagnostic/results/barrier_results/time_to_target.py +305 -0
  153. ml4t/diagnostic/results/barrier_results/validation.py +38 -0
  154. ml4t/diagnostic/results/base.py +177 -0
  155. ml4t/diagnostic/results/event_results.py +349 -0
  156. ml4t/diagnostic/results/feature_results.py +787 -0
  157. ml4t/diagnostic/results/multi_signal_results.py +431 -0
  158. ml4t/diagnostic/results/portfolio_results.py +281 -0
  159. ml4t/diagnostic/results/sharpe_results.py +448 -0
  160. ml4t/diagnostic/results/signal_results/__init__.py +74 -0
  161. ml4t/diagnostic/results/signal_results/ic.py +581 -0
  162. ml4t/diagnostic/results/signal_results/irtc.py +110 -0
  163. ml4t/diagnostic/results/signal_results/quantile.py +392 -0
  164. ml4t/diagnostic/results/signal_results/tearsheet.py +456 -0
  165. ml4t/diagnostic/results/signal_results/turnover.py +213 -0
  166. ml4t/diagnostic/results/signal_results/validation.py +147 -0
  167. ml4t/diagnostic/signal/AGENT.md +17 -0
  168. ml4t/diagnostic/signal/__init__.py +69 -0
  169. ml4t/diagnostic/signal/_report.py +152 -0
  170. ml4t/diagnostic/signal/_utils.py +261 -0
  171. ml4t/diagnostic/signal/core.py +275 -0
  172. ml4t/diagnostic/signal/quantile.py +148 -0
  173. ml4t/diagnostic/signal/result.py +214 -0
  174. ml4t/diagnostic/signal/signal_ic.py +129 -0
  175. ml4t/diagnostic/signal/turnover.py +182 -0
  176. ml4t/diagnostic/splitters/AGENT.md +19 -0
  177. ml4t/diagnostic/splitters/__init__.py +36 -0
  178. ml4t/diagnostic/splitters/base.py +501 -0
  179. ml4t/diagnostic/splitters/calendar.py +421 -0
  180. ml4t/diagnostic/splitters/calendar_config.py +91 -0
  181. ml4t/diagnostic/splitters/combinatorial.py +1064 -0
  182. ml4t/diagnostic/splitters/config.py +322 -0
  183. ml4t/diagnostic/splitters/cpcv/__init__.py +57 -0
  184. ml4t/diagnostic/splitters/cpcv/combinations.py +119 -0
  185. ml4t/diagnostic/splitters/cpcv/partitioning.py +263 -0
  186. ml4t/diagnostic/splitters/cpcv/purge_engine.py +379 -0
  187. ml4t/diagnostic/splitters/cpcv/windows.py +190 -0
  188. ml4t/diagnostic/splitters/group_isolation.py +329 -0
  189. ml4t/diagnostic/splitters/persistence.py +316 -0
  190. ml4t/diagnostic/splitters/utils.py +207 -0
  191. ml4t/diagnostic/splitters/walk_forward.py +757 -0
  192. ml4t/diagnostic/utils/__init__.py +42 -0
  193. ml4t/diagnostic/utils/config.py +542 -0
  194. ml4t/diagnostic/utils/dependencies.py +318 -0
  195. ml4t/diagnostic/utils/sessions.py +127 -0
  196. ml4t/diagnostic/validation/__init__.py +54 -0
  197. ml4t/diagnostic/validation/dataframe.py +274 -0
  198. ml4t/diagnostic/validation/returns.py +280 -0
  199. ml4t/diagnostic/validation/timeseries.py +299 -0
  200. ml4t/diagnostic/visualization/AGENT.md +19 -0
  201. ml4t/diagnostic/visualization/__init__.py +223 -0
  202. ml4t/diagnostic/visualization/backtest/__init__.py +98 -0
  203. ml4t/diagnostic/visualization/backtest/cost_attribution.py +762 -0
  204. ml4t/diagnostic/visualization/backtest/executive_summary.py +895 -0
  205. ml4t/diagnostic/visualization/backtest/interactive_controls.py +673 -0
  206. ml4t/diagnostic/visualization/backtest/statistical_validity.py +874 -0
  207. ml4t/diagnostic/visualization/backtest/tearsheet.py +565 -0
  208. ml4t/diagnostic/visualization/backtest/template_system.py +373 -0
  209. ml4t/diagnostic/visualization/backtest/trade_plots.py +1172 -0
  210. ml4t/diagnostic/visualization/barrier_plots.py +782 -0
  211. ml4t/diagnostic/visualization/core.py +1060 -0
  212. ml4t/diagnostic/visualization/dashboards/__init__.py +36 -0
  213. ml4t/diagnostic/visualization/dashboards/base.py +582 -0
  214. ml4t/diagnostic/visualization/dashboards/importance.py +801 -0
  215. ml4t/diagnostic/visualization/dashboards/interaction.py +263 -0
  216. ml4t/diagnostic/visualization/dashboards.py +43 -0
  217. ml4t/diagnostic/visualization/data_extraction/__init__.py +48 -0
  218. ml4t/diagnostic/visualization/data_extraction/importance.py +649 -0
  219. ml4t/diagnostic/visualization/data_extraction/interaction.py +504 -0
  220. ml4t/diagnostic/visualization/data_extraction/types.py +113 -0
  221. ml4t/diagnostic/visualization/data_extraction/validation.py +66 -0
  222. ml4t/diagnostic/visualization/feature_plots.py +888 -0
  223. ml4t/diagnostic/visualization/interaction_plots.py +618 -0
  224. ml4t/diagnostic/visualization/portfolio/__init__.py +41 -0
  225. ml4t/diagnostic/visualization/portfolio/dashboard.py +514 -0
  226. ml4t/diagnostic/visualization/portfolio/drawdown_plots.py +341 -0
  227. ml4t/diagnostic/visualization/portfolio/returns_plots.py +487 -0
  228. ml4t/diagnostic/visualization/portfolio/risk_plots.py +301 -0
  229. ml4t/diagnostic/visualization/report_generation.py +1343 -0
  230. ml4t/diagnostic/visualization/signal/__init__.py +103 -0
  231. ml4t/diagnostic/visualization/signal/dashboard.py +911 -0
  232. ml4t/diagnostic/visualization/signal/event_plots.py +514 -0
  233. ml4t/diagnostic/visualization/signal/ic_plots.py +635 -0
  234. ml4t/diagnostic/visualization/signal/multi_signal_dashboard.py +974 -0
  235. ml4t/diagnostic/visualization/signal/multi_signal_plots.py +603 -0
  236. ml4t/diagnostic/visualization/signal/quantile_plots.py +625 -0
  237. ml4t/diagnostic/visualization/signal/turnover_plots.py +400 -0
  238. ml4t/diagnostic/visualization/trade_shap/__init__.py +90 -0
  239. ml4t_diagnostic-0.1.0a1.dist-info/METADATA +1044 -0
  240. ml4t_diagnostic-0.1.0a1.dist-info/RECORD +242 -0
  241. ml4t_diagnostic-0.1.0a1.dist-info/WHEEL +4 -0
  242. ml4t_diagnostic-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,514 @@
1
+ """Event Study Visualization Module.
2
+
3
+ Provides interactive Plotly visualizations for event study analysis:
4
+ - CAAR time series with confidence bands
5
+ - Event drift heatmap
6
+ - AR distribution plots
7
+
8
+ All plots follow ML4T Diagnostic visualization standards.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ import plotly.graph_objects as go
16
+ from plotly.subplots import make_subplots
17
+
18
+ from ml4t.diagnostic.visualization.core import get_theme_config as get_theme
19
+
20
+ if TYPE_CHECKING:
21
+ from ml4t.diagnostic.results.event_results import (
22
+ AbnormalReturnResult,
23
+ EventStudyResult,
24
+ )
25
+
26
+
27
+ def plot_caar(
28
+ result: EventStudyResult,
29
+ show_confidence: bool = True,
30
+ show_aar_bars: bool = False,
31
+ theme: str | None = None,
32
+ width: int | None = None,
33
+ height: int | None = None,
34
+ ) -> go.Figure:
35
+ """Plot Cumulative Average Abnormal Returns (CAAR) time series.
36
+
37
+ Creates a line plot of CAAR over the event window with optional
38
+ confidence interval bands and vertical line at t=0.
39
+
40
+ Parameters
41
+ ----------
42
+ result : EventStudyResult
43
+ Event study results containing CAAR data.
44
+ show_confidence : bool, default True
45
+ Show confidence interval as shaded band.
46
+ show_aar_bars : bool, default False
47
+ Show AAR as bars in secondary y-axis.
48
+ theme : str | None
49
+ Visualization theme (None uses default).
50
+ width : int | None
51
+ Figure width in pixels.
52
+ height : int | None
53
+ Figure height in pixels.
54
+
55
+ Returns
56
+ -------
57
+ go.Figure
58
+ Plotly figure with CAAR visualization.
59
+
60
+ Examples
61
+ --------
62
+ >>> result = analysis.run()
63
+ >>> fig = plot_caar(result, show_confidence=True)
64
+ >>> fig.show()
65
+ """
66
+ theme_config = get_theme(theme)
67
+ colors = theme_config.get("colors", {})
68
+
69
+ # Create figure
70
+ if show_aar_bars:
71
+ fig = make_subplots(
72
+ rows=2,
73
+ cols=1,
74
+ shared_xaxes=True,
75
+ vertical_spacing=0.1,
76
+ row_heights=[0.7, 0.3],
77
+ subplot_titles=("Cumulative Average Abnormal Return (CAAR)", "Daily AAR"),
78
+ )
79
+ else:
80
+ fig = go.Figure()
81
+
82
+ # Get data
83
+ x = result.caar_dates
84
+ y = result.caar
85
+
86
+ # Add confidence band
87
+ if show_confidence:
88
+ fig.add_trace(
89
+ go.Scatter(
90
+ x=x + x[::-1],
91
+ y=result.caar_ci_upper + result.caar_ci_lower[::-1],
92
+ fill="toself",
93
+ fillcolor=colors.get("ci_fill", "rgba(31, 119, 180, 0.2)"),
94
+ line={"color": "rgba(0,0,0,0)"},
95
+ name=f"{int(result.confidence_level * 100)}% CI",
96
+ showlegend=True,
97
+ hoverinfo="skip",
98
+ ),
99
+ row=1 if show_aar_bars else None,
100
+ col=1 if show_aar_bars else None,
101
+ )
102
+
103
+ # Add CAAR line
104
+ fig.add_trace(
105
+ go.Scatter(
106
+ x=x,
107
+ y=y,
108
+ mode="lines+markers",
109
+ name="CAAR",
110
+ line={"color": colors.get("primary", "#1f77b4"), "width": 2},
111
+ marker={"size": 6},
112
+ hovertemplate="Day %{x}<br>CAAR: %{y:.4f}<extra></extra>",
113
+ ),
114
+ row=1 if show_aar_bars else None,
115
+ col=1 if show_aar_bars else None,
116
+ )
117
+
118
+ # Add vertical line at t=0
119
+ fig.add_vline(
120
+ x=0,
121
+ line_dash="dash",
122
+ line_color=colors.get("event_line", "red"),
123
+ annotation_text="Event",
124
+ annotation_position="top",
125
+ row=1 if show_aar_bars else None,
126
+ col=1 if show_aar_bars else None,
127
+ )
128
+
129
+ # Add horizontal line at y=0
130
+ fig.add_hline(
131
+ y=0,
132
+ line_dash="dot",
133
+ line_color="gray",
134
+ row=1 if show_aar_bars else None,
135
+ col=1 if show_aar_bars else None,
136
+ )
137
+
138
+ # Add AAR bars if requested
139
+ if show_aar_bars:
140
+ aar_x = list(result.aar_by_day.keys())
141
+ aar_y = list(result.aar_by_day.values())
142
+
143
+ bar_colors = [
144
+ colors.get("positive", "#2ca02c") if v >= 0 else colors.get("negative", "#d62728")
145
+ for v in aar_y
146
+ ]
147
+
148
+ fig.add_trace(
149
+ go.Bar(
150
+ x=aar_x,
151
+ y=aar_y,
152
+ marker_color=bar_colors,
153
+ name="AAR",
154
+ hovertemplate="Day %{x}<br>AAR: %{y:.4f}<extra></extra>",
155
+ ),
156
+ row=2,
157
+ col=1,
158
+ )
159
+
160
+ fig.add_vline(x=0, line_dash="dash", line_color="red", row=2, col=1)
161
+ fig.add_hline(y=0, line_dash="dot", line_color="gray", row=2, col=1)
162
+
163
+ # Update layout
164
+ title = f"Event Study: CAAR (n={result.n_events} events)"
165
+ if result.is_significant:
166
+ title += f" - Significant at {result.confidence_level:.0%}"
167
+
168
+ fig.update_layout(
169
+ title=title,
170
+ xaxis_title="Days Relative to Event",
171
+ yaxis_title="CAAR",
172
+ width=width or 800,
173
+ height=height or (600 if show_aar_bars else 450),
174
+ template=theme_config.get("template", "plotly_white"),
175
+ hovermode="x unified",
176
+ legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "right", "x": 1},
177
+ )
178
+
179
+ # Add statistical info annotation
180
+ annotation_text = (
181
+ f"Test: {result.test_name}<br>"
182
+ f"Stat: {result.test_statistic:.3f}<br>"
183
+ f"p-value: {result.p_value:.4f}"
184
+ )
185
+ fig.add_annotation(
186
+ xref="paper",
187
+ yref="paper",
188
+ x=0.02,
189
+ y=0.98,
190
+ text=annotation_text,
191
+ showarrow=False,
192
+ font={"size": 10},
193
+ align="left",
194
+ bgcolor="rgba(255,255,255,0.8)",
195
+ bordercolor="gray",
196
+ borderwidth=1,
197
+ )
198
+
199
+ return fig
200
+
201
+
202
+ def plot_event_heatmap(
203
+ ar_results: list[AbnormalReturnResult],
204
+ theme: str | None = None,
205
+ width: int | None = None,
206
+ height: int | None = None,
207
+ ) -> go.Figure:
208
+ """Plot event drift heatmap showing AR for each event by day.
209
+
210
+ Creates a heatmap with events on Y-axis and relative days on X-axis,
211
+ colored by abnormal return magnitude.
212
+
213
+ Parameters
214
+ ----------
215
+ ar_results : list[AbnormalReturnResult]
216
+ Individual event abnormal return results.
217
+ theme : str | None
218
+ Visualization theme.
219
+ width : int | None
220
+ Figure width.
221
+ height : int | None
222
+ Figure height.
223
+
224
+ Returns
225
+ -------
226
+ go.Figure
227
+ Plotly figure with heatmap.
228
+
229
+ Examples
230
+ --------
231
+ >>> ar_results = analysis.compute_abnormal_returns()
232
+ >>> fig = plot_event_heatmap(ar_results)
233
+ >>> fig.show()
234
+ """
235
+ if not ar_results:
236
+ raise ValueError("No abnormal return results provided")
237
+
238
+ theme_config = get_theme(theme)
239
+
240
+ # Collect all days and build matrix
241
+ all_days: set[int] = set()
242
+ for r in ar_results:
243
+ all_days.update(r.ar_by_day.keys())
244
+ sorted_days = sorted(all_days)
245
+
246
+ # Build heatmap matrix
247
+ z_matrix = []
248
+ y_labels = []
249
+ hover_texts = []
250
+
251
+ for r in ar_results:
252
+ row = []
253
+ hover_row = []
254
+ for day in sorted_days:
255
+ ar = r.ar_by_day.get(day, float("nan"))
256
+ row.append(ar)
257
+ hover_row.append(
258
+ f"Event: {r.event_id}<br>Asset: {r.asset}<br>Day: {day}<br>AR: {ar:.4f}"
259
+ if not (ar != ar) # not nan check
260
+ else f"Day {day}: No data"
261
+ )
262
+ z_matrix.append(row)
263
+ hover_texts.append(hover_row)
264
+ y_labels.append(f"{r.event_id} ({r.asset})")
265
+
266
+ fig = go.Figure(
267
+ data=go.Heatmap(
268
+ z=z_matrix,
269
+ x=sorted_days,
270
+ y=y_labels,
271
+ colorscale="RdBu_r",
272
+ zmid=0,
273
+ colorbar={"title": "AR"},
274
+ hovertemplate="%{text}<extra></extra>",
275
+ text=hover_texts,
276
+ )
277
+ )
278
+
279
+ # Add vertical line at event day
280
+ fig.add_vline(x=0, line_dash="dash", line_color="black", line_width=2)
281
+
282
+ fig.update_layout(
283
+ title="Abnormal Returns by Event and Day",
284
+ xaxis_title="Days Relative to Event",
285
+ yaxis_title="Event",
286
+ width=width or 900,
287
+ height=height or max(400, len(ar_results) * 25 + 100),
288
+ template=theme_config.get("template", "plotly_white"),
289
+ )
290
+
291
+ return fig
292
+
293
+
294
+ def plot_ar_distribution(
295
+ result: EventStudyResult,
296
+ day: int = 0,
297
+ show_kde: bool = True,
298
+ theme: str | None = None,
299
+ width: int | None = None,
300
+ height: int | None = None,
301
+ ) -> go.Figure:
302
+ """Plot distribution of abnormal returns for a specific day.
303
+
304
+ Creates a histogram with optional KDE overlay showing the
305
+ cross-sectional distribution of ARs on a given day.
306
+
307
+ Parameters
308
+ ----------
309
+ result : EventStudyResult
310
+ Event study results with individual event data.
311
+ day : int, default 0
312
+ Relative day to plot (0 = event day).
313
+ show_kde : bool, default True
314
+ Overlay kernel density estimate.
315
+ theme : str | None
316
+ Visualization theme.
317
+ width : int | None
318
+ Figure width.
319
+ height : int | None
320
+ Figure height.
321
+
322
+ Returns
323
+ -------
324
+ go.Figure
325
+ Plotly figure with AR distribution.
326
+ """
327
+ if result.individual_results is None:
328
+ raise ValueError("EventStudyResult must include individual_results")
329
+
330
+ theme_config = get_theme(theme)
331
+ colors = theme_config.get("colors", {})
332
+
333
+ # Collect ARs for the specified day
334
+ ars = []
335
+ for r in result.individual_results:
336
+ if day in r.ar_by_day:
337
+ ars.append(r.ar_by_day[day])
338
+
339
+ if not ars:
340
+ raise ValueError(f"No AR data available for day {day}")
341
+
342
+ import numpy as np
343
+
344
+ ars_array = np.array(ars)
345
+
346
+ fig = go.Figure()
347
+
348
+ # Add histogram
349
+ fig.add_trace(
350
+ go.Histogram(
351
+ x=ars_array,
352
+ nbinsx=min(20, len(ars)),
353
+ name="AR Distribution",
354
+ marker_color=colors.get("primary", "#1f77b4"),
355
+ opacity=0.7,
356
+ histnorm="probability density" if show_kde else None,
357
+ )
358
+ )
359
+
360
+ # Add KDE if requested
361
+ if show_kde and len(ars) >= 5:
362
+ from scipy import stats as sp_stats
363
+
364
+ kde = sp_stats.gaussian_kde(ars_array)
365
+ x_range = np.linspace(min(ars_array), max(ars_array), 100)
366
+ kde_y = kde(x_range)
367
+
368
+ fig.add_trace(
369
+ go.Scatter(
370
+ x=x_range,
371
+ y=kde_y,
372
+ mode="lines",
373
+ name="KDE",
374
+ line={"color": colors.get("secondary", "#ff7f0e"), "width": 2},
375
+ )
376
+ )
377
+
378
+ # Add vertical line at mean
379
+ mean_ar = float(np.mean(ars_array))
380
+ fig.add_vline(
381
+ x=mean_ar,
382
+ line_dash="dash",
383
+ line_color="red",
384
+ annotation_text=f"Mean: {mean_ar:.4f}",
385
+ annotation_position="top right",
386
+ )
387
+
388
+ # Add vertical line at 0
389
+ fig.add_vline(x=0, line_dash="dot", line_color="gray")
390
+
391
+ # Calculate statistics
392
+ std_ar = float(np.std(ars_array, ddof=1))
393
+ t_stat = mean_ar / (std_ar / np.sqrt(len(ars))) if std_ar > 0 else 0
394
+ p_val = 2 * (1 - sp_stats.t.cdf(abs(t_stat), df=len(ars) - 1)) if len(ars) > 1 else 1.0
395
+
396
+ day_label = "Event Day" if day == 0 else f"Day {day:+d}"
397
+ fig.update_layout(
398
+ title=f"Abnormal Return Distribution - {day_label}",
399
+ xaxis_title="Abnormal Return",
400
+ yaxis_title="Density" if show_kde else "Count",
401
+ width=width or 600,
402
+ height=height or 400,
403
+ template=theme_config.get("template", "plotly_white"),
404
+ )
405
+
406
+ # Add statistics annotation
407
+ annotation_text = (
408
+ f"n = {len(ars)}<br>"
409
+ f"Mean = {mean_ar:.4f}<br>"
410
+ f"Std = {std_ar:.4f}<br>"
411
+ f"t-stat = {t_stat:.3f}<br>"
412
+ f"p-value = {p_val:.4f}"
413
+ )
414
+ fig.add_annotation(
415
+ xref="paper",
416
+ yref="paper",
417
+ x=0.98,
418
+ y=0.98,
419
+ text=annotation_text,
420
+ showarrow=False,
421
+ font={"size": 10},
422
+ align="left",
423
+ bgcolor="rgba(255,255,255,0.8)",
424
+ bordercolor="gray",
425
+ borderwidth=1,
426
+ )
427
+
428
+ return fig
429
+
430
+
431
+ def plot_car_by_event(
432
+ ar_results: list[AbnormalReturnResult],
433
+ sort_by: str = "car",
434
+ top_n: int | None = None,
435
+ theme: str | None = None,
436
+ width: int | None = None,
437
+ height: int | None = None,
438
+ ) -> go.Figure:
439
+ """Plot cumulative abnormal returns by event as horizontal bar chart.
440
+
441
+ Shows CAR for each event, sorted by magnitude, useful for
442
+ identifying outliers and event heterogeneity.
443
+
444
+ Parameters
445
+ ----------
446
+ ar_results : list[AbnormalReturnResult]
447
+ Individual event results.
448
+ sort_by : str, default "car"
449
+ Sort by "car" (magnitude) or "date".
450
+ top_n : int | None
451
+ Show only top N events by magnitude.
452
+ theme : str | None
453
+ Visualization theme.
454
+ width : int | None
455
+ Figure width.
456
+ height : int | None
457
+ Figure height.
458
+
459
+ Returns
460
+ -------
461
+ go.Figure
462
+ Plotly figure with CAR bar chart.
463
+ """
464
+ if not ar_results:
465
+ raise ValueError("No abnormal return results provided")
466
+
467
+ theme_config = get_theme(theme)
468
+ colors = theme_config.get("colors", {})
469
+
470
+ # Sort results
471
+ if sort_by == "car":
472
+ sorted_results = sorted(ar_results, key=lambda x: abs(x.car), reverse=True)
473
+ else:
474
+ sorted_results = sorted(ar_results, key=lambda x: x.event_date)
475
+
476
+ # Limit to top_n if specified
477
+ if top_n is not None:
478
+ sorted_results = sorted_results[:top_n]
479
+
480
+ # Prepare data
481
+ labels = [f"{r.event_id} ({r.asset})" for r in sorted_results]
482
+ cars = [r.car for r in sorted_results]
483
+ bar_colors = [
484
+ colors.get("positive", "#2ca02c") if c >= 0 else colors.get("negative", "#d62728")
485
+ for c in cars
486
+ ]
487
+
488
+ fig = go.Figure(
489
+ data=go.Bar(
490
+ y=labels,
491
+ x=cars,
492
+ orientation="h",
493
+ marker_color=bar_colors,
494
+ hovertemplate="Event: %{y}<br>CAR: %{x:.4f}<extra></extra>",
495
+ )
496
+ )
497
+
498
+ fig.add_vline(x=0, line_dash="solid", line_color="gray")
499
+
500
+ title = "Cumulative Abnormal Return by Event"
501
+ if top_n is not None:
502
+ title += f" (Top {top_n})"
503
+
504
+ fig.update_layout(
505
+ title=title,
506
+ xaxis_title="CAR",
507
+ yaxis_title="Event",
508
+ width=width or 700,
509
+ height=height or max(400, len(sorted_results) * 25 + 100),
510
+ template=theme_config.get("template", "plotly_white"),
511
+ yaxis={"autorange": "reversed"}, # Largest at top
512
+ )
513
+
514
+ return fig