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,565 @@
|
|
|
1
|
+
"""Unified backtest tearsheet generation.
|
|
2
|
+
|
|
3
|
+
The main entry point for generating comprehensive backtest reports.
|
|
4
|
+
Combines all visualization modules into a single, publication-quality
|
|
5
|
+
HTML document.
|
|
6
|
+
|
|
7
|
+
This is the primary interface users should use:
|
|
8
|
+
from ml4t.diagnostic.visualization.backtest import generate_backtest_tearsheet
|
|
9
|
+
|
|
10
|
+
html = generate_backtest_tearsheet(
|
|
11
|
+
backtest_result,
|
|
12
|
+
template="full",
|
|
13
|
+
theme="default",
|
|
14
|
+
output_path="report.html",
|
|
15
|
+
)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from .template_system import (
|
|
27
|
+
HTML_TEMPLATE,
|
|
28
|
+
TEARSHEET_CSS,
|
|
29
|
+
TearsheetTemplate,
|
|
30
|
+
get_template,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
import plotly.graph_objects as go
|
|
35
|
+
import polars as pl
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def generate_backtest_tearsheet(
|
|
39
|
+
trades: pl.DataFrame | None = None,
|
|
40
|
+
returns: pl.Series | np.ndarray | None = None,
|
|
41
|
+
equity_curve: pl.DataFrame | None = None,
|
|
42
|
+
metrics: dict[str, Any] | None = None,
|
|
43
|
+
output_path: str | Path | None = None,
|
|
44
|
+
template: Literal["quant_trader", "hedge_fund", "risk_manager", "full"] = "full",
|
|
45
|
+
theme: Literal["default", "dark", "print", "presentation"] = "default",
|
|
46
|
+
title: str = "Backtest Analysis Report",
|
|
47
|
+
subtitle: str = "",
|
|
48
|
+
benchmark_returns: pl.Series | np.ndarray | None = None,
|
|
49
|
+
n_trials: int | None = None,
|
|
50
|
+
interactive: bool = True,
|
|
51
|
+
include_plotlyjs: bool = True,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Generate a comprehensive backtest tearsheet.
|
|
54
|
+
|
|
55
|
+
This is the main entry point for creating publication-quality backtest
|
|
56
|
+
reports. It combines all visualization modules into a single HTML document.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
trades : pl.DataFrame, optional
|
|
61
|
+
Trade records with columns like: symbol, entry_time, exit_time,
|
|
62
|
+
pnl, gross_pnl, net_pnl, mfe, mae, exit_reason, duration, size
|
|
63
|
+
returns : pl.Series or np.ndarray, optional
|
|
64
|
+
Daily returns series for portfolio-level analysis
|
|
65
|
+
equity_curve : pl.DataFrame, optional
|
|
66
|
+
Equity curve with date and equity columns
|
|
67
|
+
metrics : dict, optional
|
|
68
|
+
Pre-computed metrics dict with keys like:
|
|
69
|
+
- sharpe, cagr, max_drawdown, win_rate, profit_factor
|
|
70
|
+
- dsr_probability, min_trl, etc. for statistical validity
|
|
71
|
+
output_path : str or Path, optional
|
|
72
|
+
If provided, save HTML to this path
|
|
73
|
+
template : {"quant_trader", "hedge_fund", "risk_manager", "full"}
|
|
74
|
+
Template persona to use (determines which sections are shown)
|
|
75
|
+
theme : {"default", "dark", "print", "presentation"}
|
|
76
|
+
Visual theme for the charts
|
|
77
|
+
title : str
|
|
78
|
+
Report title
|
|
79
|
+
subtitle : str
|
|
80
|
+
Report subtitle (e.g., strategy name, date range)
|
|
81
|
+
benchmark_returns : pl.Series or np.ndarray, optional
|
|
82
|
+
Benchmark returns for comparison
|
|
83
|
+
n_trials : int, optional
|
|
84
|
+
Number of trials for DSR calculation
|
|
85
|
+
interactive : bool
|
|
86
|
+
Whether charts should be interactive (vs static images)
|
|
87
|
+
include_plotlyjs : bool
|
|
88
|
+
Whether to include Plotly.js (set False if already loaded)
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
str
|
|
93
|
+
HTML string of the complete tearsheet
|
|
94
|
+
|
|
95
|
+
Examples
|
|
96
|
+
--------
|
|
97
|
+
>>> from ml4t.diagnostic.visualization.backtest import generate_backtest_tearsheet
|
|
98
|
+
>>>
|
|
99
|
+
>>> # From trades DataFrame
|
|
100
|
+
>>> html = generate_backtest_tearsheet(
|
|
101
|
+
... trades=my_trades,
|
|
102
|
+
... metrics={"sharpe": 1.5, "max_drawdown": -0.15},
|
|
103
|
+
... template="quant_trader",
|
|
104
|
+
... output_path="strategy_report.html",
|
|
105
|
+
... )
|
|
106
|
+
>>>
|
|
107
|
+
>>> # From returns series
|
|
108
|
+
>>> html = generate_backtest_tearsheet(
|
|
109
|
+
... returns=daily_returns,
|
|
110
|
+
... template="risk_manager",
|
|
111
|
+
... n_trials=100, # For DSR
|
|
112
|
+
... )
|
|
113
|
+
"""
|
|
114
|
+
# Get template
|
|
115
|
+
tmpl = get_template(template)
|
|
116
|
+
|
|
117
|
+
# Generate sections HTML
|
|
118
|
+
sections_html = _generate_sections(
|
|
119
|
+
tmpl,
|
|
120
|
+
trades=trades,
|
|
121
|
+
returns=returns,
|
|
122
|
+
equity_curve=equity_curve,
|
|
123
|
+
metrics=metrics,
|
|
124
|
+
benchmark_returns=benchmark_returns,
|
|
125
|
+
n_trials=n_trials,
|
|
126
|
+
theme=theme,
|
|
127
|
+
interactive=interactive,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Generate full HTML - conditionally include Plotly JS
|
|
131
|
+
if include_plotlyjs:
|
|
132
|
+
css = TEARSHEET_CSS
|
|
133
|
+
else:
|
|
134
|
+
css = TEARSHEET_CSS.replace(
|
|
135
|
+
'<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>',
|
|
136
|
+
"",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
html = HTML_TEMPLATE.format(
|
|
140
|
+
theme=theme if theme == "dark" else "light",
|
|
141
|
+
title=title,
|
|
142
|
+
subtitle=subtitle or f"Template: {template}",
|
|
143
|
+
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
144
|
+
css=css,
|
|
145
|
+
sections_html=sections_html,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Save if path provided
|
|
149
|
+
if output_path:
|
|
150
|
+
output_path = Path(output_path)
|
|
151
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
output_path.write_text(html)
|
|
153
|
+
|
|
154
|
+
return html
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _generate_sections(
|
|
158
|
+
template: TearsheetTemplate,
|
|
159
|
+
trades: pl.DataFrame | None = None,
|
|
160
|
+
returns: pl.Series | np.ndarray | None = None,
|
|
161
|
+
equity_curve: pl.DataFrame | None = None,
|
|
162
|
+
metrics: dict[str, Any] | None = None,
|
|
163
|
+
benchmark_returns: pl.Series | np.ndarray | None = None,
|
|
164
|
+
n_trials: int | None = None,
|
|
165
|
+
theme: str = "default",
|
|
166
|
+
interactive: bool = True,
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Generate HTML for all enabled sections."""
|
|
169
|
+
sections_html = []
|
|
170
|
+
|
|
171
|
+
for section in template.get_enabled_sections():
|
|
172
|
+
section_html = _generate_section(
|
|
173
|
+
section.name,
|
|
174
|
+
section.title,
|
|
175
|
+
trades=trades,
|
|
176
|
+
returns=returns,
|
|
177
|
+
equity_curve=equity_curve,
|
|
178
|
+
metrics=metrics,
|
|
179
|
+
benchmark_returns=benchmark_returns,
|
|
180
|
+
n_trials=n_trials,
|
|
181
|
+
theme=theme,
|
|
182
|
+
interactive=interactive,
|
|
183
|
+
)
|
|
184
|
+
if section_html:
|
|
185
|
+
sections_html.append(section_html)
|
|
186
|
+
|
|
187
|
+
return "\n".join(sections_html)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _generate_section(
|
|
191
|
+
section_name: str,
|
|
192
|
+
section_title: str,
|
|
193
|
+
trades: pl.DataFrame | None = None,
|
|
194
|
+
returns: pl.Series | np.ndarray | None = None,
|
|
195
|
+
equity_curve: pl.DataFrame | None = None,
|
|
196
|
+
metrics: dict[str, Any] | None = None,
|
|
197
|
+
benchmark_returns: pl.Series | np.ndarray | None = None,
|
|
198
|
+
n_trials: int | None = None,
|
|
199
|
+
theme: str = "default",
|
|
200
|
+
interactive: bool = True,
|
|
201
|
+
) -> str | None:
|
|
202
|
+
"""Generate HTML for a single section."""
|
|
203
|
+
try:
|
|
204
|
+
fig = _create_section_figure(
|
|
205
|
+
section_name,
|
|
206
|
+
trades=trades,
|
|
207
|
+
returns=returns,
|
|
208
|
+
equity_curve=equity_curve,
|
|
209
|
+
metrics=metrics,
|
|
210
|
+
benchmark_returns=benchmark_returns,
|
|
211
|
+
n_trials=n_trials,
|
|
212
|
+
theme=theme,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if fig is None:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
# Convert figure to HTML
|
|
219
|
+
if interactive:
|
|
220
|
+
fig_html = fig.to_html(full_html=False, include_plotlyjs=False)
|
|
221
|
+
else:
|
|
222
|
+
fig_html = fig.to_html(full_html=False, include_plotlyjs=False)
|
|
223
|
+
|
|
224
|
+
return f"""
|
|
225
|
+
<section class="section">
|
|
226
|
+
<h2 class="section-title">{section_title}</h2>
|
|
227
|
+
<div class="chart-container">
|
|
228
|
+
{fig_html}
|
|
229
|
+
</div>
|
|
230
|
+
</section>
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
# Log error but don't fail the whole report
|
|
235
|
+
return f"""
|
|
236
|
+
<section class="section">
|
|
237
|
+
<h2 class="section-title">{section_title}</h2>
|
|
238
|
+
<div class="chart-container">
|
|
239
|
+
<p style="color: #999;">Section unavailable: {str(e)}</p>
|
|
240
|
+
</div>
|
|
241
|
+
</section>
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _create_section_figure(
|
|
246
|
+
section_name: str,
|
|
247
|
+
trades: pl.DataFrame | None = None,
|
|
248
|
+
returns: pl.Series | np.ndarray | None = None,
|
|
249
|
+
equity_curve: pl.DataFrame | None = None,
|
|
250
|
+
metrics: dict[str, Any] | None = None,
|
|
251
|
+
benchmark_returns: pl.Series | np.ndarray | None = None,
|
|
252
|
+
n_trials: int | None = None,
|
|
253
|
+
theme: str = "default",
|
|
254
|
+
) -> go.Figure | None:
|
|
255
|
+
"""Create the Plotly figure for a specific section."""
|
|
256
|
+
|
|
257
|
+
metrics = metrics or {}
|
|
258
|
+
|
|
259
|
+
# Executive Summary sections
|
|
260
|
+
if section_name == "executive_summary":
|
|
261
|
+
if not metrics:
|
|
262
|
+
return None
|
|
263
|
+
from .executive_summary import create_executive_summary
|
|
264
|
+
|
|
265
|
+
return create_executive_summary(metrics, theme=theme)
|
|
266
|
+
|
|
267
|
+
if section_name == "key_insights":
|
|
268
|
+
if not metrics:
|
|
269
|
+
return None
|
|
270
|
+
from .executive_summary import create_key_insights
|
|
271
|
+
|
|
272
|
+
insights = create_key_insights(metrics)
|
|
273
|
+
# Create a simple text figure for insights
|
|
274
|
+
import plotly.graph_objects as go
|
|
275
|
+
|
|
276
|
+
fig = go.Figure()
|
|
277
|
+
insight_text = "<br>".join([f"• [{i.category.upper()}] {i.message}" for i in insights])
|
|
278
|
+
fig.add_annotation(
|
|
279
|
+
text=insight_text or "No insights available",
|
|
280
|
+
xref="paper",
|
|
281
|
+
yref="paper",
|
|
282
|
+
x=0.5,
|
|
283
|
+
y=0.5,
|
|
284
|
+
showarrow=False,
|
|
285
|
+
font={"size": 14},
|
|
286
|
+
align="left",
|
|
287
|
+
)
|
|
288
|
+
fig.update_layout(
|
|
289
|
+
height=max(150, len(insights) * 40 + 50),
|
|
290
|
+
xaxis={"visible": False},
|
|
291
|
+
yaxis={"visible": False},
|
|
292
|
+
)
|
|
293
|
+
return fig
|
|
294
|
+
|
|
295
|
+
# Trade Analysis sections
|
|
296
|
+
if section_name == "mfe_mae":
|
|
297
|
+
if trades is None:
|
|
298
|
+
return None
|
|
299
|
+
from .trade_plots import plot_mfe_mae_scatter
|
|
300
|
+
|
|
301
|
+
return plot_mfe_mae_scatter(trades, theme=theme)
|
|
302
|
+
|
|
303
|
+
if section_name == "exit_reasons":
|
|
304
|
+
if trades is None:
|
|
305
|
+
return None
|
|
306
|
+
from .trade_plots import plot_exit_reason_breakdown
|
|
307
|
+
|
|
308
|
+
return plot_exit_reason_breakdown(trades, theme=theme)
|
|
309
|
+
|
|
310
|
+
if section_name == "trade_waterfall":
|
|
311
|
+
if trades is None:
|
|
312
|
+
return None
|
|
313
|
+
from .trade_plots import plot_trade_waterfall
|
|
314
|
+
|
|
315
|
+
return plot_trade_waterfall(trades, theme=theme)
|
|
316
|
+
|
|
317
|
+
if section_name == "duration":
|
|
318
|
+
if trades is None:
|
|
319
|
+
return None
|
|
320
|
+
from .trade_plots import plot_trade_duration_distribution
|
|
321
|
+
|
|
322
|
+
return plot_trade_duration_distribution(trades, theme=theme)
|
|
323
|
+
|
|
324
|
+
if section_name == "consecutive":
|
|
325
|
+
if trades is None:
|
|
326
|
+
return None
|
|
327
|
+
from .trade_plots import plot_consecutive_analysis
|
|
328
|
+
|
|
329
|
+
return plot_consecutive_analysis(trades, theme=theme)
|
|
330
|
+
|
|
331
|
+
if section_name == "size_return":
|
|
332
|
+
if trades is None:
|
|
333
|
+
return None
|
|
334
|
+
from .trade_plots import plot_trade_size_vs_return
|
|
335
|
+
|
|
336
|
+
return plot_trade_size_vs_return(trades, theme=theme)
|
|
337
|
+
|
|
338
|
+
# Cost Attribution sections
|
|
339
|
+
if section_name == "cost_waterfall":
|
|
340
|
+
gross_pnl = metrics.get("gross_pnl")
|
|
341
|
+
commission = metrics.get("commission", 0)
|
|
342
|
+
slippage = metrics.get("slippage", 0)
|
|
343
|
+
if gross_pnl is None:
|
|
344
|
+
return None
|
|
345
|
+
from .cost_attribution import plot_cost_waterfall
|
|
346
|
+
|
|
347
|
+
return plot_cost_waterfall(
|
|
348
|
+
gross_pnl=gross_pnl,
|
|
349
|
+
commission=commission,
|
|
350
|
+
slippage=slippage,
|
|
351
|
+
theme=theme,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if section_name == "cost_sensitivity":
|
|
355
|
+
if returns is None:
|
|
356
|
+
return None
|
|
357
|
+
from .cost_attribution import plot_cost_sensitivity
|
|
358
|
+
|
|
359
|
+
return plot_cost_sensitivity(returns, theme=theme)
|
|
360
|
+
|
|
361
|
+
if section_name == "cost_by_asset":
|
|
362
|
+
if trades is None:
|
|
363
|
+
return None
|
|
364
|
+
from .cost_attribution import plot_cost_by_asset
|
|
365
|
+
|
|
366
|
+
return plot_cost_by_asset(trades, theme=theme)
|
|
367
|
+
|
|
368
|
+
# Statistical Validity sections
|
|
369
|
+
if section_name == "statistical_summary":
|
|
370
|
+
from .statistical_validity import plot_statistical_summary_card
|
|
371
|
+
|
|
372
|
+
return plot_statistical_summary_card(metrics, theme=theme)
|
|
373
|
+
|
|
374
|
+
if section_name == "dsr_gauge":
|
|
375
|
+
dsr_prob = metrics.get("dsr_probability")
|
|
376
|
+
sharpe = metrics.get("sharpe")
|
|
377
|
+
if dsr_prob is None or sharpe is None:
|
|
378
|
+
return None
|
|
379
|
+
from .statistical_validity import plot_dsr_gauge
|
|
380
|
+
|
|
381
|
+
return plot_dsr_gauge(
|
|
382
|
+
dsr_probability=dsr_prob,
|
|
383
|
+
observed_sharpe=sharpe,
|
|
384
|
+
expected_max_sharpe=metrics.get("expected_max_sharpe"),
|
|
385
|
+
n_trials=n_trials,
|
|
386
|
+
theme=theme,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
if section_name == "confidence_intervals":
|
|
390
|
+
# Build CI dict from metrics
|
|
391
|
+
ci_metrics = {}
|
|
392
|
+
for key in ["sharpe", "cagr", "max_drawdown"]:
|
|
393
|
+
if key in metrics:
|
|
394
|
+
ci_metrics[key] = {
|
|
395
|
+
"point": metrics[key],
|
|
396
|
+
"lower_95": metrics.get(f"{key}_lower_95", metrics[key] * 0.7),
|
|
397
|
+
"upper_95": metrics.get(f"{key}_upper_95", metrics[key] * 1.3),
|
|
398
|
+
}
|
|
399
|
+
if not ci_metrics:
|
|
400
|
+
return None
|
|
401
|
+
from .statistical_validity import plot_confidence_intervals
|
|
402
|
+
|
|
403
|
+
return plot_confidence_intervals(ci_metrics, theme=theme)
|
|
404
|
+
|
|
405
|
+
if section_name == "min_trl":
|
|
406
|
+
sharpe = metrics.get("sharpe")
|
|
407
|
+
periods = metrics.get("n_periods", metrics.get("n_observations"))
|
|
408
|
+
if sharpe is None or periods is None:
|
|
409
|
+
return None
|
|
410
|
+
from .statistical_validity import plot_minimum_track_record
|
|
411
|
+
|
|
412
|
+
return plot_minimum_track_record(
|
|
413
|
+
observed_sharpe=sharpe,
|
|
414
|
+
current_periods=periods,
|
|
415
|
+
theme=theme,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if section_name == "ras_analysis":
|
|
419
|
+
original_ic = metrics.get("original_ic")
|
|
420
|
+
adjusted_ic = metrics.get("ras_adjusted_ic")
|
|
421
|
+
rademacher = metrics.get("rademacher_complexity")
|
|
422
|
+
if original_ic is None or adjusted_ic is None or rademacher is None:
|
|
423
|
+
return None
|
|
424
|
+
from .statistical_validity import plot_ras_analysis
|
|
425
|
+
|
|
426
|
+
return plot_ras_analysis(
|
|
427
|
+
original_ic=float(original_ic),
|
|
428
|
+
adjusted_ic=float(adjusted_ic),
|
|
429
|
+
rademacher_complexity=float(rademacher),
|
|
430
|
+
theme=theme,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Portfolio-level sections (use existing portfolio viz if available)
|
|
434
|
+
if section_name in (
|
|
435
|
+
"equity_curve",
|
|
436
|
+
"drawdowns",
|
|
437
|
+
"monthly_returns",
|
|
438
|
+
"annual_returns",
|
|
439
|
+
"rolling_metrics",
|
|
440
|
+
):
|
|
441
|
+
# These would integrate with existing portfolio visualization
|
|
442
|
+
# For now, return None to skip
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
if section_name in ("distribution", "tail_risk"):
|
|
446
|
+
# These would integrate with existing statistical visualization
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
# Unknown section
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class BacktestTearsheet:
|
|
454
|
+
"""Object-oriented interface for building tearsheets incrementally.
|
|
455
|
+
|
|
456
|
+
Provides a fluent API for customizing tearsheet content before generation.
|
|
457
|
+
|
|
458
|
+
Examples
|
|
459
|
+
--------
|
|
460
|
+
>>> tearsheet = BacktestTearsheet(template="quant_trader")
|
|
461
|
+
>>> tearsheet.add_trades(my_trades)
|
|
462
|
+
>>> tearsheet.add_metrics({"sharpe": 1.5, "max_drawdown": -0.15})
|
|
463
|
+
>>> tearsheet.enable_section("dsr_gauge")
|
|
464
|
+
>>> html = tearsheet.generate()
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
def __init__(
|
|
468
|
+
self,
|
|
469
|
+
template: Literal["quant_trader", "hedge_fund", "risk_manager", "full"] = "full",
|
|
470
|
+
theme: Literal["default", "dark", "print", "presentation"] = "default",
|
|
471
|
+
title: str = "Backtest Analysis Report",
|
|
472
|
+
):
|
|
473
|
+
"""Initialize tearsheet builder."""
|
|
474
|
+
self.template = get_template(template)
|
|
475
|
+
self.theme = theme
|
|
476
|
+
self.title = title
|
|
477
|
+
self.subtitle = ""
|
|
478
|
+
|
|
479
|
+
# Data
|
|
480
|
+
self.trades: pl.DataFrame | None = None
|
|
481
|
+
self.returns: pl.Series | np.ndarray | None = None
|
|
482
|
+
self.equity_curve: pl.DataFrame | None = None
|
|
483
|
+
self.metrics: dict[str, Any] = {}
|
|
484
|
+
self.benchmark_returns: pl.Series | np.ndarray | None = None
|
|
485
|
+
self.n_trials: int | None = None
|
|
486
|
+
|
|
487
|
+
def add_trades(self, trades: pl.DataFrame) -> BacktestTearsheet:
|
|
488
|
+
"""Add trade records to the tearsheet."""
|
|
489
|
+
self.trades = trades
|
|
490
|
+
return self
|
|
491
|
+
|
|
492
|
+
def add_returns(self, returns: pl.Series | np.ndarray) -> BacktestTearsheet:
|
|
493
|
+
"""Add daily returns series."""
|
|
494
|
+
self.returns = returns
|
|
495
|
+
return self
|
|
496
|
+
|
|
497
|
+
def add_equity_curve(self, equity: pl.DataFrame) -> BacktestTearsheet:
|
|
498
|
+
"""Add equity curve DataFrame."""
|
|
499
|
+
self.equity_curve = equity
|
|
500
|
+
return self
|
|
501
|
+
|
|
502
|
+
def add_metrics(self, metrics: dict[str, Any]) -> BacktestTearsheet:
|
|
503
|
+
"""Add or update metrics dictionary."""
|
|
504
|
+
self.metrics.update(metrics)
|
|
505
|
+
return self
|
|
506
|
+
|
|
507
|
+
def add_benchmark(self, returns: pl.Series | np.ndarray) -> BacktestTearsheet:
|
|
508
|
+
"""Add benchmark returns for comparison."""
|
|
509
|
+
self.benchmark_returns = returns
|
|
510
|
+
return self
|
|
511
|
+
|
|
512
|
+
def set_n_trials(self, n: int) -> BacktestTearsheet:
|
|
513
|
+
"""Set number of trials for DSR calculation."""
|
|
514
|
+
self.n_trials = n
|
|
515
|
+
return self
|
|
516
|
+
|
|
517
|
+
def set_title(self, title: str, subtitle: str = "") -> BacktestTearsheet:
|
|
518
|
+
"""Set report title and subtitle."""
|
|
519
|
+
self.title = title
|
|
520
|
+
self.subtitle = subtitle
|
|
521
|
+
return self
|
|
522
|
+
|
|
523
|
+
def enable_section(self, name: str) -> BacktestTearsheet:
|
|
524
|
+
"""Enable a section by name."""
|
|
525
|
+
self.template.enable_section(name)
|
|
526
|
+
return self
|
|
527
|
+
|
|
528
|
+
def disable_section(self, name: str) -> BacktestTearsheet:
|
|
529
|
+
"""Disable a section by name."""
|
|
530
|
+
self.template.disable_section(name)
|
|
531
|
+
return self
|
|
532
|
+
|
|
533
|
+
def generate(
|
|
534
|
+
self,
|
|
535
|
+
output_path: str | Path | None = None,
|
|
536
|
+
interactive: bool = True,
|
|
537
|
+
) -> str:
|
|
538
|
+
"""Generate the tearsheet HTML.
|
|
539
|
+
|
|
540
|
+
Parameters
|
|
541
|
+
----------
|
|
542
|
+
output_path : str or Path, optional
|
|
543
|
+
If provided, save HTML to this path
|
|
544
|
+
interactive : bool
|
|
545
|
+
Whether charts should be interactive
|
|
546
|
+
|
|
547
|
+
Returns
|
|
548
|
+
-------
|
|
549
|
+
str
|
|
550
|
+
HTML string of the complete tearsheet
|
|
551
|
+
"""
|
|
552
|
+
return generate_backtest_tearsheet(
|
|
553
|
+
trades=self.trades,
|
|
554
|
+
returns=self.returns,
|
|
555
|
+
equity_curve=self.equity_curve,
|
|
556
|
+
metrics=self.metrics,
|
|
557
|
+
output_path=output_path,
|
|
558
|
+
template=self.template.name, # type: ignore
|
|
559
|
+
theme=self.theme,
|
|
560
|
+
title=self.title,
|
|
561
|
+
subtitle=self.subtitle,
|
|
562
|
+
benchmark_returns=self.benchmark_returns,
|
|
563
|
+
n_trials=self.n_trials,
|
|
564
|
+
interactive=interactive,
|
|
565
|
+
)
|