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,635 @@
1
+ """IC (Information Coefficient) visualization plots.
2
+
3
+ This module provides interactive Plotly visualizations for IC analysis:
4
+ - plot_ic_ts: IC time series with rolling mean and significance bands
5
+ - plot_ic_histogram: IC distribution with mean and confidence intervals
6
+ - plot_ic_qq: Q-Q plot for normality assessment
7
+ - plot_ic_heatmap: Monthly IC heatmap for seasonality analysis
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from datetime import datetime
13
+ from typing import TYPE_CHECKING
14
+
15
+ import numpy as np
16
+ import plotly.graph_objects as go
17
+ from scipy import stats
18
+
19
+ from ml4t.diagnostic.visualization.core import (
20
+ create_base_figure,
21
+ format_percentage,
22
+ get_colorscale,
23
+ get_theme_config,
24
+ validate_theme,
25
+ )
26
+
27
+ if TYPE_CHECKING:
28
+ from ml4t.diagnostic.results.signal_results import SignalICResult
29
+
30
+
31
+ def plot_ic_ts(
32
+ ic_result: SignalICResult,
33
+ period: str | None = None,
34
+ rolling_window: int = 21,
35
+ show_significance: bool = True,
36
+ significance_level: float = 0.05,
37
+ theme: str | None = None,
38
+ width: int | None = None,
39
+ height: int | None = None,
40
+ ) -> go.Figure:
41
+ """Plot IC time series with rolling mean and significance bands.
42
+
43
+ Parameters
44
+ ----------
45
+ ic_result : SignalICResult
46
+ IC analysis result from SignalAnalysis.compute_ic_analysis()
47
+ period : str | None
48
+ Period to plot (e.g., "1D", "5D"). If None, uses first period.
49
+ rolling_window : int, default 21
50
+ Window size for rolling mean calculation
51
+ show_significance : bool, default True
52
+ Show significance bands (±1.96 * std for 95% CI)
53
+ significance_level : float, default 0.05
54
+ Significance level for bands (0.05 = 95% CI)
55
+ theme : str | None
56
+ Plot theme (default, dark, print, presentation)
57
+ width : int | None
58
+ Figure width in pixels
59
+ height : int | None
60
+ Figure height in pixels
61
+
62
+ Returns
63
+ -------
64
+ go.Figure
65
+ Interactive Plotly figure
66
+
67
+ Examples
68
+ --------
69
+ >>> ic_result = analyzer.compute_ic_analysis()
70
+ >>> fig = plot_ic_ts(ic_result, period="5D", rolling_window=21)
71
+ >>> fig.show()
72
+ """
73
+ theme = validate_theme(theme)
74
+ theme_config = get_theme_config(theme)
75
+
76
+ # Get period data
77
+ periods = list(ic_result.ic_by_date.keys())
78
+ if period is None:
79
+ period = periods[0]
80
+ elif period not in periods:
81
+ raise ValueError(f"Period '{period}' not found. Available: {periods}")
82
+
83
+ ic_series = np.array(ic_result.ic_by_date[period])
84
+ dates = ic_result.dates
85
+
86
+ # Convert dates to datetime if strings
87
+ if dates and isinstance(dates[0], str):
88
+ try:
89
+ dates = [datetime.fromisoformat(d) for d in dates]
90
+ except ValueError:
91
+ pass # Keep as strings if conversion fails
92
+
93
+ # Compute rolling mean
94
+ valid_mask = ~np.isnan(ic_series)
95
+ np.where(valid_mask, ic_series, 0)
96
+
97
+ rolling_mean = np.full_like(ic_series, np.nan)
98
+ for i in range(rolling_window - 1, len(ic_series)):
99
+ window = ic_series[i - rolling_window + 1 : i + 1]
100
+ window_valid = window[~np.isnan(window)]
101
+ if len(window_valid) >= rolling_window // 2:
102
+ rolling_mean[i] = np.mean(window_valid)
103
+
104
+ # Create figure
105
+ fig = create_base_figure(
106
+ title=f"IC Time Series ({period})",
107
+ xaxis_title="Date",
108
+ yaxis_title="Information Coefficient",
109
+ width=width or theme_config["defaults"]["line_height"] + 300,
110
+ height=height or theme_config["defaults"]["line_height"],
111
+ theme=theme,
112
+ )
113
+
114
+ # IC scatter points
115
+ fig.add_trace(
116
+ go.Scatter(
117
+ x=dates,
118
+ y=ic_series,
119
+ mode="markers",
120
+ name="Daily IC",
121
+ marker={
122
+ "size": 4,
123
+ "color": theme_config["colorway"][0],
124
+ "opacity": 0.5,
125
+ },
126
+ hovertemplate="Date: %{x}<br>IC: %{y:.4f}<extra></extra>",
127
+ )
128
+ )
129
+
130
+ # Rolling mean line
131
+ fig.add_trace(
132
+ go.Scatter(
133
+ x=dates,
134
+ y=rolling_mean,
135
+ mode="lines",
136
+ name=f"{rolling_window}-Day Rolling Mean",
137
+ line={
138
+ "color": theme_config["colorway"][1],
139
+ "width": 2,
140
+ },
141
+ hovertemplate="Date: %{x}<br>Rolling IC: %{y:.4f}<extra></extra>",
142
+ )
143
+ )
144
+
145
+ # Zero line
146
+ fig.add_hline(
147
+ y=0,
148
+ line_dash="dash",
149
+ line_color="gray",
150
+ opacity=0.5,
151
+ )
152
+
153
+ # Mean IC line
154
+ mean_ic = ic_result.ic_mean.get(period, 0)
155
+ fig.add_hline(
156
+ y=mean_ic,
157
+ line_dash="dot",
158
+ line_color=theme_config["colorway"][2],
159
+ annotation_text=f"Mean IC: {mean_ic:.4f}",
160
+ annotation_position="right",
161
+ )
162
+
163
+ # Significance bands
164
+ if show_significance:
165
+ ic_std = ic_result.ic_std.get(period, 0)
166
+ z_score = stats.norm.ppf(1 - significance_level / 2)
167
+ upper = z_score * ic_std / np.sqrt(len(ic_series))
168
+ lower = -upper
169
+
170
+ fig.add_hrect(
171
+ y0=lower,
172
+ y1=upper,
173
+ fillcolor="gray",
174
+ opacity=0.1,
175
+ line_width=0,
176
+ annotation_text=f"{100 * (1 - significance_level):.0f}% CI",
177
+ annotation_position="top right",
178
+ )
179
+
180
+ # Add summary annotation
181
+ positive_pct = ic_result.ic_positive_pct.get(period, 0)
182
+ ir = ic_result.ic_ir.get(period, 0)
183
+ t_stat = ic_result.ic_t_stat.get(period, 0)
184
+ p_value = ic_result.ic_p_value.get(period, 1)
185
+
186
+ summary_text = (
187
+ f"<b>Summary:</b><br>"
188
+ f"Mean IC: {mean_ic:.4f}<br>"
189
+ f"IC IR: {ir:.3f}<br>"
190
+ f"Positive %: {format_percentage(positive_pct)}<br>"
191
+ f"t-stat: {t_stat:.2f} (p={p_value:.4f})"
192
+ )
193
+
194
+ fig.add_annotation(
195
+ text=summary_text,
196
+ xref="paper",
197
+ yref="paper",
198
+ x=0.02,
199
+ y=0.98,
200
+ showarrow=False,
201
+ bgcolor="rgba(255,255,255,0.8)" if theme != "dark" else "rgba(50,50,50,0.8)",
202
+ bordercolor="gray",
203
+ borderwidth=1,
204
+ align="left",
205
+ )
206
+
207
+ fig.update_layout(
208
+ legend={
209
+ "yanchor": "top",
210
+ "y": 0.99,
211
+ "xanchor": "right",
212
+ "x": 0.99,
213
+ },
214
+ showlegend=True,
215
+ )
216
+
217
+ return fig
218
+
219
+
220
+ def plot_ic_histogram(
221
+ ic_result: SignalICResult,
222
+ period: str | None = None,
223
+ bins: int = 50,
224
+ show_kde: bool = True,
225
+ show_stats: bool = True,
226
+ theme: str | None = None,
227
+ width: int | None = None,
228
+ height: int | None = None,
229
+ ) -> go.Figure:
230
+ """Plot IC distribution histogram with optional KDE.
231
+
232
+ Parameters
233
+ ----------
234
+ ic_result : SignalICResult
235
+ IC analysis result from SignalAnalysis.compute_ic_analysis()
236
+ period : str | None
237
+ Period to plot. If None, uses first period.
238
+ bins : int, default 50
239
+ Number of histogram bins
240
+ show_kde : bool, default True
241
+ Show kernel density estimate overlay
242
+ show_stats : bool, default True
243
+ Show summary statistics annotation
244
+ theme : str | None
245
+ Plot theme
246
+ width : int | None
247
+ Figure width
248
+ height : int | None
249
+ Figure height
250
+
251
+ Returns
252
+ -------
253
+ go.Figure
254
+ Interactive Plotly figure
255
+ """
256
+ theme = validate_theme(theme)
257
+ theme_config = get_theme_config(theme)
258
+
259
+ # Get period data
260
+ periods = list(ic_result.ic_by_date.keys())
261
+ if period is None:
262
+ period = periods[0]
263
+ elif period not in periods:
264
+ raise ValueError(f"Period '{period}' not found. Available: {periods}")
265
+
266
+ ic_series = np.array(ic_result.ic_by_date[period])
267
+ ic_clean = ic_series[~np.isnan(ic_series)]
268
+
269
+ # Create figure
270
+ fig = create_base_figure(
271
+ title=f"IC Distribution ({period})",
272
+ xaxis_title="Information Coefficient",
273
+ yaxis_title="Frequency",
274
+ width=width or theme_config["defaults"]["bar_height"],
275
+ height=height or theme_config["defaults"]["bar_height"],
276
+ theme=theme,
277
+ )
278
+
279
+ # Histogram
280
+ fig.add_trace(
281
+ go.Histogram(
282
+ x=ic_clean,
283
+ nbinsx=bins,
284
+ name="IC Distribution",
285
+ marker_color=theme_config["colorway"][0],
286
+ opacity=0.7,
287
+ hovertemplate="IC: %{x:.4f}<br>Count: %{y}<extra></extra>",
288
+ )
289
+ )
290
+
291
+ # KDE overlay
292
+ if show_kde and len(ic_clean) > 10:
293
+ kde = stats.gaussian_kde(ic_clean)
294
+ x_kde = np.linspace(ic_clean.min(), ic_clean.max(), 200)
295
+ y_kde = kde(x_kde)
296
+
297
+ # Scale KDE to match histogram
298
+ hist_counts, _ = np.histogram(ic_clean, bins=bins)
299
+ bin_width = (ic_clean.max() - ic_clean.min()) / bins
300
+ y_kde_scaled = y_kde * len(ic_clean) * bin_width
301
+
302
+ fig.add_trace(
303
+ go.Scatter(
304
+ x=x_kde,
305
+ y=y_kde_scaled,
306
+ mode="lines",
307
+ name="KDE",
308
+ line={"color": theme_config["colorway"][1], "width": 2},
309
+ )
310
+ )
311
+
312
+ # Mean line
313
+ mean_ic = ic_result.ic_mean.get(period, 0)
314
+ fig.add_vline(
315
+ x=mean_ic,
316
+ line_dash="dash",
317
+ line_color=theme_config["colorway"][2],
318
+ annotation_text=f"Mean: {mean_ic:.4f}",
319
+ )
320
+
321
+ # Zero line
322
+ fig.add_vline(
323
+ x=0,
324
+ line_dash="dot",
325
+ line_color="gray",
326
+ opacity=0.7,
327
+ )
328
+
329
+ # Statistics annotation
330
+ if show_stats:
331
+ ic_std = ic_result.ic_std.get(period, 0)
332
+ skewness = float(stats.skew(ic_clean)) if len(ic_clean) > 2 else 0
333
+ kurtosis = float(stats.kurtosis(ic_clean)) if len(ic_clean) > 3 else 0
334
+
335
+ stats_text = (
336
+ f"<b>Statistics:</b><br>"
337
+ f"N: {len(ic_clean)}<br>"
338
+ f"Mean: {mean_ic:.4f}<br>"
339
+ f"Std: {ic_std:.4f}<br>"
340
+ f"Skew: {skewness:.3f}<br>"
341
+ f"Kurt: {kurtosis:.3f}"
342
+ )
343
+
344
+ fig.add_annotation(
345
+ text=stats_text,
346
+ xref="paper",
347
+ yref="paper",
348
+ x=0.98,
349
+ y=0.98,
350
+ showarrow=False,
351
+ bgcolor="rgba(255,255,255,0.8)" if theme != "dark" else "rgba(50,50,50,0.8)",
352
+ bordercolor="gray",
353
+ borderwidth=1,
354
+ align="left",
355
+ xanchor="right",
356
+ yanchor="top",
357
+ )
358
+
359
+ return fig
360
+
361
+
362
+ def plot_ic_qq(
363
+ ic_result: SignalICResult,
364
+ period: str | None = None,
365
+ theme: str | None = None,
366
+ width: int | None = None,
367
+ height: int | None = None,
368
+ ) -> go.Figure:
369
+ """Plot Q-Q plot for IC normality assessment.
370
+
371
+ Parameters
372
+ ----------
373
+ ic_result : SignalICResult
374
+ IC analysis result from SignalAnalysis.compute_ic_analysis()
375
+ period : str | None
376
+ Period to plot. If None, uses first period.
377
+ theme : str | None
378
+ Plot theme
379
+ width : int | None
380
+ Figure width
381
+ height : int | None
382
+ Figure height
383
+
384
+ Returns
385
+ -------
386
+ go.Figure
387
+ Interactive Plotly Q-Q plot
388
+ """
389
+ theme = validate_theme(theme)
390
+ theme_config = get_theme_config(theme)
391
+
392
+ # Get period data
393
+ periods = list(ic_result.ic_by_date.keys())
394
+ if period is None:
395
+ period = periods[0]
396
+ elif period not in periods:
397
+ raise ValueError(f"Period '{period}' not found. Available: {periods}")
398
+
399
+ ic_series = np.array(ic_result.ic_by_date[period])
400
+ ic_clean = ic_series[~np.isnan(ic_series)]
401
+ ic_sorted = np.sort(ic_clean)
402
+
403
+ # Theoretical quantiles
404
+ n = len(ic_sorted)
405
+ theoretical_quantiles = stats.norm.ppf(
406
+ (np.arange(1, n + 1) - 0.5) / n,
407
+ loc=np.mean(ic_clean),
408
+ scale=np.std(ic_clean, ddof=1),
409
+ )
410
+
411
+ # Create figure
412
+ fig = create_base_figure(
413
+ title=f"IC Q-Q Plot ({period})",
414
+ xaxis_title="Theoretical Quantiles (Normal)",
415
+ yaxis_title="Sample Quantiles (IC)",
416
+ width=width or theme_config["defaults"]["scatter_height"],
417
+ height=height or theme_config["defaults"]["scatter_height"],
418
+ theme=theme,
419
+ )
420
+
421
+ # Q-Q scatter
422
+ fig.add_trace(
423
+ go.Scatter(
424
+ x=theoretical_quantiles,
425
+ y=ic_sorted,
426
+ mode="markers",
427
+ name="IC Values",
428
+ marker={
429
+ "size": 6,
430
+ "color": theme_config["colorway"][0],
431
+ "opacity": 0.6,
432
+ },
433
+ hovertemplate="Theoretical: %{x:.4f}<br>Sample: %{y:.4f}<extra></extra>",
434
+ )
435
+ )
436
+
437
+ # Reference line (45-degree)
438
+ min_val = min(theoretical_quantiles.min(), ic_sorted.min())
439
+ max_val = max(theoretical_quantiles.max(), ic_sorted.max())
440
+
441
+ fig.add_trace(
442
+ go.Scatter(
443
+ x=[min_val, max_val],
444
+ y=[min_val, max_val],
445
+ mode="lines",
446
+ name="Normal Reference",
447
+ line={"color": theme_config["colorway"][1], "dash": "dash", "width": 2},
448
+ )
449
+ )
450
+
451
+ # Normality test
452
+ if len(ic_clean) >= 8:
453
+ _, shapiro_p = stats.shapiro(ic_clean[:5000]) # Shapiro-Wilk limited to 5000
454
+ _, jb_stat, jb_p = stats.jarque_bera(ic_clean)
455
+
456
+ normality_text = (
457
+ f"<b>Normality Tests:</b><br>"
458
+ f"Shapiro-Wilk p: {shapiro_p:.4f}<br>"
459
+ f"Jarque-Bera p: {jb_p:.4f}<br>"
460
+ f"{'✓ Normal' if min(shapiro_p, jb_p) > 0.05 else '✗ Non-normal'}"
461
+ )
462
+
463
+ fig.add_annotation(
464
+ text=normality_text,
465
+ xref="paper",
466
+ yref="paper",
467
+ x=0.02,
468
+ y=0.98,
469
+ showarrow=False,
470
+ bgcolor="rgba(255,255,255,0.8)" if theme != "dark" else "rgba(50,50,50,0.8)",
471
+ bordercolor="gray",
472
+ borderwidth=1,
473
+ align="left",
474
+ )
475
+
476
+ return fig
477
+
478
+
479
+ def plot_ic_heatmap(
480
+ ic_result: SignalICResult,
481
+ period: str | None = None,
482
+ colorscale: str = "rdbu",
483
+ theme: str | None = None,
484
+ width: int | None = None,
485
+ height: int | None = None,
486
+ ) -> go.Figure:
487
+ """Plot monthly IC heatmap for seasonality analysis.
488
+
489
+ Parameters
490
+ ----------
491
+ ic_result : SignalICResult
492
+ IC analysis result from SignalAnalysis.compute_ic_analysis()
493
+ period : str | None
494
+ Period to plot. If None, uses first period.
495
+ colorscale : str, default "rdbu"
496
+ Plotly colorscale name (rdbu for diverging red-blue)
497
+ theme : str | None
498
+ Plot theme
499
+ width : int | None
500
+ Figure width
501
+ height : int | None
502
+ Figure height
503
+
504
+ Returns
505
+ -------
506
+ go.Figure
507
+ Interactive Plotly heatmap
508
+ """
509
+ theme = validate_theme(theme)
510
+ theme_config = get_theme_config(theme)
511
+
512
+ # Get period data
513
+ periods = list(ic_result.ic_by_date.keys())
514
+ if period is None:
515
+ period = periods[0]
516
+ elif period not in periods:
517
+ raise ValueError(f"Period '{period}' not found. Available: {periods}")
518
+
519
+ ic_series = np.array(ic_result.ic_by_date[period])
520
+ dates = ic_result.dates
521
+
522
+ # Parse dates and create year-month structure
523
+ parsed_dates = []
524
+ for d in dates:
525
+ if isinstance(d, str):
526
+ try:
527
+ parsed_dates.append(datetime.fromisoformat(d))
528
+ except ValueError:
529
+ try:
530
+ parsed_dates.append(datetime.strptime(d, "%Y-%m-%d"))
531
+ except ValueError:
532
+ continue
533
+ elif isinstance(d, datetime):
534
+ parsed_dates.append(d)
535
+ else:
536
+ try:
537
+ parsed_dates.append(datetime.fromisoformat(str(d)))
538
+ except ValueError:
539
+ continue
540
+
541
+ if len(parsed_dates) != len(ic_series):
542
+ raise ValueError("Date parsing failed - length mismatch")
543
+
544
+ # Build year-month matrix
545
+ import pandas as pd
546
+
547
+ df = pd.DataFrame(
548
+ {
549
+ "date": parsed_dates,
550
+ "ic": ic_series,
551
+ }
552
+ )
553
+ df["year"] = df["date"].dt.year
554
+ df["month"] = df["date"].dt.month
555
+
556
+ # Pivot to get mean IC by year-month
557
+ pivot = df.pivot_table(values="ic", index="year", columns="month", aggfunc="mean")
558
+ pivot = pivot.sort_index(ascending=False) # Most recent year at top
559
+
560
+ # Month names
561
+ month_names = [
562
+ "Jan",
563
+ "Feb",
564
+ "Mar",
565
+ "Apr",
566
+ "May",
567
+ "Jun",
568
+ "Jul",
569
+ "Aug",
570
+ "Sep",
571
+ "Oct",
572
+ "Nov",
573
+ "Dec",
574
+ ]
575
+
576
+ # Create figure
577
+ fig = create_base_figure(
578
+ title=f"Monthly IC Heatmap ({period})",
579
+ xaxis_title="Month",
580
+ yaxis_title="Year",
581
+ width=width or theme_config["defaults"]["heatmap_height"],
582
+ height=height or 400 + 30 * len(pivot),
583
+ theme=theme,
584
+ )
585
+
586
+ # Get colorscale
587
+ try:
588
+ colors = get_colorscale(colorscale)
589
+ except ValueError:
590
+ colors = "RdBu"
591
+
592
+ # Symmetric color scale around zero
593
+ ic_values = pivot.values.flatten()
594
+ ic_clean = ic_values[~np.isnan(ic_values)]
595
+ if len(ic_clean) > 0:
596
+ max_abs = max(abs(ic_clean.min()), abs(ic_clean.max()))
597
+ zmin, zmax = -max_abs, max_abs
598
+ else:
599
+ zmin, zmax = -0.1, 0.1
600
+
601
+ # Heatmap
602
+ fig.add_trace(
603
+ go.Heatmap(
604
+ z=pivot.values,
605
+ x=month_names[: int(pivot.columns.max())],
606
+ y=pivot.index.astype(str).tolist(),
607
+ colorscale=colors if isinstance(colors, str) else "RdBu",
608
+ zmid=0,
609
+ zmin=zmin,
610
+ zmax=zmax,
611
+ colorbar={"title": "Mean IC"},
612
+ hovertemplate="Year: %{y}<br>Month: %{x}<br>IC: %{z:.4f}<extra></extra>",
613
+ )
614
+ )
615
+
616
+ # Add text annotations
617
+ for _i, year in enumerate(pivot.index):
618
+ for _j, month in enumerate(pivot.columns):
619
+ val = pivot.loc[year, month]
620
+ if not np.isnan(val):
621
+ text_color = "white" if abs(val) > max_abs * 0.5 else "black"
622
+ fig.add_annotation(
623
+ x=month_names[int(month) - 1],
624
+ y=str(year),
625
+ text=f"{val:.3f}",
626
+ showarrow=False,
627
+ font={"size": 10, "color": text_color},
628
+ )
629
+
630
+ fig.update_layout(
631
+ xaxis={"side": "bottom", "tickangle": 0},
632
+ yaxis={"autorange": "reversed"},
633
+ )
634
+
635
+ return fig