update pagination

This commit is contained in:
2025-07-16 21:37:05 +07:00
parent f51fd5a5f7
commit b547c01787
13 changed files with 750 additions and 91 deletions

View File

@@ -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

View File

@@ -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,7 +168,12 @@ 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 totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
@@ -182,6 +191,56 @@ public class BacktestController : BaseController
return Ok(response); 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 response = new PaginatedBacktestsResponse
{
Backtests = backtests,
TotalCount = totalCount,
CurrentPage = page,
PageSize = pageSize,
TotalPages = totalPages,
HasNextPage = page < totalPages,
HasPreviousPage = page > 1
};
return Ok(response);
}
/// <summary> /// <summary>
/// Runs a backtest with the specified configuration. /// Runs a backtest with the specified configuration.
/// The returned backtest includes a complete TradingBotConfig that preserves all /// The returned backtest includes a complete TradingBotConfig that preserves all

View File

@@ -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; }
}

View File

@@ -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);

View File

@@ -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");
} }

View File

@@ -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);
}
} }
} }

View File

@@ -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,19 +110,44 @@ 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));
return (mappedBacktests, (int)totalCount); return (mappedBacktests, (int)totalCount);
} }
@@ -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);
}
} }

View File

@@ -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; }

View 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;
}
}
}

View File

@@ -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 */}

View File

@@ -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(/[?&]$/, "");

View File

@@ -46,20 +46,20 @@ const ALL_INDICATORS = [
// Form Interface // Form Interface
interface GeneticBundleFormData { interface GeneticBundleFormData {
ticker: Ticker ticker: Ticker
timeframe: Timeframe timeframe: Timeframe
startDate: string startDate: string
endDate: string endDate: string
balance: number balance: number
populationSize: number populationSize: number
generations: number generations: number
mutationRate: number mutationRate: number
selectionMethod: GeneticSelectionMethod selectionMethod: GeneticSelectionMethod
crossoverMethod: GeneticCrossoverMethod crossoverMethod: GeneticCrossoverMethod
mutationMethod: GeneticMutationMethod mutationMethod: GeneticMutationMethod
elitismPercentage: number elitismPercentage: number
maxTakeProfit: number maxTakeProfit: number
eligibleIndicators: IndicatorType[] eligibleIndicators: IndicatorType[]
} }
const BacktestGeneticBundle: React.FC = () => { const BacktestGeneticBundle: React.FC = () => {
@@ -76,13 +76,19 @@ const BacktestGeneticBundle: React.FC = () => {
const [backtests, setBacktests] = useState<Backtest[]>([]) const [backtests, setBacktests] = useState<Backtest[]>([])
const [isLoadingBacktests, setIsLoadingBacktests] = useState(false) const [isLoadingBacktests, setIsLoadingBacktests] = useState(false)
const [isFormCollapsed, setIsFormCollapsed] = useState(false) const [isFormCollapsed, setIsFormCollapsed] = useState(false)
// Pagination state // Pagination state
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(50) const [pageSize, setPageSize] = useState(50)
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: {
@@ -225,17 +231,20 @@ const BacktestGeneticBundle: React.FC = () => {
setSelectedRequest(request) setSelectedRequest(request)
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)
@@ -261,15 +270,17 @@ const BacktestGeneticBundle: React.FC = () => {
// Handle page change // Handle page change
const handlePageChange = async (newPage: number) => { const handlePageChange = async (newPage: number) => {
if (!selectedRequest || newPage < 1 || newPage > totalPages) return if (!selectedRequest || newPage < 1 || newPage > totalPages) return
setIsLoadingBacktests(true) setIsLoadingBacktests(true)
setCurrentPage(newPage) setCurrentPage(newPage)
try { try {
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(() => [
{ {
@@ -298,7 +336,7 @@ const BacktestGeneticBundle: React.FC = () => {
const currentGen = value || 0 const currentGen = value || 0
const percentage = generations > 0 ? Math.round((currentGen / generations) * 100) : 0 const percentage = generations > 0 ? Math.round((currentGen / generations) * 100) : 0
const status = row.original.status const status = row.original.status
// Calculate color based on percentage (red to green) // Calculate color based on percentage (red to green)
const getProgressColor = (percent: number) => { const getProgressColor = (percent: number) => {
if (percent <= 25) return 'progress-error' if (percent <= 25) return 'progress-error'
@@ -317,9 +355,9 @@ const BacktestGeneticBundle: React.FC = () => {
{currentGen} / {generations} {currentGen} / {generations}
</div> </div>
</div> </div>
<progress <progress
className={`progress w-56 ${getProgressColor(percentage)}`} className={`progress w-56 ${getProgressColor(percentage)}`}
value={percentage} value={percentage}
max="100" max="100"
/> />
</div> </div>
@@ -534,13 +572,15 @@ const BacktestGeneticBundle: React.FC = () => {
<label className="label"> <label className="label">
<span className="label-text">Selection Method</span> <span className="label-text">Selection Method</span>
</label> </label>
<select <select
className="select select-bordered w-full" className="select select-bordered w-full"
{...register('selectionMethod')} {...register('selectionMethod')}
> >
<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>
@@ -550,22 +590,27 @@ const BacktestGeneticBundle: React.FC = () => {
<label className="label"> <label className="label">
<span className="label-text">Crossover Method</span> <span className="label-text">Crossover Method</span>
</label> </label>
<select <select
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>
@@ -573,15 +618,16 @@ const BacktestGeneticBundle: React.FC = () => {
<label className="label"> <label className="label">
<span className="label-text">Mutation Method</span> <span className="label-text">Mutation Method</span>
</label> </label>
<select <select
className="select select-bordered w-full" className="select select-bordered w-full"
{...register('mutationMethod')} {...register('mutationMethod')}
> >
<option value={GeneticMutationMethod.Displacement}>Displacement</option> <option value={GeneticMutationMethod.Displacement}>Displacement</option>
<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>
@@ -725,9 +771,9 @@ const BacktestGeneticBundle: React.FC = () => {
<strong>Progress:</strong> {selectedRequest.currentGeneration || 0} / {selectedRequest.generations} <strong>Progress:</strong> {selectedRequest.currentGeneration || 0} / {selectedRequest.generations}
{selectedRequest.currentGeneration && selectedRequest.generations > 0 && ( {selectedRequest.currentGeneration && selectedRequest.generations > 0 && (
<div className="mt-1"> <div className="mt-1">
<progress <progress
className="progress progress-primary w-full h-2" className="progress progress-primary w-full h-2"
value={Math.round(((selectedRequest.currentGeneration || 0) / selectedRequest.generations) * 100)} value={Math.round(((selectedRequest.currentGeneration || 0) / selectedRequest.generations) * 100)}
max="100" max="100"
/> />
</div> </div>
@@ -778,38 +824,40 @@ 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>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Fitness vs Score vs Win Rate */} {/* Fitness vs Score vs Win Rate */}
<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>
{/* TP% vs SL% vs PnL */} {/* TP% vs SL% vs PnL */}
<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}
/> />
</> </>
) : ( ) : (

View File

@@ -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"