Add agent index with pagination
This commit is contained in:
@@ -664,6 +664,198 @@ public class DataController : ControllerBase
|
|||||||
return Ok(agentIndex);
|
return Ok(agentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a paginated list of agent summaries for the agent index page
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
|
||||||
|
/// <param name="page">Page number (defaults to 1)</param>
|
||||||
|
/// <param name="pageSize">Number of items per page (defaults to 10, max 100)</param>
|
||||||
|
/// <param name="sortBy">Field to sort by (TotalPnL, PnLLast24h, TotalROI, ROILast24h, Wins, Losses, AverageWinRate, ActiveStrategiesCount, TotalVolume, VolumeLast24h)</param>
|
||||||
|
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
||||||
|
/// <returns>A paginated list of agent summaries sorted by the specified field</returns>
|
||||||
|
[HttpGet("GetAgentIndexPaginated")]
|
||||||
|
public async Task<ActionResult<PaginatedAgentIndexResponse>> GetAgentIndexPaginated(
|
||||||
|
string timeFilter = "Total",
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
string sortBy = "TotalPnL",
|
||||||
|
string sortOrder = "desc")
|
||||||
|
{
|
||||||
|
// Validate time filter
|
||||||
|
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
|
||||||
|
if (!validTimeFilters.Contains(timeFilter))
|
||||||
|
{
|
||||||
|
timeFilter = "Total"; // Default to Total if invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate pagination parameters
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate sort order
|
||||||
|
if (sortOrder != "asc" && sortOrder != "desc")
|
||||||
|
{
|
||||||
|
return BadRequest("Sort order must be 'asc' or 'desc'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate sort by field
|
||||||
|
var validSortFields = new[] { "TotalPnL", "PnLLast24h", "TotalROI", "ROILast24h", "Wins", "Losses", "AverageWinRate", "ActiveStrategiesCount", "TotalVolume", "VolumeLast24h" };
|
||||||
|
if (!validSortFields.Contains(sortBy))
|
||||||
|
{
|
||||||
|
sortBy = "TotalPnL"; // Default to TotalPnL if invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
string cacheKey = $"AgentIndex_{timeFilter}";
|
||||||
|
|
||||||
|
// Check if the agent index is already cached
|
||||||
|
var cachedIndex = _cacheService.GetValue<AgentIndexViewModel>(cacheKey);
|
||||||
|
|
||||||
|
List<AgentSummaryViewModel> allAgentSummaries;
|
||||||
|
|
||||||
|
if (cachedIndex != null)
|
||||||
|
{
|
||||||
|
allAgentSummaries = cachedIndex.AgentSummaries.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Get all agents and their strategies
|
||||||
|
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
|
||||||
|
|
||||||
|
allAgentSummaries = new List<AgentSummaryViewModel>();
|
||||||
|
|
||||||
|
// Create summaries for each agent
|
||||||
|
foreach (var agent in agentsWithStrategies)
|
||||||
|
{
|
||||||
|
var user = agent.Key;
|
||||||
|
var strategies = agent.Value;
|
||||||
|
|
||||||
|
if (strategies.Count == 0)
|
||||||
|
{
|
||||||
|
continue; // Skip agents with no strategies
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all positions from all strategies
|
||||||
|
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
|
||||||
|
|
||||||
|
// Calculate agent metrics
|
||||||
|
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
|
||||||
|
decimal pnlLast24h = TradingBox.GetPnLInTimeRange(allPositions, "24H");
|
||||||
|
|
||||||
|
decimal totalROI = TradingBox.GetROIInTimeRange(allPositions, timeFilter);
|
||||||
|
decimal roiLast24h = TradingBox.GetROIInTimeRange(allPositions, "24H");
|
||||||
|
|
||||||
|
(int wins, int losses) = TradingBox.GetWinLossCountInTimeRange(allPositions, timeFilter);
|
||||||
|
|
||||||
|
// Calculate trading volumes
|
||||||
|
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
|
||||||
|
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
|
||||||
|
|
||||||
|
// Calculate win rate
|
||||||
|
int averageWinRate = 0;
|
||||||
|
if (wins + losses > 0)
|
||||||
|
{
|
||||||
|
averageWinRate = (wins * 100) / (wins + losses);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to agent summaries
|
||||||
|
var agentSummary = new AgentSummaryViewModel
|
||||||
|
{
|
||||||
|
AgentName = user.AgentName,
|
||||||
|
TotalPnL = totalPnL,
|
||||||
|
PnLLast24h = pnlLast24h,
|
||||||
|
TotalROI = totalROI,
|
||||||
|
ROILast24h = roiLast24h,
|
||||||
|
Wins = wins,
|
||||||
|
Losses = losses,
|
||||||
|
AverageWinRate = averageWinRate,
|
||||||
|
ActiveStrategiesCount = strategies.Count,
|
||||||
|
TotalVolume = totalVolume,
|
||||||
|
VolumeLast24h = volumeLast24h
|
||||||
|
};
|
||||||
|
|
||||||
|
allAgentSummaries.Add(agentSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the results for 5 minutes
|
||||||
|
var agentIndex = new AgentIndexViewModel
|
||||||
|
{
|
||||||
|
TimeFilter = timeFilter,
|
||||||
|
AgentSummaries = allAgentSummaries
|
||||||
|
};
|
||||||
|
_cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
var sortedSummaries = sortBy switch
|
||||||
|
{
|
||||||
|
"TotalPnL" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.TotalPnL)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.TotalPnL),
|
||||||
|
"PnLLast24h" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.PnLLast24h)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.PnLLast24h),
|
||||||
|
"TotalROI" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.TotalROI)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.TotalROI),
|
||||||
|
"ROILast24h" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.ROILast24h)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.ROILast24h),
|
||||||
|
"Wins" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.Wins)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.Wins),
|
||||||
|
"Losses" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.Losses)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.Losses),
|
||||||
|
"AverageWinRate" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.AverageWinRate)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.AverageWinRate),
|
||||||
|
"ActiveStrategiesCount" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.ActiveStrategiesCount)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.ActiveStrategiesCount),
|
||||||
|
"TotalVolume" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.TotalVolume)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.TotalVolume),
|
||||||
|
"VolumeLast24h" => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.VolumeLast24h)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.VolumeLast24h),
|
||||||
|
_ => sortOrder == "desc"
|
||||||
|
? allAgentSummaries.OrderByDescending(a => a.TotalPnL)
|
||||||
|
: allAgentSummaries.OrderBy(a => a.TotalPnL)
|
||||||
|
};
|
||||||
|
|
||||||
|
var totalCount = allAgentSummaries.Count;
|
||||||
|
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
var paginatedSummaries = sortedSummaries
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var response = new PaginatedAgentIndexResponse
|
||||||
|
{
|
||||||
|
AgentSummaries = paginatedSummaries,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
HasNextPage = page < totalPages,
|
||||||
|
HasPreviousPage = page > 1,
|
||||||
|
TimeFilter = timeFilter,
|
||||||
|
SortBy = sortBy,
|
||||||
|
SortOrder = sortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves balance history for a specific agent within a date range
|
/// Retrieves balance history for a specific agent within a date range
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
namespace Managing.Api.Models.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model for paginated agent index results
|
||||||
|
/// </summary>
|
||||||
|
public class PaginatedAgentIndexResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list of agent summaries for the current page
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<AgentSummaryViewModel> AgentSummaries { get; set; } = new List<AgentSummaryViewModel>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of agents 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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Time filter applied to the data
|
||||||
|
/// </summary>
|
||||||
|
public string TimeFilter { get; set; } = "Total";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Field used for sorting
|
||||||
|
/// </summary>
|
||||||
|
public string SortBy { get; set; } = "TotalPnL";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort order (asc or desc)
|
||||||
|
/// </summary>
|
||||||
|
public string SortOrder { get; set; } = "desc";
|
||||||
|
}
|
||||||
@@ -677,7 +677,7 @@ export class BacktestClient extends AuthorizedApiBase {
|
|||||||
return Promise.resolve<PaginatedBacktestsResponse>(null as any);
|
return Promise.resolve<PaginatedBacktestsResponse>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
backtest_Run(request: RunBacktestRequest): Promise<Backtest> {
|
backtest_Run(request: RunBacktestRequest): Promise<LightBacktest> {
|
||||||
let url_ = this.baseUrl + "/Backtest/Run";
|
let url_ = this.baseUrl + "/Backtest/Run";
|
||||||
url_ = url_.replace(/[?&]$/, "");
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
@@ -699,13 +699,13 @@ export class BacktestClient extends AuthorizedApiBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected processBacktest_Run(response: Response): Promise<Backtest> {
|
protected processBacktest_Run(response: Response): Promise<LightBacktest> {
|
||||||
const status = response.status;
|
const status = response.status;
|
||||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
return response.text().then((_responseText) => {
|
return response.text().then((_responseText) => {
|
||||||
let result200: any = null;
|
let result200: any = null;
|
||||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Backtest;
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as LightBacktest;
|
||||||
return result200;
|
return result200;
|
||||||
});
|
});
|
||||||
} else if (status !== 200 && status !== 204) {
|
} else if (status !== 200 && status !== 204) {
|
||||||
@@ -713,7 +713,7 @@ export class BacktestClient extends AuthorizedApiBase {
|
|||||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve<Backtest>(null as any);
|
return Promise.resolve<LightBacktest>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
backtest_RunBundle(request: RunBundleBacktestRequest): Promise<BundleBacktestRequest> {
|
backtest_RunBundle(request: RunBundleBacktestRequest): Promise<BundleBacktestRequest> {
|
||||||
@@ -1882,6 +1882,92 @@ export class DataClient extends AuthorizedApiBase {
|
|||||||
return Promise.resolve<PlatformSummaryViewModel>(null as any);
|
return Promise.resolve<PlatformSummaryViewModel>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data_GetAgentIndex(timeFilter: string | null | undefined): Promise<AgentIndexViewModel> {
|
||||||
|
let url_ = this.baseUrl + "/Data/GetAgentIndex?";
|
||||||
|
if (timeFilter !== undefined && timeFilter !== null)
|
||||||
|
url_ += "timeFilter=" + encodeURIComponent("" + timeFilter) + "&";
|
||||||
|
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.processData_GetAgentIndex(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processData_GetAgentIndex(response: Response): Promise<AgentIndexViewModel> {
|
||||||
|
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 AgentIndexViewModel;
|
||||||
|
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<AgentIndexViewModel>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
data_GetAgentIndexPaginated(timeFilter: string | null | undefined, page: number | undefined, pageSize: number | undefined, sortBy: string | null | undefined, sortOrder: string | null | undefined): Promise<PaginatedAgentIndexResponse> {
|
||||||
|
let url_ = this.baseUrl + "/Data/GetAgentIndexPaginated?";
|
||||||
|
if (timeFilter !== undefined && timeFilter !== null)
|
||||||
|
url_ += "timeFilter=" + encodeURIComponent("" + timeFilter) + "&";
|
||||||
|
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.processData_GetAgentIndexPaginated(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processData_GetAgentIndexPaginated(response: Response): Promise<PaginatedAgentIndexResponse> {
|
||||||
|
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 PaginatedAgentIndexResponse;
|
||||||
|
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<PaginatedAgentIndexResponse>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
data_GetAgentBalances(agentName: string | null | undefined, startDate: Date | undefined, endDate: Date | null | undefined): Promise<AgentBalanceHistory> {
|
data_GetAgentBalances(agentName: string | null | undefined, startDate: Date | undefined, endDate: Date | null | undefined): Promise<AgentBalanceHistory> {
|
||||||
let url_ = this.baseUrl + "/Data/GetAgentBalances?";
|
let url_ = this.baseUrl + "/Data/GetAgentBalances?";
|
||||||
if (agentName !== undefined && agentName !== null)
|
if (agentName !== undefined && agentName !== null)
|
||||||
@@ -2776,45 +2862,6 @@ export class TradingClient extends AuthorizedApiBase {
|
|||||||
this.baseUrl = baseUrl ?? "http://localhost:5000";
|
this.baseUrl = baseUrl ?? "http://localhost:5000";
|
||||||
}
|
}
|
||||||
|
|
||||||
trading_GetPositions(positionInitiator: PositionInitiator | undefined): Promise<Position[]> {
|
|
||||||
let url_ = this.baseUrl + "/Trading/GetPositions?";
|
|
||||||
if (positionInitiator === null)
|
|
||||||
throw new Error("The parameter 'positionInitiator' cannot be null.");
|
|
||||||
else if (positionInitiator !== undefined)
|
|
||||||
url_ += "positionInitiator=" + encodeURIComponent("" + positionInitiator) + "&";
|
|
||||||
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.processTrading_GetPositions(_response);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected processTrading_GetPositions(response: Response): Promise<Position[]> {
|
|
||||||
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 Position[];
|
|
||||||
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<Position[]>(null as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
trading_GetTrade(accountName: string | null | undefined, ticker: Ticker | undefined, exchangeOrderId: string | null | undefined): Promise<Trade> {
|
trading_GetTrade(accountName: string | null | undefined, ticker: Ticker | undefined, exchangeOrderId: string | null | undefined): Promise<Trade> {
|
||||||
let url_ = this.baseUrl + "/Trading/GetTrade?";
|
let url_ = this.baseUrl + "/Trading/GetTrade?";
|
||||||
if (accountName !== undefined && accountName !== null)
|
if (accountName !== undefined && accountName !== null)
|
||||||
@@ -3670,7 +3717,7 @@ export interface Backtest {
|
|||||||
|
|
||||||
export interface TradingBotConfig {
|
export interface TradingBotConfig {
|
||||||
accountName: string;
|
accountName: string;
|
||||||
moneyManagement: MoneyManagement;
|
moneyManagement: LightMoneyManagement;
|
||||||
ticker: Ticker;
|
ticker: Ticker;
|
||||||
timeframe: Timeframe;
|
timeframe: Timeframe;
|
||||||
isForWatchingOnly: boolean;
|
isForWatchingOnly: boolean;
|
||||||
@@ -3681,7 +3728,7 @@ export interface TradingBotConfig {
|
|||||||
flipPosition: boolean;
|
flipPosition: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
riskManagement?: RiskManagement | null;
|
riskManagement?: RiskManagement | null;
|
||||||
scenario?: Scenario | null;
|
scenario?: LightScenario | null;
|
||||||
scenarioName?: string | null;
|
scenarioName?: string | null;
|
||||||
maxPositionTimeHours?: number | null;
|
maxPositionTimeHours?: number | null;
|
||||||
closeEarlyWhenProfitable?: boolean;
|
closeEarlyWhenProfitable?: boolean;
|
||||||
@@ -3692,13 +3739,12 @@ export interface TradingBotConfig {
|
|||||||
useForDynamicStopLoss?: boolean;
|
useForDynamicStopLoss?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MoneyManagement {
|
export interface LightMoneyManagement {
|
||||||
name: string;
|
name: string;
|
||||||
timeframe: Timeframe;
|
timeframe: Timeframe;
|
||||||
stopLoss: number;
|
stopLoss: number;
|
||||||
takeProfit: number;
|
takeProfit: number;
|
||||||
leverage: number;
|
leverage: number;
|
||||||
user?: User | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Timeframe {
|
export enum Timeframe {
|
||||||
@@ -3734,14 +3780,13 @@ export enum RiskToleranceLevel {
|
|||||||
Aggressive = "Aggressive",
|
Aggressive = "Aggressive",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Scenario {
|
export interface LightScenario {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
indicators?: Indicator[] | null;
|
indicators?: LightIndicator[] | null;
|
||||||
loopbackPeriod?: number | null;
|
loopbackPeriod?: number | null;
|
||||||
user?: User | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Indicator {
|
export interface LightIndicator {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
type?: IndicatorType;
|
type?: IndicatorType;
|
||||||
signalType?: SignalType;
|
signalType?: SignalType;
|
||||||
@@ -3754,7 +3799,6 @@ export interface Indicator {
|
|||||||
smoothPeriods?: number | null;
|
smoothPeriods?: number | null;
|
||||||
stochPeriods?: number | null;
|
stochPeriods?: number | null;
|
||||||
cyclePeriods?: number | null;
|
cyclePeriods?: number | null;
|
||||||
user?: User | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IndicatorType {
|
export enum IndicatorType {
|
||||||
@@ -3786,7 +3830,7 @@ export interface Position {
|
|||||||
date: Date;
|
date: Date;
|
||||||
originDirection: TradeDirection;
|
originDirection: TradeDirection;
|
||||||
ticker: Ticker;
|
ticker: Ticker;
|
||||||
moneyManagement: MoneyManagement;
|
moneyManagement: LightMoneyManagement;
|
||||||
Open: Trade;
|
Open: Trade;
|
||||||
StopLoss: Trade;
|
StopLoss: Trade;
|
||||||
TakeProfit1: Trade;
|
TakeProfit1: Trade;
|
||||||
@@ -4047,6 +4091,22 @@ export interface LightBacktestResponse {
|
|||||||
scoreMessage: string;
|
scoreMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LightBacktest {
|
||||||
|
id?: string | null;
|
||||||
|
config?: TradingBotConfig | null;
|
||||||
|
finalPnl?: number;
|
||||||
|
winRate?: number;
|
||||||
|
growthPercentage?: number;
|
||||||
|
hodlPercentage?: number;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
maxDrawdown?: number | null;
|
||||||
|
fees?: number;
|
||||||
|
sharpeRatio?: number | null;
|
||||||
|
score?: number;
|
||||||
|
scoreMessage?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RunBacktestRequest {
|
export interface RunBacktestRequest {
|
||||||
config?: TradingBotConfigRequest | null;
|
config?: TradingBotConfigRequest | null;
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
@@ -4227,6 +4287,10 @@ export interface RunGeneticRequest {
|
|||||||
eligibleIndicators?: IndicatorType[] | null;
|
eligibleIndicators?: IndicatorType[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MoneyManagement extends LightMoneyManagement {
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StartBotRequest {
|
export interface StartBotRequest {
|
||||||
config?: TradingBotConfigRequest | null;
|
config?: TradingBotConfigRequest | null;
|
||||||
}
|
}
|
||||||
@@ -4285,6 +4349,39 @@ export interface Spotlight {
|
|||||||
tickerSignals: TickerSignal[];
|
tickerSignals: TickerSignal[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Scenario {
|
||||||
|
name?: string | null;
|
||||||
|
indicators?: Indicator[] | null;
|
||||||
|
loopbackPeriod?: number | null;
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Indicator {
|
||||||
|
name?: string | null;
|
||||||
|
candles?: FixedSizeQueueOfCandle | null;
|
||||||
|
type?: IndicatorType;
|
||||||
|
signalType?: SignalType;
|
||||||
|
minimumHistory?: number;
|
||||||
|
period?: number | null;
|
||||||
|
fastPeriods?: number | null;
|
||||||
|
slowPeriods?: number | null;
|
||||||
|
signalPeriods?: number | null;
|
||||||
|
multiplier?: number | null;
|
||||||
|
smoothPeriods?: number | null;
|
||||||
|
stochPeriods?: number | null;
|
||||||
|
cyclePeriods?: number | null;
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Anonymous {
|
||||||
|
maxSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FixedSizeQueueOfCandle extends Anonymous {
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TickerSignal {
|
export interface TickerSignal {
|
||||||
ticker: Ticker;
|
ticker: Ticker;
|
||||||
fiveMinutes: LightSignal[];
|
fiveMinutes: LightSignal[];
|
||||||
@@ -4344,6 +4441,10 @@ export interface PlatformSummaryViewModel {
|
|||||||
totalPlatformPnL?: number;
|
totalPlatformPnL?: number;
|
||||||
totalPlatformVolume?: number;
|
totalPlatformVolume?: number;
|
||||||
totalPlatformVolumeLast24h?: number;
|
totalPlatformVolumeLast24h?: number;
|
||||||
|
timeFilter?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentIndexViewModel {
|
||||||
agentSummaries?: AgentSummaryViewModel[] | null;
|
agentSummaries?: AgentSummaryViewModel[] | null;
|
||||||
timeFilter?: string | null;
|
timeFilter?: string | null;
|
||||||
}
|
}
|
||||||
@@ -4362,6 +4463,19 @@ export interface AgentSummaryViewModel {
|
|||||||
volumeLast24h?: number;
|
volumeLast24h?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginatedAgentIndexResponse {
|
||||||
|
agentSummaries?: AgentSummaryViewModel[] | null;
|
||||||
|
totalCount?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
hasPreviousPage?: boolean;
|
||||||
|
timeFilter?: string | null;
|
||||||
|
sortBy?: string | null;
|
||||||
|
sortOrder?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentBalanceHistory {
|
export interface AgentBalanceHistory {
|
||||||
agentName?: string | null;
|
agentName?: string | null;
|
||||||
agentBalances?: AgentBalance[] | null;
|
agentBalances?: AgentBalance[] | null;
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export interface Backtest {
|
|||||||
|
|
||||||
export interface TradingBotConfig {
|
export interface TradingBotConfig {
|
||||||
accountName: string;
|
accountName: string;
|
||||||
moneyManagement: MoneyManagement;
|
moneyManagement: LightMoneyManagement;
|
||||||
ticker: Ticker;
|
ticker: Ticker;
|
||||||
timeframe: Timeframe;
|
timeframe: Timeframe;
|
||||||
isForWatchingOnly: boolean;
|
isForWatchingOnly: boolean;
|
||||||
@@ -253,7 +253,7 @@ export interface TradingBotConfig {
|
|||||||
flipPosition: boolean;
|
flipPosition: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
riskManagement?: RiskManagement | null;
|
riskManagement?: RiskManagement | null;
|
||||||
scenario?: Scenario | null;
|
scenario?: LightScenario | null;
|
||||||
scenarioName?: string | null;
|
scenarioName?: string | null;
|
||||||
maxPositionTimeHours?: number | null;
|
maxPositionTimeHours?: number | null;
|
||||||
closeEarlyWhenProfitable?: boolean;
|
closeEarlyWhenProfitable?: boolean;
|
||||||
@@ -264,13 +264,12 @@ export interface TradingBotConfig {
|
|||||||
useForDynamicStopLoss?: boolean;
|
useForDynamicStopLoss?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MoneyManagement {
|
export interface LightMoneyManagement {
|
||||||
name: string;
|
name: string;
|
||||||
timeframe: Timeframe;
|
timeframe: Timeframe;
|
||||||
stopLoss: number;
|
stopLoss: number;
|
||||||
takeProfit: number;
|
takeProfit: number;
|
||||||
leverage: number;
|
leverage: number;
|
||||||
user?: User | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Timeframe {
|
export enum Timeframe {
|
||||||
@@ -306,14 +305,13 @@ export enum RiskToleranceLevel {
|
|||||||
Aggressive = "Aggressive",
|
Aggressive = "Aggressive",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Scenario {
|
export interface LightScenario {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
indicators?: Indicator[] | null;
|
indicators?: LightIndicator[] | null;
|
||||||
loopbackPeriod?: number | null;
|
loopbackPeriod?: number | null;
|
||||||
user?: User | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Indicator {
|
export interface LightIndicator {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
type?: IndicatorType;
|
type?: IndicatorType;
|
||||||
signalType?: SignalType;
|
signalType?: SignalType;
|
||||||
@@ -326,7 +324,6 @@ export interface Indicator {
|
|||||||
smoothPeriods?: number | null;
|
smoothPeriods?: number | null;
|
||||||
stochPeriods?: number | null;
|
stochPeriods?: number | null;
|
||||||
cyclePeriods?: number | null;
|
cyclePeriods?: number | null;
|
||||||
user?: User | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum IndicatorType {
|
export enum IndicatorType {
|
||||||
@@ -358,7 +355,7 @@ export interface Position {
|
|||||||
date: Date;
|
date: Date;
|
||||||
originDirection: TradeDirection;
|
originDirection: TradeDirection;
|
||||||
ticker: Ticker;
|
ticker: Ticker;
|
||||||
moneyManagement: MoneyManagement;
|
moneyManagement: LightMoneyManagement;
|
||||||
Open: Trade;
|
Open: Trade;
|
||||||
StopLoss: Trade;
|
StopLoss: Trade;
|
||||||
TakeProfit1: Trade;
|
TakeProfit1: Trade;
|
||||||
@@ -619,6 +616,22 @@ export interface LightBacktestResponse {
|
|||||||
scoreMessage: string;
|
scoreMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LightBacktest {
|
||||||
|
id?: string | null;
|
||||||
|
config?: TradingBotConfig | null;
|
||||||
|
finalPnl?: number;
|
||||||
|
winRate?: number;
|
||||||
|
growthPercentage?: number;
|
||||||
|
hodlPercentage?: number;
|
||||||
|
startDate?: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
maxDrawdown?: number | null;
|
||||||
|
fees?: number;
|
||||||
|
sharpeRatio?: number | null;
|
||||||
|
score?: number;
|
||||||
|
scoreMessage?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RunBacktestRequest {
|
export interface RunBacktestRequest {
|
||||||
config?: TradingBotConfigRequest | null;
|
config?: TradingBotConfigRequest | null;
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
@@ -799,6 +812,10 @@ export interface RunGeneticRequest {
|
|||||||
eligibleIndicators?: IndicatorType[] | null;
|
eligibleIndicators?: IndicatorType[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MoneyManagement extends LightMoneyManagement {
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StartBotRequest {
|
export interface StartBotRequest {
|
||||||
config?: TradingBotConfigRequest | null;
|
config?: TradingBotConfigRequest | null;
|
||||||
}
|
}
|
||||||
@@ -857,6 +874,39 @@ export interface Spotlight {
|
|||||||
tickerSignals: TickerSignal[];
|
tickerSignals: TickerSignal[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Scenario {
|
||||||
|
name?: string | null;
|
||||||
|
indicators?: Indicator[] | null;
|
||||||
|
loopbackPeriod?: number | null;
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Indicator {
|
||||||
|
name?: string | null;
|
||||||
|
candles?: FixedSizeQueueOfCandle | null;
|
||||||
|
type?: IndicatorType;
|
||||||
|
signalType?: SignalType;
|
||||||
|
minimumHistory?: number;
|
||||||
|
period?: number | null;
|
||||||
|
fastPeriods?: number | null;
|
||||||
|
slowPeriods?: number | null;
|
||||||
|
signalPeriods?: number | null;
|
||||||
|
multiplier?: number | null;
|
||||||
|
smoothPeriods?: number | null;
|
||||||
|
stochPeriods?: number | null;
|
||||||
|
cyclePeriods?: number | null;
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Anonymous {
|
||||||
|
maxSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FixedSizeQueueOfCandle extends Anonymous {
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TickerSignal {
|
export interface TickerSignal {
|
||||||
ticker: Ticker;
|
ticker: Ticker;
|
||||||
fiveMinutes: LightSignal[];
|
fiveMinutes: LightSignal[];
|
||||||
@@ -916,6 +966,10 @@ export interface PlatformSummaryViewModel {
|
|||||||
totalPlatformPnL?: number;
|
totalPlatformPnL?: number;
|
||||||
totalPlatformVolume?: number;
|
totalPlatformVolume?: number;
|
||||||
totalPlatformVolumeLast24h?: number;
|
totalPlatformVolumeLast24h?: number;
|
||||||
|
timeFilter?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentIndexViewModel {
|
||||||
agentSummaries?: AgentSummaryViewModel[] | null;
|
agentSummaries?: AgentSummaryViewModel[] | null;
|
||||||
timeFilter?: string | null;
|
timeFilter?: string | null;
|
||||||
}
|
}
|
||||||
@@ -934,6 +988,19 @@ export interface AgentSummaryViewModel {
|
|||||||
volumeLast24h?: number;
|
volumeLast24h?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginatedAgentIndexResponse {
|
||||||
|
agentSummaries?: AgentSummaryViewModel[] | null;
|
||||||
|
totalCount?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
hasPreviousPage?: boolean;
|
||||||
|
timeFilter?: string | null;
|
||||||
|
sortBy?: string | null;
|
||||||
|
sortOrder?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentBalanceHistory {
|
export interface AgentBalanceHistory {
|
||||||
agentName?: string | null;
|
agentName?: string | null;
|
||||||
agentBalances?: AgentBalance[] | null;
|
agentBalances?: AgentBalance[] | null;
|
||||||
|
|||||||
332
src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx
Normal file
332
src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import React, {useEffect, useMemo, useState} from 'react'
|
||||||
|
import {GridTile} from '../../components/mollecules'
|
||||||
|
import Table from '../../components/mollecules/Table/Table'
|
||||||
|
import useApiUrlStore from '../../app/store/apiStore'
|
||||||
|
import {type AgentSummaryViewModel, DataClient, type PaginatedAgentIndexResponse} from '../../generated/ManagingApi'
|
||||||
|
|
||||||
|
const TIME_FILTERS = [
|
||||||
|
{ label: '24H', value: '24H' },
|
||||||
|
{ label: '3D', value: '3D' },
|
||||||
|
{ label: '1W', value: '1W' },
|
||||||
|
{ label: '1M', value: '1M' },
|
||||||
|
{ label: '1Y', value: '1Y' },
|
||||||
|
{ label: 'Total', value: 'Total' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const SORT_OPTIONS = [
|
||||||
|
{ label: 'Total PnL', value: 'TotalPnL' },
|
||||||
|
{ label: '24H PnL', value: 'PnLLast24h' },
|
||||||
|
{ label: 'Total ROI', value: 'TotalROI' },
|
||||||
|
{ label: '24H ROI', value: 'ROILast24h' },
|
||||||
|
{ label: 'Wins', value: 'Wins' },
|
||||||
|
{ label: 'Losses', value: 'Losses' },
|
||||||
|
{ label: 'Win Rate', value: 'AverageWinRate' },
|
||||||
|
{ label: 'Active Strategies', value: 'ActiveStrategiesCount' },
|
||||||
|
{ label: 'Total Volume', value: 'TotalVolume' },
|
||||||
|
{ label: '24H Volume', value: 'VolumeLast24h' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function AgentIndex({ index }: { index: number }) {
|
||||||
|
const { apiUrl } = useApiUrlStore()
|
||||||
|
const [data, setData] = useState<PaginatedAgentIndexResponse | null>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(10)
|
||||||
|
|
||||||
|
// Filter and sort state
|
||||||
|
const [timeFilter, setTimeFilter] = useState('Total')
|
||||||
|
const [sortBy, setSortBy] = useState('TotalPnL')
|
||||||
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new DataClient({}, apiUrl)
|
||||||
|
const response = await client.data_GetAgentIndexPaginated(
|
||||||
|
timeFilter,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder
|
||||||
|
)
|
||||||
|
setData(response)
|
||||||
|
} catch (err) {
|
||||||
|
setError('Failed to fetch agent data. Please try again.')
|
||||||
|
console.error('Error fetching agent data:', err)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData()
|
||||||
|
}, [currentPage, pageSize, timeFilter, sortBy, sortOrder])
|
||||||
|
|
||||||
|
const handleSort = (columnId: string) => {
|
||||||
|
if (sortBy === columnId) {
|
||||||
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
setSortBy(columnId)
|
||||||
|
setSortOrder('desc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = useMemo(() => [
|
||||||
|
{
|
||||||
|
Header: 'Agent Name',
|
||||||
|
accessor: 'agentName',
|
||||||
|
Cell: ({ value }: { value: string }) => (
|
||||||
|
<span className="font-medium">{value || 'Unknown'}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Total PnL',
|
||||||
|
accessor: 'totalPnL',
|
||||||
|
Cell: ({ value }: { value: number }) => (
|
||||||
|
<span className={value >= 0 ? 'text-green-500' : 'text-red-500'}>
|
||||||
|
{value >= 0 ? '+' : ''}${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: '24H PnL',
|
||||||
|
accessor: 'pnLLast24h',
|
||||||
|
Cell: ({ value }: { value: number }) => (
|
||||||
|
<span className={value >= 0 ? 'text-green-500' : 'text-red-500'}>
|
||||||
|
{value >= 0 ? '+' : ''}${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Total ROI',
|
||||||
|
accessor: 'totalROI',
|
||||||
|
Cell: ({ value }: { value: number }) => (
|
||||||
|
<span className={value >= 0 ? 'text-green-500' : 'text-red-500'}>
|
||||||
|
{value >= 0 ? '+' : ''}{value.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: '24H ROI',
|
||||||
|
accessor: 'roiLast24h',
|
||||||
|
Cell: ({ value }: { value: number }) => (
|
||||||
|
<span className={value >= 0 ? 'text-green-500' : 'text-red-500'}>
|
||||||
|
{value >= 0 ? '+' : ''}{value.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Wins/Losses',
|
||||||
|
accessor: 'wins',
|
||||||
|
Cell: ({ row }: { row: { original: AgentSummaryViewModel } }) => (
|
||||||
|
<span>
|
||||||
|
{row.original.wins}/{row.original.losses}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Win Rate',
|
||||||
|
accessor: 'averageWinRate',
|
||||||
|
Cell: ({ value }: { value: number }) => (
|
||||||
|
<span className="font-medium">{value.toFixed(1)}%</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Active Strategies',
|
||||||
|
accessor: 'activeStrategiesCount',
|
||||||
|
Cell: ({ value }: { value: number }) => (
|
||||||
|
<span className="badge badge-primary">{value}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Total Volume',
|
||||||
|
accessor: 'totalVolume',
|
||||||
|
Cell: ({ value }: { value: number }) => (
|
||||||
|
<span>${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: '24H Volume',
|
||||||
|
accessor: 'volumeLast24h',
|
||||||
|
Cell: ({ value }: { value: number }) => (
|
||||||
|
<span>${value.toLocaleString(undefined, { maximumFractionDigits: 2 })}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], [])
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
if (!data?.agentSummaries) return []
|
||||||
|
return data.agentSummaries.map(agent => ({
|
||||||
|
...agent,
|
||||||
|
// Ensure all numeric values are numbers for proper sorting
|
||||||
|
totalPnL: Number(agent.totalPnL) || 0,
|
||||||
|
pnLLast24h: Number(agent.pnLLast24h) || 0,
|
||||||
|
totalROI: Number(agent.totalROI) || 0,
|
||||||
|
roiLast24h: Number(agent.roiLast24h) || 0,
|
||||||
|
wins: Number(agent.wins) || 0,
|
||||||
|
losses: Number(agent.losses) || 0,
|
||||||
|
averageWinRate: Number(agent.averageWinRate) || 0,
|
||||||
|
activeStrategiesCount: Number(agent.activeStrategiesCount) || 0,
|
||||||
|
totalVolume: Number(agent.totalVolume) || 0,
|
||||||
|
volumeLast24h: Number(agent.volumeLast24h) || 0,
|
||||||
|
}))
|
||||||
|
}, [data?.agentSummaries])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto pt-6 space-y-6">
|
||||||
|
<GridTile title="Agent Index">
|
||||||
|
{/* Filters and Controls */}
|
||||||
|
<div className="flex flex-wrap gap-4 mb-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-sm font-medium">Time Filter:</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered select-sm"
|
||||||
|
value={timeFilter}
|
||||||
|
onChange={(e) => setTimeFilter(e.target.value)}
|
||||||
|
>
|
||||||
|
{TIME_FILTERS.map(filter => (
|
||||||
|
<option key={filter.value} value={filter.value}>
|
||||||
|
{filter.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-sm font-medium">Sort By:</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered select-sm"
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(e) => setSortBy(e.target.value)}
|
||||||
|
>
|
||||||
|
{SORT_OPTIONS.map(option => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-sm font-medium">Order:</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered select-sm"
|
||||||
|
value={sortOrder}
|
||||||
|
onChange={(e) => setSortOrder(e.target.value as 'asc' | 'desc')}
|
||||||
|
>
|
||||||
|
<option value="desc">Descending</option>
|
||||||
|
<option value="asc">Ascending</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-sm font-medium">Page Size:</label>
|
||||||
|
<select
|
||||||
|
className="select select-bordered select-sm"
|
||||||
|
value={pageSize}
|
||||||
|
onChange={(e) => setPageSize(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{[10, 20, 50, 100].map(size => (
|
||||||
|
<option key={size} value={size}>
|
||||||
|
{size}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading State */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex justify-center items-center py-8">
|
||||||
|
<span className="loading loading-spinner loading-lg"></span>
|
||||||
|
<span className="ml-2">Loading agents...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-error">
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Data Summary */}
|
||||||
|
{data && (
|
||||||
|
<div className="mb-4 p-4 bg-base-200 rounded-lg">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm opacity-70">Showing {data.agentSummaries?.length || 0} of {data.totalCount || 0} agents</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm opacity-70">
|
||||||
|
Page {data.currentPage || 1} of {data.totalPages || 1}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
{data && data.agentSummaries && data.agentSummaries.length > 0 && (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
data={tableData}
|
||||||
|
showPagination={false} // We'll handle pagination manually
|
||||||
|
hiddenColumns={[]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Manual Pagination */}
|
||||||
|
{data && data.totalPages && data.totalPages > 1 && (
|
||||||
|
<div className="flex justify-center items-center gap-2 mt-4">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => setCurrentPage(1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
{'<<'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => setCurrentPage(currentPage - 1)}
|
||||||
|
disabled={!data.hasPreviousPage}
|
||||||
|
>
|
||||||
|
{'<'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span className="px-4">
|
||||||
|
Page {currentPage} of {data.totalPages}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => setCurrentPage(currentPage + 1)}
|
||||||
|
disabled={!data.hasNextPage}
|
||||||
|
>
|
||||||
|
{'>'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => setCurrentPage(data.totalPages)}
|
||||||
|
disabled={currentPage === data.totalPages}
|
||||||
|
>
|
||||||
|
{'>>'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Data State */}
|
||||||
|
{data && (!data.agentSummaries || data.agentSummaries.length === 0) && !isLoading && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p>No agents found for the selected criteria.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</GridTile>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgentIndex
|
||||||
@@ -22,7 +22,7 @@ const FILTERS = [
|
|||||||
{ label: 'Total', value: 'Total', days: null },
|
{ label: 'Total', value: 'Total', days: null },
|
||||||
]
|
]
|
||||||
|
|
||||||
function AgentSearch() {
|
function AgentSearch({ index }: { index: number }) {
|
||||||
const { apiUrl } = useApiUrlStore()
|
const { apiUrl } = useApiUrlStore()
|
||||||
const [agentName, setAgentName] = useState('')
|
const [agentName, setAgentName] = useState('')
|
||||||
const [agentData, setAgentData] = useState<AgentData | null>(null)
|
const [agentData, setAgentData] = useState<AgentData | null>(null)
|
||||||
@@ -231,15 +231,15 @@ function AgentSearch() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`text-lg font-bold ${
|
<span className={`text-lg font-bold ${
|
||||||
(position.profitAndLoss?.realized || 0) >= 0 ? 'text-green-500' : 'text-red-500'
|
(position.ProfitAndLoss?.realized || 0) >= 0 ? 'text-green-500' : 'text-red-500'
|
||||||
}`}>
|
}`}>
|
||||||
{(position.profitAndLoss?.realized || 0) >= 0 ? '+' : ''}${(position.profitAndLoss?.realized || 0).toFixed(2)}
|
{(position.ProfitAndLoss?.realized || 0) >= 0 ? '+' : ''}${(position.ProfitAndLoss?.realized || 0).toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
<span className={`flex items-center gap-1 text-sm ${
|
<span className={`flex items-center gap-1 text-sm ${
|
||||||
(position.profitAndLoss?.realized || 0) >= 0 ? 'text-green-500' : 'text-red-500'
|
(position.ProfitAndLoss?.realized || 0) >= 0 ? 'text-green-500' : 'text-red-500'
|
||||||
}`}>
|
}`}>
|
||||||
{(position.profitAndLoss?.realized || 0) >= 0 ? '↑' : '↓'}
|
{(position.ProfitAndLoss?.realized || 0) >= 0 ? '↑' : '↓'}
|
||||||
{Math.abs(((position.profitAndLoss?.realized || 0) / (position.open?.price * position.open?.quantity || 1)) * 100).toFixed(2)}%
|
{Math.abs(((position.ProfitAndLoss?.realized || 0) / (position.Open?.price * position.Open?.quantity || 1)) * 100).toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const tabs: TabsType = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const Analytics: React.FC = () => {
|
const Analytics: React.FC<{ index: number }> = ({ index }) => {
|
||||||
const [selectedTab, setSelectedTab] = useState<number>(tabs[0].index)
|
const [selectedTab, setSelectedTab] = useState<number>(tabs[0].index)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const FILTERS = [
|
|||||||
{ label: 'Total', value: 'Total', days: null },
|
{ label: 'Total', value: 'Total', days: null },
|
||||||
]
|
]
|
||||||
|
|
||||||
function BestAgents() {
|
function BestAgents({ index }: { index: number }) {
|
||||||
const { apiUrl } = useApiUrlStore()
|
const { apiUrl } = useApiUrlStore()
|
||||||
const [data, setData] = useState<AgentBalanceWithBalances[]>([])
|
const [data, setData] = useState<AgentBalanceWithBalances[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
|
|
||||||
import Tabs from '../../components/mollecules/Tabs/Tabs'
|
import Tabs from '../../components/mollecules/Tabs/Tabs'
|
||||||
import type {ITabsType} from '../../global/type'
|
import type {ITabsType} from '../../global/type.tsx'
|
||||||
|
|
||||||
import Analytics from './analytics/analytics'
|
import Analytics from './analytics/analytics'
|
||||||
import Monitoring from './monitoring'
|
import Monitoring from './monitoring'
|
||||||
import BestAgents from './analytics/bestAgents'
|
import BestAgents from './analytics/bestAgents'
|
||||||
import AgentSearch from './agentSearch'
|
import AgentSearch from './agentSearch'
|
||||||
|
import AgentIndex from './agentIndex'
|
||||||
|
|
||||||
const tabs: ITabsType = [
|
const tabs: ITabsType = [
|
||||||
{
|
{
|
||||||
@@ -29,6 +30,11 @@ const tabs: ITabsType = [
|
|||||||
index: 4,
|
index: 4,
|
||||||
label: 'Agent Search',
|
label: 'Agent Search',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Component: AgentIndex,
|
||||||
|
index: 5,
|
||||||
|
label: 'Agent Index',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {ActiveBots} from '../../components/organism'
|
import {ActiveBots} from '../../components/organism'
|
||||||
|
|
||||||
const Monitoring: React.FC = () => {
|
const Monitoring: React.FC<{ index: number }> = ({ index }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-1 xl:grid-cols-1 container grid gap-8 pt-6 mx-auto mb-5">
|
<div className="sm:grid-cols-1 md:grid-cols-1 lg:grid-cols-1 xl:grid-cols-1 container grid gap-8 pt-6 mx-auto mb-5">
|
||||||
|
|||||||
Reference in New Issue
Block a user