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,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
|
+
]
|