/** * Genetic Algorithm Engine - Evolves trading strategies */ class GeneticOptimizer { constructor(data, config) { this.data = data; this.config = { populationSize: config.populationSize || 100, generations: config.generations || 50, strategiesCount: config.strategiesCount || 5, rulesRange: config.rulesRange || [3, 8], shiftRange: config.shiftRange || [1, 10], minTrades: config.minTrades || 30, minPF: config.minPF || 1.5, maxDD: config.maxDD || 25, minWR: config.minWR || 45 }; this.population = []; this.foundStrategies = []; this.isRunning = false; this.currentGeneration = 0; } /** * Initialize random population */ initializePopulation() { this.population = []; for (let i = 0; i < this.config.populationSize; i++) { const strategy = new Strategy(); strategy.generateRandomRules(this.config.rulesRange, this.config.shiftRange); strategy.randomizeParameters(); this.population.push(strategy); } } /** * Evaluate fitness for all strategies */ evaluatePopulation() { for (const strategy of this.population) { const backtester = new Backtester(this.data, strategy); strategy.metrics = backtester.run(); strategy.fitness = this.calculateFitness(strategy.metrics); } // Sort by fitness (descending) this.population.sort((a, b) => b.fitness - a.fitness); } /** * Calculate fitness score */ calculateFitness(metrics) { if (metrics.totalTrades === 0) return 0; const pf = Math.min(metrics.profitFactor, 10); const trades = Math.sqrt(metrics.totalTrades); const wrBonus = metrics.winRate > 50 ? 1 + (metrics.winRate - 50) / 100 : 1; const ddPenalty = 1 + (metrics.maxDrawdown / 100); // Calculate BUY/SELL balance bonus const buyRatio = (metrics.buyTrades / metrics.totalTrades) * 100; const sellRatio = (metrics.sellTrades / metrics.totalTrades) * 100; // Reward strategies with 30-70% split (most balanced) // Penalize strategies with <20% or >80% of one type let balanceBonus = 1.0; if (buyRatio >= 30 && buyRatio <= 70) { balanceBonus = 1.2; // 20% bonus for good balance } else if (buyRatio >= 20 && buyRatio <= 80) { balanceBonus = 1.1; // 10% bonus for acceptable balance } else { balanceBonus = 0.5; // 50% penalty for poor balance } return (Math.pow(pf, 1.5) * trades * wrBonus * balanceBonus) / ddPenalty; } /** * Check if strategy meets criteria */ meetsCriteria(strategy) { const m = strategy.metrics; // Check basic criteria const meetsBasic = ( m.totalTrades >= this.config.minTrades && m.profitFactor >= this.config.minPF && m.maxDrawdown <= this.config.maxDD && m.winRate >= this.config.minWR ); if (!meetsBasic) return false; // Check BUY/SELL balance (should be between 20-80%) const buyRatio = (m.buyTrades / m.totalTrades) * 100; const sellRatio = (m.sellTrades / m.totalTrades) * 100; // Require at least 20% of each type for balance const isBalanced = buyRatio >= 20 && sellRatio >= 20; console.log(`📊 Strategy balance: BUY=${buyRatio.toFixed(1)}%, SELL=${sellRatio.toFixed(1)}%, Balanced=${isBalanced}`); return isBalanced; } /** * Tournament selection */ tournamentSelection() { const tournamentSize = 3; let best = null; for (let i = 0; i < tournamentSize; i++) { const candidate = this.population[Math.floor(Math.random() * this.population.length)]; if (!best || candidate.fitness > best.fitness) { best = candidate; } } return best.copy(); } /** * Crossover two parent strategies */ crossover(parent1, parent2) { const child = new Strategy(); // Crossover rules const splitPoint = Math.floor(Math.random() * Math.min(parent1.rules.length, parent2.rules.length)); child.rules = [ ...parent1.rules.slice(0, splitPoint), ...parent2.rules.slice(splitPoint) ]; // Ensure rules count is within range while (child.rules.length < this.config.rulesRange[0]) { child.rules.push(child.generateRule(this.config.shiftRange)); } while (child.rules.length > this.config.rulesRange[1]) { child.rules.splice(Math.floor(Math.random() * child.rules.length), 1); } // Crossover parameters child.atrPeriod = Math.random() < 0.5 ? parent1.atrPeriod : parent2.atrPeriod; child.slMultiplier = Math.random() < 0.5 ? parent1.slMultiplier : parent2.slMultiplier; child.tpMultiplier = Math.random() < 0.5 ? parent1.tpMultiplier : parent2.tpMultiplier; return child; } /** * Mutate a strategy */ mutate(strategy) { const mutationRate = 0.2; if (Math.random() < mutationRate) { const mutationType = Math.random(); if (mutationType < 0.4) { // Mutate a rule if (strategy.rules.length > 0) { const index = Math.floor(Math.random() * strategy.rules.length); strategy.rules[index] = strategy.generateRule(this.config.shiftRange); } } else if (mutationType < 0.55) { // Add a rule if (strategy.rules.length < this.config.rulesRange[1]) { strategy.rules.push(strategy.generateRule(this.config.shiftRange)); } } else if (mutationType < 0.7) { // Remove a rule if (strategy.rules.length > this.config.rulesRange[0]) { strategy.rules.splice(Math.floor(Math.random() * strategy.rules.length), 1); } } else if (mutationType < 0.85) { // Mutate ATR period strategy.atrPeriod = Math.floor(Math.random() * 31) + 10; } else { // Mutate SL/TP strategy.slMultiplier = (Math.random() * 3) + 1; strategy.tpMultiplier = strategy.slMultiplier + (Math.random() * 5) + 0.5; } } } /** * Evolve one generation */ evolveGeneration() { const newPopulation = []; // Elitism: keep top 2 strategies newPopulation.push(this.population[0].copy()); newPopulation.push(this.population[1].copy()); // Generate rest through crossover and mutation while (newPopulation.length < this.config.populationSize) { const parent1 = this.tournamentSelection(); const parent2 = this.tournamentSelection(); const child = this.crossover(parent1, parent2); this.mutate(child); newPopulation.push(child); } this.population = newPopulation; } /** * Run the genetic algorithm */ async run(progressCallback) { console.log('🧬 GA.run() started'); console.log('📊 Data rows:', this.data.length); console.log('⚙️ Config:', this.config); this.isRunning = true; this.foundStrategies = []; this.currentGeneration = 0; const startTime = Date.now(); while (this.isRunning && this.foundStrategies.length < this.config.strategiesCount) { console.log('🔄 Starting new GA search, found so far:', this.foundStrategies.length); // Initialize new population for each search this.initializePopulation(); console.log('✅ Population initialized:', this.population.length, 'strategies'); // Evolve for specified generations for (let gen = 0; gen < this.config.generations && this.isRunning; gen++) { this.currentGeneration = gen + 1; // Evaluate fitness console.log(`Gen ${gen + 1}/${this.config.generations}: Evaluating population...`); this.evaluatePopulation(); console.log(`Gen ${gen + 1}: Best fitness = ${this.population[0].fitness.toFixed(2)}, Trades = ${this.population[0].metrics.totalTrades}`); // Report progress if (progressCallback) { const elapsed = Math.floor((Date.now() - startTime) / 1000); progressCallback({ generation: gen + 1, totalGenerations: this.config.generations, bestFitness: this.population[0].fitness, avgFitness: this.population.reduce((sum, s) => sum + s.fitness, 0) / this.population.length, foundCount: this.foundStrategies.length, targetCount: this.config.strategiesCount, elapsedTime: elapsed }); } // Check if best strategy meets criteria const best = this.population[0]; console.log(`Checking criteria: PF=${best.metrics.profitFactor.toFixed(2)}, WR=${best.metrics.winRate.toFixed(1)}%, Trades=${best.metrics.totalTrades}, DD=${best.metrics.maxDrawdown.toFixed(2)}%`); if (this.meetsCriteria(best) && !this.isDuplicate(best)) { console.log('✅ Strategy meets criteria!'); this.foundStrategies.push(best.copy()); if (progressCallback) { progressCallback({ strategyFound: best, foundCount: this.foundStrategies.length }); } // If we found enough, break if (this.foundStrategies.length >= this.config.strategiesCount) { break; } } // Evolve to next generation if (gen < this.config.generations - 1) { this.evolveGeneration(); } // Allow UI to update await this.sleep(10); } } this.isRunning = false; console.log('🏁 GA.run() complete. Returning', this.foundStrategies.length, 'strategies'); console.log('Strategies to return:', this.foundStrategies); return this.foundStrategies; } /** * Check if strategy is duplicate */ isDuplicate(strategy) { for (const existing of this.foundStrategies) { if (this.strategiesSimilar(strategy, existing)) { return true; } } return false; } /** * Check if two strategies are similar */ strategiesSimilar(s1, s2) { // Simple check: if rules are identical if (s1.rules.length !== s2.rules.length) return false; const rules1 = JSON.stringify(s1.rules); const rules2 = JSON.stringify(s2.rules); return rules1 === rules2; } /** * Stop the optimization */ stop() { this.isRunning = false; } /** * Sleep helper for async */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } }