Fix genetic backend

This commit is contained in:
2025-07-11 14:11:41 +07:00
parent d04d8f310d
commit e43a1af5ef
15 changed files with 542 additions and 205 deletions

View File

@@ -105,5 +105,6 @@ Key Principles
- Do not update ManagingApi.ts, once you made a change on the backend endpoint, execute the command to regenerate ManagingApi.ts on the frontend; cd src/Managing.Nswag && dotnet build - Do not update ManagingApi.ts, once you made a change on the backend endpoint, execute the command to regenerate ManagingApi.ts on the frontend; cd src/Managing.Nswag && dotnet build
- Do not reference new react library if a component already exist in mollecules or atoms - Do not reference new react library if a component already exist in mollecules or atoms
- After finishing the editing, build the project - After finishing the editing, build the project
- you have to pass from controller -> application -> repository, do not inject repository inside controllers
Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components. Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components.

View File

@@ -25,10 +25,10 @@
"Uri": "http://localhost:9200" "Uri": "http://localhost:9200"
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"WorkerPricesFifteenMinutes": true, "WorkerPricesFifteenMinutes": false,
"WorkerPricesOneHour": true, "WorkerPricesOneHour": false,
"WorkerPricesFourHours": true, "WorkerPricesFourHours": false,
"WorkerPricesOneDay": true, "WorkerPricesOneDay": false,
"WorkerPricesFiveMinutes": false, "WorkerPricesFiveMinutes": false,
"WorkerFee": false, "WorkerFee": false,
"WorkerPositionManager": false, "WorkerPositionManager": false,

View File

@@ -103,6 +103,25 @@ public class BacktestController : BaseController
return Ok(_backtester.DeleteBacktestByUser(user, id)); return Ok(_backtester.DeleteBacktestByUser(user, id));
} }
/// <summary>
/// Retrieves all backtests for a specific genetic request ID.
/// This endpoint is used to view the results of a genetic algorithm optimization.
/// </summary>
/// <param name="requestId">The request ID to filter backtests by.</param>
/// <returns>A list of backtests associated with the specified request ID.</returns>
[HttpGet]
[Route("ByRequestId/{requestId}")]
public async Task<ActionResult<IEnumerable<Backtest>>> GetBacktestsByRequestId(string requestId)
{
if (string.IsNullOrEmpty(requestId))
{
return BadRequest("Request ID is required");
}
var backtests = _backtester.GetBacktestsByRequestId(requestId);
return Ok(backtests);
}
/// <summary> /// <summary>
/// Runs a backtest with the specified configuration. /// Runs a backtest with the specified configuration.
/// The returned backtest includes a complete TradingBotConfig that preserves all /// The returned backtest includes a complete TradingBotConfig that preserves all

View File

@@ -7,6 +7,7 @@ public interface IBacktestRepository
{ {
void InsertBacktestForUser(User user, Backtest result); void InsertBacktestForUser(User user, Backtest result);
IEnumerable<Backtest> GetBacktestsByUser(User user); IEnumerable<Backtest> GetBacktestsByUser(User user);
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
Backtest GetBacktestByIdForUser(User user, string id); Backtest GetBacktestByIdForUser(User user, string id);
void DeleteBacktestByIdForUser(User user, string id); void DeleteBacktestByIdForUser(User user, string id);
void DeleteAllBacktestsForUser(User user); void DeleteAllBacktestsForUser(User user);

View File

@@ -49,6 +49,7 @@ namespace Managing.Application.Abstractions.Services
bool DeleteBacktest(string id); bool DeleteBacktest(string id);
bool DeleteBacktests(); bool DeleteBacktests();
IEnumerable<Backtest> GetBacktestsByUser(User user); IEnumerable<Backtest> GetBacktestsByUser(User user);
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
Backtest GetBacktestByIdForUser(User user, string id); Backtest GetBacktestByIdForUser(User user, string id);
bool DeleteBacktestByUser(User user, string id); bool DeleteBacktestByUser(User user, string id);
bool DeleteBacktestsByUser(User user); bool DeleteBacktestsByUser(User user);

View File

@@ -79,6 +79,7 @@ public interface IGeneticService
/// Runs the genetic algorithm for a specific request /// Runs the genetic algorithm for a specific request
/// </summary> /// </summary>
/// <param name="request">The genetic request to process</param> /// <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> /// <returns>The genetic algorithm result</returns>
Task<GeneticAlgorithmResult> RunGeneticAlgorithm(GeneticRequest request); Task<GeneticAlgorithmResult> RunGeneticAlgorithm(GeneticRequest request, CancellationToken cancellationToken = default);
} }

View File

@@ -70,7 +70,7 @@ public class GeneticAlgorithmWorker : BaseWorker<GeneticAlgorithmWorker>
_geneticService.UpdateGeneticRequest(request); _geneticService.UpdateGeneticRequest(request);
// Run genetic algorithm using the service // Run genetic algorithm using the service
var results = await _geneticService.RunGeneticAlgorithm(request); var results = await _geneticService.RunGeneticAlgorithm(request, cancellationToken);
// Update request with results // Update request with results
request.Status = GeneticRequestStatus.Completed; request.Status = GeneticRequestStatus.Completed;

View File

