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,79 @@
1
+ """Dashboard styling constants.
2
+
3
+ CSS styling for the Trade SHAP diagnostics dashboard.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ # Professional CSS styling for styled mode
9
+ STYLED_CSS = """
10
+ <style>
11
+ /* Professional theme colors */
12
+ :root {
13
+ --primary-color: #1f77b4;
14
+ --success-color: #51CF66;
15
+ --warning-color: #FF9800;
16
+ --error-color: #FF6B6B;
17
+ }
18
+
19
+ /* Headers with professional styling */
20
+ h1 {
21
+ color: var(--primary-color);
22
+ font-weight: 600;
23
+ padding-bottom: 1rem;
24
+ border-bottom: 2px solid #e0e0e0;
25
+ }
26
+
27
+ h2 {
28
+ font-weight: 600;
29
+ margin-top: 1.5rem;
30
+ }
31
+
32
+ /* Enhanced metrics */
33
+ [data-testid="stMetricValue"] {
34
+ font-size: 2rem;
35
+ font-weight: 600;
36
+ }
37
+
38
+ /* Polished containers */
39
+ .stExpander {
40
+ border: 1px solid #e0e0e0;
41
+ border-radius: 8px;
42
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
43
+ }
44
+
45
+ /* Professional buttons */
46
+ .stButton>button {
47
+ border-radius: 6px;
48
+ font-weight: 500;
49
+ transition: all 0.2s;
50
+ }
51
+
52
+ .stButton>button:hover {
53
+ transform: translateY(-1px);
54
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
55
+ }
56
+
57
+ /* Download buttons styling */
58
+ .stDownloadButton>button {
59
+ background-color: var(--primary-color) !important;
60
+ color: white !important;
61
+ }
62
+
63
+ /* Sidebar styling */
64
+ [data-testid="stSidebar"] {
65
+ background-color: #f8f9fa;
66
+ }
67
+
68
+ /* Tab styling */
69
+ .stTabs [data-baseweb="tab"] {
70
+ padding: 0.5rem 1rem;
71
+ font-weight: 500;
72
+ }
73
+
74
+ /* Progress bars */
75
+ .stProgress > div > div {
76
+ background-color: var(--success-color);
77
+ }
78
+ </style>
79
+ """
@@ -0,0 +1,21 @@
1
+ """Dashboard tab modules.
2
+
3
+ Each tab module provides a `render_tab(st, bundle)` function that renders
4
+ the tab content using the Streamlit instance and DashboardBundle.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ml4t.diagnostic.evaluation.trade_dashboard.tabs import (
10
+ patterns,
11
+ shap_analysis,
12
+ stat_validation,
13
+ worst_trades,
14
+ )
15
+
16
+ __all__ = [
17
+ "patterns",
18
+ "shap_analysis",
19
+ "stat_validation",
20
+ "worst_trades",
21
+ ]
@@ -0,0 +1,354 @@
1
+ """Error Patterns tab.
2
+
3
+ Displays clustered error patterns with hypotheses and recommended actions.
4
+ Applies Benjamini-Hochberg FDR correction to feature significance.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ import pandas as pd
12
+
13
+ if TYPE_CHECKING:
14
+ from ml4t.diagnostic.evaluation.trade_dashboard.types import DashboardBundle
15
+
16
+
17
+ def render_tab(st: Any, bundle: DashboardBundle) -> None:
18
+ """Render the Error Patterns tab.
19
+
20
+ Parameters
21
+ ----------
22
+ st : streamlit
23
+ Streamlit module instance.
24
+ bundle : DashboardBundle
25
+ Normalized dashboard data.
26
+ """
27
+ st.header("Error Patterns & Recommendations")
28
+
29
+ st.info(
30
+ "Error patterns are identified by clustering trades with similar "
31
+ "SHAP profiles. Each pattern includes hypotheses and actionable "
32
+ "recommendations for improvement."
33
+ )
34
+
35
+ patterns_df = bundle.patterns_df
36
+
37
+ if patterns_df.empty:
38
+ st.warning("No error patterns found. Ensure clustering was performed during analysis.")
39
+ return
40
+
41
+ patterns = patterns_df.to_dict("records")
42
+
43
+ # Sidebar filters
44
+ with st.sidebar:
45
+ st.divider()
46
+ st.subheader("Pattern Filters")
47
+
48
+ sort_by = st.selectbox(
49
+ "Sort patterns by",
50
+ options=[
51
+ "Pattern ID",
52
+ "Number of Trades (Desc)",
53
+ "Confidence (Desc)",
54
+ "Distinctiveness (Desc)",
55
+ ],
56
+ index=1,
57
+ )
58
+
59
+ max_n_trades = max(p.get("n_trades", 0) for p in patterns) if patterns else 1
60
+ min_trades = st.slider(
61
+ "Min trades in pattern",
62
+ min_value=1,
63
+ max_value=max(1, max_n_trades),
64
+ value=1,
65
+ )
66
+
67
+ show_only_with_hypothesis = st.checkbox(
68
+ "Only patterns with hypotheses",
69
+ value=False,
70
+ )
71
+
72
+ # Filter patterns
73
+ filtered_patterns = [
74
+ p
75
+ for p in patterns
76
+ if p.get("n_trades", 0) >= min_trades
77
+ and (not show_only_with_hypothesis or p.get("hypothesis"))
78
+ ]
79
+
80
+ # Sort patterns
81
+ if sort_by == "Pattern ID":
82
+ filtered_patterns.sort(key=lambda p: p.get("cluster_id", 0))
83
+ elif sort_by == "Number of Trades (Desc)":
84
+ filtered_patterns.sort(key=lambda p: p.get("n_trades", 0), reverse=True)
85
+ elif sort_by == "Confidence (Desc)":
86
+ filtered_patterns.sort(key=lambda p: p.get("confidence") or 0, reverse=True)
87
+ elif sort_by == "Distinctiveness (Desc)":
88
+ filtered_patterns.sort(key=lambda p: p.get("distinctiveness") or 0, reverse=True)
89
+
90
+ # Display count
91
+ if len(filtered_patterns) < len(patterns):
92
+ st.caption(f"Showing {len(filtered_patterns)} of {len(patterns)} patterns")
93
+
94
+ # Display patterns
95
+ for i, pattern in enumerate(filtered_patterns):
96
+ _render_pattern_card(st, pattern, expanded=(i == 0))
97
+
98
+ # Summary statistics
99
+ st.divider()
100
+ _render_pattern_summary(st, patterns, filtered_patterns)
101
+
102
+
103
+ def _render_pattern_card(st: Any, pattern: dict[str, Any], expanded: bool = False) -> None:
104
+ """Render a single pattern card."""
105
+ cluster_id = pattern.get("cluster_id", 0)
106
+ n_trades = pattern.get("n_trades", 0)
107
+ description = pattern.get("description", "No description")
108
+ hypothesis = pattern.get("hypothesis")
109
+ actions = pattern.get("actions", [])
110
+ confidence = pattern.get("confidence")
111
+ separation_score = pattern.get("separation_score")
112
+ distinctiveness = pattern.get("distinctiveness")
113
+ top_features = pattern.get("top_features", [])
114
+
115
+ with st.expander(
116
+ f"**Pattern {cluster_id}**: {n_trades} trades - {description}",
117
+ expanded=expanded,
118
+ ):
119
+ # Metrics row
120
+ col1, col2, col3, col4 = st.columns(4)
121
+
122
+ with col1:
123
+ st.metric("Trades", n_trades)
124
+
125
+ with col2:
126
+ if confidence is not None:
127
+ st.metric("Confidence", f"{confidence:.1%}")
128
+ else:
129
+ st.metric("Confidence", "N/A")
130
+
131
+ with col3:
132
+ if separation_score is not None:
133
+ st.metric(
134
+ "Separation",
135
+ f"{separation_score:.2f}",
136
+ help="Distance to nearest cluster (higher = more distinct)",
137
+ )
138
+ else:
139
+ st.metric("Separation", "N/A")
140
+
141
+ with col4:
142
+ if distinctiveness is not None:
143
+ st.metric(
144
+ "Distinctiveness",
145
+ f"{distinctiveness:.2f}",
146
+ help="Uniqueness of SHAP profile (higher = more unique)",
147
+ )
148
+ else:
149
+ st.metric("Distinctiveness", "N/A")
150
+
151
+ st.divider()
152
+
153
+ # Hypothesis
154
+ st.markdown("### Hypothesis")
155
+ if hypothesis:
156
+ st.markdown(f"> {hypothesis}")
157
+ else:
158
+ st.markdown("*No hypothesis generated for this pattern.*")
159
+
160
+ st.divider()
161
+
162
+ # Actions
163
+ st.markdown("### Recommended Actions")
164
+ if actions:
165
+ for idx, action in enumerate(actions, 1):
166
+ st.markdown(f"{idx}. {action}")
167
+ else:
168
+ st.markdown("*No actions suggested for this pattern.*")
169
+
170
+ # Top features with FDR correction
171
+ if top_features:
172
+ st.divider()
173
+ _render_pattern_features(st, top_features)
174
+
175
+
176
+ def _render_pattern_features(st: Any, top_features: list[Any]) -> None:
177
+ """Render pattern features with BH-FDR correction."""
178
+ st.markdown("### Key Features")
179
+
180
+ # Apply BH-FDR correction to p-values
181
+ features_with_fdr = _apply_fdr_correction(top_features)
182
+
183
+ feature_names = [f["feature"] for f in features_with_fdr[:10]]
184
+ feature_shap = [f["shap_value"] for f in features_with_fdr[:10]]
185
+ feature_sig = [f["significant_fdr"] for f in features_with_fdr[:10]]
186
+
187
+ # Create visualization
188
+ import plotly.graph_objects as go
189
+
190
+ colors = ["#51CF66" if sig else "#ADB5BD" for sig in feature_sig]
191
+
192
+ fig = go.Figure()
193
+
194
+ fig.add_trace(
195
+ go.Bar(
196
+ x=feature_shap,
197
+ y=feature_names,
198
+ orientation="h",
199
+ marker={"color": colors},
200
+ text=[f"{val:.4f}" for val in feature_shap],
201
+ textposition="auto",
202
+ hovertemplate="<b>%{y}</b><br>Mean SHAP: %{x:.4f}<extra></extra>",
203
+ )
204
+ )
205
+
206
+ fig.update_layout(
207
+ title="Top Features for This Pattern (FDR-corrected significance)",
208
+ xaxis_title="Mean SHAP Value",
209
+ yaxis_title="Feature",
210
+ height=max(300, len(feature_names) * 30),
211
+ yaxis={"autorange": "reversed"},
212
+ showlegend=False,
213
+ )
214
+
215
+ st.plotly_chart(fig, use_container_width=True)
216
+
217
+ # Features table with FDR info
218
+ features_df = pd.DataFrame(
219
+ [
220
+ {
221
+ "Feature": f["feature"],
222
+ "Mean SHAP": f"{f['shap_value']:.4f}",
223
+ "P-value": f"{f['p_value']:.4f}" if f["p_value"] is not None else "N/A",
224
+ "Adj. P-value (BH)": f"{f['adjusted_p']:.4f}"
225
+ if f["adjusted_p"] is not None
226
+ else "N/A",
227
+ "Significant": "Yes" if f["significant_fdr"] else "No",
228
+ }
229
+ for f in features_with_fdr[:10]
230
+ ]
231
+ )
232
+
233
+ st.dataframe(features_df, hide_index=True, use_container_width=True)
234
+
235
+ with st.expander("Understanding Feature Significance (FDR-corrected)"):
236
+ st.markdown(
237
+ """
238
+ **Benjamini-Hochberg FDR Correction:**
239
+
240
+ Raw p-values are corrected for multiple comparisons using the
241
+ Benjamini-Hochberg procedure, which controls the False Discovery Rate.
242
+
243
+ - **P-value**: Raw p-value from statistical test
244
+ - **Adj. P-value (BH)**: FDR-adjusted p-value
245
+ - **Significant**: Yes if adjusted p-value < 0.05
246
+
247
+ **Interpretation:**
248
+ - Green bars: Statistically significant after FDR correction
249
+ - Gray bars: Not significant (may be noise)
250
+
251
+ Focus on significant features for most reliable insights.
252
+ """
253
+ )
254
+
255
+
256
+ def _apply_fdr_correction(top_features: list[Any]) -> list[dict[str, Any]]:
257
+ """Apply Benjamini-Hochberg FDR correction to feature p-values.
258
+
259
+ Parameters
260
+ ----------
261
+ top_features : list
262
+ List of feature tuples. Expected formats:
263
+ - (name, shap_value)
264
+ - (name, shap_value, p_value)
265
+ - (name, shap_value, p_value_t, p_value_mw, significant)
266
+
267
+ Returns
268
+ -------
269
+ list[dict]
270
+ Features with FDR-corrected significance.
271
+ """
272
+ from ml4t.diagnostic.evaluation.trade_dashboard.stats import benjamini_hochberg_fdr
273
+
274
+ # Parse features into consistent format
275
+ parsed = []
276
+ for item in top_features:
277
+ if len(item) >= 2:
278
+ feature = item[0]
279
+ shap_val = item[1]
280
+ # Use t-test p-value if available (index 2), otherwise None
281
+ p_value = item[2] if len(item) > 2 else None
282
+ parsed.append(
283
+ {
284
+ "feature": feature,
285
+ "shap_value": shap_val,
286
+ "p_value": p_value,
287
+ }
288
+ )
289
+
290
+ if not parsed:
291
+ return []
292
+
293
+ # Extract p-values for FDR correction
294
+ p_values = [f["p_value"] for f in parsed if f["p_value"] is not None]
295
+
296
+ if len(p_values) >= 2:
297
+ # Apply BH-FDR correction
298
+ fdr_result = benjamini_hochberg_fdr(p_values, alpha=0.05)
299
+ adjusted_p_values = fdr_result["adjusted_p_values"]
300
+ rejected = fdr_result["rejected"]
301
+
302
+ # Map back to features
303
+ p_idx = 0
304
+ for f in parsed:
305
+ if f["p_value"] is not None:
306
+ f["adjusted_p"] = adjusted_p_values[p_idx]
307
+ f["significant_fdr"] = rejected[p_idx]
308
+ p_idx += 1
309
+ else:
310
+ f["adjusted_p"] = None
311
+ f["significant_fdr"] = False
312
+ else:
313
+ # Not enough p-values for FDR
314
+ for f in parsed:
315
+ f["adjusted_p"] = f["p_value"]
316
+ f["significant_fdr"] = f["p_value"] is not None and f["p_value"] < 0.05
317
+
318
+ return parsed
319
+
320
+
321
+ def _render_pattern_summary(
322
+ st: Any,
323
+ all_patterns: list[dict[str, Any]],
324
+ filtered_patterns: list[dict[str, Any]],
325
+ ) -> None:
326
+ """Render pattern summary statistics."""
327
+ st.subheader("Pattern Summary")
328
+
329
+ col1, col2, col3, col4 = st.columns(4)
330
+
331
+ with col1:
332
+ st.metric("Total Patterns", len(all_patterns))
333
+ if len(filtered_patterns) < len(all_patterns):
334
+ st.caption(f"({len(filtered_patterns)} shown)")
335
+
336
+ with col2:
337
+ patterns_with_hypotheses = sum(1 for p in all_patterns if p.get("hypothesis"))
338
+ st.metric("With Hypotheses", patterns_with_hypotheses)
339
+ st.caption(f"{patterns_with_hypotheses}/{len(all_patterns)} patterns")
340
+
341
+ with col3:
342
+ total_trades = sum(p.get("n_trades", 0) for p in all_patterns)
343
+ st.metric("Total Trades", total_trades)
344
+
345
+ with col4:
346
+ avg_confidence = [
347
+ float(p["confidence"]) for p in all_patterns if p.get("confidence") is not None
348
+ ]
349
+ if avg_confidence:
350
+ import numpy as np
351
+
352
+ st.metric("Avg Confidence", f"{float(np.mean(avg_confidence)):.1%}")
353
+ else:
354
+ st.metric("Avg Confidence", "N/A")