Strategy DSL v2 (dsl.py): 40+ action primitives, 40+ market sensors, 12 comparison operators, 16 builtins, full parser. Strategy Generator (generator.py): genetic programming evolution — crossover, mutation, tournament selection, pool management. Supporting: discrepancy tracking, execution quality, hooks, feature importance, observability, parallel eval, auto-rollback, stress testing, structured observations, trajectory recording.
135 lines
4.0 KiB
Python
135 lines
4.0 KiB
Python
"""
|
|
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,
|
|
)
|