@@ -80,8 +80,7 @@ namespace Managing.Application.Backtesting
bool withCandles = false, bool withCandles = false,
string requestId = null) string requestId = null)
{ {
var account = await GetAccountFromConfig(config); var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
var candles = GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate);
var result = await RunBacktestWithCandles(config, candles, user, withCandles, requestId); var result = await RunBacktestWithCandles(config, candles, user, withCandles, requestId);
@@ -165,7 +164,7 @@ namespace Managing.Application.Backtesting
}; };
} }
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe, private List<Candle> GetCandles(Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate) DateTime startDate, DateTime endDate)
{ {
var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,
@@ -400,6 +399,12 @@ namespace Managing.Application.Backtesting
return backtests; return backtests;
} }
public IEnumerable<Backtest> GetBacktestsByRequestId(string requestId)
{
var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList();
return backtests;
}
public Backtest GetBacktestByIdForUser(User user, string id) public Backtest GetBacktestByIdForUser(User user, string id)
{ {
var backtest = _backtestRepository.GetBacktestByIdForUser(user, id); var backtest = _backtestRepository.GetBacktestByIdForUser(user, id);
@@ -462,7 +467,5 @@ namespace Managing.Application.Backtesting
return false; return false;
} }
} }
} }
} }

View File

@@ -42,104 +42,127 @@ public class GeneticService : IGeneticService
[IndicatorType.EmaTrend] = new() { ["period"] = 14.0 }, [IndicatorType.EmaTrend] = new() { ["period"] = 14.0 },
[IndicatorType.StDev] = new() { ["period"] = 14.0 }, [IndicatorType.StDev] = new() { ["period"] = 14.0 },
[IndicatorType.ThreeWhiteSoldiers] = new() { ["period"] = 14.0 }, [IndicatorType.ThreeWhiteSoldiers] = new() { ["period"] = 14.0 },
[IndicatorType.MacdCross] = new() { [IndicatorType.MacdCross] = new()
["fastPeriods"] = 12.0, {
["slowPeriods"] = 26.0, ["fastPeriods"] = 12.0,
["signalPeriods"] = 9.0 ["slowPeriods"] = 26.0,
["signalPeriods"] = 9.0
}, },
[IndicatorType.DualEmaCross] = new() { [IndicatorType.DualEmaCross] = new()
["fastPeriods"] = 12.0, {
["slowPeriods"] = 26.0 ["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
}, },
[IndicatorType.SuperTrend] = new() { [IndicatorType.SuperTrend] = new()
["period"] = 14.0, {
["multiplier"] = 3.0 ["period"] = 14.0,
["multiplier"] = 3.0
}, },
[IndicatorType.SuperTrendCrossEma] = new() { [IndicatorType.SuperTrendCrossEma] = new()
["period"] = 14.0, {
["multiplier"] = 3.0 ["period"] = 14.0,
["multiplier"] = 3.0
}, },
[IndicatorType.ChandelierExit] = new() { [IndicatorType.ChandelierExit] = new()
["period"] = 14.0, {
["multiplier"] = 3.0 ["period"] = 14.0,
["multiplier"] = 3.0
}, },
[IndicatorType.StochRsiTrend] = new() { [IndicatorType.StochRsiTrend] = new()
["period"] = 14.0, {
["stochPeriods"] = 14.0, ["period"] = 14.0,
["signalPeriods"] = 9.0, ["stochPeriods"] = 14.0,
["smoothPeriods"] = 3.0 ["signalPeriods"] = 9.0,
["smoothPeriods"] = 3.0
}, },
[IndicatorType.Stc] = new() { [IndicatorType.Stc] = new()
["cyclePeriods"] = 10.0, {
["fastPeriods"] = 12.0, ["cyclePeriods"] = 10.0,
["slowPeriods"] = 26.0 ["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
}, },
[IndicatorType.LaggingStc] = new() { [IndicatorType.LaggingStc] = new()
["cyclePeriods"] = 10.0, {
["fastPeriods"] = 12.0, ["cyclePeriods"] = 10.0,
["slowPeriods"] = 26.0 ["fastPeriods"] = 12.0,
["slowPeriods"] = 26.0
} }
}; };
// Indicator-specific parameter ranges // Indicator-specific parameter ranges
public static readonly Dictionary<IndicatorType, Dictionary<string, (double min, double max)>> IndicatorParameterRanges = new() public static readonly Dictionary<IndicatorType, Dictionary<string, (double min, double max)>>
{ IndicatorParameterRanges = new()
[IndicatorType.RsiDivergence] = new() { {
["period"] = (5.0, 50.0) [IndicatorType.RsiDivergence] = new()
}, {
[IndicatorType.RsiDivergenceConfirm] = new() { ["period"] = (5.0, 50.0)
["period"] = (5.0, 50.0) },
}, [IndicatorType.RsiDivergenceConfirm] = new()
[IndicatorType.EmaCross] = new() { {
["period"] = (5.0, 200.0) ["period"] = (5.0, 50.0)
}, },
[IndicatorType.EmaTrend] = new() { [IndicatorType.EmaCross] = new()
["period"] = (5.0, 200.0) {
}, ["period"] = (5.0, 200.0)
[IndicatorType.StDev] = new() { },
["period"] = (5.0, 50.0) [IndicatorType.EmaTrend] = new()
}, {
[IndicatorType.ThreeWhiteSoldiers] = new() { ["period"] = (5.0, 200.0)
["period"] = (5.0, 50.0) },
}, [IndicatorType.StDev] = new()
[IndicatorType.MacdCross] = new() { {
["fastPeriods"] = (10.0, 50.0), ["period"] = (5.0, 50.0)
["slowPeriods"] = (20.0, 100.0), },
["signalPeriods"] = (5.0, 20.0) [IndicatorType.ThreeWhiteSoldiers] = new()
}, {
[IndicatorType.DualEmaCross] = new() { ["period"] = (5.0, 50.0)
["fastPeriods"] = (5.0, 300.0), },
["slowPeriods"] = (5.0, 300.0) [IndicatorType.MacdCross] = new()
}, {
[IndicatorType.SuperTrend] = new() { ["fastPeriods"] = (10.0, 50.0),
["period"] = (5.0, 50.0), ["slowPeriods"] = (20.0, 100.0),
["multiplier"] = (1.0, 10.0) ["signalPeriods"] = (5.0, 20.0)
}, },
[IndicatorType.SuperTrendCrossEma] = new() { [IndicatorType.DualEmaCross] = new()
["period"] = (5.0, 50.0), {
["multiplier"] = (1.0, 10.0) ["fastPeriods"] = (5.0, 300.0),
}, ["slowPeriods"] = (5.0, 300.0)
[IndicatorType.ChandelierExit] = new() { },
["period"] = (5.0, 50.0), [IndicatorType.SuperTrend] = new()
["multiplier"] = (1.0, 10.0) {
}, ["period"] = (5.0, 50.0),
[IndicatorType.StochRsiTrend] = new() { ["multiplier"] = (1.0, 10.0)
["period"] = (5.0, 50.0), },
["stochPeriods"] = (5.0, 30.0), [IndicatorType.SuperTrendCrossEma] = new()
["signalPeriods"] = (3.0, 15.0), {
["smoothPeriods"] = (1.0, 10.0) ["period"] = (5.0, 50.0),
}, ["multiplier"] = (1.0, 10.0)
[IndicatorType.Stc] = new() { },
["cyclePeriods"] = (5.0, 30.0), [IndicatorType.ChandelierExit] = new()
["fastPeriods"] = (5.0, 50.0), {
["slowPeriods"] = (10.0, 100.0) ["period"] = (5.0, 50.0),
}, ["multiplier"] = (1.0, 10.0)
[IndicatorType.LaggingStc] = new() { },
["cyclePeriods"] = (5.0, 30.0), [IndicatorType.StochRsiTrend] = new()
["fastPeriods"] = (5.0, 50.0), {
["slowPeriods"] = (10.0, 100.0) ["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, 30.0),
["fastPeriods"] = (5.0, 50.0),
["slowPeriods"] = (10.0, 100.0)
},
[IndicatorType.LaggingStc] = new()
{
["cyclePeriods"] = (5.0, 30.0),
["fastPeriods"] = (5.0, 50.0),
["slowPeriods"] = (10.0, 100.0)
}
};
// Indicator type to parameter mapping // Indicator type to parameter mapping
public static readonly Dictionary<IndicatorType, string[]> IndicatorParamMapping = new() public static readonly Dictionary<IndicatorType, string[]> IndicatorParamMapping = new()
@@ -236,8 +259,10 @@ public class GeneticService : IGeneticService
/// Runs the genetic algorithm for a specific request /// Runs the genetic algorithm for a specific request
/// </summary> /// </summary>
/// <param name="request">The genetic request to process</param> /// <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> /// <returns>The genetic algorithm result</returns>
public async Task<GeneticAlgorithmResult> RunGeneticAlgorithm(GeneticRequest request) public async Task<GeneticAlgorithmResult> RunGeneticAlgorithm(GeneticRequest request,
CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -247,15 +272,39 @@ public class GeneticService : IGeneticService
request.Status = GeneticRequestStatus.Running; request.Status = GeneticRequestStatus.Running;
UpdateGeneticRequest(request); UpdateGeneticRequest(request);
// Create chromosome for trading bot configuration // Create or resume chromosome for trading bot configuration
var chromosome = new TradingBotChromosome(request.EligibleIndicators, request.MaxTakeProfit); 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 // Create fitness function
var fitness = new TradingBotFitness(_backtester, request); var fitness = new TradingBotFitness(_backtester, request);
// Create genetic algorithm with better configuration // Create genetic algorithm with better configuration
var ga = new GeneticAlgorithm( var ga = new GeneticAlgorithm(
new Population(request.PopulationSize, request.PopulationSize, chromosome), population,
fitness, fitness,
GetSelection(request.SelectionMethod), GetSelection(request.SelectionMethod),
new UniformCrossover(), new UniformCrossover(),
@@ -266,14 +315,73 @@ public class GeneticService : IGeneticService
CrossoverProbability = 0.7f // Fixed crossover rate as in frontend CrossoverProbability = 0.7f // Fixed crossover rate as in frontend
}; };
// 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 5 generations
if (generationCount % 5 == 0)
{
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 // Run the genetic algorithm
ga.Start(); 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 // Get the best chromosome
var bestChromosome = ga.BestChromosome as TradingBotChromosome; var bestChromosome = ga.BestChromosome as TradingBotChromosome;
var bestFitness = ga.BestChromosome.Fitness.Value; var bestFitness = ga.BestChromosome?.Fitness ?? 0;
_logger.LogInformation("Genetic algorithm completed for request {RequestId}. Best fitness: {Fitness}", _logger.LogInformation("Genetic algorithm completed for request {RequestId}. Best fitness: {Fitness}",
request.RequestId, bestFitness); request.RequestId, bestFitness);
// Update request with results // Update request with results
@@ -289,7 +397,7 @@ public class GeneticService : IGeneticService
generations = request.Generations, generations = request.Generations,
completed_at = DateTime.UtcNow completed_at = DateTime.UtcNow
}); });
UpdateGeneticRequest(request); UpdateGeneticRequest(request);
return new GeneticAlgorithmResult return new GeneticAlgorithmResult
@@ -303,13 +411,13 @@ public class GeneticService : IGeneticService
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error running genetic algorithm for request {RequestId}", request.RequestId); _logger.LogError(ex, "Error running genetic algorithm for request {RequestId}", request.RequestId);
// Update request with error // Update request with error
request.Status = GeneticRequestStatus.Failed; request.Status = GeneticRequestStatus.Failed;
request.ErrorMessage = ex.Message; request.ErrorMessage = ex.Message;
request.CompletedAt = DateTime.UtcNow; request.CompletedAt = DateTime.UtcNow;
UpdateGeneticRequest(request); UpdateGeneticRequest(request);
throw; throw;
} }
} }
@@ -345,11 +453,18 @@ public class TradingBotChromosome : ChromosomeBase
// 3-6: Indicator selection (up to 4 indicators) // 3-6: Indicator selection (up to 4 indicators)
// 7+: Indicator parameters (period, fastPeriods, etc.) // 7+: Indicator parameters (period, fastPeriods, etc.)
public TradingBotChromosome(List<IndicatorType> eligibleIndicators, double maxTakeProfit) public TradingBotChromosome(List<IndicatorType> eligibleIndicators, double maxTakeProfit)
: base(4 + 4 + eligibleIndicators.Count * 8) // Trading params + indicator selection + indicator params : base(4 + 1 + 4 +
eligibleIndicators.Count * 8) // Trading params + loopback + indicator selection + indicator params
{ {
_eligibleIndicators = eligibleIndicators; _eligibleIndicators = eligibleIndicators;
_maxTakeProfit = maxTakeProfit; _maxTakeProfit = maxTakeProfit;
// Initialize all genes
for (int i = 0; i < Length; i++)
{
ReplaceGene(i, GenerateGene(i));
}
} }
public override Gene GenerateGene(int geneIndex) public override Gene GenerateGene(int geneIndex)
@@ -366,7 +481,12 @@ public class TradingBotChromosome : ChromosomeBase
_ => new Gene(0) _ => new Gene(0)
}; };
} }
else if (geneIndex < 8) else if (geneIndex == 4)
{
// LoopbackPeriod gene (always between 5 and 20)
return new Gene(GetRandomIntInRange((5, 20)));
}
else if (geneIndex < 9)
{ {
// Indicator selection (0 = not selected, 1 = selected) // Indicator selection (0 = not selected, 1 = selected)
return new Gene(_random.Next(2)); return new Gene(_random.Next(2));
@@ -374,46 +494,46 @@ public class TradingBotChromosome : ChromosomeBase
else else
{ {
// Indicator parameters // Indicator parameters
var indicatorIndex = (geneIndex - 8) / 8; var indicatorIndex = (geneIndex - 9) / 8;
var paramIndex = (geneIndex - 8) % 8; var paramIndex = (geneIndex - 9) % 8;
if (indicatorIndex < _eligibleIndicators.Count) if (indicatorIndex < _eligibleIndicators.Count)
{ {
var indicator = _eligibleIndicators[indicatorIndex]; var indicator = _eligibleIndicators[indicatorIndex];
var paramName = GetParameterName(paramIndex); var paramName = GetParameterName(paramIndex);
if (paramName != null && GeneticService.IndicatorParamMapping.ContainsKey(indicator)) if (paramName != null && GeneticService.IndicatorParamMapping.ContainsKey(indicator))
{ {
var requiredParams = GeneticService.IndicatorParamMapping[indicator]; var requiredParams = GeneticService.IndicatorParamMapping[indicator];
if (requiredParams.Contains(paramName)) if (requiredParams.Contains(paramName))
{ {
// Use indicator-specific ranges only // Use indicator-specific ranges only
if (GeneticService.IndicatorParameterRanges.ContainsKey(indicator) && if (GeneticService.IndicatorParameterRanges.ContainsKey(indicator) &&
GeneticService.IndicatorParameterRanges[indicator].ContainsKey(paramName)) GeneticService.IndicatorParameterRanges[indicator].ContainsKey(paramName))
{ {
var indicatorRange = GeneticService.IndicatorParameterRanges[indicator][paramName]; var indicatorRange = GeneticService.IndicatorParameterRanges[indicator][paramName];
// 70% chance to use default value, 30% chance to use random value within indicator-specific range // 70% chance to use default value, 30% chance to use random value within indicator-specific range
if (_random.NextDouble() < 0.7) if (_random.NextDouble() < 0.7)
{ {
var defaultValues = GeneticService.DefaultIndicatorValues[indicator]; var defaultValues = GeneticService.DefaultIndicatorValues[indicator];
return new Gene(defaultValues[paramName]); return new Gene(defaultValues[paramName]);
} }
else else
{ {
return new Gene(GetRandomInRange(indicatorRange)); return new Gene(GetRandomInRange(indicatorRange));
} }
} }
else else
{ {
// If no indicator-specific range is found, use default value only // If no indicator-specific range is found, use default value only
var defaultValues = GeneticService.DefaultIndicatorValues[indicator]; var defaultValues = GeneticService.DefaultIndicatorValues[indicator];
return new Gene(defaultValues[paramName]); return new Gene(defaultValues[paramName]);
} }
} }
} }
} }
return new Gene(0); return new Gene(0);
} }
} }
@@ -434,70 +554,70 @@ public class TradingBotChromosome : ChromosomeBase
{ {
var selected = new List<GeneticIndicator>(); var selected = new List<GeneticIndicator>();
var genes = GetGenes(); var genes = GetGenes();
for (int i = 0; i < 4; i++) // Check first 4 indicator slots for (int i = 0; i < 4; i++) // Check first 4 indicator slots
{ {
if (genes[4 + i].Value.ToString() == "1" && i < _eligibleIndicators.Count) if (genes[5 + i].Value.ToString() == "1" && i < _eligibleIndicators.Count)
{ {
var indicator = new GeneticIndicator var indicator = new GeneticIndicator
{ {
Type = _eligibleIndicators[i] Type = _eligibleIndicators[i]
}; };
// Add parameters for this indicator // Add parameters for this indicator
var baseIndex = 8 + i * 8; var baseIndex = 9 + i * 8;
var paramName = GetParameterName(0); // period var paramName = GetParameterName(0); // period
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName)) if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{ {
indicator.Period = Convert.ToInt32(genes[baseIndex].Value); indicator.Period = Convert.ToInt32(genes[baseIndex].Value);
} }
paramName = GetParameterName(1); // fastPeriods paramName = GetParameterName(1); // fastPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName)) if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{ {
indicator.FastPeriods = Convert.ToInt32(genes[baseIndex + 1].Value); indicator.FastPeriods = Convert.ToInt32(genes[baseIndex + 1].Value);
} }
paramName = GetParameterName(2); // slowPeriods paramName = GetParameterName(2); // slowPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName)) if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{ {
indicator.SlowPeriods = Convert.ToInt32(genes[baseIndex + 2].Value); indicator.SlowPeriods = Convert.ToInt32(genes[baseIndex + 2].Value);
} }
paramName = GetParameterName(3); // signalPeriods paramName = GetParameterName(3); // signalPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName)) if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{ {
indicator.SignalPeriods = Convert.ToInt32(genes[baseIndex + 3].Value); indicator.SignalPeriods = Convert.ToInt32(genes[baseIndex + 3].Value);
} }
paramName = GetParameterName(4); // multiplier paramName = GetParameterName(4); // multiplier
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName)) if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{ {
indicator.Multiplier = Convert.ToDouble(genes[baseIndex + 4].Value); indicator.Multiplier = Convert.ToDouble(genes[baseIndex + 4].Value);
} }
paramName = GetParameterName(5); // stochPeriods paramName = GetParameterName(5); // stochPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName)) if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{ {
indicator.StochPeriods = Convert.ToInt32(genes[baseIndex + 5].Value); indicator.StochPeriods = Convert.ToInt32(genes[baseIndex + 5].Value);
} }
paramName = GetParameterName(6); // smoothPeriods paramName = GetParameterName(6); // smoothPeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName)) if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{ {
indicator.SmoothPeriods = Convert.ToInt32(genes[baseIndex + 6].Value); indicator.SmoothPeriods = Convert.ToInt32(genes[baseIndex + 6].Value);
} }
paramName = GetParameterName(7); // cyclePeriods paramName = GetParameterName(7); // cyclePeriods
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName)) if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{ {
indicator.CyclePeriods = Convert.ToInt32(genes[baseIndex + 7].Value); indicator.CyclePeriods = Convert.ToInt32(genes[baseIndex + 7].Value);
} }
selected.Add(indicator); selected.Add(indicator);
} }
} }
return selected; return selected;
} }
@@ -505,7 +625,7 @@ public class TradingBotChromosome : ChromosomeBase
{ {
var genes = GetGenes(); var genes = GetGenes();
var selectedIndicators = GetSelectedIndicators(); var selectedIndicators = GetSelectedIndicators();
// Ensure we have at least one indicator // Ensure we have at least one indicator
if (!selectedIndicators.Any()) if (!selectedIndicators.Any())
{ {
@@ -514,25 +634,28 @@ public class TradingBotChromosome : ChromosomeBase
// Get take profit from chromosome (gene 0) // Get take profit from chromosome (gene 0)
var takeProfit = Convert.ToDouble(genes[0].Value); var takeProfit = Convert.ToDouble(genes[0].Value);
// Calculate stop loss based on 1.1:1 risk-reward ratio (gene 1) // Calculate stop loss based on 1.1:1 risk-reward ratio (gene 1)
var stopLoss = Convert.ToDouble(genes[1].Value); var stopLoss = Convert.ToDouble(genes[1].Value);
// Ensure minimum 1.1:1 risk-reward ratio and minimum 0.2% to cover fees // Ensure minimum 1.1:1 risk-reward ratio and minimum 0.2% to cover fees
var minStopLossForRR = takeProfit / 1.1; var minStopLossForRR = takeProfit / 1.1;
var minStopLossForFees = 0.2; // Minimum 0.2% to cover trading fees var minStopLossForFees = 0.2; // Minimum 0.2% to cover trading fees
var minStopLoss = Math.Max(minStopLossForRR, minStopLossForFees); var minStopLoss = Math.Max(minStopLossForRR, minStopLossForFees);
var maxStopLoss = takeProfit - 0.1; // Ensure SL is less than TP with some buffer var maxStopLoss = takeProfit - 0.1; // Ensure SL is less than TP with some buffer
// Adjust stop loss if it doesn't meet the constraints // Adjust stop loss if it doesn't meet the constraints
if (stopLoss > maxStopLoss || stopLoss < minStopLoss) if (stopLoss > maxStopLoss || stopLoss < minStopLoss)
{ {
stopLoss = GetRandomInRange((minStopLoss, maxStopLoss)); stopLoss = GetRandomInRange((minStopLoss, maxStopLoss));
} }
// Get loopback period from gene 4
var loopbackPeriod = Convert.ToInt32(genes[4].Value);
// Build scenario using selected indicators // Build scenario using selected indicators
var scenario = new Scenario($"Genetic_{request.RequestId}_Scenario", 1); var scenario = new Scenario($"Genetic_{request.RequestId}_Scenario", loopbackPeriod);
foreach (var geneticIndicator in selectedIndicators) foreach (var geneticIndicator in selectedIndicators)
{ {
var indicator = ScenarioHelpers.BuildIndicator( var indicator = ScenarioHelpers.BuildIndicator(
@@ -547,14 +670,14 @@ public class TradingBotChromosome : ChromosomeBase
smoothPeriods: geneticIndicator.SmoothPeriods, smoothPeriods: geneticIndicator.SmoothPeriods,
cyclePeriods: geneticIndicator.CyclePeriods cyclePeriods: geneticIndicator.CyclePeriods
); );
scenario.AddIndicator(indicator); scenario.AddIndicator(indicator);
} }
return new TradingBotConfig return new TradingBotConfig
{ {
Name = $"Genetic_{request.RequestId}", Name = $"Genetic_{request.RequestId}",
AccountName = "genetic_account", AccountName = "Oda-embedded",
Ticker = request.Ticker, Ticker = request.Ticker,
Timeframe = request.Timeframe, Timeframe = request.Timeframe,
BotTradingBalance = request.Balance, BotTradingBalance = request.Balance,
@@ -612,11 +735,11 @@ public class TradingBotChromosome : ChromosomeBase
}; };
} }
private bool HasParameter(IndicatorType indicator, string paramName) private bool HasParameter(IndicatorType indicator, string paramName)
{ {
return GeneticService.IndicatorParamMapping.ContainsKey(indicator) && return GeneticService.IndicatorParamMapping.ContainsKey(indicator) &&
GeneticService.IndicatorParamMapping[indicator].Contains(paramName); GeneticService.IndicatorParamMapping[indicator].Contains(paramName);
} }
} }
/// <summary> /// <summary>
@@ -658,21 +781,21 @@ public class TradingBotFitness : IFitness
return 0; return 0;
var config = tradingBotChromosome.GetTradingBotConfig(_request); var config = tradingBotChromosome.GetTradingBotConfig(_request);
// Run backtest // Run backtest
var backtest = _backtester.RunTradingBotBacktest( var backtest = _backtester.RunTradingBotBacktest(
config, config,
_request.StartDate, _request.StartDate,
_request.EndDate, _request.EndDate,
_request.User, _request.User,
false, // Don't save individual backtests true, // Don't save individual backtests
false, // Don't include candles false, // Don't include candles
_request.RequestId _request.RequestId
).Result; ).Result;
// Calculate multi-objective fitness based on backtest results // Calculate multi-objective fitness based on backtest results
var fitness = CalculateMultiObjectiveFitness(backtest, config); var fitness = CalculateMultiObjectiveFitness(backtest, config);
return fitness; return fitness;
} }
catch (Exception) catch (Exception)
@@ -688,30 +811,32 @@ public class TradingBotFitness : IFitness
return 0.1; return 0.1;
var stats = backtest.Statistics; var stats = backtest.Statistics;
// Multi-objective fitness function (matching frontend) // Multi-objective fitness function (matching frontend)
var pnlScore = Math.Max(0, (double)stats.TotalPnL / 1000); // Normalize PnL var pnlScore = Math.Max(0, (double)stats.TotalPnL / 1000); // Normalize PnL
var winRateScore = backtest.WinRate / 100.0; // Normalize win rate var winRateScore = backtest.WinRate / 100.0; // Normalize win rate
var riskRewardScore = Math.Min(2, (double)stats.WinningTrades / Math.Max(1, Math.Abs((double)stats.LoosingTrades))); var riskRewardScore =
var consistencyScore = 1 - Math.Abs((double)stats.TotalPnL - (double)backtest.FinalPnl) / Math.Max(1, Math.Abs((double)stats.TotalPnL)); Math.Min(2, (double)stats.WinningTrades / Math.Max(1, Math.Abs((double)stats.LoosingTrades)));
var consistencyScore = 1 - Math.Abs((double)stats.TotalPnL - (double)backtest.FinalPnl) /
// Risk-reward ratio bonus Math.Max(1, Math.Abs((double)stats.TotalPnL));
var riskRewardRatio = (double)(config.MoneyManagement.TakeProfit / config.MoneyManagement.StopLoss);
var riskRewardBonus = Math.Min(0.2, (riskRewardRatio - 1.1) * 0.1); // Risk-reward ratio bonus
var riskRewardRatio = (double)(config.MoneyManagement.TakeProfit / config.MoneyManagement.StopLoss);
// Drawdown score (normalized to 0-1, where lower drawdown is better) var riskRewardBonus = Math.Min(0.2, (riskRewardRatio - 1.1) * 0.1);
var maxDrawdownPc = Math.Abs((double)stats.MaxDrawdownPc);
var drawdownScore = Math.Max(0, 1 - (maxDrawdownPc / 50)); // Drawdown score (normalized to 0-1, where lower drawdown is better)
var maxDrawdownPc = Math.Abs((double)stats.MaxDrawdownPc);
// Weighted combination var drawdownScore = Math.Max(0, 1 - (maxDrawdownPc / 50));
var fitness =
pnlScore * 0.3 + // Weighted combination
winRateScore * 0.2 + var fitness =
riskRewardScore * 0.2 + pnlScore * 0.3 +
consistencyScore * 0.1 + winRateScore * 0.2 +
riskRewardBonus * 0.1 + riskRewardScore * 0.2 +
drawdownScore * 0.1; consistencyScore * 0.1 +
riskRewardBonus * 0.1 +
drawdownScore * 0.1;
return Math.Max(0, fitness); return Math.Max(0, fitness);
} }
} }

