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,219 @@
|
|
|
1
|
+
"""Sharpe ratio inference: variance estimation and multiple testing adjustment.
|
|
2
|
+
|
|
3
|
+
This module implements the statistical inference framework for Sharpe ratios:
|
|
4
|
+
|
|
5
|
+
- Variance of Sharpe ratio estimator (2025 formula with autocorrelation)
|
|
6
|
+
- Expected maximum Sharpe under null (for multiple testing)
|
|
7
|
+
- Variance rescaling factors for selection bias
|
|
8
|
+
|
|
9
|
+
References
|
|
10
|
+
----------
|
|
11
|
+
López de Prado, M., Lipton, A., & Zoonekynd, V. (2025).
|
|
12
|
+
"How to Use the Sharpe Ratio." ADIA Lab Research Paper Series, No. 19.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from scipy.stats import norm
|
|
19
|
+
|
|
20
|
+
# Euler-Mascheroni constant for E[max{Z}] calculation
|
|
21
|
+
EULER_GAMMA = 0.5772156649015329
|
|
22
|
+
|
|
23
|
+
# Standard deviation rescaling factors for maximum of K standard normals
|
|
24
|
+
# Source: López de Prado et al. (2025), Exhibit 3, page 13
|
|
25
|
+
# These are √V[max{X_k}] values for DSR variance adjustment
|
|
26
|
+
VARIANCE_RESCALING_FACTORS: dict[int, float] = {
|
|
27
|
+
1: 1.00000,
|
|
28
|
+
2: 0.82565,
|
|
29
|
+
3: 0.74798,
|
|
30
|
+
4: 0.70122,
|
|
31
|
+
5: 0.66898,
|
|
32
|
+
6: 0.64492,
|
|
33
|
+
7: 0.62603,
|
|
34
|
+
8: 0.61065,
|
|
35
|
+
9: 0.59779,
|
|
36
|
+
10: 0.58681,
|
|
37
|
+
20: 0.52131,
|
|
38
|
+
30: 0.49364,
|
|
39
|
+
40: 0.47599,
|
|
40
|
+
50: 0.46334,
|
|
41
|
+
60: 0.45361,
|
|
42
|
+
70: 0.44579,
|
|
43
|
+
80: 0.43929,
|
|
44
|
+
90: 0.43376,
|
|
45
|
+
100: 0.42942,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_variance_rescaling_factor(k: int) -> float:
|
|
50
|
+
"""Get variance rescaling factor √V[max{X_k}] for K trials.
|
|
51
|
+
|
|
52
|
+
Source: López de Prado et al. (2025), Exhibit 3, page 13.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
k : int
|
|
57
|
+
Number of independent trials
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
float
|
|
62
|
+
Rescaling factor (uses linear interpolation for unlisted values)
|
|
63
|
+
"""
|
|
64
|
+
if k in VARIANCE_RESCALING_FACTORS:
|
|
65
|
+
return VARIANCE_RESCALING_FACTORS[k]
|
|
66
|
+
|
|
67
|
+
keys = sorted(VARIANCE_RESCALING_FACTORS.keys())
|
|
68
|
+
if k < keys[0]:
|
|
69
|
+
return VARIANCE_RESCALING_FACTORS[keys[0]]
|
|
70
|
+
if k > keys[-1]:
|
|
71
|
+
return VARIANCE_RESCALING_FACTORS[keys[-1]]
|
|
72
|
+
|
|
73
|
+
# Linear interpolation
|
|
74
|
+
for i in range(len(keys) - 1):
|
|
75
|
+
if keys[i] <= k <= keys[i + 1]:
|
|
76
|
+
k1, k2 = keys[i], keys[i + 1]
|
|
77
|
+
v1, v2 = VARIANCE_RESCALING_FACTORS[k1], VARIANCE_RESCALING_FACTORS[k2]
|
|
78
|
+
return v1 + (v2 - v1) * (k - k1) / (k2 - k1)
|
|
79
|
+
|
|
80
|
+
return VARIANCE_RESCALING_FACTORS[keys[-1]]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_sharpe_variance(
|
|
84
|
+
sharpe: float,
|
|
85
|
+
n_samples: int,
|
|
86
|
+
skewness: float,
|
|
87
|
+
kurtosis: float,
|
|
88
|
+
autocorrelation: float,
|
|
89
|
+
n_trials: int = 1,
|
|
90
|
+
) -> float:
|
|
91
|
+
"""Compute variance of Sharpe ratio estimator.
|
|
92
|
+
|
|
93
|
+
Implements the full 2025 formula with autocorrelation correction:
|
|
94
|
+
|
|
95
|
+
.. math::
|
|
96
|
+
|
|
97
|
+
\\sigma^2[\\widehat{SR}] = \\frac{1}{T} \\left[
|
|
98
|
+
\\frac{1+\\rho}{1-\\rho}
|
|
99
|
+
- \\left(1 + \\frac{\\rho}{1-\\rho} + \\frac{\\rho^2}{1-\\rho^2}\\right) \\gamma_3 SR
|
|
100
|
+
+ \\frac{1+\\rho^2}{1-\\rho^2} \\frac{\\gamma_4 - 1}{4} SR^2
|
|
101
|
+
\\right] \\times V[\\max_k\\{X_k\\}]
|
|
102
|
+
|
|
103
|
+
When ρ=0 (i.i.d. assumption), this reduces to the 2014 formula:
|
|
104
|
+
|
|
105
|
+
.. math::
|
|
106
|
+
|
|
107
|
+
\\sigma^2[\\widehat{SR}] = \\frac{1}{T} \\left[
|
|
108
|
+
1 - \\gamma_3 SR + \\frac{\\gamma_4 - 1}{4} SR^2
|
|
109
|
+
\\right]
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
sharpe : float
|
|
114
|
+
Sharpe ratio (at native frequency)
|
|
115
|
+
n_samples : int
|
|
116
|
+
Number of observations (T)
|
|
117
|
+
skewness : float
|
|
118
|
+
Return skewness (γ₃)
|
|
119
|
+
kurtosis : float
|
|
120
|
+
Return kurtosis (γ₄), Pearson convention (normal = 3)
|
|
121
|
+
autocorrelation : float
|
|
122
|
+
First-order autocorrelation (ρ), must be in (-1, 1)
|
|
123
|
+
n_trials : int, default 1
|
|
124
|
+
Number of strategies (K) for variance rescaling
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
float
|
|
129
|
+
Variance of Sharpe ratio estimator
|
|
130
|
+
|
|
131
|
+
Raises
|
|
132
|
+
------
|
|
133
|
+
ValueError
|
|
134
|
+
If autocorrelation is not in (-1, 1).
|
|
135
|
+
|
|
136
|
+
References
|
|
137
|
+
----------
|
|
138
|
+
López de Prado et al. (2025), Equations 2-5, pages 5-7.
|
|
139
|
+
"""
|
|
140
|
+
rho = autocorrelation
|
|
141
|
+
|
|
142
|
+
# Validate autocorrelation
|
|
143
|
+
if abs(rho) >= 1.0:
|
|
144
|
+
raise ValueError(f"Autocorrelation must be in (-1, 1), got {rho}")
|
|
145
|
+
|
|
146
|
+
# Compute coefficients (from reference implementation)
|
|
147
|
+
# When rho=0: coef_a=1, coef_b=0, coef_c=0, so a=1, b=1, c=1 (reduces to 2014 formula)
|
|
148
|
+
coef_a = 1.0
|
|
149
|
+
if rho != 0:
|
|
150
|
+
coef_b = rho / (1 - rho)
|
|
151
|
+
coef_c = rho**2 / (1 - rho**2)
|
|
152
|
+
else:
|
|
153
|
+
coef_b = 0.0
|
|
154
|
+
coef_c = 0.0
|
|
155
|
+
|
|
156
|
+
a = coef_a + 2 * coef_b # = (1+ρ)/(1-ρ) - base term coefficient
|
|
157
|
+
b = coef_a + coef_b + coef_c # = 1 + ρ/(1-ρ) + ρ²/(1-ρ²) - skewness coefficient
|
|
158
|
+
c = coef_a + 2 * coef_c # = (1+ρ²)/(1-ρ²) - kurtosis coefficient
|
|
159
|
+
|
|
160
|
+
# Variance formula (Equation 5)
|
|
161
|
+
variance = (a - b * skewness * sharpe + c * (kurtosis - 1) / 4 * sharpe**2) / n_samples
|
|
162
|
+
|
|
163
|
+
# Apply variance rescaling for multiple testing (Equation 29)
|
|
164
|
+
if n_trials > 1:
|
|
165
|
+
rescaling_factor = get_variance_rescaling_factor(n_trials)
|
|
166
|
+
variance *= rescaling_factor**2
|
|
167
|
+
|
|
168
|
+
return max(variance, 0.0) # Ensure non-negative
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def compute_expected_max_sharpe(n_trials: int, variance_trials: float) -> float:
|
|
172
|
+
"""Compute expected maximum Sharpe ratio under null hypothesis.
|
|
173
|
+
|
|
174
|
+
.. math::
|
|
175
|
+
|
|
176
|
+
E[\\max_k\\{\\widehat{SR}_k\\}] \\approx \\sqrt{V[\\{\\widehat{SR}_k\\}]}
|
|
177
|
+
\\left((1-\\gamma) \\Phi^{-1}(1-1/K) + \\gamma \\Phi^{-1}(1-1/(Ke))\\right)
|
|
178
|
+
|
|
179
|
+
where γ is the Euler-Mascheroni constant ≈ 0.5772.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
n_trials : int
|
|
184
|
+
Number of strategies tested (K)
|
|
185
|
+
variance_trials : float
|
|
186
|
+
Cross-sectional variance of Sharpe ratios: Var[{SR_1, ..., SR_K}]
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
float
|
|
191
|
+
Expected maximum Sharpe ratio E[max{SR}]
|
|
192
|
+
|
|
193
|
+
References
|
|
194
|
+
----------
|
|
195
|
+
López de Prado et al. (2025), Equation 26, page 13.
|
|
196
|
+
Bailey & López de Prado (2014), Appendix A.1, Equation 6.
|
|
197
|
+
"""
|
|
198
|
+
if n_trials <= 1:
|
|
199
|
+
return 0.0
|
|
200
|
+
|
|
201
|
+
if variance_trials <= 0:
|
|
202
|
+
return 0.0
|
|
203
|
+
|
|
204
|
+
# E[max{Z}] for K i.i.d. standard normals (Equation 26)
|
|
205
|
+
quantile_1 = norm.ppf(1 - 1 / n_trials)
|
|
206
|
+
quantile_2 = norm.ppf(1 - 1 / (n_trials * np.e))
|
|
207
|
+
e_max_z = (1 - EULER_GAMMA) * quantile_1 + EULER_GAMMA * quantile_2
|
|
208
|
+
|
|
209
|
+
# Scale by standard deviation of Sharpe ratios across trials
|
|
210
|
+
return float(np.sqrt(variance_trials) * e_max_z)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
__all__ = [
|
|
214
|
+
"EULER_GAMMA",
|
|
215
|
+
"VARIANCE_RESCALING_FACTORS",
|
|
216
|
+
"get_variance_rescaling_factor",
|
|
217
|
+
"compute_sharpe_variance",
|
|
218
|
+
"compute_expected_max_sharpe",
|
|
219
|
+
]
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Plotly themes and styling for ml4t-diagnostic visualizations.
|
|
2
|
+
|
|
3
|
+
This module provides consistent theming across all ml4t-diagnostic visualizations,
|
|
4
|
+
including color schemes, layout templates, and accessibility options.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
# Financial color schemes
|
|
10
|
+
FINANCIAL_COLORS = {
|
|
11
|
+
# Returns and performance
|
|
12
|
+
"positive": "#00CC88", # Green for gains
|
|
13
|
+
"negative": "#FF4444", # Red for losses
|
|
14
|
+
"neutral": "#888888", # Gray for neutral
|
|
15
|
+
# Data series
|
|
16
|
+
"primary": "#3366CC", # Blue
|
|
17
|
+
"secondary": "#FF9900", # Orange
|
|
18
|
+
"tertiary": "#109618", # Dark green
|
|
19
|
+
"quaternary": "#990099", # Purple
|
|
20
|
+
# UI elements
|
|
21
|
+
"background": "#F8F9FA", # Light gray
|
|
22
|
+
"paper": "#FFFFFF", # White
|
|
23
|
+
"grid": "#E0E0E0", # Grid lines
|
|
24
|
+
"text": "#333333", # Dark gray text
|
|
25
|
+
"subtitle": "#666666", # Medium gray
|
|
26
|
+
# Quantiles (5-level)
|
|
27
|
+
"q1": "#D32F2F", # Dark red (lowest)
|
|
28
|
+
"q2": "#F57C00", # Orange
|
|
29
|
+
"q3": "#FBC02D", # Yellow
|
|
30
|
+
"q4": "#689F38", # Light green
|
|
31
|
+
"q5": "#388E3C", # Dark green (highest)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Colorblind-friendly palette
|
|
35
|
+
COLORBLIND_SAFE = {
|
|
36
|
+
"blue": "#0173B2",
|
|
37
|
+
"orange": "#DE8F05",
|
|
38
|
+
"green": "#029E73",
|
|
39
|
+
"red": "#CC78BC",
|
|
40
|
+
"purple": "#5B4E96",
|
|
41
|
+
"brown": "#A65628",
|
|
42
|
+
"pink": "#F0E442",
|
|
43
|
+
"gray": "#999999",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Layout templates
|
|
47
|
+
DEFAULT_TEMPLATE = {
|
|
48
|
+
"layout": {
|
|
49
|
+
# Typography
|
|
50
|
+
"font": {
|
|
51
|
+
"family": "Arial, Helvetica, sans-serif",
|
|
52
|
+
"size": 12,
|
|
53
|
+
"color": FINANCIAL_COLORS["text"],
|
|
54
|
+
},
|
|
55
|
+
"title": {
|
|
56
|
+
"font": {"size": 16, "color": FINANCIAL_COLORS["text"]},
|
|
57
|
+
"x": 0.5,
|
|
58
|
+
"xanchor": "center",
|
|
59
|
+
},
|
|
60
|
+
# Colors
|
|
61
|
+
"plot_bgcolor": FINANCIAL_COLORS["background"],
|
|
62
|
+
"paper_bgcolor": FINANCIAL_COLORS["paper"],
|
|
63
|
+
# Margins
|
|
64
|
+
"margin": {"l": 60, "r": 30, "t": 60, "b": 60},
|
|
65
|
+
# Hover
|
|
66
|
+
"hovermode": "closest",
|
|
67
|
+
"hoverlabel": {"bgcolor": "white", "font_size": 12, "font_family": "Arial"},
|
|
68
|
+
# Grid and axes
|
|
69
|
+
"xaxis": {
|
|
70
|
+
"gridcolor": FINANCIAL_COLORS["grid"],
|
|
71
|
+
"zeroline": False,
|
|
72
|
+
"showgrid": True,
|
|
73
|
+
"showline": True,
|
|
74
|
+
"linecolor": FINANCIAL_COLORS["grid"],
|
|
75
|
+
"tickfont": {"size": 11},
|
|
76
|
+
},
|
|
77
|
+
"yaxis": {
|
|
78
|
+
"gridcolor": FINANCIAL_COLORS["grid"],
|
|
79
|
+
"zeroline": True,
|
|
80
|
+
"zerolinecolor": FINANCIAL_COLORS["grid"],
|
|
81
|
+
"showgrid": True,
|
|
82
|
+
"showline": True,
|
|
83
|
+
"linecolor": FINANCIAL_COLORS["grid"],
|
|
84
|
+
"tickfont": {"size": 11},
|
|
85
|
+
},
|
|
86
|
+
# Legend
|
|
87
|
+
"legend": {
|
|
88
|
+
"bgcolor": "rgba(255,255,255,0.9)",
|
|
89
|
+
"bordercolor": FINANCIAL_COLORS["grid"],
|
|
90
|
+
"borderwidth": 1,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Dark theme for dashboards
|
|
96
|
+
DARK_TEMPLATE = {
|
|
97
|
+
"layout": {
|
|
98
|
+
# Typography
|
|
99
|
+
"font": {"family": "Arial, Helvetica, sans-serif", "size": 12, "color": "#E0E0E0"},
|
|
100
|
+
"title": {"font": {"size": 16, "color": "#FFFFFF"}, "x": 0.5, "xanchor": "center"},
|
|
101
|
+
# Colors
|
|
102
|
+
"plot_bgcolor": "#1E1E1E",
|
|
103
|
+
"paper_bgcolor": "#121212",
|
|
104
|
+
# Grid and axes
|
|
105
|
+
"xaxis": {
|
|
106
|
+
"gridcolor": "#333333",
|
|
107
|
+
"zeroline": False,
|
|
108
|
+
"showgrid": True,
|
|
109
|
+
"showline": True,
|
|
110
|
+
"linecolor": "#444444",
|
|
111
|
+
"tickfont": {"color": "#B0B0B0"},
|
|
112
|
+
},
|
|
113
|
+
"yaxis": {
|
|
114
|
+
"gridcolor": "#333333",
|
|
115
|
+
"zeroline": True,
|
|
116
|
+
"zerolinecolor": "#444444",
|
|
117
|
+
"showgrid": True,
|
|
118
|
+
"showline": True,
|
|
119
|
+
"linecolor": "#444444",
|
|
120
|
+
"tickfont": {"color": "#B0B0B0"},
|
|
121
|
+
},
|
|
122
|
+
# Legend
|
|
123
|
+
"legend": {
|
|
124
|
+
"bgcolor": "rgba(30,30,30,0.9)",
|
|
125
|
+
"bordercolor": "#444444",
|
|
126
|
+
"borderwidth": 1,
|
|
127
|
+
"font": {"color": "#E0E0E0"},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Print-friendly template
|
|
133
|
+
PRINT_TEMPLATE = {
|
|
134
|
+
"layout": {
|
|
135
|
+
# Black and white only
|
|
136
|
+
"font": {"family": "Times New Roman, serif", "size": 10, "color": "black"},
|
|
137
|
+
"title": {"font": {"size": 14, "color": "black"}, "x": 0.5, "xanchor": "center"},
|
|
138
|
+
# White background
|
|
139
|
+
"plot_bgcolor": "white",
|
|
140
|
+
"paper_bgcolor": "white",
|
|
141
|
+
# Minimal margins for printing
|
|
142
|
+
"margin": {"l": 40, "r": 20, "t": 40, "b": 40},
|
|
143
|
+
# High contrast grid
|
|
144
|
+
"xaxis": {
|
|
145
|
+
"gridcolor": "#CCCCCC",
|
|
146
|
+
"zeroline": True,
|
|
147
|
+
"zerolinecolor": "black",
|
|
148
|
+
"showgrid": True,
|
|
149
|
+
"showline": True,
|
|
150
|
+
"linecolor": "black",
|
|
151
|
+
"linewidth": 1,
|
|
152
|
+
},
|
|
153
|
+
"yaxis": {
|
|
154
|
+
"gridcolor": "#CCCCCC",
|
|
155
|
+
"zeroline": True,
|
|
156
|
+
"zerolinecolor": "black",
|
|
157
|
+
"showgrid": True,
|
|
158
|
+
"showline": True,
|
|
159
|
+
"linecolor": "black",
|
|
160
|
+
"linewidth": 1,
|
|
161
|
+
},
|
|
162
|
+
# No shadows or effects
|
|
163
|
+
"legend": {"bgcolor": "white", "bordercolor": "black", "borderwidth": 1},
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_color_scale(n_colors: int, scheme: str = "diverging") -> list[str]:
|
|
169
|
+
"""Get a color scale for visualizations.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
n_colors : int
|
|
174
|
+
Number of colors needed
|
|
175
|
+
scheme : str, default "diverging"
|
|
176
|
+
Color scheme type: "diverging", "sequential", "quantile", "colorblind"
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
-------
|
|
180
|
+
list[str]
|
|
181
|
+
List of hex color codes
|
|
182
|
+
"""
|
|
183
|
+
if scheme == "diverging":
|
|
184
|
+
# Red to green through white
|
|
185
|
+
if n_colors == 2:
|
|
186
|
+
return [FINANCIAL_COLORS["negative"], FINANCIAL_COLORS["positive"]]
|
|
187
|
+
if n_colors == 3:
|
|
188
|
+
return [
|
|
189
|
+
FINANCIAL_COLORS["negative"],
|
|
190
|
+
FINANCIAL_COLORS["neutral"],
|
|
191
|
+
FINANCIAL_COLORS["positive"],
|
|
192
|
+
]
|
|
193
|
+
# Use plotly's RdYlGn scale
|
|
194
|
+
import plotly.colors as pc
|
|
195
|
+
|
|
196
|
+
return pc.sample_colorscale("RdYlGn", n_colors)
|
|
197
|
+
|
|
198
|
+
if scheme == "sequential":
|
|
199
|
+
# Blue gradient
|
|
200
|
+
if n_colors <= 5:
|
|
201
|
+
return ["#E3F2FD", "#90CAF9", "#42A5F5", "#1E88E5", "#0D47A1"][:n_colors]
|
|
202
|
+
import plotly.colors as pc
|
|
203
|
+
|
|
204
|
+
return pc.sample_colorscale("Blues", n_colors)
|
|
205
|
+
|
|
206
|
+
if scheme == "quantile":
|
|
207
|
+
# Specific colors for quantiles
|
|
208
|
+
quantile_colors = [
|
|
209
|
+
FINANCIAL_COLORS["q1"],
|
|
210
|
+
FINANCIAL_COLORS["q2"],
|
|
211
|
+
FINANCIAL_COLORS["q3"],
|
|
212
|
+
FINANCIAL_COLORS["q4"],
|
|
213
|
+
FINANCIAL_COLORS["q5"],
|
|
214
|
+
]
|
|
215
|
+
if n_colors <= 5:
|
|
216
|
+
return quantile_colors[:n_colors]
|
|
217
|
+
# Interpolate if more than 5
|
|
218
|
+
import plotly.colors as pc
|
|
219
|
+
|
|
220
|
+
return pc.sample_colorscale("RdYlGn", n_colors)
|
|
221
|
+
|
|
222
|
+
if scheme == "colorblind":
|
|
223
|
+
# Colorblind-safe palette
|
|
224
|
+
colors = list(COLORBLIND_SAFE.values())
|
|
225
|
+
if n_colors <= len(colors):
|
|
226
|
+
return colors[:n_colors]
|
|
227
|
+
# Cycle through if need more
|
|
228
|
+
return (colors * (n_colors // len(colors) + 1))[:n_colors]
|
|
229
|
+
|
|
230
|
+
# Default categorical
|
|
231
|
+
import plotly.express as px
|
|
232
|
+
|
|
233
|
+
return px.colors.qualitative.Set3[:n_colors]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def apply_theme(fig: Any, theme: str = "default") -> Any:
|
|
237
|
+
"""Apply a theme to a Plotly figure.
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
fig : plotly.graph_objects.Figure
|
|
242
|
+
Figure to apply theme to
|
|
243
|
+
theme : str, default "default"
|
|
244
|
+
Theme name: "default", "dark", "print", "colorblind"
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
-------
|
|
248
|
+
plotly.graph_objects.Figure
|
|
249
|
+
Figure with theme applied
|
|
250
|
+
"""
|
|
251
|
+
if theme == "default":
|
|
252
|
+
template = DEFAULT_TEMPLATE
|
|
253
|
+
elif theme == "dark":
|
|
254
|
+
template = DARK_TEMPLATE
|
|
255
|
+
elif theme == "print":
|
|
256
|
+
template = PRINT_TEMPLATE
|
|
257
|
+
elif theme == "colorblind":
|
|
258
|
+
# Apply colorblind-safe colors to existing theme
|
|
259
|
+
template = DEFAULT_TEMPLATE.copy()
|
|
260
|
+
# Would need to update trace colors here
|
|
261
|
+
else:
|
|
262
|
+
raise ValueError(f"Unknown theme: {theme}")
|
|
263
|
+
|
|
264
|
+
# Apply template
|
|
265
|
+
fig.update_layout(template["layout"])
|
|
266
|
+
|
|
267
|
+
return fig
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def format_percentage(value: float, decimals: int = 1) -> str:
|
|
271
|
+
"""Format a value as percentage for display.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
value : float
|
|
276
|
+
Value to format (0.05 = 5%)
|
|
277
|
+
decimals : int, default 1
|
|
278
|
+
Number of decimal places
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
-------
|
|
282
|
+
str
|
|
283
|
+
Formatted percentage string
|
|
284
|
+
"""
|
|
285
|
+
return f"{value * 100:.{decimals}f}%"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def format_currency(value: float, currency: str = "$", decimals: int = 0) -> str:
|
|
289
|
+
"""Format a value as currency for display.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
value : float
|
|
294
|
+
Value to format
|
|
295
|
+
currency : str, default "$"
|
|
296
|
+
Currency symbol
|
|
297
|
+
decimals : int, default 0
|
|
298
|
+
Number of decimal places
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
-------
|
|
302
|
+
str
|
|
303
|
+
Formatted currency string
|
|
304
|
+
"""
|
|
305
|
+
if decimals == 0:
|
|
306
|
+
return f"{currency}{value:,.0f}"
|
|
307
|
+
return f"{currency}{value:,.{decimals}f}"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Accessibility helpers
|
|
311
|
+
def add_pattern_overlay(fig: Any, _trace_index: int, _pattern: str = "diagonal") -> Any:
|
|
312
|
+
"""Add pattern overlay for better accessibility.
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
fig : plotly.graph_objects.Figure
|
|
317
|
+
Figure to modify
|
|
318
|
+
trace_index : int
|
|
319
|
+
Index of trace to add pattern to
|
|
320
|
+
pattern : str, default "diagonal"
|
|
321
|
+
Pattern type: "diagonal", "vertical", "horizontal", "dot"
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
-------
|
|
325
|
+
plotly.graph_objects.Figure
|
|
326
|
+
Modified figure
|
|
327
|
+
"""
|
|
328
|
+
# This would add SVG patterns for accessibility
|
|
329
|
+
# Implementation depends on Plotly version and trace type
|
|
330
|
+
return fig
|