diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index ee6fe412..4ddcaa1e 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -131,7 +131,12 @@ public class BacktestController : BaseController return BadRequest("Request ID is required"); } - var backtests = await _backtester.GetBacktestsByRequestIdAsync(requestId); + if (!Guid.TryParse(requestId, out var requestGuid)) + { + return BadRequest("Invalid request ID format. Must be a valid GUID."); + } + + var backtests = await _backtester.GetBacktestsByRequestIdAsync(requestGuid); return Ok(backtests); } @@ -159,6 +164,11 @@ public class BacktestController : BaseController return BadRequest("Request ID is required"); } + if (!Guid.TryParse(requestId, out var requestGuid)) + { + return BadRequest("Invalid request ID format. Must be a valid GUID."); + } + if (page < 1) { return BadRequest("Page must be greater than 0"); @@ -175,7 +185,7 @@ public class BacktestController : BaseController } var (backtests, totalCount) = - await _backtester.GetBacktestsByRequestIdPaginatedAsync(requestId, page, pageSize, sortBy, sortOrder); + await _backtester.GetBacktestsByRequestIdPaginatedAsync(requestGuid, page, pageSize, sortBy, sortOrder); var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); @@ -503,8 +513,13 @@ public class BacktestController : BaseController [Route("Bundle/{id}")] public async Task> GetBundleBacktestRequest(string id) { + if (!Guid.TryParse(id, out var requestId)) + { + return BadRequest("Invalid bundle request ID format. Must be a valid GUID."); + } + var user = await GetUser(); - var bundleRequest = _backtester.GetBundleBacktestRequestByIdForUser(user, id); + var bundleRequest = _backtester.GetBundleBacktestRequestByIdForUser(user, requestId); if (bundleRequest == null) { @@ -524,13 +539,18 @@ public class BacktestController : BaseController [Route("Bundle/{id}")] public async Task DeleteBundleBacktestRequest(string id) { + if (!Guid.TryParse(id, out var requestId)) + { + return BadRequest("Invalid bundle request ID format. Must be a valid GUID."); + } + var user = await GetUser(); // First, delete the bundle request - _backtester.DeleteBundleBacktestRequestByIdForUser(user, id); + _backtester.DeleteBundleBacktestRequestByIdForUser(user, requestId); // Then, delete all related backtests - var backtestsDeleted = await _backtester.DeleteBacktestsByRequestIdAsync(id); + var backtestsDeleted = await _backtester.DeleteBacktestsByRequestIdAsync(requestId); return Ok(new { @@ -695,7 +715,11 @@ public class BacktestController : BaseController _geneticService.DeleteGeneticRequestByIdForUser(user, id); // Then, delete all related backtests - var backtestsDeleted = await _backtester.DeleteBacktestsByRequestIdAsync(id); + var backtestsDeleted = false; + if (Guid.TryParse(id, out var requestGuid)) + { + backtestsDeleted = await _backtester.DeleteBacktestsByRequestIdAsync(requestGuid); + } return Ok(new { diff --git a/src/Managing.Application.Abstractions/Grains/IBundleBacktestGrain.cs b/src/Managing.Application.Abstractions/Grains/IBundleBacktestGrain.cs new file mode 100644 index 00000000..49f65786 --- /dev/null +++ b/src/Managing.Application.Abstractions/Grains/IBundleBacktestGrain.cs @@ -0,0 +1,19 @@ +using Orleans; + +namespace Managing.Application.Abstractions.Grains; + +/// +/// Orleans grain interface for Bundle Backtest operations. +/// This is a stateless worker grain that processes bundle backtest requests. +/// Uses the bundle request ID as the primary key (Guid). +/// The grain processes a single bundle request identified by its primary key. +/// +public interface IBundleBacktestGrain : IGrainWithGuidKey +{ + /// + /// Processes the bundle backtest request for this grain's RequestId + /// The RequestId is determined by the grain's primary key + /// + /// Task representing the async operation + Task ProcessBundleRequestAsync(); +} diff --git a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs index b9b208ef..4063df62 100644 --- a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs @@ -8,13 +8,13 @@ public interface IBacktestRepository void InsertBacktestForUser(User user, Backtest result); IEnumerable GetBacktestsByUser(User user); Task> GetBacktestsByUserAsync(User user); - IEnumerable GetBacktestsByRequestId(string requestId); - Task> GetBacktestsByRequestIdAsync(string requestId); + IEnumerable GetBacktestsByRequestId(Guid requestId); + Task> GetBacktestsByRequestIdAsync(Guid requestId); - (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, + (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); - Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(string requestId, + Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); @@ -28,18 +28,19 @@ public interface IBacktestRepository Task DeleteBacktestByIdForUserAsync(User user, string id); Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable ids); void DeleteAllBacktestsForUser(User user); - Task DeleteBacktestsByRequestIdAsync(string requestId); + Task DeleteBacktestsByRequestIdAsync(Guid requestId); // Bundle backtest methods void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest); + Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest); IEnumerable GetBundleBacktestRequestsByUser(User user); Task> GetBundleBacktestRequestsByUserAsync(User user); - BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id); - Task GetBundleBacktestRequestByIdForUserAsync(User user, string id); + BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id); + Task GetBundleBacktestRequestByIdForUserAsync(User user, Guid id); void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest); Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest); - void DeleteBundleBacktestRequestByIdForUser(User user, string id); - Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string id); + void DeleteBundleBacktestRequestByIdForUser(User user, Guid id); + Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id); IEnumerable GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status); Task> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status); } \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs index 0173f1ee..b89baba0 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -56,28 +56,29 @@ namespace Managing.Application.Abstractions.Services bool DeleteBacktests(); IEnumerable GetBacktestsByUser(User user); Task> GetBacktestsByUserAsync(User user); - IEnumerable GetBacktestsByRequestId(string requestId); - Task> GetBacktestsByRequestIdAsync(string requestId); - (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); - Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); + IEnumerable GetBacktestsByRequestId(Guid requestId); + Task> GetBacktestsByRequestIdAsync(Guid requestId); + (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); + Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); Task GetBacktestByIdForUserAsync(User user, string id); Task DeleteBacktestByUserAsync(User user, string id); Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable ids); bool DeleteBacktestsByUser(User user); - Task DeleteBacktestsByRequestIdAsync(string requestId); + Task DeleteBacktestsByRequestIdAsync(Guid requestId); (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); // Bundle backtest methods void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest); + Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest); IEnumerable GetBundleBacktestRequestsByUser(User user); Task> GetBundleBacktestRequestsByUserAsync(User user); - BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id); - Task GetBundleBacktestRequestByIdForUserAsync(User user, string id); + BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id); + Task GetBundleBacktestRequestByIdForUserAsync(User user, Guid id); void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest); Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest); - void DeleteBundleBacktestRequestByIdForUser(User user, string id); - Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string id); + void DeleteBundleBacktestRequestByIdForUser(User user, Guid id); + Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id); IEnumerable GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status); Task> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status); diff --git a/src/Managing.Application/Backtests/Backtester.cs b/src/Managing.Application/Backtests/Backtester.cs index bd49bc2c..a9070ebe 100644 --- a/src/Managing.Application/Backtests/Backtester.cs +++ b/src/Managing.Application/Backtests/Backtester.cs @@ -95,7 +95,7 @@ namespace Managing.Application.Backtests try { - var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate); + var candles = await GetCandles(config.Ticker, config.Timeframe, startDate, endDate); return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata); } catch (Exception ex) @@ -197,11 +197,11 @@ namespace Managing.Application.Backtests return await _accountService.GetAccountByAccountName(config.AccountName, false, false); } - private HashSet GetCandles(Ticker ticker, Timeframe timeframe, + private async Task> GetCandles(Ticker ticker, Timeframe timeframe, DateTime startDate, DateTime endDate) { - var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, - startDate, timeframe, endDate).Result; + var candles = await _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, + startDate, timeframe, endDate); if (candles == null || candles.Count == 0) throw new Exception($"No candles for {ticker} on {timeframe} timeframe"); @@ -277,19 +277,19 @@ namespace Managing.Application.Backtests return backtests; } - public IEnumerable GetBacktestsByRequestId(string requestId) + public IEnumerable GetBacktestsByRequestId(Guid requestId) { var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList(); return backtests; } - public async Task> GetBacktestsByRequestIdAsync(string requestId) + public async Task> GetBacktestsByRequestIdAsync(Guid requestId) { var backtests = await _backtestRepository.GetBacktestsByRequestIdAsync(requestId); return backtests; } - public (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, + public (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var (backtests, totalCount) = @@ -298,7 +298,7 @@ namespace Managing.Application.Backtests } public async Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync( - string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") + Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var (backtests, totalCount) = await _backtestRepository.GetBacktestsByRequestIdPaginatedAsync(requestId, page, pageSize, sortBy, @@ -384,7 +384,7 @@ namespace Managing.Application.Backtests } } - public async Task DeleteBacktestsByRequestIdAsync(string requestId) + public async Task DeleteBacktestsByRequestIdAsync(Guid requestId) { try { @@ -418,6 +418,17 @@ namespace Managing.Application.Backtests public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest) { _backtestRepository.InsertBundleBacktestRequestForUser(user, bundleRequest); + + // Trigger the BundleBacktestGrain to process this request + TriggerBundleBacktestGrain(bundleRequest.RequestId); + } + + public async Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest) + { + await _backtestRepository.InsertBundleBacktestRequestForUserAsync(user, bundleRequest); + + // Trigger the BundleBacktestGrain to process this request + await TriggerBundleBacktestGrainAsync(bundleRequest.RequestId); } public IEnumerable GetBundleBacktestRequestsByUser(User user) @@ -430,12 +441,12 @@ namespace Managing.Application.Backtests return await _backtestRepository.GetBundleBacktestRequestsByUserAsync(user); } - public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id) + public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id) { return _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id); } - public async Task GetBundleBacktestRequestByIdForUserAsync(User user, string id) + public async Task GetBundleBacktestRequestByIdForUserAsync(User user, Guid id) { return await _backtestRepository.GetBundleBacktestRequestByIdForUserAsync(user, id); } @@ -450,12 +461,12 @@ namespace Managing.Application.Backtests await _backtestRepository.UpdateBundleBacktestRequestAsync(bundleRequest); } - public void DeleteBundleBacktestRequestByIdForUser(User user, string id) + public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id) { _backtestRepository.DeleteBundleBacktestRequestByIdForUser(user, id); } - public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string id) + public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id) { await _backtestRepository.DeleteBundleBacktestRequestByIdForUserAsync(user, id); } @@ -480,5 +491,65 @@ namespace Managing.Application.Backtests if (string.IsNullOrWhiteSpace(requestId) || response == null) return; await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response); } + + /// + /// Triggers the BundleBacktestGrain to process a bundle request synchronously (fire and forget) + /// + private void TriggerBundleBacktestGrain(Guid bundleRequestId) + { + try + { + var bundleBacktestGrain = _grainFactory.GetGrain(bundleRequestId); + + // Fire and forget - don't await + _ = Task.Run(async () => + { + try + { + await bundleBacktestGrain.ProcessBundleRequestAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error triggering BundleBacktestGrain for request {RequestId}", + bundleRequestId); + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in TriggerBundleBacktestGrain for request {RequestId}", bundleRequestId); + } + } + + /// + /// Triggers the BundleBacktestGrain to process a bundle request asynchronously + /// + private Task TriggerBundleBacktestGrainAsync(Guid bundleRequestId) + { + try + { + var bundleBacktestGrain = _grainFactory.GetGrain(bundleRequestId); + + // Fire and forget - don't await the actual processing + return Task.Run(async () => + { + try + { + await bundleBacktestGrain.ProcessBundleRequestAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error triggering BundleBacktestGrain for request {RequestId}", + bundleRequestId); + } + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in TriggerBundleBacktestGrainAsync for request {RequestId}", + bundleRequestId); + return Task.CompletedTask; + } + } } } \ No newline at end of file diff --git a/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs index 8183dfc0..3fb7a604 100644 --- a/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/BacktestTradingBotGrain.cs @@ -131,7 +131,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams); // Generate requestId if not provided - var finalRequestId = requestId ?? Guid.NewGuid().ToString(); + var finalRequestId = requestId != null ? Guid.Parse(requestId) : Guid.NewGuid(); // Create backtest result with conditional candles and indicators values var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals, diff --git a/src/Managing.Application/Grains/BundleBacktestGrain.cs b/src/Managing.Application/Grains/BundleBacktestGrain.cs new file mode 100644 index 00000000..7f130a84 --- /dev/null +++ b/src/Managing.Application/Grains/BundleBacktestGrain.cs @@ -0,0 +1,411 @@ +using System.Text.Json; +using Managing.Application.Abstractions.Grains; +using Managing.Application.Abstractions.Services; +using Managing.Domain.Backtests; +using Managing.Domain.Bots; +using Managing.Domain.MoneyManagements; +using Managing.Domain.Scenarios; +using Managing.Domain.Strategies; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Orleans.Concurrency; + +namespace Managing.Application.Grains; + +/// +/// Stateless worker grain for processing bundle backtest requests +/// Uses the bundle request ID as the primary key (Guid) +/// Implements IRemindable for automatic retry of failed bundles +/// +[StatelessWorker] +public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable +{ + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + + // Reminder configuration + private const string RETRY_REMINDER_NAME = "BundleBacktestRetry"; + private static readonly TimeSpan RETRY_INTERVAL = TimeSpan.FromMinutes(30); + + public BundleBacktestGrain( + ILogger logger, + IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + public async Task ProcessBundleRequestAsync() + { + // Get the RequestId from the grain's primary key + var bundleRequestId = this.GetPrimaryKey(); + + try + { + // Create a new service scope to get fresh instances of services with scoped DbContext + using var scope = _scopeFactory.CreateScope(); + var backtester = scope.ServiceProvider.GetRequiredService(); + var messengerService = scope.ServiceProvider.GetRequiredService(); + + // Get the specific bundle request by ID + var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId); + if (bundleRequest == null) + { + _logger.LogError("Bundle request {RequestId} not found", bundleRequestId); + return; + } + + // Process only this specific bundle request + await ProcessBundleRequest(bundleRequest, backtester, messengerService); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in BundleBacktestGrain for request {RequestId}", bundleRequestId); + throw; + } + } + + private async Task GetBundleRequestById(IBacktester backtester, Guid bundleRequestId) + { + try + { + // Get pending and failed bundle backtest requests for retry capability + var pendingRequests = await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Pending); + var failedRequests = await backtester.GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus.Failed); + + var allRequests = pendingRequests.Concat(failedRequests); + return allRequests.FirstOrDefault(r => r.RequestId == bundleRequestId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get bundle request {RequestId}", bundleRequestId); + return null; + } + } + + private async Task ProcessBundleRequest( + BundleBacktestRequest bundleRequest, + IBacktester backtester, + IMessengerService messengerService) + { + try + { + _logger.LogInformation("Starting to process bundle backtest request {RequestId}", bundleRequest.RequestId); + + // Update status to running + bundleRequest.Status = BundleBacktestRequestStatus.Running; + await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); + + // Deserialize the backtest requests as strongly-typed objects + var backtestRequests = JsonSerializer.Deserialize>(bundleRequest.BacktestRequestsJson); + if (backtestRequests == null) + { + throw new InvalidOperationException("Failed to deserialize backtest requests"); + } + + // Process each backtest request sequentially + for (int i = 0; i < backtestRequests.Count; i++) + { + await ProcessSingleBacktest(backtester, backtestRequests[i], bundleRequest, i); + } + + // Update final status and send notifications + await UpdateFinalStatus(bundleRequest, backtester, messengerService); + + _logger.LogInformation("Completed processing bundle backtest request {RequestId} with status {Status}", + bundleRequest.RequestId, bundleRequest.Status); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing bundle backtest request {RequestId}", bundleRequest.RequestId); + await HandleBundleRequestError(bundleRequest, backtester, ex); + } + } + + private async Task ProcessSingleBacktest( + IBacktester backtester, + RunBacktestRequest runBacktestRequest, + BundleBacktestRequest bundleRequest, + int index) + { + try + { + // Get total count from deserialized requests instead of string splitting + var backtestRequests = JsonSerializer.Deserialize>(bundleRequest.BacktestRequestsJson); + var totalCount = backtestRequests?.Count ?? 0; + + // Update current backtest being processed + bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}"; + await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); + + // Run the backtest directly with the strongly-typed request + var backtestId = await RunSingleBacktest(backtester, runBacktestRequest, bundleRequest, index); + if (!string.IsNullOrEmpty(backtestId)) + { + bundleRequest.Results.Add(backtestId); + } + + // Update progress + bundleRequest.CompletedBacktests++; + await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); + + _logger.LogInformation("Completed backtest {Index} for bundle request {RequestId}", + index + 1, bundleRequest.RequestId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing backtest {Index} for bundle request {RequestId}", + index + 1, bundleRequest.RequestId); + bundleRequest.FailedBacktests++; + await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); + } + } + + private async Task RunSingleBacktest( + IBacktester backtester, + RunBacktestRequest runBacktestRequest, + BundleBacktestRequest bundleRequest, + int index) + { + if (runBacktestRequest?.Config == null) + { + _logger.LogError("Invalid RunBacktestRequest in bundle (null config)"); + return string.Empty; + } + + // Map MoneyManagement + MoneyManagement moneyManagement = null; + if (!string.IsNullOrEmpty(runBacktestRequest.Config.MoneyManagementName)) + { + _logger.LogWarning("MoneyManagementName provided but cannot resolve in grain context: {Name}", + runBacktestRequest.Config.MoneyManagementName); + } + else if (runBacktestRequest.Config.MoneyManagement != null) + { + var mmReq = runBacktestRequest.Config.MoneyManagement; + moneyManagement = new MoneyManagement + { + Name = mmReq.Name, + Timeframe = mmReq.Timeframe, + StopLoss = mmReq.StopLoss, + TakeProfit = mmReq.TakeProfit, + Leverage = mmReq.Leverage + }; + moneyManagement.FormatPercentage(); + } + + // Map Scenario + LightScenario scenario = null; + if (runBacktestRequest.Config.Scenario != null) + { + var sReq = runBacktestRequest.Config.Scenario; + scenario = new LightScenario(sReq.Name, sReq.LoopbackPeriod) + { + Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type) + { + SignalType = i.SignalType, + MinimumHistory = i.MinimumHistory, + Period = i.Period, + FastPeriods = i.FastPeriods, + SlowPeriods = i.SlowPeriods, + SignalPeriods = i.SignalPeriods, + Multiplier = i.Multiplier, + SmoothPeriods = i.SmoothPeriods, + StochPeriods = i.StochPeriods, + CyclePeriods = i.CyclePeriods + }).ToList() ?? new List() + }; + } + + // Map TradingBotConfig + var backtestConfig = new TradingBotConfig + { + AccountName = runBacktestRequest.Config.AccountName, + MoneyManagement = moneyManagement, + Ticker = runBacktestRequest.Config.Ticker, + ScenarioName = runBacktestRequest.Config.ScenarioName, + Scenario = scenario, + Timeframe = runBacktestRequest.Config.Timeframe, + IsForWatchingOnly = runBacktestRequest.Config.IsForWatchingOnly, + BotTradingBalance = runBacktestRequest.Config.BotTradingBalance, + IsForBacktest = true, + CooldownPeriod = runBacktestRequest.Config.CooldownPeriod ?? 1, + MaxLossStreak = runBacktestRequest.Config.MaxLossStreak, + MaxPositionTimeHours = runBacktestRequest.Config.MaxPositionTimeHours, + FlipOnlyWhenInProfit = runBacktestRequest.Config.FlipOnlyWhenInProfit, + FlipPosition = runBacktestRequest.Config.FlipPosition, + Name = $"{bundleRequest.Name} #{index + 1}", + CloseEarlyWhenProfitable = runBacktestRequest.Config.CloseEarlyWhenProfitable, + UseSynthApi = runBacktestRequest.Config.UseSynthApi, + UseForPositionSizing = runBacktestRequest.Config.UseForPositionSizing, + UseForSignalFiltering = runBacktestRequest.Config.UseForSignalFiltering, + UseForDynamicStopLoss = runBacktestRequest.Config.UseForDynamicStopLoss + }; + + // Run the backtest + var result = await backtester.RunTradingBotBacktest( + backtestConfig, + runBacktestRequest.StartDate, + runBacktestRequest.EndDate, + bundleRequest.User, + true, + runBacktestRequest.WithCandles, + bundleRequest.RequestId.ToString() + ); + + _logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequest.RequestId); + return result.Id; + } + + private async Task UpdateFinalStatus( + BundleBacktestRequest bundleRequest, + IBacktester backtester, + IMessengerService messengerService) + { + if (bundleRequest.FailedBacktests == 0) + { + bundleRequest.Status = BundleBacktestRequestStatus.Completed; + await NotifyUser(bundleRequest, messengerService); + } + else if (bundleRequest.CompletedBacktests == 0) + { + bundleRequest.Status = BundleBacktestRequestStatus.Failed; + bundleRequest.ErrorMessage = "All backtests failed"; + } + else + { + bundleRequest.Status = BundleBacktestRequestStatus.Completed; + bundleRequest.ErrorMessage = $"{bundleRequest.FailedBacktests} backtests failed"; + await NotifyUser(bundleRequest, messengerService); + } + + bundleRequest.CompletedAt = DateTime.UtcNow; + bundleRequest.CurrentBacktest = null; + await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); + + // Unregister retry reminder since bundle completed + await UnregisterRetryReminder(); + } + + private async Task HandleBundleRequestError( + BundleBacktestRequest bundleRequest, + IBacktester backtester, + Exception ex) + { + bundleRequest.Status = BundleBacktestRequestStatus.Failed; + bundleRequest.ErrorMessage = ex.Message; + bundleRequest.CompletedAt = DateTime.UtcNow; + await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); + + // Register retry reminder for failed bundle + await RegisterRetryReminder(); + } + + private async Task NotifyUser(BundleBacktestRequest bundleRequest, IMessengerService messengerService) + { + if (bundleRequest.User?.TelegramChannel != null) + { + var message = bundleRequest.FailedBacktests == 0 + ? $"✅ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed successfully." + : $"⚠️ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed with {bundleRequest.FailedBacktests} failed backtests."; + + await messengerService.SendMessage(message, bundleRequest.User.TelegramChannel); + } + } + + #region IRemindable Implementation + + /// + /// Handles reminder callbacks for automatic retry of failed bundle backtests + /// + public async Task ReceiveReminder(string reminderName, TickStatus status) + { + if (reminderName != RETRY_REMINDER_NAME) + { + _logger.LogWarning("Unknown reminder {ReminderName} received", reminderName); + return; + } + + var bundleRequestId = this.GetPrimaryKey(); + _logger.LogInformation("Retry reminder triggered for bundle request {RequestId}", bundleRequestId); + + try + { + using var scope = _scopeFactory.CreateScope(); + var backtester = scope.ServiceProvider.GetRequiredService(); + + // Get the bundle request + var bundleRequest = await GetBundleRequestById(backtester, bundleRequestId); + if (bundleRequest == null) + { + _logger.LogWarning("Bundle request {RequestId} not found during retry", bundleRequestId); + await UnregisterRetryReminder(); + return; + } + + // Check if bundle is still failed + if (bundleRequest.Status != BundleBacktestRequestStatus.Failed) + { + _logger.LogInformation("Bundle request {RequestId} is no longer failed (status: {Status}), unregistering reminder", + bundleRequestId, bundleRequest.Status); + await UnregisterRetryReminder(); + return; + } + + // Retry the bundle processing + _logger.LogInformation("Retrying failed bundle request {RequestId}", bundleRequestId); + + // Reset status to pending for retry + bundleRequest.Status = BundleBacktestRequestStatus.Pending; + bundleRequest.ErrorMessage = null; + bundleRequest.CurrentBacktest = null; + await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); + + // Process the bundle again + await ProcessBundleRequestAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during bundle backtest retry for request {RequestId}", bundleRequestId); + } + } + + /// + /// Registers a retry reminder for this bundle request + /// + private async Task RegisterRetryReminder() + { + try + { + await this.RegisterOrUpdateReminder(RETRY_REMINDER_NAME, RETRY_INTERVAL, RETRY_INTERVAL); + _logger.LogInformation("Registered retry reminder for bundle request {RequestId}", this.GetPrimaryKey()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to register retry reminder for bundle request {RequestId}", this.GetPrimaryKey()); + } + } + + /// + /// Unregisters the retry reminder for this bundle request + /// + private async Task UnregisterRetryReminder() + { + try + { + var reminder = await this.GetReminder(RETRY_REMINDER_NAME); + if (reminder != null) + { + await this.UnregisterReminder(reminder); + _logger.LogInformation("Unregistered retry reminder for bundle request {RequestId}", this.GetPrimaryKey()); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to unregister retry reminder for bundle request {RequestId}", this.GetPrimaryKey()); + } + } + + #endregion + +} diff --git a/src/Managing.Application/Workers/BundleBacktestWorker.cs b/src/Managing.Application/Workers/BundleBacktestWorker.cs index 714e5c27..8d35c06a 100644 --- a/src/Managing.Application/Workers/BundleBacktestWorker.cs +++ b/src/Managing.Application/Workers/BundleBacktestWorker.cs @@ -277,7 +277,7 @@ public class BundleBacktestWorker : BaseWorker bundleRequest.User, // No user context in worker true, runBacktestRequest.WithCandles, - bundleRequest.RequestId // Use bundleRequestId as requestId for traceability + bundleRequest.RequestId.ToString() // Use bundleRequestId as requestId for traceability ); _logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequest.RequestId); diff --git a/src/Managing.Application/Workers/NotifyBundleBacktestWorker.cs b/src/Managing.Application/Workers/NotifyBundleBacktestWorker.cs index bf73ab44..11a50fd8 100644 --- a/src/Managing.Application/Workers/NotifyBundleBacktestWorker.cs +++ b/src/Managing.Application/Workers/NotifyBundleBacktestWorker.cs @@ -13,7 +13,7 @@ public class NotifyBundleBacktestWorker : BaseWorker { private readonly IServiceProvider _serviceProvider; private readonly IHubContext _hubContext; - private readonly ConcurrentDictionary> _sentBacktestIds = new(); + private readonly ConcurrentDictionary> _sentBacktestIds = new(); public NotifyBundleBacktestWorker( IServiceProvider serviceProvider, @@ -39,7 +39,6 @@ public class NotifyBundleBacktestWorker : BaseWorker foreach (var bundle in runningBundles) { var requestId = bundle.RequestId; - if (string.IsNullOrEmpty(requestId)) continue; // Fetch all backtests for this bundle var (backtests, _) = backtester.GetBacktestsByRequestIdPaginated(requestId, 1, 100); diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index af864de0..b94bc360 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -501,10 +501,12 @@ public static class ApiBootstrap services.AddHostedService(); } - if (configuration.GetValue("WorkerBundleBacktest", false)) - { - services.AddHostedService(); - } + // DEPRECATED: BundleBacktestWorker has been replaced by BundleBacktestGrain + // Bundle backtest processing is now handled by Orleans grain triggered directly from Backtester.cs + // if (configuration.GetValue("WorkerBundleBacktest", false)) + // { + // services.AddHostedService(); + // } return services; } diff --git a/src/Managing.Domain/Backtests/Backtest.cs b/src/Managing.Domain/Backtests/Backtest.cs index d65f8773..ec2d34ce 100644 --- a/src/Managing.Domain/Backtests/Backtest.cs +++ b/src/Managing.Domain/Backtests/Backtest.cs @@ -52,7 +52,7 @@ public class Backtest [Required] public List> WalletBalances { get; set; } [Required] public User User { get; set; } [Required] public double Score { get; set; } - public string RequestId { get; set; } + public Guid RequestId { get; set; } public object? Metadata { get; set; } public string ScoreMessage { get; set; } = string.Empty; diff --git a/src/Managing.Domain/Backtests/BundleBacktestRequest.cs b/src/Managing.Domain/Backtests/BundleBacktestRequest.cs index 1089f334..f0dcf064 100644 --- a/src/Managing.Domain/Backtests/BundleBacktestRequest.cs +++ b/src/Managing.Domain/Backtests/BundleBacktestRequest.cs @@ -10,7 +10,7 @@ public class BundleBacktestRequest { public BundleBacktestRequest() { - RequestId = Guid.NewGuid().ToString(); + RequestId = Guid.NewGuid(); CreatedAt = DateTime.UtcNow; Status = BundleBacktestRequestStatus.Pending; Results = new List(); @@ -21,7 +21,7 @@ public class BundleBacktestRequest /// Constructor that allows setting a specific ID /// /// The specific ID to use - public BundleBacktestRequest(string requestId) + public BundleBacktestRequest(Guid requestId) { RequestId = requestId; CreatedAt = DateTime.UtcNow; @@ -34,7 +34,7 @@ public class BundleBacktestRequest /// Unique identifier for the bundle backtest request /// [Required] - public string RequestId { get; set; } + public Guid RequestId { get; set; } /// /// The user who created this request diff --git a/src/Managing.Infrastructure.Database/Migrations/20250915052125_ChangeRequestIdToGuid.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20250915052125_ChangeRequestIdToGuid.Designer.cs new file mode 100644 index 00000000..8043c8dc --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250915052125_ChangeRequestIdToGuid.Designer.cs @@ -0,0 +1,1438 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20250915052125_ChangeRequestIdToGuid")] + partial class ChangeRequestIdToGuid + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Score"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BacktestRequestsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Fee") + .HasColumnType("decimal(18,8)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20250915052125_ChangeRequestIdToGuid.cs b/src/Managing.Infrastructure.Database/Migrations/20250915052125_ChangeRequestIdToGuid.cs new file mode 100644 index 00000000..7debcc39 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20250915052125_ChangeRequestIdToGuid.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class ChangeRequestIdToGuid : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Convert BundleBacktestRequests.RequestId from varchar to uuid + // First, ensure all values are valid UUIDs or convert them + migrationBuilder.Sql(@" + UPDATE ""BundleBacktestRequests"" + SET ""RequestId"" = gen_random_uuid()::text + WHERE ""RequestId"" IS NULL OR ""RequestId"" = '' OR + ""RequestId"" !~ '^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$'; + "); + + // Now convert the column type using the USING clause + migrationBuilder.Sql(@" + ALTER TABLE ""BundleBacktestRequests"" + ALTER COLUMN ""RequestId"" TYPE uuid USING ""RequestId""::uuid; + "); + + // Convert Backtests.RequestId from varchar to uuid + // First, ensure all values are valid UUIDs or convert them + migrationBuilder.Sql(@" + UPDATE ""Backtests"" + SET ""RequestId"" = gen_random_uuid()::text + WHERE ""RequestId"" IS NULL OR ""RequestId"" = '' OR + ""RequestId"" !~ '^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$'; + "); + + // Now convert the column type using the USING clause + migrationBuilder.Sql(@" + ALTER TABLE ""Backtests"" + ALTER COLUMN ""RequestId"" TYPE uuid USING ""RequestId""::uuid; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Convert BundleBacktestRequests.RequestId from uuid back to varchar + migrationBuilder.Sql(@" + ALTER TABLE ""BundleBacktestRequests"" + ALTER COLUMN ""RequestId"" TYPE character varying(255) USING ""RequestId""::text; + "); + + // Convert Backtests.RequestId from uuid back to varchar + migrationBuilder.Sql(@" + ALTER TABLE ""Backtests"" + ALTER COLUMN ""RequestId"" TYPE character varying(255) USING ""RequestId""::text; + "); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index d726db58..7f27d2c4 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -176,10 +176,9 @@ namespace Managing.Infrastructure.Databases.Migrations .IsRequired() .HasColumnType("jsonb"); - b.Property("RequestId") - .IsRequired() + b.Property("RequestId") .HasMaxLength(255) - .HasColumnType("character varying(255)"); + .HasColumnType("uuid"); b.Property("Score") .HasColumnType("double precision"); @@ -339,10 +338,9 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("ProgressInfo") .HasColumnType("text"); - b.Property("RequestId") - .IsRequired() + b.Property("RequestId") .HasMaxLength(255) - .HasColumnType("character varying(255)"); + .HasColumnType("uuid"); b.Property("ResultsJson") .IsRequired() diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BacktestEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BacktestEntity.cs index a5d0059c..2b8499a2 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BacktestEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BacktestEntity.cs @@ -15,8 +15,7 @@ public class BacktestEntity public string Identifier { get; set; } = string.Empty; [Required] - [MaxLength(255)] - public string RequestId { get; set; } = string.Empty; + public Guid RequestId { get; set; } [Required] [Column(TypeName = "decimal(18,8)")] diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BundleBacktestRequestEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BundleBacktestRequestEntity.cs index e9fd9bc4..b18a0352 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BundleBacktestRequestEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BundleBacktestRequestEntity.cs @@ -12,8 +12,7 @@ public class BundleBacktestRequestEntity public int Id { get; set; } [Required] - [MaxLength(255)] - public string RequestId { get; set; } = string.Empty; + public Guid RequestId { get; set; } // Foreign key to User entity public int? UserId { get; set; } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs index dc99a504..50db7e3a 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs @@ -91,7 +91,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository return entities.Select(PostgreSqlMappers.Map); } - public IEnumerable GetBacktestsByRequestId(string requestId) + public IEnumerable GetBacktestsByRequestId(Guid requestId) { var entities = _context.Backtests .AsNoTracking() @@ -101,7 +101,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository return entities.Select(PostgreSqlMappers.Map); } - public async Task> GetBacktestsByRequestIdAsync(string requestId) + public async Task> GetBacktestsByRequestIdAsync(Guid requestId) { var entities = await _context.Backtests .AsNoTracking() @@ -112,7 +112,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository return entities.Select(PostgreSqlMappers.Map); } - public (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, + public (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var stopwatch = Stopwatch.StartNew(); @@ -185,7 +185,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository } public async Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync( - string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") + Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var stopwatch = Stopwatch.StartNew(); @@ -348,7 +348,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository } } - public void DeleteBacktestsByRequestId(string requestId) + public void DeleteBacktestsByRequestId(Guid requestId) { var entities = _context.Backtests .AsTracking() @@ -362,7 +362,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository } } - public async Task DeleteBacktestsByRequestIdAsync(string requestId) + public async Task DeleteBacktestsByRequestIdAsync(Guid requestId) { var entities = await _context.Backtests .AsTracking() @@ -580,7 +580,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository return entities.Select(PostgreSqlMappers.Map); } - public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id) + public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id) { var entity = _context.BundleBacktestRequests .AsNoTracking() @@ -590,7 +590,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository return entity != null ? PostgreSqlMappers.Map(entity) : null; } - public async Task GetBundleBacktestRequestByIdForUserAsync(User user, string id) + public async Task GetBundleBacktestRequestByIdForUserAsync(User user, Guid id) { var entity = await _context.BundleBacktestRequests .AsNoTracking() @@ -682,7 +682,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository } } - public void DeleteBundleBacktestRequestByIdForUser(User user, string id) + public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id) { var entity = _context.BundleBacktestRequests .AsTracking() @@ -695,7 +695,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository } } - public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string id) + public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id) { var entity = await _context.BundleBacktestRequests .AsTracking() diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestBundleForm.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestBundleForm.tsx index 3fd18303..fb595dec 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestBundleForm.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestBundleForm.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {AccountClient, BacktestClient} from '../../generated/ManagingApi'; import type { MoneyManagementRequest, @@ -61,9 +61,6 @@ const BacktestBundleForm: React.FC = () => { const { data: accounts, isSuccess } = useQuery({ queryFn: async () => { const fetchedAccounts = await accountClient.account_GetAccounts(); - if (fetchedAccounts.length > 0 && accountName === 'default') { - setAccountName(fetchedAccounts[0].name); - } return fetchedAccounts; }, queryKey: ['accounts'], @@ -88,13 +85,20 @@ const BacktestBundleForm: React.FC = () => { const [flipOnlyInProfit, setFlipOnlyInProfit] = useState(false); const [closeEarly, setCloseEarly] = useState(false); const [startingCapital, setStartingCapital] = useState(10000); - const [accountName, setAccountName] = useState(accounts?.[0]?.name ?? ''); + const [accountName, setAccountName] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const { scenario, setCustomScenario } = useCustomScenario(); + // Set account name when accounts are loaded + useEffect(() => { + if (accounts && accounts.length > 0 && !accountName) { + setAccountName(accounts[0].name); + } + }, [accounts, accountName]); + // Placeholder for cart summary const totalBacktests = moneyManagementVariants.length * timeRangeVariants.length * (selectedAssets.length || 1); @@ -159,6 +163,7 @@ const BacktestBundleForm: React.FC = () => { const client = new BacktestClient({} as any, apiUrl); const requests = generateRequests(); if (!strategyName) throw new Error('Strategy name is required'); + if (!accountName) throw new Error('Account selection is required'); if (requests.length === 0) throw new Error('No backtest variants to run'); await client.backtest_RunBundle({ name: strategyName, requests }); setSuccess('Bundle backtest started successfully!'); @@ -195,12 +200,17 @@ const BacktestBundleForm: React.FC = () => { className="select select-bordered w-full" value={accountName} onChange={e => setAccountName(e.target.value)} + disabled={!accounts || accounts.length === 0} > - {accounts?.map(account => ( - - ))} + {!accounts || accounts.length === 0 ? ( + + ) : ( + accounts.map(account => ( + + )) + )}