#!/usr/bin/env python3 """ Progress Manager for tracking multiple parallel backtest tasks """ import threading import time import sys from typing import Dict, Optional, Callable from dataclasses import dataclass @dataclass class TaskProgress: """Represents progress information for a single task""" task_id: str name: str current: int total: int start_time: float last_update: float @property def percentage(self) -> float: """Calculate completion percentage""" if self.total == 0: return 0.0 return (self.current / self.total) * 100 @property def elapsed_time(self) -> float: """Calculate elapsed time in seconds""" return time.time() - self.start_time @property def eta(self) -> Optional[float]: """Estimate time to completion in seconds""" if self.current == 0 or self.percentage >= 100: return None elapsed = self.elapsed_time rate = self.current / elapsed remaining = self.total - self.current return remaining / rate if rate > 0 else None class ProgressManager: """Manages progress tracking for multiple parallel tasks""" def __init__(self, update_interval: float = 1.0, display_width: int = 50): """ Initialize progress manager Args: update_interval: How often to update display (seconds) display_width: Width of progress bar in characters """ self.tasks: Dict[str, TaskProgress] = {} self.update_interval = update_interval self.display_width = display_width self.lock = threading.Lock() self.display_thread: Optional[threading.Thread] = None self.running = False self.last_display_height = 0 def start_task(self, task_id: str, name: str, total: int) -> None: """ Start tracking a new task Args: task_id: Unique identifier for the task name: Human-readable name for the task total: Total number of steps in the task """ with self.lock: self.tasks[task_id] = TaskProgress( task_id=task_id, name=name, current=0, total=total, start_time=time.time(), last_update=time.time() ) def update_progress(self, task_id: str, current: int) -> None: """ Update progress for a specific task Args: task_id: Task identifier current: Current progress value """ with self.lock: if task_id in self.tasks: self.tasks[task_id].current = current self.tasks[task_id].last_update = time.time() def complete_task(self, task_id: str) -> None: """ Mark a task as completed Args: task_id: Task identifier """ with self.lock: if task_id in self.tasks: task = self.tasks[task_id] task.current = task.total task.last_update = time.time() def start_display(self) -> None: """Start the progress display thread""" if not self.running: self.running = True self.display_thread = threading.Thread(target=self._display_loop, daemon=True) self.display_thread.start() def stop_display(self) -> None: """Stop the progress display thread""" self.running = False if self.display_thread: self.display_thread.join(timeout=1.0) self._clear_display() def _display_loop(self) -> None: """Main loop for updating the progress display""" while self.running: self._update_display() time.sleep(self.update_interval) def _update_display(self) -> None: """Update the console display with current progress""" with self.lock: if not self.tasks: return # Clear previous display self._clear_display() # Build display lines lines = [] for task in sorted(self.tasks.values(), key=lambda t: t.task_id): line = self._format_progress_line(task) lines.append(line) # Print all lines for line in lines: print(line, flush=True) self.last_display_height = len(lines) def _clear_display(self) -> None: """Clear the previous progress display""" if self.last_display_height > 0: # Move cursor up and clear lines for _ in range(self.last_display_height): sys.stdout.write('\033[F') # Move cursor up one line sys.stdout.write('\033[K') # Clear line sys.stdout.flush() def _format_progress_line(self, task: TaskProgress) -> str: """ Format a single progress line for display Args: task: TaskProgress instance Returns: Formatted progress string """ # Progress bar filled_width = int(task.percentage / 100 * self.display_width) bar = '█' * filled_width + '░' * (self.display_width - filled_width) # Time information elapsed_str = self._format_time(task.elapsed_time) eta_str = self._format_time(task.eta) if task.eta else "N/A" # Format line line = (f"{task.name:<25} │{bar}│ " f"{task.percentage:5.1f}% " f"({task.current:,}/{task.total:,}) " f"⏱ {elapsed_str} ETA: {eta_str}") return line def _format_time(self, seconds: float) -> str: """ Format time duration for display Args: seconds: Time in seconds Returns: Formatted time string """ if seconds < 60: return f"{seconds:.0f}s" elif seconds < 3600: minutes = seconds / 60 return f"{minutes:.1f}m" else: hours = seconds / 3600 return f"{hours:.1f}h" def get_task_progress_callback(self, task_id: str) -> Callable[[int], None]: """ Get a progress callback function for a specific task Args: task_id: Task identifier Returns: Callback function that updates progress for this task """ def callback(current: int) -> None: self.update_progress(task_id, current) return callback def all_tasks_completed(self) -> bool: """Check if all tasks are completed""" with self.lock: return all(task.current >= task.total for task in self.tasks.values()) def get_summary(self) -> str: """Get a summary of all tasks""" with self.lock: total_tasks = len(self.tasks) completed_tasks = sum(1 for task in self.tasks.values() if task.current >= task.total) return f"Tasks: {completed_tasks}/{total_tasks} completed"