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,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
+ ]