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,456 @@
|
|
|
1
|
+
"""SignalTearSheet class for complete signal analysis results.
|
|
2
|
+
|
|
3
|
+
This module provides the SignalTearSheet class that aggregates all signal
|
|
4
|
+
analysis components (IC, quantile, turnover, IR_tc) into a single exportable
|
|
5
|
+
result object with visualization and export capabilities.
|
|
6
|
+
|
|
7
|
+
References
|
|
8
|
+
----------
|
|
9
|
+
Lopez de Prado, M. (2018). "Advances in Financial Machine Learning"
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Literal
|
|
16
|
+
|
|
17
|
+
import polars as pl
|
|
18
|
+
from pydantic import Field
|
|
19
|
+
|
|
20
|
+
from ml4t.diagnostic.results.base import BaseResult
|
|
21
|
+
from ml4t.diagnostic.results.signal_results.ic import SignalICResult
|
|
22
|
+
from ml4t.diagnostic.results.signal_results.irtc import IRtcResult
|
|
23
|
+
from ml4t.diagnostic.results.signal_results.quantile import QuantileAnalysisResult
|
|
24
|
+
from ml4t.diagnostic.results.signal_results.turnover import TurnoverAnalysisResult
|
|
25
|
+
from ml4t.diagnostic.results.signal_results.validation import _figure_from_data
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SignalTearSheet(BaseResult):
|
|
29
|
+
"""Complete tear sheet containing all signal analysis results.
|
|
30
|
+
|
|
31
|
+
Aggregates IC, quantile, turnover, and visualization data into
|
|
32
|
+
a single exportable result object.
|
|
33
|
+
|
|
34
|
+
Examples
|
|
35
|
+
--------
|
|
36
|
+
>>> tear_sheet = signal_analysis.create_tear_sheet()
|
|
37
|
+
>>> tear_sheet.show() # Display in Jupyter
|
|
38
|
+
>>> tear_sheet.save_html("signal_report.html")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
analysis_type: str = Field(default="signal_tear_sheet", frozen=True)
|
|
42
|
+
|
|
43
|
+
# ==========================================================================
|
|
44
|
+
# Component Results
|
|
45
|
+
# ==========================================================================
|
|
46
|
+
|
|
47
|
+
ic_analysis: SignalICResult | None = Field(
|
|
48
|
+
default=None,
|
|
49
|
+
description="Signal IC analysis results",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
quantile_analysis: QuantileAnalysisResult | None = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
description="Quantile analysis results",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
turnover_analysis: TurnoverAnalysisResult | None = Field(
|
|
58
|
+
default=None,
|
|
59
|
+
description="Turnover analysis results",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
ir_tc_analysis: IRtcResult | None = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
description="IR_tc analysis results",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# ==========================================================================
|
|
68
|
+
# Metadata
|
|
69
|
+
# ==========================================================================
|
|
70
|
+
|
|
71
|
+
signal_name: str = Field(
|
|
72
|
+
default="signal",
|
|
73
|
+
description="Name of the signal analyzed",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
n_assets: int = Field(
|
|
77
|
+
...,
|
|
78
|
+
description="Number of unique assets",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
n_dates: int = Field(
|
|
82
|
+
...,
|
|
83
|
+
description="Number of unique dates",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
date_range: tuple[str, str] = Field(
|
|
87
|
+
...,
|
|
88
|
+
description="Date range (start, end) in ISO format",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# ==========================================================================
|
|
92
|
+
# Figures (stored as JSON for serialization)
|
|
93
|
+
# ==========================================================================
|
|
94
|
+
|
|
95
|
+
figures: dict[str, Any] = Field(
|
|
96
|
+
default_factory=dict,
|
|
97
|
+
description="Plotly figures as JSON (for HTML export)",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
|
|
101
|
+
"""Get results as Polars DataFrame.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
name : str | None
|
|
106
|
+
DataFrame to retrieve - routes to component results
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
pl.DataFrame
|
|
111
|
+
Requested DataFrame
|
|
112
|
+
"""
|
|
113
|
+
if name is None or name == "summary":
|
|
114
|
+
return self._build_summary_df()
|
|
115
|
+
|
|
116
|
+
# Route to component results
|
|
117
|
+
if name.startswith("ic_"):
|
|
118
|
+
if self.ic_analysis is None:
|
|
119
|
+
raise ValueError("IC analysis not available")
|
|
120
|
+
component_name = name[3:] if name != "ic_analysis" else None
|
|
121
|
+
return self.ic_analysis.get_dataframe(component_name)
|
|
122
|
+
|
|
123
|
+
if name.startswith("quantile_"):
|
|
124
|
+
if self.quantile_analysis is None:
|
|
125
|
+
raise ValueError("Quantile analysis not available")
|
|
126
|
+
component_name = name[9:] if name != "quantile_analysis" else None
|
|
127
|
+
return self.quantile_analysis.get_dataframe(component_name)
|
|
128
|
+
|
|
129
|
+
if name.startswith("turnover_"):
|
|
130
|
+
if self.turnover_analysis is None:
|
|
131
|
+
raise ValueError("Turnover analysis not available")
|
|
132
|
+
component_name = name[9:] if name != "turnover_analysis" else None
|
|
133
|
+
return self.turnover_analysis.get_dataframe(component_name)
|
|
134
|
+
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Unknown DataFrame name: {name}. Use 'summary' or prefix with "
|
|
137
|
+
"'ic_', 'quantile_', 'turnover_'"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def list_available_dataframes(self) -> list[str]:
|
|
141
|
+
"""List available DataFrame views."""
|
|
142
|
+
available = ["summary"]
|
|
143
|
+
if self.ic_analysis:
|
|
144
|
+
available.extend([f"ic_{n}" for n in self.ic_analysis.list_available_dataframes()])
|
|
145
|
+
if self.quantile_analysis:
|
|
146
|
+
available.extend(
|
|
147
|
+
[f"quantile_{n}" for n in self.quantile_analysis.list_available_dataframes()]
|
|
148
|
+
)
|
|
149
|
+
if self.turnover_analysis:
|
|
150
|
+
available.extend(
|
|
151
|
+
[f"turnover_{n}" for n in self.turnover_analysis.list_available_dataframes()]
|
|
152
|
+
)
|
|
153
|
+
return available
|
|
154
|
+
|
|
155
|
+
def _build_summary_df(self) -> pl.DataFrame:
|
|
156
|
+
"""Build summary DataFrame with key metrics."""
|
|
157
|
+
rows = [
|
|
158
|
+
{"metric": "signal_name", "value": self.signal_name},
|
|
159
|
+
{"metric": "n_assets", "value": str(self.n_assets)},
|
|
160
|
+
{"metric": "n_dates", "value": str(self.n_dates)},
|
|
161
|
+
{"metric": "date_range_start", "value": self.date_range[0]},
|
|
162
|
+
{"metric": "date_range_end", "value": self.date_range[1]},
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
if self.ic_analysis:
|
|
166
|
+
for period, ic in self.ic_analysis.ic_mean.items():
|
|
167
|
+
rows.append({"metric": f"ic_mean_{period}", "value": f"{ic:.4f}"})
|
|
168
|
+
|
|
169
|
+
return pl.DataFrame(rows)
|
|
170
|
+
|
|
171
|
+
def summary(self) -> str:
|
|
172
|
+
"""Get human-readable summary of complete tear sheet."""
|
|
173
|
+
lines = [
|
|
174
|
+
"=" * 60,
|
|
175
|
+
f"Signal Analysis Tear Sheet: {self.signal_name}",
|
|
176
|
+
"=" * 60,
|
|
177
|
+
"",
|
|
178
|
+
f"Assets: {self.n_assets:>10}",
|
|
179
|
+
f"Dates: {self.n_dates:>10}",
|
|
180
|
+
f"Range: {self.date_range[0]} to {self.date_range[1]}",
|
|
181
|
+
f"Created: {self.created_at}",
|
|
182
|
+
"",
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
if self.ic_analysis:
|
|
186
|
+
lines.append("--- IC Analysis ---")
|
|
187
|
+
lines.append(self.ic_analysis.summary())
|
|
188
|
+
|
|
189
|
+
if self.quantile_analysis:
|
|
190
|
+
lines.append("--- Quantile Analysis ---")
|
|
191
|
+
lines.append(self.quantile_analysis.summary())
|
|
192
|
+
|
|
193
|
+
if self.turnover_analysis:
|
|
194
|
+
lines.append("--- Turnover Analysis ---")
|
|
195
|
+
lines.append(self.turnover_analysis.summary())
|
|
196
|
+
|
|
197
|
+
if self.ir_tc_analysis:
|
|
198
|
+
lines.append("--- IR_tc Analysis ---")
|
|
199
|
+
lines.append(self.ir_tc_analysis.summary())
|
|
200
|
+
|
|
201
|
+
return "\n".join(lines)
|
|
202
|
+
|
|
203
|
+
def show(self) -> None:
|
|
204
|
+
"""Display tear sheet in Jupyter notebook.
|
|
205
|
+
|
|
206
|
+
Renders all figures inline using IPython display.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
from IPython.display import HTML, display
|
|
210
|
+
|
|
211
|
+
# Display summary
|
|
212
|
+
display(HTML(f"<h2>Signal Analysis: {self.signal_name}</h2>"))
|
|
213
|
+
display(HTML(f"<p>{self.n_assets} assets, {self.n_dates} dates</p>"))
|
|
214
|
+
|
|
215
|
+
# Display figures
|
|
216
|
+
for _name, fig_json in self.figures.items():
|
|
217
|
+
fig = _figure_from_data(fig_json)
|
|
218
|
+
fig.show()
|
|
219
|
+
|
|
220
|
+
except ImportError:
|
|
221
|
+
print("IPython not available. Use save_html() instead.")
|
|
222
|
+
print(self.summary())
|
|
223
|
+
|
|
224
|
+
def save_html(
|
|
225
|
+
self,
|
|
226
|
+
path: str | Path,
|
|
227
|
+
use_dashboard: bool = True,
|
|
228
|
+
include_plotlyjs: str | bool = "cdn",
|
|
229
|
+
theme: Literal["light", "dark"] = "light",
|
|
230
|
+
) -> Path:
|
|
231
|
+
"""Save tear sheet as self-contained HTML file.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
path : str | Path
|
|
236
|
+
Output file path
|
|
237
|
+
use_dashboard : bool, default=True
|
|
238
|
+
If True, use multi-tab SignalDashboard format.
|
|
239
|
+
If False, use simple stacked plot layout.
|
|
240
|
+
include_plotlyjs : str | bool
|
|
241
|
+
How to include plotly.js: 'cdn', 'directory', True (embed), False
|
|
242
|
+
theme : str, default='light'
|
|
243
|
+
Theme for dashboard: 'light' or 'dark' (only used if use_dashboard=True)
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
Path
|
|
248
|
+
Path to saved file
|
|
249
|
+
"""
|
|
250
|
+
path = Path(path)
|
|
251
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
|
|
253
|
+
if use_dashboard:
|
|
254
|
+
# Use multi-tab dashboard format
|
|
255
|
+
from ml4t.diagnostic.visualization.signal.dashboard import SignalDashboard
|
|
256
|
+
|
|
257
|
+
dashboard = SignalDashboard(
|
|
258
|
+
title=f"Signal Analysis: {self.signal_name}",
|
|
259
|
+
theme=theme,
|
|
260
|
+
)
|
|
261
|
+
html = dashboard.generate(self)
|
|
262
|
+
path.write_text(html)
|
|
263
|
+
else:
|
|
264
|
+
# Use simple stacked layout (legacy behavior)
|
|
265
|
+
import plotly.io as pio
|
|
266
|
+
|
|
267
|
+
# NOTE: Plotly.js is included via pio.to_html with include_plotlyjs parameter
|
|
268
|
+
# Do NOT add hardcoded CDN script here - it would duplicate the inclusion
|
|
269
|
+
html_parts = [
|
|
270
|
+
"<!DOCTYPE html>",
|
|
271
|
+
"<html>",
|
|
272
|
+
"<head>",
|
|
273
|
+
f"<title>Signal Analysis: {self.signal_name}</title>",
|
|
274
|
+
"<style>",
|
|
275
|
+
"body { font-family: -apple-system, system-ui, sans-serif; margin: 40px; }",
|
|
276
|
+
"h1 { color: #2C3E50; }",
|
|
277
|
+
".summary { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; }",
|
|
278
|
+
".plot-container { margin-bottom: 40px; }",
|
|
279
|
+
"</style>",
|
|
280
|
+
"</head>",
|
|
281
|
+
"<body>",
|
|
282
|
+
f"<h1>Signal Analysis: {self.signal_name}</h1>",
|
|
283
|
+
"<div class='summary'>",
|
|
284
|
+
f"<p><strong>Assets:</strong> {self.n_assets}</p>",
|
|
285
|
+
f"<p><strong>Dates:</strong> {self.n_dates}</p>",
|
|
286
|
+
f"<p><strong>Range:</strong> {self.date_range[0]} to {self.date_range[1]}</p>",
|
|
287
|
+
f"<p><strong>Generated:</strong> {self.created_at}</p>",
|
|
288
|
+
"</div>",
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
# Add figures
|
|
292
|
+
plotlyjs_included = False
|
|
293
|
+
for name, fig_json in self.figures.items():
|
|
294
|
+
fig = _figure_from_data(fig_json)
|
|
295
|
+
fig_html = pio.to_html(
|
|
296
|
+
fig,
|
|
297
|
+
include_plotlyjs=include_plotlyjs if not plotlyjs_included else False,
|
|
298
|
+
full_html=False,
|
|
299
|
+
)
|
|
300
|
+
html_parts.append("<div class='plot-container'>")
|
|
301
|
+
html_parts.append(f"<h2>{name.replace('_', ' ').title()}</h2>")
|
|
302
|
+
html_parts.append(fig_html)
|
|
303
|
+
html_parts.append("</div>")
|
|
304
|
+
plotlyjs_included = True
|
|
305
|
+
|
|
306
|
+
html_parts.extend(["</body>", "</html>"])
|
|
307
|
+
path.write_text("\n".join(html_parts))
|
|
308
|
+
|
|
309
|
+
return path
|
|
310
|
+
|
|
311
|
+
def save_json(self, path: str | Path, exclude_figures: bool = False) -> Path:
|
|
312
|
+
"""Export all metrics as structured JSON.
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
path : str | Path
|
|
317
|
+
Output file path
|
|
318
|
+
exclude_figures : bool, default=False
|
|
319
|
+
If True, exclude figure JSON data to reduce file size
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
Path
|
|
324
|
+
Path to saved file
|
|
325
|
+
|
|
326
|
+
Examples
|
|
327
|
+
--------
|
|
328
|
+
>>> tear_sheet.save_json("signal_metrics.json")
|
|
329
|
+
>>> tear_sheet.save_json("signal_compact.json", exclude_figures=True)
|
|
330
|
+
"""
|
|
331
|
+
import json
|
|
332
|
+
|
|
333
|
+
path = Path(path)
|
|
334
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
|
|
336
|
+
data = self.to_dict(exclude_none=True)
|
|
337
|
+
|
|
338
|
+
if exclude_figures:
|
|
339
|
+
data.pop("figures", None)
|
|
340
|
+
|
|
341
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
342
|
+
json.dump(data, f, indent=2, default=str)
|
|
343
|
+
|
|
344
|
+
return path
|
|
345
|
+
|
|
346
|
+
def save_png(
|
|
347
|
+
self,
|
|
348
|
+
output_dir: str | Path,
|
|
349
|
+
figures: list[str] | None = None,
|
|
350
|
+
width: int = 1200,
|
|
351
|
+
height: int = 600,
|
|
352
|
+
scale: float = 2.0,
|
|
353
|
+
) -> list[Path]:
|
|
354
|
+
"""Export figures as PNG images.
|
|
355
|
+
|
|
356
|
+
Requires the `kaleido` package for static image export.
|
|
357
|
+
Install with: pip install kaleido
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
output_dir : str | Path
|
|
362
|
+
Output directory for PNG files
|
|
363
|
+
figures : list[str] | None
|
|
364
|
+
List of figure names to export. If None, exports all figures.
|
|
365
|
+
width : int, default=1200
|
|
366
|
+
Image width in pixels
|
|
367
|
+
height : int, default=600
|
|
368
|
+
Image height in pixels
|
|
369
|
+
scale : float, default=2.0
|
|
370
|
+
Scale factor for resolution (2.0 = 2x resolution)
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
list[Path]
|
|
375
|
+
Paths to saved PNG files
|
|
376
|
+
|
|
377
|
+
Raises
|
|
378
|
+
------
|
|
379
|
+
ImportError
|
|
380
|
+
If kaleido is not installed
|
|
381
|
+
|
|
382
|
+
Examples
|
|
383
|
+
--------
|
|
384
|
+
>>> paths = tear_sheet.save_png("./images/")
|
|
385
|
+
>>> paths = tear_sheet.save_png("./images/", figures=["ic_time_series"])
|
|
386
|
+
"""
|
|
387
|
+
try:
|
|
388
|
+
import plotly.io as pio
|
|
389
|
+
|
|
390
|
+
# Check if kaleido is available
|
|
391
|
+
pio.kaleido.scope # noqa: B018 - Check if kaleido is installed
|
|
392
|
+
except (ImportError, AttributeError) as e:
|
|
393
|
+
raise ImportError(
|
|
394
|
+
"kaleido is required for PNG export. Install with: pip install kaleido"
|
|
395
|
+
) from e
|
|
396
|
+
|
|
397
|
+
output_dir = Path(output_dir)
|
|
398
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
399
|
+
|
|
400
|
+
saved_paths: list[Path] = []
|
|
401
|
+
figure_names = figures if figures is not None else list(self.figures.keys())
|
|
402
|
+
|
|
403
|
+
for name in figure_names:
|
|
404
|
+
if name not in self.figures:
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
fig_json = self.figures[name]
|
|
408
|
+
fig = _figure_from_data(fig_json)
|
|
409
|
+
|
|
410
|
+
output_path = output_dir / f"{name}.png"
|
|
411
|
+
fig.write_image(
|
|
412
|
+
str(output_path),
|
|
413
|
+
width=width,
|
|
414
|
+
height=height,
|
|
415
|
+
scale=scale,
|
|
416
|
+
)
|
|
417
|
+
saved_paths.append(output_path)
|
|
418
|
+
|
|
419
|
+
return saved_paths
|
|
420
|
+
|
|
421
|
+
def to_dashboard(self, theme: Literal["light", "dark"] = "light") -> Any:
|
|
422
|
+
"""Convert to SignalDashboard for customization.
|
|
423
|
+
|
|
424
|
+
Returns a SignalDashboard instance that can be further customized
|
|
425
|
+
before generating HTML output.
|
|
426
|
+
|
|
427
|
+
Parameters
|
|
428
|
+
----------
|
|
429
|
+
theme : Literal["light", "dark"], default='light'
|
|
430
|
+
Dashboard theme: 'light' or 'dark'
|
|
431
|
+
|
|
432
|
+
Returns
|
|
433
|
+
-------
|
|
434
|
+
SignalDashboard
|
|
435
|
+
Dashboard instance ready for customization
|
|
436
|
+
|
|
437
|
+
Examples
|
|
438
|
+
--------
|
|
439
|
+
>>> dashboard = tear_sheet.to_dashboard(theme="dark")
|
|
440
|
+
>>> dashboard.title = "Custom Title"
|
|
441
|
+
>>> html = dashboard.generate(tear_sheet)
|
|
442
|
+
"""
|
|
443
|
+
from ml4t.diagnostic.visualization.signal.dashboard import SignalDashboard
|
|
444
|
+
|
|
445
|
+
return SignalDashboard(
|
|
446
|
+
title=f"Signal Analysis: {self.signal_name}",
|
|
447
|
+
theme=theme,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def to_dict(self, *, exclude_none: bool = False) -> dict[str, Any]:
|
|
451
|
+
"""Export to dictionary, excluding large figure data by default."""
|
|
452
|
+
data = super().to_dict(exclude_none=exclude_none)
|
|
453
|
+
# Optionally exclude figures to reduce size
|
|
454
|
+
if exclude_none and not self.figures:
|
|
455
|
+
data.pop("figures", None)
|
|
456
|
+
return data
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Turnover analysis result classes for signal analysis.
|
|
2
|
+
|
|
3
|
+
This module provides result classes for storing turnover analysis outputs including
|
|
4
|
+
quantile turnover rates, signal autocorrelation, and stability metrics.
|
|
5
|
+
|
|
6
|
+
References
|
|
7
|
+
----------
|
|
8
|
+
Lopez de Prado, M. (2018). "Advances in Financial Machine Learning"
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import polars as pl
|
|
16
|
+
from pydantic import Field, model_validator
|
|
17
|
+
|
|
18
|
+
from ml4t.diagnostic.results.base import BaseResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TurnoverAnalysisResult(BaseResult):
|
|
22
|
+
"""Results from turnover analysis.
|
|
23
|
+
|
|
24
|
+
Contains quantile turnover rates, signal autocorrelation,
|
|
25
|
+
and stability metrics.
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
>>> result = turnover_result
|
|
30
|
+
>>> print(result.summary())
|
|
31
|
+
>>> df = result.get_dataframe("turnover")
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
analysis_type: str = Field(default="turnover_analysis", frozen=True)
|
|
35
|
+
|
|
36
|
+
# ==========================================================================
|
|
37
|
+
# Quantile Turnover
|
|
38
|
+
# ==========================================================================
|
|
39
|
+
|
|
40
|
+
quantile_turnover: dict[str, dict[str, float]] = Field(
|
|
41
|
+
...,
|
|
42
|
+
description="Turnover rate by quantile and period: {period: {quantile: turnover}}",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
mean_turnover: dict[str, float] = Field(
|
|
46
|
+
...,
|
|
47
|
+
description="Mean turnover across all quantiles per period",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
top_quantile_turnover: dict[str, float] = Field(
|
|
51
|
+
...,
|
|
52
|
+
description="Turnover for top quantile (long positions)",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
bottom_quantile_turnover: dict[str, float] = Field(
|
|
56
|
+
...,
|
|
57
|
+
description="Turnover for bottom quantile (short positions)",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ==========================================================================
|
|
61
|
+
# Signal Autocorrelation
|
|
62
|
+
# ==========================================================================
|
|
63
|
+
|
|
64
|
+
autocorrelation: dict[str, list[float]] = Field(
|
|
65
|
+
...,
|
|
66
|
+
description="Autocorrelation by lag: {period: [ac_lag1, ac_lag2, ...]}",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
autocorrelation_lags: list[int] = Field(
|
|
70
|
+
...,
|
|
71
|
+
description="Lag values used",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
mean_autocorrelation: dict[str, float] = Field(
|
|
75
|
+
...,
|
|
76
|
+
description="Mean autocorrelation (average across first 5 lags)",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ==========================================================================
|
|
80
|
+
# Stability Metrics
|
|
81
|
+
# ==========================================================================
|
|
82
|
+
|
|
83
|
+
half_life: dict[str, float | None] = Field(
|
|
84
|
+
...,
|
|
85
|
+
description="Signal half-life in periods (time for AC to decay by 50%)",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# ==========================================================================
|
|
89
|
+
# Validation
|
|
90
|
+
# ==========================================================================
|
|
91
|
+
|
|
92
|
+
@model_validator(mode="after")
|
|
93
|
+
def _validate_keys(self) -> TurnoverAnalysisResult:
|
|
94
|
+
"""Validate that all period-keyed dicts share the same keys and list lengths match."""
|
|
95
|
+
# Get reference period set from quantile_turnover
|
|
96
|
+
period_set = set(self.quantile_turnover.keys())
|
|
97
|
+
|
|
98
|
+
# Validate period-keyed dicts
|
|
99
|
+
period_dicts: list[tuple[str, dict[str, Any]]] = [
|
|
100
|
+
("mean_turnover", self.mean_turnover),
|
|
101
|
+
("top_quantile_turnover", self.top_quantile_turnover),
|
|
102
|
+
("bottom_quantile_turnover", self.bottom_quantile_turnover),
|
|
103
|
+
("autocorrelation", self.autocorrelation),
|
|
104
|
+
("mean_autocorrelation", self.mean_autocorrelation),
|
|
105
|
+
("half_life", self.half_life),
|
|
106
|
+
]
|
|
107
|
+
for name, d in period_dicts:
|
|
108
|
+
if set(d.keys()) != period_set:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Key mismatch in '{name}': expected {period_set}, got {set(d.keys())}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Validate autocorrelation list lengths match autocorrelation_lags
|
|
114
|
+
n_lags = len(self.autocorrelation_lags)
|
|
115
|
+
for period, ac_values in self.autocorrelation.items():
|
|
116
|
+
if len(ac_values) != n_lags:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Length mismatch in autocorrelation['{period}']: "
|
|
119
|
+
f"expected {n_lags} (len(autocorrelation_lags)), got {len(ac_values)}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
# ==========================================================================
|
|
125
|
+
# Methods
|
|
126
|
+
# ==========================================================================
|
|
127
|
+
|
|
128
|
+
def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
|
|
129
|
+
"""Get results as Polars DataFrame.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
name : str | None
|
|
134
|
+
DataFrame to retrieve:
|
|
135
|
+
- None or "turnover": Turnover by quantile
|
|
136
|
+
- "autocorrelation": Autocorrelation by lag
|
|
137
|
+
- "summary": Summary statistics
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
pl.DataFrame
|
|
142
|
+
Requested DataFrame
|
|
143
|
+
"""
|
|
144
|
+
if name is None or name == "turnover":
|
|
145
|
+
periods = list(self.quantile_turnover.keys())
|
|
146
|
+
if not periods:
|
|
147
|
+
return pl.DataFrame()
|
|
148
|
+
|
|
149
|
+
quantiles = list(self.quantile_turnover[periods[0]].keys())
|
|
150
|
+
rows = []
|
|
151
|
+
for period in periods:
|
|
152
|
+
for q in quantiles:
|
|
153
|
+
rows.append(
|
|
154
|
+
{
|
|
155
|
+
"period": period,
|
|
156
|
+
"quantile": q,
|
|
157
|
+
"turnover": self.quantile_turnover[period][q],
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
return pl.DataFrame(rows)
|
|
161
|
+
|
|
162
|
+
if name == "autocorrelation":
|
|
163
|
+
periods = list(self.autocorrelation.keys())
|
|
164
|
+
rows = []
|
|
165
|
+
for period in periods:
|
|
166
|
+
for i, lag in enumerate(self.autocorrelation_lags):
|
|
167
|
+
rows.append(
|
|
168
|
+
{
|
|
169
|
+
"period": period,
|
|
170
|
+
"lag": lag,
|
|
171
|
+
"autocorrelation": self.autocorrelation[period][i],
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
return pl.DataFrame(rows)
|
|
175
|
+
|
|
176
|
+
if name == "summary":
|
|
177
|
+
periods = list(self.mean_turnover.keys())
|
|
178
|
+
return pl.DataFrame(
|
|
179
|
+
{
|
|
180
|
+
"period": periods,
|
|
181
|
+
"mean_turnover": [self.mean_turnover[p] for p in periods],
|
|
182
|
+
"top_turnover": [self.top_quantile_turnover[p] for p in periods],
|
|
183
|
+
"bottom_turnover": [self.bottom_quantile_turnover[p] for p in periods],
|
|
184
|
+
"mean_autocorrelation": [self.mean_autocorrelation[p] for p in periods],
|
|
185
|
+
"half_life": [self.half_life[p] for p in periods],
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Unknown DataFrame name: {name}. Available: 'turnover', 'autocorrelation', 'summary'"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def list_available_dataframes(self) -> list[str]:
|
|
194
|
+
"""List available DataFrame views."""
|
|
195
|
+
return ["turnover", "autocorrelation", "summary"]
|
|
196
|
+
|
|
197
|
+
def summary(self) -> str:
|
|
198
|
+
"""Get human-readable summary of turnover analysis results."""
|
|
199
|
+
lines = ["=" * 60, "Turnover Analysis Summary", "=" * 60, ""]
|
|
200
|
+
|
|
201
|
+
for period in self.mean_turnover:
|
|
202
|
+
lines.append(f"Period: {period}")
|
|
203
|
+
lines.append("-" * 40)
|
|
204
|
+
lines.append(f" Mean Turnover: {self.mean_turnover[period]:>8.2%}")
|
|
205
|
+
lines.append(f" Top Quantile: {self.top_quantile_turnover[period]:>8.2%}")
|
|
206
|
+
lines.append(f" Bottom Quantile: {self.bottom_quantile_turnover[period]:>8.2%}")
|
|
207
|
+
lines.append(f" Mean Autocorrelation: {self.mean_autocorrelation[period]:>8.4f}")
|
|
208
|
+
|
|
209
|
+
if self.half_life[period] is not None:
|
|
210
|
+
lines.append(f" Signal Half-Life: {self.half_life[period]:>8.1f} periods")
|
|
211
|
+
lines.append("")
|
|
212
|
+
|
|
213
|
+
return "\n".join(lines)
|