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,527 @@
|
|
|
1
|
+
"""Core Information Coefficient (IC) metrics.
|
|
2
|
+
|
|
3
|
+
This module provides the fundamental IC calculations used for evaluating
|
|
4
|
+
feature predictiveness.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Union, cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import polars as pl
|
|
12
|
+
from scipy import stats
|
|
13
|
+
from scipy.stats import spearmanr
|
|
14
|
+
|
|
15
|
+
from ml4t.diagnostic.backends.adapter import DataFrameAdapter
|
|
16
|
+
from ml4t.diagnostic.evaluation.metrics.basic import compute_forward_returns
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from numpy.typing import NDArray
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def information_coefficient(
|
|
23
|
+
predictions: Union[pl.Series, pd.Series, "NDArray[Any]"],
|
|
24
|
+
returns: Union[pl.Series, pd.Series, "NDArray[Any]"],
|
|
25
|
+
method: str = "spearman",
|
|
26
|
+
confidence_intervals: bool = False,
|
|
27
|
+
alpha: float = 0.05,
|
|
28
|
+
) -> float | dict[str, float]:
|
|
29
|
+
"""Calculate Information Coefficient between predictions and returns.
|
|
30
|
+
|
|
31
|
+
The Information Coefficient measures the linear relationship between model
|
|
32
|
+
predictions and subsequent returns. Spearman correlation is preferred as it's
|
|
33
|
+
robust to outliers and non-linear relationships.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
predictions : Union[pl.Series, pd.Series, np.ndarray]
|
|
38
|
+
Model predictions or scores
|
|
39
|
+
returns : Union[pl.Series, pd.Series, np.ndarray]
|
|
40
|
+
Forward returns corresponding to predictions
|
|
41
|
+
method : str, default "spearman"
|
|
42
|
+
Correlation method: "spearman" or "pearson"
|
|
43
|
+
confidence_intervals : bool, default False
|
|
44
|
+
Whether to return confidence intervals
|
|
45
|
+
alpha : float, default 0.05
|
|
46
|
+
Significance level for confidence intervals
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
Union[float, dict]
|
|
51
|
+
If confidence_intervals=False: IC value
|
|
52
|
+
If confidence_intervals=True: dict with 'ic', 'lower_ci', 'upper_ci', 'p_value'
|
|
53
|
+
|
|
54
|
+
Examples
|
|
55
|
+
--------
|
|
56
|
+
>>> predictions = np.array([0.1, 0.3, -0.2, 0.5])
|
|
57
|
+
>>> returns = np.array([0.02, 0.05, -0.01, 0.08])
|
|
58
|
+
>>> ic = information_coefficient(predictions, returns)
|
|
59
|
+
>>> print(f"IC: {ic:.3f}")
|
|
60
|
+
IC: 0.800
|
|
61
|
+
|
|
62
|
+
>>> # With confidence intervals
|
|
63
|
+
>>> result = information_coefficient(predictions, returns, confidence_intervals=True)
|
|
64
|
+
>>> print(f"IC: {result['ic']:.3f} [{result['lower_ci']:.3f}, {result['upper_ci']:.3f}]")
|
|
65
|
+
IC: 0.800 [-0.602, 0.995]
|
|
66
|
+
"""
|
|
67
|
+
# Convert inputs to numpy for consistent handling
|
|
68
|
+
pred_array = DataFrameAdapter.to_numpy(predictions).flatten()
|
|
69
|
+
ret_array = DataFrameAdapter.to_numpy(returns).flatten()
|
|
70
|
+
|
|
71
|
+
# Validate inputs
|
|
72
|
+
if len(pred_array) != len(ret_array):
|
|
73
|
+
raise ValueError("Predictions and returns must have the same length")
|
|
74
|
+
|
|
75
|
+
if len(pred_array) < 2:
|
|
76
|
+
if confidence_intervals:
|
|
77
|
+
return {
|
|
78
|
+
"ic": np.nan,
|
|
79
|
+
"lower_ci": np.nan,
|
|
80
|
+
"upper_ci": np.nan,
|
|
81
|
+
"p_value": np.nan,
|
|
82
|
+
}
|
|
83
|
+
return np.nan
|
|
84
|
+
|
|
85
|
+
# Remove NaN pairs
|
|
86
|
+
valid_mask = ~(np.isnan(pred_array) | np.isnan(ret_array))
|
|
87
|
+
pred_clean = pred_array[valid_mask]
|
|
88
|
+
ret_clean = ret_array[valid_mask]
|
|
89
|
+
|
|
90
|
+
if len(pred_clean) < 2:
|
|
91
|
+
if confidence_intervals:
|
|
92
|
+
return {
|
|
93
|
+
"ic": np.nan,
|
|
94
|
+
"lower_ci": np.nan,
|
|
95
|
+
"upper_ci": np.nan,
|
|
96
|
+
"p_value": np.nan,
|
|
97
|
+
}
|
|
98
|
+
return np.nan
|
|
99
|
+
|
|
100
|
+
# Calculate correlation
|
|
101
|
+
if method == "spearman":
|
|
102
|
+
ic_value, p_value = spearmanr(pred_clean, ret_clean)
|
|
103
|
+
elif method == "pearson":
|
|
104
|
+
ic_value, p_value = stats.pearsonr(pred_clean, ret_clean)
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Unknown correlation method: {method}")
|
|
107
|
+
|
|
108
|
+
# Handle edge cases
|
|
109
|
+
if np.isnan(ic_value):
|
|
110
|
+
if confidence_intervals:
|
|
111
|
+
return {
|
|
112
|
+
"ic": np.nan,
|
|
113
|
+
"lower_ci": np.nan,
|
|
114
|
+
"upper_ci": np.nan,
|
|
115
|
+
"p_value": np.nan,
|
|
116
|
+
}
|
|
117
|
+
return np.nan
|
|
118
|
+
|
|
119
|
+
# Return simple IC if no confidence intervals requested
|
|
120
|
+
if not confidence_intervals:
|
|
121
|
+
return float(ic_value)
|
|
122
|
+
|
|
123
|
+
# Calculate confidence intervals using Fisher transformation
|
|
124
|
+
n = len(pred_clean)
|
|
125
|
+
if n < 4: # Need sufficient data for meaningful CI
|
|
126
|
+
return {
|
|
127
|
+
"ic": float(ic_value),
|
|
128
|
+
"lower_ci": np.nan,
|
|
129
|
+
"upper_ci": np.nan,
|
|
130
|
+
"p_value": float(p_value) if not np.isnan(p_value) else np.nan,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Fisher transformation for correlation confidence intervals
|
|
134
|
+
z = np.arctanh(ic_value) # Fisher z-transform
|
|
135
|
+
se = 1 / np.sqrt(n - 3) # Standard error
|
|
136
|
+
z_critical = stats.norm.ppf(1 - alpha / 2)
|
|
137
|
+
|
|
138
|
+
# Transform back to correlation scale
|
|
139
|
+
lower_z = z - z_critical * se
|
|
140
|
+
upper_z = z + z_critical * se
|
|
141
|
+
lower_ci = np.tanh(lower_z)
|
|
142
|
+
upper_ci = np.tanh(upper_z)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"ic": float(ic_value),
|
|
146
|
+
"lower_ci": float(lower_ci),
|
|
147
|
+
"upper_ci": float(upper_ci),
|
|
148
|
+
"p_value": float(p_value) if not np.isnan(p_value) else np.nan,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def compute_ic_series(
|
|
153
|
+
predictions: pl.DataFrame | pd.DataFrame,
|
|
154
|
+
returns: pl.DataFrame | pd.DataFrame,
|
|
155
|
+
pred_col: str = "prediction",
|
|
156
|
+
ret_col: str = "forward_return",
|
|
157
|
+
date_col: str = "date",
|
|
158
|
+
method: str = "spearman",
|
|
159
|
+
min_periods: int = 10,
|
|
160
|
+
) -> pl.DataFrame | pd.DataFrame:
|
|
161
|
+
"""Compute IC time series for temporal analysis (Alphalens-style).
|
|
162
|
+
|
|
163
|
+
This function computes the Information Coefficient for each time period
|
|
164
|
+
(typically daily), enabling temporal analysis of prediction quality.
|
|
165
|
+
This is THE fundamental visualization in Alphalens.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
predictions : Union[pl.DataFrame, pd.DataFrame]
|
|
170
|
+
DataFrame with predictions, indexed or with date column
|
|
171
|
+
returns : Union[pl.DataFrame, pd.DataFrame]
|
|
172
|
+
DataFrame with forward returns, matching predictions structure
|
|
173
|
+
pred_col : str, default "prediction"
|
|
174
|
+
Column name for predictions/features
|
|
175
|
+
ret_col : str, default "forward_return"
|
|
176
|
+
Column name for forward returns
|
|
177
|
+
date_col : str, default "date"
|
|
178
|
+
Column name for dates (for grouping by period)
|
|
179
|
+
method : str, default "spearman"
|
|
180
|
+
Correlation method: "spearman" or "pearson"
|
|
181
|
+
min_periods : int, default 10
|
|
182
|
+
Minimum observations per period for valid IC calculation
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
Union[pl.DataFrame, pd.DataFrame]
|
|
187
|
+
Time series of IC values with columns: [date_col, 'ic', 'n_obs']
|
|
188
|
+
|
|
189
|
+
Examples
|
|
190
|
+
--------
|
|
191
|
+
>>> # Create sample data
|
|
192
|
+
>>> dates = pd.date_range("2024-01-01", periods=100)
|
|
193
|
+
>>> pred_df = pd.DataFrame({
|
|
194
|
+
... "date": dates,
|
|
195
|
+
... "prediction": np.random.randn(100)
|
|
196
|
+
... })
|
|
197
|
+
>>> ret_df = pd.DataFrame({
|
|
198
|
+
... "date": dates,
|
|
199
|
+
... "forward_return": np.random.randn(100) * 0.02
|
|
200
|
+
... })
|
|
201
|
+
>>> ic_series = compute_ic_series(pred_df, ret_df)
|
|
202
|
+
>>> print(ic_series.head())
|
|
203
|
+
"""
|
|
204
|
+
is_polars = isinstance(predictions, pl.DataFrame)
|
|
205
|
+
|
|
206
|
+
if is_polars:
|
|
207
|
+
# Merge predictions and returns
|
|
208
|
+
predictions_pl = cast(pl.DataFrame, predictions)
|
|
209
|
+
returns_pl = cast(pl.DataFrame, returns)
|
|
210
|
+
df = predictions_pl.join(returns_pl, on=date_col, how="inner")
|
|
211
|
+
|
|
212
|
+
# Use group_by().map_groups() for efficient per-group processing
|
|
213
|
+
def compute_group_ic(group: pl.DataFrame) -> pl.DataFrame:
|
|
214
|
+
"""Compute IC for a single date group."""
|
|
215
|
+
pred_array = group[pred_col].to_numpy()
|
|
216
|
+
ret_array = group[ret_col].to_numpy()
|
|
217
|
+
|
|
218
|
+
# Remove NaN pairs
|
|
219
|
+
valid_mask = ~(np.isnan(pred_array) | np.isnan(ret_array))
|
|
220
|
+
pred_clean = pred_array[valid_mask]
|
|
221
|
+
ret_clean = ret_array[valid_mask]
|
|
222
|
+
|
|
223
|
+
n_obs = len(pred_clean)
|
|
224
|
+
|
|
225
|
+
if n_obs >= min_periods:
|
|
226
|
+
ic_val = information_coefficient(
|
|
227
|
+
pred_clean, ret_clean, method=method, confidence_intervals=False
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
ic_val = np.nan
|
|
231
|
+
|
|
232
|
+
return pl.DataFrame({date_col: [group[date_col][0]], "ic": [ic_val], "n_obs": [n_obs]})
|
|
233
|
+
|
|
234
|
+
return df.group_by(date_col).map_groups(compute_group_ic).sort(date_col)
|
|
235
|
+
|
|
236
|
+
# pandas - use different variable name to avoid type conflict
|
|
237
|
+
# Merge predictions and returns
|
|
238
|
+
predictions_pd = cast(pd.DataFrame, predictions)
|
|
239
|
+
returns_pd = cast(pd.DataFrame, returns)
|
|
240
|
+
df_pd = pd.merge(predictions_pd, returns_pd, on=date_col, how="inner")
|
|
241
|
+
|
|
242
|
+
# Group by date and compute IC
|
|
243
|
+
def compute_period_ic(group: pd.DataFrame) -> pd.Series:
|
|
244
|
+
# Explicitly convert to ndarray to handle ExtensionArray types
|
|
245
|
+
pred_array = np.asarray(group[pred_col].values, dtype=np.float64)
|
|
246
|
+
ret_array = np.asarray(group[ret_col].values, dtype=np.float64)
|
|
247
|
+
|
|
248
|
+
# Remove NaN pairs
|
|
249
|
+
valid_mask = ~(np.isnan(pred_array) | np.isnan(ret_array))
|
|
250
|
+
pred_clean = pred_array[valid_mask]
|
|
251
|
+
ret_clean = ret_array[valid_mask]
|
|
252
|
+
|
|
253
|
+
n_obs = len(pred_clean)
|
|
254
|
+
|
|
255
|
+
if n_obs >= min_periods:
|
|
256
|
+
ic_val = information_coefficient(
|
|
257
|
+
pred_clean, ret_clean, method=method, confidence_intervals=False
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
ic_val = np.nan
|
|
261
|
+
|
|
262
|
+
return pd.Series({"ic": ic_val, "n_obs": n_obs})
|
|
263
|
+
|
|
264
|
+
ic_series = df_pd.groupby(date_col, group_keys=False).apply(compute_period_ic).reset_index()
|
|
265
|
+
|
|
266
|
+
return ic_series
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def compute_ic_by_horizon(
|
|
270
|
+
predictions: pl.DataFrame | pd.DataFrame,
|
|
271
|
+
prices: pl.DataFrame | pd.DataFrame,
|
|
272
|
+
horizons: list[int] | None = None,
|
|
273
|
+
pred_col: str = "prediction",
|
|
274
|
+
price_col: str = "close",
|
|
275
|
+
date_col: str = "date",
|
|
276
|
+
group_col: str | None = None,
|
|
277
|
+
method: str = "spearman",
|
|
278
|
+
) -> dict[int, float]:
|
|
279
|
+
"""Compute IC across multiple forward return horizons.
|
|
280
|
+
|
|
281
|
+
This function computes IC for different forward-looking periods
|
|
282
|
+
(e.g., 1-day, 5-day, 21-day), which is essential for understanding
|
|
283
|
+
prediction persistence and optimal holding periods.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
predictions : Union[pl.DataFrame, pd.DataFrame]
|
|
288
|
+
DataFrame with predictions
|
|
289
|
+
prices : Union[pl.DataFrame, pd.DataFrame]
|
|
290
|
+
DataFrame with prices to compute forward returns
|
|
291
|
+
horizons : list[int], default [1, 5, 21]
|
|
292
|
+
Forward periods to analyze (in days/bars)
|
|
293
|
+
pred_col : str, default "prediction"
|
|
294
|
+
Column name for predictions
|
|
295
|
+
price_col : str, default "close"
|
|
296
|
+
Column name for prices
|
|
297
|
+
date_col : str, default "date"
|
|
298
|
+
Column name for dates
|
|
299
|
+
group_col : str | None, default None
|
|
300
|
+
Column for grouping (e.g., 'symbol')
|
|
301
|
+
method : str, default "spearman"
|
|
302
|
+
Correlation method
|
|
303
|
+
|
|
304
|
+
Returns
|
|
305
|
+
-------
|
|
306
|
+
dict[int, float | dict]
|
|
307
|
+
Dictionary mapping horizon -> IC value
|
|
308
|
+
Keys are horizon periods, values are IC (or dict with CI if requested)
|
|
309
|
+
|
|
310
|
+
Examples
|
|
311
|
+
--------
|
|
312
|
+
>>> pred_df = pd.DataFrame({"date": dates, "prediction": preds})
|
|
313
|
+
>>> price_df = pd.DataFrame({"date": dates, "close": prices})
|
|
314
|
+
>>> ic_by_horizon = compute_ic_by_horizon(
|
|
315
|
+
... pred_df, price_df, horizons=[1, 5, 21]
|
|
316
|
+
... )
|
|
317
|
+
>>> print(f"1-day IC: {ic_by_horizon[1]:.3f}")
|
|
318
|
+
>>> print(f"5-day IC: {ic_by_horizon[5]:.3f}")
|
|
319
|
+
"""
|
|
320
|
+
# Compute forward returns for all horizons
|
|
321
|
+
if horizons is None:
|
|
322
|
+
horizons = [1, 5, 21]
|
|
323
|
+
prices_with_fwd = compute_forward_returns(
|
|
324
|
+
prices, periods=horizons, price_col=price_col, group_col=group_col
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Merge with predictions - declare type before branching
|
|
328
|
+
df: pl.DataFrame | pd.DataFrame
|
|
329
|
+
|
|
330
|
+
if isinstance(predictions, pl.DataFrame):
|
|
331
|
+
# Type is narrowed by isinstance check, but prices_with_fwd needs cast
|
|
332
|
+
prices_with_fwd_pl = cast(pl.DataFrame, prices_with_fwd)
|
|
333
|
+
df = predictions.join(prices_with_fwd_pl, on=date_col, how="inner")
|
|
334
|
+
elif isinstance(predictions, pd.DataFrame):
|
|
335
|
+
prices_with_fwd_pd = cast(pd.DataFrame, prices_with_fwd)
|
|
336
|
+
df = pd.merge(predictions, prices_with_fwd_pd, on=date_col, how="inner")
|
|
337
|
+
else:
|
|
338
|
+
raise TypeError(
|
|
339
|
+
f"predictions must be pl.DataFrame or pd.DataFrame, got {type(predictions)}"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Compute IC for each horizon
|
|
343
|
+
ic_results: dict[int, float] = {}
|
|
344
|
+
|
|
345
|
+
for horizon in horizons:
|
|
346
|
+
ret_col = f"fwd_ret_{horizon}"
|
|
347
|
+
|
|
348
|
+
# Extract arrays - df type is known from construction above
|
|
349
|
+
if isinstance(df, pl.DataFrame):
|
|
350
|
+
pred_array = df[pred_col].to_numpy()
|
|
351
|
+
ret_array = df[ret_col].to_numpy()
|
|
352
|
+
else:
|
|
353
|
+
pred_array = df[pred_col].to_numpy()
|
|
354
|
+
ret_array = df[ret_col].to_numpy()
|
|
355
|
+
|
|
356
|
+
# Compute IC (confidence_intervals=False returns float)
|
|
357
|
+
ic_result = information_coefficient(
|
|
358
|
+
pred_array, ret_array, method=method, confidence_intervals=False
|
|
359
|
+
)
|
|
360
|
+
# When confidence_intervals=False, returns float; otherwise dict
|
|
361
|
+
if isinstance(ic_result, dict):
|
|
362
|
+
ic_val = float(ic_result.get("ic", np.nan))
|
|
363
|
+
else:
|
|
364
|
+
ic_val = float(ic_result)
|
|
365
|
+
|
|
366
|
+
ic_results[horizon] = ic_val
|
|
367
|
+
|
|
368
|
+
return ic_results
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def compute_ic_ir(
|
|
372
|
+
ic_series: Union[pl.DataFrame, pd.DataFrame, "NDArray[Any]"],
|
|
373
|
+
ic_col: str = "ic",
|
|
374
|
+
annualization_factor: float = np.sqrt(252),
|
|
375
|
+
confidence_intervals: bool = False,
|
|
376
|
+
n_bootstrap: int = 10000,
|
|
377
|
+
alpha: float = 0.05,
|
|
378
|
+
) -> float | dict[str, float]:
|
|
379
|
+
"""Compute IC Information Ratio (IC-IR) - risk-adjusted IC metric.
|
|
380
|
+
|
|
381
|
+
IC-IR is analogous to the Sharpe ratio but for IC instead of returns.
|
|
382
|
+
It measures the consistency of predictive power by computing mean IC
|
|
383
|
+
divided by the standard deviation of IC.
|
|
384
|
+
|
|
385
|
+
Higher IC-IR indicates more consistent predictions. IC-IR > 0.5 is
|
|
386
|
+
generally considered good, IC-IR > 1.0 is excellent.
|
|
387
|
+
|
|
388
|
+
Parameters
|
|
389
|
+
----------
|
|
390
|
+
ic_series : Union[pl.DataFrame, pd.DataFrame, np.ndarray]
|
|
391
|
+
Time series of IC values (from compute_ic_series)
|
|
392
|
+
ic_col : str, default "ic"
|
|
393
|
+
Column name for IC values (if DataFrame)
|
|
394
|
+
annualization_factor : float, default sqrt(252)
|
|
395
|
+
Factor to annualize IC-IR (sqrt(periods_per_year))
|
|
396
|
+
- Daily: sqrt(252) ~ 15.87
|
|
397
|
+
- Weekly: sqrt(52) ~ 7.21
|
|
398
|
+
- Monthly: sqrt(12) ~ 3.46
|
|
399
|
+
confidence_intervals : bool, default False
|
|
400
|
+
Whether to compute bootstrap confidence intervals
|
|
401
|
+
n_bootstrap : int, default 10000
|
|
402
|
+
Number of bootstrap samples for CI computation
|
|
403
|
+
alpha : float, default 0.05
|
|
404
|
+
Significance level for confidence intervals (95% CI)
|
|
405
|
+
|
|
406
|
+
Returns
|
|
407
|
+
-------
|
|
408
|
+
Union[float, dict]
|
|
409
|
+
If confidence_intervals=False: IC-IR value
|
|
410
|
+
If confidence_intervals=True: dict with 'ic_ir', 'lower_ci', 'upper_ci'
|
|
411
|
+
|
|
412
|
+
Examples
|
|
413
|
+
--------
|
|
414
|
+
>>> # Compute IC series first
|
|
415
|
+
>>> ic_series = compute_ic_series(pred_df, ret_df)
|
|
416
|
+
>>>
|
|
417
|
+
>>> # Compute IC-IR
|
|
418
|
+
>>> ic_ir = compute_ic_ir(ic_series)
|
|
419
|
+
>>> print(f"IC-IR: {ic_ir:.3f}")
|
|
420
|
+
IC-IR: 0.645
|
|
421
|
+
>>>
|
|
422
|
+
>>> # With confidence intervals
|
|
423
|
+
>>> result = compute_ic_ir(ic_series, confidence_intervals=True)
|
|
424
|
+
>>> print(f"IC-IR: {result['ic_ir']:.3f} [{result['lower_ci']:.3f}, {result['upper_ci']:.3f}]")
|
|
425
|
+
IC-IR: 0.645 [0.412, 0.891]
|
|
426
|
+
|
|
427
|
+
Notes
|
|
428
|
+
-----
|
|
429
|
+
IC-IR Interpretation:
|
|
430
|
+
- IC-IR < 0.3: Weak/inconsistent predictive power
|
|
431
|
+
- IC-IR 0.3-0.5: Moderate consistency
|
|
432
|
+
- IC-IR 0.5-1.0: Good consistency
|
|
433
|
+
- IC-IR > 1.0: Excellent consistency
|
|
434
|
+
|
|
435
|
+
The annualization factor adjusts IC-IR to an annual scale for easier
|
|
436
|
+
interpretation and comparison across different rebalancing frequencies.
|
|
437
|
+
"""
|
|
438
|
+
# Extract IC values
|
|
439
|
+
ic_values: NDArray[Any]
|
|
440
|
+
if isinstance(ic_series, pl.DataFrame | pd.DataFrame):
|
|
441
|
+
is_polars = isinstance(ic_series, pl.DataFrame)
|
|
442
|
+
if is_polars:
|
|
443
|
+
ic_values = cast(pl.DataFrame, ic_series)[ic_col].to_numpy()
|
|
444
|
+
else:
|
|
445
|
+
ic_values = cast(pd.DataFrame, ic_series)[ic_col].to_numpy()
|
|
446
|
+
else:
|
|
447
|
+
ic_values = np.asarray(ic_series).flatten()
|
|
448
|
+
|
|
449
|
+
# Remove NaN values
|
|
450
|
+
ic_clean: NDArray[Any] = ic_values[~np.isnan(ic_values)]
|
|
451
|
+
|
|
452
|
+
# Validate sufficient data
|
|
453
|
+
if len(ic_clean) < 2:
|
|
454
|
+
if confidence_intervals:
|
|
455
|
+
return {
|
|
456
|
+
"ic_ir": np.nan,
|
|
457
|
+
"lower_ci": np.nan,
|
|
458
|
+
"upper_ci": np.nan,
|
|
459
|
+
"mean_ic": np.nan,
|
|
460
|
+
"std_ic": np.nan,
|
|
461
|
+
"n_periods": len(ic_clean),
|
|
462
|
+
}
|
|
463
|
+
return np.nan
|
|
464
|
+
|
|
465
|
+
# Compute IC-IR
|
|
466
|
+
mean_ic = float(np.mean(ic_clean))
|
|
467
|
+
std_ic = float(np.std(ic_clean, ddof=1)) # Sample std
|
|
468
|
+
|
|
469
|
+
if std_ic == 0:
|
|
470
|
+
# Perfect consistency (all IC values identical)
|
|
471
|
+
ic_ir = np.inf if mean_ic > 0 else -np.inf if mean_ic < 0 else np.nan
|
|
472
|
+
else:
|
|
473
|
+
ic_ir = (mean_ic / std_ic) * annualization_factor
|
|
474
|
+
|
|
475
|
+
# Return simple IC-IR if no CI requested
|
|
476
|
+
if not confidence_intervals:
|
|
477
|
+
return float(ic_ir)
|
|
478
|
+
|
|
479
|
+
# Bootstrap confidence intervals
|
|
480
|
+
if len(ic_clean) < 10:
|
|
481
|
+
# Insufficient data for meaningful bootstrap
|
|
482
|
+
return {
|
|
483
|
+
"ic_ir": float(ic_ir),
|
|
484
|
+
"lower_ci": np.nan,
|
|
485
|
+
"upper_ci": np.nan,
|
|
486
|
+
"mean_ic": float(mean_ic),
|
|
487
|
+
"std_ic": float(std_ic),
|
|
488
|
+
"n_periods": len(ic_clean),
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# Perform bootstrap
|
|
492
|
+
rng = np.random.RandomState(42) # For reproducibility
|
|
493
|
+
bootstrap_ics = []
|
|
494
|
+
|
|
495
|
+
for _ in range(n_bootstrap):
|
|
496
|
+
# Resample with replacement
|
|
497
|
+
sample = rng.choice(ic_clean, size=len(ic_clean), replace=True)
|
|
498
|
+
sample_mean = np.mean(sample)
|
|
499
|
+
sample_std = np.std(sample, ddof=1)
|
|
500
|
+
|
|
501
|
+
if sample_std > 0:
|
|
502
|
+
bootstrap_ic_ir = (sample_mean / sample_std) * annualization_factor
|
|
503
|
+
bootstrap_ics.append(bootstrap_ic_ir)
|
|
504
|
+
|
|
505
|
+
if len(bootstrap_ics) == 0:
|
|
506
|
+
# Bootstrap failed (all samples had zero std)
|
|
507
|
+
return {
|
|
508
|
+
"ic_ir": float(ic_ir),
|
|
509
|
+
"lower_ci": np.nan,
|
|
510
|
+
"upper_ci": np.nan,
|
|
511
|
+
"mean_ic": float(mean_ic),
|
|
512
|
+
"std_ic": float(std_ic),
|
|
513
|
+
"n_periods": len(ic_clean),
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# Compute percentile confidence intervals
|
|
517
|
+
lower_ci = np.percentile(bootstrap_ics, (alpha / 2) * 100)
|
|
518
|
+
upper_ci = np.percentile(bootstrap_ics, (1 - alpha / 2) * 100)
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
"ic_ir": float(ic_ir),
|
|
522
|
+
"lower_ci": float(lower_ci),
|
|
523
|
+
"upper_ci": float(upper_ci),
|
|
524
|
+
"mean_ic": float(mean_ic),
|
|
525
|
+
"std_ic": float(std_ic),
|
|
526
|
+
"n_periods": len(ic_clean),
|
|
527
|
+
}
|