gaurv007 commited on
Commit
b5839f0
·
verified ·
1 Parent(s): 9fb868c

Upload alpha_factory/orchestration/pipeline.py

Browse files
Files changed (1) hide show
  1. alpha_factory/orchestration/pipeline.py +265 -347
alpha_factory/orchestration/pipeline.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
- Pipeline Orchestrator v3Full 7-layer pipeline with all personas wired,
3
- local simulation, BRAIN submission, winner memory, mutation iteration,
4
- parallel batch processing, and token budget enforcement.
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._daily_tokens = 0
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
- # === PROVEN TEMPLATE MODE ===
 
 
 
90
  if self.config.use_proven_templates:
91
- results = await self._run_proven_batch(
92
- batch_size, existing_themes, existing_tags, dead_themes,
93
- existing_hashes, batch_themes_used, failed_fields
94
- )
95
- promoted = results["promoted"]
96
- iterated = results["iterated"]
97
- killed = results["killed"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  else:
99
- # === LLM MODE: parallel candidate generation ===
100
  tasks = []
101
  for i in range(batch_size):
102
- tasks.append(self._run_single_candidate(
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 _run_proven_batch(
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
- # STEP 2: Generate hypothesis
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
- # STEP 4: Static lint
322
- lint_result = lint(expression.expression)
323
- if not lint_result.passed:
324
- console.print(f" [red]LINT FAIL:[/] {lint_result.errors}")
325
- self._consecutive_lint_fails += 1
326
- return Verdict.KILL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
- self._consecutive_lint_fails = 0
329
- if lint_result.warnings:
330
- console.print(f" [yellow]Warnings:[/] {lint_result.warnings}")
 
 
 
331
 
332
- # STEP 5: Dedup
333
- alpha_id = quick_dedup_hash(
334
- expression.expression, blueprint.neutralization.value, blueprint.decay
335
- )
336
- if alpha_id in existing_hashes:
337
- console.print(f" [red]DEDUP:[/] Already exists")
338
- return Verdict.KILL
 
 
 
 
 
 
 
339
 
340
- existing_hashes.add(alpha_id)
341
-
342
- # STEP 6: Store
343
- self.store.insert_alpha(
344
- alpha_id=alpha_id,
345
- expression=expression.expression,
346
- neutralization=blueprint.neutralization.value,
347
- decay=blueprint.decay,
348
- fields_used=expression.fields_used,
349
- operators_used=expression.operators_used,
350
- archetype=expression.archetype_used,
351
- theme=theme,
352
- anomaly_tag=blueprint.anomaly_tag.value,
353
- academic_anchor=blueprint.academic_anchor,
354
- family_id=alpha_id[:8],
355
  )
 
 
 
 
 
 
 
356
 
357
- # STEP 7: Local simulation (triage sanity check only, not hard filter)
358
- local_metrics = None
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
- # STEP 8: Acceptance checklist (gate before BRAIN submission)
379
- from ..schemas import Expression as ExprSchema
380
- checklist = run_acceptance_checklist(
381
- blueprint=blueprint,
382
- expression=ExprSchema(
383
- expression=expression.expression,
384
- fields_used=expression.fields_used,
385
- operators_used=expression.operators_used,
386
- archetype_used=expression.archetype_used,
387
- ),
388
- lint_result=lint_result,
389
- alpha_id=alpha_id,
390
- existing_hashes=existing_hashes,
391
- existing_anomaly_tags=existing_tags,
392
- max_corr_to_library=max_corr,
393
- local_sim_sharpe=local_metrics.sharpe if local_metrics else 1.5,
394
- local_sim_fitness=local_metrics.fitness if local_metrics else 1.2,
395
- local_sim_turnover=local_metrics.turnover if local_metrics else 0.3,
396
- sign_validated=True,
397
- )
398
- if not checklist.all_passed:
399
- console.print(f" [red]CHECKLIST FAIL:[/] {checklist.blocking_failures}")
400
- return Verdict.KILL
401
- console.print(f" [green]CHECKLIST PASS[/]")
402
-
403
- # STEP 9: Crowd Scout — novelty check
404
- # Compute a synthetic correlation based on fields/archetype overlap
405
- max_corr = self._estimate_correlation(expression, existing_hashes)
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
- # STEP 11: BRAIN submission or dry run
416
- verdict = await self._submit_or_dryrun(
417
- alpha_id, expression.expression,
418
- blueprint.neutralization.value, blueprint.decay
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  )
420
-
421
- if verdict == Verdict.KILL:
 
 
 
 
 
 
 
 
 
 
422
  return Verdict.KILL
423
 
424
- # STEP 10: Performance Surgeon (if BRAIN metrics available)
425
- metrics = None
426
- if self.brain is not None:
427
- metrics = self._get_synthetic_metrics(alpha_id)
428
-
429
- if metrics:
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]Surgeon:[/] {surgeon_result.verdict.value} {surgeon_result.reason[:80]}...")
444
-
445
- if surgeon_result.verdict == Verdict.ITERATE and iteration < self.config.max_iterations_per_family:
446
- mutations = generate_mutations(expression.expression, blueprint.decay)
447
- if mutations:
448
- self.winner_memory.queue_for_iteration(
449
- alpha_id, expression.expression,
450
- metrics.sharpe_os, metrics.turnover,
451
- surgeon_result.iteration_suggestion
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
- console.print(f" [cyan]Gatekeeper:[/] {'GO' if gate_result.go_no_go else 'NO-GO'} (conf={gate_result.confidence:.2f})")
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
- if self.brain is None:
477
- console.print(" [yellow]DRY RUN — returning ITERATE[/]")
478
-
479
- return Verdict.ITERATE
480
 
481
- except Exception as e:
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
- import aiohttp
496
- async with aiohttp.ClientSession() as session:
497
- # Re-init brain with fresh session
498
- brain = BrainClient(session, self.config.brain)
499
- result = await brain.submit_alpha(expression, neutralization, decay)
500
-
501
- if result.get("status") == "DONE":
502
- self._daily_submissions += 1
503
- metrics = brain.parse_metrics(result, alpha_id)
504
- self.store.update_metrics(alpha_id, metrics, 0.0)
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" [red]BRAIN FAIL: {result.get('error', 'unknown')}[/]")
515
- return Verdict.KILL
 
 
 
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
- # Simplified: return 0.3 as baseline (would need actual BRAIN correlation API)
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 v4Refactored 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,