Files
managing-apps/src/Managing.Application/GeneticService.cs

1095 lines
44 KiB
C#

using System.Text.Json;
using GeneticSharp;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Backtests;
using Managing.Core;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Risk;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Users;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application;
/// <summary>
/// Service implementation for managing genetic algorithm requests
/// </summary>
public class GeneticService : IGeneticService
{
private readonly IGeneticRepository _geneticRepository;
private readonly IBacktester _backtester;
private readonly ILogger<GeneticService> _logger;
private readonly IMessengerService _messengerService;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IGrainFactory _grainFactory;
private readonly IJobRepository _jobRepository;
// Predefined parameter ranges for each indicator (matching backtestGenetic.tsx)
public static readonly Dictionary<string, (double min, double max)> ParameterRanges = new()
{
// Trading Parameters only - indicator parameters are now handled by IndicatorParameterRanges
["stopLoss"] = (0.2, 50.0), // Minimum 0.2% to cover fees, no upper limit (set to 50% as practical max)
["leverage"] = (1.0, 10.0),
["cooldownPeriod"] = (5.0, 25.0),
["maxLossStreak"] = (0.0, 4.0),
["maxPositionTimeHours"] = (0, 48.0)
};
// Default indicator values per indicator type (matching CustomScenario.tsx)
public static readonly Dictionary<IndicatorType, Dictionary<string, double>> DefaultIndicatorValues = new()
{
[IndicatorType.RsiDivergence] = new() { ["period"] = 14.0 },
[IndicatorType.RsiDivergenceConfirm] = new() { ["period"] = 14.0 },
[IndicatorType.EmaCross] = new() { ["period"] = 14.0 },
[IndicatorType.EmaTrend] = new() { ["period"] = 14.0 },
[IndicatorType.StDev] = new() { ["period"] = 14.0 },
[IndicatorType.ThreeWhiteSoldiers] = new() { ["period"] = 14.0 },
[IndicatorType.MacdCross] = new()
{
["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0,
["signalPeriods"] = 9.0
},
[IndicatorType.DualEmaCross] = new()
{
["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
},
[IndicatorType.SuperTrend] = new()
{
["period"] = 14.0,
["multiplier"] = 3.0
},
[IndicatorType.SuperTrendCrossEma] = new()
{
["period"] = 14.0,
["multiplier"] = 3.0
},
[IndicatorType.ChandelierExit] = new()
{
["period"] = 14.0,
["multiplier"] = 3.0
},
[IndicatorType.StochRsiTrend] = new()
{
["period"] = 14.0,
["stochPeriods"] = 14.0,
["signalPeriods"] = 9.0,
["smoothPeriods"] = 3.0
},
[IndicatorType.Stc] = new()
{
["cyclePeriods"] = 10.0,
["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
},
[IndicatorType.LaggingStc] = new()
{
["cyclePeriods"] = 10.0,
["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
}
};
// Indicator-specific parameter ranges
public static readonly Dictionary<IndicatorType, Dictionary<string, (double min, double max)>>
IndicatorParameterRanges = new()
{
[IndicatorType.RsiDivergence] = new()
{
["period"] = (6.0, 70.0)
},
[IndicatorType.RsiDivergenceConfirm] = new()
{
["period"] = (6.0, 70.0)
},
[IndicatorType.EmaCross] = new()
{
["period"] = (10.0, 300.0)
},
[IndicatorType.EmaTrend] = new()
{
["period"] = (10.0, 300.0)
},
[IndicatorType.StDev] = new()
{
["period"] = (5.0, 50.0)
},
[IndicatorType.ThreeWhiteSoldiers] = new()
{
["period"] = (5.0, 50.0)
},
[IndicatorType.MacdCross] = new()
{
["fastPeriods"] = (10.0, 70.0),
["slowPeriods"] = (20.0, 120.0),
["signalPeriods"] = (5.0, 50.0)
},
[IndicatorType.DualEmaCross] = new()
{
["fastPeriods"] = (5.0, 300.0),
["slowPeriods"] = (5.0, 300.0)
},
[IndicatorType.SuperTrend] = new()
{
["period"] = (5.0, 50.0),
["multiplier"] = (1.0, 10.0)
},
[IndicatorType.SuperTrendCrossEma] = new()
{
["period"] = (5.0, 50.0),
["multiplier"] = (1.0, 10.0)
},
[IndicatorType.ChandelierExit] = new()
{
["period"] = (5.0, 50.0),
["multiplier"] = (1.0, 10.0)
},
[IndicatorType.StochRsiTrend] = new()
{
["period"] = (5.0, 50.0),
["stochPeriods"] = (5.0, 30.0),
["signalPeriods"] = (3.0, 15.0),
["smoothPeriods"] = (1.0, 10.0)
},
[IndicatorType.Stc] = new()
{
["cyclePeriods"] = (5.0, 50.0),
["fastPeriods"] = (5.0, 70.0),
["slowPeriods"] = (10.0, 120.0)
},
[IndicatorType.LaggingStc] = new()
{
["cyclePeriods"] = (5.0, 30.0),
["fastPeriods"] = (5.0, 50.0),
["slowPeriods"] = (10.0, 100.0)
}
};
// Indicator type to parameter mapping
public static readonly Dictionary<IndicatorType, string[]> IndicatorParamMapping = new()
{
[IndicatorType.RsiDivergence] = ["period"],
[IndicatorType.RsiDivergenceConfirm] = ["period"],
[IndicatorType.EmaCross] = ["period"],
[IndicatorType.EmaTrend] = ["period"],
[IndicatorType.StDev] = ["period"],
[IndicatorType.ThreeWhiteSoldiers] = ["period"],
[IndicatorType.MacdCross] = ["fastPeriods", "slowPeriods", "signalPeriods"],
[IndicatorType.DualEmaCross] = ["fastPeriods", "slowPeriods"],
[IndicatorType.SuperTrend] = ["period", "multiplier"],
[IndicatorType.SuperTrendCrossEma] = ["period", "multiplier"],
[IndicatorType.ChandelierExit] = ["period", "multiplier"],
[IndicatorType.StochRsiTrend] = ["period", "stochPeriods", "signalPeriods", "smoothPeriods"],
[IndicatorType.Stc] = ["cyclePeriods", "fastPeriods", "slowPeriods"],
[IndicatorType.LaggingStc] = ["cyclePeriods", "fastPeriods", "slowPeriods"]
};
public GeneticService(
IGeneticRepository geneticRepository,
IBacktester backtester,
ILogger<GeneticService> logger,
IMessengerService messengerService,
IServiceScopeFactory serviceScopeFactory,
IGrainFactory grainFactory,
IJobRepository jobRepository)
{
_geneticRepository = geneticRepository;
_backtester = backtester;
_logger = logger;
_messengerService = messengerService;
_serviceScopeFactory = serviceScopeFactory;
_grainFactory = grainFactory;
_jobRepository = jobRepository;
}
public async Task<GeneticRequest> CreateGeneticRequestAsync(
User user,
Ticker ticker,
Timeframe timeframe,
DateTime startDate,
DateTime endDate,
decimal balance,
int populationSize,
int generations,
double mutationRate,
GeneticSelectionMethod selectionMethod,
GeneticCrossoverMethod crossoverMethod,
GeneticMutationMethod mutationMethod,
int elitismPercentage,
double maxTakeProfit,
List<IndicatorType> eligibleIndicators)
{
var id = Guid.NewGuid().ToString();
var geneticRequest = new GeneticRequest(id)
{
Ticker = ticker,
Timeframe = timeframe,
StartDate = startDate,
EndDate = endDate,
Balance = balance,
PopulationSize = populationSize,
Generations = generations,
MutationRate = mutationRate,
SelectionMethod = selectionMethod,
CrossoverMethod = crossoverMethod,
MutationMethod = mutationMethod,
ElitismPercentage = elitismPercentage,
MaxTakeProfit = maxTakeProfit,
EligibleIndicators = eligibleIndicators,
Status = GeneticRequestStatus.Pending
};
_geneticRepository.InsertGeneticRequestForUser(user, geneticRequest);
// Create a single job for this genetic request that will run until completion
try
{
var job = new Job
{
UserId = user.Id,
Status = JobStatus.Pending,
JobType = JobType.Genetic,
Priority = 0,
ConfigJson = "{}", // Not needed for genetic jobs, GeneticRequestId is used
StartDate = startDate,
EndDate = endDate,
GeneticRequestId = id,
RetryCount = 0,
MaxRetries = 3,
IsRetryable = true
};
await _jobRepository.CreateAsync(job);
_logger.LogInformation("Created genetic job {JobId} for genetic request {RequestId}", job.Id, id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create job for genetic request {RequestId}", id);
throw;
}
return geneticRequest;
}
public IEnumerable<GeneticRequest> GetGeneticRequestsByUser(User user)
{
return _geneticRepository.GetGeneticRequestsByUser(user);
}
public GeneticRequest GetGeneticRequestByIdForUser(User user, string id)
{
return _geneticRepository.GetGeneticRequestByIdForUser(user, id);
}
public async Task UpdateGeneticRequestAsync(GeneticRequest geneticRequest)
{
await _geneticRepository.UpdateGeneticRequestAsync(geneticRequest);
}
public void DeleteGeneticRequestByIdForUser(User user, string id)
{
_geneticRepository.DeleteGeneticRequestByIdForUser(user, id);
}
public Task<List<GeneticRequest>> GetGeneticRequestsAsync(GeneticRequestStatus status)
{
return _geneticRepository.GetGeneticRequestsAsync(status);
}
/// <summary>
/// Runs the genetic algorithm for a specific request
/// </summary>
/// <param name="request">The genetic request to process</param>
/// <param name="cancellationToken">Cancellation token to stop the algorithm</param>
/// <returns>The genetic algorithm result</returns>
public async Task<GeneticAlgorithmResult> RunGeneticAlgorithm(GeneticRequest request,
CancellationToken cancellationToken = default)
{
try
{
_logger.LogInformation("Starting genetic algorithm for request {RequestId}", request.RequestId);
// Update status to running
request.Status = GeneticRequestStatus.Running;
await UpdateGeneticRequestAsync(request);
// Create or resume chromosome for trading bot configuration
TradingBotChromosome chromosome;
Population population;
if (!string.IsNullOrEmpty(request.BestChromosome) && request.CurrentGeneration > 0)
{
// Resume from previous state (best chromosome only)
chromosome = new TradingBotChromosome(request.EligibleIndicators, request.MaxTakeProfit);
var savedChromosome = JsonSerializer.Deserialize<double[]>(request.BestChromosome);
if (savedChromosome != null)
{
chromosome.ReplaceGenes(0, savedChromosome.Select(g => new Gene(g)).ToArray());
}
population = new Population(request.PopulationSize, request.PopulationSize, chromosome);
_logger.LogInformation(
"Resuming genetic algorithm for request {RequestId} from generation {Generation} with best chromosome",
request.RequestId, request.CurrentGeneration);
}
else
{
// Start fresh
chromosome = new TradingBotChromosome(request.EligibleIndicators, request.MaxTakeProfit);
population = new Population(request.PopulationSize, request.PopulationSize, chromosome);
_logger.LogInformation("Starting fresh genetic algorithm for request {RequestId}", request.RequestId);
}
// Load candles once at the beginning to avoid repeated database queries
// This significantly reduces database connections during genetic algorithm execution
_logger.LogInformation(
"Loading candles for genetic algorithm {RequestId}: {Ticker} on {Timeframe} from {StartDate} to {EndDate}",
request.RequestId, request.Ticker, request.Timeframe, request.StartDate, request.EndDate);
HashSet<Candle> candles;
try
{
candles = await ServiceScopeHelpers.WithScopedService<ICandleRepository, HashSet<Candle>>(
_serviceScopeFactory,
async candleRepository => await candleRepository.GetCandles(
TradingExchanges.Evm, // Default exchange for genetic algorithms
request.Ticker,
request.Timeframe,
request.StartDate,
request.EndDate
)
);
if (candles == null || candles.Count == 0)
{
throw new InvalidOperationException(
$"No candles found for {request.Ticker} on {request.Timeframe} from {request.StartDate} to {request.EndDate}");
}
_logger.LogInformation("Loaded {CandleCount} candles for genetic algorithm {RequestId}",
candles.Count, request.RequestId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load candles for genetic algorithm {RequestId}", request.RequestId);
throw;
}
// Create fitness function with pre-loaded candles to avoid database queries during evaluation
var fitness = new TradingBotFitness(_serviceScopeFactory, request, candles, _logger);
// Create genetic algorithm with better configuration
// Limit parallelism to prevent database connection pool exhaustion
// Each fitness evaluation creates a database connection, so we need to limit
// the number of parallel evaluations to stay within connection pool limits
// Default PostgreSQL pool is 20 connections, so we limit to 4 threads per genetic job
// This leaves room for other operations and multiple concurrent genetic jobs
var maxParallelThreads = Math.Min(4, Environment.ProcessorCount);
var ga = new GeneticAlgorithm(
population,
fitness,
GetSelection(request.SelectionMethod),
GetCrossover(request.CrossoverMethod),
GetMutation(request.MutationMethod))
{
Termination = new OrTermination(
new GenerationNumberTermination(request.Generations),
new FitnessStagnationTermination(15)
),
MutationProbability = (float)request.MutationRate,
CrossoverProbability = 0.75f, // Fixed crossover rate as in frontend
TaskExecutor = new ParallelTaskExecutor
{
MinThreads = 2,
MaxThreads = maxParallelThreads
}
};
// Set the genetic algorithm reference in the fitness function
fitness.SetGeneticAlgorithm(ga);
// Custom termination condition that checks for cancellation
var originalTermination = ga.Termination;
ga.Termination = new GenerationNumberTermination(request.Generations);
// Add cancellation check in the generation event
// Run the genetic algorithm with periodic checks for cancellation
var generationCount = 0;
ga.GenerationRan += async (sender, e) =>
{
try
{
generationCount = ga.GenerationsNumber;
// Update progress every generation
var bestFitness = ga.BestChromosome?.Fitness ?? 0;
var bestChromosomeJson = (string?)null;
var bestIndividual = (string?)null;
if (ga.BestChromosome is TradingBotChromosome bestChromosome)
{
var genes = bestChromosome.GetGenes();
var geneValues = genes.Select(g =>
{
if (g.Value is double doubleValue) return doubleValue;
if (g.Value is int intValue) return (double)intValue;
return Convert.ToDouble(g.Value.ToString());
}).ToArray();
bestChromosomeJson = JsonSerializer.Serialize(geneValues);
bestIndividual = bestChromosome.ToString();
}
// Update ProgressInfo with current generation information
var progressInfo = JsonSerializer.Serialize(new
{
generation = generationCount,
best_fitness = bestFitness,
population_size = request.PopulationSize,
generations = request.Generations,
updated_at = DateTime.UtcNow
});
// Update the domain object for local use
request.CurrentGeneration = generationCount;
request.BestFitnessSoFar = bestFitness;
request.BestChromosome = bestChromosomeJson;
request.BestIndividual = bestIndividual;
request.ProgressInfo = progressInfo;
// Update the database with current generation progress using a new scope
// This prevents DbContext concurrency issues when running in parallel
await ServiceScopeHelpers.WithScopedService<IGeneticService>(
_serviceScopeFactory,
async geneticService =>
{
// Reload the request from the database in the new scope
// Use the user from the original request to get the request by ID
var dbRequest =
geneticService.GetGeneticRequestByIdForUser(request.User, request.RequestId);
if (dbRequest != null)
{
// Update the loaded request with current generation data
dbRequest.CurrentGeneration = generationCount;
dbRequest.BestFitnessSoFar = bestFitness;
dbRequest.BestChromosome = bestChromosomeJson;
dbRequest.BestIndividual = bestIndividual;
dbRequest.ProgressInfo = progressInfo;
// Save the update
await geneticService.UpdateGeneticRequestAsync(dbRequest);
}
});
_logger.LogDebug(
"Updated genetic request {RequestId} at generation {Generation} with fitness {Fitness}",
request.RequestId, generationCount, bestFitness);
// Check for cancellation
if (cancellationToken.IsCancellationRequested)
{
ga.Stop();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating genetic request {RequestId} at generation {Generation}",
request.RequestId, generationCount);
// Don't throw - continue with next generation
}
};
// Run the genetic algorithm
ga.Start();
// Check if the algorithm was cancelled
if (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Genetic algorithm cancelled for request {RequestId}", request.RequestId);
// Update request status to pending so it can be resumed
request.Status = GeneticRequestStatus.Pending;
await UpdateGeneticRequestAsync(request);
return new GeneticAlgorithmResult
{
BestFitness = request.BestFitnessSoFar ?? 0,
BestIndividual = request.BestIndividual ?? "unknown",
ProgressInfo = request.ProgressInfo,
CompletedAt = DateTime.UtcNow
};
}
// Get the best chromosome
var bestChromosome = ga.BestChromosome as TradingBotChromosome;
var bestFitness = ga.BestChromosome?.Fitness ?? 0;
_logger.LogInformation("Genetic algorithm completed for request {RequestId}. Best fitness: {Fitness}",
request.RequestId, bestFitness);
// Update request with final results
request.Status = GeneticRequestStatus.Completed;
request.CompletedAt = DateTime.UtcNow;
request.BestFitness = bestFitness;
request.BestIndividual = bestChromosome?.ToString() ?? "unknown";
request.CurrentGeneration = ga.GenerationsNumber;
request.BestFitnessSoFar = bestFitness;
// Update BestChromosome if not already set
if (bestChromosome != null && string.IsNullOrEmpty(request.BestChromosome))
{
var genes = bestChromosome.GetGenes();
var geneValues = genes.Select(g =>
{
if (g.Value is double doubleValue) return doubleValue;
if (g.Value is int intValue) return (double)intValue;
return Convert.ToDouble(g.Value.ToString());
}).ToArray();
request.BestChromosome = JsonSerializer.Serialize(geneValues);
}
request.ProgressInfo = JsonSerializer.Serialize(new
{
generation = ga.GenerationsNumber,
best_fitness = bestFitness,
population_size = request.PopulationSize,
generations = request.Generations,
completed_at = DateTime.UtcNow
});
await UpdateGeneticRequestAsync(request);
_logger.LogInformation(
"Final update completed for genetic request {RequestId}. Generation: {Generation}, Best Fitness: {Fitness}",
request.RequestId, ga.GenerationsNumber, bestFitness);
// Send notification about the completed genetic algorithm
try
{
await _messengerService.SendGeneticAlgorithmNotification(request, bestFitness, bestChromosome);
}
catch (Exception notificationEx)
{
_logger.LogWarning(notificationEx,
"Failed to send genetic algorithm notification for request {RequestId}", request.RequestId);
}
return new GeneticAlgorithmResult
{
BestFitness = bestFitness,
BestIndividual = bestChromosome?.ToString() ?? "unknown",
ProgressInfo = request.ProgressInfo,
CompletedAt = DateTime.UtcNow
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error running genetic algorithm for request {RequestId}", request.RequestId);
// Update request with error
request.Status = GeneticRequestStatus.Failed;
request.ErrorMessage = ex.Message;
request.CompletedAt = DateTime.UtcNow;
await UpdateGeneticRequestAsync(request);
throw;
}
}
private ISelection GetSelection(GeneticSelectionMethod selectionMethod)
{
return selectionMethod switch
{
GeneticSelectionMethod.Elite => new EliteSelection(),
GeneticSelectionMethod.Roulette => new RouletteWheelSelection(),
GeneticSelectionMethod.StochasticUniversalSampling => new StochasticUniversalSamplingSelection(),
GeneticSelectionMethod.Tournament => new TournamentSelection(),
GeneticSelectionMethod.Truncation => new TruncationSelection(),
_ => new TournamentSelection()
};
}
private ICrossover GetCrossover(GeneticCrossoverMethod crossoverMethod)
{
return crossoverMethod switch
{
GeneticCrossoverMethod.AlternatingPosition => new AlternatingPositionCrossover(),
GeneticCrossoverMethod.CutAndSplice => new CutAndSpliceCrossover(),
GeneticCrossoverMethod.Cycle => new CycleCrossover(),
GeneticCrossoverMethod.OnePoint => new OnePointCrossover(),
GeneticCrossoverMethod.OrderBased => new OrderBasedCrossover(),
GeneticCrossoverMethod.Ordered => new OrderedCrossover(),
GeneticCrossoverMethod.PartiallyMapped => new PartiallyMappedCrossover(),
GeneticCrossoverMethod.PositionBased => new PositionBasedCrossover(),
GeneticCrossoverMethod.ThreeParent => new ThreeParentCrossover(),
GeneticCrossoverMethod.TwoPoint => new TwoPointCrossover(),
GeneticCrossoverMethod.Uniform => new UniformCrossover(),
GeneticCrossoverMethod.VotingRecombination => new VotingRecombinationCrossover(),
_ => new UniformCrossover()
};
}
private IMutation GetMutation(GeneticMutationMethod mutationMethod)
{
return mutationMethod switch
{
GeneticMutationMethod.Displacement => new DisplacementMutation(),
GeneticMutationMethod.FlipBit => new FlipBitMutation(),
GeneticMutationMethod.Insertion => new InsertionMutation(),
GeneticMutationMethod.PartialShuffle => new PartialShuffleMutation(),
GeneticMutationMethod.ReverseSequence => new ReverseSequenceMutation(),
GeneticMutationMethod.Twors => new TworsMutation(),
GeneticMutationMethod.Uniform => new UniformMutation(true),
_ => new UniformMutation(true)
};
}
}
/// <summary>
/// Chromosome representing a trading bot configuration with predefined parameter ranges
/// </summary>
public class TradingBotChromosome : ChromosomeBase
{
private readonly List<IndicatorType> _eligibleIndicators;
private readonly double _maxTakeProfit;
private readonly Random _random = new Random();
private int[] _indicatorSelectionPattern;
// Gene structure:
// 0-3: Trading parameters (takeProfit, stopLoss, cooldownPeriod, maxLossStreak)
// 4: Loopback period
// 5-4+N: Indicator selection (N = number of eligible indicators)
// 5+N+: Indicator parameters (period, fastPeriods, etc.)
public TradingBotChromosome(List<IndicatorType> eligibleIndicators, double maxTakeProfit)
: base(4 + 1 + eligibleIndicators.Count +
eligibleIndicators.Count * 8) // Trading params + loopback + indicator selection + indicator params
{
_eligibleIndicators = eligibleIndicators;
_maxTakeProfit = maxTakeProfit;
// Initialize genes with proper constraint handling
InitializeGenesWithConstraints();
}
public override Gene GenerateGene(int geneIndex)
{
if (geneIndex < 4)
{
// Trading parameters
return geneIndex switch
{
0 => new Gene(GetRandomInRange((0.9, _maxTakeProfit))), // Take profit (0.9% to max TP)
1 => new Gene(GetRandomInRange((0.2, 50.0))), // Stop loss (will be constrained in GetTradingBotConfig)
2 => new Gene(GetRandomIntInRange(GeneticService.ParameterRanges["cooldownPeriod"])), // Cooldown period
3 => new Gene(GetRandomIntInRange(GeneticService.ParameterRanges["maxLossStreak"])), // Max loss streak
_ => new Gene(0)
};
}
else if (geneIndex == 4)
{
// LoopbackPeriod gene (always between 5 and 20)
return new Gene(GetRandomIntInRange((5, 20)));
}
else if (geneIndex < 5 + _eligibleIndicators.Count)
{
// Indicator selection (0 = not selected, 1 = selected)
// Generate a random combination of up to 4 indicators
if (_indicatorSelectionPattern == null)
{
GenerateIndicatorSelectionPattern();
}
return new Gene(_indicatorSelectionPattern[geneIndex - 5]);
}
else
{
// Indicator parameters
var indicatorIndex = (geneIndex - (5 + _eligibleIndicators.Count)) / 8;
var paramIndex = (geneIndex - (5 + _eligibleIndicators.Count)) % 8;
if (indicatorIndex < _eligibleIndicators.Count)
{
var indicator = _eligibleIndicators[indicatorIndex];
var paramName = GetParameterName(paramIndex);
if (paramName != null && GeneticService.IndicatorParamMapping.ContainsKey(indicator))
{
var requiredParams = GeneticService.IndicatorParamMapping[indicator];
if (requiredParams.Contains(paramName))
{
// Use indicator-specific ranges only
if (GeneticService.IndicatorParameterRanges.ContainsKey(indicator) &&
GeneticService.IndicatorParameterRanges[indicator].ContainsKey(paramName))
{
var indicatorRange = GeneticService.IndicatorParameterRanges[indicator][paramName];
// 70% chance to use default value, 30% chance to use random value within indicator-specific range
if (_random.NextDouble() < 0.7)
{
var defaultValues = GeneticService.DefaultIndicatorValues[indicator];
return new Gene(defaultValues[paramName]);
}
else
{
return new Gene(GetRandomInRange(indicatorRange));
}
}
else
{
// If no indicator-specific range is found, use default value only
var defaultValues = GeneticService.DefaultIndicatorValues[indicator];
return new Gene(defaultValues[paramName]);
}
}
}
}
return new Gene(0);
}
}
public override IChromosome CreateNew()
{
return new TradingBotChromosome(_eligibleIndicators, _maxTakeProfit);
}
public override IChromosome Clone()
{
var clone = new TradingBotChromosome(_eligibleIndicators, _maxTakeProfit);
clone.ReplaceGenes(0, GetGenes());
// Copy the selection pattern to maintain consistency
if (_indicatorSelectionPattern != null)
{
clone._indicatorSelectionPattern = (int[])_indicatorSelectionPattern.Clone();
}
return clone;
}
public List<LightIndicator> GetSelectedIndicators()
{
var selected = new List<LightIndicator>();
var genes = GetGenes();
// Check all indicator selection slots (genes 5 to 5+N-1 where N is number of eligible indicators)
for (int i = 0; i < _eligibleIndicators.Count; i++)
{
if (genes[5 + i].Value.ToString() == "1")
{
var indicator = new LightIndicator(_eligibleIndicators[i].ToString(), _eligibleIndicators[i])
{
Type = _eligibleIndicators[i]
};
// Add parameters for this indicator
var baseIndex = 5 + _eligibleIndicators.Count + i * 8;
var paramName = GetParameterName(0); // period
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.Period = Convert.ToInt32(genes[baseIndex].Value);
}
paramName = GetParameterName(1); // fastPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.FastPeriods = Convert.ToInt32(genes[baseIndex + 1].Value);
}
paramName = GetParameterName(2); // slowPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.SlowPeriods = Convert.ToInt32(genes[baseIndex + 2].Value);
}
paramName = GetParameterName(3); // signalPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.SignalPeriods = Convert.ToInt32(genes[baseIndex + 3].Value);
}
paramName = GetParameterName(4); // multiplier
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.Multiplier = Convert.ToDouble(genes[baseIndex + 4].Value);
}
paramName = GetParameterName(5); // stochPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.StochPeriods = Convert.ToInt32(genes[baseIndex + 5].Value);
}
paramName = GetParameterName(6); // smoothPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.SmoothPeriods = Convert.ToInt32(genes[baseIndex + 6].Value);
}
paramName = GetParameterName(7); // cyclePeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.CyclePeriods = Convert.ToInt32(genes[baseIndex + 7].Value);
}
selected.Add(indicator);
}
}
return selected;
}
public TradingBotConfig GetTradingBotConfig(GeneticRequest request)
{
var genes = GetGenes();
var selectedIndicators = GetSelectedIndicators();
// The selection pattern ensures at least one indicator is always selected
// No need for fallback logic
// Get take profit from chromosome (gene 0)
var takeProfit = Convert.ToDouble(genes[0].Value);
// Get stop loss from chromosome (gene 1)
var stopLoss = Convert.ToDouble(genes[1].Value);
// Enforce proper risk-reward constraints
var minStopLoss = 0.2; // Minimum 0.2% to cover fees
var maxStopLoss = takeProfit / 1.1; // Ensure risk-reward ratio is at least 1.1:1
// Generate a random stop loss between min and max
var randomStopLoss = GetRandomInRange((minStopLoss, maxStopLoss));
// Use the random value instead of clamping the original
stopLoss = randomStopLoss;
// Log the generated values (for debugging)
Console.WriteLine($"Generated: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Get loopback period from gene 4
var loopbackPeriod = Convert.ToInt32(genes[4].Value);
// Build scenario using selected indicators
var scenario = new LightScenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod)
{
Indicators = selectedIndicators
};
var mm = new MoneyManagement
{
Name = $"Genetic_{request.RequestId}_MM",
Timeframe = request.Timeframe,
StopLoss = Convert.ToDecimal(stopLoss),
TakeProfit = Convert.ToDecimal(takeProfit),
Leverage = 1.0m
};
mm.FormatPercentage();
return new TradingBotConfig
{
Name = $"Genetic_{request.RequestId}",
AccountName = request.User.Accounts.FirstOrDefault().Name,
Ticker = request.Ticker,
Timeframe = request.Timeframe,
BotTradingBalance = request.Balance,
IsForBacktest = true,
IsForWatchingOnly = false,
CooldownPeriod = Convert.ToInt32(genes[2].Value),
MaxLossStreak = Convert.ToInt32(genes[3].Value),
FlipPosition = false,
FlipOnlyWhenInProfit = true,
CloseEarlyWhenProfitable = true,
MaxPositionTimeHours = 0, // Always 0 to prevent early position cutting
UseSynthApi = false,
UseForPositionSizing = false,
UseForSignalFiltering = false,
UseForDynamicStopLoss = false,
Scenario = scenario,
MoneyManagement = mm,
RiskManagement = new RiskManagement
{
RiskTolerance = RiskToleranceLevel.Moderate
}
};
}
private double GetRandomInRange((double min, double max) range)
{
return _random.NextDouble() * (range.max - range.min) + range.min;
}
private int GetRandomIntInRange((double min, double max) range)
{
return _random.Next((int)range.min, (int)range.max + 1);
}
private string GetParameterName(int index)
{
return index switch
{
0 => "period",
1 => "fastPeriods",
2 => "slowPeriods",
3 => "signalPeriods",
4 => "multiplier",
5 => "stochPeriods",
6 => "smoothPeriods",
7 => "cyclePeriods",
_ => null
};
}
private bool HasParameter(IndicatorType indicator, string paramName)
{
return GeneticService.IndicatorParamMapping.ContainsKey(indicator) &&
GeneticService.IndicatorParamMapping[indicator].Contains(paramName);
}
private void GenerateIndicatorSelectionPattern()
{
// Generate a random combination of up to 4 indicators
var selectedCount = _random.Next(1, Math.Min(5, _eligibleIndicators.Count + 1)); // 1 to 4 indicators
var selectedIndices = new HashSet<int>();
// Randomly select indices
while (selectedIndices.Count < selectedCount)
{
selectedIndices.Add(_random.Next(_eligibleIndicators.Count));
}
// Create the selection pattern
_indicatorSelectionPattern = new int[_eligibleIndicators.Count];
for (int i = 0; i < _eligibleIndicators.Count; i++)
{
_indicatorSelectionPattern[i] = selectedIndices.Contains(i) ? 1 : 0;
}
}
/// <summary>
/// Initializes genes with proper constraint handling for take profit and stop loss
/// </summary>
private void InitializeGenesWithConstraints()
{
// Generate take profit first (gene 0)
var takeProfit = GetRandomInRange((0.9, _maxTakeProfit));
ReplaceGene(0, new Gene(takeProfit));
// Generate stop loss with proper constraints (gene 1)
var minStopLoss = 0.2; // Minimum 0.2% to cover fees
var maxStopLoss = takeProfit / 1.1; // Ensure risk-reward ratio is at least 1.1:1
var stopLoss = GetRandomInRange((minStopLoss, maxStopLoss));
ReplaceGene(1, new Gene(stopLoss));
// Log the initial values (for debugging)
Console.WriteLine($"Initialized: TP={takeProfit:F2}%, SL={stopLoss:F2}% (RR={takeProfit / stopLoss:F2}:1)");
// Initialize remaining genes normally
for (int i = 2; i < Length; i++)
{
ReplaceGene(i, GenerateGene(i));
}
}
}
/// <summary>
/// Multi-objective fitness function for trading bot optimization
/// </summary>
public class TradingBotFitness : IFitness
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly GeneticRequest _request;
private GeneticAlgorithm _geneticAlgorithm;
private readonly ILogger<GeneticService> _logger;
private readonly HashSet<Candle> _candles;
private static readonly SemaphoreSlim _dbSemaphore = new SemaphoreSlim(4, 4); // Limit concurrent DB operations
public TradingBotFitness(
IServiceScopeFactory serviceScopeFactory,
GeneticRequest request,
HashSet<Candle> candles,
ILogger<GeneticService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_request = request;
_candles = candles;
_logger = logger;
}
public void SetGeneticAlgorithm(GeneticAlgorithm geneticAlgorithm)
{
_geneticAlgorithm = geneticAlgorithm;
}
public double Evaluate(IChromosome chromosome)
{
try
{
var tradingBotChromosome = chromosome as TradingBotChromosome;
if (tradingBotChromosome == null)
return 0;
var config = tradingBotChromosome.GetTradingBotConfig(_request);
// Run backtest synchronously using BacktestExecutor (no job creation, no DB writes)
// Use semaphore to limit concurrent database operations (for indicator calculations)
// This prevents connection pool exhaustion when running large populations
_dbSemaphore.Wait();
LightBacktest lightBacktest;
try
{
lightBacktest = ServiceScopeHelpers.WithScopedService<BacktestExecutor, LightBacktest>(
_serviceScopeFactory,
async executor => await executor.ExecuteAsync(
config,
_candles,
_request.User,
save: false, // Don't save backtest results for genetic algorithm
withCandles: false,
requestId: _request.RequestId,
bundleRequestId: null, // Genetic algorithm doesn't use bundle requests
metadata: new GeneticBacktestMetadata(_geneticAlgorithm?.GenerationsNumber ?? 0,
_request.RequestId),
progressCallback: null,
cancellationToken: default
)
).GetAwaiter().GetResult();
}
finally
{
_dbSemaphore.Release();
}
// Calculate multi-objective fitness based on backtest results
var fitness = CalculateFitness(lightBacktest, config);
return fitness;
}
catch (Exception ex)
{
_logger.LogWarning("Fitness evaluation failed for chromosome: {Message}", ex.Message);
// Return low fitness for failed backtests
return 0;
}
}
private double CalculateFitness(LightBacktest lightBacktest, TradingBotConfig config)
{
if (lightBacktest == null)
return 0.1;
// Calculate base fitness from backtest score
var baseFitness = lightBacktest.Score;
// Return base fitness (no penalty for now)
return baseFitness;
}
}