View File

@@ -154,6 +154,21 @@ public class GeneticRequest
/// Progress information (JSON serialized) /// Progress information (JSON serialized)
/// </summary> /// </summary>
public string? ProgressInfo { get; set; } public string? ProgressInfo { get; set; }
/// <summary>
/// The best chromosome found so far (JSON serialized)
/// </summary>
public string? BestChromosome { get; set; }
/// <summary>
/// Current generation number when the algorithm was stopped
/// </summary>
public int CurrentGeneration { get; set; }
/// <summary>
/// The best fitness score achieved so far
/// </summary>
public double? BestFitnessSoFar { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -32,6 +32,15 @@ public class BacktestRepository : IBacktestRepository
return backtests.Select(b => MongoMappers.Map(b)); return backtests.Select(b => MongoMappers.Map(b));
} }
public IEnumerable<Backtest> GetBacktestsByRequestId(string requestId)
{
var backtests = _backtestRepository.AsQueryable()
.Where(b => b.RequestId == requestId)
.ToList();
return backtests.Select(b => MongoMappers.Map(b));
}
public Backtest GetBacktestByIdForUser(User user, string id) public Backtest GetBacktestByIdForUser(User user, string id)
{ {
var backtest = _backtestRepository.FindById(id); var backtest = _backtestRepository.FindById(id);

View File

@@ -27,5 +27,9 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
public string? BestIndividual { get; set; } public string? BestIndividual { get; set; }
public string? ErrorMessage { get; set; } public string? ErrorMessage { get; set; }
public string? ProgressInfo { get; set; } public string? ProgressInfo { get; set; }
public string? BestChromosome { get; set; }
public double? BestFitnessSoFar { get; set; }
public int CurrentGeneration { get; set; }
public DateTime? UpdatedAt { get; set; }
} }
} }

