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,408 @@
|
|
|
1
|
+
"""Minimum Track Record Length (MinTRL) calculation.
|
|
2
|
+
|
|
3
|
+
MinTRL is the minimum number of observations required to reject the null
|
|
4
|
+
hypothesis (SR ≤ target) at the specified confidence level.
|
|
5
|
+
|
|
6
|
+
References
|
|
7
|
+
----------
|
|
8
|
+
López de Prado, M., Lipton, A., & Zoonekynd, V. (2025).
|
|
9
|
+
"How to Use the Sharpe Ratio." ADIA Lab Research Paper Series, No. 19.
|
|
10
|
+
Equation 11, page 9.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Literal
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
from numpy.typing import ArrayLike
|
|
21
|
+
from scipy.stats import norm
|
|
22
|
+
|
|
23
|
+
from ml4t.diagnostic.evaluation.stats.moments import compute_return_statistics
|
|
24
|
+
from ml4t.diagnostic.evaluation.stats.sharpe_inference import compute_expected_max_sharpe
|
|
25
|
+
|
|
26
|
+
# Type alias
|
|
27
|
+
Frequency = Literal["daily", "weekly", "monthly"]
|
|
28
|
+
|
|
29
|
+
# Default trading periods per year
|
|
30
|
+
DEFAULT_PERIODS_PER_YEAR: dict[str, int] = {
|
|
31
|
+
"daily": 252,
|
|
32
|
+
"weekly": 52,
|
|
33
|
+
"monthly": 12,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class MinTRLResult:
|
|
39
|
+
"""Result of Minimum Track Record Length calculation.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
min_trl : float
|
|
44
|
+
Minimum observations needed to reject null at specified confidence.
|
|
45
|
+
Can be math.inf if observed SR <= target SR.
|
|
46
|
+
min_trl_years : float
|
|
47
|
+
Minimum track record in calendar years. Can be math.inf.
|
|
48
|
+
current_samples : int
|
|
49
|
+
Current number of observations.
|
|
50
|
+
has_adequate_sample : bool
|
|
51
|
+
Whether current_samples >= min_trl.
|
|
52
|
+
deficit : float
|
|
53
|
+
Additional observations needed (0 if adequate). Can be math.inf.
|
|
54
|
+
deficit_years : float
|
|
55
|
+
Additional years needed (0 if adequate). Can be math.inf.
|
|
56
|
+
observed_sharpe : float
|
|
57
|
+
The observed Sharpe ratio used in calculation.
|
|
58
|
+
target_sharpe : float
|
|
59
|
+
The target Sharpe ratio (null hypothesis).
|
|
60
|
+
confidence_level : float
|
|
61
|
+
Confidence level for the test (e.g., 0.95).
|
|
62
|
+
skewness : float
|
|
63
|
+
Skewness of returns (0 for normal).
|
|
64
|
+
excess_kurtosis : float
|
|
65
|
+
Excess kurtosis of returns (Fisher convention: 0 for normal).
|
|
66
|
+
autocorrelation : float
|
|
67
|
+
Lag-1 autocorrelation of returns.
|
|
68
|
+
frequency : str
|
|
69
|
+
Return frequency ('daily', 'weekly', etc.).
|
|
70
|
+
periods_per_year : int
|
|
71
|
+
Periods per year for annualization.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
min_trl: float
|
|
75
|
+
min_trl_years: float
|
|
76
|
+
current_samples: int
|
|
77
|
+
has_adequate_sample: bool
|
|
78
|
+
deficit: float
|
|
79
|
+
deficit_years: float
|
|
80
|
+
|
|
81
|
+
# Parameters used
|
|
82
|
+
observed_sharpe: float
|
|
83
|
+
target_sharpe: float
|
|
84
|
+
confidence_level: float
|
|
85
|
+
skewness: float
|
|
86
|
+
excess_kurtosis: float
|
|
87
|
+
autocorrelation: float
|
|
88
|
+
frequency: str
|
|
89
|
+
periods_per_year: int
|
|
90
|
+
|
|
91
|
+
def interpret(self) -> str:
|
|
92
|
+
"""Generate human-readable interpretation."""
|
|
93
|
+
if math.isinf(self.min_trl):
|
|
94
|
+
return (
|
|
95
|
+
f"Minimum Track Record Length (MinTRL)\n"
|
|
96
|
+
f" Observed Sharpe: {self.observed_sharpe:.4f}\n"
|
|
97
|
+
f" Target Sharpe: {self.target_sharpe:.4f}\n"
|
|
98
|
+
f" Confidence: {self.confidence_level:.0%}\n"
|
|
99
|
+
f"\n"
|
|
100
|
+
f" MinTRL: INFINITE (observed SR <= target SR)\n"
|
|
101
|
+
f" Status: Cannot reject null hypothesis at any sample size"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if self.has_adequate_sample:
|
|
105
|
+
status = f"ADEQUATE: {self.current_samples} >= {int(self.min_trl)} observations"
|
|
106
|
+
else:
|
|
107
|
+
status = (
|
|
108
|
+
f"INSUFFICIENT: Need {int(self.deficit)} more observations "
|
|
109
|
+
f"({self.deficit_years:.1f} more years)"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
f"Minimum Track Record Length (MinTRL)\n"
|
|
114
|
+
f" Observed Sharpe: {self.observed_sharpe:.4f}\n"
|
|
115
|
+
f" Target Sharpe: {self.target_sharpe:.4f}\n"
|
|
116
|
+
f" Confidence: {self.confidence_level:.0%}\n"
|
|
117
|
+
f"\n"
|
|
118
|
+
f" MinTRL: {int(self.min_trl)} observations ({self.min_trl_years:.1f} years)\n"
|
|
119
|
+
f" Current: {self.current_samples} observations\n"
|
|
120
|
+
f" Status: {status}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _compute_min_trl_core(
|
|
125
|
+
observed_sharpe: float,
|
|
126
|
+
target_sharpe: float,
|
|
127
|
+
confidence_level: float,
|
|
128
|
+
skewness: float,
|
|
129
|
+
kurtosis: float,
|
|
130
|
+
autocorrelation: float,
|
|
131
|
+
) -> float:
|
|
132
|
+
"""Core MinTRL formula (internal).
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
observed_sharpe : float
|
|
137
|
+
Observed Sharpe ratio at native frequency
|
|
138
|
+
target_sharpe : float
|
|
139
|
+
Null hypothesis threshold (SR₀)
|
|
140
|
+
confidence_level : float
|
|
141
|
+
Required confidence level (e.g., 0.95)
|
|
142
|
+
skewness : float
|
|
143
|
+
Return skewness (γ₃)
|
|
144
|
+
kurtosis : float
|
|
145
|
+
Return kurtosis (γ₄), Pearson convention (normal = 3)
|
|
146
|
+
autocorrelation : float
|
|
147
|
+
First-order autocorrelation (ρ)
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
float
|
|
152
|
+
Minimum number of observations. Returns math.inf if
|
|
153
|
+
observed SR <= target SR.
|
|
154
|
+
"""
|
|
155
|
+
rho = autocorrelation
|
|
156
|
+
sr_diff = observed_sharpe - target_sharpe
|
|
157
|
+
|
|
158
|
+
# If observed SR <= target SR, MinTRL is infinite
|
|
159
|
+
if sr_diff <= 1e-10:
|
|
160
|
+
return float("inf")
|
|
161
|
+
|
|
162
|
+
# z-score for confidence level
|
|
163
|
+
z_alpha = norm.ppf(confidence_level)
|
|
164
|
+
|
|
165
|
+
# Coefficients (same as variance formula)
|
|
166
|
+
coef_a = 1.0
|
|
167
|
+
if rho != 0 and abs(rho) < 1:
|
|
168
|
+
coef_b = rho / (1 - rho)
|
|
169
|
+
coef_c = rho**2 / (1 - rho**2)
|
|
170
|
+
else:
|
|
171
|
+
coef_b = 0.0
|
|
172
|
+
coef_c = 0.0
|
|
173
|
+
|
|
174
|
+
a = coef_a + 2 * coef_b
|
|
175
|
+
b = coef_a + coef_b + coef_c
|
|
176
|
+
c = coef_a + 2 * coef_c
|
|
177
|
+
|
|
178
|
+
# Variance term (without 1/T factor)
|
|
179
|
+
var_term = a - b * skewness * target_sharpe + c * (kurtosis - 1) / 4 * target_sharpe**2
|
|
180
|
+
|
|
181
|
+
# MinTRL formula (Equation 11)
|
|
182
|
+
try:
|
|
183
|
+
min_trl = var_term * (z_alpha / sr_diff) ** 2
|
|
184
|
+
if np.isinf(min_trl):
|
|
185
|
+
return float("inf")
|
|
186
|
+
return float(np.ceil(max(min_trl, 1)))
|
|
187
|
+
except (OverflowError, FloatingPointError):
|
|
188
|
+
return float("inf")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def compute_min_trl(
|
|
192
|
+
returns: ArrayLike | None = None,
|
|
193
|
+
observed_sharpe: float | None = None,
|
|
194
|
+
target_sharpe: float = 0.0,
|
|
195
|
+
confidence_level: float = 0.95,
|
|
196
|
+
frequency: Frequency = "daily",
|
|
197
|
+
periods_per_year: int | None = None,
|
|
198
|
+
*,
|
|
199
|
+
skewness: float | None = None,
|
|
200
|
+
excess_kurtosis: float | None = None,
|
|
201
|
+
autocorrelation: float | None = None,
|
|
202
|
+
) -> MinTRLResult:
|
|
203
|
+
"""Compute Minimum Track Record Length (MinTRL).
|
|
204
|
+
|
|
205
|
+
MinTRL is the minimum number of observations required to reject the null
|
|
206
|
+
hypothesis (SR <= target) at the specified confidence level.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
returns : array-like, optional
|
|
211
|
+
Return series. If provided, statistics are computed from it.
|
|
212
|
+
observed_sharpe : float, optional
|
|
213
|
+
Observed Sharpe ratio. Required if returns not provided.
|
|
214
|
+
target_sharpe : float, default 0.0
|
|
215
|
+
Null hypothesis threshold (SR₀).
|
|
216
|
+
confidence_level : float, default 0.95
|
|
217
|
+
Required confidence level (1 - α).
|
|
218
|
+
frequency : {"daily", "weekly", "monthly"}, default "daily"
|
|
219
|
+
Return frequency.
|
|
220
|
+
periods_per_year : int, optional
|
|
221
|
+
Periods per year (for converting to calendar time).
|
|
222
|
+
skewness : float, optional
|
|
223
|
+
Override computed skewness.
|
|
224
|
+
excess_kurtosis : float, optional
|
|
225
|
+
Override computed excess kurtosis (Fisher convention, normal=0).
|
|
226
|
+
autocorrelation : float, optional
|
|
227
|
+
Override computed autocorrelation.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
MinTRLResult
|
|
232
|
+
Results including min_trl, min_trl_years, and adequacy assessment.
|
|
233
|
+
min_trl can be math.inf if observed SR <= target SR.
|
|
234
|
+
|
|
235
|
+
Examples
|
|
236
|
+
--------
|
|
237
|
+
From returns:
|
|
238
|
+
|
|
239
|
+
>>> result = compute_min_trl(daily_returns, frequency="daily")
|
|
240
|
+
>>> print(f"Need {result.min_trl_years:.1f} years of data")
|
|
241
|
+
|
|
242
|
+
From statistics:
|
|
243
|
+
|
|
244
|
+
>>> result = compute_min_trl(
|
|
245
|
+
... observed_sharpe=0.5,
|
|
246
|
+
... target_sharpe=0.0,
|
|
247
|
+
... confidence_level=0.95,
|
|
248
|
+
... skewness=-1.0,
|
|
249
|
+
... excess_kurtosis=2.0,
|
|
250
|
+
... autocorrelation=0.1,
|
|
251
|
+
... )
|
|
252
|
+
"""
|
|
253
|
+
# Resolve periods per year
|
|
254
|
+
if periods_per_year is None:
|
|
255
|
+
periods_per_year = DEFAULT_PERIODS_PER_YEAR[frequency]
|
|
256
|
+
|
|
257
|
+
# Get statistics from returns or use provided values
|
|
258
|
+
if returns is not None:
|
|
259
|
+
ret_arr = np.asarray(returns).flatten()
|
|
260
|
+
ret_arr = ret_arr[~np.isnan(ret_arr)]
|
|
261
|
+
obs_sr, comp_skew, comp_kurt, comp_rho, n_samples = compute_return_statistics(ret_arr)
|
|
262
|
+
|
|
263
|
+
if observed_sharpe is None:
|
|
264
|
+
observed_sharpe = obs_sr
|
|
265
|
+
else:
|
|
266
|
+
if observed_sharpe is None:
|
|
267
|
+
raise ValueError("Either returns or observed_sharpe must be provided")
|
|
268
|
+
n_samples = 0 # Unknown
|
|
269
|
+
comp_skew = 0.0
|
|
270
|
+
comp_kurt = 3.0 # Pearson
|
|
271
|
+
comp_rho = 0.0
|
|
272
|
+
|
|
273
|
+
# Use provided or computed statistics
|
|
274
|
+
skew = skewness if skewness is not None else comp_skew
|
|
275
|
+
if excess_kurtosis is not None:
|
|
276
|
+
kurt = excess_kurtosis + 3.0 # Fisher -> Pearson
|
|
277
|
+
else:
|
|
278
|
+
kurt = comp_kurt
|
|
279
|
+
rho = autocorrelation if autocorrelation is not None else comp_rho
|
|
280
|
+
|
|
281
|
+
# Compute MinTRL
|
|
282
|
+
min_trl = _compute_min_trl_core(
|
|
283
|
+
observed_sharpe=observed_sharpe,
|
|
284
|
+
target_sharpe=target_sharpe,
|
|
285
|
+
confidence_level=confidence_level,
|
|
286
|
+
skewness=skew,
|
|
287
|
+
kurtosis=kurt,
|
|
288
|
+
autocorrelation=rho,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
is_inf = math.isinf(min_trl)
|
|
292
|
+
min_trl_years = float("inf") if is_inf else min_trl / periods_per_year
|
|
293
|
+
has_adequate = False if is_inf or n_samples == 0 else n_samples >= min_trl
|
|
294
|
+
deficit = (
|
|
295
|
+
float("inf") if is_inf else max(0.0, min_trl - n_samples) if n_samples > 0 else min_trl
|
|
296
|
+
)
|
|
297
|
+
deficit_years = float("inf") if is_inf else deficit / periods_per_year
|
|
298
|
+
|
|
299
|
+
return MinTRLResult(
|
|
300
|
+
min_trl=min_trl,
|
|
301
|
+
min_trl_years=float(min_trl_years),
|
|
302
|
+
current_samples=n_samples,
|
|
303
|
+
has_adequate_sample=has_adequate,
|
|
304
|
+
deficit=deficit,
|
|
305
|
+
deficit_years=float(deficit_years),
|
|
306
|
+
observed_sharpe=float(observed_sharpe),
|
|
307
|
+
target_sharpe=target_sharpe,
|
|
308
|
+
confidence_level=confidence_level,
|
|
309
|
+
skewness=float(skew),
|
|
310
|
+
excess_kurtosis=float(kurt - 3.0),
|
|
311
|
+
autocorrelation=float(rho),
|
|
312
|
+
frequency=frequency,
|
|
313
|
+
periods_per_year=periods_per_year,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def min_trl_fwer(
|
|
318
|
+
observed_sharpe: float,
|
|
319
|
+
n_trials: int,
|
|
320
|
+
variance_trials: float,
|
|
321
|
+
target_sharpe: float = 0.0,
|
|
322
|
+
confidence_level: float = 0.95,
|
|
323
|
+
frequency: Frequency = "daily",
|
|
324
|
+
periods_per_year: int | None = None,
|
|
325
|
+
*,
|
|
326
|
+
skewness: float = 0.0,
|
|
327
|
+
excess_kurtosis: float = 0.0,
|
|
328
|
+
autocorrelation: float = 0.0,
|
|
329
|
+
) -> MinTRLResult:
|
|
330
|
+
"""Compute MinTRL under FWER multiple testing adjustment.
|
|
331
|
+
|
|
332
|
+
When selecting the best strategy from K trials, the MinTRL must be adjusted
|
|
333
|
+
to account for the selection bias.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
observed_sharpe : float
|
|
338
|
+
Observed Sharpe ratio of the best strategy.
|
|
339
|
+
n_trials : int
|
|
340
|
+
Number of strategies tested (K).
|
|
341
|
+
variance_trials : float
|
|
342
|
+
Cross-sectional variance of Sharpe ratios.
|
|
343
|
+
target_sharpe : float, default 0.0
|
|
344
|
+
Original null hypothesis threshold.
|
|
345
|
+
confidence_level : float, default 0.95
|
|
346
|
+
Required confidence level.
|
|
347
|
+
frequency : {"daily", "weekly", "monthly"}, default "daily"
|
|
348
|
+
Return frequency.
|
|
349
|
+
periods_per_year : int, optional
|
|
350
|
+
Periods per year.
|
|
351
|
+
skewness : float, default 0.0
|
|
352
|
+
Return skewness.
|
|
353
|
+
excess_kurtosis : float, default 0.0
|
|
354
|
+
Return excess kurtosis (Fisher, normal=0).
|
|
355
|
+
autocorrelation : float, default 0.0
|
|
356
|
+
Return autocorrelation.
|
|
357
|
+
|
|
358
|
+
Returns
|
|
359
|
+
-------
|
|
360
|
+
MinTRLResult
|
|
361
|
+
Results with min_trl adjusted for multiple testing.
|
|
362
|
+
"""
|
|
363
|
+
if periods_per_year is None:
|
|
364
|
+
periods_per_year = DEFAULT_PERIODS_PER_YEAR[frequency]
|
|
365
|
+
|
|
366
|
+
kurtosis = excess_kurtosis + 3.0
|
|
367
|
+
|
|
368
|
+
# Compute expected max Sharpe (selection bias adjustment)
|
|
369
|
+
expected_max = compute_expected_max_sharpe(n_trials, variance_trials)
|
|
370
|
+
adjusted_target = target_sharpe + expected_max
|
|
371
|
+
|
|
372
|
+
# Compute MinTRL with adjusted target
|
|
373
|
+
min_trl = _compute_min_trl_core(
|
|
374
|
+
observed_sharpe=observed_sharpe,
|
|
375
|
+
target_sharpe=adjusted_target,
|
|
376
|
+
confidence_level=confidence_level,
|
|
377
|
+
skewness=skewness,
|
|
378
|
+
kurtosis=kurtosis,
|
|
379
|
+
autocorrelation=autocorrelation,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
is_inf = math.isinf(min_trl)
|
|
383
|
+
min_trl_years = float("inf") if is_inf else min_trl / periods_per_year
|
|
384
|
+
|
|
385
|
+
return MinTRLResult(
|
|
386
|
+
min_trl=min_trl,
|
|
387
|
+
min_trl_years=float(min_trl_years),
|
|
388
|
+
current_samples=0,
|
|
389
|
+
has_adequate_sample=False,
|
|
390
|
+
deficit=min_trl,
|
|
391
|
+
deficit_years=float(min_trl_years),
|
|
392
|
+
observed_sharpe=float(observed_sharpe),
|
|
393
|
+
target_sharpe=float(adjusted_target),
|
|
394
|
+
confidence_level=confidence_level,
|
|
395
|
+
skewness=float(skewness),
|
|
396
|
+
excess_kurtosis=float(excess_kurtosis),
|
|
397
|
+
autocorrelation=float(autocorrelation),
|
|
398
|
+
frequency=frequency,
|
|
399
|
+
periods_per_year=periods_per_year,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
__all__ = [
|
|
404
|
+
"MinTRLResult",
|
|
405
|
+
"compute_min_trl",
|
|
406
|
+
"min_trl_fwer",
|
|
407
|
+
"DEFAULT_PERIODS_PER_YEAR",
|
|
408
|
+
]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Return statistics computation for Sharpe ratio analysis.
|
|
2
|
+
|
|
3
|
+
This module provides functions for computing the statistical moments
|
|
4
|
+
needed for Sharpe ratio inference: mean, std, skewness, kurtosis,
|
|
5
|
+
and autocorrelation.
|
|
6
|
+
|
|
7
|
+
These are the building blocks for DSR/PSR calculations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from numpy.typing import ArrayLike
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def compute_return_statistics(
|
|
17
|
+
returns: ArrayLike,
|
|
18
|
+
) -> tuple[float, float, float, float, int]:
|
|
19
|
+
"""Compute Sharpe ratio and distribution statistics from returns.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
returns : array-like
|
|
24
|
+
Array of returns (not prices). NaN values are removed.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
tuple of (sharpe, skewness, kurtosis, autocorrelation, n_samples)
|
|
29
|
+
- sharpe: Sharpe ratio (mean/std) at native frequency
|
|
30
|
+
- skewness: Fisher's skewness (γ₃)
|
|
31
|
+
- kurtosis: Pearson kurtosis (γ₄), normal = 3
|
|
32
|
+
- autocorrelation: Lag-1 autocorrelation (ρ)
|
|
33
|
+
- n_samples: Number of valid observations
|
|
34
|
+
|
|
35
|
+
Raises
|
|
36
|
+
------
|
|
37
|
+
ValueError
|
|
38
|
+
If fewer than 2 observations or zero variance.
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
Kurtosis is returned in Pearson convention (normal=3) for internal use.
|
|
43
|
+
Convert to Fisher (normal=0) for public API: excess_kurtosis = kurtosis - 3.
|
|
44
|
+
"""
|
|
45
|
+
returns = np.asarray(returns).flatten()
|
|
46
|
+
returns = returns[~np.isnan(returns)]
|
|
47
|
+
|
|
48
|
+
n = len(returns)
|
|
49
|
+
if n < 2:
|
|
50
|
+
raise ValueError("Need at least 2 return observations")
|
|
51
|
+
|
|
52
|
+
mean = np.mean(returns)
|
|
53
|
+
std = np.std(returns, ddof=1)
|
|
54
|
+
|
|
55
|
+
if std == 0:
|
|
56
|
+
raise ValueError("Return series has zero variance")
|
|
57
|
+
|
|
58
|
+
sharpe = mean / std
|
|
59
|
+
|
|
60
|
+
# Skewness (γ₃) - Fisher's definition
|
|
61
|
+
skewness = float(((returns - mean) ** 3).mean() / std**3)
|
|
62
|
+
|
|
63
|
+
# Kurtosis (γ₄) - Pearson (normal = 3)
|
|
64
|
+
kurtosis = float(((returns - mean) ** 4).mean() / std**4)
|
|
65
|
+
|
|
66
|
+
# First-order autocorrelation (ρ)
|
|
67
|
+
if n > 2:
|
|
68
|
+
autocorr = np.corrcoef(returns[:-1], returns[1:])[0, 1]
|
|
69
|
+
if np.isnan(autocorr):
|
|
70
|
+
autocorr = 0.0
|
|
71
|
+
else:
|
|
72
|
+
autocorr = 0.0
|
|
73
|
+
|
|
74
|
+
return float(sharpe), skewness, kurtosis, float(autocorr), n
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compute_sharpe(returns: ArrayLike) -> float:
|
|
78
|
+
"""Compute Sharpe ratio from returns.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
returns : array-like
|
|
83
|
+
Array of returns.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
float
|
|
88
|
+
Sharpe ratio (mean/std) at native frequency.
|
|
89
|
+
"""
|
|
90
|
+
sharpe, _, _, _, _ = compute_return_statistics(returns)
|
|
91
|
+
return sharpe
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def compute_skewness(returns: ArrayLike) -> float:
|
|
95
|
+
"""Compute skewness from returns.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
returns : array-like
|
|
100
|
+
Array of returns.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
float
|
|
105
|
+
Fisher's skewness (γ₃).
|
|
106
|
+
"""
|
|
107
|
+
_, skewness, _, _, _ = compute_return_statistics(returns)
|
|
108
|
+
return skewness
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def compute_kurtosis(returns: ArrayLike, excess: bool = True) -> float:
|
|
112
|
+
"""Compute kurtosis from returns.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
returns : array-like
|
|
117
|
+
Array of returns.
|
|
118
|
+
excess : bool, default True
|
|
119
|
+
If True, return Fisher/excess kurtosis (normal=0).
|
|
120
|
+
If False, return Pearson kurtosis (normal=3).
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
float
|
|
125
|
+
Kurtosis value.
|
|
126
|
+
"""
|
|
127
|
+
_, _, kurtosis, _, _ = compute_return_statistics(returns)
|
|
128
|
+
return kurtosis - 3.0 if excess else kurtosis
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def compute_autocorrelation(returns: ArrayLike, lag: int = 1) -> float:
|
|
132
|
+
"""Compute autocorrelation from returns.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
returns : array-like
|
|
137
|
+
Array of returns.
|
|
138
|
+
lag : int, default 1
|
|
139
|
+
Lag for autocorrelation. Currently only lag=1 is supported.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
float
|
|
144
|
+
Autocorrelation at specified lag.
|
|
145
|
+
|
|
146
|
+
Raises
|
|
147
|
+
------
|
|
148
|
+
ValueError
|
|
149
|
+
If lag != 1 (not yet implemented).
|
|
150
|
+
"""
|
|
151
|
+
if lag != 1:
|
|
152
|
+
raise ValueError("Only lag=1 autocorrelation is currently supported")
|
|
153
|
+
|
|
154
|
+
_, _, _, autocorr, _ = compute_return_statistics(returns)
|
|
155
|
+
return autocorr
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = [
|
|
159
|
+
"compute_return_statistics",
|
|
160
|
+
"compute_sharpe",
|
|
161
|
+
"compute_skewness",
|
|
162
|
+
"compute_kurtosis",
|
|
163
|
+
"compute_autocorrelation",
|
|
164
|
+
]
|