mirror of
https://github.com/FxPouya/FxMathQuantWebApp.git
synced 2026-07-28 02:37:44 +00:00
333 lines
11 KiB
JavaScript
333 lines
11 KiB
JavaScript
/**
|
|
* Genetic Algorithm Engine - Evolves trading strategies
|
|
*/
|
|
class GeneticOptimizer {
|
|
constructor(data, config, symbol = '') {
|
|
this.data = data;
|
|
this.symbol = symbol;
|
|
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, this.symbol);
|
|
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));
|
|
}
|
|
}
|