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,456 @@
1
+ """SignalTearSheet class for complete signal analysis results.
2
+
3
+ This module provides the SignalTearSheet class that aggregates all signal
4
+ analysis components (IC, quantile, turnover, IR_tc) into a single exportable
5
+ result object with visualization and export capabilities.
6
+
7
+ References
8
+ ----------
9
+ Lopez de Prado, M. (2018). "Advances in Financial Machine Learning"
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Any, Literal
16
+
17
+ import polars as pl
18
+ from pydantic import Field
19
+
20
+ from ml4t.diagnostic.results.base import BaseResult
21
+ from ml4t.diagnostic.results.signal_results.ic import SignalICResult
22
+ from ml4t.diagnostic.results.signal_results.irtc import IRtcResult
23
+ from ml4t.diagnostic.results.signal_results.quantile import QuantileAnalysisResult
24
+ from ml4t.diagnostic.results.signal_results.turnover import TurnoverAnalysisResult
25
+ from ml4t.diagnostic.results.signal_results.validation import _figure_from_data
26
+
27
+
28
+ class SignalTearSheet(BaseResult):
29
+ """Complete tear sheet containing all signal analysis results.
30
+
31
+ Aggregates IC, quantile, turnover, and visualization data into
32
+ a single exportable result object.
33
+
34
+ Examples
35
+ --------
36
+ >>> tear_sheet = signal_analysis.create_tear_sheet()
37
+ >>> tear_sheet.show() # Display in Jupyter
38
+ >>> tear_sheet.save_html("signal_report.html")
39
+ """
40
+
41
+ analysis_type: str = Field(default="signal_tear_sheet", frozen=True)
42
+
43
+ # ==========================================================================
44
+ # Component Results
45
+ # ==========================================================================
46
+
47
+ ic_analysis: SignalICResult | None = Field(
48
+ default=None,
49
+ description="Signal IC analysis results",
50
+ )
51
+
52
+ quantile_analysis: QuantileAnalysisResult | None = Field(
53
+ default=None,
54
+ description="Quantile analysis results",
55
+ )
56
+
57
+ turnover_analysis: TurnoverAnalysisResult | None = Field(
58
+ default=None,
59
+ description="Turnover analysis results",
60
+ )
61
+
62
+ ir_tc_analysis: IRtcResult | None = Field(
63
+ default=None,
64
+ description="IR_tc analysis results",
65
+ )
66
+
67
+ # ==========================================================================
68
+ # Metadata
69
+ # ==========================================================================
70
+
71
+ signal_name: str = Field(
72
+ default="signal",
73
+ description="Name of the signal analyzed",
74
+ )
75
+
76
+ n_assets: int = Field(
77
+ ...,
78
+ description="Number of unique assets",
79
+ )
80
+
81
+ n_dates: int = Field(
82
+ ...,
83
+ description="Number of unique dates",
84
+ )
85
+
86
+ date_range: tuple[str, str] = Field(
87
+ ...,
88
+ description="Date range (start, end) in ISO format",
89
+ )
90
+
91
+ # ==========================================================================
92
+ # Figures (stored as JSON for serialization)
93
+ # ==========================================================================
94
+
95
+ figures: dict[str, Any] = Field(
96
+ default_factory=dict,
97
+ description="Plotly figures as JSON (for HTML export)",
98
+ )
99
+
100
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
101
+ """Get results as Polars DataFrame.
102
+
103
+ Parameters
104
+ ----------
105
+ name : str | None
106
+ DataFrame to retrieve - routes to component results
107
+
108
+ Returns
109
+ -------
110
+ pl.DataFrame
111
+ Requested DataFrame
112
+ """
113
+ if name is None or name == "summary":
114
+ return self._build_summary_df()
115
+
116
+ # Route to component results
117
+ if name.startswith("ic_"):
118
+ if self.ic_analysis is None:
119
+ raise ValueError("IC analysis not available")
120
+ component_name = name[3:] if name != "ic_analysis" else None
121
+ return self.ic_analysis.get_dataframe(component_name)
122
+
123
+ if name.startswith("quantile_"):
124
+ if self.quantile_analysis is None:
125
+ raise ValueError("Quantile analysis not available")
126
+ component_name = name[9:] if name != "quantile_analysis" else None
127
+ return self.quantile_analysis.get_dataframe(component_name)
128
+
129
+ if name.startswith("turnover_"):
130
+ if self.turnover_analysis is None:
131
+ raise ValueError("Turnover analysis not available")
132
+ component_name = name[9:] if name != "turnover_analysis" else None
133
+ return self.turnover_analysis.get_dataframe(component_name)
134
+
135
+ raise ValueError(
136
+ f"Unknown DataFrame name: {name}. Use 'summary' or prefix with "
137
+ "'ic_', 'quantile_', 'turnover_'"
138
+ )
139
+
140
+ def list_available_dataframes(self) -> list[str]:
141
+ """List available DataFrame views."""
142
+ available = ["summary"]
143
+ if self.ic_analysis:
144
+ available.extend([f"ic_{n}" for n in self.ic_analysis.list_available_dataframes()])
145
+ if self.quantile_analysis:
146
+ available.extend(
147
+ [f"quantile_{n}" for n in self.quantile_analysis.list_available_dataframes()]
148
+ )
149
+ if self.turnover_analysis:
150
+ available.extend(
151
+ [f"turnover_{n}" for n in self.turnover_analysis.list_available_dataframes()]
152
+ )
153
+ return available
154
+
155
+ def _build_summary_df(self) -> pl.DataFrame:
156
+ """Build summary DataFrame with key metrics."""
157
+ rows = [
158
+ {"metric": "signal_name", "value": self.signal_name},
159
+ {"metric": "n_assets", "value": str(self.n_assets)},
160
+ {"metric": "n_dates", "value": str(self.n_dates)},
161
+ {"metric": "date_range_start", "value": self.date_range[0]},
162
+ {"metric": "date_range_end", "value": self.date_range[1]},
163
+ ]
164
+
165
+ if self.ic_analysis:
166
+ for period, ic in self.ic_analysis.ic_mean.items():
167
+ rows.append({"metric": f"ic_mean_{period}", "value": f"{ic:.4f}"})
168
+
169
+ return pl.DataFrame(rows)
170
+
171
+ def summary(self) -> str:
172
+ """Get human-readable summary of complete tear sheet."""
173
+ lines = [
174
+ "=" * 60,
175
+ f"Signal Analysis Tear Sheet: {self.signal_name}",
176
+ "=" * 60,
177
+ "",
178
+ f"Assets: {self.n_assets:>10}",
179
+ f"Dates: {self.n_dates:>10}",
180
+ f"Range: {self.date_range[0]} to {self.date_range[1]}",
181
+ f"Created: {self.created_at}",
182
+ "",
183
+ ]
184
+
185
+ if self.ic_analysis:
186
+ lines.append("--- IC Analysis ---")
187
+ lines.append(self.ic_analysis.summary())
188
+
189
+ if self.quantile_analysis:
190
+ lines.append("--- Quantile Analysis ---")
191
+ lines.append(self.quantile_analysis.summary())
192
+
193
+ if self.turnover_analysis:
194
+ lines.append("--- Turnover Analysis ---")
195
+ lines.append(self.turnover_analysis.summary())
196
+
197
+ if self.ir_tc_analysis:
198
+ lines.append("--- IR_tc Analysis ---")
199
+ lines.append(self.ir_tc_analysis.summary())
200
+
201
+ return "\n".join(lines)
202
+
203
+ def show(self) -> None:
204
+ """Display tear sheet in Jupyter notebook.
205
+
206
+ Renders all figures inline using IPython display.
207
+ """
208
+ try:
209
+ from IPython.display import HTML, display
210
+
211
+ # Display summary
212
+ display(HTML(f"<h2>Signal Analysis: {self.signal_name}</h2>"))
213
+ display(HTML(f"<p>{self.n_assets} assets, {self.n_dates} dates</p>"))
214
+
215
+ # Display figures
216
+ for _name, fig_json in self.figures.items():
217
+ fig = _figure_from_data(fig_json)
218
+ fig.show()
219
+
220
+ except ImportError:
221
+ print("IPython not available. Use save_html() instead.")
222
+ print(self.summary())
223
+
224
+ def save_html(
225
+ self,
226
+ path: str | Path,
227
+ use_dashboard: bool = True,
228
+ include_plotlyjs: str | bool = "cdn",
229
+ theme: Literal["light", "dark"] = "light",
230
+ ) -> Path:
231
+ """Save tear sheet as self-contained HTML file.
232
+
233
+ Parameters
234
+ ----------
235
+ path : str | Path
236
+ Output file path
237
+ use_dashboard : bool, default=True
238
+ If True, use multi-tab SignalDashboard format.
239
+ If False, use simple stacked plot layout.
240
+ include_plotlyjs : str | bool
241
+ How to include plotly.js: 'cdn', 'directory', True (embed), False
242
+ theme : str, default='light'
243
+ Theme for dashboard: 'light' or 'dark' (only used if use_dashboard=True)
244
+
245
+ Returns
246
+ -------
247
+ Path
248
+ Path to saved file
249
+ """
250
+ path = Path(path)
251
+ path.parent.mkdir(parents=True, exist_ok=True)
252
+
253
+ if use_dashboard:
254
+ # Use multi-tab dashboard format
255
+ from ml4t.diagnostic.visualization.signal.dashboard import SignalDashboard
256
+
257
+ dashboard = SignalDashboard(
258
+ title=f"Signal Analysis: {self.signal_name}",
259
+ theme=theme,
260
+ )
261
+ html = dashboard.generate(self)
262
+ path.write_text(html)
263
+ else:
264
+ # Use simple stacked layout (legacy behavior)
265
+ import plotly.io as pio
266
+
267
+ # NOTE: Plotly.js is included via pio.to_html with include_plotlyjs parameter
268
+ # Do NOT add hardcoded CDN script here - it would duplicate the inclusion
269
+ html_parts = [
270
+ "<!DOCTYPE html>",
271
+ "<html>",
272
+ "<head>",
273
+ f"<title>Signal Analysis: {self.signal_name}</title>",
274
+ "<style>",
275
+ "body { font-family: -apple-system, system-ui, sans-serif; margin: 40px; }",
276
+ "h1 { color: #2C3E50; }",
277
+ ".summary { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; }",
278
+ ".plot-container { margin-bottom: 40px; }",
279
+ "</style>",
280
+ "</head>",
281
+ "<body>",
282
+ f"<h1>Signal Analysis: {self.signal_name}</h1>",
283
+ "<div class='summary'>",
284
+ f"<p><strong>Assets:</strong> {self.n_assets}</p>",
285
+ f"<p><strong>Dates:</strong> {self.n_dates}</p>",
286
+ f"<p><strong>Range:</strong> {self.date_range[0]} to {self.date_range[1]}</p>",
287
+ f"<p><strong>Generated:</strong> {self.created_at}</p>",
288
+ "</div>",
289
+ ]
290
+
291
+ # Add figures
292
+ plotlyjs_included = False
293
+ for name, fig_json in self.figures.items():
294
+ fig = _figure_from_data(fig_json)
295
+ fig_html = pio.to_html(
296
+ fig,
297
+ include_plotlyjs=include_plotlyjs if not plotlyjs_included else False,
298
+ full_html=False,
299
+ )
300
+ html_parts.append("<div class='plot-container'>")
301
+ html_parts.append(f"<h2>{name.replace('_', ' ').title()}</h2>")
302
+ html_parts.append(fig_html)
303
+ html_parts.append("</div>")
304
+ plotlyjs_included = True
305
+
306
+ html_parts.extend(["</body>", "</html>"])
307
+ path.write_text("\n".join(html_parts))
308
+
309
+ return path
310
+
311
+ def save_json(self, path: str | Path, exclude_figures: bool = False) -> Path:
312
+ """Export all metrics as structured JSON.
313
+
314
+ Parameters
315
+ ----------
316
+ path : str | Path
317
+ Output file path
318
+ exclude_figures : bool, default=False
319
+ If True, exclude figure JSON data to reduce file size
320
+
321
+ Returns
322
+ -------
323
+ Path
324
+ Path to saved file
325
+
326
+ Examples
327
+ --------
328
+ >>> tear_sheet.save_json("signal_metrics.json")
329
+ >>> tear_sheet.save_json("signal_compact.json", exclude_figures=True)
330
+ """
331
+ import json
332
+
333
+ path = Path(path)
334
+ path.parent.mkdir(parents=True, exist_ok=True)
335
+
336
+ data = self.to_dict(exclude_none=True)
337
+
338
+ if exclude_figures:
339
+ data.pop("figures", None)
340
+
341
+ with open(path, "w", encoding="utf-8") as f:
342
+ json.dump(data, f, indent=2, default=str)
343
+
344
+ return path
345
+
346
+ def save_png(
347
+ self,
348
+ output_dir: str | Path,
349
+ figures: list[str] | None = None,
350
+ width: int = 1200,
351
+ height: int = 600,
352
+ scale: float = 2.0,
353
+ ) -> list[Path]:
354
+ """Export figures as PNG images.
355
+
356
+ Requires the `kaleido` package for static image export.
357
+ Install with: pip install kaleido
358
+
359
+ Parameters
360
+ ----------
361
+ output_dir : str | Path
362
+ Output directory for PNG files
363
+ figures : list[str] | None
364
+ List of figure names to export. If None, exports all figures.
365
+ width : int, default=1200
366
+ Image width in pixels
367
+ height : int, default=600
368
+ Image height in pixels
369
+ scale : float, default=2.0
370
+ Scale factor for resolution (2.0 = 2x resolution)
371
+
372
+ Returns
373
+ -------
374
+ list[Path]
375
+ Paths to saved PNG files
376
+
377
+ Raises
378
+ ------
379
+ ImportError
380
+ If kaleido is not installed
381
+
382
+ Examples
383
+ --------
384
+ >>> paths = tear_sheet.save_png("./images/")
385
+ >>> paths = tear_sheet.save_png("./images/", figures=["ic_time_series"])
386
+ """
387
+ try:
388
+ import plotly.io as pio
389
+
390
+ # Check if kaleido is available
391
+ pio.kaleido.scope # noqa: B018 - Check if kaleido is installed
392
+ except (ImportError, AttributeError) as e:
393
+ raise ImportError(
394
+ "kaleido is required for PNG export. Install with: pip install kaleido"
395
+ ) from e
396
+
397
+ output_dir = Path(output_dir)
398
+ output_dir.mkdir(parents=True, exist_ok=True)
399
+
400
+ saved_paths: list[Path] = []
401
+ figure_names = figures if figures is not None else list(self.figures.keys())
402
+
403
+ for name in figure_names:
404
+ if name not in self.figures:
405
+ continue
406
+
407
+ fig_json = self.figures[name]
408
+ fig = _figure_from_data(fig_json)
409
+
410
+ output_path = output_dir / f"{name}.png"
411
+ fig.write_image(
412
+ str(output_path),
413
+ width=width,
414
+ height=height,
415
+ scale=scale,
416
+ )
417
+ saved_paths.append(output_path)
418
+
419
+ return saved_paths
420
+
421
+ def to_dashboard(self, theme: Literal["light", "dark"] = "light") -> Any:
422
+ """Convert to SignalDashboard for customization.
423
+
424
+ Returns a SignalDashboard instance that can be further customized
425
+ before generating HTML output.
426
+
427
+ Parameters
428
+ ----------
429
+ theme : Literal["light", "dark"], default='light'
430
+ Dashboard theme: 'light' or 'dark'
431
+
432
+ Returns
433
+ -------
434
+ SignalDashboard
435
+ Dashboard instance ready for customization
436
+
437
+ Examples
438
+ --------
439
+ >>> dashboard = tear_sheet.to_dashboard(theme="dark")
440
+ >>> dashboard.title = "Custom Title"
441
+ >>> html = dashboard.generate(tear_sheet)
442
+ """
443
+ from ml4t.diagnostic.visualization.signal.dashboard import SignalDashboard
444
+
445
+ return SignalDashboard(
446
+ title=f"Signal Analysis: {self.signal_name}",
447
+ theme=theme,
448
+ )
449
+
450
+ def to_dict(self, *, exclude_none: bool = False) -> dict[str, Any]:
451
+ """Export to dictionary, excluding large figure data by default."""
452
+ data = super().to_dict(exclude_none=exclude_none)
453
+ # Optionally exclude figures to reduce size
454
+ if exclude_none and not self.figures:
455
+ data.pop("figures", None)
456
+ return data
@@ -0,0 +1,213 @@
1
+ """Turnover analysis result classes for signal analysis.
2
+
3
+ This module provides result classes for storing turnover analysis outputs including
4
+ quantile turnover rates, signal autocorrelation, and stability metrics.
5
+
6
+ References
7
+ ----------
8
+ Lopez de Prado, M. (2018). "Advances in Financial Machine Learning"
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ import polars as pl
16
+ from pydantic import Field, model_validator
17
+
18
+ from ml4t.diagnostic.results.base import BaseResult
19
+
20
+
21
+ class TurnoverAnalysisResult(BaseResult):
22
+ """Results from turnover analysis.
23
+
24
+ Contains quantile turnover rates, signal autocorrelation,
25
+ and stability metrics.
26
+
27
+ Examples
28
+ --------
29
+ >>> result = turnover_result
30
+ >>> print(result.summary())
31
+ >>> df = result.get_dataframe("turnover")
32
+ """
33
+
34
+ analysis_type: str = Field(default="turnover_analysis", frozen=True)
35
+
36
+ # ==========================================================================
37
+ # Quantile Turnover
38
+ # ==========================================================================
39
+
40
+ quantile_turnover: dict[str, dict[str, float]] = Field(
41
+ ...,
42
+ description="Turnover rate by quantile and period: {period: {quantile: turnover}}",
43
+ )
44
+
45
+ mean_turnover: dict[str, float] = Field(
46
+ ...,
47
+ description="Mean turnover across all quantiles per period",
48
+ )
49
+
50
+ top_quantile_turnover: dict[str, float] = Field(
51
+ ...,
52
+ description="Turnover for top quantile (long positions)",
53
+ )
54
+
55
+ bottom_quantile_turnover: dict[str, float] = Field(
56
+ ...,
57
+ description="Turnover for bottom quantile (short positions)",
58
+ )
59
+
60
+ # ==========================================================================
61
+ # Signal Autocorrelation
62
+ # ==========================================================================
63
+
64
+ autocorrelation: dict[str, list[float]] = Field(
65
+ ...,
66
+ description="Autocorrelation by lag: {period: [ac_lag1, ac_lag2, ...]}",
67
+ )
68
+
69
+ autocorrelation_lags: list[int] = Field(
70
+ ...,
71
+ description="Lag values used",
72
+ )
73
+
74
+ mean_autocorrelation: dict[str, float] = Field(
75
+ ...,
76
+ description="Mean autocorrelation (average across first 5 lags)",
77
+ )
78
+
79
+ # ==========================================================================
80
+ # Stability Metrics
81
+ # ==========================================================================
82
+
83
+ half_life: dict[str, float | None] = Field(
84
+ ...,
85
+ description="Signal half-life in periods (time for AC to decay by 50%)",
86
+ )
87
+
88
+ # ==========================================================================
89
+ # Validation
90
+ # ==========================================================================
91
+
92
+ @model_validator(mode="after")
93
+ def _validate_keys(self) -> TurnoverAnalysisResult:
94
+ """Validate that all period-keyed dicts share the same keys and list lengths match."""
95
+ # Get reference period set from quantile_turnover
96
+ period_set = set(self.quantile_turnover.keys())
97
+
98
+ # Validate period-keyed dicts
99
+ period_dicts: list[tuple[str, dict[str, Any]]] = [
100
+ ("mean_turnover", self.mean_turnover),
101
+ ("top_quantile_turnover", self.top_quantile_turnover),
102
+ ("bottom_quantile_turnover", self.bottom_quantile_turnover),
103
+ ("autocorrelation", self.autocorrelation),
104
+ ("mean_autocorrelation", self.mean_autocorrelation),
105
+ ("half_life", self.half_life),
106
+ ]
107
+ for name, d in period_dicts:
108
+ if set(d.keys()) != period_set:
109
+ raise ValueError(
110
+ f"Key mismatch in '{name}': expected {period_set}, got {set(d.keys())}"
111
+ )
112
+
113
+ # Validate autocorrelation list lengths match autocorrelation_lags
114
+ n_lags = len(self.autocorrelation_lags)
115
+ for period, ac_values in self.autocorrelation.items():
116
+ if len(ac_values) != n_lags:
117
+ raise ValueError(
118
+ f"Length mismatch in autocorrelation['{period}']: "
119
+ f"expected {n_lags} (len(autocorrelation_lags)), got {len(ac_values)}"
120
+ )
121
+
122
+ return self
123
+
124
+ # ==========================================================================
125
+ # Methods
126
+ # ==========================================================================
127
+
128
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
129
+ """Get results as Polars DataFrame.
130
+
131
+ Parameters
132
+ ----------
133
+ name : str | None
134
+ DataFrame to retrieve:
135
+ - None or "turnover": Turnover by quantile
136
+ - "autocorrelation": Autocorrelation by lag
137
+ - "summary": Summary statistics
138
+
139
+ Returns
140
+ -------
141
+ pl.DataFrame
142
+ Requested DataFrame
143
+ """
144
+ if name is None or name == "turnover":
145
+ periods = list(self.quantile_turnover.keys())
146
+ if not periods:
147
+ return pl.DataFrame()
148
+
149
+ quantiles = list(self.quantile_turnover[periods[0]].keys())
150
+ rows = []
151
+ for period in periods:
152
+ for q in quantiles:
153
+ rows.append(
154
+ {
155
+ "period": period,
156
+ "quantile": q,
157
+ "turnover": self.quantile_turnover[period][q],
158
+ }
159
+ )
160
+ return pl.DataFrame(rows)
161
+
162
+ if name == "autocorrelation":
163
+ periods = list(self.autocorrelation.keys())
164
+ rows = []
165
+ for period in periods:
166
+ for i, lag in enumerate(self.autocorrelation_lags):
167
+ rows.append(
168
+ {
169
+ "period": period,
170
+ "lag": lag,
171
+ "autocorrelation": self.autocorrelation[period][i],
172
+ }
173
+ )
174
+ return pl.DataFrame(rows)
175
+
176
+ if name == "summary":
177
+ periods = list(self.mean_turnover.keys())
178
+ return pl.DataFrame(
179
+ {
180
+ "period": periods,
181
+ "mean_turnover": [self.mean_turnover[p] for p in periods],
182
+ "top_turnover": [self.top_quantile_turnover[p] for p in periods],
183
+ "bottom_turnover": [self.bottom_quantile_turnover[p] for p in periods],
184
+ "mean_autocorrelation": [self.mean_autocorrelation[p] for p in periods],
185
+ "half_life": [self.half_life[p] for p in periods],
186
+ }
187
+ )
188
+
189
+ raise ValueError(
190
+ f"Unknown DataFrame name: {name}. Available: 'turnover', 'autocorrelation', 'summary'"
191
+ )
192
+
193
+ def list_available_dataframes(self) -> list[str]:
194
+ """List available DataFrame views."""
195
+ return ["turnover", "autocorrelation", "summary"]
196
+
197
+ def summary(self) -> str:
198
+ """Get human-readable summary of turnover analysis results."""
199
+ lines = ["=" * 60, "Turnover Analysis Summary", "=" * 60, ""]
200
+
201
+ for period in self.mean_turnover:
202
+ lines.append(f"Period: {period}")
203
+ lines.append("-" * 40)
204
+ lines.append(f" Mean Turnover: {self.mean_turnover[period]:>8.2%}")
205
+ lines.append(f" Top Quantile: {self.top_quantile_turnover[period]:>8.2%}")
206
+ lines.append(f" Bottom Quantile: {self.bottom_quantile_turnover[period]:>8.2%}")
207
+ lines.append(f" Mean Autocorrelation: {self.mean_autocorrelation[period]:>8.4f}")
208
+
209
+ if self.half_life[period] is not None:
210
+ lines.append(f" Signal Half-Life: {self.half_life[period]:>8.1f} periods")
211
+ lines.append("")
212
+
213
+ return "\n".join(lines)