Bundle from worker to grain

This commit is contained in:
2025-09-15 12:56:59 +07:00
parent 77e6ce0789
commit 63bc7bbe59
19 changed files with 2112 additions and 79 deletions

View File

@@ -131,7 +131,12 @@ public class BacktestController : BaseController
return BadRequest("Request ID is required"); 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); return Ok(backtests);
} }
@@ -159,6 +164,11 @@ public class BacktestController : BaseController
return BadRequest("Request ID is required"); 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) if (page < 1)
{ {
return BadRequest("Page must be greater than 0"); return BadRequest("Page must be greater than 0");
@@ -175,7 +185,7 @@ public class BacktestController : BaseController
} }
var (backtests, totalCount) = 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); var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
@@ -503,8 +513,13 @@ public class BacktestController : BaseController
[Route("Bundle/{id}")] [Route("Bundle/{id}")]
public async Task<ActionResult<BundleBacktestRequest>> GetBundleBacktestRequest(string id) public async Task<ActionResult<BundleBacktestRequest>> 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 user = await GetUser();
var bundleRequest = _backtester.GetBundleBacktestRequestByIdForUser(user, id); var bundleRequest = _backtester.GetBundleBacktestRequestByIdForUser(user, requestId);
if (bundleRequest == null) if (bundleRequest == null)
{ {
@@ -524,13 +539,18 @@ public class BacktestController : BaseController
[Route("Bundle/{id}")] [Route("Bundle/{id}")]
public async Task<ActionResult> DeleteBundleBacktestRequest(string id) public async Task<ActionResult> 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(); var user = await GetUser();
// First, delete the bundle request // First, delete the bundle request
_backtester.DeleteBundleBacktestRequestByIdForUser(user, id); _backtester.DeleteBundleBacktestRequestByIdForUser(user, requestId);
// Then, delete all related backtests // Then, delete all related backtests
var backtestsDeleted = await _backtester.DeleteBacktestsByRequestIdAsync(id); var backtestsDeleted = await _backtester.DeleteBacktestsByRequestIdAsync(requestId);
return Ok(new return Ok(new
{ {
@@ -695,7 +715,11 @@ public class BacktestController : BaseController
_geneticService.DeleteGeneticRequestByIdForUser(user, id); _geneticService.DeleteGeneticRequestByIdForUser(user, id);
// Then, delete all related backtests // 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 return Ok(new
{ {

View File

@@ -0,0 +1,19 @@
using Orleans;
namespace Managing.Application.Abstractions.Grains;
/// <summary>
/// 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.
/// </summary>
public interface IBundleBacktestGrain : IGrainWithGuidKey
{
/// <summary>
/// Processes the bundle backtest request for this grain's RequestId
/// The RequestId is determined by the grain's primary key
/// </summary>
/// <returns>Task representing the async operation</returns>
Task ProcessBundleRequestAsync();
}

View File

@@ -8,13 +8,13 @@ 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);
Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user); Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user);
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId); IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId);
Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(string requestId); Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId);
(IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page,
int pageSize, string sortBy = "score", string sortOrder = "desc"); int pageSize, string sortBy = "score", string sortOrder = "desc");
Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(string requestId, Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(Guid requestId,
int page, int page,
int pageSize, string sortBy = "score", string sortOrder = "desc"); int pageSize, string sortBy = "score", string sortOrder = "desc");
@@ -28,18 +28,19 @@ public interface IBacktestRepository
Task DeleteBacktestByIdForUserAsync(User user, string id); Task DeleteBacktestByIdForUserAsync(User user, string id);
Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids); Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids);
void DeleteAllBacktestsForUser(User user); void DeleteAllBacktestsForUser(User user);
Task DeleteBacktestsByRequestIdAsync(string requestId); Task DeleteBacktestsByRequestIdAsync(Guid requestId);
// Bundle backtest methods // Bundle backtest methods
void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest); void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest);
Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest);
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user); IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user);
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user); Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user);
BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id); BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id);
Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, string id); Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id);
void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest); void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest);
Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest); Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest);
void DeleteBundleBacktestRequestByIdForUser(User user, string id); void DeleteBundleBacktestRequestByIdForUser(User user, Guid id);
Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string id); Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id);
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status); IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status);
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status); Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status);
} }

View File

