""" Training Parallelism — parallel evaluation across scenarios. CMA-ES evaluates sequentially. This module parallelizes evaluation across scenarios for 4-8x faster convergence. """ from __future__ import annotations import concurrent.futures import time from dataclasses import dataclass from typing import Any, Callable, List, Optional, Sequence from malkhut.state import FulfilmentPolicyParams, MarketWorldState from malkhut.training.cma_trainer import PolicyEvaluator, Scenario class ParallelEvaluator: """ Parallel evaluation of strategies across scenarios. Uses ThreadPoolExecutor for I/O-bound scenarios. Uses ProcessPoolExecutor for CPU-bound scenarios. """ def __init__( self, evaluator: PolicyEvaluator, max_workers: int = 4, ) -> None: self.evaluator = evaluator self._max_workers = max_workers def evaluate_candidate( self, params: FulfilmentPolicyParams, scenarios: Sequence[Scenario], rng_seed: int = 0, ) -> tuple[float, list]: """Evaluate candidate with parallel scenario execution.""" if len(scenarios) <= 1 or self._max_workers <= 1: return self.evaluator.evaluate_candidate(params, scenarios, rng_seed) # Split scenarios across workers chunk_size = max(1, len(scenarios) // self._max_workers) chunks = [] for i in range(0, len(scenarios), chunk_size): chunks.append(scenarios[i:i + chunk_size]) # Parallel evaluation all_results = [] with concurrent.futures.ThreadPoolExecutor(max_workers=self._max_workers) as executor: futures = [] for i, chunk in enumerate(chunks): future = executor.submit( self.evaluator.evaluate_candidate, params, chunk, rng_seed + i, ) futures.append(future) for future in concurrent.futures.as_completed(futures): score, results = future.result() all_results.extend(results) # Aggregate scores if all_results: scores = [r.pnl_bps for r in all_results] avg_score = sum(scores) / len(scores) else: avg_score = 0.0 return avg_score, all_results @dataclass(frozen=True, slots=True) class TrainingMetrics: """Metrics for a training run.""" total_time_s: float generations: int total_evals: int best_score: float avg_score: float score_improvement: float convergence_gen: int class TrainingMonitor: """ Monitor training progress and convergence. Tracks metrics, detects convergence, logs progress. """ def __init__(self) -> None: self._scores: list[float] = [] self._times: list[float] = [] self._start_time = time.time() def record_generation(self, score: float) -> None: self._scores.append(score) self._times.append(time.time()) @property def best_score(self) -> float: return max(self._scores) if self._scores else 0.0 @property def avg_score(self) -> float: return sum(self._scores) / len(self._scores) if self._scores else 0.0 @property def improvement(self) -> float: if len(self._scores) < 2: return 0.0 return self._scores[-1] - self._scores[0] @property def converged(self) -> bool: if len(self._scores) < 5: return False recent = self._scores[-5:] variance = sum((s - self.avg_score) ** 2 for s in recent) / len(recent) return variance < 0.01 # low variance = converged def metrics(self) -> TrainingMetrics: return TrainingMetrics( total_time_s=time.time() - self._start_time, generations=len(self._scores), total_evals=0, best_score=self.best_score, avg_score=self.avg_score, score_improvement=self.improvement, convergence_gen=len(self._scores) if self.converged else -1, )