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,625 @@
|
|
|
1
|
+
"""Quantile returns visualization plots.
|
|
2
|
+
|
|
3
|
+
This module provides interactive Plotly visualizations for quantile analysis:
|
|
4
|
+
- plot_quantile_returns_bar: Mean returns by quantile (bar chart)
|
|
5
|
+
- plot_quantile_returns_violin: Return distributions by quantile
|
|
6
|
+
- plot_cumulative_returns: Cumulative returns by quantile over time
|
|
7
|
+
- plot_spread_timeseries: Top-bottom spread time series
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import plotly.graph_objects as go
|
|
17
|
+
|
|
18
|
+
from ml4t.diagnostic.visualization.core import (
|
|
19
|
+
create_base_figure,
|
|
20
|
+
format_percentage,
|
|
21
|
+
get_colorscale,
|
|
22
|
+
get_theme_config,
|
|
23
|
+
validate_theme,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from ml4t.diagnostic.results.signal_results import QuantileAnalysisResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_quantile_colors(n_quantiles: int, theme_config: dict[str, Any]) -> list[str]:
|
|
31
|
+
"""Get diverging colors for quantiles (red → green progression)."""
|
|
32
|
+
# Use a custom diverging scale: red for bottom, gray for middle, green for top
|
|
33
|
+
colors: list[str]
|
|
34
|
+
if n_quantiles <= 5:
|
|
35
|
+
colors = ["#D32F2F", "#F57C00", "#FBC02D", "#689F38", "#388E3C"][:n_quantiles]
|
|
36
|
+
else:
|
|
37
|
+
# Generate more colors via interpolation
|
|
38
|
+
try:
|
|
39
|
+
raw_colors = get_colorscale("rdylgn", n_colors=n_quantiles, reverse=False)
|
|
40
|
+
if isinstance(raw_colors[0], tuple): # Continuous colorscale format
|
|
41
|
+
colors = [str(c[1]) if isinstance(c, tuple) else str(c) for c in raw_colors]
|
|
42
|
+
else:
|
|
43
|
+
colors = [str(c) for c in raw_colors]
|
|
44
|
+
except (ValueError, IndexError):
|
|
45
|
+
# Fallback to theme colorway
|
|
46
|
+
colorway = theme_config.get("colorway", ["#1f77b4"])
|
|
47
|
+
colors = (colorway * ((n_quantiles // len(colorway)) + 1))[:n_quantiles]
|
|
48
|
+
return colors
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def plot_quantile_returns_bar(
|
|
52
|
+
quantile_result: QuantileAnalysisResult,
|
|
53
|
+
period: str | None = None,
|
|
54
|
+
show_error_bars: bool = True,
|
|
55
|
+
show_spread: bool = True,
|
|
56
|
+
theme: str | None = None,
|
|
57
|
+
width: int | None = None,
|
|
58
|
+
height: int | None = None,
|
|
59
|
+
) -> go.Figure:
|
|
60
|
+
"""Plot mean returns by quantile as a bar chart.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
quantile_result : QuantileAnalysisResult
|
|
65
|
+
Quantile analysis result from SignalAnalysis.compute_quantile_analysis()
|
|
66
|
+
period : str | None
|
|
67
|
+
Period to plot (e.g., "1D", "5D"). If None, uses first period.
|
|
68
|
+
show_error_bars : bool, default True
|
|
69
|
+
Show standard error bars
|
|
70
|
+
show_spread : bool, default True
|
|
71
|
+
Show top-bottom spread annotation
|
|
72
|
+
theme : str | None
|
|
73
|
+
Plot theme (default, dark, print, presentation)
|
|
74
|
+
width : int | None
|
|
75
|
+
Figure width in pixels
|
|
76
|
+
height : int | None
|
|
77
|
+
Figure height in pixels
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
go.Figure
|
|
82
|
+
Interactive Plotly figure
|
|
83
|
+
|
|
84
|
+
Examples
|
|
85
|
+
--------
|
|
86
|
+
>>> quantile_result = analyzer.compute_quantile_analysis()
|
|
87
|
+
>>> fig = plot_quantile_returns_bar(quantile_result, period="5D")
|
|
88
|
+
>>> fig.show()
|
|
89
|
+
"""
|
|
90
|
+
theme = validate_theme(theme)
|
|
91
|
+
theme_config = get_theme_config(theme)
|
|
92
|
+
|
|
93
|
+
# Get period data
|
|
94
|
+
periods = quantile_result.periods
|
|
95
|
+
if period is None:
|
|
96
|
+
period = periods[0]
|
|
97
|
+
elif period not in periods:
|
|
98
|
+
raise ValueError(f"Period '{period}' not found. Available: {periods}")
|
|
99
|
+
|
|
100
|
+
n_quantiles = quantile_result.n_quantiles
|
|
101
|
+
quantile_labels = quantile_result.quantile_labels
|
|
102
|
+
mean_returns = quantile_result.mean_returns[period]
|
|
103
|
+
std_returns = quantile_result.std_returns[period]
|
|
104
|
+
counts = quantile_result.count_by_quantile
|
|
105
|
+
|
|
106
|
+
# Get colors
|
|
107
|
+
colors = _get_quantile_colors(n_quantiles, theme_config)
|
|
108
|
+
|
|
109
|
+
# Create figure
|
|
110
|
+
fig = create_base_figure(
|
|
111
|
+
title=f"Mean Returns by Quantile ({period})",
|
|
112
|
+
xaxis_title="Quantile",
|
|
113
|
+
yaxis_title="Mean Forward Return",
|
|
114
|
+
width=width or theme_config["defaults"]["bar_height"],
|
|
115
|
+
height=height or theme_config["defaults"]["bar_height"],
|
|
116
|
+
theme=theme,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Prepare data
|
|
120
|
+
x_labels = quantile_labels
|
|
121
|
+
y_values = [mean_returns.get(q, 0) for q in quantile_labels]
|
|
122
|
+
y_std = [std_returns.get(q, 0) for q in quantile_labels]
|
|
123
|
+
|
|
124
|
+
# Compute standard errors
|
|
125
|
+
y_stderr = []
|
|
126
|
+
for q, std in zip(quantile_labels, y_std, strict=False):
|
|
127
|
+
count = counts.get(q, 1)
|
|
128
|
+
y_stderr.append(std / np.sqrt(count) if count > 0 else 0)
|
|
129
|
+
|
|
130
|
+
# Bar chart
|
|
131
|
+
fig.add_trace(
|
|
132
|
+
go.Bar(
|
|
133
|
+
x=x_labels,
|
|
134
|
+
y=y_values,
|
|
135
|
+
marker_color=colors,
|
|
136
|
+
error_y={
|
|
137
|
+
"type": "data",
|
|
138
|
+
"array": y_stderr,
|
|
139
|
+
"visible": show_error_bars,
|
|
140
|
+
"color": "gray",
|
|
141
|
+
}
|
|
142
|
+
if show_error_bars
|
|
143
|
+
else None,
|
|
144
|
+
hovertemplate=("Quantile: %{x}<br>Mean Return: %{y:.4f}<br><extra></extra>"),
|
|
145
|
+
name="Mean Return",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Zero line
|
|
150
|
+
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5)
|
|
151
|
+
|
|
152
|
+
# Spread annotation
|
|
153
|
+
if show_spread:
|
|
154
|
+
spread = quantile_result.spread_mean.get(period, 0)
|
|
155
|
+
spread_t = quantile_result.spread_t_stat.get(period, 0)
|
|
156
|
+
spread_p = quantile_result.spread_p_value.get(period, 1)
|
|
157
|
+
monotonic = quantile_result.is_monotonic.get(period, False)
|
|
158
|
+
direction = quantile_result.monotonicity_direction.get(period, "none")
|
|
159
|
+
|
|
160
|
+
spread_text = (
|
|
161
|
+
f"<b>Spread Analysis:</b><br>"
|
|
162
|
+
f"Top - Bottom: {format_percentage(spread)}<br>"
|
|
163
|
+
f"t-stat: {spread_t:.2f} (p={spread_p:.4f})<br>"
|
|
164
|
+
f"Monotonic: {'✓ ' + direction if monotonic else '✗ No'}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
fig.add_annotation(
|
|
168
|
+
text=spread_text,
|
|
169
|
+
xref="paper",
|
|
170
|
+
yref="paper",
|
|
171
|
+
x=0.98,
|
|
172
|
+
y=0.98,
|
|
173
|
+
showarrow=False,
|
|
174
|
+
bgcolor="rgba(255,255,255,0.8)" if theme != "dark" else "rgba(50,50,50,0.8)",
|
|
175
|
+
bordercolor="gray",
|
|
176
|
+
borderwidth=1,
|
|
177
|
+
align="left",
|
|
178
|
+
xanchor="right",
|
|
179
|
+
yanchor="top",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Format y-axis as percentage
|
|
183
|
+
fig.update_yaxes(tickformat=".2%")
|
|
184
|
+
|
|
185
|
+
return fig
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def plot_quantile_returns_violin(
|
|
189
|
+
quantile_result: QuantileAnalysisResult,
|
|
190
|
+
factor_data: dict | None = None,
|
|
191
|
+
period: str | None = None,
|
|
192
|
+
show_box: bool = True,
|
|
193
|
+
theme: str | None = None,
|
|
194
|
+
width: int | None = None,
|
|
195
|
+
height: int | None = None,
|
|
196
|
+
) -> go.Figure:
|
|
197
|
+
"""Plot return distributions by quantile as violin plots.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
quantile_result : QuantileAnalysisResult
|
|
202
|
+
Quantile analysis result from SignalAnalysis.compute_quantile_analysis()
|
|
203
|
+
factor_data : dict | None
|
|
204
|
+
Raw factor data dict with 'quantile' and return columns.
|
|
205
|
+
If None, uses synthetic data from result statistics.
|
|
206
|
+
period : str | None
|
|
207
|
+
Period to plot. If None, uses first period.
|
|
208
|
+
show_box : bool, default True
|
|
209
|
+
Show box plot inside violin
|
|
210
|
+
theme : str | None
|
|
211
|
+
Plot theme
|
|
212
|
+
width : int | None
|
|
213
|
+
Figure width
|
|
214
|
+
height : int | None
|
|
215
|
+
Figure height
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
go.Figure
|
|
220
|
+
Interactive Plotly figure
|
|
221
|
+
"""
|
|
222
|
+
theme = validate_theme(theme)
|
|
223
|
+
theme_config = get_theme_config(theme)
|
|
224
|
+
|
|
225
|
+
# Get period data
|
|
226
|
+
periods = quantile_result.periods
|
|
227
|
+
if period is None:
|
|
228
|
+
period = periods[0]
|
|
229
|
+
elif period not in periods:
|
|
230
|
+
raise ValueError(f"Period '{period}' not found. Available: {periods}")
|
|
231
|
+
|
|
232
|
+
n_quantiles = quantile_result.n_quantiles
|
|
233
|
+
quantile_labels = quantile_result.quantile_labels
|
|
234
|
+
colors = _get_quantile_colors(n_quantiles, theme_config)
|
|
235
|
+
|
|
236
|
+
# Create figure
|
|
237
|
+
fig = create_base_figure(
|
|
238
|
+
title=f"Return Distribution by Quantile ({period})",
|
|
239
|
+
xaxis_title="Quantile",
|
|
240
|
+
yaxis_title="Forward Return",
|
|
241
|
+
width=width or theme_config["defaults"]["bar_height"] + 200,
|
|
242
|
+
height=height or theme_config["defaults"]["bar_height"],
|
|
243
|
+
theme=theme,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# If we have raw data, use it; otherwise generate synthetic
|
|
247
|
+
if factor_data is not None and "quantile" in factor_data:
|
|
248
|
+
import polars as pl
|
|
249
|
+
|
|
250
|
+
if isinstance(factor_data, pl.DataFrame):
|
|
251
|
+
# Extract return column for this period
|
|
252
|
+
return_col = period.replace("D", "D_fwd_return")
|
|
253
|
+
for i, q_label in enumerate(quantile_labels):
|
|
254
|
+
q_num = i + 1
|
|
255
|
+
q_data = factor_data.filter(pl.col("quantile") == q_num)
|
|
256
|
+
returns = q_data[return_col].to_numpy()
|
|
257
|
+
returns = returns[~np.isnan(returns)]
|
|
258
|
+
|
|
259
|
+
fig.add_trace(
|
|
260
|
+
go.Violin(
|
|
261
|
+
y=returns,
|
|
262
|
+
name=q_label,
|
|
263
|
+
box_visible=show_box,
|
|
264
|
+
meanline_visible=True,
|
|
265
|
+
fillcolor=colors[i],
|
|
266
|
+
line_color=colors[i],
|
|
267
|
+
opacity=0.6,
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
# Generate synthetic violin data from mean/std
|
|
272
|
+
# This is approximate but useful when raw data isn't available
|
|
273
|
+
mean_returns = quantile_result.mean_returns[period]
|
|
274
|
+
std_returns = quantile_result.std_returns[period]
|
|
275
|
+
counts = quantile_result.count_by_quantile
|
|
276
|
+
|
|
277
|
+
for i, q_label in enumerate(quantile_labels):
|
|
278
|
+
mean = mean_returns.get(q_label, 0)
|
|
279
|
+
std = std_returns.get(q_label, 0.01)
|
|
280
|
+
n = counts.get(q_label, 100)
|
|
281
|
+
|
|
282
|
+
# Generate synthetic sample
|
|
283
|
+
np.random.seed(42 + i) # Reproducible
|
|
284
|
+
synthetic = np.random.normal(mean, std, min(n, 1000))
|
|
285
|
+
|
|
286
|
+
fig.add_trace(
|
|
287
|
+
go.Violin(
|
|
288
|
+
y=synthetic,
|
|
289
|
+
name=q_label,
|
|
290
|
+
box_visible=show_box,
|
|
291
|
+
meanline_visible=True,
|
|
292
|
+
fillcolor=colors[i],
|
|
293
|
+
line_color=colors[i],
|
|
294
|
+
opacity=0.6,
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Zero line
|
|
299
|
+
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5)
|
|
300
|
+
|
|
301
|
+
# Format y-axis
|
|
302
|
+
fig.update_yaxes(tickformat=".2%")
|
|
303
|
+
fig.update_layout(showlegend=False)
|
|
304
|
+
|
|
305
|
+
return fig
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def plot_cumulative_returns(
|
|
309
|
+
quantile_result: QuantileAnalysisResult,
|
|
310
|
+
period: str | None = None,
|
|
311
|
+
show_spread: bool = True,
|
|
312
|
+
theme: str | None = None,
|
|
313
|
+
width: int | None = None,
|
|
314
|
+
height: int | None = None,
|
|
315
|
+
) -> go.Figure:
|
|
316
|
+
"""Plot cumulative returns by quantile over time.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
quantile_result : QuantileAnalysisResult
|
|
321
|
+
Quantile analysis result with cumulative_returns computed.
|
|
322
|
+
period : str | None
|
|
323
|
+
Period to plot. If None, uses first period.
|
|
324
|
+
show_spread : bool, default True
|
|
325
|
+
Show top-bottom spread as shaded area
|
|
326
|
+
theme : str | None
|
|
327
|
+
Plot theme
|
|
328
|
+
width : int | None
|
|
329
|
+
Figure width
|
|
330
|
+
height : int | None
|
|
331
|
+
Figure height
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
go.Figure
|
|
336
|
+
Interactive Plotly figure
|
|
337
|
+
|
|
338
|
+
Raises
|
|
339
|
+
------
|
|
340
|
+
ValueError
|
|
341
|
+
If cumulative_returns not available in result
|
|
342
|
+
"""
|
|
343
|
+
theme = validate_theme(theme)
|
|
344
|
+
theme_config = get_theme_config(theme)
|
|
345
|
+
|
|
346
|
+
# Check for cumulative returns
|
|
347
|
+
if quantile_result.cumulative_returns is None:
|
|
348
|
+
raise ValueError(
|
|
349
|
+
"Cumulative returns not computed. Set cumulative_returns=True in SignalConfig."
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Get period data
|
|
353
|
+
periods = quantile_result.periods
|
|
354
|
+
if period is None:
|
|
355
|
+
period = periods[0]
|
|
356
|
+
elif period not in periods:
|
|
357
|
+
raise ValueError(f"Period '{period}' not found. Available: {periods}")
|
|
358
|
+
|
|
359
|
+
n_quantiles = quantile_result.n_quantiles
|
|
360
|
+
quantile_labels = quantile_result.quantile_labels
|
|
361
|
+
colors = _get_quantile_colors(n_quantiles, theme_config)
|
|
362
|
+
|
|
363
|
+
cum_returns = quantile_result.cumulative_returns[period]
|
|
364
|
+
dates_raw = quantile_result.cumulative_dates
|
|
365
|
+
|
|
366
|
+
# Convert dates or create fallback indices
|
|
367
|
+
if dates_raw is not None and len(dates_raw) > 0:
|
|
368
|
+
if isinstance(dates_raw[0], str):
|
|
369
|
+
try:
|
|
370
|
+
dates: list[Any] = [datetime.fromisoformat(d) for d in dates_raw]
|
|
371
|
+
except ValueError:
|
|
372
|
+
dates = list(dates_raw)
|
|
373
|
+
else:
|
|
374
|
+
dates = list(dates_raw)
|
|
375
|
+
else:
|
|
376
|
+
# Fallback to integer indices if no dates provided
|
|
377
|
+
max_len = max(len(v) for v in cum_returns.values()) if cum_returns else 0
|
|
378
|
+
dates = list(range(max_len))
|
|
379
|
+
|
|
380
|
+
# Create figure
|
|
381
|
+
fig = create_base_figure(
|
|
382
|
+
title=f"Cumulative Returns by Quantile ({period})",
|
|
383
|
+
xaxis_title="Date",
|
|
384
|
+
yaxis_title="Cumulative Return",
|
|
385
|
+
width=width or theme_config["defaults"]["line_height"] + 300,
|
|
386
|
+
height=height or theme_config["defaults"]["line_height"],
|
|
387
|
+
theme=theme,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# Plot each quantile
|
|
391
|
+
for i, q_label in enumerate(quantile_labels):
|
|
392
|
+
cum_ret = cum_returns.get(q_label, [])
|
|
393
|
+
if len(cum_ret) == 0:
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
fig.add_trace(
|
|
397
|
+
go.Scatter(
|
|
398
|
+
x=dates[: len(cum_ret)],
|
|
399
|
+
y=cum_ret,
|
|
400
|
+
mode="lines",
|
|
401
|
+
name=q_label,
|
|
402
|
+
line={"color": colors[i], "width": 2},
|
|
403
|
+
hovertemplate=f"{q_label}<br>Date: %{{x}}<br>Cum. Return: %{{y:.2%}}<extra></extra>",
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Spread area (top minus bottom)
|
|
408
|
+
if show_spread and n_quantiles >= 2:
|
|
409
|
+
top_ret = cum_returns.get(quantile_labels[-1], [])
|
|
410
|
+
bottom_ret = cum_returns.get(quantile_labels[0], [])
|
|
411
|
+
|
|
412
|
+
if len(top_ret) > 0 and len(bottom_ret) > 0:
|
|
413
|
+
min_len = min(len(top_ret), len(bottom_ret))
|
|
414
|
+
spread = [top_ret[i] - bottom_ret[i] for i in range(min_len)]
|
|
415
|
+
|
|
416
|
+
fig.add_trace(
|
|
417
|
+
go.Scatter(
|
|
418
|
+
x=dates[:min_len],
|
|
419
|
+
y=spread,
|
|
420
|
+
mode="lines",
|
|
421
|
+
name="Spread (Top - Bottom)",
|
|
422
|
+
line={"color": "purple", "width": 2, "dash": "dash"},
|
|
423
|
+
hovertemplate="Spread<br>Date: %{x}<br>Spread: %{y:.2%}<extra></extra>",
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Zero line
|
|
428
|
+
fig.add_hline(y=0, line_dash="dot", line_color="gray", opacity=0.5)
|
|
429
|
+
|
|
430
|
+
# Format y-axis
|
|
431
|
+
fig.update_yaxes(tickformat=".0%")
|
|
432
|
+
|
|
433
|
+
fig.update_layout(
|
|
434
|
+
legend={
|
|
435
|
+
"yanchor": "top",
|
|
436
|
+
"y": 0.99,
|
|
437
|
+
"xanchor": "left",
|
|
438
|
+
"x": 0.01,
|
|
439
|
+
},
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return fig
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def plot_spread_timeseries(
|
|
446
|
+
quantile_result: QuantileAnalysisResult,
|
|
447
|
+
period: str | None = None,
|
|
448
|
+
rolling_window: int = 21,
|
|
449
|
+
show_confidence: bool = True,
|
|
450
|
+
theme: str | None = None,
|
|
451
|
+
width: int | None = None,
|
|
452
|
+
height: int | None = None,
|
|
453
|
+
) -> go.Figure:
|
|
454
|
+
"""Plot top-bottom spread over time with rolling statistics.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
quantile_result : QuantileAnalysisResult
|
|
459
|
+
Quantile analysis result with cumulative_returns computed.
|
|
460
|
+
period : str | None
|
|
461
|
+
Period to plot. If None, uses first period.
|
|
462
|
+
rolling_window : int, default 21
|
|
463
|
+
Window for rolling mean/std calculation
|
|
464
|
+
show_confidence : bool, default True
|
|
465
|
+
Show confidence band around rolling mean
|
|
466
|
+
theme : str | None
|
|
467
|
+
Plot theme
|
|
468
|
+
width : int | None
|
|
469
|
+
Figure width
|
|
470
|
+
height : int | None
|
|
471
|
+
Figure height
|
|
472
|
+
|
|
473
|
+
Returns
|
|
474
|
+
-------
|
|
475
|
+
go.Figure
|
|
476
|
+
Interactive Plotly figure
|
|
477
|
+
"""
|
|
478
|
+
theme = validate_theme(theme)
|
|
479
|
+
theme_config = get_theme_config(theme)
|
|
480
|
+
|
|
481
|
+
# Check for cumulative returns
|
|
482
|
+
if quantile_result.cumulative_returns is None:
|
|
483
|
+
raise ValueError(
|
|
484
|
+
"Cumulative returns not computed. Set cumulative_returns=True in SignalConfig."
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Get period data
|
|
488
|
+
periods = quantile_result.periods
|
|
489
|
+
if period is None:
|
|
490
|
+
period = periods[0]
|
|
491
|
+
elif period not in periods:
|
|
492
|
+
raise ValueError(f"Period '{period}' not found. Available: {periods}")
|
|
493
|
+
|
|
494
|
+
quantile_labels = quantile_result.quantile_labels
|
|
495
|
+
cum_returns = quantile_result.cumulative_returns[period]
|
|
496
|
+
dates_raw = quantile_result.cumulative_dates
|
|
497
|
+
|
|
498
|
+
# Convert dates or create fallback indices
|
|
499
|
+
if dates_raw is not None and len(dates_raw) > 0:
|
|
500
|
+
if isinstance(dates_raw[0], str):
|
|
501
|
+
try:
|
|
502
|
+
dates: list[Any] = [datetime.fromisoformat(d) for d in dates_raw]
|
|
503
|
+
except ValueError:
|
|
504
|
+
dates = list(dates_raw)
|
|
505
|
+
else:
|
|
506
|
+
dates = list(dates_raw)
|
|
507
|
+
else:
|
|
508
|
+
# Fallback to integer indices if no dates provided
|
|
509
|
+
max_len = max(len(v) for v in cum_returns.values()) if cum_returns else 0
|
|
510
|
+
dates = list(range(max_len))
|
|
511
|
+
|
|
512
|
+
# Compute daily spread (difference in daily returns)
|
|
513
|
+
top_cum = np.array(cum_returns.get(quantile_labels[-1], []))
|
|
514
|
+
bottom_cum = np.array(cum_returns.get(quantile_labels[0], []))
|
|
515
|
+
|
|
516
|
+
if len(top_cum) < 2 or len(bottom_cum) < 2:
|
|
517
|
+
raise ValueError("Insufficient data for spread calculation")
|
|
518
|
+
|
|
519
|
+
min_len = min(len(top_cum), len(bottom_cum))
|
|
520
|
+
top_cum = top_cum[:min_len]
|
|
521
|
+
bottom_cum = bottom_cum[:min_len]
|
|
522
|
+
dates = dates[:min_len]
|
|
523
|
+
|
|
524
|
+
# Daily returns from cumulative
|
|
525
|
+
top_daily = np.diff(top_cum, prepend=0)
|
|
526
|
+
bottom_daily = np.diff(bottom_cum, prepend=0)
|
|
527
|
+
spread_daily = top_daily - bottom_daily
|
|
528
|
+
|
|
529
|
+
# Rolling statistics
|
|
530
|
+
rolling_mean = np.full_like(spread_daily, np.nan)
|
|
531
|
+
rolling_std = np.full_like(spread_daily, np.nan)
|
|
532
|
+
|
|
533
|
+
for i in range(rolling_window - 1, len(spread_daily)):
|
|
534
|
+
window = spread_daily[i - rolling_window + 1 : i + 1]
|
|
535
|
+
rolling_mean[i] = np.mean(window)
|
|
536
|
+
rolling_std[i] = np.std(window, ddof=1)
|
|
537
|
+
|
|
538
|
+
# Create figure
|
|
539
|
+
fig = create_base_figure(
|
|
540
|
+
title=f"Spread Time Series ({period}) - Top vs Bottom Quantile",
|
|
541
|
+
xaxis_title="Date",
|
|
542
|
+
yaxis_title="Daily Spread Return",
|
|
543
|
+
width=width or theme_config["defaults"]["line_height"] + 300,
|
|
544
|
+
height=height or theme_config["defaults"]["line_height"],
|
|
545
|
+
theme=theme,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# Daily spread scatter
|
|
549
|
+
fig.add_trace(
|
|
550
|
+
go.Scatter(
|
|
551
|
+
x=dates,
|
|
552
|
+
y=spread_daily,
|
|
553
|
+
mode="markers",
|
|
554
|
+
name="Daily Spread",
|
|
555
|
+
marker={
|
|
556
|
+
"size": 4,
|
|
557
|
+
"color": theme_config["colorway"][0],
|
|
558
|
+
"opacity": 0.4,
|
|
559
|
+
},
|
|
560
|
+
hovertemplate="Date: %{x}<br>Spread: %{y:.4f}<extra></extra>",
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Rolling mean
|
|
565
|
+
fig.add_trace(
|
|
566
|
+
go.Scatter(
|
|
567
|
+
x=dates,
|
|
568
|
+
y=rolling_mean,
|
|
569
|
+
mode="lines",
|
|
570
|
+
name=f"{rolling_window}-Day Rolling Mean",
|
|
571
|
+
line={"color": theme_config["colorway"][1], "width": 2},
|
|
572
|
+
hovertemplate="Date: %{x}<br>Rolling Mean: %{y:.4f}<extra></extra>",
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Confidence band
|
|
577
|
+
if show_confidence:
|
|
578
|
+
upper = rolling_mean + 1.96 * rolling_std
|
|
579
|
+
lower = rolling_mean - 1.96 * rolling_std
|
|
580
|
+
|
|
581
|
+
fig.add_trace(
|
|
582
|
+
go.Scatter(
|
|
583
|
+
x=list(dates) + list(reversed(dates)),
|
|
584
|
+
y=list(upper) + list(reversed(lower)),
|
|
585
|
+
fill="toself",
|
|
586
|
+
fillcolor="rgba(128, 128, 128, 0.2)",
|
|
587
|
+
line={"width": 0},
|
|
588
|
+
showlegend=True,
|
|
589
|
+
name="95% CI",
|
|
590
|
+
hoverinfo="skip",
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# Zero line
|
|
595
|
+
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5)
|
|
596
|
+
|
|
597
|
+
# Summary statistics
|
|
598
|
+
mean_spread = quantile_result.spread_mean.get(period, 0)
|
|
599
|
+
t_stat = quantile_result.spread_t_stat.get(period, 0)
|
|
600
|
+
p_value = quantile_result.spread_p_value.get(period, 1)
|
|
601
|
+
|
|
602
|
+
summary_text = (
|
|
603
|
+
f"<b>Spread Statistics:</b><br>"
|
|
604
|
+
f"Mean: {format_percentage(mean_spread)}<br>"
|
|
605
|
+
f"t-stat: {t_stat:.2f}<br>"
|
|
606
|
+
f"p-value: {p_value:.4f}"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
fig.add_annotation(
|
|
610
|
+
text=summary_text,
|
|
611
|
+
xref="paper",
|
|
612
|
+
yref="paper",
|
|
613
|
+
x=0.02,
|
|
614
|
+
y=0.98,
|
|
615
|
+
showarrow=False,
|
|
616
|
+
bgcolor="rgba(255,255,255,0.8)" if theme != "dark" else "rgba(50,50,50,0.8)",
|
|
617
|
+
bordercolor="gray",
|
|
618
|
+
borderwidth=1,
|
|
619
|
+
align="left",
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# Format y-axis
|
|
623
|
+
fig.update_yaxes(tickformat=".2%")
|
|
624
|
+
|
|
625
|
+
return fig
|