Add agent index with pagination

This commit is contained in:
2025-07-30 22:27:01 +07:00
parent 20b0881084
commit 4b0da0e864
10 changed files with 844 additions and 76 deletions

View File

@@ -664,6 +664,198 @@ public class DataController : ControllerBase
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>
/// Retrieves balance history for a specific agent within a date range
/// </summary>

View File

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

View File

@@ -677,7 +677,7 @@ export class BacktestClient extends AuthorizedApiBase {
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";
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;
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<Backtest>(null as any);
return Promise.resolve<LightBacktest>(null as any);
}
backtest_RunBundle(request: RunBundleBacktestRequest): Promise<BundleBacktestRequest> {
@@ -1882,6 +1882,92 @@ export class DataClient extends AuthorizedApiBase {
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> {
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<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> {
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;

View File

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

View 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

View File

@@ -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<AgentData | null>(null)
@@ -231,15 +231,15 @@ function AgentSearch() {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<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 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 ? '↑' : '↓'}
{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)}%
</span>
</div>
</div>

View File

@@ -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)
return (

View File

@@ -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<AgentBalanceWithBalances[]>([])
const [isLoading, setIsLoading] = useState(true)

View File

@@ -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 = () => {

View File

@@ -1,6 +1,6 @@
import {ActiveBots} from '../../components/organism'
const Monitoring: React.FC = () => {
const Monitoring: React.FC<{ index: number }> = ({ index }) => {
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">