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.
Files changed (242) hide show
  1. ml4t/diagnostic/AGENT.md +25 -0
  2. ml4t/diagnostic/__init__.py +166 -0
  3. ml4t/diagnostic/backends/__init__.py +10 -0
  4. ml4t/diagnostic/backends/adapter.py +192 -0
  5. ml4t/diagnostic/backends/polars_backend.py +899 -0
  6. ml4t/diagnostic/caching/__init__.py +40 -0
  7. ml4t/diagnostic/caching/cache.py +331 -0
  8. ml4t/diagnostic/caching/decorators.py +131 -0
  9. ml4t/diagnostic/caching/smart_cache.py +339 -0
  10. ml4t/diagnostic/config/AGENT.md +24 -0
  11. ml4t/diagnostic/config/README.md +267 -0
  12. ml4t/diagnostic/config/__init__.py +219 -0
  13. ml4t/diagnostic/config/barrier_config.py +277 -0
  14. ml4t/diagnostic/config/base.py +301 -0
  15. ml4t/diagnostic/config/event_config.py +148 -0
  16. ml4t/diagnostic/config/feature_config.py +404 -0
  17. ml4t/diagnostic/config/multi_signal_config.py +55 -0
  18. ml4t/diagnostic/config/portfolio_config.py +215 -0
  19. ml4t/diagnostic/config/report_config.py +391 -0
  20. ml4t/diagnostic/config/sharpe_config.py +202 -0
  21. ml4t/diagnostic/config/signal_config.py +206 -0
  22. ml4t/diagnostic/config/trade_analysis_config.py +310 -0
  23. ml4t/diagnostic/config/validation.py +279 -0
  24. ml4t/diagnostic/core/__init__.py +29 -0
  25. ml4t/diagnostic/core/numba_utils.py +315 -0
  26. ml4t/diagnostic/core/purging.py +372 -0
  27. ml4t/diagnostic/core/sampling.py +471 -0
  28. ml4t/diagnostic/errors/__init__.py +205 -0
  29. ml4t/diagnostic/evaluation/AGENT.md +26 -0
  30. ml4t/diagnostic/evaluation/__init__.py +437 -0
  31. ml4t/diagnostic/evaluation/autocorrelation.py +531 -0
  32. ml4t/diagnostic/evaluation/barrier_analysis.py +1050 -0
  33. ml4t/diagnostic/evaluation/binary_metrics.py +910 -0
  34. ml4t/diagnostic/evaluation/dashboard.py +715 -0
  35. ml4t/diagnostic/evaluation/diagnostic_plots.py +1037 -0
  36. ml4t/diagnostic/evaluation/distribution/__init__.py +499 -0
  37. ml4t/diagnostic/evaluation/distribution/moments.py +299 -0
  38. ml4t/diagnostic/evaluation/distribution/tails.py +777 -0
  39. ml4t/diagnostic/evaluation/distribution/tests.py +470 -0
  40. ml4t/diagnostic/evaluation/drift/__init__.py +139 -0
  41. ml4t/diagnostic/evaluation/drift/analysis.py +432 -0
  42. ml4t/diagnostic/evaluation/drift/domain_classifier.py +517 -0
  43. ml4t/diagnostic/evaluation/drift/population_stability_index.py +310 -0
  44. ml4t/diagnostic/evaluation/drift/wasserstein.py +388 -0
  45. ml4t/diagnostic/evaluation/event_analysis.py +647 -0
  46. ml4t/diagnostic/evaluation/excursion.py +390 -0
  47. ml4t/diagnostic/evaluation/feature_diagnostics.py +873 -0
  48. ml4t/diagnostic/evaluation/feature_outcome.py +666 -0
  49. ml4t/diagnostic/evaluation/framework.py +935 -0
  50. ml4t/diagnostic/evaluation/metric_registry.py +255 -0
  51. ml4t/diagnostic/evaluation/metrics/AGENT.md +23 -0
  52. ml4t/diagnostic/evaluation/metrics/__init__.py +133 -0
  53. ml4t/diagnostic/evaluation/metrics/basic.py +160 -0
  54. ml4t/diagnostic/evaluation/metrics/conditional_ic.py +469 -0
  55. ml4t/diagnostic/evaluation/metrics/feature_outcome.py +475 -0
  56. ml4t/diagnostic/evaluation/metrics/ic_statistics.py +446 -0
  57. ml4t/diagnostic/evaluation/metrics/importance_analysis.py +338 -0
  58. ml4t/diagnostic/evaluation/metrics/importance_classical.py +375 -0
  59. ml4t/diagnostic/evaluation/metrics/importance_mda.py +371 -0
  60. ml4t/diagnostic/evaluation/metrics/importance_shap.py +715 -0
  61. ml4t/diagnostic/evaluation/metrics/information_coefficient.py +527 -0
  62. ml4t/diagnostic/evaluation/metrics/interactions.py +772 -0
  63. ml4t/diagnostic/evaluation/metrics/monotonicity.py +226 -0
  64. ml4t/diagnostic/evaluation/metrics/risk_adjusted.py +324 -0
  65. ml4t/diagnostic/evaluation/multi_signal.py +550 -0
  66. ml4t/diagnostic/evaluation/portfolio_analysis/__init__.py +83 -0
  67. ml4t/diagnostic/evaluation/portfolio_analysis/analysis.py +734 -0
  68. ml4t/diagnostic/evaluation/portfolio_analysis/metrics.py +589 -0
  69. ml4t/diagnostic/evaluation/portfolio_analysis/results.py +334 -0
  70. ml4t/diagnostic/evaluation/report_generation.py +824 -0
  71. ml4t/diagnostic/evaluation/signal_selector.py +452 -0
  72. ml4t/diagnostic/evaluation/stat_registry.py +139 -0
  73. ml4t/diagnostic/evaluation/stationarity/__init__.py +97 -0
  74. ml4t/diagnostic/evaluation/stationarity/analysis.py +518 -0
  75. ml4t/diagnostic/evaluation/stationarity/augmented_dickey_fuller.py +296 -0
  76. ml4t/diagnostic/evaluation/stationarity/kpss_test.py +308 -0
  77. ml4t/diagnostic/evaluation/stationarity/phillips_perron.py +365 -0
  78. ml4t/diagnostic/evaluation/stats/AGENT.md +43 -0
  79. ml4t/diagnostic/evaluation/stats/__init__.py +191 -0
  80. ml4t/diagnostic/evaluation/stats/backtest_overfitting.py +219 -0
  81. ml4t/diagnostic/evaluation/stats/bootstrap.py +228 -0
  82. ml4t/diagnostic/evaluation/stats/deflated_sharpe_ratio.py +591 -0
  83. ml4t/diagnostic/evaluation/stats/false_discovery_rate.py +295 -0
  84. ml4t/diagnostic/evaluation/stats/hac_standard_errors.py +108 -0
  85. ml4t/diagnostic/evaluation/stats/minimum_track_record.py +408 -0
  86. ml4t/diagnostic/evaluation/stats/moments.py +164 -0
  87. ml4t/diagnostic/evaluation/stats/rademacher_adjustment.py +436 -0
  88. ml4t/diagnostic/evaluation/stats/reality_check.py +155 -0
  89. ml4t/diagnostic/evaluation/stats/sharpe_inference.py +219 -0
  90. ml4t/diagnostic/evaluation/themes.py +330 -0
  91. ml4t/diagnostic/evaluation/threshold_analysis.py +957 -0
  92. ml4t/diagnostic/evaluation/trade_analysis.py +1136 -0
  93. ml4t/diagnostic/evaluation/trade_dashboard/__init__.py +32 -0
  94. ml4t/diagnostic/evaluation/trade_dashboard/app.py +315 -0
  95. ml4t/diagnostic/evaluation/trade_dashboard/export/__init__.py +18 -0
  96. ml4t/diagnostic/evaluation/trade_dashboard/export/csv.py +82 -0
  97. ml4t/diagnostic/evaluation/trade_dashboard/export/html.py +276 -0
  98. ml4t/diagnostic/evaluation/trade_dashboard/io.py +166 -0
  99. ml4t/diagnostic/evaluation/trade_dashboard/normalize.py +304 -0
  100. ml4t/diagnostic/evaluation/trade_dashboard/stats.py +386 -0
  101. ml4t/diagnostic/evaluation/trade_dashboard/style.py +79 -0
  102. ml4t/diagnostic/evaluation/trade_dashboard/tabs/__init__.py +21 -0
  103. ml4t/diagnostic/evaluation/trade_dashboard/tabs/patterns.py +354 -0
  104. ml4t/diagnostic/evaluation/trade_dashboard/tabs/shap_analysis.py +280 -0
  105. ml4t/diagnostic/evaluation/trade_dashboard/tabs/stat_validation.py +186 -0
  106. ml4t/diagnostic/evaluation/trade_dashboard/tabs/worst_trades.py +236 -0
  107. ml4t/diagnostic/evaluation/trade_dashboard/types.py +129 -0
  108. ml4t/diagnostic/evaluation/trade_shap/__init__.py +102 -0
  109. ml4t/diagnostic/evaluation/trade_shap/alignment.py +188 -0
  110. ml4t/diagnostic/evaluation/trade_shap/characterize.py +413 -0
  111. ml4t/diagnostic/evaluation/trade_shap/cluster.py +302 -0
  112. ml4t/diagnostic/evaluation/trade_shap/explain.py +208 -0
  113. ml4t/diagnostic/evaluation/trade_shap/hypotheses/__init__.py +23 -0
  114. ml4t/diagnostic/evaluation/trade_shap/hypotheses/generator.py +290 -0
  115. ml4t/diagnostic/evaluation/trade_shap/hypotheses/matcher.py +251 -0
  116. ml4t/diagnostic/evaluation/trade_shap/hypotheses/templates.yaml +467 -0
  117. ml4t/diagnostic/evaluation/trade_shap/models.py +386 -0
  118. ml4t/diagnostic/evaluation/trade_shap/normalize.py +116 -0
  119. ml4t/diagnostic/evaluation/trade_shap/pipeline.py +263 -0
  120. ml4t/diagnostic/evaluation/trade_shap_dashboard.py +283 -0
  121. ml4t/diagnostic/evaluation/trade_shap_diagnostics.py +588 -0
  122. ml4t/diagnostic/evaluation/validated_cv.py +535 -0
  123. ml4t/diagnostic/evaluation/visualization.py +1050 -0
  124. ml4t/diagnostic/evaluation/volatility/__init__.py +45 -0
  125. ml4t/diagnostic/evaluation/volatility/analysis.py +351 -0
  126. ml4t/diagnostic/evaluation/volatility/arch.py +258 -0
  127. ml4t/diagnostic/evaluation/volatility/garch.py +460 -0
  128. ml4t/diagnostic/integration/__init__.py +48 -0
  129. ml4t/diagnostic/integration/backtest_contract.py +671 -0
  130. ml4t/diagnostic/integration/data_contract.py +316 -0
  131. ml4t/diagnostic/integration/engineer_contract.py +226 -0
  132. ml4t/diagnostic/logging/__init__.py +77 -0
  133. ml4t/diagnostic/logging/logger.py +245 -0
  134. ml4t/diagnostic/logging/performance.py +234 -0
  135. ml4t/diagnostic/logging/progress.py +234 -0
  136. ml4t/diagnostic/logging/wandb.py +412 -0
  137. ml4t/diagnostic/metrics/__init__.py +9 -0
  138. ml4t/diagnostic/metrics/percentiles.py +128 -0
  139. ml4t/diagnostic/py.typed +1 -0
  140. ml4t/diagnostic/reporting/__init__.py +43 -0
  141. ml4t/diagnostic/reporting/base.py +130 -0
  142. ml4t/diagnostic/reporting/html_renderer.py +275 -0
  143. ml4t/diagnostic/reporting/json_renderer.py +51 -0
  144. ml4t/diagnostic/reporting/markdown_renderer.py +117 -0
  145. ml4t/diagnostic/results/AGENT.md +24 -0
  146. ml4t/diagnostic/results/__init__.py +105 -0
  147. ml4t/diagnostic/results/barrier_results/__init__.py +36 -0
  148. ml4t/diagnostic/results/barrier_results/hit_rate.py +304 -0
  149. ml4t/diagnostic/results/barrier_results/precision_recall.py +266 -0
  150. ml4t/diagnostic/results/barrier_results/profit_factor.py +297 -0
  151. ml4t/diagnostic/results/barrier_results/tearsheet.py +397 -0
  152. ml4t/diagnostic/results/barrier_results/time_to_target.py +305 -0
  153. ml4t/diagnostic/results/barrier_results/validation.py +38 -0
  154. ml4t/diagnostic/results/base.py +177 -0
  155. ml4t/diagnostic/results/event_results.py +349 -0
  156. ml4t/diagnostic/results/feature_results.py +787 -0
  157. ml4t/diagnostic/results/multi_signal_results.py +431 -0
  158. ml4t/diagnostic/results/portfolio_results.py +281 -0
  159. ml4t/diagnostic/results/sharpe_results.py +448 -0
  160. ml4t/diagnostic/results/signal_results/__init__.py +74 -0
  161. ml4t/diagnostic/results/signal_results/ic.py +581 -0
  162. ml4t/diagnostic/results/signal_results/irtc.py +110 -0
  163. ml4t/diagnostic/results/signal_results/quantile.py +392 -0
  164. ml4t/diagnostic/results/signal_results/tearsheet.py +456 -0
  165. ml4t/diagnostic/results/signal_results/turnover.py +213 -0
  166. ml4t/diagnostic/results/signal_results/validation.py +147 -0
  167. ml4t/diagnostic/signal/AGENT.md +17 -0
  168. ml4t/diagnostic/signal/__init__.py +69 -0
  169. ml4t/diagnostic/signal/_report.py +152 -0
  170. ml4t/diagnostic/signal/_utils.py +261 -0
  171. ml4t/diagnostic/signal/core.py +275 -0
  172. ml4t/diagnostic/signal/quantile.py +148 -0
  173. ml4t/diagnostic/signal/result.py +214 -0
  174. ml4t/diagnostic/signal/signal_ic.py +129 -0
  175. ml4t/diagnostic/signal/turnover.py +182 -0
  176. ml4t/diagnostic/splitters/AGENT.md +19 -0
  177. ml4t/diagnostic/splitters/__init__.py +36 -0
  178. ml4t/diagnostic/splitters/base.py +501 -0
  179. ml4t/diagnostic/splitters/calendar.py +421 -0
  180. ml4t/diagnostic/splitters/calendar_config.py +91 -0
  181. ml4t/diagnostic/splitters/combinatorial.py +1064 -0
  182. ml4t/diagnostic/splitters/config.py +322 -0
  183. ml4t/diagnostic/splitters/cpcv/__init__.py +57 -0
  184. ml4t/diagnostic/splitters/cpcv/combinations.py +119 -0
  185. ml4t/diagnostic/splitters/cpcv/partitioning.py +263 -0
  186. ml4t/diagnostic/splitters/cpcv/purge_engine.py +379 -0
  187. ml4t/diagnostic/splitters/cpcv/windows.py +190 -0
  188. ml4t/diagnostic/splitters/group_isolation.py +329 -0
  189. ml4t/diagnostic/splitters/persistence.py +316 -0
  190. ml4t/diagnostic/splitters/utils.py +207 -0
  191. ml4t/diagnostic/splitters/walk_forward.py +757 -0
  192. ml4t/diagnostic/utils/__init__.py +42 -0
  193. ml4t/diagnostic/utils/config.py +542 -0
  194. ml4t/diagnostic/utils/dependencies.py +318 -0
  195. ml4t/diagnostic/utils/sessions.py +127 -0
  196. ml4t/diagnostic/validation/__init__.py +54 -0
  197. ml4t/diagnostic/validation/dataframe.py +274 -0
  198. ml4t/diagnostic/validation/returns.py +280 -0
  199. ml4t/diagnostic/validation/timeseries.py +299 -0
  200. ml4t/diagnostic/visualization/AGENT.md +19 -0
  201. ml4t/diagnostic/visualization/__init__.py +223 -0
  202. ml4t/diagnostic/visualization/backtest/__init__.py +98 -0
  203. ml4t/diagnostic/visualization/backtest/cost_attribution.py +762 -0
  204. ml4t/diagnostic/visualization/backtest/executive_summary.py +895 -0
  205. ml4t/diagnostic/visualization/backtest/interactive_controls.py +673 -0
  206. ml4t/diagnostic/visualization/backtest/statistical_validity.py +874 -0
  207. ml4t/diagnostic/visualization/backtest/tearsheet.py +565 -0
  208. ml4t/diagnostic/visualization/backtest/template_system.py +373 -0
  209. ml4t/diagnostic/visualization/backtest/trade_plots.py +1172 -0
  210. ml4t/diagnostic/visualization/barrier_plots.py +782 -0
  211. ml4t/diagnostic/visualization/core.py +1060 -0
  212. ml4t/diagnostic/visualization/dashboards/__init__.py +36 -0
  213. ml4t/diagnostic/visualization/dashboards/base.py +582 -0
  214. ml4t/diagnostic/visualization/dashboards/importance.py +801 -0
  215. ml4t/diagnostic/visualization/dashboards/interaction.py +263 -0
  216. ml4t/diagnostic/visualization/dashboards.py +43 -0
  217. ml4t/diagnostic/visualization/data_extraction/__init__.py +48 -0
  218. ml4t/diagnostic/visualization/data_extraction/importance.py +649 -0
  219. ml4t/diagnostic/visualization/data_extraction/interaction.py +504 -0
  220. ml4t/diagnostic/visualization/data_extraction/types.py +113 -0
  221. ml4t/diagnostic/visualization/data_extraction/validation.py +66 -0
  222. ml4t/diagnostic/visualization/feature_plots.py +888 -0
  223. ml4t/diagnostic/visualization/interaction_plots.py +618 -0
  224. ml4t/diagnostic/visualization/portfolio/__init__.py +41 -0
  225. ml4t/diagnostic/visualization/portfolio/dashboard.py +514 -0
  226. ml4t/diagnostic/visualization/portfolio/drawdown_plots.py +341 -0
  227. ml4t/diagnostic/visualization/portfolio/returns_plots.py +487 -0
  228. ml4t/diagnostic/visualization/portfolio/risk_plots.py +301 -0
  229. ml4t/diagnostic/visualization/report_generation.py +1343 -0
  230. ml4t/diagnostic/visualization/signal/__init__.py +103 -0
  231. ml4t/diagnostic/visualization/signal/dashboard.py +911 -0
  232. ml4t/diagnostic/visualization/signal/event_plots.py +514 -0
  233. ml4t/diagnostic/visualization/signal/ic_plots.py +635 -0
  234. ml4t/diagnostic/visualization/signal/multi_signal_dashboard.py +974 -0
  235. ml4t/diagnostic/visualization/signal/multi_signal_plots.py +603 -0
  236. ml4t/diagnostic/visualization/signal/quantile_plots.py +625 -0
  237. ml4t/diagnostic/visualization/signal/turnover_plots.py +400 -0
  238. ml4t/diagnostic/visualization/trade_shap/__init__.py +90 -0
  239. ml4t_diagnostic-0.1.0a1.dist-info/METADATA +1044 -0
  240. ml4t_diagnostic-0.1.0a1.dist-info/RECORD +242 -0
  241. ml4t_diagnostic-0.1.0a1.dist-info/WHEEL +4 -0
  242. ml4t_diagnostic-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,782 @@
