@@ -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+
598922if __name__ == '__main__' :
599923 main ()
0 commit comments