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,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)
|