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,874 @@
|
|
|
1
|
+
"""Statistical validity visualizations for backtest analysis.
|
|
2
|
+
|
|
3
|
+
Provides interactive Plotly visualizations for statistical rigor:
|
|
4
|
+
- DSR (Deflated Sharpe Ratio) gauge with probability zones
|
|
5
|
+
- Confidence interval forest plots
|
|
6
|
+
- RAS (Rademacher Anti-Serum) overfitting detection
|
|
7
|
+
- MinTRL (Minimum Track Record Length) analysis
|
|
8
|
+
|
|
9
|
+
These visualizations help traders understand whether their backtest results
|
|
10
|
+
are statistically significant or likely due to overfitting/chance.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import plotly.graph_objects as go
|
|
19
|
+
|
|
20
|
+
from ml4t.diagnostic.visualization.core import get_theme_config
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def plot_dsr_gauge(
|
|
27
|
+
dsr_probability: float,
|
|
28
|
+
observed_sharpe: float,
|
|
29
|
+
expected_max_sharpe: float | None = None,
|
|
30
|
+
n_trials: int | None = None,
|
|
31
|
+
title: str = "Deflated Sharpe Ratio",
|
|
32
|
+
show_legend: bool = True,
|
|
33
|
+
theme: str | None = None,
|
|
34
|
+
height: int = 350,
|
|
35
|
+
width: int = 500,
|
|
36
|
+
) -> go.Figure:
|
|
37
|
+
"""Create a gauge chart showing DSR probability.
|
|
38
|
+
|
|
39
|
+
The Deflated Sharpe Ratio corrects for selection bias when choosing
|
|
40
|
+
the best strategy from multiple tests. A DSR probability < 0.05
|
|
41
|
+
suggests the performance is statistically significant.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
dsr_probability : float
|
|
46
|
+
DSR probability value (0-1), where lower is more significant.
|
|
47
|
+
Typically displayed as 1 - dsr for "confidence" interpretation.
|
|
48
|
+
observed_sharpe : float
|
|
49
|
+
The observed Sharpe ratio being tested
|
|
50
|
+
expected_max_sharpe : float, optional
|
|
51
|
+
The expected maximum Sharpe under null hypothesis
|
|
52
|
+
n_trials : int, optional
|
|
53
|
+
Number of trials/strategies tested (for annotation)
|
|
54
|
+
title : str
|
|
55
|
+
Chart title
|
|
56
|
+
show_legend : bool
|
|
57
|
+
Whether to show the color zone legend
|
|
58
|
+
theme : str, optional
|
|
59
|
+
Theme name (default, dark, print, presentation)
|
|
60
|
+
height : int
|
|
61
|
+
Figure height in pixels
|
|
62
|
+
width : int
|
|
63
|
+
Figure width in pixels
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
go.Figure
|
|
68
|
+
Plotly figure with gauge chart
|
|
69
|
+
|
|
70
|
+
Examples
|
|
71
|
+
--------
|
|
72
|
+
>>> fig = plot_dsr_gauge(
|
|
73
|
+
... dsr_probability=0.03,
|
|
74
|
+
... observed_sharpe=2.1,
|
|
75
|
+
... n_trials=100,
|
|
76
|
+
... )
|
|
77
|
+
>>> fig.show()
|
|
78
|
+
"""
|
|
79
|
+
theme_config = get_theme_config(theme)
|
|
80
|
+
|
|
81
|
+
# Convert to "confidence" (1 - p-value style)
|
|
82
|
+
# High confidence = good, Low confidence = bad
|
|
83
|
+
confidence = (1 - dsr_probability) * 100
|
|
84
|
+
|
|
85
|
+
# Color zones: Red (not significant) -> Yellow (marginal) -> Green (significant)
|
|
86
|
+
# Standard thresholds: p < 0.05 (95%), p < 0.01 (99%)
|
|
87
|
+
fig = go.Figure(
|
|
88
|
+
go.Indicator(
|
|
89
|
+
mode="gauge+number",
|
|
90
|
+
value=confidence,
|
|
91
|
+
number={"suffix": "%", "font": {"size": 36}},
|
|
92
|
+
title={"text": title, "font": {"size": 18}},
|
|
93
|
+
gauge={
|
|
94
|
+
"axis": {
|
|
95
|
+
"range": [0, 100],
|
|
96
|
+
"tickwidth": 1,
|
|
97
|
+
"tickcolor": "darkgray",
|
|
98
|
+
"tickvals": [0, 50, 90, 95, 99, 100],
|
|
99
|
+
"ticktext": ["0%", "50%", "90%", "95%", "99%", "100%"],
|
|
100
|
+
},
|
|
101
|
+
"bar": {"color": "darkblue"},
|
|
102
|
+
"bgcolor": "white",
|
|
103
|
+
"borderwidth": 2,
|
|
104
|
+
"bordercolor": "gray",
|
|
105
|
+
"steps": [
|
|
106
|
+
{"range": [0, 50], "color": "#EF553B"}, # Red - not significant
|
|
107
|
+
{"range": [50, 90], "color": "#FFA15A"}, # Orange - weak
|
|
108
|
+
{"range": [90, 95], "color": "#FECB52"}, # Yellow - marginal
|
|
109
|
+
{"range": [95, 99], "color": "#00CC96"}, # Green - significant
|
|
110
|
+
{"range": [99, 100], "color": "#19D3F3"}, # Cyan - highly significant
|
|
111
|
+
],
|
|
112
|
+
"threshold": {
|
|
113
|
+
"line": {"color": "black", "width": 4},
|
|
114
|
+
"thickness": 0.75,
|
|
115
|
+
"value": confidence,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Add annotations
|
|
122
|
+
annotations = []
|
|
123
|
+
|
|
124
|
+
# DSR probability annotation
|
|
125
|
+
annotations.append(
|
|
126
|
+
{
|
|
127
|
+
"x": 0.5,
|
|
128
|
+
"y": 0.25,
|
|
129
|
+
"text": f"DSR p-value: {dsr_probability:.4f}",
|
|
130
|
+
"showarrow": False,
|
|
131
|
+
"font": {"size": 14},
|
|
132
|
+
"xref": "paper",
|
|
133
|
+
"yref": "paper",
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Observed Sharpe
|
|
138
|
+
annotations.append(
|
|
139
|
+
{
|
|
140
|
+
"x": 0.5,
|
|
141
|
+
"y": 0.15,
|
|
142
|
+
"text": f"Observed Sharpe: {observed_sharpe:.2f}",
|
|
143
|
+
"showarrow": False,
|
|
144
|
+
"font": {"size": 12},
|
|
145
|
+
"xref": "paper",
|
|
146
|
+
"yref": "paper",
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Expected max Sharpe if provided
|
|
151
|
+
if expected_max_sharpe is not None:
|
|
152
|
+
annotations.append(
|
|
153
|
+
{
|
|
154
|
+
"x": 0.5,
|
|
155
|
+
"y": 0.08,
|
|
156
|
+
"text": f"E[max SR]: {expected_max_sharpe:.2f}",
|
|
157
|
+
"showarrow": False,
|
|
158
|
+
"font": {"size": 12},
|
|
159
|
+
"xref": "paper",
|
|
160
|
+
"yref": "paper",
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Number of trials
|
|
165
|
+
if n_trials is not None:
|
|
166
|
+
annotations.append(
|
|
167
|
+
{
|
|
168
|
+
"x": 0.5,
|
|
169
|
+
"y": 0.01,
|
|
170
|
+
"text": f"(N={n_trials} trials)",
|
|
171
|
+
"showarrow": False,
|
|
172
|
+
"font": {"size": 11, "color": "gray"},
|
|
173
|
+
"xref": "paper",
|
|
174
|
+
"yref": "paper",
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Build layout
|
|
179
|
+
layout_updates = {
|
|
180
|
+
"height": height,
|
|
181
|
+
"width": width,
|
|
182
|
+
"annotations": annotations,
|
|
183
|
+
"margin": {"l": 40, "r": 40, "t": 60, "b": 40},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for key, value in theme_config["layout"].items():
|
|
187
|
+
if key not in layout_updates:
|
|
188
|
+
layout_updates[key] = value
|
|
189
|
+
|
|
190
|
+
fig.update_layout(**layout_updates)
|
|
191
|
+
|
|
192
|
+
return fig
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def plot_confidence_intervals(
|
|
196
|
+
metrics: dict[str, dict[str, float]],
|
|
197
|
+
confidence_levels: list[float] | None = None,
|
|
198
|
+
title: str = "Metric Confidence Intervals",
|
|
199
|
+
orientation: Literal["h", "v"] = "h",
|
|
200
|
+
show_point_estimate: bool = True,
|
|
201
|
+
theme: str | None = None,
|
|
202
|
+
height: int = 400,
|
|
203
|
+
width: int | None = None,
|
|
204
|
+
) -> go.Figure:
|
|
205
|
+
"""Create a forest plot showing confidence intervals for multiple metrics.
|
|
206
|
+
|
|
207
|
+
Visualizes bootstrap or analytical confidence intervals at multiple
|
|
208
|
+
confidence levels (e.g., 90%, 95%, 99%).
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
metrics : dict[str, dict[str, float]]
|
|
213
|
+
Dictionary mapping metric names to their CI values.
|
|
214
|
+
Each value should have keys: 'point', 'lower_90', 'upper_90',
|
|
215
|
+
'lower_95', 'upper_95', 'lower_99', 'upper_99' (based on levels).
|
|
216
|
+
confidence_levels : list[float], optional
|
|
217
|
+
Confidence levels to display (default: [0.90, 0.95, 0.99])
|
|
218
|
+
title : str
|
|
219
|
+
Chart title
|
|
220
|
+
orientation : {"h", "v"}
|
|
221
|
+
Horizontal or vertical orientation
|
|
222
|
+
show_point_estimate : bool
|
|
223
|
+
Whether to show the point estimate marker
|
|
224
|
+
theme : str, optional
|
|
225
|
+
Theme name
|
|
226
|
+
height : int
|
|
227
|
+
Figure height in pixels
|
|
228
|
+
width : int, optional
|
|
229
|
+
Figure width in pixels
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
go.Figure
|
|
234
|
+
Plotly figure with forest plot
|
|
235
|
+
|
|
236
|
+
Examples
|
|
237
|
+
--------
|
|
238
|
+
>>> metrics = {
|
|
239
|
+
... "Sharpe": {"point": 1.5, "lower_95": 0.8, "upper_95": 2.2},
|
|
240
|
+
... "CAGR": {"point": 0.15, "lower_95": 0.08, "upper_95": 0.22},
|
|
241
|
+
... }
|
|
242
|
+
>>> fig = plot_confidence_intervals(metrics)
|
|
243
|
+
>>> fig.show()
|
|
244
|
+
"""
|
|
245
|
+
theme_config = get_theme_config(theme)
|
|
246
|
+
colors = theme_config["colorway"]
|
|
247
|
+
|
|
248
|
+
if confidence_levels is None:
|
|
249
|
+
confidence_levels = [0.90, 0.95, 0.99]
|
|
250
|
+
|
|
251
|
+
# Sort confidence levels (widest first for plotting)
|
|
252
|
+
confidence_levels = sorted(confidence_levels, reverse=True)
|
|
253
|
+
|
|
254
|
+
fig = go.Figure()
|
|
255
|
+
|
|
256
|
+
metric_names = list(metrics.keys())
|
|
257
|
+
n_metrics = len(metric_names)
|
|
258
|
+
|
|
259
|
+
# Colors for different confidence levels (lighter to darker)
|
|
260
|
+
level_colors = {
|
|
261
|
+
0.99: "rgba(99, 110, 250, 0.3)", # Lightest - widest CI
|
|
262
|
+
0.95: "rgba(99, 110, 250, 0.5)",
|
|
263
|
+
0.90: "rgba(99, 110, 250, 0.7)", # Darkest - narrowest CI
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for i, metric_name in enumerate(metric_names):
|
|
267
|
+
metric_data = metrics[metric_name]
|
|
268
|
+
point = metric_data.get("point", metric_data.get("estimate", 0))
|
|
269
|
+
|
|
270
|
+
# Plot confidence intervals from widest to narrowest
|
|
271
|
+
for level in confidence_levels:
|
|
272
|
+
level_pct = int(level * 100)
|
|
273
|
+
lower_key = f"lower_{level_pct}"
|
|
274
|
+
upper_key = f"upper_{level_pct}"
|
|
275
|
+
|
|
276
|
+
if lower_key in metric_data and upper_key in metric_data:
|
|
277
|
+
lower = metric_data[lower_key]
|
|
278
|
+
upper = metric_data[upper_key]
|
|
279
|
+
|
|
280
|
+
color = level_colors.get(level, "rgba(99, 110, 250, 0.5)")
|
|
281
|
+
|
|
282
|
+
if orientation == "h":
|
|
283
|
+
fig.add_trace(
|
|
284
|
+
go.Scatter(
|
|
285
|
+
x=[lower, upper],
|
|
286
|
+
y=[i, i],
|
|
287
|
+
mode="lines",
|
|
288
|
+
line={"color": color, "width": 8 if level == 0.95 else 5},
|
|
289
|
+
name=f"{level_pct}% CI" if i == 0 else None,
|
|
290
|
+
showlegend=(i == 0),
|
|
291
|
+
hovertemplate=f"{metric_name}<br>{level_pct}% CI: [{lower:.3f}, {upper:.3f}]<extra></extra>",
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
fig.add_trace(
|
|
296
|
+
go.Scatter(
|
|
297
|
+
x=[i, i],
|
|
298
|
+
y=[lower, upper],
|
|
299
|
+
mode="lines",
|
|
300
|
+
line={"color": color, "width": 8 if level == 0.95 else 5},
|
|
301
|
+
name=f"{level_pct}% CI" if i == 0 else None,
|
|
302
|
+
showlegend=(i == 0),
|
|
303
|
+
hovertemplate=f"{metric_name}<br>{level_pct}% CI: [{lower:.3f}, {upper:.3f}]<extra></extra>",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Add point estimate
|
|
308
|
+
if show_point_estimate:
|
|
309
|
+
if orientation == "h":
|
|
310
|
+
fig.add_trace(
|
|
311
|
+
go.Scatter(
|
|
312
|
+
x=[point],
|
|
313
|
+
y=[i],
|
|
314
|
+
mode="markers",
|
|
315
|
+
marker={"color": colors[0], "size": 12, "symbol": "diamond"},
|
|
316
|
+
name="Point Estimate" if i == 0 else None,
|
|
317
|
+
showlegend=(i == 0),
|
|
318
|
+
hovertemplate=f"{metric_name}: {point:.3f}<extra></extra>",
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
else:
|
|
322
|
+
fig.add_trace(
|
|
323
|
+
go.Scatter(
|
|
324
|
+
x=[i],
|
|
325
|
+
y=[point],
|
|
326
|
+
mode="markers",
|
|
327
|
+
marker={"color": colors[0], "size": 12, "symbol": "diamond"},
|
|
328
|
+
name="Point Estimate" if i == 0 else None,
|
|
329
|
+
showlegend=(i == 0),
|
|
330
|
+
hovertemplate=f"{metric_name}: {point:.3f}<extra></extra>",
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Add zero reference line for Sharpe-like metrics
|
|
335
|
+
if orientation == "h":
|
|
336
|
+
fig.add_vline(x=0, line_dash="dash", line_color="gray", line_width=1)
|
|
337
|
+
else:
|
|
338
|
+
fig.add_hline(y=0, line_dash="dash", line_color="gray", line_width=1)
|
|
339
|
+
|
|
340
|
+
# Build layout
|
|
341
|
+
if orientation == "h":
|
|
342
|
+
layout_updates = {
|
|
343
|
+
"title": {"text": title, "font": {"size": 18}},
|
|
344
|
+
"height": max(height, n_metrics * 60 + 100),
|
|
345
|
+
"xaxis": {"title": "Value", "zeroline": True},
|
|
346
|
+
"yaxis": {
|
|
347
|
+
"tickvals": list(range(n_metrics)),
|
|
348
|
+
"ticktext": metric_names,
|
|
349
|
+
"autorange": "reversed",
|
|
350
|
+
},
|
|
351
|
+
"legend": {"yanchor": "top", "y": 0.99, "xanchor": "right", "x": 0.99},
|
|
352
|
+
}
|
|
353
|
+
else:
|
|
354
|
+
layout_updates = {
|
|
355
|
+
"title": {"text": title, "font": {"size": 18}},
|
|
356
|
+
"height": height,
|
|
357
|
+
"yaxis": {"title": "Value", "zeroline": True},
|
|
358
|
+
"xaxis": {
|
|
359
|
+
"tickvals": list(range(n_metrics)),
|
|
360
|
+
"ticktext": metric_names,
|
|
361
|
+
},
|
|
362
|
+
"legend": {"yanchor": "top", "y": 0.99, "xanchor": "right", "x": 0.99},
|
|
363
|
+
}
|
|
364
|
+
if width:
|
|
365
|
+
layout_updates["width"] = width
|
|
366
|
+
|
|
367
|
+
for key, value in theme_config["layout"].items():
|
|
368
|
+
if key not in layout_updates:
|
|
369
|
+
layout_updates[key] = value
|
|
370
|
+
|
|
371
|
+
fig.update_layout(**layout_updates)
|
|
372
|
+
|
|
373
|
+
return fig
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def plot_ras_analysis(
|
|
377
|
+
original_ic: float,
|
|
378
|
+
adjusted_ic: float,
|
|
379
|
+
rademacher_complexity: float,
|
|
380
|
+
kappa: float = 0.02,
|
|
381
|
+
n_features: int | None = None,
|
|
382
|
+
n_observations: int | None = None,
|
|
383
|
+
title: str = "Rademacher Anti-Serum Analysis",
|
|
384
|
+
theme: str | None = None,
|
|
385
|
+
height: int = 400,
|
|
386
|
+
width: int = 600,
|
|
387
|
+
) -> go.Figure:
|
|
388
|
+
"""Visualize Rademacher Anti-Serum (RAS) overfitting adjustment.
|
|
389
|
+
|
|
390
|
+
The RAS method adjusts Information Coefficients for data mining bias
|
|
391
|
+
by estimating the Rademacher complexity of the strategy search space.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
original_ic : float
|
|
396
|
+
Original (unadjusted) Information Coefficient
|
|
397
|
+
adjusted_ic : float
|
|
398
|
+
RAS-adjusted Information Coefficient
|
|
399
|
+
rademacher_complexity : float
|
|
400
|
+
Estimated Rademacher complexity R̂
|
|
401
|
+
kappa : float
|
|
402
|
+
The practical bound parameter used (default: 0.02)
|
|
403
|
+
n_features : int, optional
|
|
404
|
+
Number of features/strategies tested
|
|
405
|
+
n_observations : int, optional
|
|
406
|
+
Number of observations
|
|
407
|
+
title : str
|
|
408
|
+
Chart title
|
|
409
|
+
theme : str, optional
|
|
410
|
+
Theme name
|
|
411
|
+
height : int
|
|
412
|
+
Figure height in pixels
|
|
413
|
+
width : int
|
|
414
|
+
Figure width in pixels
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
go.Figure
|
|
419
|
+
Plotly figure with RAS analysis
|
|
420
|
+
|
|
421
|
+
Notes
|
|
422
|
+
-----
|
|
423
|
+
The RAS adjustment is:
|
|
424
|
+
IC_adj = max(0, IC_original - 2 * (R̂ + κ))
|
|
425
|
+
|
|
426
|
+
where R̂ is the Rademacher complexity and κ is a practical bound.
|
|
427
|
+
"""
|
|
428
|
+
theme_config = get_theme_config(theme)
|
|
429
|
+
colors = theme_config["colorway"]
|
|
430
|
+
|
|
431
|
+
# Calculate the haircut percentage
|
|
432
|
+
haircut_pct = (1 - adjusted_ic / original_ic) * 100 if original_ic != 0 else 100
|
|
433
|
+
|
|
434
|
+
# Create waterfall chart
|
|
435
|
+
fig = go.Figure()
|
|
436
|
+
|
|
437
|
+
categories = ["Original IC", "Rademacher (2R̂)", "Practical κ", "Adjusted IC"]
|
|
438
|
+
values = [original_ic, -2 * rademacher_complexity, -2 * kappa, adjusted_ic]
|
|
439
|
+
measures = ["absolute", "relative", "relative", "total"]
|
|
440
|
+
|
|
441
|
+
fig.add_trace(
|
|
442
|
+
go.Waterfall(
|
|
443
|
+
name="RAS Adjustment",
|
|
444
|
+
orientation="v",
|
|
445
|
+
x=categories,
|
|
446
|
+
y=values,
|
|
447
|
+
measure=measures,
|
|
448
|
+
text=[f"{v:.4f}" for v in values],
|
|
449
|
+
textposition="outside",
|
|
450
|
+
decreasing={"marker": {"color": "#EF553B"}},
|
|
451
|
+
increasing={"marker": {"color": colors[0]}},
|
|
452
|
+
totals={"marker": {"color": "#00CC96" if adjusted_ic > 0 else "#EF553B"}},
|
|
453
|
+
connector={"line": {"color": "rgba(128, 128, 128, 0.5)", "width": 2}},
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Add annotations
|
|
458
|
+
annotations = []
|
|
459
|
+
|
|
460
|
+
# Haircut percentage
|
|
461
|
+
annotations.append(
|
|
462
|
+
{
|
|
463
|
+
"x": 0.5,
|
|
464
|
+
"y": -0.15,
|
|
465
|
+
"text": f"IC Haircut: {haircut_pct:.1f}% | R̂ = {rademacher_complexity:.4f} | κ = {kappa:.4f}",
|
|
466
|
+
"showarrow": False,
|
|
467
|
+
"font": {"size": 12},
|
|
468
|
+
"xref": "paper",
|
|
469
|
+
"yref": "paper",
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Significance indicator
|
|
474
|
+
if adjusted_ic > 0:
|
|
475
|
+
sig_text = "Statistically significant after RAS adjustment"
|
|
476
|
+
sig_color = "#00CC96"
|
|
477
|
+
else:
|
|
478
|
+
sig_text = "Not significant after RAS adjustment (IC ≤ 0)"
|
|
479
|
+
sig_color = "#EF553B"
|
|
480
|
+
|
|
481
|
+
annotations.append(
|
|
482
|
+
{
|
|
483
|
+
"x": 0.5,
|
|
484
|
+
"y": -0.22,
|
|
485
|
+
"text": sig_text,
|
|
486
|
+
"showarrow": False,
|
|
487
|
+
"font": {"size": 13, "color": sig_color, "weight": "bold"},
|
|
488
|
+
"xref": "paper",
|
|
489
|
+
"yref": "paper",
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# N and T if provided
|
|
494
|
+
if n_features is not None and n_observations is not None:
|
|
495
|
+
annotations.append(
|
|
496
|
+
{
|
|
497
|
+
"x": 0.5,
|
|
498
|
+
"y": 1.08,
|
|
499
|
+
"text": f"N={n_features} features, T={n_observations} observations",
|
|
500
|
+
"showarrow": False,
|
|
501
|
+
"font": {"size": 11, "color": "gray"},
|
|
502
|
+
"xref": "paper",
|
|
503
|
+
"yref": "paper",
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Build layout
|
|
508
|
+
layout_updates = {
|
|
509
|
+
"title": {"text": title, "font": {"size": 18}},
|
|
510
|
+
"height": height,
|
|
511
|
+
"width": width,
|
|
512
|
+
"yaxis": {"title": "Information Coefficient"},
|
|
513
|
+
"showlegend": False,
|
|
514
|
+
"annotations": annotations,
|
|
515
|
+
"margin": {"l": 60, "r": 40, "t": 80, "b": 100},
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
for key, value in theme_config["layout"].items():
|
|
519
|
+
if key not in layout_updates:
|
|
520
|
+
layout_updates[key] = value
|
|
521
|
+
|
|
522
|
+
fig.update_layout(**layout_updates)
|
|
523
|
+
|
|
524
|
+
return fig
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def plot_minimum_track_record(
|
|
528
|
+
observed_sharpe: float,
|
|
529
|
+
current_periods: int,
|
|
530
|
+
sr_benchmark: float = 0.0,
|
|
531
|
+
confidence: float = 0.95,
|
|
532
|
+
max_periods: int | None = None,
|
|
533
|
+
periods_per_year: int = 252,
|
|
534
|
+
title: str = "Minimum Track Record Length",
|
|
535
|
+
theme: str | None = None,
|
|
536
|
+
height: int = 400,
|
|
537
|
+
width: int | None = None,
|
|
538
|
+
) -> go.Figure:
|
|
539
|
+
"""Visualize minimum track record length (MinTRL) analysis.
|
|
540
|
+
|
|
541
|
+
Shows how many periods are needed to achieve statistical significance
|
|
542
|
+
for the observed Sharpe ratio, and whether the current track record
|
|
543
|
+
is sufficient.
|
|
544
|
+
|
|
545
|
+
Parameters
|
|
546
|
+
----------
|
|
547
|
+
observed_sharpe : float
|
|
548
|
+
The observed Sharpe ratio (annualized)
|
|
549
|
+
current_periods : int
|
|
550
|
+
Current number of observation periods
|
|
551
|
+
sr_benchmark : float
|
|
552
|
+
Benchmark Sharpe ratio for comparison (default: 0)
|
|
553
|
+
confidence : float
|
|
554
|
+
Target confidence level (default: 0.95)
|
|
555
|
+
max_periods : int, optional
|
|
556
|
+
Maximum periods to show on x-axis
|
|
557
|
+
periods_per_year : int
|
|
558
|
+
Periods per year for time conversion (default: 252 for daily)
|
|
559
|
+
title : str
|
|
560
|
+
Chart title
|
|
561
|
+
theme : str, optional
|
|
562
|
+
Theme name
|
|
563
|
+
height : int
|
|
564
|
+
Figure height in pixels
|
|
565
|
+
width : int, optional
|
|
566
|
+
Figure width in pixels
|
|
567
|
+
|
|
568
|
+
Returns
|
|
569
|
+
-------
|
|
570
|
+
go.Figure
|
|
571
|
+
Plotly figure with MinTRL analysis
|
|
572
|
+
|
|
573
|
+
Notes
|
|
574
|
+
-----
|
|
575
|
+
The minimum track record length formula is:
|
|
576
|
+
MinTRL = 1 + (1 - γ₃*SR + γ₄*SR²/4) * (z_α / SR)²
|
|
577
|
+
|
|
578
|
+
where γ₃ is skewness, γ₄ is excess kurtosis, and z_α is the
|
|
579
|
+
critical value for confidence level α.
|
|
580
|
+
"""
|
|
581
|
+
from scipy import stats
|
|
582
|
+
|
|
583
|
+
theme_config = get_theme_config(theme)
|
|
584
|
+
colors = theme_config["colorway"]
|
|
585
|
+
|
|
586
|
+
# Calculate MinTRL (simplified, assuming normal returns)
|
|
587
|
+
z_alpha = stats.norm.ppf(confidence)
|
|
588
|
+
sharpe_diff = observed_sharpe - sr_benchmark
|
|
589
|
+
|
|
590
|
+
if sharpe_diff <= 0:
|
|
591
|
+
min_trl = float("inf")
|
|
592
|
+
else:
|
|
593
|
+
# Simplified MinTRL (assuming γ₃=0, γ₄=3)
|
|
594
|
+
min_trl = (z_alpha / sharpe_diff) ** 2
|
|
595
|
+
|
|
596
|
+
# Convert to years
|
|
597
|
+
min_trl_years = min_trl / periods_per_year if min_trl != float("inf") else float("inf")
|
|
598
|
+
current_years = current_periods / periods_per_year
|
|
599
|
+
|
|
600
|
+
# Determine max periods for x-axis
|
|
601
|
+
if max_periods is None:
|
|
602
|
+
if min_trl != float("inf"):
|
|
603
|
+
max_periods = int(max(min_trl * 1.5, current_periods * 1.2))
|
|
604
|
+
else:
|
|
605
|
+
max_periods = current_periods * 2
|
|
606
|
+
|
|
607
|
+
# Generate data for the required SR curve at different track record lengths
|
|
608
|
+
periods_range = np.linspace(10, max_periods, 100)
|
|
609
|
+
|
|
610
|
+
# Required SR to achieve significance at each track record length
|
|
611
|
+
# SR_required = z_alpha / sqrt(T)
|
|
612
|
+
required_sr = z_alpha / np.sqrt(periods_range) + sr_benchmark
|
|
613
|
+
|
|
614
|
+
fig = go.Figure()
|
|
615
|
+
|
|
616
|
+
# Required SR curve
|
|
617
|
+
fig.add_trace(
|
|
618
|
+
go.Scatter(
|
|
619
|
+
x=periods_range / periods_per_year,
|
|
620
|
+
y=required_sr,
|
|
621
|
+
mode="lines",
|
|
622
|
+
name=f"{int(confidence * 100)}% Significance Threshold",
|
|
623
|
+
line={"color": colors[1] if len(colors) > 1 else "orange", "width": 2, "dash": "dash"},
|
|
624
|
+
fill="tozeroy",
|
|
625
|
+
fillcolor="rgba(239, 85, 59, 0.2)",
|
|
626
|
+
hovertemplate="Track Record: %{x:.1f} years<br>Required SR: %{y:.2f}<extra></extra>",
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# Horizontal line at observed Sharpe
|
|
631
|
+
fig.add_trace(
|
|
632
|
+
go.Scatter(
|
|
633
|
+
x=[0, max_periods / periods_per_year],
|
|
634
|
+
y=[observed_sharpe, observed_sharpe],
|
|
635
|
+
mode="lines",
|
|
636
|
+
name=f"Observed SR: {observed_sharpe:.2f}",
|
|
637
|
+
line={"color": colors[0], "width": 3},
|
|
638
|
+
hovertemplate="Observed Sharpe: %{y:.2f}<extra></extra>",
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
# Current position marker
|
|
643
|
+
is_significant = current_periods >= min_trl
|
|
644
|
+
marker_color = "#00CC96" if is_significant else "#EF553B"
|
|
645
|
+
|
|
646
|
+
fig.add_trace(
|
|
647
|
+
go.Scatter(
|
|
648
|
+
x=[current_years],
|
|
649
|
+
y=[observed_sharpe],
|
|
650
|
+
mode="markers",
|
|
651
|
+
name="Current Position",
|
|
652
|
+
marker={"color": marker_color, "size": 15, "symbol": "star"},
|
|
653
|
+
hovertemplate=f"Current: {current_years:.1f} years<br>SR: {observed_sharpe:.2f}<extra></extra>",
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
# Add vertical line at MinTRL
|
|
658
|
+
if min_trl != float("inf") and min_trl <= max_periods:
|
|
659
|
+
fig.add_vline(
|
|
660
|
+
x=min_trl_years,
|
|
661
|
+
line_dash="dot",
|
|
662
|
+
line_color="gray",
|
|
663
|
+
annotation_text=f"MinTRL: {min_trl_years:.1f}y",
|
|
664
|
+
annotation_position="top",
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# Add significance zone annotation
|
|
668
|
+
annotations = []
|
|
669
|
+
|
|
670
|
+
if is_significant:
|
|
671
|
+
status_text = (
|
|
672
|
+
f"Track record sufficient ({current_years:.1f}y ≥ MinTRL {min_trl_years:.1f}y)"
|
|
673
|
+
)
|
|
674
|
+
status_color = "#00CC96"
|
|
675
|
+
elif min_trl == float("inf"):
|
|
676
|
+
status_text = "Cannot achieve significance (SR ≤ benchmark)"
|
|
677
|
+
status_color = "#EF553B"
|
|
678
|
+
else:
|
|
679
|
+
deficit = min_trl_years - current_years
|
|
680
|
+
status_text = f"Need {deficit:.1f} more years (MinTRL: {min_trl_years:.1f}y)"
|
|
681
|
+
status_color = "#FFA15A"
|
|
682
|
+
|
|
683
|
+
annotations.append(
|
|
684
|
+
{
|
|
685
|
+
"x": 0.5,
|
|
686
|
+
"y": -0.15,
|
|
687
|
+
"text": status_text,
|
|
688
|
+
"showarrow": False,
|
|
689
|
+
"font": {"size": 13, "color": status_color, "weight": "bold"},
|
|
690
|
+
"xref": "paper",
|
|
691
|
+
"yref": "paper",
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Build layout
|
|
696
|
+
layout_updates = {
|
|
697
|
+
"title": {"text": title, "font": {"size": 18}},
|
|
698
|
+
"height": height,
|
|
699
|
+
"xaxis": {"title": "Track Record Length (Years)", "rangemode": "tozero"},
|
|
700
|
+
"yaxis": {"title": "Sharpe Ratio", "rangemode": "tozero"},
|
|
701
|
+
"legend": {"yanchor": "top", "y": 0.99, "xanchor": "right", "x": 0.99},
|
|
702
|
+
"annotations": annotations,
|
|
703
|
+
"margin": {"b": 80},
|
|
704
|
+
}
|
|
705
|
+
if width:
|
|
706
|
+
layout_updates["width"] = width
|
|
707
|
+
|
|
708
|
+
for key, value in theme_config["layout"].items():
|
|
709
|
+
if key not in layout_updates:
|
|
710
|
+
layout_updates[key] = value
|
|
711
|
+
|
|
712
|
+
fig.update_layout(**layout_updates)
|
|
713
|
+
|
|
714
|
+
return fig
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def plot_statistical_summary_card(
|
|
718
|
+
metrics: dict[str, Any],
|
|
719
|
+
title: str = "Statistical Validity Summary",
|
|
720
|
+
theme: str | None = None,
|
|
721
|
+
height: int = 300,
|
|
722
|
+
width: int = 700,
|
|
723
|
+
) -> go.Figure:
|
|
724
|
+
"""Create an executive summary card for statistical validity checks.
|
|
725
|
+
|
|
726
|
+
Combines multiple statistical tests into a single traffic-light display
|
|
727
|
+
showing overall strategy validity.
|
|
728
|
+
|
|
729
|
+
Parameters
|
|
730
|
+
----------
|
|
731
|
+
metrics : dict[str, Any]
|
|
732
|
+
Dictionary with statistical metrics. Expected keys:
|
|
733
|
+
- dsr_probability: DSR p-value
|
|
734
|
+
- dsr_significant: bool
|
|
735
|
+
- min_trl: minimum track record length
|
|
736
|
+
- current_trl: current track record length
|
|
737
|
+
- trl_sufficient: bool
|
|
738
|
+
- ras_adjusted_ic: RAS-adjusted IC (optional)
|
|
739
|
+
- ras_significant: bool (optional)
|
|
740
|
+
title : str
|
|
741
|
+
Chart title
|
|
742
|
+
theme : str, optional
|
|
743
|
+
Theme name
|
|
744
|
+
height : int
|
|
745
|
+
Figure height in pixels
|
|
746
|
+
width : int
|
|
747
|
+
Figure width in pixels
|
|
748
|
+
|
|
749
|
+
Returns
|
|
750
|
+
-------
|
|
751
|
+
go.Figure
|
|
752
|
+
Plotly figure with summary card
|
|
753
|
+
"""
|
|
754
|
+
theme_config = get_theme_config(theme)
|
|
755
|
+
|
|
756
|
+
# Extract metrics with defaults
|
|
757
|
+
dsr_prob = metrics.get("dsr_probability", None)
|
|
758
|
+
dsr_sig = metrics.get("dsr_significant", None)
|
|
759
|
+
min_trl = metrics.get("min_trl", None)
|
|
760
|
+
current_trl = metrics.get("current_trl", None)
|
|
761
|
+
trl_sufficient = metrics.get("trl_sufficient", None)
|
|
762
|
+
ras_ic = metrics.get("ras_adjusted_ic", None)
|
|
763
|
+
ras_sig = metrics.get("ras_significant", None)
|
|
764
|
+
|
|
765
|
+
# Build indicators
|
|
766
|
+
indicators = []
|
|
767
|
+
|
|
768
|
+
# DSR check
|
|
769
|
+
if dsr_prob is not None:
|
|
770
|
+
if dsr_sig:
|
|
771
|
+
indicators.append(("DSR", f"p={dsr_prob:.3f}", "green", "Significant"))
|
|
772
|
+
elif dsr_prob < 0.10:
|
|
773
|
+
indicators.append(("DSR", f"p={dsr_prob:.3f}", "yellow", "Marginal"))
|
|
774
|
+
else:
|
|
775
|
+
indicators.append(("DSR", f"p={dsr_prob:.3f}", "red", "Not Significant"))
|
|
776
|
+
|
|
777
|
+
# MinTRL check
|
|
778
|
+
if min_trl is not None and current_trl is not None:
|
|
779
|
+
if trl_sufficient:
|
|
780
|
+
indicators.append(
|
|
781
|
+
("Track Record", f"{current_trl:.0f}/{min_trl:.0f}", "green", "Sufficient")
|
|
782
|
+
)
|
|
783
|
+
else:
|
|
784
|
+
indicators.append(
|
|
785
|
+
("Track Record", f"{current_trl:.0f}/{min_trl:.0f}", "red", "Insufficient")
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
# RAS check
|
|
789
|
+
if ras_ic is not None:
|
|
790
|
+
if ras_sig:
|
|
791
|
+
indicators.append(("RAS IC", f"{ras_ic:.4f}", "green", "Significant"))
|
|
792
|
+
else:
|
|
793
|
+
indicators.append(("RAS IC", f"{ras_ic:.4f}", "red", "Not Significant"))
|
|
794
|
+
|
|
795
|
+
if not indicators:
|
|
796
|
+
indicators = [("No Data", "-", "gray", "No statistical tests available")]
|
|
797
|
+
|
|
798
|
+
# Create table-like figure
|
|
799
|
+
n_cols = len(indicators)
|
|
800
|
+
|
|
801
|
+
# Color mapping
|
|
802
|
+
color_map = {
|
|
803
|
+
"green": "#00CC96",
|
|
804
|
+
"yellow": "#FECB52",
|
|
805
|
+
"red": "#EF553B",
|
|
806
|
+
"gray": "#888888",
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
fig = go.Figure()
|
|
810
|
+
|
|
811
|
+
for i, (name, value, color, status) in enumerate(indicators):
|
|
812
|
+
x_pos = (i + 0.5) / n_cols
|
|
813
|
+
|
|
814
|
+
# Status icon (colored circle)
|
|
815
|
+
fig.add_annotation(
|
|
816
|
+
x=x_pos,
|
|
817
|
+
y=0.75,
|
|
818
|
+
text="●",
|
|
819
|
+
showarrow=False,
|
|
820
|
+
font={"size": 40, "color": color_map[color]},
|
|
821
|
+
xref="paper",
|
|
822
|
+
yref="paper",
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
# Metric name
|
|
826
|
+
fig.add_annotation(
|
|
827
|
+
x=x_pos,
|
|
828
|
+
y=0.5,
|
|
829
|
+
text=f"<b>{name}</b>",
|
|
830
|
+
showarrow=False,
|
|
831
|
+
font={"size": 14},
|
|
832
|
+
xref="paper",
|
|
833
|
+
yref="paper",
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
# Value
|
|
837
|
+
fig.add_annotation(
|
|
838
|
+
x=x_pos,
|
|
839
|
+
y=0.35,
|
|
840
|
+
text=value,
|
|
841
|
+
showarrow=False,
|
|
842
|
+
font={"size": 12},
|
|
843
|
+
xref="paper",
|
|
844
|
+
yref="paper",
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
# Status text
|
|
848
|
+
fig.add_annotation(
|
|
849
|
+
x=x_pos,
|
|
850
|
+
y=0.2,
|
|
851
|
+
text=status,
|
|
852
|
+
showarrow=False,
|
|
853
|
+
font={"size": 11, "color": color_map[color]},
|
|
854
|
+
xref="paper",
|
|
855
|
+
yref="paper",
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
# Build layout
|
|
859
|
+
layout_updates = {
|
|
860
|
+
"title": {"text": title, "font": {"size": 18}, "x": 0.5},
|
|
861
|
+
"height": height,
|
|
862
|
+
"width": width,
|
|
863
|
+
"xaxis": {"visible": False, "range": [0, 1]},
|
|
864
|
+
"yaxis": {"visible": False, "range": [0, 1]},
|
|
865
|
+
"margin": {"l": 20, "r": 20, "t": 60, "b": 20},
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
for key, value in theme_config["layout"].items():
|
|
869
|
+
if key not in layout_updates:
|
|
870
|
+
layout_updates[key] = value
|
|
871
|
+
|
|
872
|
+
fig.update_layout(**layout_updates)
|
|
873
|
+
|
|
874
|
+
return fig
|