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,635 @@
|
|
|
1
|
+
"""IC (Information Coefficient) visualization plots.
|
|
2
|
+
|
|
3
|
+
This module provides interactive Plotly visualizations for IC analysis:
|
|
4
|
+
- plot_ic_ts: IC time series with rolling mean and significance bands
|
|
5
|
+
- plot_ic_histogram: IC distribution with mean and confidence intervals
|
|
6
|
+
- plot_ic_qq: Q-Q plot for normality assessment
|
|
7
|
+
- plot_ic_heatmap: Monthly IC heatmap for seasonality analysis
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import plotly.graph_objects as go
|
|
17
|
+
from scipy import stats
|
|
18
|
+
|
|
19
|
+
from ml4t.diagnostic.visualization.core import (
|
|
20
|
+
create_base_figure,
|
|
21
|
+
format_percentage,
|
|
22
|
+
get_colorscale,
|
|
23
|
+
get_theme_config,
|
|
24
|
+
validate_theme,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from ml4t.diagnostic.results.signal_results import SignalICResult
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def plot_ic_ts(
|
|
32
|
+
ic_result: SignalICResult,
|
|
33
|
+
period: str | None = None,
|
|
34
|
+
rolling_window: int = 21,
|
|
35
|
+
show_significance: bool = True,
|
|
36
|
+
significance_level: float = 0.05,
|
|
37
|
+
theme: str | None = None,
|
|
38
|
+
width: int | None = None,
|
|
39
|
+
height: int | None = None,
|
|
40
|
+
) -> go.Figure:
|
|
41
|
+
"""Plot IC time series with rolling mean and significance bands.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
ic_result : SignalICResult
|
|
46
|
+
IC analysis result from SignalAnalysis.compute_ic_analysis()
|
|
47
|
+
period : str | None
|
|
48
|
+
Period to plot (e.g., "1D", "5D"). If None, uses first period.
|
|
49
|
+
rolling_window : int, default 21
|
|
50
|
+
Window size for rolling mean calculation
|
|
51
|
+
show_significance : bool, default True
|
|
52
|
+
Show significance bands (±1.96 * std for 95% CI)
|
|
53
|
+
significance_level : float, default 0.05
|
|
54
|
+
Significance level for bands (0.05 = 95% CI)
|
|
55
|
+
theme : str | None
|
|
56
|
+
Plot theme (default, dark, print, presentation)
|
|
57
|
+
width : int | None
|
|
58
|
+
Figure width in pixels
|
|
59
|
+
height : int | None
|
|
60
|
+
Figure height in pixels
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
go.Figure
|
|
65
|
+
Interactive Plotly figure
|
|
66
|
+
|
|
67
|
+
Examples
|
|
68
|
+
--------
|
|
69
|
+
>>> ic_result = analyzer.compute_ic_analysis()
|
|
70
|
+
>>> fig = plot_ic_ts(ic_result, period="5D", rolling_window=21)
|
|
71
|
+
>>> fig.show()
|
|
72
|
+
"""
|
|
73
|
+
theme = validate_theme(theme)
|
|
74
|
+
theme_config = get_theme_config(theme)
|
|
75
|
+
|
|
76
|
+
# Get period data
|
|
77
|
+
periods = list(ic_result.ic_by_date.keys())
|
|
78
|
+
if period is None:
|
|
79
|
+
period = periods[0]
|
|
80
|
+
elif period not in periods:
|
|
81
|
+
raise ValueError(f"Period '{period}' not found. Available: {periods}")
|
|
82
|
+
|
|
83
|
+
ic_series = np.array(ic_result.ic_by_date[period])
|
|
84
|
+
dates = ic_result.dates
|
|
85
|
+
|
|
86
|
+
# Convert dates to datetime if strings
|
|
87
|
+
if dates and isinstance(dates[0], str):
|
|
88
|
+
try:
|
|
89
|
+
dates = [datetime.fromisoformat(d) for d in dates]
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass # Keep as strings if conversion fails
|
|
92
|
+
|
|
93
|
+
# Compute rolling mean
|
|
94
|
+
valid_mask = ~np.isnan(ic_series)
|
|
95
|
+
np.where(valid_mask, ic_series, 0)
|
|
96
|
+
|
|
97
|
+
rolling_mean = np.full_like(ic_series, np.nan)
|
|
98
|
+
for i in range(rolling_window - 1, len(ic_series)):
|
|
99
|
+
window = ic_series[i - rolling_window + 1 : i + 1]
|
|
100
|
+
window_valid = window[~np.isnan(window)]
|
|
101
|
+
if len(window_valid) >= rolling_window // 2:
|
|
102
|
+
rolling_mean[i] = np.mean(window_valid)
|
|
103
|
+
|
|
104
|
+
# Create figure
|
|
105
|
+
fig = create_base_figure(
|
|
106
|
+
title=f"IC Time Series ({period})",
|
|
107
|
+
xaxis_title="Date",
|
|
108
|
+
yaxis_title="Information Coefficient",
|
|
109
|
+
width=width or theme_config["defaults"]["line_height"] + 300,
|
|
110
|
+
height=height or theme_config["defaults"]["line_height"],
|
|
111
|
+
theme=theme,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# IC scatter points
|
|
115
|
+
fig.add_trace(
|
|
116
|
+
go.Scatter(
|
|
117
|
+
x=dates,
|
|
118
|
+
y=ic_series,
|
|
119
|
+
mode="markers",
|
|
120
|
+
name="Daily IC",
|
|
121
|
+
marker={
|
|
122
|
+
"size": 4,
|
|
123
|
+
"color": theme_config["colorway"][0],
|
|
124
|
+
"opacity": 0.5,
|
|
125
|
+
},
|
|
126
|
+
hovertemplate="Date: %{x}<br>IC: %{y:.4f}<extra></extra>",
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Rolling mean line
|
|
131
|
+
fig.add_trace(
|
|
132
|
+
go.Scatter(
|
|
133
|
+
x=dates,
|
|
134
|
+
y=rolling_mean,
|
|
135
|
+
mode="lines",
|
|
136
|
+
name=f"{rolling_window}-Day Rolling Mean",
|
|
137
|
+
line={
|
|
138
|
+
"color": theme_config["colorway"][1],
|
|
139
|
+
"width": 2,
|
|
140
|
+
},
|
|
141
|
+
hovertemplate="Date: %{x}<br>Rolling IC: %{y:.4f}<extra></extra>",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Zero line
|
|
146
|
+
fig.add_hline(
|
|
147
|
+
y=0,
|
|
148
|
+
line_dash="dash",
|
|
149
|
+
line_color="gray",
|
|
150
|
+
opacity=0.5,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Mean IC line
|
|
154
|
+
mean_ic = ic_result.ic_mean.get(period, 0)
|
|
155
|
+
fig.add_hline(
|
|
156
|
+
y=mean_ic,
|
|
157
|
+
line_dash="dot",
|
|
158
|
+
line_color=theme_config["colorway"][2],
|
|
159
|
+
annotation_text=f"Mean IC: {mean_ic:.4f}",
|
|
160
|
+
annotation_position="right",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Significance bands
|
|
164
|
+
if show_significance:
|
|
165
|
+
ic_std = ic_result.ic_std.get(period, 0)
|
|
166
|
+
z_score = stats.norm.ppf(1 - significance_level / 2)
|
|
167
|
+
upper = z_score * ic_std / np.sqrt(len(ic_series))
|
|
168
|
+
lower = -upper
|
|
169
|
+
|
|
170
|
+
fig.add_hrect(
|
|
171
|
+
y0=lower,
|
|
172
|
+
y1=upper,
|
|
173
|
+
fillcolor="gray",
|
|
174
|
+
opacity=0.1,
|
|
175
|
+
line_width=0,
|
|
176
|
+
annotation_text=f"{100 * (1 - significance_level):.0f}% CI",
|
|
177
|
+
annotation_position="top right",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Add summary annotation
|
|
181
|
+
positive_pct = ic_result.ic_positive_pct.get(period, 0)
|
|
182
|
+
ir = ic_result.ic_ir.get(period, 0)
|
|
183
|
+
t_stat = ic_result.ic_t_stat.get(period, 0)
|
|
184
|
+
p_value = ic_result.ic_p_value.get(period, 1)
|
|
185
|
+
|
|
186
|
+
summary_text = (
|
|
187
|
+
f"<b>Summary:</b><br>"
|
|
188
|
+
f"Mean IC: {mean_ic:.4f}<br>"
|
|
189
|
+
f"IC IR: {ir:.3f}<br>"
|
|
190
|
+
f"Positive %: {format_percentage(positive_pct)}<br>"
|
|
191
|
+
f"t-stat: {t_stat:.2f} (p={p_value:.4f})"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
fig.add_annotation(
|
|
195
|
+
text=summary_text,
|
|
196
|
+
xref="paper",
|
|
197
|
+
yref="paper",
|
|
198
|
+
x=0.02,
|
|
199
|
+
y=0.98,
|
|
200
|
+
showarrow=False,
|
|
201
|
+
bgcolor="rgba(255,255,255,0.8)" if theme != "dark" else "rgba(50,50,50,0.8)",
|
|
202
|
+
bordercolor="gray",
|
|
203
|
+
borderwidth=1,
|
|
204
|
+
align="left",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
fig.update_layout(
|
|
208
|
+
legend={
|
|
209
|
+
"yanchor": "top",
|
|
210
|
+
"y": 0.99,
|
|
211
|
+
"xanchor": "right",
|
|
212
|
+
"x": 0.99,
|
|
213
|
+
},
|
|
214
|
+
showlegend=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return fig
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def plot_ic_histogram(
|
|
221
|
+
ic_result: SignalICResult,
|
|
222
|
+
period: str | None = None,
|
|
223
|
+
bins: int = 50,
|
|
224
|
+
show_kde: bool = True,
|
|
225
|
+
show_stats: bool = True,
|
|
226
|
+
theme: str | None = None,
|
|
227
|
+
width: int | None = None,
|
|
228
|
+
height: int | None = None,
|
|
229
|
+
) -> go.Figure:
|
|
230
|
+
"""Plot IC distribution histogram with optional KDE.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
ic_result : SignalICResult
|
|
235
|
+
IC analysis result from SignalAnalysis.compute_ic_analysis()
|
|
236
|
+
period : str | None
|
|
237
|
+
Period to plot. If None, uses first period.
|
|
238
|
+
bins : int, default 50
|
|
239
|
+
Number of histogram bins
|
|
240
|
+
show_kde : bool, default True
|
|
241
|
+
Show kernel density estimate overlay
|
|
242
|
+
show_stats : bool, default True
|
|
243
|
+
Show summary statistics annotation
|
|
244
|
+
theme : str | None
|
|
245
|
+
Plot theme
|
|
246
|
+
width : int | None
|
|
247
|
+
Figure width
|
|
248
|
+
height : int | None
|
|
249
|
+
Figure height
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
go.Figure
|
|
254
|
+
Interactive Plotly figure
|
|
255
|
+
"""
|
|
256
|
+
theme = validate_theme(theme)
|
|
257
|
+
theme_config = get_theme_config(theme)
|
|
258
|
+
|
|
259
|
+
# Get period data
|
|
260
|
+
periods = list(ic_result.ic_by_date.keys())
|
|
261
|
+
if period is None:
|
|
262
|
+
period = periods[0]
|
|
263
|
+
elif period not in periods:
|
|
264
|
+
raise ValueError(f"Period '{period}' not found. Available: {periods}")
|
|
265
|
+
|
|
266
|
+
ic_series = np.array(ic_result.ic_by_date[period])
|
|
267
|
+
ic_clean = ic_series[~np.isnan(ic_series)]
|
|
268
|
+
|
|
269
|
+
# Create figure
|
|
270
|
+
fig = create_base_figure(
|
|
271
|
+
title=f"IC Distribution ({period})",
|
|
272
|
+
xaxis_title="Information Coefficient",
|
|
273
|
+
yaxis_title="Frequency",
|
|
274
|
+
width=width or theme_config["defaults"]["bar_height"],
|
|
275
|
+
height=height or theme_config["defaults"]["bar_height"],
|
|
276
|
+
theme=theme,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Histogram
|
|
280
|
+
fig.add_trace(
|
|
281
|
+
go.Histogram(
|
|
282
|
+
x=ic_clean,
|
|
283
|
+
nbinsx=bins,
|
|
284
|
+
name="IC Distribution",
|
|
285
|
+
marker_color=theme_config["colorway"][0],
|
|
286
|
+
opacity=0.7,
|
|
287
|
+
hovertemplate="IC: %{x:.4f}<br>Count: %{y}<extra></extra>",
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# KDE overlay
|
|
292
|
+
if show_kde and len(ic_clean) > 10:
|
|
293
|
+
kde = stats.gaussian_kde(ic_clean)
|
|
294
|
+
x_kde = np.linspace(ic_clean.min(), ic_clean.max(), 200)
|
|
295
|
+
y_kde = kde(x_kde)
|
|
296
|
+
|
|
297
|
+
# Scale KDE to match histogram
|
|
298
|
+
hist_counts, _ = np.histogram(ic_clean, bins=bins)
|
|
299
|
+
bin_width = (ic_clean.max() - ic_clean.min()) / bins
|
|
300
|
+
y_kde_scaled = y_kde * len(ic_clean) * bin_width
|
|
301
|
+
|
|
302
|
+
fig.add_trace(
|
|
303
|
+
go.Scatter(
|
|
304
|
+
x=x_kde,
|
|
305
|
+
y=y_kde_scaled,
|
|
306
|
+
mode="lines",
|
|
307
|
+
name="KDE",
|
|
308
|
+
line={"color": theme_config["colorway"][1], "width": 2},
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Mean line
|
|
313
|
+
mean_ic = ic_result.ic_mean.get(period, 0)
|
|
314
|
+
fig.add_vline(
|
|
315
|
+
x=mean_ic,
|
|
316
|
+
line_dash="dash",
|
|
317
|
+
line_color=theme_config["colorway"][2],
|
|
318
|
+
annotation_text=f"Mean: {mean_ic:.4f}",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Zero line
|
|
322
|
+
fig.add_vline(
|
|
323
|
+
x=0,
|
|
324
|
+
line_dash="dot",
|
|
325
|
+
line_color="gray",
|
|
326
|
+
opacity=0.7,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Statistics annotation
|
|
330
|
+
if show_stats:
|
|
331
|
+
ic_std = ic_result.ic_std.get(period, 0)
|
|
332
|
+
skewness = float(stats.skew(ic_clean)) if len(ic_clean) > 2 else 0
|
|
333
|
+
kurtosis = float(stats.kurtosis(ic_clean)) if len(ic_clean) > 3 else 0
|
|
334
|
+
|
|
335
|
+
stats_text = (
|
|
336
|
+
f"<b>Statistics:</b><br>"
|
|
337
|
+
f"N: {len(ic_clean)}<br>"
|
|
338
|
+
f"Mean: {mean_ic:.4f}<br>"
|
|
339
|
+
f"Std: {ic_std:.4f}<br>"
|
|
340
|
+
f"Skew: {skewness:.3f}<br>"
|
|
341
|
+
f"Kurt: {kurtosis:.3f}"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
fig.add_annotation(
|
|
345
|
+
text=stats_text,
|
|
346
|
+
xref="paper",
|
|
347
|
+
yref="paper",
|
|
348
|
+
x=0.98,
|
|
349
|
+
y=0.98,
|
|
350
|
+
showarrow=False,
|
|
351
|
+
bgcolor="rgba(255,255,255,0.8)" if theme != "dark" else "rgba(50,50,50,0.8)",
|
|
352
|
+
bordercolor="gray",
|
|
353
|
+
borderwidth=1,
|
|
354
|
+
align="left",
|
|
355
|
+
xanchor="right",
|
|
356
|
+
yanchor="top",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return fig
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def plot_ic_qq(
|
|
363
|
+
ic_result: SignalICResult,
|
|
364
|
+
period: str | None = None,
|
|
365
|
+
theme: str | None = None,
|
|
366
|
+
width: int | None = None,
|
|
367
|
+
height: int | None = None,
|
|
368
|
+
) -> go.Figure:
|
|
369
|
+
"""Plot Q-Q plot for IC normality assessment.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
ic_result : SignalICResult
|
|
374
|
+
IC analysis result from SignalAnalysis.compute_ic_analysis()
|
|
375
|
+
period : str | None
|
|
376
|
+
Period to plot. If None, uses first period.
|
|
377
|
+
theme : str | None
|
|
378
|
+
Plot theme
|
|
379
|
+
width : int | None
|
|
380
|
+
Figure width
|
|
381
|
+
height : int | None
|
|
382
|
+
Figure height
|
|
383
|
+
|
|
384
|
+
Returns
|
|
385
|
+
-------
|
|
386
|
+
go.Figure
|
|
387
|
+
Interactive Plotly Q-Q plot
|
|
388
|
+
"""
|
|
389
|
+
theme = validate_theme(theme)
|
|
390
|
+
theme_config = get_theme_config(theme)
|
|
391
|
+
|
|
392
|
+
# Get period data
|
|
393
|
+
periods = list(ic_result.ic_by_date.keys())
|
|
394
|
+
if period is None:
|
|
395
|
+
period = periods[0]
|
|
396
|
+
elif period not in periods:
|
|
397
|
+
raise ValueError(f"Period '{period}' not found. Available: {periods}")
|
|
398
|
+
|
|
399
|
+
ic_series = np.array(ic_result.ic_by_date[period])
|
|
400
|
+
ic_clean = ic_series[~np.isnan(ic_series)]
|
|
401
|
+
ic_sorted = np.sort(ic_clean)
|
|
402
|
+
|
|
403
|
+
# Theoretical quantiles
|
|
404
|
+
n = len(ic_sorted)
|
|
405
|
+
theoretical_quantiles = stats.norm.ppf(
|
|
406
|
+
(np.arange(1, n + 1) - 0.5) / n,
|
|
407
|
+
loc=np.mean(ic_clean),
|
|
408
|
+
scale=np.std(ic_clean, ddof=1),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Create figure
|
|
412
|
+
fig = create_base_figure(
|
|
413
|
+
title=f"IC Q-Q Plot ({period})",
|
|
414
|
+
xaxis_title="Theoretical Quantiles (Normal)",
|
|
415
|
+
yaxis_title="Sample Quantiles (IC)",
|
|
416
|
+
width=width or theme_config["defaults"]["scatter_height"],
|
|
417
|
+
height=height or theme_config["defaults"]["scatter_height"],
|
|
418
|
+
theme=theme,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Q-Q scatter
|
|
422
|
+
fig.add_trace(
|
|
423
|
+
go.Scatter(
|
|
424
|
+
x=theoretical_quantiles,
|
|
425
|
+
y=ic_sorted,
|
|
426
|
+
mode="markers",
|
|
427
|
+
name="IC Values",
|
|
428
|
+
marker={
|
|
429
|
+
"size": 6,
|
|
430
|
+
"color": theme_config["colorway"][0],
|
|
431
|
+
"opacity": 0.6,
|
|
432
|
+
},
|
|
433
|
+
hovertemplate="Theoretical: %{x:.4f}<br>Sample: %{y:.4f}<extra></extra>",
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Reference line (45-degree)
|
|
438
|
+
min_val = min(theoretical_quantiles.min(), ic_sorted.min())
|
|
439
|
+
max_val = max(theoretical_quantiles.max(), ic_sorted.max())
|
|
440
|
+
|
|
441
|
+
fig.add_trace(
|
|
442
|
+
go.Scatter(
|
|
443
|
+
x=[min_val, max_val],
|
|
444
|
+
y=[min_val, max_val],
|
|
445
|
+
mode="lines",
|
|
446
|
+
name="Normal Reference",
|
|
447
|
+
line={"color": theme_config["colorway"][1], "dash": "dash", "width": 2},
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Normality test
|
|
452
|
+
if len(ic_clean) >= 8:
|
|
453
|
+
_, shapiro_p = stats.shapiro(ic_clean[:5000]) # Shapiro-Wilk limited to 5000
|
|
454
|
+
_, jb_stat, jb_p = stats.jarque_bera(ic_clean)
|
|
455
|
+
|
|
456
|
+
normality_text = (
|
|
457
|
+
f"<b>Normality Tests:</b><br>"
|
|
458
|
+
f"Shapiro-Wilk p: {shapiro_p:.4f}<br>"
|
|
459
|
+
f"Jarque-Bera p: {jb_p:.4f}<br>"
|
|
460
|
+
f"{'✓ Normal' if min(shapiro_p, jb_p) > 0.05 else '✗ Non-normal'}"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
fig.add_annotation(
|
|
464
|
+
text=normality_text,
|
|
465
|
+
xref="paper",
|
|
466
|
+
yref="paper",
|
|
467
|
+
x=0.02,
|
|
468
|
+
y=0.98,
|
|
469
|
+
showarrow=False,
|
|
470
|
+
bgcolor="rgba(255,255,255,0.8)" if theme != "dark" else "rgba(50,50,50,0.8)",
|
|
471
|
+
bordercolor="gray",
|
|
472
|
+
borderwidth=1,
|
|
473
|
+
align="left",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return fig
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def plot_ic_heatmap(
|
|
480
|
+
ic_result: SignalICResult,
|
|
481
|
+
period: str | None = None,
|
|
482
|
+
colorscale: str = "rdbu",
|
|
483
|
+
theme: str | None = None,
|
|
484
|
+
width: int | None = None,
|
|
485
|
+
height: int | None = None,
|
|
486
|
+
) -> go.Figure:
|
|
487
|
+
"""Plot monthly IC heatmap for seasonality analysis.
|
|
488
|
+
|
|
489
|
+
Parameters
|
|
490
|
+
----------
|
|
491
|
+
ic_result : SignalICResult
|
|
492
|
+
IC analysis result from SignalAnalysis.compute_ic_analysis()
|
|
493
|
+
period : str | None
|
|
494
|
+
Period to plot. If None, uses first period.
|
|
495
|
+
colorscale : str, default "rdbu"
|
|
496
|
+
Plotly colorscale name (rdbu for diverging red-blue)
|
|
497
|
+
theme : str | None
|
|
498
|
+
Plot theme
|
|
499
|
+
width : int | None
|
|
500
|
+
Figure width
|
|
501
|
+
height : int | None
|
|
502
|
+
Figure height
|
|
503
|
+
|
|
504
|
+
Returns
|
|
505
|
+
-------
|
|
506
|
+
go.Figure
|
|
507
|
+
Interactive Plotly heatmap
|
|
508
|
+
"""
|
|
509
|
+
theme = validate_theme(theme)
|
|
510
|
+
theme_config = get_theme_config(theme)
|
|
511
|
+
|
|
512
|
+
# Get period data
|
|
513
|
+
periods = list(ic_result.ic_by_date.keys())
|
|
514
|
+
if period is None:
|
|
515
|
+
period = periods[0]
|
|
516
|
+
elif period not in periods:
|
|
517
|
+
raise ValueError(f"Period '{period}' not found. Available: {periods}")
|
|
518
|
+
|
|
519
|
+
ic_series = np.array(ic_result.ic_by_date[period])
|
|
520
|
+
dates = ic_result.dates
|
|
521
|
+
|
|
522
|
+
# Parse dates and create year-month structure
|
|
523
|
+
parsed_dates = []
|
|
524
|
+
for d in dates:
|
|
525
|
+
if isinstance(d, str):
|
|
526
|
+
try:
|
|
527
|
+
parsed_dates.append(datetime.fromisoformat(d))
|
|
528
|
+
except ValueError:
|
|
529
|
+
try:
|
|
530
|
+
parsed_dates.append(datetime.strptime(d, "%Y-%m-%d"))
|
|
531
|
+
except ValueError:
|
|
532
|
+
continue
|
|
533
|
+
elif isinstance(d, datetime):
|
|
534
|
+
parsed_dates.append(d)
|
|
535
|
+
else:
|
|
536
|
+
try:
|
|
537
|
+
parsed_dates.append(datetime.fromisoformat(str(d)))
|
|
538
|
+
except ValueError:
|
|
539
|
+
continue
|
|
540
|
+
|
|
541
|
+
if len(parsed_dates) != len(ic_series):
|
|
542
|
+
raise ValueError("Date parsing failed - length mismatch")
|
|
543
|
+
|
|
544
|
+
# Build year-month matrix
|
|
545
|
+
import pandas as pd
|
|
546
|
+
|
|
547
|
+
df = pd.DataFrame(
|
|
548
|
+
{
|
|
549
|
+
"date": parsed_dates,
|
|
550
|
+
"ic": ic_series,
|
|
551
|
+
}
|
|
552
|
+
)
|
|
553
|
+
df["year"] = df["date"].dt.year
|
|
554
|
+
df["month"] = df["date"].dt.month
|
|
555
|
+
|
|
556
|
+
# Pivot to get mean IC by year-month
|
|
557
|
+
pivot = df.pivot_table(values="ic", index="year", columns="month", aggfunc="mean")
|
|
558
|
+
pivot = pivot.sort_index(ascending=False) # Most recent year at top
|
|
559
|
+
|
|
560
|
+
# Month names
|
|
561
|
+
month_names = [
|
|
562
|
+
"Jan",
|
|
563
|
+
"Feb",
|
|
564
|
+
"Mar",
|
|
565
|
+
"Apr",
|
|
566
|
+
"May",
|
|
567
|
+
"Jun",
|
|
568
|
+
"Jul",
|
|
569
|
+
"Aug",
|
|
570
|
+
"Sep",
|
|
571
|
+
"Oct",
|
|
572
|
+
"Nov",
|
|
573
|
+
"Dec",
|
|
574
|
+
]
|
|
575
|
+
|
|
576
|
+
# Create figure
|
|
577
|
+
fig = create_base_figure(
|
|
578
|
+
title=f"Monthly IC Heatmap ({period})",
|
|
579
|
+
xaxis_title="Month",
|
|
580
|
+
yaxis_title="Year",
|
|
581
|
+
width=width or theme_config["defaults"]["heatmap_height"],
|
|
582
|
+
height=height or 400 + 30 * len(pivot),
|
|
583
|
+
theme=theme,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
# Get colorscale
|
|
587
|
+
try:
|
|
588
|
+
colors = get_colorscale(colorscale)
|
|
589
|
+
except ValueError:
|
|
590
|
+
colors = "RdBu"
|
|
591
|
+
|
|
592
|
+
# Symmetric color scale around zero
|
|
593
|
+
ic_values = pivot.values.flatten()
|
|
594
|
+
ic_clean = ic_values[~np.isnan(ic_values)]
|
|
595
|
+
if len(ic_clean) > 0:
|
|
596
|
+
max_abs = max(abs(ic_clean.min()), abs(ic_clean.max()))
|
|
597
|
+
zmin, zmax = -max_abs, max_abs
|
|
598
|
+
else:
|
|
599
|
+
zmin, zmax = -0.1, 0.1
|
|
600
|
+
|
|
601
|
+
# Heatmap
|
|
602
|
+
fig.add_trace(
|
|
603
|
+
go.Heatmap(
|
|
604
|
+
z=pivot.values,
|
|
605
|
+
x=month_names[: int(pivot.columns.max())],
|
|
606
|
+
y=pivot.index.astype(str).tolist(),
|
|
607
|
+
colorscale=colors if isinstance(colors, str) else "RdBu",
|
|
608
|
+
zmid=0,
|
|
609
|
+
zmin=zmin,
|
|
610
|
+
zmax=zmax,
|
|
611
|
+
colorbar={"title": "Mean IC"},
|
|
612
|
+
hovertemplate="Year: %{y}<br>Month: %{x}<br>IC: %{z:.4f}<extra></extra>",
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Add text annotations
|
|
617
|
+
for _i, year in enumerate(pivot.index):
|
|
618
|
+
for _j, month in enumerate(pivot.columns):
|
|
619
|
+
val = pivot.loc[year, month]
|
|
620
|
+
if not np.isnan(val):
|
|
621
|
+
text_color = "white" if abs(val) > max_abs * 0.5 else "black"
|
|
622
|
+
fig.add_annotation(
|
|
623
|
+
x=month_names[int(month) - 1],
|
|
624
|
+
y=str(year),
|
|
625
|
+
text=f"{val:.3f}",
|
|
626
|
+
showarrow=False,
|
|
627
|
+
font={"size": 10, "color": text_color},
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
fig.update_layout(
|
|
631
|
+
xaxis={"side": "bottom", "tickangle": 0},
|
|
632
|
+
yaxis={"autorange": "reversed"},
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
return fig
|