@@ -56,28 +56,29 @@ namespace Managing.Application.Abstractions.Services
bool DeleteBacktests(); bool DeleteBacktests();
IEnumerable<Backtest> GetBacktestsByUser(User user); IEnumerable<Backtest> GetBacktestsByUser(User user);
Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user); Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user);
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId); IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId);
Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(string requestId); Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId);
(IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(Guid requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
Task<Backtest> GetBacktestByIdForUserAsync(User user, string id); Task<Backtest> GetBacktestByIdForUserAsync(User user, string id);
Task<bool> DeleteBacktestByUserAsync(User user, string id); Task<bool> DeleteBacktestByUserAsync(User user, string id);
Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids); Task<bool> DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids);
bool DeleteBacktestsByUser(User user); bool DeleteBacktestsByUser(User user);
Task<bool> DeleteBacktestsByRequestIdAsync(string requestId); Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId);
(IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
// Bundle backtest methods // Bundle backtest methods
void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest); void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest);
Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest);
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user); IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user);
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user); Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user);
BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id); BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id);
Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, string id); Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id);
void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest); void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest);
Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest); Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest);
void DeleteBundleBacktestRequestByIdForUser(User user, string id); void DeleteBundleBacktestRequestByIdForUser(User user, Guid id);
Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string id); Task DeleteBundleBacktestRequestByIdForUserAsync(User user, Guid id);
IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status); IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status);
Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status); Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status);

View File

@@ -95,7 +95,7 @@ namespace Managing.Application.Backtests
try 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); return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
} }
catch (Exception ex) catch (Exception ex)
@@ -197,11 +197,11 @@ namespace Managing.Application.Backtests
return await _accountService.GetAccountByAccountName(config.AccountName, false, false); return await _accountService.GetAccountByAccountName(config.AccountName, false, false);
} }
private HashSet<Candle> GetCandles(Ticker ticker, Timeframe timeframe, private async Task<HashSet<Candle>> GetCandles(Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate) DateTime startDate, DateTime endDate)
{ {
var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, var candles = await _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker,
startDate, timeframe, endDate).Result; startDate, timeframe, endDate);
if (candles == null || candles.Count == 0) if (candles == null || candles.Count == 0)
throw new Exception($"No candles for {ticker} on {timeframe} timeframe"); throw new Exception($"No candles for {ticker} on {timeframe} timeframe");
@@ -277,19 +277,19 @@ namespace Managing.Application.Backtests
return backtests; return backtests;
} }
public IEnumerable<Backtest> GetBacktestsByRequestId(string requestId) public IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId)
{ {
var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList(); var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList();
return backtests; return backtests;
} }
public async Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(string requestId) public async Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId)
{ {
var backtests = await _backtestRepository.GetBacktestsByRequestIdAsync(requestId); var backtests = await _backtestRepository.GetBacktestsByRequestIdAsync(requestId);
return backtests; return backtests;
} }
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId,
int page, int pageSize, string sortBy = "score", string sortOrder = "desc") int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{ {
var (backtests, totalCount) = var (backtests, totalCount) =
@@ -298,7 +298,7 @@ namespace Managing.Application.Backtests
} }
public async Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync( public async Task<(IEnumerable<LightBacktest> 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) = var (backtests, totalCount) =
await _backtestRepository.GetBacktestsByRequestIdPaginatedAsync(requestId, page, pageSize, sortBy, await _backtestRepository.GetBacktestsByRequestIdPaginatedAsync(requestId, page, pageSize, sortBy,
@@ -384,7 +384,7 @@ namespace Managing.Application.Backtests
} }
} }
public async Task<bool> DeleteBacktestsByRequestIdAsync(string requestId) public async Task<bool> DeleteBacktestsByRequestIdAsync(Guid requestId)
{ {
try try
{ {
@@ -418,6 +418,17 @@ namespace Managing.Application.Backtests
public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest) public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest)
{ {
_backtestRepository.InsertBundleBacktestRequestForUser(user, 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<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user) public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user)
@@ -430,12 +441,12 @@ namespace Managing.Application.Backtests
return await _backtestRepository.GetBundleBacktestRequestsByUserAsync(user); return await _backtestRepository.GetBundleBacktestRequestsByUserAsync(user);
} }
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id) public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id)
{ {
return _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id); return _backtestRepository.GetBundleBacktestRequestByIdForUser(user, id);
} }
public async Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, string id) public async Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id)
{ {
return await _backtestRepository.GetBundleBacktestRequestByIdForUserAsync(user, id); return await _backtestRepository.GetBundleBacktestRequestByIdForUserAsync(user, id);
} }
@@ -450,12 +461,12 @@ namespace Managing.Application.Backtests
await _backtestRepository.UpdateBundleBacktestRequestAsync(bundleRequest); await _backtestRepository.UpdateBundleBacktestRequestAsync(bundleRequest);
} }
public void DeleteBundleBacktestRequestByIdForUser(User user, string id) public void DeleteBundleBacktestRequestByIdForUser(User user, Guid id)
{ {
_backtestRepository.DeleteBundleBacktestRequestByIdForUser(user, 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); await _backtestRepository.DeleteBundleBacktestRequestByIdForUserAsync(user, id);
} }
@@ -480,5 +491,65 @@ namespace Managing.Application.Backtests
if (string.IsNullOrWhiteSpace(requestId) || response == null) return; if (string.IsNullOrWhiteSpace(requestId) || response == null) return;
await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response); await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response);
} }
/// <summary>
/// Triggers the BundleBacktestGrain to process a bundle request synchronously (fire and forget)
/// </summary>
private void TriggerBundleBacktestGrain(Guid bundleRequestId)
{
try
{
var bundleBacktestGrain = _grainFactory.GetGrain<IBundleBacktestGrain>(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);
}
}
/// <summary>
/// Triggers the BundleBacktestGrain to process a bundle request asynchronously
/// </summary>
private Task TriggerBundleBacktestGrainAsync(Guid bundleRequestId)
{
try
{
var bundleBacktestGrain = _grainFactory.GetGrain<IBundleBacktestGrain>(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;
}
}
} }
} }