View File

@@ -498,6 +498,44 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<Backtest>(null as any); return Promise.resolve<Backtest>(null as any);
} }
backtest_GetBacktestsByRequestId(requestId: string): Promise<Backtest[]> {
let url_ = this.baseUrl + "/Backtest/ByRequestId/{requestId}";
if (requestId === undefined || requestId === null)
throw new Error("The parameter 'requestId' must be defined.");
url_ = url_.replace("{requestId}", encodeURIComponent("" + requestId));
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processBacktest_GetBacktestsByRequestId(_response);
});
}
protected processBacktest_GetBacktestsByRequestId(response: Response): Promise<Backtest[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Backtest[];
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Backtest[]>(null as any);
}
backtest_Run(request: RunBacktestRequest): Promise<Backtest> { backtest_Run(request: RunBacktestRequest): Promise<Backtest> {
let url_ = this.baseUrl + "/Backtest/Run"; let url_ = this.baseUrl + "/Backtest/Run";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
@@ -3251,6 +3289,7 @@ export interface Backtest {
user: User; user: User;
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
score: number; score: number;
requestId?: string | null;
} }
export interface TradingBotConfig { export interface TradingBotConfig {
@@ -3691,6 +3730,9 @@ export interface GeneticRequest {
bestIndividual?: string | null; bestIndividual?: string | null;
errorMessage?: string | null; errorMessage?: string | null;
progressInfo?: string | null; progressInfo?: string | null;
bestChromosome?: string | null;
currentGeneration?: number;
bestFitnessSoFar?: number | null;
} }
export enum GeneticRequestStatus { export enum GeneticRequestStatus {

View File

@@ -236,6 +236,7 @@ export interface Backtest {
user: User; user: User;
indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; };
score: number; score: number;
requestId?: string | null;
} }
export interface TradingBotConfig { export interface TradingBotConfig {
@@ -676,6 +677,9 @@ export interface GeneticRequest {
bestIndividual?: string | null; bestIndividual?: string | null;
errorMessage?: string | null; errorMessage?: string | null;
progressInfo?: string | null; progressInfo?: string | null;
bestChromosome?: string | null;
currentGeneration?: number;
bestFitnessSoFar?: number | null;
} }
export enum GeneticRequestStatus { export enum GeneticRequestStatus {

View File

@@ -4,6 +4,7 @@ import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../app/store/apiStore' import useApiUrlStore from '../../app/store/apiStore'
import { import {
type Backtest,
BacktestClient, BacktestClient,
type GeneticRequest, type GeneticRequest,
IndicatorType, IndicatorType,
@@ -55,6 +56,10 @@ const BacktestGeneticBundle: React.FC = () => {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [selectedIndicators, setSelectedIndicators] = useState<IndicatorType[]>(ALL_INDICATORS) const [selectedIndicators, setSelectedIndicators] = useState<IndicatorType[]>(ALL_INDICATORS)
const [geneticRequests, setGeneticRequests] = useState<GeneticRequest[]>([]) const [geneticRequests, setGeneticRequests] = useState<GeneticRequest[]>([])
const [selectedRequest, setSelectedRequest] = useState<GeneticRequest | null>(null)
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
const [backtests, setBacktests] = useState<Backtest[]>([])
const [isLoadingBacktests, setIsLoadingBacktests] = useState(false)
// Form setup // Form setup
const {register, handleSubmit, watch, setValue, formState: {errors}} = useForm<GeneticBundleFormData>({ const {register, handleSubmit, watch, setValue, formState: {errors}} = useForm<GeneticBundleFormData>({
@@ -187,6 +192,30 @@ const BacktestGeneticBundle: React.FC = () => {
} }
} }
// Handle view details
const handleViewDetails = async (request: GeneticRequest) => {
setSelectedRequest(request)
setIsViewModalOpen(true)
setIsLoadingBacktests(true)
try {
const backtestsData = await backtestClient.backtest_GetBacktestsByRequestId(request.requestId)
setBacktests(backtestsData)
} catch (error) {
console.error('Error fetching backtests:', error)
new Toast('Failed to load backtest details', false)
} finally {
setIsLoadingBacktests(false)
}
}
// Close view modal
const closeViewModal = () => {
setIsViewModalOpen(false)
setSelectedRequest(null)
setBacktests([])
}
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="card bg-base-100 shadow-xl"> <div className="card bg-base-100 shadow-xl">
@@ -433,10 +462,7 @@ const BacktestGeneticBundle: React.FC = () => {
<td> <td>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => { onClick={() => handleViewDetails(request)}
// TODO: Implement view details
new Toast('View details not implemented yet', false)
}}
className="btn btn-sm btn-outline" className="btn btn-sm btn-outline"
> >
View View
@@ -465,6 +491,92 @@ const BacktestGeneticBundle: React.FC = () => {
</div> </div>
</div> </div>
)} )}
{/* View Details Modal */}
{isViewModalOpen && selectedRequest && (
<div className="modal modal-open">
<div className="modal-box w-11/12 max-w-6xl">
<h3 className="font-bold text-lg mb-4">
Genetic Request Details - {selectedRequest.requestId.slice(0, 8)}...
</h3>
<div className="grid grid-cols-2 gap-4 mb-6">
<div>
<strong>Ticker:</strong> {selectedRequest.ticker}
</div>
<div>
<strong>Timeframe:</strong> {selectedRequest.timeframe}
</div>
<div>
<strong>Status:</strong>
<span className={`badge ${getStatusBadgeColor(selectedRequest.status)} ml-2`}>
{selectedRequest.status}
</span>
</div>
<div>
<strong>Created:</strong> {new Date(selectedRequest.createdAt).toLocaleString()}
</div>
{selectedRequest.completedAt && (
<div>
<strong>Completed:</strong> {new Date(selectedRequest.completedAt).toLocaleString()}
</div>
)}
</div>
<div className="mb-6">
<h4 className="font-semibold mb-2">Backtest Results ({backtests.length})</h4>
{isLoadingBacktests ? (
<div className="flex justify-center">
<span className="loading loading-spinner loading-md"></span>
</div>
) : backtests.length > 0 ? (
<div className="overflow-x-auto">
<table className="table table-zebra">
<thead>
<tr>
<th>ID</th>
<th>Final PnL</th>
<th>Win Rate</th>
<th>Growth %</th>
<th>Score</th>
<th>Positions</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{backtests.map((backtest) => (
<tr key={backtest.id}>
<td className="font-mono text-xs">{backtest.id.slice(0, 8)}...</td>
<td className={backtest.finalPnl >= 0 ? 'text-success' : 'text-error'}>
${backtest.finalPnl.toFixed(2)}
</td>
<td>{backtest.winRate}%</td>
<td className={backtest.growthPercentage >= 0 ? 'text-success' : 'text-error'}>
{backtest.growthPercentage.toFixed(2)}%
</td>
<td>{backtest.score.toFixed(2)}</td>
<td>{backtest.positions?.length || 0}</td>
<td>{new Date(backtest.startDate).toLocaleDateString()}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center text-gray-500 py-8">
No backtest results found for this request.
</div>
)}
</div>
<div className="modal-action">
<button onClick={closeViewModal} className="btn">
Close
</button>
</div>
</div>
</div>
)}
</div> </div>
) )
} }