1
+ """Barrier analysis visualization plots.
2
+
3
+ This module provides interactive Plotly visualizations for triple barrier analysis:
4
+ - plot_hit_rate_heatmap: Heatmap of hit rates by quantile and outcome type
5
+ - plot_profit_factor_bar: Bar chart of profit factor by quantile
6
+ - plot_precision_recall_curve: Precision/recall curve with F1 peak
7
+ - plot_time_to_target_box: Box plots of bars to exit by quantile and outcome
8
+
9
+ All plots follow the consistent API pattern:
10
+ - Accept result objects from BarrierAnalysis methods
11
+ - Return go.Figure objects
12
+ - Support theme customization
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ import numpy as np
20
+ import plotly.graph_objects as go
21
+
22
+ from ml4t.diagnostic.visualization.core import (
23
+ create_base_figure,
24
+ get_colorscale,
25
+ get_theme_config,
26
+ validate_theme,
27
+ )
28
+
29
+ if TYPE_CHECKING:
30
+ from ml4t.diagnostic.results.barrier_results import (
31
+ HitRateResult,
32
+ PrecisionRecallResult,
33
+ ProfitFactorResult,
34
+ TimeToTargetResult,
35
+ )
36
+
37
+
38
+ def _get_quantile_colors(n_quantiles: int, theme_config: dict) -> list[str]:
39
+ """Get diverging colors for quantiles (red → green progression)."""
40
+ colors: list[str]
41
+ if n_quantiles <= 5:
42
+ colors = ["#D32F2F", "#F57C00", "#FBC02D", "#689F38", "#388E3C"][:n_quantiles]
43
+ else:
44
+ try:
45
+ raw_colors = get_colorscale("rdylgn", n_colors=n_quantiles, reverse=False)
46
+ if isinstance(raw_colors[0], tuple):
47
+ colors = [str(c[1]) if isinstance(c, tuple) else str(c) for c in raw_colors]
48
+ else:
49
+ colors = [str(c) for c in raw_colors]
50
+ except (ValueError, IndexError):
51
+ colorway = theme_config["colorway"]
52
+ repeated = colorway * ((n_quantiles // len(colorway)) + 1)
53
+ colors = [str(c) for c in repeated[:n_quantiles]]
54
+ return colors
55
+
56
+
57
+ def _get_outcome_colors() -> dict[str, str]:
58
+ """Get colors for barrier outcomes."""
59
+ return {
60
+ "tp": "#2ECC71", # Green for take-profit
61
+ "sl": "#E74C3C", # Red for stop-loss
62
+ "timeout": "#95A5A6", # Gray for timeout
63
+ }
64
+
65
+
66
+ def plot_hit_rate_heatmap(
67
+ hit_rate_result: HitRateResult,
68
+ show_counts: bool = True,
69
+ show_chi2: bool = True,
70
+ theme: str | None = None,
71
+ width: int | None = None,
72
+ height: int | None = None,
73
+ ) -> go.Figure:
74
+ """Plot hit rates as a heatmap (quantile x outcome type).
75
+
76
+ Creates a heatmap showing hit rates for each outcome type (TP, SL, timeout)
77
+ across signal quantiles. Includes chi-square test annotation.
78
+
79
+ Parameters
80
+ ----------
81
+ hit_rate_result : HitRateResult
82
+ Hit rate analysis result from BarrierAnalysis.compute_hit_rates()
83
+ show_counts : bool, default True
84
+ Show observation counts in cell text
85
+ show_chi2 : bool, default True
86
+ Show chi-square test results annotation
87
+ theme : str | None
88
+ Plot theme (default, dark, print, presentation)
89
+ width : int | None
90
+ Figure width in pixels
91
+ height : int | None
92
+ Figure height in pixels
93
+
94
+ Returns
95
+ -------
96
+ go.Figure
97
+ Interactive Plotly figure
98
+
99
+ Examples
100
+ --------
101
+ >>> hit_rates = analysis.compute_hit_rates()
102
+ >>> fig = plot_hit_rate_heatmap(hit_rates)
103
+ >>> fig.show()
104
+ """
105
+ theme = validate_theme(theme)
106
+ theme_config = get_theme_config(theme)
107
+
108
+ quantile_labels = hit_rate_result.quantile_labels
109
+ outcome_labels = ["Take-Profit", "Stop-Loss", "Timeout"]
110
+
111
+ # Build heatmap data matrix [outcomes x quantiles]
112
+ z_values = [
113
+ [hit_rate_result.hit_rate_tp[q] for q in quantile_labels],
114
+ [hit_rate_result.hit_rate_sl[q] for q in quantile_labels],
115
+ [hit_rate_result.hit_rate_timeout[q] for q in quantile_labels],
116
+ ]
117
+
118
+ # Build text annotations (rate % and optionally count)
119
+ text_values = []
120
+ for i, outcome in enumerate(["tp", "sl", "timeout"]):
121
+ row_text = []
122
+ for q in quantile_labels:
123
+ rate = z_values[i][quantile_labels.index(q)]
124
+ count_dict = {
125
+ "tp": hit_rate_result.count_tp,
126
+ "sl": hit_rate_result.count_sl,
127
+ "timeout": hit_rate_result.count_timeout,
128
+ }
129
+ count = count_dict[outcome][q]
130
+ if show_counts:
131
+ row_text.append(f"{rate:.1%}<br>n={count:,}")
132
+ else:
133
+ row_text.append(f"{rate:.1%}")
134
+ text_values.append(row_text)
135
+
136
+ # Create figure
137
+ fig = create_base_figure(
138
+ title="Hit Rate by Signal Quantile and Outcome",
139
+ xaxis_title="Signal Quantile",
140
+ yaxis_title="Outcome Type",
141
+ width=width or theme_config["defaults"]["heatmap_height"],
142
+ height=height or 400,
143
+ theme=theme,
144
+ )
145
+
146
+ # Add heatmap
147
+ fig.add_trace(
148
+ go.Heatmap(
149
+ z=z_values,
150
+ x=quantile_labels,
151
+ y=outcome_labels,
152
+ text=text_values,
153
+ texttemplate="%{text}",
154
+ textfont={"size": 11},
155
+ colorscale="RdYlGn",
156
+ colorbar={
157
+ "title": "Hit Rate",
158
+ "tickformat": ".0%",
159
+ },
160
+ hovertemplate=(
161
+ "Quantile: %{x}<br>Outcome: %{y}<br>Hit Rate: %{z:.2%}<br><extra></extra>"
162
+ ),
163
+ )
164
+ )
165
+
166
+ # Chi-square annotation
167
+ if show_chi2:
168
+ sig_text = "✓" if hit_rate_result.is_significant else "✗"
169
+ chi2_text = (
170
+ f"<b>Chi-Square Test:</b><br>"
171
+ f"χ² = {hit_rate_result.chi2_statistic:.2f}<br>"
172
+ f"p = {hit_rate_result.chi2_p_value:.4f}<br>"
173
+ f"Significant: {sig_text} (α={hit_rate_result.significance_level})"
174
+ )
175
+
176
+ fig.add_annotation(
177
+ text=chi2_text,
178
+ xref="paper",
179
+ yref="paper",
180
+ x=1.02,
181
+ y=1.0,
182
+ showarrow=False,
183
+ bgcolor="rgba(255,255,255,0.9)" if theme != "dark" else "rgba(50,50,50,0.9)",
184
+ bordercolor="gray",
185
+ borderwidth=1,
186
+ align="left",
187
+ xanchor="left",
188
+ yanchor="top",
189
+ font={"size": 10},
190
+ )
191
+
192
+ fig.update_layout(
193
+ xaxis={"side": "bottom"},
194
+ yaxis={"autorange": "reversed"},
195
+ )
196
+
197
+ return fig
198
+
199
+
200
+ def plot_profit_factor_bar(
201
+ profit_factor_result: ProfitFactorResult,
202
+ show_reference_line: bool = True,
203
+ show_average_return: bool = True,
204
+ theme: str | None = None,
205
+ width: int | None = None,
206
+ height: int | None = None,
207
+ ) -> go.Figure:
208
+ """Plot profit factor by quantile as a bar chart.
209
+
210
+ Creates a bar chart showing profit factor for each signal quantile,
211
+ with reference line at PF=1.0 (breakeven).
212
+
213
+ Parameters
214
+ ----------
215
+ profit_factor_result : ProfitFactorResult
216
+ Profit factor result from BarrierAnalysis.compute_profit_factor()
217
+ show_reference_line : bool, default True
218
+ Show horizontal line at PF=1.0 (breakeven)
219
+ show_average_return : bool, default True
220
+ Show average return as secondary y-axis
221
+ theme : str | None
222
+ Plot theme (default, dark, print, presentation)
223
+ width : int | None
224
+ Figure width in pixels
225
+ height : int | None
226
+ Figure height in pixels
227
+
228
+ Returns
229
+ -------
230
+ go.Figure
231
+ Interactive Plotly figure
232
+
233
+ Examples
234
+ --------
235
+ >>> pf = analysis.compute_profit_factor()
236
+ >>> fig = plot_profit_factor_bar(pf)
237
+ >>> fig.show()
238
+ """
239
+ theme = validate_theme(theme)
240
+ theme_config = get_theme_config(theme)
241
+
242
+ quantile_labels = profit_factor_result.quantile_labels
243
+ n_quantiles = profit_factor_result.n_quantiles
244
+
245
+ # Get colors
246
+ colors = _get_quantile_colors(n_quantiles, theme_config)
247
+
248
+ # Prepare data
249
+ pf_values = [profit_factor_result.profit_factor[q] for q in quantile_labels]
250
+ avg_returns = [profit_factor_result.avg_return[q] for q in quantile_labels]
251
+
252
+ # Create figure
253
+ fig = create_base_figure(
254
+ title="Profit Factor by Signal Quantile",
255
+ xaxis_title="Signal Quantile",
256
+ yaxis_title="Profit Factor",
257
+ width=width or theme_config["defaults"]["bar_height"],
258
+ height=height or theme_config["defaults"]["bar_height"],
259
+ theme=theme,
260
+ )
261
+
262
+ # Bar chart for profit factor
263
+ fig.add_trace(
264
+ go.Bar(
265
+ x=quantile_labels,
266
+ y=pf_values,
267
+ marker_color=colors,
268
+ name="Profit Factor",
269
+ hovertemplate=("Quantile: %{x}<br>Profit Factor: %{y:.2f}<br><extra></extra>"),
270
+ )
271
+ )
272
+
273
+ # Reference line at PF=1.0
274
+ if show_reference_line:
275
+ fig.add_hline(
276
+ y=1.0,
277
+ line_dash="dash",
278
+ line_color="gray",
279
+ line_width=2,
280
+ annotation_text="Breakeven (PF=1)",
281
+ annotation_position="right",
282
+ annotation={"font_size": 10, "font_color": "gray"},
283
+ )
284
+
285
+ # Secondary y-axis for average return
286
+ if show_average_return:
287
+ fig.add_trace(
288
+ go.Scatter(
289
+ x=quantile_labels,
290
+ y=avg_returns,
291
+ mode="lines+markers",
292
+ name="Avg Return",
293
+ yaxis="y2",
294
+ line={"color": theme_config["colorway"][1], "width": 2},
295
+ marker={"size": 8},
296
+ hovertemplate=("Quantile: %{x}<br>Avg Return: %{y:.4%}<br><extra></extra>"),
297
+ )
298
+ )
299
+
300
+ # Update layout for secondary y-axis
301
+ fig.update_layout(
302
+ yaxis2={
303
+ "title": "Average Return",
304
+ "overlaying": "y",
305
+ "side": "right",
306
+ "tickformat": ".2%",
307
+ "showgrid": False,
308
+ },
309
+ legend={
310
+ "yanchor": "top",
311
+ "y": 0.99,
312
+ "xanchor": "left",
313
+ "x": 0.01,
314
+ },
315
+ )
316
+
317
+ # Monotonicity annotation
318
+ direction = profit_factor_result.pf_direction
319
+ monotonic = profit_factor_result.pf_monotonic
320
+ rho = profit_factor_result.pf_spearman
321
+
322
+ mono_text = (
323
+ f"<b>Monotonicity:</b><br>"
324
+ f"Monotonic: {'✓' if monotonic else '✗'} ({direction})<br>"
325
+ f"Spearman ρ: {rho:.3f}<br>"
326
+ f"Overall PF: {profit_factor_result.overall_profit_factor:.2f}"
327
+ )
328
+
329
+ fig.add_annotation(
330
+ text=mono_text,
331
+ xref="paper",
332
+ yref="paper",
333
+ x=0.02,
334
+ y=0.98,
335
+ showarrow=False,
336
+ bgcolor="rgba(255,255,255,0.9)" if theme != "dark" else "rgba(50,50,50,0.9)",
337
+ bordercolor="gray",
338
+ borderwidth=1,
339
+ align="left",
340
+ xanchor="left",
341
+ yanchor="top",
342
+ font={"size": 10},
343
+ )
344
+
345
+ return fig
346
+
347
+
348
+ def plot_precision_recall_curve(
349
+ precision_recall_result: PrecisionRecallResult,
350
+ show_f1_peak: bool = True,
351
+ show_lift: bool = True,
352
+ theme: str | None = None,
353
+ width: int | None = None,
354
+ height: int | None = None,
355
+ ) -> go.Figure:
356
+ """Plot cumulative precision/recall curves with F1 score.
357
+
358
+ Creates a line chart showing cumulative precision, recall, and F1 score
359
+ as signal quantile threshold moves from top (D10) to bottom (D1).
360
+
361
+ Parameters
362
+ ----------
363
+ precision_recall_result : PrecisionRecallResult
364
+ Precision/recall result from BarrierAnalysis.compute_precision_recall()
365
+ show_f1_peak : bool, default True
366
+ Highlight the quantile with best F1 score
367
+ show_lift : bool, default True
368
+ Show lift curve on secondary y-axis
369
+ theme : str | None
370
+ Plot theme (default, dark, print, presentation)
371
+ width : int | None
372
+ Figure width in pixels
373
+ height : int | None
374
+ Figure height in pixels
375
+
376
+ Returns
377
+ -------
378
+ go.Figure
379
+ Interactive Plotly figure
380
+
381
+ Examples
382
+ --------
383
+ >>> pr = analysis.compute_precision_recall()
384
+ >>> fig = plot_precision_recall_curve(pr)
385
+ >>> fig.show()
386
+ """
387
+ theme = validate_theme(theme)
388
+ theme_config = get_theme_config(theme)
389
+
390
+ quantile_labels = precision_recall_result.quantile_labels
391
+
392
+ # Prepare data - reversed order (from D10 to D1 for cumulative threshold)
393
+ reversed_labels = list(reversed(quantile_labels))
394
+
395
+ cum_precision = [precision_recall_result.cumulative_precision_tp[q] for q in reversed_labels]
396
+ cum_recall = [precision_recall_result.cumulative_recall_tp[q] for q in reversed_labels]
397
+ cum_f1 = [precision_recall_result.cumulative_f1_tp[q] for q in reversed_labels]
398
+ cum_lift = [precision_recall_result.cumulative_lift_tp[q] for q in reversed_labels]
399
+
400
+ # Create figure
401
+ fig = create_base_figure(
402
+ title="Cumulative Precision/Recall Curve (Top Quantiles)",
403
+ xaxis_title="Include Down to Quantile (from top)",
404
+ yaxis_title="Rate",
405
+ width=width or theme_config["defaults"]["line_height"] + 200,
406
+ height=height or theme_config["defaults"]["line_height"],
407
+ theme=theme,
408
+ )
409
+
410
+ # Precision line
411
+ fig.add_trace(
412
+ go.Scatter(
413
+ x=reversed_labels,
414
+ y=cum_precision,
415
+ mode="lines+markers",
416
+ name="Cumulative Precision",
417
+ line={"color": "#3498DB", "width": 2},
418
+ marker={"size": 8},
419
+ hovertemplate=("Threshold: %{x}<br>Precision: %{y:.2%}<br><extra></extra>"),
420
+ )
421
+ )
422
+
423
+ # Recall line
424
+ fig.add_trace(
425
+ go.Scatter(
426
+ x=reversed_labels,
427
+ y=cum_recall,
428
+ mode="lines+markers",
429
+ name="Cumulative Recall",
430
+ line={"color": "#E74C3C", "width": 2},
431
+ marker={"size": 8},
432
+ hovertemplate=("Threshold: %{x}<br>Recall: %{y:.2%}<br><extra></extra>"),
433
+ )
434
+ )
435
+
436
+ # F1 score line
437
+ fig.add_trace(
438
+ go.Scatter(
439
+ x=reversed_labels,
440
+ y=cum_f1,
441
+ mode="lines+markers",
442
+ name="Cumulative F1",
443
+ line={"color": "#9B59B6", "width": 3, "dash": "dash"},
444
+ marker={"size": 10, "symbol": "diamond"},
445
+ hovertemplate=("Threshold: %{x}<br>F1 Score: %{y:.4f}<br><extra></extra>"),
446
+ )
447
+ )
448
+
449
+ # Baseline horizontal line
450
+ baseline = precision_recall_result.baseline_tp_rate
451
+ fig.add_hline(
452
+ y=baseline,
453
+ line_dash="dot",
454
+ line_color="gray",
455
+ line_width=1,
456
+ annotation_text=f"Baseline: {baseline:.1%}",
457
+ annotation_position="right",
458
+ annotation={"font_size": 10, "font_color": "gray"},
459
+ )
460
+
461
+ # F1 peak marker
462
+ if show_f1_peak:
463
+ best_q = precision_recall_result.best_f1_quantile
464
+ best_f1 = precision_recall_result.best_f1_score
465
+
466
+ fig.add_trace(
467
+ go.Scatter(
468
+ x=[best_q],
469
+ y=[best_f1],
470
+ mode="markers+text",
471
+ name=f"Best F1 ({best_q})",
472
+ marker={"size": 15, "color": "#F39C12", "symbol": "star"},
473
+ text=[f"Best F1: {best_f1:.4f}"],
474
+ textposition="top center",
475
+ hovertemplate=(
476
+ f"<b>Best F1 Score</b><br>"
477
+ f"Quantile: {best_q}<br>"
478
+ f"F1: {best_f1:.4f}<br>"
479
+ f"<extra></extra>"
480
+ ),
481
+ )
482
+ )
483
+
484
+ # Lift curve on secondary axis
485
+ if show_lift:
486
+ fig.add_trace(
487
+ go.Scatter(
488
+ x=reversed_labels,
489
+ y=cum_lift,
490
+ mode="lines+markers",
491
+ name="Cumulative Lift",
492
+ yaxis="y2",
493
+ line={"color": "#2ECC71", "width": 2},
494
+ marker={"size": 6, "symbol": "triangle-up"},
495
+ hovertemplate=("Threshold: %{x}<br>Lift: %{y:.2f}x<br><extra></extra>"),
496
+ )
497
+ )
498
+
499
+ fig.update_layout(
500
+ yaxis2={
501
+ "title": "Lift (vs baseline)",
502
+ "overlaying": "y",
503
+ "side": "right",
504
+ "showgrid": False,
505
+ },
506
+ )
507
+
508
+ # Format y-axis as percentage
509
+ fig.update_yaxes(tickformat=".0%")
510
+
511
+ fig.update_layout(
512
+ legend={
513
+ "yanchor": "bottom",
514
+ "y": 0.01,
515
+ "xanchor": "right",
516
+ "x": 0.99,
517
+ },
518
+ )
519
+
520
+ return fig
521
+
522
+
523
+ def plot_time_to_target_box(
524
+ time_to_target_result: TimeToTargetResult,
525
+ outcome_type: str = "all",
526
+ show_mean: bool = True,
527
+ show_median_line: bool = True,
528
+ theme: str | None = None,
529
+ width: int | None = None,
530
+ height: int | None = None,
531
+ ) -> go.Figure:
532
+ """Plot time-to-target as box plots by quantile.
533
+
534
+ Creates box plots showing the distribution of bars to exit for each
535
+ signal quantile. Can show all outcomes or filter by type.
536
+
537
+ Parameters
538
+ ----------
539
+ time_to_target_result : TimeToTargetResult
540
+ Time-to-target result from BarrierAnalysis.compute_time_to_target()
541
+ outcome_type : str, default "all"
542
+ Which outcomes to show: "all", "tp", "sl", "comparison"
543
+ "comparison" shows TP and SL side by side
544
+ show_mean : bool, default True
545
+ Show mean marker on box plots
546
+ show_median_line : bool, default True
547
+ Show overall median as horizontal line
548
+ theme : str | None
549
+ Plot theme (default, dark, print, presentation)
550
+ width : int | None
551
+ Figure width in pixels
552
+ height : int | None
553
+ Figure height in pixels
554
+
555
+ Returns
556
+ -------
557
+ go.Figure
558
+ Interactive Plotly figure
559
+
560
+ Examples
561
+ --------
562
+ >>> ttt = analysis.compute_time_to_target()
563
+ >>> fig = plot_time_to_target_box(ttt, outcome_type="comparison")
564
+ >>> fig.show()
565
+ """
566
+ theme = validate_theme(theme)
567
+ theme_config = get_theme_config(theme)
568
+
569
+ quantile_labels = time_to_target_result.quantile_labels
570
+ n_quantiles = time_to_target_result.n_quantiles
571
+ outcome_colors = _get_outcome_colors()
572
+
573
+ # Create figure
574
+ title_suffix = {
575
+ "all": "(All Outcomes)",
576
+ "tp": "(Take-Profit)",
577
+ "sl": "(Stop-Loss)",
578
+ "comparison": "(TP vs SL)",
579
+ }
580
+ fig = create_base_figure(
581
+ title=f"Time to Target by Signal Quantile {title_suffix.get(outcome_type, '')}",
582
+ xaxis_title="Signal Quantile",
583
+ yaxis_title="Bars to Exit",
584
+ width=width or theme_config["defaults"]["bar_height"] + 200,
585
+ height=height or theme_config["defaults"]["bar_height"],
586
+ theme=theme,
587
+ )
588
+
589
+ if outcome_type == "comparison":
590
+ # Side-by-side comparison of TP and SL
591
+ for i, q in enumerate(quantile_labels):
592
+ # TP box
593
+ mean_tp = time_to_target_result.mean_bars_tp[q]
594
+ median_tp = time_to_target_result.median_bars_tp[q]
595
+ std_tp = time_to_target_result.std_bars_tp[q]
596
+ count_tp = time_to_target_result.count_tp[q]
597
+
598
+ # Create synthetic box data from statistics
599
+ if count_tp > 0 and not np.isnan(mean_tp):
600
+ q1_tp = max(0, mean_tp - 0.675 * std_tp)
601
+ q3_tp = mean_tp + 0.675 * std_tp
602
+ whisker_low_tp = max(0, mean_tp - 1.5 * std_tp)
603
+ whisker_high_tp = mean_tp + 1.5 * std_tp
604
+
605
+ fig.add_trace(
606
+ go.Box(
607
+ x=[q],
608
+ q1=[q1_tp],
609
+ median=[median_tp],
610
+ q3=[q3_tp],
611
+ lowerfence=[whisker_low_tp],
612
+ upperfence=[whisker_high_tp],
613
+ mean=[mean_tp] if show_mean else None,
614
+ boxmean=show_mean,
615
+ name="Take-Profit" if i == 0 else None,
616
+ legendgroup="tp",
617
+ showlegend=(i == 0),
618
+ marker_color=outcome_colors["tp"],
619
+ offsetgroup="tp",
620
+ hovertemplate=(
621
+ f"Quantile: {q}<br>"
622
+ f"Outcome: Take-Profit<br>"
623
+ f"Mean: {mean_tp:.1f} bars<br>"
624
+ f"Median: {median_tp:.1f} bars<br>"
625
+ f"Count: {count_tp}<br>"
626
+ "<extra></extra>"
627
+ ),
628
+ )
629
+ )
630
+
631
+ # SL box
632
+ mean_sl = time_to_target_result.mean_bars_sl[q]
633
+ median_sl = time_to_target_result.median_bars_sl[q]
634
+ std_sl = time_to_target_result.std_bars_sl[q]
635
+ count_sl = time_to_target_result.count_sl[q]
636
+
637
+ if count_sl > 0 and not np.isnan(mean_sl):
638
+ q1_sl = max(0, mean_sl - 0.675 * std_sl)
639
+ q3_sl = mean_sl + 0.675 * std_sl
640
+ whisker_low_sl = max(0, mean_sl - 1.5 * std_sl)
641
+ whisker_high_sl = mean_sl + 1.5 * std_sl
642
+
643
+ fig.add_trace(
644
+ go.Box(
645
+ x=[q],
646
+ q1=[q1_sl],
647
+ median=[median_sl],
648
+ q3=[q3_sl],
649
+ lowerfence=[whisker_low_sl],
650
+ upperfence=[whisker_high_sl],
651
+ mean=[mean_sl] if show_mean else None,
652
+ boxmean=show_mean,
653
+ name="Stop-Loss" if i == 0 else None,
654
+ legendgroup="sl",
655
+ showlegend=(i == 0),
656
+ marker_color=outcome_colors["sl"],
657
+ offsetgroup="sl",
658
+ hovertemplate=(
659
+ f"Quantile: {q}<br>"
660
+ f"Outcome: Stop-Loss<br>"
661
+ f"Mean: {mean_sl:.1f} bars<br>"
662
+ f"Median: {median_sl:.1f} bars<br>"
663
+ f"Count: {count_sl}<br>"
664
+ "<extra></extra>"
665
+ ),
666
+ )
667
+ )
668
+
669
+ fig.update_layout(boxmode="group")
670
+
671
+ else:
672
+ # Single outcome type or all
673
+ if outcome_type == "tp":
674
+ mean_bars = time_to_target_result.mean_bars_tp
675
+ median_bars = time_to_target_result.median_bars_tp
676
+ std_bars = time_to_target_result.std_bars_tp
677
+ counts = time_to_target_result.count_tp
678
+ color = outcome_colors["tp"]
679
+ elif outcome_type == "sl":
680
+ mean_bars = time_to_target_result.mean_bars_sl
681
+ median_bars = time_to_target_result.median_bars_sl
682
+ std_bars = time_to_target_result.std_bars_sl
683
+ counts = time_to_target_result.count_sl
684
+ color = outcome_colors["sl"]
685
+ else: # all
686
+ mean_bars = time_to_target_result.mean_bars_all
687
+ median_bars = time_to_target_result.median_bars_all
688
+ std_bars = time_to_target_result.std_bars_all
689
+ counts = {
690
+ q: time_to_target_result.count_tp[q]
691
+ + time_to_target_result.count_sl[q]
692
+ + time_to_target_result.count_timeout[q]
693
+ for q in quantile_labels
694
+ }
695
+ color = theme_config["colorway"][0]
696
+
697
+ # Get quantile colors
698
+ colors = _get_quantile_colors(n_quantiles, theme_config)
699
+
700
+ for i, q in enumerate(quantile_labels):
701
+ mean = mean_bars[q]
702
+ median = median_bars[q]
703
+ std = std_bars[q]
704
+ count = counts[q]
705
+
706
+ if count > 0 and not np.isnan(mean):
707
+ q1 = max(0, mean - 0.675 * std)
708
+ q3 = mean + 0.675 * std
709
+ whisker_low = max(0, mean - 1.5 * std)
710
+ whisker_high = mean + 1.5 * std
711
+
712
+ fig.add_trace(
713
+ go.Box(
714
+ x=[q],
715
+ q1=[q1],
716
+ median=[median],
717
+ q3=[q3],
718
+ lowerfence=[whisker_low],
719
+ upperfence=[whisker_high],
720
+ mean=[mean] if show_mean else None,
721
+ boxmean=show_mean,
722
+ name=q,
723
+ showlegend=False,
724
+ marker_color=colors[i] if outcome_type == "all" else color,
725
+ hovertemplate=(
726
+ f"Quantile: {q}<br>"
727
+ f"Mean: {mean:.1f} bars<br>"
728
+ f"Median: {median:.1f} bars<br>"
729
+ f"Std: {std:.1f}<br>"
730
+ f"Count: {count}<br>"
731
+ "<extra></extra>"
732
+ ),
733
+ )
734
+ )
735
+
736
+ # Overall median line
737
+ if show_median_line:
738
+ overall_median = time_to_target_result.overall_median_bars
739
+ fig.add_hline(
740
+ y=overall_median,
741
+ line_dash="dash",
742
+ line_color="gray",
743
+ line_width=2,
744
+ annotation_text=f"Overall Median: {overall_median:.1f}",
745
+ annotation_position="right",
746
+ annotation={"font_size": 10, "font_color": "gray"},
747
+ )
748
+
749
+ # Summary annotation
750
+ summary_text = (
751
+ f"<b>Overall Statistics:</b><br>"
752
+ f"Mean: {time_to_target_result.overall_mean_bars:.1f} bars<br>"
753
+ f"Median: {time_to_target_result.overall_median_bars:.1f} bars<br>"
754
+ f"TP Mean: {time_to_target_result.overall_mean_bars_tp:.1f} bars<br>"
755
+ f"SL Mean: {time_to_target_result.overall_mean_bars_sl:.1f} bars"
756
+ )
757
+
758
+ fig.add_annotation(
759
+ text=summary_text,
760
+ xref="paper",
761
+ yref="paper",
762
+ x=0.98,
763
+ y=0.98,
764
+ showarrow=False,
765
+ bgcolor="rgba(255,255,255,0.9)" if theme != "dark" else "rgba(50,50,50,0.9)",
766
+ bordercolor="gray",
767
+ borderwidth=1,
768
+ align="left",
769
+ xanchor="right",
770
+ yanchor="top",
771
+ font={"size": 10},
772
+ )
773
+
774
+ return fig
775
+
776
+
777
+ __all__ = [
778
+ "plot_hit_rate_heatmap",
779
+ "plot_profit_factor_bar",
780
+ "plot_precision_recall_curve",
781
+ "plot_time_to_target_box",
782
+ ]