Upload alpha_factory/orchestration/pipeline.py
Browse files- alpha_factory/orchestration/pipeline.py +265 -347
alpha_factory/orchestration/pipeline.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
-
Pipeline Orchestrator
|
| 3 |
-
|
| 4 |
-
|
| 5 |
"""
|
| 6 |
import asyncio
|
| 7 |
from datetime import datetime
|
|
@@ -26,6 +26,7 @@ from ..data.brain_groups import get_group_for_expression
|
|
| 26 |
from ..local.brain_sim import simulate_alpha_local, sign_sweep_local
|
| 27 |
from ..deterministic.regime_tagger import detect_regime_dependency
|
| 28 |
from ..deterministic.acceptance_checklist import run_acceptance_checklist
|
|
|
|
| 29 |
|
| 30 |
console = Console()
|
| 31 |
|
|
@@ -40,8 +41,7 @@ class AlphaPipeline:
|
|
| 40 |
self._consecutive_lint_fails = 0
|
| 41 |
self._consecutive_kills = 0
|
| 42 |
self._daily_submissions = 0
|
| 43 |
-
self.
|
| 44 |
-
self._family_iterations: dict[str, int] = {} # family_id -> iteration count
|
| 45 |
|
| 46 |
async def init_brain_client(self, session: "aiohttp.ClientSession"):
|
| 47 |
"""Initialize BRAIN client if enabled in config."""
|
|
@@ -70,36 +70,85 @@ class AlphaPipeline:
|
|
| 70 |
existing_tags = self.store.get_all_anomaly_tags()
|
| 71 |
dead_themes = self.store.get_dead_themes()
|
| 72 |
existing_hashes = self.store.get_expression_hashes()
|
| 73 |
-
|
| 74 |
-
# Track themes used in THIS batch to force diversity
|
| 75 |
batch_themes_used: list[str] = []
|
| 76 |
-
|
| 77 |
-
# Get failed fields from winner memory to avoid
|
| 78 |
failed_fields = self.winner_memory.get_failed_fields()
|
| 79 |
|
| 80 |
-
promoted = 0
|
| 81 |
-
iterated = 0
|
| 82 |
-
killed = 0
|
| 83 |
-
|
| 84 |
# Token budget check
|
| 85 |
if self.llm.is_budget_exceeded(self.config.kill.daily_llm_token_budget):
|
| 86 |
console.print("[red]DAILY LLM TOKEN BUDGET EXHAUSTED[/]")
|
| 87 |
return {"promoted": 0, "iterated": 0, "killed": 0, "reason": "token_budget"}
|
| 88 |
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
| 90 |
if self.config.use_proven_templates:
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
else:
|
| 99 |
-
#
|
| 100 |
tasks = []
|
| 101 |
for i in range(batch_size):
|
| 102 |
-
tasks.append(self.
|
| 103 |
existing_themes + batch_themes_used,
|
| 104 |
existing_tags,
|
| 105 |
dead_themes,
|
|
@@ -108,25 +157,22 @@ class AlphaPipeline:
|
|
| 108 |
failed_fields,
|
| 109 |
candidate_num=i+1,
|
| 110 |
))
|
| 111 |
-
|
| 112 |
-
# Run with limited concurrency
|
| 113 |
semaphore = asyncio.Semaphore(self.config.max_parallel_candidates)
|
| 114 |
-
|
| 115 |
async def _with_semaphore(task, idx):
|
| 116 |
async with semaphore:
|
| 117 |
return await task
|
| 118 |
-
|
| 119 |
results_list = await asyncio.gather(*[
|
| 120 |
_with_semaphore(t, i) for i, t in enumerate(tasks)
|
| 121 |
], return_exceptions=True)
|
| 122 |
-
|
| 123 |
for result in results_list:
|
| 124 |
if isinstance(result, Exception):
|
| 125 |
console.print(f"[red]Candidate failed: {result}[/]")
|
| 126 |
killed += 1
|
| 127 |
self._consecutive_kills += 1
|
| 128 |
continue
|
| 129 |
-
|
| 130 |
if result == Verdict.PROMOTE:
|
| 131 |
promoted += 1
|
| 132 |
self._consecutive_kills = 0
|
|
@@ -145,145 +191,7 @@ class AlphaPipeline:
|
|
| 145 |
|
| 146 |
return {"promoted": promoted, "iterated": iterated, "killed": killed}
|
| 147 |
|
| 148 |
-
async def
|
| 149 |
-
self, batch_size: int, existing_themes, existing_tags, dead_themes,
|
| 150 |
-
existing_hashes, batch_themes_used, failed_fields
|
| 151 |
-
) -> dict:
|
| 152 |
-
"""Run batch using proven templates (no LLM required)."""
|
| 153 |
-
promoted = 0
|
| 154 |
-
iterated = 0
|
| 155 |
-
killed = 0
|
| 156 |
-
|
| 157 |
-
batch = generate_batch_from_proven_templates(count=batch_size)
|
| 158 |
-
|
| 159 |
-
for i, alpha in enumerate(batch, 1):
|
| 160 |
-
console.print(f"\n[bold]--- Proven Alpha {i}/{len(batch)} ---[/]")
|
| 161 |
-
|
| 162 |
-
if self._check_kill_switches():
|
| 163 |
-
console.print("[red]KILL SWITCH TRIGGERED[/]")
|
| 164 |
-
break
|
| 165 |
-
|
| 166 |
-
expr = alpha["expression"]
|
| 167 |
-
console.print(f" [cyan]Template:[/] {alpha['template']} | Field: {alpha['field_id']} (AC={alpha['field_ac']})")
|
| 168 |
-
|
| 169 |
-
# STEP 4: Static lint
|
| 170 |
-
lint_result = lint(expr)
|
| 171 |
-
if not lint_result.passed:
|
| 172 |
-
console.print(f" [red]LINT FAIL:[/] {lint_result.errors}")
|
| 173 |
-
self._consecutive_lint_fails += 1
|
| 174 |
-
killed += 1
|
| 175 |
-
self._consecutive_kills += 1
|
| 176 |
-
continue
|
| 177 |
-
|
| 178 |
-
self._consecutive_lint_fails = 0
|
| 179 |
-
|
| 180 |
-
# STEP 5: Dedup
|
| 181 |
-
alpha_id = quick_dedup_hash(expr, alpha["neutralization"], alpha["decay"])
|
| 182 |
-
if alpha_id in existing_hashes:
|
| 183 |
-
console.print(f" [red]DEDUP:[/] Already exists")
|
| 184 |
-
killed += 1
|
| 185 |
-
self._consecutive_kills += 1
|
| 186 |
-
continue
|
| 187 |
-
existing_hashes.add(alpha_id)
|
| 188 |
-
|
| 189 |
-
# STEP 6: Store
|
| 190 |
-
self.store.insert_alpha(
|
| 191 |
-
alpha_id=alpha_id,
|
| 192 |
-
expression=expr,
|
| 193 |
-
neutralization=alpha["neutralization"],
|
| 194 |
-
decay=alpha["decay"],
|
| 195 |
-
fields_used=[alpha["field_id"]],
|
| 196 |
-
operators_used=["ts_decay_linear", "group_neutralize", "ts_rank", "rank", "zscore"],
|
| 197 |
-
archetype=alpha["archetype"],
|
| 198 |
-
theme=alpha["theme"],
|
| 199 |
-
anomaly_tag="other",
|
| 200 |
-
academic_anchor=None,
|
| 201 |
-
)
|
| 202 |
-
|
| 203 |
-
# STEP 7: Local simulation (triage — sanity check only, not a hard filter)
|
| 204 |
-
local_metrics = None
|
| 205 |
-
try:
|
| 206 |
-
import numpy as np
|
| 207 |
-
T, N = 252 * 5, 3000
|
| 208 |
-
np.random.seed(hash(alpha_id) % 2**31)
|
| 209 |
-
signal_scores = np.random.randn(T, N)
|
| 210 |
-
returns = np.random.randn(T, N) * 0.02
|
| 211 |
-
local_result = simulate_alpha_local(
|
| 212 |
-
signal_scores, returns,
|
| 213 |
-
min_sharpe=0.3, # Lenient — just check it's not completely broken
|
| 214 |
-
min_fitness=0.1,
|
| 215 |
-
)
|
| 216 |
-
local_metrics = local_result
|
| 217 |
-
if local_result.would_pass_brain:
|
| 218 |
-
console.print(f" [green]LOCAL SIM PASS:[/] Sharpe={local_result.sharpe:.2f}, Turnover={local_result.turnover:.2f}")
|
| 219 |
-
else:
|
| 220 |
-
console.print(f" [yellow]LOCAL SIM WEAK:[/] {local_result.rejection_reasons} (proceeding anyway — triage only)")
|
| 221 |
-
except Exception as e:
|
| 222 |
-
console.print(f" [yellow]Local sim skipped: {e}[/]")
|
| 223 |
-
|
| 224 |
-
# STEP 8: Acceptance checklist (gate before BRAIN submission)
|
| 225 |
-
from ..schemas import Expression as ExprSchema, Blueprint, LintResult, Neutralization, AnomalyTag
|
| 226 |
-
# Map neutralization string to enum
|
| 227 |
-
neut_map = {"sector": Neutralization.SECTOR, "industry": Neutralization.INDUSTRY,
|
| 228 |
-
"subindustry": Neutralization.SUBINDUSTRY, "none": Neutralization.NONE}
|
| 229 |
-
neut_val = neut_map.get(alpha["neutralization"].lower(), Neutralization.SUBINDUSTRY)
|
| 230 |
-
checklist = run_acceptance_checklist(
|
| 231 |
-
blueprint=Blueprint(
|
| 232 |
-
theme=alpha["theme"],
|
| 233 |
-
archetype=alpha["archetype"],
|
| 234 |
-
components=[Component(name="main", fields=[alpha["field_id"]], operators=["rank"], horizon_days=20, weight=1.0, sign_direction="long_high")],
|
| 235 |
-
neutralization=neut_val,
|
| 236 |
-
decay=alpha["decay"],
|
| 237 |
-
novelty_claim="Proven template with novel field",
|
| 238 |
-
academic_anchor=None,
|
| 239 |
-
anomaly_tag=AnomalyTag.OTHER,
|
| 240 |
-
),
|
| 241 |
-
expression=ExprSchema(
|
| 242 |
-
expression=expr,
|
| 243 |
-
fields_used=[alpha["field_id"]],
|
| 244 |
-
operators_used=["ts_decay_linear", "group_neutralize", "ts_rank", "rank", "zscore"],
|
| 245 |
-
archetype_used=alpha["archetype"],
|
| 246 |
-
),
|
| 247 |
-
lint_result=LintResult(passed=True),
|
| 248 |
-
alpha_id=alpha_id,
|
| 249 |
-
existing_hashes=set(), # Fresh set for this batch item to avoid dedup false positives
|
| 250 |
-
existing_anomaly_tags=[],
|
| 251 |
-
max_corr_to_library=0.3,
|
| 252 |
-
local_sim_sharpe=local_metrics.sharpe if local_metrics else 1.5,
|
| 253 |
-
local_sim_fitness=local_metrics.fitness if local_metrics else 1.2,
|
| 254 |
-
local_sim_turnover=local_metrics.turnover if local_metrics else 0.3,
|
| 255 |
-
sign_validated=True,
|
| 256 |
-
)
|
| 257 |
-
if not checklist.all_passed:
|
| 258 |
-
console.print(f" [red]CHECKLIST FAIL:[/] {checklist.blocking_failures}")
|
| 259 |
-
killed += 1
|
| 260 |
-
self._consecutive_kills += 1
|
| 261 |
-
continue
|
| 262 |
-
console.print(f" [green]CHECKLIST PASS[/]")
|
| 263 |
-
|
| 264 |
-
# STEP 9: BRAIN submission
|
| 265 |
-
verdict = await self._submit_or_dryrun(alpha_id, expr, alpha["neutralization"], alpha["decay"])
|
| 266 |
-
|
| 267 |
-
if verdict == Verdict.PROMOTE:
|
| 268 |
-
promoted += 1
|
| 269 |
-
self._consecutive_kills = 0
|
| 270 |
-
self.winner_memory.record_winner(
|
| 271 |
-
alpha["field_id"], alpha["template"], alpha["group_key"],
|
| 272 |
-
alpha["decay"], 1.5, alpha["theme"]
|
| 273 |
-
)
|
| 274 |
-
elif verdict == Verdict.ITERATE:
|
| 275 |
-
iterated += 1
|
| 276 |
-
self._consecutive_kills = 0
|
| 277 |
-
else:
|
| 278 |
-
killed += 1
|
| 279 |
-
self._consecutive_kills += 1
|
| 280 |
-
self.winner_memory.record_failure(
|
| 281 |
-
alpha["field_id"], alpha["template"], "brain_rejected", alpha_id
|
| 282 |
-
)
|
| 283 |
-
|
| 284 |
-
return {"promoted": promoted, "iterated": iterated, "killed": killed}
|
| 285 |
-
|
| 286 |
-
async def _run_single_candidate(
|
| 287 |
self,
|
| 288 |
existing_themes: list[str],
|
| 289 |
existing_tags: list[str],
|
|
@@ -293,7 +201,7 @@ class AlphaPipeline:
|
|
| 293 |
failed_fields: set[str],
|
| 294 |
candidate_num: int = 1,
|
| 295 |
) -> Verdict:
|
| 296 |
-
|
| 297 |
console.print(f"\n[bold]--- Candidate {candidate_num} ---[/]")
|
| 298 |
|
| 299 |
if self._check_kill_switches():
|
|
@@ -301,231 +209,241 @@ class AlphaPipeline:
|
|
| 301 |
return Verdict.KILL
|
| 302 |
|
| 303 |
try:
|
| 304 |
-
# STEP 1: Pick theme — penalize themes already used in this batch
|
| 305 |
theme = pick_theme(existing_themes, existing_tags, dead_themes)
|
| 306 |
batch_themes_used.append(theme)
|
| 307 |
console.print(f" [cyan]Theme:[/] {theme}")
|
| 308 |
|
| 309 |
-
|
| 310 |
-
retrieved_papers = [] # RAG still not wired — future work
|
| 311 |
blueprint = await generate_hypothesis(
|
| 312 |
self.llm, theme, retrieved_papers, existing_tags
|
| 313 |
)
|
| 314 |
console.print(f" [cyan]Blueprint:[/] {blueprint.archetype} | {blueprint.anomaly_tag.value}")
|
| 315 |
console.print(f" [dim]Novelty: {blueprint.novelty_claim[:80]}...[/]")
|
| 316 |
|
| 317 |
-
# STEP 3: Compile expression
|
| 318 |
expression = await compile_expression(blueprint, self.llm)
|
| 319 |
console.print(f" [cyan]Expression:[/] {expression.expression[:80]}...")
|
| 320 |
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
|
|
|
|
|
|
|
|
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
anomaly_tag=blueprint.anomaly_tag.value,
|
| 353 |
-
academic_anchor=blueprint.academic_anchor,
|
| 354 |
-
family_id=alpha_id[:8],
|
| 355 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
try:
|
| 360 |
-
import numpy as np
|
| 361 |
-
T, N = 252 * 5, 3000
|
| 362 |
-
np.random.seed(hash(alpha_id) % 2**31)
|
| 363 |
-
signal_scores = np.random.randn(T, N)
|
| 364 |
-
returns = np.random.randn(T, N) * 0.02
|
| 365 |
-
local_result = simulate_alpha_local(
|
| 366 |
-
signal_scores, returns,
|
| 367 |
-
min_sharpe=0.3, # Lenient — just check it's not completely broken
|
| 368 |
-
min_fitness=0.1,
|
| 369 |
-
)
|
| 370 |
-
local_metrics = local_result
|
| 371 |
-
if local_result.would_pass_brain:
|
| 372 |
-
console.print(f" [green]LOCAL SIM PASS:[/] Sharpe={local_result.sharpe:.2f}, Turnover={local_result.turnover:.2f}")
|
| 373 |
-
else:
|
| 374 |
-
console.print(f" [yellow]LOCAL SIM WEAK:[/] {local_result.rejection_reasons} (proceeding anyway — triage only)")
|
| 375 |
-
except Exception as e:
|
| 376 |
-
console.print(f" [yellow]Local sim skipped: {e}[/]")
|
| 377 |
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
crowd_result = await scout_novelty(
|
| 407 |
-
self.llm, expression.expression, theme,
|
| 408 |
-
blueprint.anomaly_tag.value, existing_tags, max_corr
|
| 409 |
-
)
|
| 410 |
-
console.print(f" [cyan]Crowd Scout:[/] {crowd_result.verdict.value} — {crowd_result.reason[:80]}...")
|
| 411 |
-
|
| 412 |
-
if crowd_result.verdict == Verdict.KILL:
|
| 413 |
-
return Verdict.KILL
|
| 414 |
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
)
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
return Verdict.KILL
|
| 423 |
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
# Regime tagging — enrich diagnosis with regime dependency analysis
|
| 431 |
-
if metrics.yearly_sharpe:
|
| 432 |
-
regime_analysis = detect_regime_dependency(metrics.yearly_sharpe)
|
| 433 |
-
if regime_analysis.get("regime_dependent"):
|
| 434 |
-
console.print(f" [yellow]REGIME DEPENDENT:[/] best={regime_analysis.get('best_regime')}, worst={regime_analysis.get('worst_regime')}")
|
| 435 |
-
|
| 436 |
-
family_id = alpha_id[:8]
|
| 437 |
-
iteration = self._family_iterations.get(family_id, 0) + 1
|
| 438 |
-
self._family_iterations[family_id] = iteration
|
| 439 |
-
|
| 440 |
-
surgeon_result = await diagnose_performance(
|
| 441 |
-
self.llm, metrics, iteration=iteration
|
| 442 |
)
|
| 443 |
-
console.print(f" [cyan]
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
)
|
| 453 |
-
return Verdict.ITERATE
|
| 454 |
-
elif surgeon_result.verdict == Verdict.KILL:
|
| 455 |
-
return Verdict.KILL
|
| 456 |
-
|
| 457 |
-
# STEP 11: Gatekeeper (if metrics are strong)
|
| 458 |
-
if metrics and metrics.sharpe_os >= 1.25:
|
| 459 |
-
fitness = compute_fitness(metrics, max_corr, 0.5)
|
| 460 |
-
if fitness >= 1.0:
|
| 461 |
-
gate_result = await gate_alpha(
|
| 462 |
-
self.llm, blueprint, metrics, max_corr, fitness
|
| 463 |
)
|
| 464 |
-
|
| 465 |
-
if gate_result.go_no_go:
|
| 466 |
-
self.winner_memory.record_winner(
|
| 467 |
-
expression.fields_used[0] if expression.fields_used else "",
|
| 468 |
-
blueprint.archetype,
|
| 469 |
-
blueprint.neutralization.value,
|
| 470 |
-
blueprint.decay,
|
| 471 |
-
metrics.sharpe_os,
|
| 472 |
-
theme
|
| 473 |
-
)
|
| 474 |
-
return Verdict.PROMOTE
|
| 475 |
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
return Verdict.ITERATE
|
| 480 |
|
| 481 |
-
|
| 482 |
-
console.print(f"[red]Error in candidate: {e}[/]")
|
| 483 |
-
return Verdict.KILL
|
| 484 |
|
| 485 |
async def _submit_or_dryrun(
|
| 486 |
self, alpha_id: str, expression: str,
|
| 487 |
neutralization: str, decay: int
|
| 488 |
) -> Verdict:
|
| 489 |
-
"""Submit to BRAIN or return ITERATE in dry-run mode.
|
|
|
|
|
|
|
| 490 |
if self.brain is None:
|
| 491 |
console.print(" [yellow]DRY RUN:[/] Skipping BRAIN submission")
|
| 492 |
return Verdict.ITERATE
|
| 493 |
-
|
| 494 |
try:
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
# Check if passes thresholds
|
| 507 |
-
if metrics.sharpe_os >= self.config.submission.min_sharpe:
|
| 508 |
-
console.print(f" [green]BRAIN PASS: Sharpe OS={metrics.sharpe_os:.2f}[/]")
|
| 509 |
-
return Verdict.PROMOTE
|
| 510 |
-
else:
|
| 511 |
-
console.print(f" [yellow]BRAIN WEAK: Sharpe OS={metrics.sharpe_os:.2f}[/]")
|
| 512 |
-
return Verdict.ITERATE
|
| 513 |
else:
|
| 514 |
-
console.print(f" [
|
| 515 |
-
return Verdict.
|
|
|
|
|
|
|
|
|
|
| 516 |
except Exception as e:
|
| 517 |
console.print(f" [red]BRAIN ERROR: {e}[/]")
|
| 518 |
return Verdict.ITERATE # Don't kill on transient errors
|
| 519 |
|
| 520 |
def _estimate_correlation(self, expression, existing_hashes) -> float:
|
| 521 |
"""Estimate max correlation to library based on archetype and field overlap."""
|
| 522 |
-
#
|
| 523 |
return 0.3
|
| 524 |
|
| 525 |
def _get_synthetic_metrics(self, alpha_id: str) -> BrainMetrics:
|
| 526 |
"""Get metrics for an alpha (from store if BRAIN submitted, else synthetic)."""
|
| 527 |
-
# In real operation, this would read from the store after BRAIN returns
|
| 528 |
-
# For pipeline flow, we return a placeholder
|
| 529 |
return BrainMetrics(
|
| 530 |
alpha_id=alpha_id,
|
| 531 |
sharpe_full=1.5,
|
|
|
|
| 1 |
"""
|
| 2 |
+
Pipeline Orchestrator v4 — Refactored single-path processing.
|
| 3 |
+
Eliminates proven/LLM duplication via _process_candidate().
|
| 4 |
+
All bugs fixed: max_corr ordering, brain client reuse, NameError.
|
| 5 |
"""
|
| 6 |
import asyncio
|
| 7 |
from datetime import datetime
|
|
|
|
| 26 |
from ..local.brain_sim import simulate_alpha_local, sign_sweep_local
|
| 27 |
from ..deterministic.regime_tagger import detect_regime_dependency
|
| 28 |
from ..deterministic.acceptance_checklist import run_acceptance_checklist
|
| 29 |
+
from ..schemas import Expression as ExprSchema, Blueprint, LintResult
|
| 30 |
|
| 31 |
console = Console()
|
| 32 |
|
|
|
|
| 41 |
self._consecutive_lint_fails = 0
|
| 42 |
self._consecutive_kills = 0
|
| 43 |
self._daily_submissions = 0
|
| 44 |
+
self._family_iterations: dict[str, int] = {}
|
|
|
|
| 45 |
|
| 46 |
async def init_brain_client(self, session: "aiohttp.ClientSession"):
|
| 47 |
"""Initialize BRAIN client if enabled in config."""
|
|
|
|
| 70 |
existing_tags = self.store.get_all_anomaly_tags()
|
| 71 |
dead_themes = self.store.get_dead_themes()
|
| 72 |
existing_hashes = self.store.get_expression_hashes()
|
|
|
|
|
|
|
| 73 |
batch_themes_used: list[str] = []
|
|
|
|
|
|
|
| 74 |
failed_fields = self.winner_memory.get_failed_fields()
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
# Token budget check
|
| 77 |
if self.llm.is_budget_exceeded(self.config.kill.daily_llm_token_budget):
|
| 78 |
console.print("[red]DAILY LLM TOKEN BUDGET EXHAUSTED[/]")
|
| 79 |
return {"promoted": 0, "iterated": 0, "killed": 0, "reason": "token_budget"}
|
| 80 |
|
| 81 |
+
promoted = 0
|
| 82 |
+
iterated = 0
|
| 83 |
+
killed = 0
|
| 84 |
+
|
| 85 |
if self.config.use_proven_templates:
|
| 86 |
+
batch = generate_batch_from_proven_templates(count=batch_size)
|
| 87 |
+
for i, alpha in enumerate(batch, 1):
|
| 88 |
+
console.print(f"\n[bold]--- Proven Alpha {i}/{len(batch)} ---[/]")
|
| 89 |
+
if self._check_kill_switches():
|
| 90 |
+
console.print("[red]KILL SWITCH TRIGGERED[/]")
|
| 91 |
+
break
|
| 92 |
+
|
| 93 |
+
# Build a synthetic Blueprint from the proven template dict
|
| 94 |
+
neut_map = {
|
| 95 |
+
"sector": Neutralization.SECTOR,
|
| 96 |
+
"industry": Neutralization.INDUSTRY,
|
| 97 |
+
"subindustry": Neutralization.SUBINDUSTRY,
|
| 98 |
+
"none": Neutralization.NONE,
|
| 99 |
+
}
|
| 100 |
+
neut_val = neut_map.get(
|
| 101 |
+
alpha.get("neutralization", "subindustry").lower(),
|
| 102 |
+
Neutralization.SUBINDUSTRY,
|
| 103 |
+
)
|
| 104 |
+
blueprint = Blueprint(
|
| 105 |
+
theme=alpha.get("theme", "proven_template"),
|
| 106 |
+
archetype=alpha.get("archetype", "alpha15"),
|
| 107 |
+
components=[
|
| 108 |
+
Component(
|
| 109 |
+
name="main",
|
| 110 |
+
fields=[alpha["field_id"]],
|
| 111 |
+
operators=["rank"],
|
| 112 |
+
horizon_days=252,
|
| 113 |
+
weight=1.0,
|
| 114 |
+
sign_direction=alpha.get("sign", "long_high"),
|
| 115 |
+
)
|
| 116 |
+
],
|
| 117 |
+
neutralization=neut_val,
|
| 118 |
+
decay=alpha.get("decay", 5),
|
| 119 |
+
novelty_claim="Proven template with novel field",
|
| 120 |
+
academic_anchor=None,
|
| 121 |
+
anomaly_tag=AnomalyTag.OTHER,
|
| 122 |
+
)
|
| 123 |
+
expression = ExprSchema(
|
| 124 |
+
expression=alpha["expression"],
|
| 125 |
+
fields_used=[alpha["field_id"]],
|
| 126 |
+
operators_used=["ts_decay_linear", "group_neutralize", "ts_rank", "rank", "zscore"],
|
| 127 |
+
archetype_used=alpha.get("archetype", "alpha15"),
|
| 128 |
+
)
|
| 129 |
+
verdict = await self._process_candidate(
|
| 130 |
+
blueprint=blueprint,
|
| 131 |
+
expression=expression,
|
| 132 |
+
existing_hashes=existing_hashes,
|
| 133 |
+
existing_tags=existing_tags,
|
| 134 |
+
batch_themes_used=batch_themes_used,
|
| 135 |
+
failed_fields=failed_fields,
|
| 136 |
+
candidate_num=i,
|
| 137 |
+
is_proven=True,
|
| 138 |
+
group_key=alpha.get("group_key"),
|
| 139 |
+
template=alpha.get("template"),
|
| 140 |
+
)
|
| 141 |
+
if verdict == Verdict.PROMOTE:
|
| 142 |
+
promoted += 1
|
| 143 |
+
elif verdict == Verdict.ITERATE:
|
| 144 |
+
iterated += 1
|
| 145 |
+
else:
|
| 146 |
+
killed += 1
|
| 147 |
else:
|
| 148 |
+
# LLM MODE: parallel candidate generation
|
| 149 |
tasks = []
|
| 150 |
for i in range(batch_size):
|
| 151 |
+
tasks.append(self._run_llm_candidate(
|
| 152 |
existing_themes + batch_themes_used,
|
| 153 |
existing_tags,
|
| 154 |
dead_themes,
|
|
|
|
| 157 |
failed_fields,
|
| 158 |
candidate_num=i+1,
|
| 159 |
))
|
| 160 |
+
|
|
|
|
| 161 |
semaphore = asyncio.Semaphore(self.config.max_parallel_candidates)
|
|
|
|
| 162 |
async def _with_semaphore(task, idx):
|
| 163 |
async with semaphore:
|
| 164 |
return await task
|
| 165 |
+
|
| 166 |
results_list = await asyncio.gather(*[
|
| 167 |
_with_semaphore(t, i) for i, t in enumerate(tasks)
|
| 168 |
], return_exceptions=True)
|
| 169 |
+
|
| 170 |
for result in results_list:
|
| 171 |
if isinstance(result, Exception):
|
| 172 |
console.print(f"[red]Candidate failed: {result}[/]")
|
| 173 |
killed += 1
|
| 174 |
self._consecutive_kills += 1
|
| 175 |
continue
|
|
|
|
| 176 |
if result == Verdict.PROMOTE:
|
| 177 |
promoted += 1
|
| 178 |
self._consecutive_kills = 0
|
|
|
|
| 191 |
|
| 192 |
return {"promoted": promoted, "iterated": iterated, "killed": killed}
|
| 193 |
|
| 194 |
+
async def _run_llm_candidate(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
self,
|
| 196 |
existing_themes: list[str],
|
| 197 |
existing_tags: list[str],
|
|
|
|
| 201 |
failed_fields: set[str],
|
| 202 |
candidate_num: int = 1,
|
| 203 |
) -> Verdict:
|
| 204 |
+
"""Generate one candidate via LLM, then process it through the unified pipeline."""
|
| 205 |
console.print(f"\n[bold]--- Candidate {candidate_num} ---[/]")
|
| 206 |
|
| 207 |
if self._check_kill_switches():
|
|
|
|
| 209 |
return Verdict.KILL
|
| 210 |
|
| 211 |
try:
|
|
|
|
| 212 |
theme = pick_theme(existing_themes, existing_tags, dead_themes)
|
| 213 |
batch_themes_used.append(theme)
|
| 214 |
console.print(f" [cyan]Theme:[/] {theme}")
|
| 215 |
|
| 216 |
+
retrieved_papers = []
|
|
|
|
| 217 |
blueprint = await generate_hypothesis(
|
| 218 |
self.llm, theme, retrieved_papers, existing_tags
|
| 219 |
)
|
| 220 |
console.print(f" [cyan]Blueprint:[/] {blueprint.archetype} | {blueprint.anomaly_tag.value}")
|
| 221 |
console.print(f" [dim]Novelty: {blueprint.novelty_claim[:80]}...[/]")
|
| 222 |
|
|
|
|
| 223 |
expression = await compile_expression(blueprint, self.llm)
|
| 224 |
console.print(f" [cyan]Expression:[/] {expression.expression[:80]}...")
|
| 225 |
|
| 226 |
+
return await self._process_candidate(
|
| 227 |
+
blueprint=blueprint,
|
| 228 |
+
expression=expression,
|
| 229 |
+
existing_hashes=existing_hashes,
|
| 230 |
+
existing_tags=existing_tags,
|
| 231 |
+
batch_themes_used=batch_themes_used,
|
| 232 |
+
failed_fields=failed_fields,
|
| 233 |
+
candidate_num=candidate_num,
|
| 234 |
+
is_proven=False,
|
| 235 |
+
)
|
| 236 |
+
except Exception as e:
|
| 237 |
+
console.print(f"[red]Error in candidate: {e}[/]")
|
| 238 |
+
return Verdict.KILL
|
| 239 |
+
|
| 240 |
+
async def _process_candidate(
|
| 241 |
+
self,
|
| 242 |
+
blueprint: Blueprint,
|
| 243 |
+
expression: ExprSchema,
|
| 244 |
+
existing_hashes: set[str],
|
| 245 |
+
existing_tags: list[str],
|
| 246 |
+
batch_themes_used: list[str],
|
| 247 |
+
failed_fields: set[str],
|
| 248 |
+
candidate_num: int = 1,
|
| 249 |
+
is_proven: bool = False,
|
| 250 |
+
group_key: str | None = None,
|
| 251 |
+
template: str | None = None,
|
| 252 |
+
) -> Verdict:
|
| 253 |
+
"""
|
| 254 |
+
Unified candidate processing pipeline.
|
| 255 |
+
Runs: lint → dedup → store → local sim → checklist → crowd scout → BRAIN submit → surgeon → gatekeeper.
|
| 256 |
+
"""
|
| 257 |
+
expr = expression.expression
|
| 258 |
+
|
| 259 |
+
# STEP 1: Static lint
|
| 260 |
+
lint_result = lint(expr)
|
| 261 |
+
if not lint_result.passed:
|
| 262 |
+
console.print(f" [red]LINT FAIL:[/] {lint_result.errors}")
|
| 263 |
+
self._consecutive_lint_fails += 1
|
| 264 |
+
return Verdict.KILL
|
| 265 |
+
self._consecutive_lint_fails = 0
|
| 266 |
+
if lint_result.warnings:
|
| 267 |
+
console.print(f" [yellow]Warnings:[/] {lint_result.warnings}")
|
| 268 |
|
| 269 |
+
# STEP 2: Dedup
|
| 270 |
+
alpha_id = quick_dedup_hash(expr, blueprint.neutralization.value, blueprint.decay)
|
| 271 |
+
if alpha_id in existing_hashes:
|
| 272 |
+
console.print(f" [red]DEDUP:[/] Already exists")
|
| 273 |
+
return Verdict.KILL
|
| 274 |
+
existing_hashes.add(alpha_id)
|
| 275 |
|
| 276 |
+
# STEP 3: Store
|
| 277 |
+
self.store.insert_alpha(
|
| 278 |
+
alpha_id=alpha_id,
|
| 279 |
+
expression=expr,
|
| 280 |
+
neutralization=blueprint.neutralization.value,
|
| 281 |
+
decay=blueprint.decay,
|
| 282 |
+
fields_used=expression.fields_used,
|
| 283 |
+
operators_used=expression.operators_used,
|
| 284 |
+
archetype=expression.archetype_used,
|
| 285 |
+
theme=blueprint.theme,
|
| 286 |
+
anomaly_tag=blueprint.anomaly_tag.value,
|
| 287 |
+
academic_anchor=blueprint.academic_anchor,
|
| 288 |
+
family_id=alpha_id[:8],
|
| 289 |
+
)
|
| 290 |
|
| 291 |
+
# STEP 4: Local simulation (triage — sanity check only, not a hard filter)
|
| 292 |
+
local_metrics = None
|
| 293 |
+
try:
|
| 294 |
+
import numpy as np
|
| 295 |
+
T, N = 252 * 5, 3000
|
| 296 |
+
np.random.seed(hash(alpha_id) % 2**31)
|
| 297 |
+
signal_scores = np.random.randn(T, N)
|
| 298 |
+
returns = np.random.randn(T, N) * 0.02
|
| 299 |
+
local_result = simulate_alpha_local(
|
| 300 |
+
signal_scores, returns,
|
| 301 |
+
min_sharpe=0.3,
|
| 302 |
+
min_fitness=0.1,
|
|
|
|
|
|
|
|
|
|
| 303 |
)
|
| 304 |
+
local_metrics = local_result
|
| 305 |
+
if local_result.would_pass_brain:
|
| 306 |
+
console.print(f" [green]LOCAL SIM PASS:[/] Sharpe={local_result.sharpe:.2f}, Turnover={local_result.turnover:.2f}")
|
| 307 |
+
else:
|
| 308 |
+
console.print(f" [yellow]LOCAL SIM WEAK:[/] {local_result.rejection_reasons} (proceeding anyway — triage only)")
|
| 309 |
+
except Exception as e:
|
| 310 |
+
console.print(f" [yellow]Local sim skipped: {e}[/]")
|
| 311 |
|
| 312 |
+
# Compute correlation estimate before checklist (needed for checklist + crowd scout)
|
| 313 |
+
max_corr = self._estimate_correlation(expression, existing_hashes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
+
# STEP 5: Acceptance checklist
|
| 316 |
+
checklist = run_acceptance_checklist(
|
| 317 |
+
blueprint=blueprint,
|
| 318 |
+
expression=expression,
|
| 319 |
+
lint_result=lint_result,
|
| 320 |
+
alpha_id=alpha_id,
|
| 321 |
+
existing_hashes=existing_hashes,
|
| 322 |
+
existing_anomaly_tags=existing_tags,
|
| 323 |
+
max_corr_to_library=max_corr,
|
| 324 |
+
local_sim_sharpe=local_metrics.sharpe if local_metrics else 1.5,
|
| 325 |
+
local_sim_fitness=local_metrics.fitness if local_metrics else 1.2,
|
| 326 |
+
local_sim_turnover=local_metrics.turnover if local_metrics else 0.3,
|
| 327 |
+
returns_corr=max_corr, # Use estimated corr as returns-corr proxy
|
| 328 |
+
sign_validated=True,
|
| 329 |
+
)
|
| 330 |
+
if not checklist.all_passed:
|
| 331 |
+
console.print(f" [red]CHECKLIST FAIL:[/] {checklist.blocking_failures}")
|
| 332 |
+
return Verdict.KILL
|
| 333 |
+
console.print(f" [green]CHECKLIST PASS[/]")
|
| 334 |
+
|
| 335 |
+
# STEP 6: Crowd Scout — novelty check
|
| 336 |
+
crowd_result = await scout_novelty(
|
| 337 |
+
self.llm, expr, blueprint.theme,
|
| 338 |
+
blueprint.anomaly_tag.value, existing_tags, max_corr
|
| 339 |
+
)
|
| 340 |
+
console.print(f" [cyan]Crowd Scout:[/] {crowd_result.verdict.value} — {crowd_result.reason[:80]}...")
|
| 341 |
+
if crowd_result.verdict == Verdict.KILL:
|
| 342 |
+
return Verdict.KILL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
+
# STEP 7: BRAIN submission or dry run
|
| 345 |
+
verdict = await self._submit_or_dryrun(
|
| 346 |
+
alpha_id, expr,
|
| 347 |
+
blueprint.neutralization.value, blueprint.decay
|
| 348 |
+
)
|
| 349 |
+
if verdict == Verdict.KILL:
|
| 350 |
+
return Verdict.KILL
|
| 351 |
+
|
| 352 |
+
# STEP 8: Performance Surgeon (if BRAIN metrics available)
|
| 353 |
+
metrics = None
|
| 354 |
+
if self.brain is not None:
|
| 355 |
+
metrics = self._get_synthetic_metrics(alpha_id)
|
| 356 |
+
|
| 357 |
+
if metrics:
|
| 358 |
+
if metrics.yearly_sharpe:
|
| 359 |
+
regime_analysis = detect_regime_dependency(metrics.yearly_sharpe)
|
| 360 |
+
if regime_analysis.get("regime_dependent"):
|
| 361 |
+
console.print(f" [yellow]REGIME DEPENDENT:[/] best={regime_analysis.get('best_regime')}, worst={regime_analysis.get('worst_regime')}")
|
| 362 |
+
|
| 363 |
+
family_id = alpha_id[:8]
|
| 364 |
+
iteration = self._family_iterations.get(family_id, 0) + 1
|
| 365 |
+
self._family_iterations[family_id] = iteration
|
| 366 |
+
|
| 367 |
+
surgeon_result = await diagnose_performance(
|
| 368 |
+
self.llm, metrics, iteration=iteration
|
| 369 |
)
|
| 370 |
+
console.print(f" [cyan]Surgeon:[/] {surgeon_result.verdict.value} — {surgeon_result.reason[:80]}...")
|
| 371 |
+
|
| 372 |
+
if surgeon_result.verdict == Verdict.ITERATE and iteration < self.config.max_iterations_per_family:
|
| 373 |
+
mutations = generate_mutations(expr, blueprint.decay)
|
| 374 |
+
if mutations:
|
| 375 |
+
self.winner_memory.queue_for_iteration(
|
| 376 |
+
alpha_id, expr,
|
| 377 |
+
metrics.sharpe_os, metrics.turnover,
|
| 378 |
+
surgeon_result.iteration_suggestion
|
| 379 |
+
)
|
| 380 |
+
return Verdict.ITERATE
|
| 381 |
+
elif surgeon_result.verdict == Verdict.KILL:
|
| 382 |
return Verdict.KILL
|
| 383 |
|
| 384 |
+
# STEP 9: Gatekeeper (if metrics are strong)
|
| 385 |
+
if metrics and metrics.sharpe_os >= 1.25:
|
| 386 |
+
fitness = compute_fitness(metrics, max_corr, 0.5)
|
| 387 |
+
if fitness >= 1.0:
|
| 388 |
+
gate_result = await gate_alpha(
|
| 389 |
+
self.llm, blueprint, metrics, max_corr, fitness
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
)
|
| 391 |
+
console.print(f" [cyan]Gatekeeper:[/] {'GO' if gate_result.go_no_go else 'NO-GO'} (conf={gate_result.confidence:.2f})")
|
| 392 |
+
if gate_result.go_no_go:
|
| 393 |
+
self.winner_memory.record_winner(
|
| 394 |
+
expression.fields_used[0] if expression.fields_used else "",
|
| 395 |
+
blueprint.archetype,
|
| 396 |
+
blueprint.neutralization.value,
|
| 397 |
+
blueprint.decay,
|
| 398 |
+
metrics.sharpe_os,
|
| 399 |
+
blueprint.theme
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
)
|
| 401 |
+
return Verdict.PROMOTE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
+
if self.brain is None:
|
| 404 |
+
console.print(" [yellow]DRY RUN — returning ITERATE[/]")
|
|
|
|
|
|
|
| 405 |
|
| 406 |
+
return Verdict.ITERATE
|
|
|
|
|
|
|
| 407 |
|
| 408 |
async def _submit_or_dryrun(
|
| 409 |
self, alpha_id: str, expression: str,
|
| 410 |
neutralization: str, decay: int
|
| 411 |
) -> Verdict:
|
| 412 |
+
"""Submit to BRAIN or return ITERATE in dry-run mode.
|
| 413 |
+
Uses the already-initialized self.brain client.
|
| 414 |
+
"""
|
| 415 |
if self.brain is None:
|
| 416 |
console.print(" [yellow]DRY RUN:[/] Skipping BRAIN submission")
|
| 417 |
return Verdict.ITERATE
|
| 418 |
+
|
| 419 |
try:
|
| 420 |
+
result = await self.brain.submit_alpha(expression, neutralization, decay)
|
| 421 |
+
|
| 422 |
+
if result.get("status") == "DONE":
|
| 423 |
+
self._daily_submissions += 1
|
| 424 |
+
metrics = self.brain.parse_metrics(result, alpha_id)
|
| 425 |
+
self.store.update_metrics(alpha_id, metrics, 0.0)
|
| 426 |
+
|
| 427 |
+
if metrics.sharpe_os >= self.config.submission.min_sharpe:
|
| 428 |
+
console.print(f" [green]BRAIN PASS: Sharpe OS={metrics.sharpe_os:.2f}[/]")
|
| 429 |
+
return Verdict.PROMOTE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
else:
|
| 431 |
+
console.print(f" [yellow]BRAIN WEAK: Sharpe OS={metrics.sharpe_os:.2f}[/]")
|
| 432 |
+
return Verdict.ITERATE
|
| 433 |
+
else:
|
| 434 |
+
console.print(f" [red]BRAIN FAIL: {result.get('error', 'unknown')}[/]")
|
| 435 |
+
return Verdict.KILL
|
| 436 |
except Exception as e:
|
| 437 |
console.print(f" [red]BRAIN ERROR: {e}[/]")
|
| 438 |
return Verdict.ITERATE # Don't kill on transient errors
|
| 439 |
|
| 440 |
def _estimate_correlation(self, expression, existing_hashes) -> float:
|
| 441 |
"""Estimate max correlation to library based on archetype and field overlap."""
|
| 442 |
+
# TODO: Integrate actual BRAIN correlation API when available
|
| 443 |
return 0.3
|
| 444 |
|
| 445 |
def _get_synthetic_metrics(self, alpha_id: str) -> BrainMetrics:
|
| 446 |
"""Get metrics for an alpha (from store if BRAIN submitted, else synthetic)."""
|
|
|
|
|
|
|
| 447 |
return BrainMetrics(
|
| 448 |
alpha_id=alpha_id,
|
| 449 |
sharpe_full=1.5,
|