From 4b0da0e864325649c1c60c6ba7ec1af2fe25a32a Mon Sep 17 00:00:00 2001 From: cryptooda Date: Wed, 30 Jul 2025 22:27:01 +0700 Subject: [PATCH] Add agent index with pagination --- .../Controllers/DataController.cs | 192 ++++++++++ .../Responses/PaginatedAgentIndexResponse.cs | 57 +++ .../src/generated/ManagingApi.ts | 220 +++++++++--- .../src/generated/ManagingApiTypes.ts | 87 ++++- .../src/pages/dashboardPage/agentIndex.tsx | 332 ++++++++++++++++++ .../src/pages/dashboardPage/agentSearch.tsx | 12 +- .../dashboardPage/analytics/analytics.tsx | 6 +- .../dashboardPage/analytics/bestAgents.tsx | 2 +- .../src/pages/dashboardPage/dashboard.tsx | 8 +- .../src/pages/dashboardPage/monitoring.tsx | 4 +- 10 files changed, 844 insertions(+), 76 deletions(-) create mode 100644 src/Managing.Api/Models/Responses/PaginatedAgentIndexResponse.cs create mode 100644 src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index a425c17..89ecb1b 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -664,6 +664,198 @@ public class DataController : ControllerBase return Ok(agentIndex); } + /// + /// Retrieves a paginated list of agent summaries for the agent index page + /// + /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) + /// Page number (defaults to 1) + /// Number of items per page (defaults to 10, max 100) + /// Field to sort by (TotalPnL, PnLLast24h, TotalROI, ROILast24h, Wins, Losses, AverageWinRate, ActiveStrategiesCount, TotalVolume, VolumeLast24h) + /// Sort order - "asc" or "desc" (defaults to "desc") + /// A paginated list of agent summaries sorted by the specified field + [HttpGet("GetAgentIndexPaginated")] + public async Task> 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(cacheKey); + + List 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(); + + // 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(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); + } + /// /// Retrieves balance history for a specific agent within a date range /// diff --git a/src/Managing.Api/Models/Responses/PaginatedAgentIndexResponse.cs b/src/Managing.Api/Models/Responses/PaginatedAgentIndexResponse.cs new file mode 100644 index 0000000..7332e3b --- /dev/null +++ b/src/Managing.Api/Models/Responses/PaginatedAgentIndexResponse.cs @@ -0,0 +1,57 @@ +namespace Managing.Api.Models.Responses; + +/// +/// Response model for paginated agent index results +/// +public class PaginatedAgentIndexResponse +{ + /// + /// The list of agent summaries for the current page + /// + public IEnumerable AgentSummaries { get; set; } = new List(); + + /// + /// Total number of agents across all pages + /// + public int TotalCount { get; set; } + + /// + /// Current page number + /// + public int CurrentPage { get; set; } + + /// + /// Number of items per page + /// + public int PageSize { get; set; } + + /// + /// Total number of pages + /// + public int TotalPages { get; set; } + + /// + /// Whether there are more pages available + /// + public bool HasNextPage { get; set; } + + /// + /// Whether there are previous pages available + /// + public bool HasPreviousPage { get; set; } + + /// + /// Time filter applied to the data + /// + public string TimeFilter { get; set; } = "Total"; + + /// + /// Field used for sorting + /// + public string SortBy { get; set; } = "TotalPnL"; + + /// + /// Sort order (asc or desc) + /// + public string SortOrder { get; set; } = "desc"; +} \ No newline at end of file diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 0bc8926..30e5cd7 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -677,7 +677,7 @@ export class BacktestClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - backtest_Run(request: RunBacktestRequest): Promise { + backtest_Run(request: RunBacktestRequest): Promise { let url_ = this.baseUrl + "/Backtest/Run"; url_ = url_.replace(/[?&]$/, ""); @@ -699,13 +699,13 @@ export class BacktestClient extends AuthorizedApiBase { }); } - protected processBacktest_Run(response: Response): Promise { + protected processBacktest_Run(response: Response): Promise { 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 Backtest; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as LightBacktest; return result200; }); } 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 Promise.resolve(null as any); + return Promise.resolve(null as any); } backtest_RunBundle(request: RunBundleBacktestRequest): Promise { @@ -1882,6 +1882,92 @@ export class DataClient extends AuthorizedApiBase { return Promise.resolve(null as any); } + data_GetAgentIndex(timeFilter: string | null | undefined): Promise { + 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 { + 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(null as any); + } + + data_GetAgentIndexPaginated(timeFilter: string | null | undefined, page: number | undefined, pageSize: number | undefined, sortBy: string | null | undefined, sortOrder: string | null | undefined): Promise { + 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 { + 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(null as any); + } + data_GetAgentBalances(agentName: string | null | undefined, startDate: Date | undefined, endDate: Date | null | undefined): Promise { let url_ = this.baseUrl + "/Data/GetAgentBalances?"; if (agentName !== undefined && agentName !== null) @@ -2776,45 +2862,6 @@ export class TradingClient extends AuthorizedApiBase { this.baseUrl = baseUrl ?? "http://localhost:5000"; } - trading_GetPositions(positionInitiator: PositionInitiator | undefined): Promise { - 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 { - 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(null as any); - } - trading_GetTrade(accountName: string | null | undefined, ticker: Ticker | undefined, exchangeOrderId: string | null | undefined): Promise { let url_ = this.baseUrl + "/Trading/GetTrade?"; if (accountName !== undefined && accountName !== null) @@ -3670,7 +3717,7 @@ export interface Backtest { export interface TradingBotConfig { accountName: string; - moneyManagement: MoneyManagement; + moneyManagement: LightMoneyManagement; ticker: Ticker; timeframe: Timeframe; isForWatchingOnly: boolean; @@ -3681,7 +3728,7 @@ export interface TradingBotConfig { flipPosition: boolean; name: string; riskManagement?: RiskManagement | null; - scenario?: Scenario | null; + scenario?: LightScenario | null; scenarioName?: string | null; maxPositionTimeHours?: number | null; closeEarlyWhenProfitable?: boolean; @@ -3692,13 +3739,12 @@ export interface TradingBotConfig { useForDynamicStopLoss?: boolean; } -export interface MoneyManagement { +export interface LightMoneyManagement { name: string; timeframe: Timeframe; stopLoss: number; takeProfit: number; leverage: number; - user?: User | null; } export enum Timeframe { @@ -3734,14 +3780,13 @@ export enum RiskToleranceLevel { Aggressive = "Aggressive", } -export interface Scenario { +export interface LightScenario { name?: string | null; - indicators?: Indicator[] | null; + indicators?: LightIndicator[] | null; loopbackPeriod?: number | null; - user?: User | null; } -export interface Indicator { +export interface LightIndicator { name?: string | null; type?: IndicatorType; signalType?: SignalType; @@ -3754,7 +3799,6 @@ export interface Indicator { smoothPeriods?: number | null; stochPeriods?: number | null; cyclePeriods?: number | null; - user?: User | null; } export enum IndicatorType { @@ -3786,7 +3830,7 @@ export interface Position { date: Date; originDirection: TradeDirection; ticker: Ticker; - moneyManagement: MoneyManagement; + moneyManagement: LightMoneyManagement; Open: Trade; StopLoss: Trade; TakeProfit1: Trade; @@ -4047,6 +4091,22 @@ export interface LightBacktestResponse { 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 { config?: TradingBotConfigRequest | null; startDate?: Date; @@ -4227,6 +4287,10 @@ export interface RunGeneticRequest { eligibleIndicators?: IndicatorType[] | null; } +export interface MoneyManagement extends LightMoneyManagement { + user?: User | null; +} + export interface StartBotRequest { config?: TradingBotConfigRequest | null; } @@ -4285,6 +4349,39 @@ export interface Spotlight { 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 { ticker: Ticker; fiveMinutes: LightSignal[]; @@ -4344,6 +4441,10 @@ export interface PlatformSummaryViewModel { totalPlatformPnL?: number; totalPlatformVolume?: number; totalPlatformVolumeLast24h?: number; + timeFilter?: string | null; +} + +export interface AgentIndexViewModel { agentSummaries?: AgentSummaryViewModel[] | null; timeFilter?: string | null; } @@ -4362,6 +4463,19 @@ export interface AgentSummaryViewModel { 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 { agentName?: string | null; agentBalances?: AgentBalance[] | null; diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index fa06b4b..eb4de46 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -242,7 +242,7 @@ export interface Backtest { export interface TradingBotConfig { accountName: string; - moneyManagement: MoneyManagement; + moneyManagement: LightMoneyManagement; ticker: Ticker; timeframe: Timeframe; isForWatchingOnly: boolean; @@ -253,7 +253,7 @@ export interface TradingBotConfig { flipPosition: boolean; name: string; riskManagement?: RiskManagement | null; - scenario?: Scenario | null; + scenario?: LightScenario | null; scenarioName?: string | null; maxPositionTimeHours?: number | null; closeEarlyWhenProfitable?: boolean; @@ -264,13 +264,12 @@ export interface TradingBotConfig { useForDynamicStopLoss?: boolean; } -export interface MoneyManagement { +export interface LightMoneyManagement { name: string; timeframe: Timeframe; stopLoss: number; takeProfit: number; leverage: number; - user?: User | null; } export enum Timeframe { @@ -306,14 +305,13 @@ export enum RiskToleranceLevel { Aggressive = "Aggressive", } -export interface Scenario { +export interface LightScenario { name?: string | null; - indicators?: Indicator[] | null; + indicators?: LightIndicator[] | null; loopbackPeriod?: number | null; - user?: User | null; } -export interface Indicator { +export interface LightIndicator { name?: string | null; type?: IndicatorType; signalType?: SignalType; @@ -326,7 +324,6 @@ export interface Indicator { smoothPeriods?: number | null; stochPeriods?: number | null; cyclePeriods?: number | null; - user?: User | null; } export enum IndicatorType { @@ -358,7 +355,7 @@ export interface Position { date: Date; originDirection: TradeDirection; ticker: Ticker; - moneyManagement: MoneyManagement; + moneyManagement: LightMoneyManagement; Open: Trade; StopLoss: Trade; TakeProfit1: Trade; @@ -619,6 +616,22 @@ export interface LightBacktestResponse { 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 { config?: TradingBotConfigRequest | null; startDate?: Date; @@ -799,6 +812,10 @@ export interface RunGeneticRequest { eligibleIndicators?: IndicatorType[] | null; } +export interface MoneyManagement extends LightMoneyManagement { + user?: User | null; +} + export interface StartBotRequest { config?: TradingBotConfigRequest | null; } @@ -857,6 +874,39 @@ export interface Spotlight { 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 { ticker: Ticker; fiveMinutes: LightSignal[]; @@ -916,6 +966,10 @@ export interface PlatformSummaryViewModel { totalPlatformPnL?: number; totalPlatformVolume?: number; totalPlatformVolumeLast24h?: number; + timeFilter?: string | null; +} + +export interface AgentIndexViewModel { agentSummaries?: AgentSummaryViewModel[] | null; timeFilter?: string | null; } @@ -934,6 +988,19 @@ export interface AgentSummaryViewModel { 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 { agentName?: string | null; agentBalances?: AgentBalance[] | null; diff --git a/src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx b/src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx new file mode 100644 index 0000000..1d61b8c --- /dev/null +++ b/src/Managing.WebApp/src/pages/dashboardPage/agentIndex.tsx @@ -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(null) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(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 }) => ( + {value || 'Unknown'} + ), + }, + { + Header: 'Total PnL', + accessor: 'totalPnL', + Cell: ({ value }: { value: number }) => ( + = 0 ? 'text-green-500' : 'text-red-500'}> + {value >= 0 ? '+' : ''}${value.toLocaleString(undefined, { maximumFractionDigits: 2 })} + + ), + }, + { + Header: '24H PnL', + accessor: 'pnLLast24h', + Cell: ({ value }: { value: number }) => ( + = 0 ? 'text-green-500' : 'text-red-500'}> + {value >= 0 ? '+' : ''}${value.toLocaleString(undefined, { maximumFractionDigits: 2 })} + + ), + }, + { + Header: 'Total ROI', + accessor: 'totalROI', + Cell: ({ value }: { value: number }) => ( + = 0 ? 'text-green-500' : 'text-red-500'}> + {value >= 0 ? '+' : ''}{value.toFixed(2)}% + + ), + }, + { + Header: '24H ROI', + accessor: 'roiLast24h', + Cell: ({ value }: { value: number }) => ( + = 0 ? 'text-green-500' : 'text-red-500'}> + {value >= 0 ? '+' : ''}{value.toFixed(2)}% + + ), + }, + { + Header: 'Wins/Losses', + accessor: 'wins', + Cell: ({ row }: { row: { original: AgentSummaryViewModel } }) => ( + + {row.original.wins}/{row.original.losses} + + ), + }, + { + Header: 'Win Rate', + accessor: 'averageWinRate', + Cell: ({ value }: { value: number }) => ( + {value.toFixed(1)}% + ), + }, + { + Header: 'Active Strategies', + accessor: 'activeStrategiesCount', + Cell: ({ value }: { value: number }) => ( + {value} + ), + }, + { + Header: 'Total Volume', + accessor: 'totalVolume', + Cell: ({ value }: { value: number }) => ( + ${value.toLocaleString(undefined, { maximumFractionDigits: 2 })} + ), + }, + { + Header: '24H Volume', + accessor: 'volumeLast24h', + Cell: ({ value }: { value: number }) => ( + ${value.toLocaleString(undefined, { maximumFractionDigits: 2 })} + ), + }, + ], []) + + 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 ( +
+ + {/* Filters and Controls */} +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + {/* Loading State */} + {isLoading && ( +
+ + Loading agents... +
+ )} + + {/* Error State */} + {error && ( +
+ {error} +
+ )} + + {/* Data Summary */} + {data && ( +
+
+
+ Showing {data.agentSummaries?.length || 0} of {data.totalCount || 0} agents +
+
+ Page {data.currentPage || 1} of {data.totalPages || 1} +
+
+
+ )} + + {/* Table */} + {data && data.agentSummaries && data.agentSummaries.length > 0 && ( + + )} + + {/* Manual Pagination */} + {data && data.totalPages && data.totalPages > 1 && ( +
+ + + + + Page {currentPage} of {data.totalPages} + + + + +
+ )} + + {/* No Data State */} + {data && (!data.agentSummaries || data.agentSummaries.length === 0) && !isLoading && ( +
+

No agents found for the selected criteria.

+
+ )} + + + ) +} + +export default AgentIndex \ No newline at end of file diff --git a/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx b/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx index b0eb70b..24e74b5 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/agentSearch.tsx @@ -22,7 +22,7 @@ const FILTERS = [ { label: 'Total', value: 'Total', days: null }, ] -function AgentSearch() { +function AgentSearch({ index }: { index: number }) { const { apiUrl } = useApiUrlStore() const [agentName, setAgentName] = useState('') const [agentData, setAgentData] = useState(null) @@ -231,15 +231,15 @@ function AgentSearch() {
= 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)} = 0 ? 'text-green-500' : 'text-red-500' + (position.ProfitAndLoss?.realized || 0) >= 0 ? 'text-green-500' : 'text-red-500' }`}> - {(position.profitAndLoss?.realized || 0) >= 0 ? '↑' : '↓'} - {Math.abs(((position.profitAndLoss?.realized || 0) / (position.open?.price * position.open?.quantity || 1)) * 100).toFixed(2)}% + {(position.ProfitAndLoss?.realized || 0) >= 0 ? '↑' : '↓'} + {Math.abs(((position.ProfitAndLoss?.realized || 0) / (position.Open?.price * position.Open?.quantity || 1)) * 100).toFixed(2)}%
diff --git a/src/Managing.WebApp/src/pages/dashboardPage/analytics/analytics.tsx b/src/Managing.WebApp/src/pages/dashboardPage/analytics/analytics.tsx index 1bbde8c..cc30d6e 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/analytics/analytics.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/analytics/analytics.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react' +import {useState} from 'react' import Tabs from '../../../components/mollecules/Tabs/Tabs' -import type { TabsType } from '../../../global/type' +import type {TabsType} from '../../../global/type' import Cme from './cme' import Futures from './futures' @@ -37,7 +37,7 @@ const tabs: TabsType = [ }, ] -const Analytics: React.FC = () => { +const Analytics: React.FC<{ index: number }> = ({ index }) => { const [selectedTab, setSelectedTab] = useState(tabs[0].index) return ( diff --git a/src/Managing.WebApp/src/pages/dashboardPage/analytics/bestAgents.tsx b/src/Managing.WebApp/src/pages/dashboardPage/analytics/bestAgents.tsx index 901be4a..93df341 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/analytics/bestAgents.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/analytics/bestAgents.tsx @@ -23,7 +23,7 @@ const FILTERS = [ { label: 'Total', value: 'Total', days: null }, ] -function BestAgents() { +function BestAgents({ index }: { index: number }) { const { apiUrl } = useApiUrlStore() const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(true) diff --git a/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx b/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx index 02eb82d..0c04277 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/dashboard.tsx @@ -1,12 +1,13 @@ import React, {useEffect, useState} from 'react' 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 Monitoring from './monitoring' import BestAgents from './analytics/bestAgents' import AgentSearch from './agentSearch' +import AgentIndex from './agentIndex' const tabs: ITabsType = [ { @@ -29,6 +30,11 @@ const tabs: ITabsType = [ index: 4, label: 'Agent Search', }, + { + Component: AgentIndex, + index: 5, + label: 'Agent Index', + }, ] const Dashboard: React.FC = () => { diff --git a/src/Managing.WebApp/src/pages/dashboardPage/monitoring.tsx b/src/Managing.WebApp/src/pages/dashboardPage/monitoring.tsx index 10264af..fa93bce 100644 --- a/src/Managing.WebApp/src/pages/dashboardPage/monitoring.tsx +++ b/src/Managing.WebApp/src/pages/dashboardPage/monitoring.tsx @@ -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 ( <>