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,392 @@
1
+ """Quantile analysis result classes for signal analysis.
2
+
3
+ This module provides result classes for storing quantile analysis outputs including
4
+ mean returns by quantile, spread statistics, and monotonicity tests.
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
+ from ml4t.diagnostic.results.signal_results.validation import _normalize_period
20
+
21
+
22
+ class QuantileAnalysisResult(BaseResult):
23
+ """Results from quantile analysis.
24
+
25
+ Contains mean returns by quantile, spread analysis, and
26
+ monotonicity test results.
27
+
28
+ Examples
29
+ --------
30
+ >>> result = quantile_result
31
+ >>> print(result.summary())
32
+ >>> df = result.get_dataframe("mean_returns")
33
+ """
34
+
35
+ analysis_type: str = Field(default="quantile_analysis", frozen=True)
36
+
37
+ # ==========================================================================
38
+ # Quantile Configuration
39
+ # ==========================================================================
40
+
41
+ n_quantiles: int = Field(
42
+ ...,
43
+ description="Number of quantile bins",
44
+ )
45
+
46
+ quantile_labels: list[str] = Field(
47
+ ...,
48
+ description="Labels for each quantile (e.g., ['Q1', 'Q2', ..., 'Q5'])",
49
+ )
50
+
51
+ periods: list[str] = Field(
52
+ ...,
53
+ description="Forward return periods analyzed",
54
+ )
55
+
56
+ # ==========================================================================
57
+ # Mean Returns by Quantile
58
+ # ==========================================================================
59
+
60
+ mean_returns: dict[str, dict[str, float]] = Field(
61
+ ...,
62
+ description="Mean returns: {period: {quantile: mean_return}}",
63
+ )
64
+
65
+ std_returns: dict[str, dict[str, float]] = Field(
66
+ ...,
67
+ description="Std deviation of returns: {period: {quantile: std}}",
68
+ )
69
+
70
+ count_by_quantile: dict[str, int] = Field(
71
+ ...,
72
+ description="Number of observations per quantile",
73
+ )
74
+
75
+ # ==========================================================================
76
+ # Spread Analysis (Top - Bottom)
77
+ # ==========================================================================
78
+
79
+ spread_mean: dict[str, float] = Field(
80
+ ...,
81
+ description="Mean spread (top quantile - bottom quantile) per period",
82
+ )
83
+
84
+ spread_std: dict[str, float] = Field(
85
+ ...,
86
+ description="Std deviation of spread per period",
87
+ )
88
+
89
+ spread_t_stat: dict[str, float] = Field(
90
+ ...,
91
+ description="T-statistic for spread != 0",
92
+ )
93
+
94
+ spread_p_value: dict[str, float] = Field(
95
+ ...,
96
+ description="P-value for spread significance",
97
+ )
98
+
99
+ spread_ci_lower: dict[str, float] = Field(
100
+ ...,
101
+ description="Lower confidence interval for spread",
102
+ )
103
+
104
+ spread_ci_upper: dict[str, float] = Field(
105
+ ...,
106
+ description="Upper confidence interval for spread",
107
+ )
108
+
109
+ confidence_level: float = Field(
110
+ default=0.95,
111
+ description="Confidence level used for intervals",
112
+ )
113
+
114
+ # ==========================================================================
115
+ # Monotonicity Test
116
+ # ==========================================================================
117
+
118
+ is_monotonic: dict[str, bool] = Field(
119
+ ...,
120
+ description="Whether returns are monotonic across quantiles per period",
121
+ )
122
+
123
+ monotonicity_direction: dict[str, str] = Field(
124
+ ...,
125
+ description="Direction of monotonicity: 'increasing', 'decreasing', or 'none'",
126
+ )
127
+
128
+ rank_correlation: dict[str, float] = Field(
129
+ ...,
130
+ description="Spearman correlation between quantile rank and mean return",
131
+ )
132
+
133
+ # ==========================================================================
134
+ # Cumulative Returns (Optional)
135
+ # ==========================================================================
136
+
137
+ cumulative_returns: dict[str, dict[str, list[float]]] | None = Field(
138
+ default=None,
139
+ description="Cumulative returns by quantile: {period: {quantile: [values]}}",
140
+ )
141
+
142
+ cumulative_dates: list[str] | None = Field(
143
+ default=None,
144
+ description="Dates for cumulative returns",
145
+ )
146
+
147
+ # ==========================================================================
148
+ # Validation
149
+ # ==========================================================================
150
+
151
+ @model_validator(mode="after")
152
+ def _validate_keys(self) -> QuantileAnalysisResult:
153
+ """Validate that all dicts use consistent period and quantile keys."""
154
+ period_set = set(self.periods)
155
+ quantile_set = set(self.quantile_labels)
156
+
157
+ # Validate period-keyed dicts (flat dicts)
158
+ period_dicts: list[tuple[str, dict[str, Any]]] = [
159
+ ("spread_mean", self.spread_mean),
160
+ ("spread_std", self.spread_std),
161
+ ("spread_t_stat", self.spread_t_stat),
162
+ ("spread_p_value", self.spread_p_value),
163
+ ("spread_ci_lower", self.spread_ci_lower),
164
+ ("spread_ci_upper", self.spread_ci_upper),
165
+ ("is_monotonic", self.is_monotonic),
166
+ ("monotonicity_direction", self.monotonicity_direction),
167
+ ("rank_correlation", self.rank_correlation),
168
+ ]
169
+ for name, d in period_dicts:
170
+ if set(d.keys()) != period_set:
171
+ raise ValueError(
172
+ f"Key mismatch in '{name}': expected {period_set}, got {set(d.keys())}"
173
+ )
174
+
175
+ # Validate nested period-keyed dicts (mean_returns, std_returns)
176
+ for name, d in [("mean_returns", self.mean_returns), ("std_returns", self.std_returns)]:
177
+ if set(d.keys()) != period_set:
178
+ raise ValueError(
179
+ f"Key mismatch in '{name}' (outer keys): expected {period_set}, got {set(d.keys())}"
180
+ )
181
+ for period, inner in d.items():
182
+ if set(inner.keys()) != quantile_set:
183
+ raise ValueError(
184
+ f"Key mismatch in '{name}[{period}]': expected {quantile_set}, got {set(inner.keys())}"
185
+ )
186
+
187
+ # Validate quantile-keyed dict
188
+ if set(self.count_by_quantile.keys()) != quantile_set:
189
+ raise ValueError(
190
+ f"Key mismatch in 'count_by_quantile': expected {quantile_set}, got {set(self.count_by_quantile.keys())}"
191
+ )
192
+
193
+ # Validate n_quantiles consistency
194
+ if self.n_quantiles != len(self.quantile_labels):
195
+ raise ValueError(
196
+ f"n_quantiles ({self.n_quantiles}) != len(quantile_labels) ({len(self.quantile_labels)})"
197
+ )
198
+
199
+ return self
200
+
201
+ # ==========================================================================
202
+ # Methods
203
+ # ==========================================================================
204
+
205
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
206
+ """Get results as Polars DataFrame.
207
+
208
+ Parameters
209
+ ----------
210
+ name : str | None
211
+ DataFrame to retrieve:
212
+ - None or "mean_returns": Mean returns by quantile x period
213
+ - "spread": Spread statistics by period
214
+ - "cumulative": Cumulative returns time series (if available)
215
+
216
+ Returns
217
+ -------
218
+ pl.DataFrame
219
+ Requested DataFrame
220
+ """
221
+ if name is None or name == "mean_returns":
222
+ mean_return_rows: list[dict[str, Any]] = []
223
+ for period in self.periods:
224
+ for q in self.quantile_labels:
225
+ mean_return_rows.append(
226
+ {
227
+ "period": period,
228
+ "quantile": q,
229
+ "mean_return": self.mean_returns[period][q],
230
+ "std_return": self.std_returns[period][q],
231
+ }
232
+ )
233
+ return pl.DataFrame(mean_return_rows)
234
+
235
+ if name == "spread":
236
+ return pl.DataFrame(
237
+ {
238
+ "period": self.periods,
239
+ "spread_mean": [self.spread_mean[p] for p in self.periods],
240
+ "spread_std": [self.spread_std[p] for p in self.periods],
241
+ "spread_t_stat": [self.spread_t_stat[p] for p in self.periods],
242
+ "spread_p_value": [self.spread_p_value[p] for p in self.periods],
243
+ "spread_ci_lower": [self.spread_ci_lower[p] for p in self.periods],
244
+ "spread_ci_upper": [self.spread_ci_upper[p] for p in self.periods],
245
+ "is_monotonic": [self.is_monotonic[p] for p in self.periods],
246
+ "monotonicity_direction": [
247
+ self.monotonicity_direction[p] for p in self.periods
248
+ ],
249
+ "rank_correlation": [self.rank_correlation[p] for p in self.periods],
250
+ }
251
+ )
252
+
253
+ if name == "cumulative":
254
+ if self.cumulative_returns is None or self.cumulative_dates is None:
255
+ raise ValueError("Cumulative returns not available")
256
+ # Build wide DataFrame with dates and all quantile series
257
+ rows: list[dict[str, Any]] = []
258
+ for i, date in enumerate(self.cumulative_dates):
259
+ row: dict[str, Any] = {"date": date}
260
+ for period in self.periods:
261
+ for q in self.quantile_labels:
262
+ col_name = f"{period}_{q}"
263
+ row[col_name] = self.cumulative_returns[period][q][i]
264
+ rows.append(row)
265
+ return pl.DataFrame(rows)
266
+
267
+ raise ValueError(
268
+ f"Unknown DataFrame name: {name}. Available: 'mean_returns', 'spread', 'cumulative'"
269
+ )
270
+
271
+ def list_available_dataframes(self) -> list[str]:
272
+ """List available DataFrame views."""
273
+ dfs = ["mean_returns", "spread"]
274
+ if self.cumulative_returns is not None:
275
+ dfs.append("cumulative")
276
+ return dfs
277
+
278
+ def summary(self) -> str:
279
+ """Get human-readable summary of quantile analysis results."""
280
+ lines = ["=" * 60, "Quantile Analysis Summary", "=" * 60, ""]
281
+
282
+ for period in self.periods:
283
+ lines.append(f"Period: {period}")
284
+ lines.append("-" * 40)
285
+ lines.append("Quantile Mean Return Std")
286
+
287
+ for q in self.quantile_labels:
288
+ mean = self.mean_returns[period][q]
289
+ std = self.std_returns[period][q]
290
+ lines.append(f" {q:<10} {mean:>10.4%} {std:>10.4%}")
291
+
292
+ lines.append("")
293
+ lines.append(f"Spread (Top-Bottom): {self.spread_mean[period]:>10.4%}")
294
+ lines.append(f"Spread t-stat: {self.spread_t_stat[period]:>10.2f}")
295
+ lines.append(f"Spread p-value: {self.spread_p_value[period]:>10.4f}")
296
+ lines.append(
297
+ f"Monotonic: {self.is_monotonic[period]} ({self.monotonicity_direction[period]})"
298
+ )
299
+ lines.append("")
300
+
301
+ return "\n".join(lines)
302
+
303
+ # =========================================================================
304
+ # Convenience Accessor Methods
305
+ # =========================================================================
306
+
307
+ def get_quantile_returns(self, period: int | str) -> dict[str, float]:
308
+ """Get mean returns for all quantiles at a specific period.
309
+
310
+ Parameters
311
+ ----------
312
+ period : int | str
313
+ Period as integer (21) or string ('21' or '21D').
314
+
315
+ Returns
316
+ -------
317
+ dict[str, float]
318
+ Dict mapping quantile label to mean return: {'Q1': 0.01, 'Q2': 0.02, ...}
319
+
320
+ Examples
321
+ --------
322
+ >>> returns = quantile_result.get_quantile_returns(21)
323
+ >>> for q, ret in returns.items():
324
+ ... print(f"{q}: {ret:.2%}")
325
+ """
326
+ key = _normalize_period(period)
327
+ return self.mean_returns.get(key, {})
328
+
329
+ def get_spread(self, period: int | str) -> tuple[float, float, float]:
330
+ """Get spread statistics for a period.
331
+
332
+ Parameters
333
+ ----------
334
+ period : int | str
335
+ Period as integer or string.
336
+
337
+ Returns
338
+ -------
339
+ tuple[float, float, float]
340
+ Tuple of (spread_mean, spread_t_stat, spread_p_value).
341
+ Returns (nan, nan, nan) if period not found.
342
+
343
+ Examples
344
+ --------
345
+ >>> spread, t_stat, p_val = quantile_result.get_spread(21)
346
+ >>> print(f"Spread: {spread:.2%} (t={t_stat:.2f}, p={p_val:.4f})")
347
+ """
348
+ key = _normalize_period(period)
349
+ return (
350
+ self.spread_mean.get(key, float("nan")),
351
+ self.spread_t_stat.get(key, float("nan")),
352
+ self.spread_p_value.get(key, float("nan")),
353
+ )
354
+
355
+ def get_top_quantile_return(self, period: int | str) -> float | None:
356
+ """Get mean return for the top quantile (long side)."""
357
+ key = _normalize_period(period)
358
+ if key not in self.mean_returns:
359
+ return None
360
+ # Top quantile is the last one
361
+ top_label = self.quantile_labels[-1]
362
+ return self.mean_returns[key].get(top_label)
363
+
364
+ def get_bottom_quantile_return(self, period: int | str) -> float | None:
365
+ """Get mean return for the bottom quantile (short side)."""
366
+ key = _normalize_period(period)
367
+ if key not in self.mean_returns:
368
+ return None
369
+ # Bottom quantile is the first one
370
+ bottom_label = self.quantile_labels[0]
371
+ return self.mean_returns[key].get(bottom_label)
372
+
373
+ def is_spread_significant(self, period: int | str, alpha: float = 0.05) -> bool:
374
+ """Check if spread is statistically significant for a period.
375
+
376
+ Parameters
377
+ ----------
378
+ period : int | str
379
+ Period to check.
380
+ alpha : float, default 0.05
381
+ Significance level.
382
+
383
+ Returns
384
+ -------
385
+ bool
386
+ True if spread p-value < alpha.
387
+ """
388
+ key = _normalize_period(period)
389
+ p_val = self.spread_p_value.get(key)
390
+ if p_val is None:
391
+ return False
392
+ return p_val < alpha