View File

@@ -131,7 +131,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams); var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams);
// Generate requestId if not provided // 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 // Create backtest result with conditional candles and indicators values
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals, var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals,

View File

@@ -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;
/// <summary>
/// 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
/// </summary>
[StatelessWorker]
public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
{
private readonly ILogger<BundleBacktestGrain> _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<BundleBacktestGrain> 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<IBacktester>();
var messengerService = scope.ServiceProvider.GetRequiredService<IMessengerService>();
// 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<BundleBacktestRequest> 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<List<RunBacktestRequest>>(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<List<RunBacktestRequest>>(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<string> 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<LightIndicator>()
};
}
// 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
/// <summary>
/// Handles reminder callbacks for automatic retry of failed bundle backtests
/// </summary>
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<IBacktester>();
// 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);
}
}
/// <summary>
/// Registers a retry reminder for this bundle request
/// </summary>
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());
}
}
/// <summary>
/// Unregisters the retry reminder for this bundle request
/// </summary>
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
}

View File

@@ -277,7 +277,7 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
bundleRequest.User, // No user context in worker bundleRequest.User, // No user context in worker
true, true,
runBacktestRequest.WithCandles, 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); _logger.LogInformation("Processed backtest for bundle request {RequestId}", bundleRequest.RequestId);

View File

@@ -13,7 +13,7 @@ public class NotifyBundleBacktestWorker : BaseWorker<NotifyBundleBacktestWorker>
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IHubContext<BacktestHub> _hubContext; private readonly IHubContext<BacktestHub> _hubContext;
private readonly ConcurrentDictionary<string, HashSet<string>> _sentBacktestIds = new(); private readonly ConcurrentDictionary<Guid, HashSet<string>> _sentBacktestIds = new();
public NotifyBundleBacktestWorker( public NotifyBundleBacktestWorker(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
@@ -39,7 +39,6 @@ public class NotifyBundleBacktestWorker : BaseWorker<NotifyBundleBacktestWorker>
foreach (var bundle in runningBundles) foreach (var bundle in runningBundles)
{ {
var requestId = bundle.RequestId; var requestId = bundle.RequestId;
if (string.IsNullOrEmpty(requestId)) continue;
// Fetch all backtests for this bundle // Fetch all backtests for this bundle
var (backtests, _) = backtester.GetBacktestsByRequestIdPaginated(requestId, 1, 100); var (backtests, _) = backtester.GetBacktestsByRequestIdPaginated(requestId, 1, 100);

View File

@@ -501,10 +501,12 @@ public static class ApiBootstrap
services.AddHostedService<GeneticAlgorithmWorker>(); services.AddHostedService<GeneticAlgorithmWorker>();
} }
if (configuration.GetValue<bool>("WorkerBundleBacktest", false)) // DEPRECATED: BundleBacktestWorker has been replaced by BundleBacktestGrain
{ // Bundle backtest processing is now handled by Orleans grain triggered directly from Backtester.cs
services.AddHostedService<BundleBacktestWorker>(); // if (configuration.GetValue<bool>("WorkerBundleBacktest", false))
} // {
// services.AddHostedService<BundleBacktestWorker>();
// }
return services; return services;
} }

View File

