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,245 @@
1
+ """
2
+ Structured Logging for ML4T Diagnostic
3
+
4
+ Provides configurable logging with levels, JSON output, and context preservation.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import sys
10
+ from datetime import datetime
11
+ from enum import Enum
12
+
13
+
14
+ class LogLevel(str, Enum):
15
+ """Log level enumeration."""
16
+
17
+ DEBUG = "DEBUG"
18
+ INFO = "INFO"
19
+ WARNING = "WARNING"
20
+ ERROR = "ERROR"
21
+
22
+
23
+ class QEvalLogger:
24
+ """
25
+ Structured logger for ML4T Diagnostic operations.
26
+
27
+ Provides:
28
+ - Configurable log levels
29
+ - JSON-structured output
30
+ - Context preservation
31
+ - Performance timing
32
+ - Debug mode support
33
+
34
+ Example:
35
+ >>> logger = QEvalLogger("mlquant.evaluation.metrics")
36
+ >>> logger.info("Computing metric", metric="sharpe_ratio", n_samples=100)
37
+ >>> logger.debug("Intermediate result", value=0.5)
38
+ >>> logger.error("Computation failed", error="division by zero")
39
+ """
40
+
41
+ def __init__(self, name: str, level: LogLevel = LogLevel.INFO, output_json: bool = False):
42
+ """
43
+ Initialize logger.
44
+
45
+ Args:
46
+ name: Logger name (usually module name)
47
+ level: Minimum log level to display
48
+ output_json: Whether to output JSON format
49
+ """
50
+ self.name = name
51
+ self.level = level
52
+ self.output_json = output_json
53
+ self._python_logger = logging.getLogger(name)
54
+ self._configure_python_logger()
55
+
56
+ def _configure_python_logger(self):
57
+ """Configure underlying Python logger."""
58
+ # Map our levels to Python logging levels
59
+ level_map = {
60
+ LogLevel.DEBUG: logging.DEBUG,
61
+ LogLevel.INFO: logging.INFO,
62
+ LogLevel.WARNING: logging.WARNING,
63
+ LogLevel.ERROR: logging.ERROR,
64
+ }
65
+ self._python_logger.setLevel(level_map[self.level])
66
+
67
+ # Add handler if none exists
68
+ if not self._python_logger.handlers:
69
+ handler = logging.StreamHandler(sys.stderr)
70
+ handler.setFormatter(
71
+ logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
72
+ )
73
+ self._python_logger.addHandler(handler)
74
+
75
+ def _format_message(self, level: str, message: str, **context) -> str:
76
+ """
77
+ Format log message.
78
+
79
+ Args:
80
+ level: Log level
81
+ message: Log message
82
+ **context: Additional context fields
83
+
84
+ Returns:
85
+ Formatted message string
86
+ """
87
+ if self.output_json:
88
+ log_entry = {
89
+ "timestamp": datetime.now().isoformat(),
90
+ "logger": self.name,
91
+ "level": level,
92
+ "message": message,
93
+ **context,
94
+ }
95
+ return json.dumps(log_entry)
96
+ else:
97
+ # Human-readable format
98
+ context_str = " ".join(f"{k}={v}" for k, v in context.items())
99
+ if context_str:
100
+ return f"{message} ({context_str})"
101
+ return message
102
+
103
+ def debug(self, message: str, **context):
104
+ """
105
+ Log debug message.
106
+
107
+ Args:
108
+ message: Debug message
109
+ **context: Additional context
110
+ """
111
+ if self._should_log(LogLevel.DEBUG):
112
+ formatted = self._format_message("DEBUG", message, **context)
113
+ self._python_logger.debug(formatted)
114
+
115
+ def info(self, message: str, **context):
116
+ """
117
+ Log info message.
118
+
119
+ Args:
120
+ message: Info message
121
+ **context: Additional context
122
+ """
123
+ if self._should_log(LogLevel.INFO):
124
+ formatted = self._format_message("INFO", message, **context)
125
+ self._python_logger.info(formatted)
126
+
127
+ def warning(self, message: str, **context):
128
+ """
129
+ Log warning message.
130
+
131
+ Args:
132
+ message: Warning message
133
+ **context: Additional context
134
+ """
135
+ if self._should_log(LogLevel.WARNING):
136
+ formatted = self._format_message("WARNING", message, **context)
137
+ self._python_logger.warning(formatted)
138
+
139
+ def error(self, message: str, **context):
140
+ """
141
+ Log error message.
142
+
143
+ Args:
144
+ message: Error message
145
+ **context: Additional context
146
+ """
147
+ if self._should_log(LogLevel.ERROR):
148
+ formatted = self._format_message("ERROR", message, **context)
149
+ self._python_logger.error(formatted)
150
+
151
+ def _should_log(self, level: LogLevel) -> bool:
152
+ """Check if message should be logged at given level."""
153
+ level_order = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR]
154
+ return level_order.index(level) >= level_order.index(self.level)
155
+
156
+ def timed(self, operation: str):
157
+ """
158
+ Context manager for timing operations.
159
+
160
+ Args:
161
+ operation: Operation name
162
+
163
+ Example:
164
+ >>> with logger.timed("compute_sharpe"):
165
+ ... result = compute_sharpe_ratio(returns)
166
+ """
167
+ from .performance import PerformanceTracker
168
+
169
+ return PerformanceTracker(operation, logger=self)
170
+
171
+
172
+ # Global logger registry
173
+ _loggers: dict[str, QEvalLogger] = {}
174
+ _global_level: LogLevel = LogLevel.INFO
175
+ _global_json_output: bool = False
176
+
177
+
178
+ def get_logger(name: str) -> QEvalLogger:
179
+ """
180
+ Get or create logger for module.
181
+
182
+ Args:
183
+ name: Logger name (usually __name__)
184
+
185
+ Returns:
186
+ QEvalLogger instance
187
+
188
+ Example:
189
+ >>> logger = get_logger(__name__)
190
+ >>> logger.info("Processing data")
191
+ """
192
+ if name not in _loggers:
193
+ _loggers[name] = QEvalLogger(name, level=_global_level, output_json=_global_json_output)
194
+ return _loggers[name]
195
+
196
+
197
+ def set_log_level(level: LogLevel):
198
+ """
199
+ Set global log level.
200
+
201
+ Args:
202
+ level: Minimum log level
203
+
204
+ Example:
205
+ >>> set_log_level(LogLevel.DEBUG)
206
+ """
207
+ global _global_level
208
+ _global_level = level
209
+
210
+ # Update existing loggers
211
+ for logger in _loggers.values():
212
+ logger.level = level
213
+ logger._configure_python_logger()
214
+
215
+
216
+ def get_log_level() -> LogLevel:
217
+ """
218
+ Get current global log level.
219
+
220
+ Returns:
221
+ Current log level
222
+ """
223
+ return _global_level
224
+
225
+
226
+ def configure_logging(level: LogLevel = LogLevel.INFO, output_json: bool = False):
227
+ """
228
+ Configure global logging settings.
229
+
230
+ Args:
231
+ level: Minimum log level
232
+ output_json: Whether to output JSON format
233
+
234
+ Example:
235
+ >>> configure_logging(LogLevel.DEBUG, output_json=True)
236
+ """
237
+ global _global_level, _global_json_output
238
+ _global_level = level
239
+ _global_json_output = output_json
240
+
241
+ # Update existing loggers
242
+ for logger in _loggers.values():
243
+ logger.level = level
244
+ logger.output_json = output_json
245
+ logger._configure_python_logger()
@@ -0,0 +1,234 @@
1
+ """
2
+ Performance Metrics Tracking
3
+
4
+ Provides timing and performance measurement utilities.
5
+ """
6
+
7
+ import time
8
+ from contextlib import contextmanager
9
+ from functools import wraps
10
+ from typing import Any
11
+
12
+
13
+ class PerformanceTracker:
14
+ """
15
+ Context manager for timing operations.
16
+
17
+ Example:
18
+ >>> with PerformanceTracker("sharpe_computation") as tracker:
19
+ ... result = compute_sharpe_ratio(returns)
20
+ >>> print(f"Elapsed: {tracker.elapsed:.3f}s")
21
+ """
22
+
23
+ def __init__(self, operation: str, logger: Any | None = None):
24
+ """
25
+ Initialize performance tracker.
26
+
27
+ Args:
28
+ operation: Operation name
29
+ logger: Optional logger to record timing
30
+ """
31
+ self.operation = operation
32
+ self.logger = logger
33
+ self.start_time: float | None = None
34
+ self.end_time: float | None = None
35
+
36
+ def __enter__(self):
37
+ """Start timing."""
38
+ self.start_time = time.time()
39
+ if self.logger:
40
+ self.logger.debug(f"Starting {self.operation}")
41
+ return self
42
+
43
+ def __exit__(self, exc_type, exc_val, exc_tb):
44
+ """Stop timing and log result."""
45
+ self.end_time = time.time()
46
+
47
+ if self.logger:
48
+ if exc_type is None:
49
+ self.logger.info(f"Completed {self.operation}", elapsed_seconds=self.elapsed)
50
+ else:
51
+ self.logger.error(
52
+ f"Failed {self.operation}", elapsed_seconds=self.elapsed, error=str(exc_val)
53
+ )
54
+
55
+ @property
56
+ def elapsed(self) -> float:
57
+ """
58
+ Get elapsed time in seconds.
59
+
60
+ Returns:
61
+ Elapsed time (0 if not complete)
62
+ """
63
+ if self.start_time is None:
64
+ return 0.0
65
+
66
+ end = self.end_time if self.end_time else time.time()
67
+ return end - self.start_time
68
+
69
+
70
+ def timed(func):
71
+ """
72
+ Decorator to time function execution.
73
+
74
+ Args:
75
+ func: Function to time
76
+
77
+ Returns:
78
+ Wrapped function that logs execution time
79
+
80
+ Example:
81
+ >>> @timed
82
+ ... def compute_sharpe(returns):
83
+ ... return returns.mean() / returns.std()
84
+ """
85
+
86
+ @wraps(func)
87
+ def wrapper(*args, **kwargs):
88
+ start = time.time()
89
+ try:
90
+ result = func(*args, **kwargs)
91
+ elapsed = time.time() - start
92
+ print(f"{func.__name__}: {elapsed:.3f}s", flush=True)
93
+ return result
94
+ except Exception as e:
95
+ elapsed = time.time() - start
96
+ print(f"{func.__name__} failed after {elapsed:.3f}s: {e}", flush=True)
97
+ raise
98
+
99
+ return wrapper
100
+
101
+
102
+ class PerformanceMonitor:
103
+ """
104
+ Monitor and aggregate performance metrics.
105
+
106
+ Example:
107
+ >>> monitor = PerformanceMonitor()
108
+ >>> with monitor.track("operation1"):
109
+ ... do_work()
110
+ >>> with monitor.track("operation2"):
111
+ ... do_more_work()
112
+ >>> print(monitor.summary())
113
+ """
114
+
115
+ def __init__(self):
116
+ """Initialize performance monitor."""
117
+ self.metrics: dict[str, list[float]] = {}
118
+
119
+ def track(self, operation: str):
120
+ """
121
+ Track operation timing.
122
+
123
+ Args:
124
+ operation: Operation name
125
+
126
+ Returns:
127
+ Context manager for timing
128
+
129
+ Example:
130
+ >>> with monitor.track("compute"):
131
+ ... result = expensive_computation()
132
+ """
133
+ return _MonitoredOperation(self, operation)
134
+
135
+ def record(self, operation: str, elapsed: float):
136
+ """
137
+ Record timing for operation.
138
+
139
+ Args:
140
+ operation: Operation name
141
+ elapsed: Elapsed time in seconds
142
+ """
143
+ if operation not in self.metrics:
144
+ self.metrics[operation] = []
145
+ self.metrics[operation].append(elapsed)
146
+
147
+ def summary(self) -> dict[str, dict[str, float]]:
148
+ """
149
+ Get summary statistics for all operations.
150
+
151
+ Returns:
152
+ Dict mapping operation name to statistics
153
+ (count, total, mean, min, max)
154
+
155
+ Example:
156
+ >>> stats = monitor.summary()
157
+ >>> print(stats["compute"]["mean"])
158
+ 0.523
159
+ """
160
+ summary = {}
161
+
162
+ for operation, timings in self.metrics.items():
163
+ if timings:
164
+ summary[operation] = {
165
+ "count": len(timings),
166
+ "total": sum(timings),
167
+ "mean": sum(timings) / len(timings),
168
+ "min": min(timings),
169
+ "max": max(timings),
170
+ }
171
+
172
+ return summary
173
+
174
+ def reset(self):
175
+ """Clear all metrics."""
176
+ self.metrics.clear()
177
+
178
+
179
+ class _MonitoredOperation:
180
+ """Internal context manager for PerformanceMonitor."""
181
+
182
+ def __init__(self, monitor: PerformanceMonitor, operation: str):
183
+ self.monitor = monitor
184
+ self.operation = operation
185
+ self.start_time: float | None = None
186
+
187
+ def __enter__(self):
188
+ self.start_time = time.time()
189
+ return self
190
+
191
+ def __exit__(self, exc_type, exc_val, exc_tb):
192
+ if self.start_time:
193
+ elapsed = time.time() - self.start_time
194
+ self.monitor.record(self.operation, elapsed)
195
+
196
+
197
+ # Global performance monitor instance
198
+ _global_monitor = PerformanceMonitor()
199
+
200
+
201
+ def get_performance_monitor() -> PerformanceMonitor:
202
+ """
203
+ Get global performance monitor.
204
+
205
+ Returns:
206
+ Global PerformanceMonitor instance
207
+
208
+ Example:
209
+ >>> monitor = get_performance_monitor()
210
+ >>> with monitor.track("operation"):
211
+ ... do_work()
212
+ """
213
+ return _global_monitor
214
+
215
+
216
+ @contextmanager
217
+ def measure_time(operation: str):
218
+ """
219
+ Context manager to measure and print operation time.
220
+
221
+ Args:
222
+ operation: Operation name
223
+
224
+ Example:
225
+ >>> with measure_time("data_loading"):
226
+ ... data = load_large_dataset()
227
+ data_loading: 2.345s
228
+ """
229
+ start = time.time()
230
+ try:
231
+ yield
232
+ finally:
233
+ elapsed = time.time() - start
234
+ print(f"{operation}: {elapsed:.3f}s", flush=True)
@@ -0,0 +1,234 @@
1
+ """
2
+ Progress Indicators for Long-Running Operations
3
+
4
+ Provides progress bars and indicators for batch operations.
5
+ """
6
+
7
+ import sys
8
+ import time
9
+ from collections.abc import Iterable, Sized
10
+ from contextlib import contextmanager
11
+ from typing import Any, cast
12
+
13
+
14
+ class ProgressBar:
15
+ """
16
+ Simple progress bar for terminal output.
17
+
18
+ Example:
19
+ >>> progress = ProgressBar(total=100, description="Processing")
20
+ >>> for i in range(100):
21
+ ... progress.update(1)
22
+ >>> progress.close()
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ total: int,
28
+ description: str = "",
29
+ width: int = 50,
30
+ show_percentage: bool = True,
31
+ show_count: bool = True,
32
+ ):
33
+ """
34
+ Initialize progress bar.
35
+
36
+ Args:
37
+ total: Total number of items
38
+ description: Progress description
39
+ width: Width of progress bar in characters
40
+ show_percentage: Show percentage complete
41
+ show_count: Show current/total count
42
+ """
43
+ self.total = total
44
+ self.description = description
45
+ self.width = width
46
+ self.show_percentage = show_percentage
47
+ self.show_count = show_count
48
+ self.current = 0
49
+ self.start_time = time.time()
50
+
51
+ def update(self, n: int = 1):
52
+ """
53
+ Update progress by n items.
54
+
55
+ Args:
56
+ n: Number of items processed
57
+ """
58
+ self.current += n
59
+ self._render()
60
+
61
+ def _render(self):
62
+ """Render progress bar to terminal."""
63
+ # Calculate percentage
64
+ percentage = self.current / self.total if self.total > 0 else 0
65
+
66
+ # Build progress bar
67
+ filled = int(self.width * percentage)
68
+ bar = "█" * filled + "░" * (self.width - filled)
69
+
70
+ # Build status text
71
+ parts = []
72
+ if self.description:
73
+ parts.append(self.description)
74
+
75
+ parts.append(f"[{bar}]")
76
+
77
+ if self.show_percentage:
78
+ parts.append(f"{percentage * 100:.1f}%")
79
+
80
+ if self.show_count:
81
+ parts.append(f"({self.current}/{self.total})")
82
+
83
+ # Add elapsed time
84
+ elapsed = time.time() - self.start_time
85
+ if elapsed > 1:
86
+ parts.append(f"{elapsed:.1f}s")
87
+
88
+ # Write to stderr (doesn't interfere with stdout)
89
+ sys.stderr.write("\r" + " ".join(parts))
90
+ sys.stderr.flush()
91
+
92
+ def close(self):
93
+ """Close progress bar and move to next line."""
94
+ sys.stderr.write("\n")
95
+ sys.stderr.flush()
96
+
97
+ def __enter__(self):
98
+ """Context manager entry."""
99
+ return self
100
+
101
+ def __exit__(self, exc_type, exc_val, exc_tb):
102
+ """Context manager exit."""
103
+ self.close()
104
+
105
+
106
+ @contextmanager
107
+ def progress_indicator(iterable: Iterable[Any], total: int | None = None, description: str = ""):
108
+ """
109
+ Context manager for iterating with progress indicator.
110
+
111
+ Args:
112
+ iterable: Items to iterate over
113
+ total: Total number of items (or len(iterable))
114
+ description: Progress description
115
+
116
+ Yields:
117
+ Items from iterable with progress updates
118
+
119
+ Example:
120
+ >>> items = range(100)
121
+ >>> with progress_indicator(items, description="Processing") as progress:
122
+ ... for item in progress:
123
+ ... process(item)
124
+ """
125
+ # Try to get length if not provided
126
+ if total is None:
127
+ if hasattr(iterable, "__len__"):
128
+ total = len(cast(Sized, iterable))
129
+ else:
130
+ total = 0 # Unknown length
131
+
132
+ progress = ProgressBar(total=total, description=description)
133
+
134
+ try:
135
+ for item in iterable:
136
+ yield item
137
+ progress.update(1)
138
+ finally:
139
+ progress.close()
140
+
141
+
142
+ def spinner(description: str = "Working"):
143
+ """
144
+ Simple spinner for indefinite operations.
145
+
146
+ Args:
147
+ description: Operation description
148
+
149
+ Example:
150
+ >>> spin = spinner("Computing")
151
+ >>> next(spin) # Show next frame
152
+ >>> next(spin)
153
+ >>> # When done, just stop calling next()
154
+ """
155
+ frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
156
+ i = 0
157
+
158
+ while True:
159
+ frame = frames[i % len(frames)]
160
+ sys.stderr.write(f"\r{frame} {description}")
161
+ sys.stderr.flush()
162
+ i += 1
163
+ yield
164
+
165
+
166
+ class ProgressTracker:
167
+ """
168
+ Track progress across multiple stages.
169
+
170
+ Example:
171
+ >>> tracker = ProgressTracker(["load", "process", "save"])
172
+ >>> tracker.start("load")
173
+ >>> # ... loading ...
174
+ >>> tracker.complete("load")
175
+ >>> tracker.start("process")
176
+ >>> # ... processing ...
177
+ >>> tracker.complete("process")
178
+ """
179
+
180
+ def __init__(self, stages: list[str]):
181
+ """
182
+ Initialize progress tracker.
183
+
184
+ Args:
185
+ stages: List of stage names
186
+ """
187
+ self.stages = stages
188
+ self.current_stage: str | None = None
189
+ self.completed_stages: set[str] = set()
190
+ self.start_times: dict[str, float] = {}
191
+
192
+ def start(self, stage: str):
193
+ """
194
+ Start a stage.
195
+
196
+ Args:
197
+ stage: Stage name
198
+ """
199
+ if stage not in self.stages:
200
+ raise ValueError(f"Unknown stage: {stage}")
201
+
202
+ self.current_stage = stage
203
+ self.start_times[stage] = time.time()
204
+
205
+ # Display progress
206
+ current_idx = self.stages.index(stage)
207
+ total = len(self.stages)
208
+ print(f"[{current_idx + 1}/{total}] Starting: {stage}", file=sys.stderr)
209
+
210
+ def complete(self, stage: str):
211
+ """
212
+ Mark stage as complete.
213
+
214
+ Args:
215
+ stage: Stage name
216
+ """
217
+ self.completed_stages.add(stage)
218
+ self.current_stage = None
219
+
220
+ # Display completion
221
+ if stage in self.start_times:
222
+ elapsed = time.time() - self.start_times[stage]
223
+ print(f"✓ Completed: {stage} ({elapsed:.2f}s)", file=sys.stderr)
224
+
225
+ def progress(self) -> float:
226
+ """
227
+ Get overall progress (0.0 to 1.0).
228
+
229
+ Returns:
230
+ Progress fraction
231
+ """
232
+ if not self.stages:
233
+ return 1.0
234
+ return len(self.completed_stages) / len(self.stages)