950 lines
36 KiB
C#
950 lines
36 KiB
C#
using System.Text.Json;
|
|
using GeneticSharp;
|
|
using Managing.Application.Abstractions.Repositories;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Domain.Backtests;
|
|
using Managing.Domain.Bots;
|
|
using Managing.Domain.MoneyManagements;
|
|
using Managing.Domain.Risk;
|
|
using Managing.Domain.Scenarios;
|
|
using Managing.Domain.Users;
|
|
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;
|
|
|
|
// 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)
|
|
{
|
|
_geneticRepository = geneticRepository;
|
|
_backtester = backtester;
|
|
_logger = logger;
|
|
_messengerService = messengerService;
|
|
}
|
|
|
|
public GeneticRequest CreateGeneticRequest(
|
|
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);
|
|
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 void UpdateGeneticRequest(GeneticRequest geneticRequest)
|
|
{
|
|
_geneticRepository.UpdateGeneticRequest(geneticRequest);
|
|
}
|
|
|
|
public void DeleteGeneticRequestByIdForUser(User user, string id)
|
|
{
|
|
_geneticRepository.DeleteGeneticRequestByIdForUser(user, id);
|
|
}
|
|
|
|
public IEnumerable<GeneticRequest> GetPendingGeneticRequests()
|
|
{
|
|
return _geneticRepository.GetPendingGeneticRequests();
|
|
}
|
|
|
|
/// <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;
|
|
UpdateGeneticRequest(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);
|
|
}
|
|
|
|
// Create fitness function first
|
|
var fitness = new TradingBotFitness(_backtester, request, _logger);
|
|
|
|
// Create genetic algorithm with better configuration
|
|
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 = 4,
|
|
MaxThreads = Environment.ProcessorCount
|
|
}
|
|
};
|
|
|
|
// 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 += (sender, e) =>
|
|
{
|
|
generationCount = ga.GenerationsNumber;
|
|
|
|
// Update progress every generation
|
|
var bestFitness = ga.BestChromosome?.Fitness ?? 0;
|
|
request.CurrentGeneration = generationCount;
|
|
request.BestFitnessSoFar = bestFitness;
|
|
|
|
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();
|
|
request.BestChromosome = JsonSerializer.Serialize(geneValues);
|
|
}
|
|
|
|
UpdateGeneticRequest(request);
|
|
|
|
// Check for cancellation
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
ga.Stop();
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
UpdateGeneticRequest(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 results
|
|
request.Status = GeneticRequestStatus.Completed;
|
|
request.CompletedAt = DateTime.UtcNow;
|
|
request.BestFitness = bestFitness;
|
|
request.BestIndividual = bestChromosome?.ToString() ?? "unknown";
|
|
request.ProgressInfo = JsonSerializer.Serialize(new
|
|
{
|
|
generation = ga.GenerationsNumber,
|
|
best_fitness = bestFitness,
|
|
population_size = request.PopulationSize,
|
|
generations = request.Generations,
|
|
completed_at = DateTime.UtcNow
|
|
});
|
|
|
|
UpdateGeneticRequest(request);
|
|
|
|
// 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;
|
|
UpdateGeneticRequest(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<GeneticIndicator> GetSelectedIndicators()
|
|
{
|
|
var selected = new List<GeneticIndicator>();
|
|
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 GeneticIndicator
|
|
{
|
|
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 Scenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod);
|
|
|
|
foreach (var geneticIndicator in selectedIndicators)
|
|
{
|
|
var indicator = ScenarioHelpers.BuildIndicator(
|
|
type: geneticIndicator.Type,
|
|
name: $"Genetic_{geneticIndicator.Type}_{Guid.NewGuid():N}",
|
|
period: geneticIndicator.Period,
|
|
fastPeriods: geneticIndicator.FastPeriods,
|
|
slowPeriods: geneticIndicator.SlowPeriods,
|
|
signalPeriods: geneticIndicator.SignalPeriods,
|
|
multiplier: geneticIndicator.Multiplier,
|
|
stochPeriods: geneticIndicator.StochPeriods,
|
|
smoothPeriods: geneticIndicator.SmoothPeriods,
|
|
cyclePeriods: geneticIndicator.CyclePeriods
|
|
);
|
|
|
|
scenario.AddIndicator(indicator);
|
|
}
|
|
|
|
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 = "Oda-embedded",
|
|
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>
|
|
/// Genetic indicator with parameters
|
|
/// </summary>
|
|
public class GeneticIndicator
|
|
{
|
|
public IndicatorType Type { get; set; }
|
|
public int? Period { get; set; }
|
|
public int? FastPeriods { get; set; }
|
|
public int? SlowPeriods { get; set; }
|
|
public int? SignalPeriods { get; set; }
|
|
public double? Multiplier { get; set; }
|
|
public int? StochPeriods { get; set; }
|
|
public int? SmoothPeriods { get; set; }
|
|
public int? CyclePeriods { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Multi-objective fitness function for trading bot optimization
|
|
/// </summary>
|
|
public class TradingBotFitness : IFitness
|
|
{
|
|
private readonly IBacktester _backtester;
|
|
private readonly GeneticRequest _request;
|
|
private GeneticAlgorithm _geneticAlgorithm;
|
|
private readonly ILogger<GeneticService> _logger;
|
|
|
|
public TradingBotFitness(IBacktester backtester, GeneticRequest request, ILogger<GeneticService> logger)
|
|
{
|
|
_backtester = backtester;
|
|
_request = request;
|
|
_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);
|
|
|
|
// Get current generation number (default to 0 if not available)
|
|
var currentGeneration = _geneticAlgorithm?.GenerationsNumber ?? 0;
|
|
|
|
// Run backtest
|
|
var backtest = _backtester.RunTradingBotBacktest(
|
|
config,
|
|
_request.StartDate,
|
|
_request.EndDate,
|
|
_request.User,
|
|
true,
|
|
false, // Don't include candles
|
|
_request.RequestId,
|
|
new
|
|
{
|
|
generation = currentGeneration
|
|
}
|
|
).Result;
|
|
|
|
// Calculate multi-objective fitness based on backtest results
|
|
var fitness = CalculateFitness(backtest, config);
|
|
|
|
return fitness;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Return low fitness for failed backtests
|
|
return 0.1;
|
|
}
|
|
}
|
|
|
|
private double CalculateFitness(Backtest backtest, TradingBotConfig config)
|
|
{
|
|
if (backtest == null || backtest.Statistics == null)
|
|
return 0.1;
|
|
|
|
// Calculate base fitness from backtest score
|
|
var baseFitness = backtest.Score;
|
|
|
|
// Return base fitness (no penalty for now)
|
|
return baseFitness;
|
|
}
|
|
} |