@@ -52,7 +52,7 @@ public class Backtest
[Required] public List<KeyValuePair<DateTime, decimal>> WalletBalances { get; set; } [Required] public List<KeyValuePair<DateTime, decimal>> WalletBalances { get; set; }
[Required] public User User { get; set; } [Required] public User User { get; set; }
[Required] public double Score { get; set; } [Required] public double Score { get; set; }
public string RequestId { get; set; } public Guid RequestId { get; set; }
public object? Metadata { get; set; } public object? Metadata { get; set; }
public string ScoreMessage { get; set; } = string.Empty; public string ScoreMessage { get; set; } = string.Empty;

View File

@@ -10,7 +10,7 @@ public class BundleBacktestRequest
{ {
public BundleBacktestRequest() public BundleBacktestRequest()
{ {
RequestId = Guid.NewGuid().ToString(); RequestId = Guid.NewGuid();
CreatedAt = DateTime.UtcNow; CreatedAt = DateTime.UtcNow;
Status = BundleBacktestRequestStatus.Pending; Status = BundleBacktestRequestStatus.Pending;
Results = new List<string>(); Results = new List<string>();
@@ -21,7 +21,7 @@ public class BundleBacktestRequest
/// Constructor that allows setting a specific ID /// Constructor that allows setting a specific ID
/// </summary> /// </summary>
/// <param name="requestId">The specific ID to use</param> /// <param name="requestId">The specific ID to use</param>
public BundleBacktestRequest(string requestId) public BundleBacktestRequest(Guid requestId)
{ {
RequestId = requestId; RequestId = requestId;
CreatedAt = DateTime.UtcNow; CreatedAt = DateTime.UtcNow;
@@ -34,7 +34,7 @@ public class BundleBacktestRequest
/// Unique identifier for the bundle backtest request /// Unique identifier for the bundle backtest request
/// </summary> /// </summary>
[Required] [Required]
public string RequestId { get; set; } public Guid RequestId { get; set; }
/// <summary> /// <summary>
/// The user who created this request /// The user who created this request

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,61 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class ChangeRequestIdToGuid : Migration
{
/// <inheritdoc />
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;
");
}
/// <inheritdoc />
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;
");
}
}
}

View File

@@ -176,10 +176,9 @@ namespace Managing.Infrastructure.Databases.Migrations
.IsRequired() .IsRequired()
.HasColumnType("jsonb"); .HasColumnType("jsonb");
b.Property<string>("RequestId") b.Property<Guid>("RequestId")
.IsRequired()
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnType("character varying(255)"); .HasColumnType("uuid");
b.Property<double>("Score") b.Property<double>("Score")
.HasColumnType("double precision"); .HasColumnType("double precision");
@@ -339,10 +338,9 @@ namespace Managing.Infrastructure.Databases.Migrations
b.Property<string>("ProgressInfo") b.Property<string>("ProgressInfo")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("RequestId") b.Property<Guid>("RequestId")
.IsRequired()
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnType("character varying(255)"); .HasColumnType("uuid");
b.Property<string>("ResultsJson") b.Property<string>("ResultsJson")
.IsRequired() .IsRequired()

View File

@@ -15,8 +15,7 @@ public class BacktestEntity
public string Identifier { get; set; } = string.Empty; public string Identifier { get; set; } = string.Empty;
[Required] [Required]
[MaxLength(255)] public Guid RequestId { get; set; }
public string RequestId { get; set; } = string.Empty;
[Required] [Required]
[Column(TypeName = "decimal(18,8)")] [Column(TypeName = "decimal(18,8)")]

View File

@@ -12,8 +12,7 @@ public class BundleBacktestRequestEntity
public int Id { get; set; } public int Id { get; set; }
[Required] [Required]
[MaxLength(255)] public Guid RequestId { get; set; }
public string RequestId { get; set; } = string.Empty;
// Foreign key to User entity // Foreign key to User entity
public int? UserId { get; set; } public int? UserId { get; set; }

View File

@@ -91,7 +91,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
return entities.Select(PostgreSqlMappers.Map); return entities.Select(PostgreSqlMappers.Map);
} }
public IEnumerable<Backtest> GetBacktestsByRequestId(string requestId) public IEnumerable<Backtest> GetBacktestsByRequestId(Guid requestId)
{ {
var entities = _context.Backtests var entities = _context.Backtests
.AsNoTracking() .AsNoTracking()
@@ -101,7 +101,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
return entities.Select(PostgreSqlMappers.Map); return entities.Select(PostgreSqlMappers.Map);
} }
public async Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(string requestId) public async Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(Guid requestId)
{ {
var entities = await _context.Backtests var entities = await _context.Backtests
.AsNoTracking() .AsNoTracking()
@@ -112,7 +112,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
return entities.Select(PostgreSqlMappers.Map); return entities.Select(PostgreSqlMappers.Map);
} }
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(Guid requestId,
int page, int pageSize, string sortBy = "score", string sortOrder = "desc") int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{ {
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
@@ -185,7 +185,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
} }
public async Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync( public async Task<(IEnumerable<LightBacktest> 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(); 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 var entities = _context.Backtests
.AsTracking() .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 var entities = await _context.Backtests
.AsTracking() .AsTracking()
@@ -580,7 +580,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
return entities.Select(PostgreSqlMappers.Map); return entities.Select(PostgreSqlMappers.Map);
} }
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id) public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, Guid id)
{ {
var entity = _context.BundleBacktestRequests var entity = _context.BundleBacktestRequests
.AsNoTracking() .AsNoTracking()
@@ -590,7 +590,7 @@ public class PostgreSqlBacktestRepository : IBacktestRepository
return entity != null ? PostgreSqlMappers.Map(entity) : null; return entity != null ? PostgreSqlMappers.Map(entity) : null;
} }
public async Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, string id) public async Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, Guid id)
{ {
var entity = await _context.BundleBacktestRequests var entity = await _context.BundleBacktestRequests
.AsNoTracking() .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 var entity = _context.BundleBacktestRequests
.AsTracking() .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 var entity = await _context.BundleBacktestRequests
.AsTracking() .AsTracking()

View File

@@ -1,4 +1,4 @@
import React, {useState} from 'react'; import React, {useEffect, useState} from 'react';
import {AccountClient, BacktestClient} from '../../generated/ManagingApi'; import {AccountClient, BacktestClient} from '../../generated/ManagingApi';
import type { import type {
MoneyManagementRequest, MoneyManagementRequest,
@@ -61,9 +61,6 @@ const BacktestBundleForm: React.FC = () => {
const { data: accounts, isSuccess } = useQuery({ const { data: accounts, isSuccess } = useQuery({
queryFn: async () => { queryFn: async () => {
const fetchedAccounts = await accountClient.account_GetAccounts(); const fetchedAccounts = await accountClient.account_GetAccounts();
if (fetchedAccounts.length > 0 && accountName === 'default') {
setAccountName(fetchedAccounts[0].name);
}
return fetchedAccounts; return fetchedAccounts;
}, },
queryKey: ['accounts'], queryKey: ['accounts'],
@@ -88,13 +85,20 @@ const BacktestBundleForm: React.FC = () => {
const [flipOnlyInProfit, setFlipOnlyInProfit] = useState(false); const [flipOnlyInProfit, setFlipOnlyInProfit] = useState(false);
const [closeEarly, setCloseEarly] = useState(false); const [closeEarly, setCloseEarly] = useState(false);
const [startingCapital, setStartingCapital] = useState(10000); const [startingCapital, setStartingCapital] = useState(10000);
const [accountName, setAccountName] = useState(accounts?.[0]?.name ?? ''); const [accountName, setAccountName] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const { scenario, setCustomScenario } = useCustomScenario(); 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 // Placeholder for cart summary
const totalBacktests = const totalBacktests =
moneyManagementVariants.length * timeRangeVariants.length * (selectedAssets.length || 1); moneyManagementVariants.length * timeRangeVariants.length * (selectedAssets.length || 1);
@@ -159,6 +163,7 @@ const BacktestBundleForm: React.FC = () => {
const client = new BacktestClient({} as any, apiUrl); const client = new BacktestClient({} as any, apiUrl);
const requests = generateRequests(); const requests = generateRequests();
if (!strategyName) throw new Error('Strategy name is required'); 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'); if (requests.length === 0) throw new Error('No backtest variants to run');
await client.backtest_RunBundle({ name: strategyName, requests }); await client.backtest_RunBundle({ name: strategyName, requests });
setSuccess('Bundle backtest started successfully!'); setSuccess('Bundle backtest started successfully!');
@@ -195,12 +200,17 @@ const BacktestBundleForm: React.FC = () => {
className="select select-bordered w-full" className="select select-bordered w-full"
value={accountName} value={accountName}
onChange={e => setAccountName(e.target.value)} onChange={e => setAccountName(e.target.value)}
disabled={!accounts || accounts.length === 0}
> >
{accounts?.map(account => ( {!accounts || accounts.length === 0 ? (
<option value="">Loading accounts...</option>
) : (
accounts.map(account => (
<option key={account.name} value={account.name}> <option key={account.name} value={account.name}>
{account.name} {account.name}
</option> </option>
))} ))
)}
</select> </select>
</div> </div>