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.
- ml4t/diagnostic/AGENT.md +25 -0
- ml4t/diagnostic/__init__.py +166 -0
- ml4t/diagnostic/backends/__init__.py +10 -0
- ml4t/diagnostic/backends/adapter.py +192 -0
- ml4t/diagnostic/backends/polars_backend.py +899 -0
- ml4t/diagnostic/caching/__init__.py +40 -0
- ml4t/diagnostic/caching/cache.py +331 -0
- ml4t/diagnostic/caching/decorators.py +131 -0
- ml4t/diagnostic/caching/smart_cache.py +339 -0
- ml4t/diagnostic/config/AGENT.md +24 -0
- ml4t/diagnostic/config/README.md +267 -0
- ml4t/diagnostic/config/__init__.py +219 -0
- ml4t/diagnostic/config/barrier_config.py +277 -0
- ml4t/diagnostic/config/base.py +301 -0
- ml4t/diagnostic/config/event_config.py +148 -0
- ml4t/diagnostic/config/feature_config.py +404 -0
- ml4t/diagnostic/config/multi_signal_config.py +55 -0
- ml4t/diagnostic/config/portfolio_config.py +215 -0
- ml4t/diagnostic/config/report_config.py +391 -0
- ml4t/diagnostic/config/sharpe_config.py +202 -0
- ml4t/diagnostic/config/signal_config.py +206 -0
- ml4t/diagnostic/config/trade_analysis_config.py +310 -0
- ml4t/diagnostic/config/validation.py +279 -0
- ml4t/diagnostic/core/__init__.py +29 -0
- ml4t/diagnostic/core/numba_utils.py +315 -0
- ml4t/diagnostic/core/purging.py +372 -0
- ml4t/diagnostic/core/sampling.py +471 -0
- ml4t/diagnostic/errors/__init__.py +205 -0
- ml4t/diagnostic/evaluation/AGENT.md +26 -0
- ml4t/diagnostic/evaluation/__init__.py +437 -0
- ml4t/diagnostic/evaluation/autocorrelation.py +531 -0
- ml4t/diagnostic/evaluation/barrier_analysis.py +1050 -0
- ml4t/diagnostic/evaluation/binary_metrics.py +910 -0
- ml4t/diagnostic/evaluation/dashboard.py +715 -0
- ml4t/diagnostic/evaluation/diagnostic_plots.py +1037 -0
- ml4t/diagnostic/evaluation/distribution/__init__.py +499 -0
- ml4t/diagnostic/evaluation/distribution/moments.py +299 -0
- ml4t/diagnostic/evaluation/distribution/tails.py +777 -0
- ml4t/diagnostic/evaluation/distribution/tests.py +470 -0
- ml4t/diagnostic/evaluation/drift/__init__.py +139 -0
- ml4t/diagnostic/evaluation/drift/analysis.py +432 -0
- ml4t/diagnostic/evaluation/drift/domain_classifier.py +517 -0
- ml4t/diagnostic/evaluation/drift/population_stability_index.py +310 -0
- ml4t/diagnostic/evaluation/drift/wasserstein.py +388 -0
- ml4t/diagnostic/evaluation/event_analysis.py +647 -0
- ml4t/diagnostic/evaluation/excursion.py +390 -0
- ml4t/diagnostic/evaluation/feature_diagnostics.py +873 -0
- ml4t/diagnostic/evaluation/feature_outcome.py +666 -0
- ml4t/diagnostic/evaluation/framework.py +935 -0
- ml4t/diagnostic/evaluation/metric_registry.py +255 -0
- ml4t/diagnostic/evaluation/metrics/AGENT.md +23 -0
- ml4t/diagnostic/evaluation/metrics/__init__.py +133 -0
- ml4t/diagnostic/evaluation/metrics/basic.py +160 -0
- ml4t/diagnostic/evaluation/metrics/conditional_ic.py +469 -0
- ml4t/diagnostic/evaluation/metrics/feature_outcome.py +475 -0
- ml4t/diagnostic/evaluation/metrics/ic_statistics.py +446 -0
- ml4t/diagnostic/evaluation/metrics/importance_analysis.py +338 -0
- ml4t/diagnostic/evaluation/metrics/importance_classical.py +375 -0
- ml4t/diagnostic/evaluation/metrics/importance_mda.py +371 -0
- ml4t/diagnostic/evaluation/metrics/importance_shap.py +715 -0
- ml4t/diagnostic/evaluation/metrics/information_coefficient.py +527 -0
- ml4t/diagnostic/evaluation/metrics/interactions.py +772 -0
- ml4t/diagnostic/evaluation/metrics/monotonicity.py +226 -0
- ml4t/diagnostic/evaluation/metrics/risk_adjusted.py +324 -0
- ml4t/diagnostic/evaluation/multi_signal.py +550 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/__init__.py +83 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/analysis.py +734 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/metrics.py +589 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/results.py +334 -0
- ml4t/diagnostic/evaluation/report_generation.py +824 -0
- ml4t/diagnostic/evaluation/signal_selector.py +452 -0
- ml4t/diagnostic/evaluation/stat_registry.py +139 -0
- ml4t/diagnostic/evaluation/stationarity/__init__.py +97 -0
- ml4t/diagnostic/evaluation/stationarity/analysis.py +518 -0
- ml4t/diagnostic/evaluation/stationarity/augmented_dickey_fuller.py +296 -0
- ml4t/diagnostic/evaluation/stationarity/kpss_test.py +308 -0
- ml4t/diagnostic/evaluation/stationarity/phillips_perron.py +365 -0
- ml4t/diagnostic/evaluation/stats/AGENT.md +43 -0
- ml4t/diagnostic/evaluation/stats/__init__.py +191 -0
- ml4t/diagnostic/evaluation/stats/backtest_overfitting.py +219 -0
- ml4t/diagnostic/evaluation/stats/bootstrap.py +228 -0
- ml4t/diagnostic/evaluation/stats/deflated_sharpe_ratio.py +591 -0
- ml4t/diagnostic/evaluation/stats/false_discovery_rate.py +295 -0
- ml4t/diagnostic/evaluation/stats/hac_standard_errors.py +108 -0
- ml4t/diagnostic/evaluation/stats/minimum_track_record.py +408 -0
- ml4t/diagnostic/evaluation/stats/moments.py +164 -0
- ml4t/diagnostic/evaluation/stats/rademacher_adjustment.py +436 -0
- ml4t/diagnostic/evaluation/stats/reality_check.py +155 -0
- ml4t/diagnostic/evaluation/stats/sharpe_inference.py +219 -0
- ml4t/diagnostic/evaluation/themes.py +330 -0
- ml4t/diagnostic/evaluation/threshold_analysis.py +957 -0
- ml4t/diagnostic/evaluation/trade_analysis.py +1136 -0
- ml4t/diagnostic/evaluation/trade_dashboard/__init__.py +32 -0
- ml4t/diagnostic/evaluation/trade_dashboard/app.py +315 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/__init__.py +18 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/csv.py +82 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/html.py +276 -0
- ml4t/diagnostic/evaluation/trade_dashboard/io.py +166 -0
- ml4t/diagnostic/evaluation/trade_dashboard/normalize.py +304 -0
- ml4t/diagnostic/evaluation/trade_dashboard/stats.py +386 -0
- ml4t/diagnostic/evaluation/trade_dashboard/style.py +79 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/__init__.py +21 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/patterns.py +354 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/shap_analysis.py +280 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/stat_validation.py +186 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/worst_trades.py +236 -0
- ml4t/diagnostic/evaluation/trade_dashboard/types.py +129 -0
- ml4t/diagnostic/evaluation/trade_shap/__init__.py +102 -0
- ml4t/diagnostic/evaluation/trade_shap/alignment.py +188 -0
- ml4t/diagnostic/evaluation/trade_shap/characterize.py +413 -0
- ml4t/diagnostic/evaluation/trade_shap/cluster.py +302 -0
- ml4t/diagnostic/evaluation/trade_shap/explain.py +208 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/__init__.py +23 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/generator.py +290 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/matcher.py +251 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/templates.yaml +467 -0
- ml4t/diagnostic/evaluation/trade_shap/models.py +386 -0
- ml4t/diagnostic/evaluation/trade_shap/normalize.py +116 -0
- ml4t/diagnostic/evaluation/trade_shap/pipeline.py +263 -0
- ml4t/diagnostic/evaluation/trade_shap_dashboard.py +283 -0
- ml4t/diagnostic/evaluation/trade_shap_diagnostics.py +588 -0
- ml4t/diagnostic/evaluation/validated_cv.py +535 -0
- ml4t/diagnostic/evaluation/visualization.py +1050 -0
- ml4t/diagnostic/evaluation/volatility/__init__.py +45 -0
- ml4t/diagnostic/evaluation/volatility/analysis.py +351 -0
- ml4t/diagnostic/evaluation/volatility/arch.py +258 -0
- ml4t/diagnostic/evaluation/volatility/garch.py +460 -0
- ml4t/diagnostic/integration/__init__.py +48 -0
- ml4t/diagnostic/integration/backtest_contract.py +671 -0
- ml4t/diagnostic/integration/data_contract.py +316 -0
- ml4t/diagnostic/integration/engineer_contract.py +226 -0
- ml4t/diagnostic/logging/__init__.py +77 -0
- ml4t/diagnostic/logging/logger.py +245 -0
- ml4t/diagnostic/logging/performance.py +234 -0
- ml4t/diagnostic/logging/progress.py +234 -0
- ml4t/diagnostic/logging/wandb.py +412 -0
- ml4t/diagnostic/metrics/__init__.py +9 -0
- ml4t/diagnostic/metrics/percentiles.py +128 -0
- ml4t/diagnostic/py.typed +1 -0
- ml4t/diagnostic/reporting/__init__.py +43 -0
- ml4t/diagnostic/reporting/base.py +130 -0
- ml4t/diagnostic/reporting/html_renderer.py +275 -0
- ml4t/diagnostic/reporting/json_renderer.py +51 -0
- ml4t/diagnostic/reporting/markdown_renderer.py +117 -0
- ml4t/diagnostic/results/AGENT.md +24 -0
- ml4t/diagnostic/results/__init__.py +105 -0
- ml4t/diagnostic/results/barrier_results/__init__.py +36 -0
- ml4t/diagnostic/results/barrier_results/hit_rate.py +304 -0
- ml4t/diagnostic/results/barrier_results/precision_recall.py +266 -0
- ml4t/diagnostic/results/barrier_results/profit_factor.py +297 -0
- ml4t/diagnostic/results/barrier_results/tearsheet.py +397 -0
- ml4t/diagnostic/results/barrier_results/time_to_target.py +305 -0
- ml4t/diagnostic/results/barrier_results/validation.py +38 -0
- ml4t/diagnostic/results/base.py +177 -0
- ml4t/diagnostic/results/event_results.py +349 -0
- ml4t/diagnostic/results/feature_results.py +787 -0
- ml4t/diagnostic/results/multi_signal_results.py +431 -0
- ml4t/diagnostic/results/portfolio_results.py +281 -0
- ml4t/diagnostic/results/sharpe_results.py +448 -0
- ml4t/diagnostic/results/signal_results/__init__.py +74 -0
- ml4t/diagnostic/results/signal_results/ic.py +581 -0
- ml4t/diagnostic/results/signal_results/irtc.py +110 -0
- ml4t/diagnostic/results/signal_results/quantile.py +392 -0
- ml4t/diagnostic/results/signal_results/tearsheet.py +456 -0
- ml4t/diagnostic/results/signal_results/turnover.py +213 -0
- ml4t/diagnostic/results/signal_results/validation.py +147 -0
- ml4t/diagnostic/signal/AGENT.md +17 -0
- ml4t/diagnostic/signal/__init__.py +69 -0
- ml4t/diagnostic/signal/_report.py +152 -0
- ml4t/diagnostic/signal/_utils.py +261 -0
- ml4t/diagnostic/signal/core.py +275 -0
- ml4t/diagnostic/signal/quantile.py +148 -0
- ml4t/diagnostic/signal/result.py +214 -0
- ml4t/diagnostic/signal/signal_ic.py +129 -0
- ml4t/diagnostic/signal/turnover.py +182 -0
- ml4t/diagnostic/splitters/AGENT.md +19 -0
- ml4t/diagnostic/splitters/__init__.py +36 -0
- ml4t/diagnostic/splitters/base.py +501 -0
- ml4t/diagnostic/splitters/calendar.py +421 -0
- ml4t/diagnostic/splitters/calendar_config.py +91 -0
- ml4t/diagnostic/splitters/combinatorial.py +1064 -0
- ml4t/diagnostic/splitters/config.py +322 -0
- ml4t/diagnostic/splitters/cpcv/__init__.py +57 -0
- ml4t/diagnostic/splitters/cpcv/combinations.py +119 -0
- ml4t/diagnostic/splitters/cpcv/partitioning.py +263 -0
- ml4t/diagnostic/splitters/cpcv/purge_engine.py +379 -0
- ml4t/diagnostic/splitters/cpcv/windows.py +190 -0
- ml4t/diagnostic/splitters/group_isolation.py +329 -0
- ml4t/diagnostic/splitters/persistence.py +316 -0
- ml4t/diagnostic/splitters/utils.py +207 -0
- ml4t/diagnostic/splitters/walk_forward.py +757 -0
- ml4t/diagnostic/utils/__init__.py +42 -0
- ml4t/diagnostic/utils/config.py +542 -0
- ml4t/diagnostic/utils/dependencies.py +318 -0
- ml4t/diagnostic/utils/sessions.py +127 -0
- ml4t/diagnostic/validation/__init__.py +54 -0
- ml4t/diagnostic/validation/dataframe.py +274 -0
- ml4t/diagnostic/validation/returns.py +280 -0
- ml4t/diagnostic/validation/timeseries.py +299 -0
- ml4t/diagnostic/visualization/AGENT.md +19 -0
- ml4t/diagnostic/visualization/__init__.py +223 -0
- ml4t/diagnostic/visualization/backtest/__init__.py +98 -0
- ml4t/diagnostic/visualization/backtest/cost_attribution.py +762 -0
- ml4t/diagnostic/visualization/backtest/executive_summary.py +895 -0
- ml4t/diagnostic/visualization/backtest/interactive_controls.py +673 -0
- ml4t/diagnostic/visualization/backtest/statistical_validity.py +874 -0
- ml4t/diagnostic/visualization/backtest/tearsheet.py +565 -0
- ml4t/diagnostic/visualization/backtest/template_system.py +373 -0
- ml4t/diagnostic/visualization/backtest/trade_plots.py +1172 -0
- ml4t/diagnostic/visualization/barrier_plots.py +782 -0
- ml4t/diagnostic/visualization/core.py +1060 -0
- ml4t/diagnostic/visualization/dashboards/__init__.py +36 -0
- ml4t/diagnostic/visualization/dashboards/base.py +582 -0
- ml4t/diagnostic/visualization/dashboards/importance.py +801 -0
- ml4t/diagnostic/visualization/dashboards/interaction.py +263 -0
- ml4t/diagnostic/visualization/dashboards.py +43 -0
- ml4t/diagnostic/visualization/data_extraction/__init__.py +48 -0
- ml4t/diagnostic/visualization/data_extraction/importance.py +649 -0
- ml4t/diagnostic/visualization/data_extraction/interaction.py +504 -0
- ml4t/diagnostic/visualization/data_extraction/types.py +113 -0
- ml4t/diagnostic/visualization/data_extraction/validation.py +66 -0
- ml4t/diagnostic/visualization/feature_plots.py +888 -0
- ml4t/diagnostic/visualization/interaction_plots.py +618 -0
- ml4t/diagnostic/visualization/portfolio/__init__.py +41 -0
- ml4t/diagnostic/visualization/portfolio/dashboard.py +514 -0
- ml4t/diagnostic/visualization/portfolio/drawdown_plots.py +341 -0
- ml4t/diagnostic/visualization/portfolio/returns_plots.py +487 -0
- ml4t/diagnostic/visualization/portfolio/risk_plots.py +301 -0
- ml4t/diagnostic/visualization/report_generation.py +1343 -0
- ml4t/diagnostic/visualization/signal/__init__.py +103 -0
- ml4t/diagnostic/visualization/signal/dashboard.py +911 -0
- ml4t/diagnostic/visualization/signal/event_plots.py +514 -0
- ml4t/diagnostic/visualization/signal/ic_plots.py +635 -0
- ml4t/diagnostic/visualization/signal/multi_signal_dashboard.py +974 -0
- ml4t/diagnostic/visualization/signal/multi_signal_plots.py +603 -0
- ml4t/diagnostic/visualization/signal/quantile_plots.py +625 -0
- ml4t/diagnostic/visualization/signal/turnover_plots.py +400 -0
- ml4t/diagnostic/visualization/trade_shap/__init__.py +90 -0
- ml4t_diagnostic-0.1.0a1.dist-info/METADATA +1044 -0
- ml4t_diagnostic-0.1.0a1.dist-info/RECORD +242 -0
- ml4t_diagnostic-0.1.0a1.dist-info/WHEEL +4 -0
- 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
|