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,305 @@
1
+ """Time-to-target analysis results for barrier outcomes.
2
+
3
+ This module provides the TimeToTargetResult class for storing time-to-target
4
+ metrics (mean, median, std bars to TP/SL/timeout) by signal quantile.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import polars as pl
10
+ from pydantic import Field, model_validator
11
+
12
+ from ml4t.diagnostic.results.barrier_results.validation import _validate_quantile_dict_keys
13
+ from ml4t.diagnostic.results.base import BaseResult
14
+
15
+
16
+ class TimeToTargetResult(BaseResult):
17
+ """Results from time-to-target analysis by signal decile.
18
+
19
+ Analyzes how quickly different signal quantiles reach their barrier
20
+ outcomes (TP, SL, or timeout).
21
+
22
+ Examples
23
+ --------
24
+ >>> result = time_to_target_result
25
+ >>> print(result.summary())
26
+ >>> df = result.get_dataframe()
27
+ """
28
+
29
+ analysis_type: str = Field(default="barrier_time_to_target", frozen=True)
30
+
31
+ # ==========================================================================
32
+ # Configuration
33
+ # ==========================================================================
34
+
35
+ n_quantiles: int = Field(
36
+ ...,
37
+ description="Number of quantiles used",
38
+ )
39
+
40
+ quantile_labels: list[str] = Field(
41
+ ...,
42
+ description="Labels for each quantile (e.g., ['D1', 'D2', ..., 'D10'])",
43
+ )
44
+
45
+ # ==========================================================================
46
+ # Mean Time to Exit by Quantile and Outcome
47
+ # ==========================================================================
48
+
49
+ mean_bars_tp: dict[str, float] = Field(
50
+ ...,
51
+ description="Mean bars to TP per quantile",
52
+ )
53
+
54
+ mean_bars_sl: dict[str, float] = Field(
55
+ ...,
56
+ description="Mean bars to SL per quantile",
57
+ )
58
+
59
+ mean_bars_timeout: dict[str, float] = Field(
60
+ ...,
61
+ description="Mean bars to timeout per quantile",
62
+ )
63
+
64
+ mean_bars_all: dict[str, float] = Field(
65
+ ...,
66
+ description="Mean bars to any exit per quantile",
67
+ )
68
+
69
+ # ==========================================================================
70
+ # Median Time to Exit
71
+ # ==========================================================================
72
+
73
+ median_bars_tp: dict[str, float] = Field(
74
+ ...,
75
+ description="Median bars to TP per quantile",
76
+ )
77
+
78
+ median_bars_sl: dict[str, float] = Field(
79
+ ...,
80
+ description="Median bars to SL per quantile",
81
+ )
82
+
83
+ median_bars_all: dict[str, float] = Field(
84
+ ...,
85
+ description="Median bars to any exit per quantile",
86
+ )
87
+
88
+ # ==========================================================================
89
+ # Standard Deviation
90
+ # ==========================================================================
91
+
92
+ std_bars_tp: dict[str, float] = Field(
93
+ ...,
94
+ description="Std dev of bars to TP per quantile",
95
+ )
96
+
97
+ std_bars_sl: dict[str, float] = Field(
98
+ ...,
99
+ description="Std dev of bars to SL per quantile",
100
+ )
101
+
102
+ std_bars_all: dict[str, float] = Field(
103
+ ...,
104
+ description="Std dev of bars to any exit per quantile",
105
+ )
106
+
107
+ # ==========================================================================
108
+ # Counts
109
+ # ==========================================================================
110
+
111
+ count_tp: dict[str, int] = Field(
112
+ ...,
113
+ description="Number of TP outcomes per quantile",
114
+ )
115
+
116
+ count_sl: dict[str, int] = Field(
117
+ ...,
118
+ description="Number of SL outcomes per quantile",
119
+ )
120
+
121
+ count_timeout: dict[str, int] = Field(
122
+ ...,
123
+ description="Number of timeout outcomes per quantile",
124
+ )
125
+
126
+ # ==========================================================================
127
+ # Overall Statistics
128
+ # ==========================================================================
129
+
130
+ overall_mean_bars: float = Field(
131
+ ...,
132
+ description="Overall mean bars to exit",
133
+ )
134
+
135
+ overall_median_bars: float = Field(
136
+ ...,
137
+ description="Overall median bars to exit",
138
+ )
139
+
140
+ overall_mean_bars_tp: float = Field(
141
+ ...,
142
+ description="Overall mean bars to TP",
143
+ )
144
+
145
+ overall_mean_bars_sl: float = Field(
146
+ ...,
147
+ description="Overall mean bars to SL",
148
+ )
149
+
150
+ n_observations: int = Field(
151
+ ...,
152
+ description="Total number of observations",
153
+ )
154
+
155
+ # ==========================================================================
156
+ # Speed Analysis
157
+ # ==========================================================================
158
+
159
+ tp_faster_than_sl: dict[str, bool] = Field(
160
+ ...,
161
+ description="Whether TP is reached faster than SL on average per quantile",
162
+ )
163
+
164
+ speed_advantage_tp: dict[str, float] = Field(
165
+ ...,
166
+ description="Speed advantage of TP over SL (positive = TP faster) per quantile",
167
+ )
168
+
169
+ # ==========================================================================
170
+ # Validation
171
+ # ==========================================================================
172
+
173
+ @model_validator(mode="after")
174
+ def _validate_quantile_keys(self) -> TimeToTargetResult:
175
+ """Validate that all quantile-keyed dicts have consistent keys."""
176
+ if self.n_quantiles != len(self.quantile_labels):
177
+ raise ValueError(
178
+ f"n_quantiles ({self.n_quantiles}) != len(quantile_labels) ({len(self.quantile_labels)})"
179
+ )
180
+ _validate_quantile_dict_keys(
181
+ self.quantile_labels,
182
+ [
183
+ ("mean_bars_tp", self.mean_bars_tp),
184
+ ("mean_bars_sl", self.mean_bars_sl),
185
+ ("mean_bars_timeout", self.mean_bars_timeout),
186
+ ("mean_bars_all", self.mean_bars_all),
187
+ ("median_bars_tp", self.median_bars_tp),
188
+ ("median_bars_sl", self.median_bars_sl),
189
+ ("median_bars_all", self.median_bars_all),
190
+ ("std_bars_tp", self.std_bars_tp),
191
+ ("std_bars_sl", self.std_bars_sl),
192
+ ("std_bars_all", self.std_bars_all),
193
+ ("count_tp", self.count_tp),
194
+ ("count_sl", self.count_sl),
195
+ ("count_timeout", self.count_timeout),
196
+ ("tp_faster_than_sl", self.tp_faster_than_sl),
197
+ ("speed_advantage_tp", self.speed_advantage_tp),
198
+ ],
199
+ )
200
+ return self
201
+
202
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
203
+ """Get results as Polars DataFrame.
204
+
205
+ Parameters
206
+ ----------
207
+ name : str | None
208
+ DataFrame to retrieve:
209
+ - None or "time_to_target": Mean times by quantile and outcome
210
+ - "detailed": Full statistics including median and std
211
+ - "summary": Overall statistics
212
+
213
+ Returns
214
+ -------
215
+ pl.DataFrame
216
+ Requested DataFrame
217
+ """
218
+ if name is None or name == "time_to_target":
219
+ return pl.DataFrame(
220
+ {
221
+ "quantile": self.quantile_labels,
222
+ "mean_bars_tp": [self.mean_bars_tp[q] for q in self.quantile_labels],
223
+ "mean_bars_sl": [self.mean_bars_sl[q] for q in self.quantile_labels],
224
+ "mean_bars_timeout": [self.mean_bars_timeout[q] for q in self.quantile_labels],
225
+ "mean_bars_all": [self.mean_bars_all[q] for q in self.quantile_labels],
226
+ "tp_faster": [self.tp_faster_than_sl[q] for q in self.quantile_labels],
227
+ }
228
+ )
229
+
230
+ if name == "detailed":
231
+ return pl.DataFrame(
232
+ {
233
+ "quantile": self.quantile_labels,
234
+ "mean_bars_tp": [self.mean_bars_tp[q] for q in self.quantile_labels],
235
+ "median_bars_tp": [self.median_bars_tp[q] for q in self.quantile_labels],
236
+ "std_bars_tp": [self.std_bars_tp[q] for q in self.quantile_labels],
237
+ "mean_bars_sl": [self.mean_bars_sl[q] for q in self.quantile_labels],
238
+ "median_bars_sl": [self.median_bars_sl[q] for q in self.quantile_labels],
239
+ "std_bars_sl": [self.std_bars_sl[q] for q in self.quantile_labels],
240
+ "count_tp": [self.count_tp[q] for q in self.quantile_labels],
241
+ "count_sl": [self.count_sl[q] for q in self.quantile_labels],
242
+ "count_timeout": [self.count_timeout[q] for q in self.quantile_labels],
243
+ }
244
+ )
245
+
246
+ if name == "summary":
247
+ return pl.DataFrame(
248
+ {
249
+ "metric": [
250
+ "n_observations",
251
+ "n_quantiles",
252
+ "overall_mean_bars",
253
+ "overall_median_bars",
254
+ "overall_mean_bars_tp",
255
+ "overall_mean_bars_sl",
256
+ ],
257
+ "value": [
258
+ float(self.n_observations),
259
+ float(self.n_quantiles),
260
+ self.overall_mean_bars,
261
+ self.overall_median_bars,
262
+ self.overall_mean_bars_tp,
263
+ self.overall_mean_bars_sl,
264
+ ],
265
+ }
266
+ )
267
+
268
+ raise ValueError(
269
+ f"Unknown DataFrame name: {name}. Available: 'time_to_target', 'detailed', 'summary'"
270
+ )
271
+
272
+ def list_available_dataframes(self) -> list[str]:
273
+ """List available DataFrame views."""
274
+ return ["time_to_target", "detailed", "summary"]
275
+
276
+ def summary(self) -> str:
277
+ """Get human-readable summary of time-to-target results."""
278
+ lines = [
279
+ "=" * 60,
280
+ "Barrier Time-to-Target Analysis",
281
+ "=" * 60,
282
+ "",
283
+ f"Observations: {self.n_observations:>10,}",
284
+ f"Quantiles: {self.n_quantiles:>10}",
285
+ "",
286
+ "Overall Time to Exit:",
287
+ f" Mean Bars: {self.overall_mean_bars:>10.1f}",
288
+ f" Median Bars: {self.overall_median_bars:>10.1f}",
289
+ f" Mean Bars (TP): {self.overall_mean_bars_tp:>10.1f}",
290
+ f" Mean Bars (SL): {self.overall_mean_bars_sl:>10.1f}",
291
+ "",
292
+ "-" * 60,
293
+ "Mean Bars to Exit by Quantile:",
294
+ "-" * 60,
295
+ f"{'Quantile':<10} {'TP':>8} {'SL':>8} {'Timeout':>8} {'All':>8} {'TP Faster?':>12}",
296
+ ]
297
+
298
+ for q in self.quantile_labels:
299
+ tp_faster = "Yes" if self.tp_faster_than_sl[q] else "No"
300
+ lines.append(
301
+ f"{q:<10} {self.mean_bars_tp[q]:>8.1f} {self.mean_bars_sl[q]:>8.1f} "
302
+ f"{self.mean_bars_timeout[q]:>8.1f} {self.mean_bars_all[q]:>8.1f} {tp_faster:>12}"
303
+ )
304
+
305
+ return "\n".join(lines)
@@ -0,0 +1,38 @@
1
+ """Validation helper functions for barrier analysis results.
2
+
3
+ This module provides validation utilities for quantile-keyed dictionaries
4
+ used across all barrier result classes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+
12
+ def _validate_quantile_dict_keys(
13
+ quantile_labels: list[str],
14
+ dicts: list[tuple[str, dict[str, Any]]],
15
+ ) -> None:
16
+ """Validate that all quantile-keyed dicts have the expected keys.
17
+
18
+ Parameters
19
+ ----------
20
+ quantile_labels : list[str]
21
+ Expected quantile labels (keys).
22
+ dicts : list[tuple[str, dict]]
23
+ List of (field_name, dict) tuples to validate.
24
+
25
+ Raises
26
+ ------
27
+ ValueError
28
+ If any dict has different keys than quantile_labels.
29
+ """
30
+ expected_keys = set(quantile_labels)
31
+ for name, d in dicts:
32
+ actual_keys = set(d.keys())
33
+ if actual_keys != expected_keys:
34
+ missing = expected_keys - actual_keys
35
+ extra = actual_keys - expected_keys
36
+ raise ValueError(
37
+ f"Key mismatch in '{name}': missing={missing or 'none'}, extra={extra or 'none'}"
38
+ )
@@ -0,0 +1,177 @@
1
+ """Base result class with common functionality for all evaluation results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from typing import Any
7
+
8
+ import polars as pl
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+
12
+ class BaseResult(BaseModel):
13
+ """Base class for all evaluation results.
14
+
15
+ Provides common functionality:
16
+ - Metadata (timestamp, version, analysis type)
17
+ - JSON export via model_dump_json()
18
+ - Dict export via to_dict()
19
+ - DataFrame conversion via get_dataframe()
20
+ - Human-readable summary via summary()
21
+
22
+ All result schemas inherit from this class to ensure consistent behavior.
23
+
24
+ Examples:
25
+ >>> result = FeatureDiagnosticsResult(...)
26
+ >>> json_str = result.model_dump_json(indent=2)
27
+ >>> df = result.get_dataframe()
28
+ >>> available = result.list_available_dataframes()
29
+ >>> print(result.summary())
30
+ """
31
+
32
+ model_config = ConfigDict(
33
+ extra="forbid", # Catch typos
34
+ validate_assignment=True, # Validate on mutation
35
+ arbitrary_types_allowed=True, # Allow Polars types if needed
36
+ use_enum_values=True, # Serialize enums as values
37
+ )
38
+
39
+ # Metadata fields - present in all results
40
+ created_at: str = Field(
41
+ default_factory=lambda: datetime.now(UTC).isoformat(),
42
+ description="ISO timestamp of result creation (UTC)",
43
+ )
44
+ analysis_type: str = Field(
45
+ ...,
46
+ description="Type of analysis performed (e.g., 'feature_diagnostics')",
47
+ )
48
+ version: str = Field(
49
+ default="2.0.0",
50
+ description="ML4T Diagnostic version used to generate results",
51
+ )
52
+
53
+ def to_dict(self, *, exclude_none: bool = False) -> dict[str, Any]:
54
+ """Export to Python dictionary.
55
+
56
+ Args:
57
+ exclude_none: Exclude fields with None values
58
+
59
+ Returns:
60
+ Dictionary representation of result
61
+ """
62
+ return self.model_dump(exclude_none=exclude_none, mode="python")
63
+
64
+ def to_json_string(self, *, indent: int | None = 2) -> str:
65
+ """Export to JSON string.
66
+
67
+ Args:
68
+ indent: Indentation level (None for compact)
69
+
70
+ Returns:
71
+ JSON string representation
72
+ """
73
+ return self.model_dump_json(indent=indent)
74
+
75
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
76
+ """Get results as Polars DataFrame.
77
+
78
+ Provides programmatic access to underlying data for QEngine storage
79
+ and further analysis. Subclasses should override to provide specific
80
+ DataFrame views.
81
+
82
+ Args:
83
+ name: Optional DataFrame name to retrieve specific view.
84
+ If None, returns primary/default DataFrame.
85
+ Use list_available_dataframes() to see available names.
86
+
87
+ Returns:
88
+ Polars DataFrame with results
89
+
90
+ Raises:
91
+ NotImplementedError: If not implemented by subclass
92
+ ValueError: If requested DataFrame name not available
93
+
94
+ Examples:
95
+ >>> result = FeatureDiagnosticsResult(...)
96
+ >>> df = result.get_dataframe() # Primary DataFrame
97
+ >>> df = result.get_dataframe("stationarity") # Specific view
98
+ >>> available = result.list_available_dataframes()
99
+ >>> for name in available:
100
+ ... df = result.get_dataframe(name)
101
+ """
102
+ raise NotImplementedError(f"{self.__class__.__name__} must implement get_dataframe()")
103
+
104
+ def list_available_dataframes(self) -> list[str]:
105
+ """List available DataFrame views for this result.
106
+
107
+ Returns names that can be passed to get_dataframe() to retrieve
108
+ specific data views. Useful for discovery and QEngine integration.
109
+
110
+ Returns:
111
+ List of available DataFrame names
112
+
113
+ Examples:
114
+ >>> result.list_available_dataframes()
115
+ ['primary', 'stationarity', 'autocorrelation', 'volatility']
116
+ """
117
+ # Default implementation - subclasses should override
118
+ return ["primary"]
119
+
120
+ def get_dataframe_schema(self, name: str | None = None) -> dict[str, str]:
121
+ """Get schema information for a DataFrame.
122
+
123
+ Returns column names and types for a DataFrame without loading data.
124
+ Useful for QEngine to understand data structure before retrieval.
125
+
126
+ Args:
127
+ name: DataFrame name (None for primary)
128
+
129
+ Returns:
130
+ Dictionary mapping column names to Polars dtype strings
131
+
132
+ Examples:
133
+ >>> result.get_dataframe_schema("stationarity")
134
+ {
135
+ 'feature': 'String',
136
+ 'adf_pvalue': 'Float64',
137
+ 'is_stationary': 'Boolean'
138
+ }
139
+ """
140
+ # Get actual DataFrame and extract schema
141
+ df = self.get_dataframe(name)
142
+ return {col: str(dtype) for col, dtype in zip(df.columns, df.dtypes, strict=False)}
143
+
144
+ def summary(self) -> str:
145
+ """Get human-readable summary of results.
146
+
147
+ This method should be overridden by subclasses to provide
148
+ meaningful summaries of their specific data.
149
+
150
+ Returns:
151
+ Formatted summary string
152
+
153
+ Raises:
154
+ NotImplementedError: If not implemented by subclass
155
+ """
156
+ raise NotImplementedError(f"{self.__class__.__name__} must implement summary()")
157
+
158
+ def interpret(self) -> list[str]:
159
+ """Get human-readable interpretation of results.
160
+
161
+ Returns actionable insights and recommendations based on
162
+ the analysis results. Default implementation returns empty list.
163
+
164
+ Returns:
165
+ List of interpretation strings with insights and recommendations
166
+
167
+ Examples:
168
+ >>> result.interpret()
169
+ ['Strategy is statistically significant (DSR=98.2% > 95%)',
170
+ 'Recommendation: Strategy shows robust performance']
171
+ """
172
+ # Default implementation - subclasses can override
173
+ return []
174
+
175
+ def __repr__(self) -> str:
176
+ """Concise representation showing type and timestamp."""
177
+ return f"{self.__class__.__name__}(analysis_type={self.analysis_type!r}, created_at={self.created_at!r})"