update pagination
This commit is contained in:
@@ -102,7 +102,7 @@ Key Principles
|
|||||||
- Before creating new object or new method/function check if there a code that can be called
|
- Before creating new object or new method/function check if there a code that can be called
|
||||||
- Most the time you will need to update multiple layer of code files. Make sure to reference all the method that you created when required
|
- Most the time you will need to update multiple layer of code files. Make sure to reference all the method that you created when required
|
||||||
- When you think its necessary update all the code from the database to the front end
|
- When you think its necessary update all the code from the database to the front end
|
||||||
- Do not update ManagingApi.ts, once you made a change on the backend endpoint, execute the command to regenerate ManagingApi.ts on the frontend; cd src/Managing.Nswag && dotnet build
|
- Do not update ManagingApi.ts, once you made a change on the backend endpoint, launch run the the Managing.Api project to run the API before running Nswag, then execute the command to regenerate ManagingApi.ts on the frontend; cd src/Managing.Nswag && dotnet build
|
||||||
- Do not reference new react library if a component already exist in mollecules or atoms
|
- Do not reference new react library if a component already exist in mollecules or atoms
|
||||||
- After finishing the editing, build the project
|
- After finishing the editing, build the project
|
||||||
- you have to pass from controller -> application -> repository, do not inject repository inside controllers
|
- you have to pass from controller -> application -> repository, do not inject repository inside controllers
|
||||||
|
|||||||
@@ -141,13 +141,17 @@ public class BacktestController : BaseController
|
|||||||
/// <param name="requestId">The request ID to filter backtests by.</param>
|
/// <param name="requestId">The request ID to filter backtests by.</param>
|
||||||
/// <param name="page">Page number (defaults to 1)</param>
|
/// <param name="page">Page number (defaults to 1)</param>
|
||||||
/// <param name="pageSize">Number of items per page (defaults to 50, max 100)</param>
|
/// <param name="pageSize">Number of items per page (defaults to 50, max 100)</param>
|
||||||
|
/// <param name="sortBy">Field to sort by (defaults to "score")</param>
|
||||||
|
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
||||||
/// <returns>A paginated list of backtests associated with the specified request ID.</returns>
|
/// <returns>A paginated list of backtests associated with the specified request ID.</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("ByRequestId/{requestId}/Paginated")]
|
[Route("ByRequestId/{requestId}/Paginated")]
|
||||||
public async Task<ActionResult<PaginatedBacktestsResponse>> GetBacktestsByRequestIdPaginated(
|
public async Task<ActionResult<PaginatedBacktestsResponse>> GetBacktestsByRequestIdPaginated(
|
||||||
string requestId,
|
string requestId,
|
||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 50)
|
int pageSize = 50,
|
||||||
|
string sortBy = "score",
|
||||||
|
string sortOrder = "desc")
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(requestId))
|
if (string.IsNullOrEmpty(requestId))
|
||||||
{
|
{
|
||||||
@@ -164,8 +168,63 @@ public class BacktestController : BaseController
|
|||||||
return BadRequest("Page size must be between 1 and 100");
|
return BadRequest("Page size must be between 1 and 100");
|
||||||
}
|
}
|
||||||
|
|
||||||
var (backtests, totalCount) = _backtester.GetBacktestsByRequestIdPaginated(requestId, page, pageSize);
|
if (sortOrder != "asc" && sortOrder != "desc")
|
||||||
|
{
|
||||||
|
return BadRequest("Sort order must be 'asc' or 'desc'");
|
||||||
|
}
|
||||||
|
|
||||||
|
var (backtests, totalCount) = _backtester.GetBacktestsByRequestIdPaginated(requestId, page, pageSize, sortBy, sortOrder);
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||||
|
|
||||||
|
var response = new PaginatedBacktestsResponse
|
||||||
|
{
|
||||||
|
Backtests = backtests,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
HasNextPage = page < totalPages,
|
||||||
|
HasPreviousPage = page > 1
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves paginated backtests for the authenticated user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="page">Page number (defaults to 1)</param>
|
||||||
|
/// <param name="pageSize">Number of items per page (defaults to 50, max 100)</param>
|
||||||
|
/// <param name="sortBy">Field to sort by (defaults to "score")</param>
|
||||||
|
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
||||||
|
/// <returns>A paginated list of backtests for the user.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[Route("Paginated")]
|
||||||
|
public async Task<ActionResult<PaginatedBacktestsResponse>> GetBacktestsPaginated(
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
string sortBy = "score",
|
||||||
|
string sortOrder = "desc")
|
||||||
|
{
|
||||||
|
var user = await GetUser();
|
||||||
|
|
||||||
|
if (page < 1)
|
||||||
|
{
|
||||||
|
return BadRequest("Page must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageSize < 1 || pageSize > 100)
|
||||||
|
{
|
||||||
|
return BadRequest("Page size must be between 1 and 100");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortOrder != "asc" && sortOrder != "desc")
|
||||||
|
{
|
||||||
|
return BadRequest("Sort order must be 'asc' or 'desc'");
|
||||||
|
}
|
||||||
|
|
||||||
|
var (backtests, totalCount) = _backtester.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder);
|
||||||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||||
|
|
||||||
var response = new PaginatedBacktestsResponse
|
var response = new PaginatedBacktestsResponse
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using Managing.Domain.Backtests;
|
||||||
|
|
||||||
|
namespace Managing.Api.Models.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for paginated backtest results
|
||||||
|
/// </summary>
|
||||||
|
public class PaginatedBacktestsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list of backtests for the current page
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Backtest> Backtests { get; set; } = new List<Backtest>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of backtests across all pages
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current page number
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentPage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items per page
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of pages
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether there are more pages available
|
||||||
|
/// </summary>
|
||||||
|
public bool HasNextPage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether there are previous pages available
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPreviousPage { get; set; }
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ public interface IBacktestRepository
|
|||||||
void InsertBacktestForUser(User user, Backtest result);
|
void InsertBacktestForUser(User user, Backtest result);
|
||||||
IEnumerable<Backtest> GetBacktestsByUser(User user);
|
IEnumerable<Backtest> GetBacktestsByUser(User user);
|
||||||
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
|
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
|
||||||
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize);
|
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||||
|
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||||
Backtest GetBacktestByIdForUser(User user, string id);
|
Backtest GetBacktestByIdForUser(User user, string id);
|
||||||
void DeleteBacktestByIdForUser(User user, string id);
|
void DeleteBacktestByIdForUser(User user, string id);
|
||||||
void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
|
void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
|
||||||
|
|||||||
@@ -54,11 +54,12 @@ namespace Managing.Application.Abstractions.Services
|
|||||||
bool DeleteBacktests();
|
bool DeleteBacktests();
|
||||||
IEnumerable<Backtest> GetBacktestsByUser(User user);
|
IEnumerable<Backtest> GetBacktestsByUser(User user);
|
||||||
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
|
IEnumerable<Backtest> GetBacktestsByRequestId(string requestId);
|
||||||
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize);
|
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||||
Backtest GetBacktestByIdForUser(User user, string id);
|
Backtest GetBacktestByIdForUser(User user, string id);
|
||||||
bool DeleteBacktestByUser(User user, string id);
|
bool DeleteBacktestByUser(User user, string id);
|
||||||
bool DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
|
bool DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids);
|
||||||
bool DeleteBacktestsByUser(User user);
|
bool DeleteBacktestsByUser(User user);
|
||||||
|
(IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -448,9 +448,9 @@ namespace Managing.Application.Backtesting
|
|||||||
return backtests;
|
return backtests;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize)
|
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||||
{
|
{
|
||||||
var (backtests, totalCount) = _backtestRepository.GetBacktestsByRequestIdPaginated(requestId, page, pageSize);
|
var (backtests, totalCount) = _backtestRepository.GetBacktestsByRequestIdPaginated(requestId, page, pageSize, sortBy, sortOrder);
|
||||||
return (backtests, totalCount);
|
return (backtests, totalCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,5 +530,11 @@ namespace Managing.Application.Backtesting
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||||
|
{
|
||||||
|
var (backtests, totalCount) = _backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder);
|
||||||
|
return (backtests, totalCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,50 @@ public class BacktestRepository : IBacktestRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
// User-specific operations
|
// User-specific operations
|
||||||
public void InsertBacktestForUser(User user, Backtest backtest)
|
public void InsertBacktestForUser(User user, Backtest result)
|
||||||
{
|
{
|
||||||
backtest.User = user;
|
ValidateBacktestData(result);
|
||||||
_backtestRepository.InsertOne(MongoMappers.Map(backtest));
|
result.User = user;
|
||||||
|
var dto = MongoMappers.Map(result);
|
||||||
|
_backtestRepository.InsertOne(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that all numeric fields in the backtest are of the correct type
|
||||||
|
/// </summary>
|
||||||
|
private void ValidateBacktestData(Backtest backtest)
|
||||||
|
{
|
||||||
|
// Ensure FinalPnl is a valid decimal
|
||||||
|
if (backtest.FinalPnl.GetType() != typeof(decimal))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"FinalPnl must be of type decimal, but got {backtest.FinalPnl.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure other numeric fields are correct
|
||||||
|
if (backtest.GrowthPercentage.GetType() != typeof(decimal))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"GrowthPercentage must be of type decimal, but got {backtest.GrowthPercentage.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backtest.HodlPercentage.GetType() != typeof(decimal))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"HodlPercentage must be of type decimal, but got {backtest.HodlPercentage.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backtest.Score.GetType() != typeof(double))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Score must be of type double, but got {backtest.Score.GetType().Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backtest.WinRate.GetType() != typeof(int))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"WinRate must be of type int, but got {backtest.WinRate.GetType().Name}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Backtest> GetBacktestsByUser(User user)
|
public IEnumerable<Backtest> GetBacktestsByUser(User user)
|
||||||
@@ -43,7 +83,8 @@ public class BacktestRepository : IBacktestRepository
|
|||||||
return backtests.Select(b => MongoMappers.Map(b));
|
return backtests.Select(b => MongoMappers.Map(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize)
|
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId,
|
||||||
|
int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||||
{
|
{
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
var collection = _backtestRepository.GetCollection(); // You may need to expose this in your repo
|
var collection = _backtestRepository.GetCollection(); // You may need to expose this in your repo
|
||||||
@@ -69,16 +110,41 @@ public class BacktestRepository : IBacktestRepository
|
|||||||
.Include(b => b.Metadata)
|
.Include(b => b.Metadata)
|
||||||
.Include(b => b.Config);
|
.Include(b => b.Config);
|
||||||
|
|
||||||
|
// Build sort definition
|
||||||
|
var sortDefinition = sortBy.ToLower() switch
|
||||||
|
{
|
||||||
|
"score" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.Score)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.Score),
|
||||||
|
"finalpnl" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.FinalPnl)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.FinalPnl),
|
||||||
|
"winrate" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.WinRate)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.WinRate),
|
||||||
|
"growthpercentage" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.GrowthPercentage)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.GrowthPercentage),
|
||||||
|
"hodlpercentage" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.HodlPercentage)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.HodlPercentage),
|
||||||
|
_ => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.Score)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.Score)
|
||||||
|
};
|
||||||
|
|
||||||
var afterProjectionMs = stopwatch.ElapsedMilliseconds;
|
var afterProjectionMs = stopwatch.ElapsedMilliseconds;
|
||||||
var backtests = collection
|
var backtests = collection
|
||||||
.Find(filter)
|
.Find(filter)
|
||||||
.Project<BacktestDto>(projection)
|
.Project<BacktestDto>(projection)
|
||||||
|
.Sort(sortDefinition)
|
||||||
.Skip((page - 1) * pageSize)
|
.Skip((page - 1) * pageSize)
|
||||||
.Limit(pageSize)
|
.Limit(pageSize)
|
||||||
.ToList();
|
.ToList();
|
||||||
var afterToListMs = stopwatch.ElapsedMilliseconds;
|
var afterToListMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
Console.WriteLine($"[BacktestRepo] Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Projection: {afterProjectionMs - afterCountMs}ms, ToList: {afterToListMs - afterProjectionMs}ms, Total: {afterToListMs}ms");
|
Console.WriteLine(
|
||||||
|
$"[BacktestRepo] Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Projection: {afterProjectionMs - afterCountMs}ms, ToList: {afterToListMs - afterProjectionMs}ms, Total: {afterToListMs}ms");
|
||||||
|
|
||||||
var mappedBacktests = backtests.Select(b => MongoMappers.Map(b));
|
var mappedBacktests = backtests.Select(b => MongoMappers.Map(b));
|
||||||
|
|
||||||
@@ -131,4 +197,72 @@ public class BacktestRepository : IBacktestRepository
|
|||||||
_backtestRepository.DeleteById(backtest.Id.ToString());
|
_backtestRepository.DeleteById(backtest.Id.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (IEnumerable<Backtest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page,
|
||||||
|
int pageSize, string sortBy = "score", string sortOrder = "desc")
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
var collection = _backtestRepository.GetCollection();
|
||||||
|
|
||||||
|
var filter = Builders<BacktestDto>.Filter.Eq(b => b.User.Name, user.Name);
|
||||||
|
|
||||||
|
var afterQueryMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
var totalCount = collection.CountDocuments(filter);
|
||||||
|
var afterCountMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
var projection = Builders<BacktestDto>.Projection
|
||||||
|
.Include(b => b.Identifier)
|
||||||
|
.Include(b => b.FinalPnl)
|
||||||
|
.Include(b => b.WinRate)
|
||||||
|
.Include(b => b.GrowthPercentage)
|
||||||
|
.Include(b => b.HodlPercentage)
|
||||||
|
.Include(b => b.User)
|
||||||
|
.Include(b => b.Statistics)
|
||||||
|
.Include(b => b.StartDate)
|
||||||
|
.Include(b => b.EndDate)
|
||||||
|
.Include(b => b.Score)
|
||||||
|
.Include(b => b.RequestId)
|
||||||
|
.Include(b => b.Metadata)
|
||||||
|
.Include(b => b.Config);
|
||||||
|
|
||||||
|
// Build sort definition
|
||||||
|
var sortDefinition = sortBy.ToLower() switch
|
||||||
|
{
|
||||||
|
"score" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.Score)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.Score),
|
||||||
|
"finalpnl" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.FinalPnl)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.FinalPnl),
|
||||||
|
"winrate" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.WinRate)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.WinRate),
|
||||||
|
"growthpercentage" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.GrowthPercentage)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.GrowthPercentage),
|
||||||
|
"hodlpercentage" => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.HodlPercentage)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.HodlPercentage),
|
||||||
|
_ => sortOrder == "desc"
|
||||||
|
? Builders<BacktestDto>.Sort.Descending(b => b.Score)
|
||||||
|
: Builders<BacktestDto>.Sort.Ascending(b => b.Score)
|
||||||
|
};
|
||||||
|
|
||||||
|
var afterProjectionMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
var backtests = collection
|
||||||
|
.Find(filter)
|
||||||
|
.Project<BacktestDto>(projection)
|
||||||
|
.Sort(sortDefinition)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Limit(pageSize)
|
||||||
|
.ToList();
|
||||||
|
var afterToListMs = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[BacktestRepo] User Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Projection: {afterProjectionMs - afterCountMs}ms, ToList: {afterToListMs - afterProjectionMs}ms, Total: {afterToListMs}ms");
|
||||||
|
|
||||||
|
var mappedBacktests = backtests.Select(b => MongoMappers.Map(b));
|
||||||
|
|
||||||
|
return (mappedBacktests, (int)totalCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections
|
|||||||
public class BacktestDto : Document
|
public class BacktestDto : Document
|
||||||
{
|
{
|
||||||
public decimal FinalPnl { get; set; }
|
public decimal FinalPnl { get; set; }
|
||||||
|
|
||||||
public int WinRate { get; set; }
|
public int WinRate { get; set; }
|
||||||
public decimal GrowthPercentage { get; set; }
|
public decimal GrowthPercentage { get; set; }
|
||||||
public decimal HodlPercentage { get; set; }
|
public decimal HodlPercentage { get; set; }
|
||||||
|
|||||||
117
src/Managing.Infrastructure.Database/MongoDb/IndexService.cs
Normal file
117
src/Managing.Infrastructure.Database/MongoDb/IndexService.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using Managing.Infrastructure.Databases.MongoDb.Collections;
|
||||||
|
using Managing.Infrastructure.Databases.MongoDb.Configurations;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
|
||||||
|
namespace Managing.Infrastructure.Databases.MongoDb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service responsible for creating and managing MongoDB indexes
|
||||||
|
/// </summary>
|
||||||
|
public class IndexService
|
||||||
|
{
|
||||||
|
private readonly IMongoDatabase _database;
|
||||||
|
private readonly ILogger<IndexService> _logger;
|
||||||
|
|
||||||
|
public IndexService(
|
||||||
|
IOptions<ManagingDatabaseSettings> databaseSettings,
|
||||||
|
ILogger<IndexService> logger)
|
||||||
|
{
|
||||||
|
var settings = databaseSettings.Value;
|
||||||
|
var client = new MongoClient(settings.ConnectionString);
|
||||||
|
_database = client.GetDatabase(settings.DatabaseName);
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates all necessary indexes for the application
|
||||||
|
/// </summary>
|
||||||
|
public async Task CreateIndexesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Creating MongoDB indexes...");
|
||||||
|
|
||||||
|
// Create indexes for BacktestDto
|
||||||
|
await CreateBacktestIndexesAsync();
|
||||||
|
|
||||||
|
// Create indexes for GeneticRequestDto
|
||||||
|
await CreateGeneticRequestIndexesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("MongoDB indexes created successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to create MongoDB indexes");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates indexes for the BacktestDto collection
|
||||||
|
/// </summary>
|
||||||
|
private async Task CreateBacktestIndexesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var collection = _database.GetCollection<BacktestDto>("Backtests");
|
||||||
|
|
||||||
|
// Create index on RequestId for faster queries
|
||||||
|
var requestIdIndexKeys = Builders<BacktestDto>.IndexKeys.Ascending(b => b.RequestId);
|
||||||
|
var requestIdIndexOptions = new CreateIndexOptions { Name = "RequestId_Index" };
|
||||||
|
var requestIdIndexModel = new CreateIndexModel<BacktestDto>(requestIdIndexKeys, requestIdIndexOptions);
|
||||||
|
|
||||||
|
// Create index (MongoDB will ignore if it already exists)
|
||||||
|
await collection.Indexes.CreateOneAsync(requestIdIndexModel);
|
||||||
|
|
||||||
|
_logger.LogInformation("Backtest RequestId index created successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to create Backtest indexes");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates indexes for the GeneticRequestDto collection
|
||||||
|
/// </summary>
|
||||||
|
private async Task CreateGeneticRequestIndexesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var collection = _database.GetCollection<GeneticRequestDto>("GeneticRequests");
|
||||||
|
|
||||||
|
// Create index on RequestId for faster queries
|
||||||
|
var requestIdIndexKeys = Builders<GeneticRequestDto>.IndexKeys.Ascending(gr => gr.RequestId);
|
||||||
|
var requestIdIndexOptions = new CreateIndexOptions { Name = "RequestId_Index" };
|
||||||
|
var requestIdIndexModel = new CreateIndexModel<GeneticRequestDto>(requestIdIndexKeys, requestIdIndexOptions);
|
||||||
|
|
||||||
|
// Create index on User.Name for user-specific queries
|
||||||
|
var userIndexKeys = Builders<GeneticRequestDto>.IndexKeys.Ascending("User.Name");
|
||||||
|
var userIndexOptions = new CreateIndexOptions { Name = "User_Name_Index" };
|
||||||
|
var userIndexModel = new CreateIndexModel<GeneticRequestDto>(userIndexKeys, userIndexOptions);
|
||||||
|
|
||||||
|
// Create index on Status for filtering by status
|
||||||
|
var statusIndexKeys = Builders<GeneticRequestDto>.IndexKeys.Ascending(gr => gr.Status);
|
||||||
|
var statusIndexOptions = new CreateIndexOptions { Name = "Status_Index" };
|
||||||
|
var statusIndexModel = new CreateIndexModel<GeneticRequestDto>(statusIndexKeys, statusIndexOptions);
|
||||||
|
|
||||||
|
// Create indexes (MongoDB will ignore if they already exist)
|
||||||
|
await collection.Indexes.CreateManyAsync(new[]
|
||||||
|
{
|
||||||
|
requestIdIndexModel,
|
||||||
|
userIndexModel,
|
||||||
|
statusIndexModel
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("GeneticRequest indexes created successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to create GeneticRequest indexes");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,145 @@
|
|||||||
import {ChevronDownIcon, ChevronRightIcon, CogIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
|
import {ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, CogIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
|
import {useExpanded, useFilters, usePagination, useSortBy, useTable,} from 'react-table'
|
||||||
|
|
||||||
import useApiUrlStore from '../../../app/store/apiStore'
|
import useApiUrlStore from '../../../app/store/apiStore'
|
||||||
import useBacktestStore from '../../../app/store/backtestStore'
|
import useBacktestStore from '../../../app/store/backtestStore'
|
||||||
import type {Backtest} from '../../../generated/ManagingApi'
|
import type {Backtest} from '../../../generated/ManagingApi'
|
||||||
import {BacktestClient} from '../../../generated/ManagingApi'
|
import {BacktestClient} from '../../../generated/ManagingApi'
|
||||||
import {ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter, Table} from '../../mollecules'
|
import {ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter} from '../../mollecules'
|
||||||
import {UnifiedTradingModal} from '../index'
|
import {UnifiedTradingModal} from '../index'
|
||||||
import Toast from '../../mollecules/Toast/Toast'
|
import Toast from '../../mollecules/Toast/Toast'
|
||||||
|
|
||||||
import BacktestRowDetails from './backtestRowDetails'
|
import BacktestRowDetails from './backtestRowDetails'
|
||||||
|
|
||||||
|
// Custom Table component for server-side sorting
|
||||||
|
const ServerSortableTable = ({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
renderRowSubCompontent,
|
||||||
|
onSortChange,
|
||||||
|
currentSort
|
||||||
|
}: any) => {
|
||||||
|
const defaultColumn = React.useMemo<any>(
|
||||||
|
() => ({
|
||||||
|
Filter: SelectColumnFilter,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
prepareRow,
|
||||||
|
visibleColumns,
|
||||||
|
rows,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
defaultColumn,
|
||||||
|
},
|
||||||
|
useFilters,
|
||||||
|
useSortBy,
|
||||||
|
useExpanded,
|
||||||
|
usePagination
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSortClick = (column: any) => {
|
||||||
|
if (!onSortChange || !column.canSort) return;
|
||||||
|
|
||||||
|
const isCurrentlySorted = currentSort?.sortBy === column.id;
|
||||||
|
const newSortOrder = isCurrentlySorted && currentSort?.sortOrder === 'desc' ? 'asc' : 'desc';
|
||||||
|
|
||||||
|
onSortChange(column.id, newSortOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full mt-3 mb-3 overflow-x-auto">
|
||||||
|
<table {...getTableProps()} className="table-compact table">
|
||||||
|
<thead>
|
||||||
|
{headerGroups.map((headerGroup: any) => (
|
||||||
|
<tr {...headerGroup.getHeaderGroupProps()}>
|
||||||
|
{headerGroup.headers.map((column: any) => (
|
||||||
|
<th
|
||||||
|
{...column.getHeaderProps()}
|
||||||
|
onClick={() => handleSortClick(column)}
|
||||||
|
className={column.canSort ? 'cursor-pointer hover:bg-base-200' : ''}
|
||||||
|
>
|
||||||
|
<p className="mb-2 text-center">
|
||||||
|
{column.render('Header')}
|
||||||
|
</p>
|
||||||
|
<span className="relative">
|
||||||
|
{(() => {
|
||||||
|
// Map backend field names to column IDs for comparison
|
||||||
|
const backendToColumnMapping: { [key: string]: string } = {
|
||||||
|
'score': 'score',
|
||||||
|
'finalpnl': 'finalPnl',
|
||||||
|
'winrate': 'winRate',
|
||||||
|
'growthpercentage': 'growthPercentage',
|
||||||
|
'hodlpercentage': 'hodlPercentage',
|
||||||
|
};
|
||||||
|
const columnId = backendToColumnMapping[currentSort?.sortBy || ''] || currentSort?.sortBy;
|
||||||
|
|
||||||
|
return currentSort?.sortBy && columnId === column.id ? (
|
||||||
|
currentSort.sortOrder === 'desc' ? (
|
||||||
|
<ChevronDownIcon className="text-primary absolute right-0 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronUpIcon className="text-secondary absolute right-0 w-4" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
{column.canFilter ? column.render('Filter') : null}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody {...getTableBodyProps()}>
|
||||||
|
{rows.map((row: any) => {
|
||||||
|
prepareRow(row)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tr {...row.getRowProps()}>
|
||||||
|
{row.cells.map((cell: any) => {
|
||||||
|
return (
|
||||||
|
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
{row.isExpanded ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={visibleColumns.length}>
|
||||||
|
{renderRowSubCompontent({ row })}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface BacktestTableProps {
|
interface BacktestTableProps {
|
||||||
list: Backtest[] | undefined
|
list: Backtest[] | undefined
|
||||||
isFetching?: boolean
|
isFetching?: boolean
|
||||||
displaySummary?: boolean
|
displaySummary?: boolean
|
||||||
|
onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void
|
||||||
|
currentSort?: { sortBy: string; sortOrder: 'asc' | 'desc' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displaySummary = true}) => {
|
const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displaySummary = true, onSortChange, currentSort}) => {
|
||||||
const [rows, setRows] = useState<Backtest[]>([])
|
const [rows, setRows] = useState<Backtest[]>([])
|
||||||
const {apiUrl} = useApiUrlStore()
|
const {apiUrl} = useApiUrlStore()
|
||||||
const {removeBacktest} = useBacktestStore()
|
const {removeBacktest} = useBacktestStore()
|
||||||
@@ -52,6 +172,23 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
|||||||
const [showConfigDisplayModal, setShowConfigDisplayModal] = useState(false)
|
const [showConfigDisplayModal, setShowConfigDisplayModal] = useState(false)
|
||||||
const [selectedBacktestForConfigView, setSelectedBacktestForConfigView] = useState<Backtest | null>(null)
|
const [selectedBacktestForConfigView, setSelectedBacktestForConfigView] = useState<Backtest | null>(null)
|
||||||
|
|
||||||
|
// Handle sort change
|
||||||
|
const handleSortChange = (columnId: string, sortOrder: 'asc' | 'desc') => {
|
||||||
|
if (!onSortChange) return;
|
||||||
|
|
||||||
|
// Map column IDs to backend field names
|
||||||
|
const sortByMapping: { [key: string]: string } = {
|
||||||
|
'score': 'score',
|
||||||
|
'finalPnl': 'finalpnl',
|
||||||
|
'winRate': 'winrate',
|
||||||
|
'growthPercentage': 'growthpercentage',
|
||||||
|
'hodlPercentage': 'hodlpercentage',
|
||||||
|
};
|
||||||
|
|
||||||
|
const backendSortBy = sortByMapping[columnId] || 'score';
|
||||||
|
onSortChange(backendSortBy, sortOrder);
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenBotConfigModal = (backtest: Backtest) => {
|
const handleOpenBotConfigModal = (backtest: Backtest) => {
|
||||||
setSelectedBacktest(backtest)
|
setSelectedBacktest(backtest)
|
||||||
setShowBotConfigModal(true)
|
setShowBotConfigModal(true)
|
||||||
@@ -157,6 +294,8 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
disableFilters: true,
|
disableFilters: true,
|
||||||
|
disableSortBy: false,
|
||||||
|
sortType: 'basic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Filter: SelectColumnFilter,
|
Filter: SelectColumnFilter,
|
||||||
@@ -197,6 +336,7 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
|||||||
Header: 'Pnl $',
|
Header: 'Pnl $',
|
||||||
accessor: 'finalPnl',
|
accessor: 'finalPnl',
|
||||||
disableFilters: true,
|
disableFilters: true,
|
||||||
|
disableSortBy: false,
|
||||||
sortType: 'basic',
|
sortType: 'basic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -204,6 +344,8 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
|||||||
Header: 'Winrate',
|
Header: 'Winrate',
|
||||||
accessor: 'winRate',
|
accessor: 'winRate',
|
||||||
disableFilters: true,
|
disableFilters: true,
|
||||||
|
disableSortBy: false,
|
||||||
|
sortType: 'basic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Cell: ({cell}: any) => (
|
Cell: ({cell}: any) => (
|
||||||
@@ -212,6 +354,7 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
|||||||
Header: 'Hodl %',
|
Header: 'Hodl %',
|
||||||
accessor: 'hodlPercentage',
|
accessor: 'hodlPercentage',
|
||||||
disableFilters: true,
|
disableFilters: true,
|
||||||
|
disableSortBy: false,
|
||||||
sortType: 'basic',
|
sortType: 'basic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -221,6 +364,7 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
|||||||
Header: 'Pnl %',
|
Header: 'Pnl %',
|
||||||
accessor: 'growthPercentage',
|
accessor: 'growthPercentage',
|
||||||
disableFilters: true,
|
disableFilters: true,
|
||||||
|
disableSortBy: false,
|
||||||
sortType: 'basic',
|
sortType: 'basic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -525,7 +669,7 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Table
|
<ServerSortableTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
renderRowSubCompontent={({row}: any) => (
|
renderRowSubCompontent={({row}: any) => (
|
||||||
@@ -533,6 +677,8 @@ const BacktestTable: React.FC<BacktestTableProps> = ({list, isFetching, displayS
|
|||||||
backtest={row.original}
|
backtest={row.original}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
|
currentSort={currentSort}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Bot Configuration Modal */}
|
{/* Bot Configuration Modal */}
|
||||||
|
|||||||
@@ -580,7 +580,7 @@ export class BacktestClient extends AuthorizedApiBase {
|
|||||||
return Promise.resolve<Backtest[]>(null as any);
|
return Promise.resolve<Backtest[]>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
backtest_GetBacktestsByRequestIdPaginated(requestId: string, page: number | undefined, pageSize: number | undefined): Promise<PaginatedBacktestsResponse> {
|
backtest_GetBacktestsByRequestIdPaginated(requestId: string, page: number | undefined, pageSize: number | undefined, sortBy: string | null | undefined, sortOrder: string | null | undefined): Promise<PaginatedBacktestsResponse> {
|
||||||
let url_ = this.baseUrl + "/Backtest/ByRequestId/{requestId}/Paginated?";
|
let url_ = this.baseUrl + "/Backtest/ByRequestId/{requestId}/Paginated?";
|
||||||
if (requestId === undefined || requestId === null)
|
if (requestId === undefined || requestId === null)
|
||||||
throw new Error("The parameter 'requestId' must be defined.");
|
throw new Error("The parameter 'requestId' must be defined.");
|
||||||
@@ -593,6 +593,10 @@ export class BacktestClient extends AuthorizedApiBase {
|
|||||||
throw new Error("The parameter 'pageSize' cannot be null.");
|
throw new Error("The parameter 'pageSize' cannot be null.");
|
||||||
else if (pageSize !== undefined)
|
else if (pageSize !== undefined)
|
||||||
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
|
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
|
||||||
|
if (sortBy !== undefined && sortBy !== null)
|
||||||
|
url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&";
|
||||||
|
if (sortOrder !== undefined && sortOrder !== null)
|
||||||
|
url_ += "sortOrder=" + encodeURIComponent("" + sortOrder) + "&";
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
let options_: RequestInit = {
|
let options_: RequestInit = {
|
||||||
@@ -626,6 +630,53 @@ export class BacktestClient extends AuthorizedApiBase {
|
|||||||
return Promise.resolve<PaginatedBacktestsResponse>(null as any);
|
return Promise.resolve<PaginatedBacktestsResponse>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
backtest_GetBacktestsPaginated(page: number | undefined, pageSize: number | undefined, sortBy: string | null | undefined, sortOrder: string | null | undefined): Promise<PaginatedBacktestsResponse> {
|
||||||
|
let url_ = this.baseUrl + "/Backtest/Paginated?";
|
||||||
|
if (page === null)
|
||||||
|
throw new Error("The parameter 'page' cannot be null.");
|
||||||
|
else if (page !== undefined)
|
||||||
|
url_ += "page=" + encodeURIComponent("" + page) + "&";
|
||||||
|
if (pageSize === null)
|
||||||
|
throw new Error("The parameter 'pageSize' cannot be null.");
|
||||||
|
else if (pageSize !== undefined)
|
||||||
|
url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&";
|
||||||
|
if (sortBy !== undefined && sortBy !== null)
|
||||||
|
url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&";
|
||||||
|
if (sortOrder !== undefined && sortOrder !== null)
|
||||||
|
url_ += "sortOrder=" + encodeURIComponent("" + sortOrder) + "&";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processBacktest_GetBacktestsPaginated(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processBacktest_GetBacktestsPaginated(response: Response): Promise<PaginatedBacktestsResponse> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as PaginatedBacktestsResponse;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<PaginatedBacktestsResponse>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
backtest_Run(request: RunBacktestRequest): Promise<Backtest> {
|
backtest_Run(request: RunBacktestRequest): Promise<Backtest> {
|
||||||
let url_ = this.baseUrl + "/Backtest/Run";
|
let url_ = this.baseUrl + "/Backtest/Run";
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
const [totalBacktests, setTotalBacktests] = useState(0)
|
const [totalBacktests, setTotalBacktests] = useState(0)
|
||||||
const [totalPages, setTotalPages] = useState(0)
|
const [totalPages, setTotalPages] = useState(0)
|
||||||
|
|
||||||
|
// Sorting state
|
||||||
|
const [currentSort, setCurrentSort] = useState<{ sortBy: string; sortOrder: 'asc' | 'desc' }>({
|
||||||
|
sortBy: 'score',
|
||||||
|
sortOrder: 'desc'
|
||||||
|
})
|
||||||
|
|
||||||
// Form setup
|
// Form setup
|
||||||
const {register, handleSubmit, watch, setValue, formState: {errors}} = useForm<GeneticBundleFormData>({
|
const {register, handleSubmit, watch, setValue, formState: {errors}} = useForm<GeneticBundleFormData>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -226,16 +232,19 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
setIsViewModalOpen(true)
|
setIsViewModalOpen(true)
|
||||||
setIsLoadingBacktests(true)
|
setIsLoadingBacktests(true)
|
||||||
|
|
||||||
// Reset pagination state
|
// Reset pagination and sorting state
|
||||||
setCurrentPage(1)
|
setCurrentPage(1)
|
||||||
setTotalBacktests(0)
|
setTotalBacktests(0)
|
||||||
setTotalPages(0)
|
setTotalPages(0)
|
||||||
|
setCurrentSort({sortBy: 'score', sortOrder: 'desc'})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: PaginatedBacktestsResponse = await backtestClient.backtest_GetBacktestsByRequestIdPaginated(
|
const response: PaginatedBacktestsResponse = await backtestClient.backtest_GetBacktestsByRequestIdPaginated(
|
||||||
request.requestId,
|
request.requestId,
|
||||||
1,
|
1,
|
||||||
pageSize
|
pageSize,
|
||||||
|
currentSort.sortBy,
|
||||||
|
currentSort.sortOrder
|
||||||
)
|
)
|
||||||
setBacktests(response.backtests || [])
|
setBacktests(response.backtests || [])
|
||||||
setTotalBacktests(response.totalCount || 0)
|
setTotalBacktests(response.totalCount || 0)
|
||||||
@@ -269,7 +278,9 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
const response: PaginatedBacktestsResponse = await backtestClient.backtest_GetBacktestsByRequestIdPaginated(
|
const response: PaginatedBacktestsResponse = await backtestClient.backtest_GetBacktestsByRequestIdPaginated(
|
||||||
selectedRequest.requestId,
|
selectedRequest.requestId,
|
||||||
newPage,
|
newPage,
|
||||||
pageSize
|
pageSize,
|
||||||
|
currentSort.sortBy,
|
||||||
|
currentSort.sortOrder
|
||||||
)
|
)
|
||||||
setBacktests(response.backtests || [])
|
setBacktests(response.backtests || [])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -280,6 +291,33 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle sort change
|
||||||
|
const handleSortChange = async (sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||||
|
if (!selectedRequest) return
|
||||||
|
|
||||||
|
setIsLoadingBacktests(true)
|
||||||
|
setCurrentSort({sortBy, sortOrder})
|
||||||
|
setCurrentPage(1) // Reset to first page when sorting
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: PaginatedBacktestsResponse = await backtestClient.backtest_GetBacktestsByRequestIdPaginated(
|
||||||
|
selectedRequest.requestId,
|
||||||
|
1,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder
|
||||||
|
)
|
||||||
|
setBacktests(response.backtests || [])
|
||||||
|
setTotalBacktests(response.totalCount || 0)
|
||||||
|
setTotalPages(response.totalPages || 0)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching backtests:', error)
|
||||||
|
new Toast('Failed to load backtest details', false)
|
||||||
|
} finally {
|
||||||
|
setIsLoadingBacktests(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Table columns for genetic requests
|
// Table columns for genetic requests
|
||||||
const geneticRequestsColumns = useMemo(() => [
|
const geneticRequestsColumns = useMemo(() => [
|
||||||
{
|
{
|
||||||
@@ -540,7 +578,9 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<option value={GeneticSelectionMethod.Elite}>Elite Selection</option>
|
<option value={GeneticSelectionMethod.Elite}>Elite Selection</option>
|
||||||
<option value={GeneticSelectionMethod.Roulette}>Roulette Wheel</option>
|
<option value={GeneticSelectionMethod.Roulette}>Roulette Wheel</option>
|
||||||
<option value={GeneticSelectionMethod.StochasticUniversalSampling}>Stochastic Universal Sampling</option>
|
<option value={GeneticSelectionMethod.StochasticUniversalSampling}>Stochastic
|
||||||
|
Universal Sampling
|
||||||
|
</option>
|
||||||
<option value={GeneticSelectionMethod.Tournament}>Tournament Selection</option>
|
<option value={GeneticSelectionMethod.Tournament}>Tournament Selection</option>
|
||||||
<option value={GeneticSelectionMethod.Truncation}>Truncation Selection</option>
|
<option value={GeneticSelectionMethod.Truncation}>Truncation Selection</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -554,18 +594,23 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
className="select select-bordered w-full"
|
className="select select-bordered w-full"
|
||||||
{...register('crossoverMethod')}
|
{...register('crossoverMethod')}
|
||||||
>
|
>
|
||||||
<option value={GeneticCrossoverMethod.AlternatingPosition}>Alternating Position (AP)</option>
|
<option value={GeneticCrossoverMethod.AlternatingPosition}>Alternating Position
|
||||||
|
(AP)
|
||||||
|
</option>
|
||||||
<option value={GeneticCrossoverMethod.CutAndSplice}>Cut and Splice</option>
|
<option value={GeneticCrossoverMethod.CutAndSplice}>Cut and Splice</option>
|
||||||
<option value={GeneticCrossoverMethod.Cycle}>Cycle (CX)</option>
|
<option value={GeneticCrossoverMethod.Cycle}>Cycle (CX)</option>
|
||||||
<option value={GeneticCrossoverMethod.OnePoint}>One-Point (C1)</option>
|
<option value={GeneticCrossoverMethod.OnePoint}>One-Point (C1)</option>
|
||||||
<option value={GeneticCrossoverMethod.OrderBased}>Order-based (OX2)</option>
|
<option value={GeneticCrossoverMethod.OrderBased}>Order-based (OX2)</option>
|
||||||
<option value={GeneticCrossoverMethod.Ordered}>Ordered (OX1)</option>
|
<option value={GeneticCrossoverMethod.Ordered}>Ordered (OX1)</option>
|
||||||
<option value={GeneticCrossoverMethod.PartiallyMapped}>Partially Mapped (PMX)</option>
|
<option value={GeneticCrossoverMethod.PartiallyMapped}>Partially Mapped (PMX)
|
||||||
|
</option>
|
||||||
<option value={GeneticCrossoverMethod.PositionBased}>Position-based (POS)</option>
|
<option value={GeneticCrossoverMethod.PositionBased}>Position-based (POS)</option>
|
||||||
<option value={GeneticCrossoverMethod.ThreeParent}>Three Parent</option>
|
<option value={GeneticCrossoverMethod.ThreeParent}>Three Parent</option>
|
||||||
<option value={GeneticCrossoverMethod.TwoPoint}>Two-Point (C2)</option>
|
<option value={GeneticCrossoverMethod.TwoPoint}>Two-Point (C2)</option>
|
||||||
<option value={GeneticCrossoverMethod.Uniform}>Uniform</option>
|
<option value={GeneticCrossoverMethod.Uniform}>Uniform</option>
|
||||||
<option value={GeneticCrossoverMethod.VotingRecombination}>Voting Recombination (VR)</option>
|
<option value={GeneticCrossoverMethod.VotingRecombination}>Voting Recombination
|
||||||
|
(VR)
|
||||||
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -581,7 +626,8 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
<option value={GeneticMutationMethod.FlipBit}>Flip Bit</option>
|
<option value={GeneticMutationMethod.FlipBit}>Flip Bit</option>
|
||||||
<option value={GeneticMutationMethod.Insertion}>Insertion</option>
|
<option value={GeneticMutationMethod.Insertion}>Insertion</option>
|
||||||
<option value={GeneticMutationMethod.PartialShuffle}>Partial Shuffle (PSM)</option>
|
<option value={GeneticMutationMethod.PartialShuffle}>Partial Shuffle (PSM)</option>
|
||||||
<option value={GeneticMutationMethod.ReverseSequence}>Reverse Sequence (RSM)</option>
|
<option value={GeneticMutationMethod.ReverseSequence}>Reverse Sequence (RSM)
|
||||||
|
</option>
|
||||||
<option value={GeneticMutationMethod.Twors}>Twors</option>
|
<option value={GeneticMutationMethod.Twors}>Twors</option>
|
||||||
<option value={GeneticMutationMethod.Uniform}>Uniform</option>
|
<option value={GeneticMutationMethod.Uniform}>Uniform</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -778,7 +824,7 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
<div className="card bg-base-100 shadow-xl">
|
<div className="card bg-base-100 shadow-xl">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h3 className="card-title">Score vs Generation</h3>
|
<h3 className="card-title">Score vs Generation</h3>
|
||||||
<ScoreVsGeneration backtests={backtests} theme={theme} />
|
<ScoreVsGeneration backtests={backtests} theme={theme}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -788,7 +834,7 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
<div className="card bg-base-100 shadow-xl">
|
<div className="card bg-base-100 shadow-xl">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h3 className="card-title">Fitness vs Score vs Win Rate</h3>
|
<h3 className="card-title">Fitness vs Score vs Win Rate</h3>
|
||||||
<Fitness3DPlot backtests={backtests} theme={theme} />
|
<Fitness3DPlot backtests={backtests} theme={theme}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -796,20 +842,22 @@ const BacktestGeneticBundle: React.FC = () => {
|
|||||||
<div className="card bg-base-100 shadow-xl">
|
<div className="card bg-base-100 shadow-xl">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h3 className="card-title">Take Profit vs Stop Loss vs PnL</h3>
|
<h3 className="card-title">Take Profit vs Stop Loss vs PnL</h3>
|
||||||
<TPvsSLvsPnL3DPlot backtests={backtests} theme={theme} />
|
<TPvsSLvsPnL3DPlot backtests={backtests} theme={theme}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Strategy Comparison Radar Chart */}
|
{/* Strategy Comparison Radar Chart */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<IndicatorsComparison backtests={backtests} />
|
<IndicatorsComparison backtests={backtests}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BacktestTable
|
<BacktestTable
|
||||||
list={backtests}
|
list={backtests}
|
||||||
isFetching={false}
|
isFetching={false}
|
||||||
displaySummary={false}
|
displaySummary={false}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
|
currentSort={currentSort}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {ColorSwatchIcon, TrashIcon} from '@heroicons/react/solid'
|
import {ColorSwatchIcon, TrashIcon} from '@heroicons/react/solid'
|
||||||
import {useQuery} from '@tanstack/react-query'
|
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
|
|
||||||
import 'react-toastify/dist/ReactToastify.css'
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
@@ -8,8 +7,11 @@ import useBacktestStore from '../../app/store/backtestStore'
|
|||||||
import {Loader, Slider} from '../../components/atoms'
|
import {Loader, Slider} from '../../components/atoms'
|
||||||
import {Modal, Toast} from '../../components/mollecules'
|
import {Modal, Toast} from '../../components/mollecules'
|
||||||
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
|
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
|
||||||
|
import type {Backtest} from '../../generated/ManagingApi'
|
||||||
import {BacktestClient} from '../../generated/ManagingApi'
|
import {BacktestClient} from '../../generated/ManagingApi'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
const BacktestScanner: React.FC = () => {
|
const BacktestScanner: React.FC = () => {
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const [showModalRemoveBacktest, setShowModalRemoveBacktest] = useState(false)
|
const [showModalRemoveBacktest, setShowModalRemoveBacktest] = useState(false)
|
||||||
@@ -18,30 +20,50 @@ const BacktestScanner: React.FC = () => {
|
|||||||
winRate: 50,
|
winRate: 50,
|
||||||
score: 50
|
score: 50
|
||||||
})
|
})
|
||||||
const { apiUrl } = useApiUrlStore()
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const { backtests: backtestingResult, setBacktests, setLoading } = useBacktestStore()
|
const [totalBacktests, setTotalBacktests] = useState(0)
|
||||||
const client = new BacktestClient({}, apiUrl)
|
const [totalPages, setTotalPages] = useState(0)
|
||||||
|
const [currentSort, setCurrentSort] = useState<{ sortBy: string; sortOrder: 'asc' | 'desc' }>({
|
||||||
const { isLoading, refetch, data: backtests } = useQuery({
|
sortBy: 'score',
|
||||||
queryFn: () => client.backtest_Backtests(),
|
sortOrder: 'desc'
|
||||||
queryKey: ['backtests'],
|
|
||||||
})
|
})
|
||||||
|
const [backtests, setBacktests] = useState<Backtest[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const { apiUrl } = useApiUrlStore()
|
||||||
if (backtests) {
|
const { setBacktests: setBacktestsFromStore, setLoading } = useBacktestStore()
|
||||||
setBacktests(backtests)
|
const backtestClient = new BacktestClient({}, apiUrl)
|
||||||
|
|
||||||
|
// Fetch paginated/sorted backtests
|
||||||
|
const fetchBacktests = async (page = 1, sort = currentSort) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await backtestClient.backtest_GetBacktestsPaginated(page, PAGE_SIZE, sort.sortBy, sort.sortOrder)
|
||||||
|
setBacktests((response.backtests as Backtest[]) || [])
|
||||||
|
setTotalBacktests(response.totalCount || 0)
|
||||||
|
setTotalPages(response.totalPages || 0)
|
||||||
|
} catch (err) {
|
||||||
|
new Toast('Failed to load backtests', false)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [backtests, setBacktests])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
fetchBacktests(currentPage, currentSort)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [currentPage, currentSort])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBacktestsFromStore(backtests)
|
||||||
setLoading(isLoading)
|
setLoading(isLoading)
|
||||||
}, [isLoading, setLoading])
|
}, [backtests, setBacktestsFromStore, setLoading, isLoading])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (backtestingResult && showModalRemoveBacktest) {
|
if (backtests && showModalRemoveBacktest) {
|
||||||
calculateFilteredCount()
|
calculateFilteredCount()
|
||||||
}
|
}
|
||||||
}, [backtestingResult, showModalRemoveBacktest])
|
}, [backtests, showModalRemoveBacktest])
|
||||||
|
|
||||||
const openModalRemoveBacktests = () => {
|
const openModalRemoveBacktests = () => {
|
||||||
setShowModalRemoveBacktest(true)
|
setShowModalRemoveBacktest(true)
|
||||||
@@ -50,14 +72,14 @@ const BacktestScanner: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const calculateFilteredCount = (formData?: any) => {
|
const calculateFilteredCount = (formData?: any) => {
|
||||||
if (!backtestingResult) {
|
if (!backtests) {
|
||||||
setFilteredCount(0)
|
setFilteredCount(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = formData || filterValues
|
const filters = formData || filterValues
|
||||||
|
|
||||||
const filteredBacktests = backtestingResult.filter((backtest: any) => {
|
const filteredBacktests = backtests.filter((backtest: any) => {
|
||||||
// Ensure values are numbers and handle potential null/undefined values
|
// Ensure values are numbers and handle potential null/undefined values
|
||||||
const backtestWinRate = Number(backtest.winRate) || 0
|
const backtestWinRate = Number(backtest.winRate) || 0
|
||||||
const backtestScore = Number(backtest.score) || 0
|
const backtestScore = Number(backtest.score) || 0
|
||||||
@@ -78,7 +100,7 @@ const BacktestScanner: React.FC = () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Filtered count:', filteredBacktests.length, 'Total:', backtestingResult.length)
|
console.log('Filtered count:', filteredBacktests.length, 'Total:', backtests.length)
|
||||||
setFilteredCount(filteredBacktests.length)
|
setFilteredCount(filteredBacktests.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,11 +125,11 @@ const BacktestScanner: React.FC = () => {
|
|||||||
const notify = new Toast(`Deleting Backtests...`)
|
const notify = new Toast(`Deleting Backtests...`)
|
||||||
closeModalRemoveBacktest()
|
closeModalRemoveBacktest()
|
||||||
|
|
||||||
if (!backtestingResult) {
|
if (!backtests) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const backTestToDelete = backtestingResult.filter((backtest: any) => {
|
const backTestToDelete = backtests.filter((backtest: any) => {
|
||||||
// Ensure values are numbers and handle potential null/undefined values
|
// Ensure values are numbers and handle potential null/undefined values
|
||||||
const backtestWinRate = Number(backtest.winRate) || 0
|
const backtestWinRate = Number(backtest.winRate) || 0
|
||||||
const backtestScore = Number(backtest.score) || 0
|
const backtestScore = Number(backtest.score) || 0
|
||||||
@@ -125,11 +147,11 @@ const BacktestScanner: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const backtestIds = backTestToDelete.map((backtest: any) => backtest.id)
|
const backtestIds = backTestToDelete.map((backtest: any) => backtest.id)
|
||||||
await client.backtest_DeleteBacktests({ backtestIds })
|
await backtestClient.backtest_DeleteBacktests({ backtestIds })
|
||||||
notify.update('success', `${backTestToDelete.length} backtests deleted successfully`)
|
notify.update('success', `${backTestToDelete.length} backtests deleted successfully`)
|
||||||
|
|
||||||
// Refetch backtests to update the list
|
// Refetch backtests to update the list
|
||||||
refetch()
|
fetchBacktests(currentPage, currentSort)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
notify.update('error', err?.message || 'An error occurred while deleting backtests')
|
notify.update('error', err?.message || 'An error occurred while deleting backtests')
|
||||||
}
|
}
|
||||||
@@ -143,6 +165,18 @@ const BacktestScanner: React.FC = () => {
|
|||||||
setShowModal(false)
|
setShowModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sorting handler
|
||||||
|
const handleSortChange = (sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||||
|
setCurrentSort({ sortBy, sortOrder })
|
||||||
|
setCurrentPage(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination handler
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
if (newPage < 1 || (totalPages && newPage > totalPages)) return
|
||||||
|
setCurrentPage(newPage)
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Loader />
|
return <Loader />
|
||||||
}
|
}
|
||||||
@@ -163,7 +197,24 @@ const BacktestScanner: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BacktestTable list={backtestingResult} isFetching={isLoading} />
|
<BacktestTable
|
||||||
|
list={backtests}
|
||||||
|
isFetching={isLoading}
|
||||||
|
onSortChange={handleSortChange}
|
||||||
|
currentSort={currentSort}
|
||||||
|
/>
|
||||||
|
{/* Pagination controls */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center gap-2 my-4">
|
||||||
|
<button className="btn btn-sm" onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage <= 1}>
|
||||||
|
«
|
||||||
|
</button>
|
||||||
|
<span className="text-sm">Page {currentPage} of {totalPages}</span>
|
||||||
|
<button className="btn btn-sm" onClick={() => handlePageChange(currentPage + 1)} disabled={currentPage >= totalPages}>
|
||||||
|
»
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<UnifiedTradingModal
|
<UnifiedTradingModal
|
||||||
mode="backtest"
|
mode="backtest"
|
||||||
|
|||||||
Reference in New Issue
Block a user