Skip to content
This repository was archived by the owner on Jul 12, 2026. It is now read-only.

Commit dde9283

Browse files
committed
Merge remote-tracking branch 'origin/claude/add-evolve-to-gamma-Kh22K' into claude/assess-gamma-quality-qhC6n
2 parents 94b8ce9 + 7ea9fe6 commit dde9283

9 files changed

Lines changed: 1747 additions & 4 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "quantcoder-cli"
7-
version = "2.0.0-alpha.1"
8-
description = "A modern CLI coding assistant for generating QuantConnect trading algorithms from research articles"
7+
version = "2.1.0-alpha.1"
8+
description = "A modern CLI coding assistant for generating QuantConnect trading algorithms from research articles with AlphaEvolve-inspired evolution"
99
readme = "README.md"
1010
requires-python = ">=3.10"
1111
license = {text = "MIT"}
1212
authors = [
1313
{name = "SL-MAR", email = "smr.laignel@gmail.com"}
1414
]
15-
keywords = ["quantconnect", "trading", "algorithmic-trading", "cli", "ai", "llm"]
15+
keywords = ["quantconnect", "trading", "algorithmic-trading", "cli", "ai", "llm", "evolution", "alphaevolve"]
1616
classifiers = [
1717
"Development Status :: 4 - Beta",
1818
"Programming Language :: Python :: 3",

quantcoder/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
QuantCoder - A modern CLI coding assistant for QuantConnect algorithms.
33
44
Inspired by Mistral Vibe CLI architecture.
5+
Features AlphaEvolve-inspired strategy evolution.
56
"""
67

7-
__version__ = "2.0.0-alpha.1"
8+
__version__ = "2.1.0-alpha.1"
89
__author__ = "SL-MAR"

quantcoder/cli.py

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,5 +595,329 @@ def library_export(format, output):
595595
console.print(f"[red]Error exporting library: {e}[/red]")
596596

597597

598+
# ============================================================================
599+
# EVOLUTION MODE COMMANDS (AlphaEvolve-inspired)
600+
# ============================================================================
601+
602+
EVOLUTIONS_DIR = "data/evolutions"
603+
GENERATED_CODE_DIR = "generated_code"
604+
605+
606+
@main.group()
607+
def evolve():
608+
"""
609+
AlphaEvolve-inspired strategy evolution.
610+
611+
Evolve trading algorithms through LLM-generated variations,
612+
evaluated via QuantConnect backtests.
613+
"""
614+
pass
615+
616+
617+
@evolve.command(name='start')
618+
@click.argument('article_id', type=int, required=False)
619+
@click.option('--code', type=click.Path(exists=True), help='Path to algorithm file to evolve')
620+
@click.option('--resume', 'resume_id', help='Resume a previous evolution by ID')
621+
@click.option('--gens', 'max_generations', default=10, help='Maximum generations to run')
622+
@click.option('--variants', 'variants_per_gen', default=5, help='Variants per generation')
623+
@click.option('--elite', 'elite_size', default=3, help='Elite pool size')
624+
@click.option('--patience', default=3, help='Stop after N generations without improvement')
625+
@click.option('--qc-user', envvar='QC_USER_ID', help='QuantConnect user ID')
626+
@click.option('--qc-token', envvar='QC_API_TOKEN', help='QuantConnect API token')
627+
@click.option('--qc-project', envvar='QC_PROJECT_ID', type=int, help='QuantConnect project ID')
628+
@click.pass_context
629+
def evolve_start(ctx, article_id, code, resume_id, max_generations, variants_per_gen,
630+
elite_size, patience, qc_user, qc_token, qc_project):
631+
"""
632+
Evolve a trading algorithm using AlphaEvolve-inspired optimization.
633+
634+
This command takes a generated algorithm and evolves it through multiple
635+
generations of LLM-generated variations, evaluated via QuantConnect backtests.
636+
637+
ARTICLE_ID: The article number to evolve (must have generated code first)
638+
639+
Unlike traditional parameter optimization, this explores STRUCTURAL variations:
640+
- Indicator changes (SMA -> EMA, add RSI, etc.)
641+
- Risk management modifications
642+
- Entry/exit logic changes
643+
- Universe selection tweaks
644+
645+
Examples:
646+
quantcoder evolve start 1 # Evolve article 1's algorithm
647+
quantcoder evolve start 1 --gens 5 # Run for 5 generations
648+
quantcoder evolve start --code algo.py # Evolve from file
649+
quantcoder evolve start --resume abc123 # Resume evolution abc123
650+
"""
651+
import asyncio
652+
import os
653+
import json
654+
from pathlib import Path
655+
from quantcoder.evolver import EvolutionEngine, EvolutionConfig
656+
657+
# Validate QuantConnect credentials
658+
if not all([qc_user, qc_token, qc_project]):
659+
console.print("[red]Error: QuantConnect credentials required.[/red]")
660+
console.print("")
661+
console.print("[yellow]Set via environment variables:[/yellow]")
662+
console.print(" export QC_USER_ID=your_user_id")
663+
console.print(" export QC_API_TOKEN=your_api_token")
664+
console.print(" export QC_PROJECT_ID=your_project_id")
665+
console.print("")
666+
console.print("[yellow]Or use command options:[/yellow]")
667+
console.print(" quantcoder evolve start 1 --qc-user ID --qc-token TOKEN --qc-project PROJECT")
668+
ctx.exit(1)
669+
670+
# Handle resume mode
671+
if resume_id:
672+
console.print(f"[cyan]Resuming evolution: {resume_id}[/cyan]")
673+
baseline_code = None
674+
source_paper = None
675+
elif code:
676+
# Load from file
677+
code_path = Path(code)
678+
with open(code_path, 'r') as f:
679+
baseline_code = f.read()
680+
source_paper = str(code_path)
681+
elif article_id:
682+
# Load the generated code for this article
683+
code_path = Path(GENERATED_CODE_DIR) / f"algorithm_{article_id}.py"
684+
if not code_path.exists():
685+
console.print(f"[red]Error: No generated code found for article {article_id}.[/red]")
686+
console.print(f"[yellow]Run 'quantcoder generate {article_id}' first.[/yellow]")
687+
ctx.exit(1)
688+
689+
with open(code_path, 'r') as f:
690+
baseline_code = f.read()
691+
692+
# Get article info for reference
693+
source_paper = f"article_{article_id}"
694+
articles_file = Path("articles.json")
695+
if articles_file.exists():
696+
with open(articles_file, 'r') as f:
697+
articles = json.load(f)
698+
if 0 < article_id <= len(articles):
699+
source_paper = articles[article_id - 1].get('title', source_paper)
700+
else:
701+
console.print("[red]Error: Provide ARTICLE_ID, --code, or --resume[/red]")
702+
ctx.exit(1)
703+
704+
# Create evolution config
705+
config = EvolutionConfig(
706+
qc_user_id=qc_user,
707+
qc_api_token=qc_token,
708+
qc_project_id=qc_project,
709+
max_generations=max_generations,
710+
variants_per_generation=variants_per_gen,
711+
elite_pool_size=elite_size,
712+
convergence_patience=patience
713+
)
714+
715+
# Display configuration
716+
console.print("")
717+
console.print(Panel.fit(
718+
f"[bold]Max generations:[/bold] {max_generations}\n"
719+
f"[bold]Variants/gen:[/bold] {variants_per_gen}\n"
720+
f"[bold]Elite pool size:[/bold] {elite_size}\n"
721+
f"[bold]Convergence patience:[/bold] {patience}",
722+
title="[bold cyan]AlphaEvolve Strategy Optimization[/bold cyan]",
723+
border_style="cyan"
724+
))
725+
console.print("")
726+
727+
async def run_evolution():
728+
engine = EvolutionEngine(config)
729+
730+
# Set up progress callback
731+
def on_generation_complete(state, gen):
732+
best = state.elite_pool.get_best()
733+
if best and best.fitness:
734+
console.print(f"\n[green]Generation {gen} complete.[/green] Best fitness: {best.fitness:.4f}")
735+
736+
engine.on_generation_complete = on_generation_complete
737+
738+
# Run evolution
739+
if resume_id:
740+
result = await engine.evolve(baseline_code="", source_paper="", resume_id=resume_id)
741+
else:
742+
result = await engine.evolve(baseline_code, source_paper)
743+
744+
return result, engine
745+
746+
try:
747+
result, engine = asyncio.run(run_evolution())
748+
749+
# Report results
750+
console.print("")
751+
console.print(Panel.fit(
752+
result.get_summary(),
753+
title="[bold green]EVOLUTION COMPLETE[/bold green]",
754+
border_style="green"
755+
))
756+
757+
# Export best variant
758+
best = engine.get_best_variant()
759+
if best:
760+
output_path = Path(GENERATED_CODE_DIR) / f"evolved_{result.evolution_id}.py"
761+
output_path.parent.mkdir(parents=True, exist_ok=True)
762+
engine.export_best_code(str(output_path))
763+
console.print(f"\n[green]Best algorithm saved to:[/green] {output_path}")
764+
765+
console.print(f"\n[cyan]Evolution ID:[/cyan] {result.evolution_id}")
766+
console.print(f"[dim]To resume: quantcoder evolve start --resume {result.evolution_id}[/dim]")
767+
768+
except Exception as e:
769+
console.print(f"[red]Error: Evolution failed - {e}[/red]")
770+
ctx.exit(1)
771+
772+
773+
@evolve.command(name='list')
774+
def evolve_list():
775+
"""
776+
List all saved evolution runs.
777+
778+
Shows evolution IDs, status, and best fitness for each saved evolution.
779+
"""
780+
import os
781+
import json
782+
from pathlib import Path
783+
784+
evolutions_dir = Path(EVOLUTIONS_DIR)
785+
786+
if not evolutions_dir.exists():
787+
console.print("[yellow]No evolutions found.[/yellow]")
788+
return
789+
790+
evolution_files = list(evolutions_dir.glob("*.json"))
791+
792+
if not evolution_files:
793+
console.print("[yellow]No evolutions found.[/yellow]")
794+
return
795+
796+
console.print("\n[bold cyan]Saved Evolutions[/bold cyan]")
797+
console.print("-" * 60)
798+
799+
for filepath in sorted(evolution_files):
800+
try:
801+
with open(filepath, 'r') as f:
802+
data = json.load(f)
803+
804+
evo_id = data.get('evolution_id', 'unknown')
805+
status = data.get('status', 'unknown')
806+
generation = data.get('current_generation', 0)
807+
elite = data.get('elite_pool', {}).get('variants', [])
808+
best_fitness = elite[0].get('fitness', 'N/A') if elite else 'N/A'
809+
810+
status_color = {
811+
'completed': 'green',
812+
'running': 'yellow',
813+
'failed': 'red'
814+
}.get(status, 'white')
815+
816+
console.print(
817+
f" [cyan]{evo_id}[/cyan]: "
818+
f"Gen {generation}, "
819+
f"Status: [{status_color}]{status}[/{status_color}], "
820+
f"Best: {best_fitness}"
821+
)
822+
except Exception as e:
823+
console.print(f" [red]{filepath.name}: Error reading - {e}[/red]")
824+
825+
console.print("-" * 60)
826+
console.print("[dim]Resume with: quantcoder evolve start --resume <id>[/dim]")
827+
828+
829+
@evolve.command(name='show')
830+
@click.argument('evolution_id')
831+
def evolve_show(evolution_id):
832+
"""
833+
Show details of a specific evolution.
834+
835+
EVOLUTION_ID: The evolution ID to show
836+
"""
837+
import json
838+
from pathlib import Path
839+
840+
filepath = Path(EVOLUTIONS_DIR) / f"{evolution_id}.json"
841+
842+
if not filepath.exists():
843+
console.print(f"[red]Evolution {evolution_id} not found.[/red]")
844+
return
845+
846+
with open(filepath, 'r') as f:
847+
data = json.load(f)
848+
849+
# Summary
850+
console.print(Panel.fit(
851+
f"[bold]Evolution ID:[/bold] {data.get('evolution_id')}\n"
852+
f"[bold]Status:[/bold] {data.get('status')}\n"
853+
f"[bold]Generation:[/bold] {data.get('current_generation')}\n"
854+
f"[bold]Total Variants:[/bold] {len(data.get('all_variants', {}))}\n"
855+
f"[bold]Source:[/bold] {data.get('source_paper', 'N/A')}",
856+
title=f"[bold cyan]Evolution {evolution_id}[/bold cyan]",
857+
border_style="cyan"
858+
))
859+
860+
# Elite pool
861+
elite = data.get('elite_pool', {}).get('variants', [])
862+
if elite:
863+
console.print("\n[bold]Elite Pool:[/bold]")
864+
for i, variant in enumerate(elite, 1):
865+
metrics = variant.get('metrics', {})
866+
console.print(
867+
f" {i}. [cyan]{variant['id']}[/cyan] (Gen {variant['generation']}): "
868+
f"Fitness={variant.get('fitness', 'N/A'):.4f if variant.get('fitness') else 'N/A'}"
869+
)
870+
if metrics:
871+
console.print(
872+
f" Sharpe={metrics.get('sharpe_ratio', 0):.2f}, "
873+
f"Return={metrics.get('total_return', 0):.1%}, "
874+
f"MaxDD={metrics.get('max_drawdown', 0):.1%}"
875+
)
876+
877+
878+
@evolve.command(name='export')
879+
@click.argument('evolution_id')
880+
@click.option('--output', type=click.Path(), help='Output file path')
881+
def evolve_export(evolution_id, output):
882+
"""
883+
Export the best algorithm from an evolution.
884+
885+
EVOLUTION_ID: The evolution ID to export from
886+
"""
887+
import json
888+
from pathlib import Path
889+
890+
filepath = Path(EVOLUTIONS_DIR) / f"{evolution_id}.json"
891+
892+
if not filepath.exists():
893+
console.print(f"[red]Evolution {evolution_id} not found.[/red]")
894+
return
895+
896+
with open(filepath, 'r') as f:
897+
data = json.load(f)
898+
899+
elite = data.get('elite_pool', {}).get('variants', [])
900+
if not elite:
901+
console.print("[red]No elite variants found in this evolution.[/red]")
902+
return
903+
904+
best = elite[0]
905+
output_path = Path(output) if output else Path(GENERATED_CODE_DIR) / f"evolved_{evolution_id}.py"
906+
output_path.parent.mkdir(parents=True, exist_ok=True)
907+
908+
with open(output_path, 'w') as f:
909+
f.write(f"# Evolution: {evolution_id}\n")
910+
f.write(f"# Variant: {best['id']} (Generation {best['generation']})\n")
911+
f.write(f"# Fitness: {best.get('fitness', 'N/A')}\n")
912+
if best.get('metrics'):
913+
f.write(f"# Sharpe: {best['metrics'].get('sharpe_ratio', 0):.2f}\n")
914+
f.write(f"# Max Drawdown: {best['metrics'].get('max_drawdown', 0):.1%}\n")
915+
f.write(f"# Description: {best.get('mutation_description', 'N/A')}\n")
916+
f.write("#\n")
917+
f.write(best.get('code', ''))
918+
919+
console.print(f"[green]Exported best variant to:[/green] {output_path}")
920+
921+
598922
if __name__ == '__main__':
599923
main()

quantcoder/evolver/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
Evolution Layer for QuantCoder
3+
==============================
4+
5+
AlphaEvolve-inspired strategy optimization that explores the strategy space
6+
using LLM-generated variations instead of traditional parameter grid search.
7+
8+
Adapted for QuantCoder v2.0 with async support and multi-provider LLM.
9+
10+
Components:
11+
- EvolutionEngine: Main orchestrator for the evolution loop
12+
- VariationGenerator: LLM-based strategy variation creator
13+
- QCEvaluator: QuantConnect backtest integration
14+
- ElitePool: Persistence layer for best-performing strategies
15+
"""
16+
17+
from .engine import EvolutionEngine
18+
from .variation import VariationGenerator
19+
from .evaluator import QCEvaluator
20+
from .persistence import ElitePool, EvolutionState, Variant
21+
from .config import EvolutionConfig, FitnessWeights
22+
23+
__all__ = [
24+
'EvolutionEngine',
25+
'VariationGenerator',
26+
'QCEvaluator',
27+
'ElitePool',
28+
'EvolutionState',
29+
'Variant',
30+
'EvolutionConfig',
31+
'FitnessWeights',
32+
]

0 commit comments

Comments
 (0)