diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index 08d73d3..d3d3ee9 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -8,10 +8,13 @@ using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Shared.Helpers; using Managing.Domain.Statistics; +using Managing.Domain.Trades; +using Managing.Domain.Users; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using System.Linq; using static Managing.Common.Enums; namespace Managing.Api.Controllers; @@ -329,4 +332,117 @@ public class DataController : ControllerBase Positions = strategy.Positions.OrderByDescending(p => p.Date).ToList() // Include sorted positions with most recent first }; } + + /// + /// Retrieves a summary of platform activity across all agents + /// + /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) + /// A summary of platform activity including per-agent statistics + [HttpGet("GetPlatformSummary")] + public async Task> GetPlatformSummary(string timeFilter = "Total") + { + // Validate time filter + var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" }; + if (!validTimeFilters.Contains(timeFilter)) + { + timeFilter = "Total"; // Default to Total if invalid + } + + string cacheKey = $"PlatformSummary_{timeFilter}"; + + // Check if the platform summary is already cached + var cachedSummary = _cacheService.GetValue(cacheKey); + + if (cachedSummary != null) + { + return Ok(cachedSummary); + } + + // Get all agents and their strategies + var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter)); + + // Create the platform summary + var summary = new PlatformSummaryViewModel + { + TotalAgents = agentsWithStrategies.Count, + TotalActiveStrategies = agentsWithStrategies.Values.Sum(list => list.Count), + TimeFilter = timeFilter + }; + + // Calculate total platform metrics + decimal totalPlatformPnL = 0; + decimal totalPlatformVolume = 0; + decimal totalPlatformVolumeLast24h = 0; + + // 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 + { + Username = user.Name, + TotalPnL = totalPnL, + PnLLast24h = pnlLast24h, + TotalROI = totalROI, + ROILast24h = roiLast24h, + Wins = wins, + Losses = losses, + AverageWinRate = averageWinRate, + ActiveStrategiesCount = strategies.Count, + TotalVolume = totalVolume, + VolumeLast24h = volumeLast24h + }; + + summary.AgentSummaries.Add(agentSummary); + + // Add to platform totals + totalPlatformPnL += totalPnL; + totalPlatformVolume += totalVolume; + totalPlatformVolumeLast24h += volumeLast24h; + } + + // Set the platform totals + summary.TotalPlatformPnL = totalPlatformPnL; + summary.TotalPlatformVolume = totalPlatformVolume; + summary.TotalPlatformVolumeLast24h = totalPlatformVolumeLast24h; + + // Sort agent summaries by total PnL (highest first) + summary.AgentSummaries = summary.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList(); + + // Cache the results for 5 minutes + _cacheService.SaveValue(cacheKey, summary, TimeSpan.FromMinutes(5)); + + return Ok(summary); + } } \ No newline at end of file diff --git a/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs b/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs new file mode 100644 index 0000000..590839d --- /dev/null +++ b/src/Managing.Api/Models/Responses/AgentSummaryViewModel.cs @@ -0,0 +1,104 @@ +namespace Managing.Api.Models.Responses +{ + /// + /// Summary of agent performance and activity + /// + public class AgentSummaryViewModel + { + /// + /// Username of the agent + /// + public string Username { get; set; } + + /// + /// Total profit and loss in USD + /// + public decimal TotalPnL { get; set; } + + /// + /// Profit and loss in the last 24 hours in USD + /// + public decimal PnLLast24h { get; set; } + + /// + /// Total return on investment as a percentage + /// + public decimal TotalROI { get; set; } + + /// + /// Return on investment in the last 24 hours as a percentage + /// + public decimal ROILast24h { get; set; } + + /// + /// Number of winning trades + /// + public int Wins { get; set; } + + /// + /// Number of losing trades + /// + public int Losses { get; set; } + + /// + /// Average win rate as a percentage + /// + public int AverageWinRate { get; set; } + + /// + /// Number of active strategies for this agent + /// + public int ActiveStrategiesCount { get; set; } + + /// + /// Total volume traded by this agent in USD + /// + public decimal TotalVolume { get; set; } + + /// + /// Volume traded in the last 24 hours in USD + /// + public decimal VolumeLast24h { get; set; } + } + + /// + /// Platform-wide statistics including per-agent summaries + /// + public class PlatformSummaryViewModel + { + /// + /// Total number of agents on the platform + /// + public int TotalAgents { get; set; } + + /// + /// Total number of active strategies across all agents + /// + public int TotalActiveStrategies { get; set; } + + /// + /// Total platform-wide profit and loss in USD + /// + public decimal TotalPlatformPnL { get; set; } + + /// + /// Total volume traded across all agents in USD + /// + public decimal TotalPlatformVolume { get; set; } + + /// + /// Total volume traded across all agents in the last 24 hours in USD + /// + public decimal TotalPlatformVolumeLast24h { get; set; } + + /// + /// Summaries for each agent + /// + public List AgentSummaries { get; set; } = new List(); + + /// + /// Time filter applied to the data + /// + public string TimeFilter { get; set; } = "Total"; + } +} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/Commands/GetAllAgentsCommand.cs b/src/Managing.Application/ManageBot/Commands/GetAllAgentsCommand.cs new file mode 100644 index 0000000..ac54de6 --- /dev/null +++ b/src/Managing.Application/ManageBot/Commands/GetAllAgentsCommand.cs @@ -0,0 +1,22 @@ +using Managing.Application.Abstractions; +using Managing.Domain.Users; +using MediatR; + +namespace Managing.Application.ManageBot.Commands +{ + /// + /// Command to retrieve all active agents and their strategies + /// + public class GetAllAgentsCommand : IRequest>> + { + /// + /// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) + /// + public string TimeFilter { get; } + + public GetAllAgentsCommand(string timeFilter = "Total") + { + TimeFilter = timeFilter; + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/GetAllAgentsCommandHandler.cs b/src/Managing.Application/ManageBot/GetAllAgentsCommandHandler.cs new file mode 100644 index 0000000..e0f4392 --- /dev/null +++ b/src/Managing.Application/ManageBot/GetAllAgentsCommandHandler.cs @@ -0,0 +1,101 @@ +using Managing.Application.Abstractions; +using Managing.Application.Abstractions.Services; +using Managing.Application.ManageBot.Commands; +using Managing.Common; +using Managing.Domain.Users; +using MediatR; + +namespace Managing.Application.ManageBot +{ + /// + /// Handler for retrieving all agents and their strategies + /// + public class GetAllAgentsCommandHandler : IRequestHandler>> + { + private readonly IBotService _botService; + private readonly IAccountService _accountService; + + public GetAllAgentsCommandHandler(IBotService botService, IAccountService accountService) + { + _botService = botService; + _accountService = accountService; + } + + public async Task>> Handle(GetAllAgentsCommand request, + CancellationToken cancellationToken) + { + var result = new Dictionary>(); + var allActiveBots = _botService.GetActiveBots(); + + // Group bots by user + foreach (var bot in allActiveBots) + { + if (bot.User == null) + { + // Skip bots without a user (this shouldn't happen, but just to be safe) + continue; + } + + // Apply time filtering if needed (except for "Total") + if (request.TimeFilter != "Total") + { + // Check if this bot had activity within the specified time range + if (!BotHasActivityInTimeRange(bot, request.TimeFilter)) + { + continue; // Skip this bot if it doesn't have activity in the time range + } + } + + // Add the bot to the user's list + if (!result.ContainsKey(bot.User)) + { + result[bot.User] = new List(); + } + + result[bot.User].Add(bot); + } + + return result; + } + + /// + /// Checks if a bot has had trading activity within the specified time range + /// + private bool BotHasActivityInTimeRange(ITradingBot bot, string timeFilter) + { + // Convert time filter to a DateTime + DateTime cutoffDate = DateTime.UtcNow; + + switch (timeFilter) + { + case "24H": + cutoffDate = DateTime.UtcNow.AddHours(-24); + break; + case "3D": + cutoffDate = DateTime.UtcNow.AddDays(-3); + break; + case "1W": + cutoffDate = DateTime.UtcNow.AddDays(-7); + break; + case "1M": + cutoffDate = DateTime.UtcNow.AddMonths(-1); + break; + case "1Y": + cutoffDate = DateTime.UtcNow.AddYears(-1); + break; + default: + // Default to "Total" (no filtering) + return true; + } + + // Check if there are any positions with activity after the cutoff date + return bot.Positions.Any(p => + p.Date >= cutoffDate || + (p.Open.Date >= cutoffDate) || + (p.StopLoss.Status == Enums.TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || + (p.TakeProfit1.Status == Enums.TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || + (p.TakeProfit2 != null && p.TakeProfit2.Status == Enums.TradeStatus.Filled && + p.TakeProfit2.Date >= cutoffDate)); + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/Trading/ClosePositionCommandHandler.cs b/src/Managing.Application/Trading/ClosePositionCommandHandler.cs index 24389bb..a030db3 100644 --- a/src/Managing.Application/Trading/ClosePositionCommandHandler.cs +++ b/src/Managing.Application/Trading/ClosePositionCommandHandler.cs @@ -35,11 +35,26 @@ public class ClosePositionCommandHandler( ? request.ExecutionPrice.GetValueOrDefault() : exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow); + // Check if position still open + var p = (await exchangeService.GetBrokerPositions(account)) + .FirstOrDefault(x => x.Ticker == request.Position.Ticker); + + if (p == null) + { + request.Position.Status = PositionStatus.Finished; + request.Position.ProfitAndLoss = + TradingBox.GetProfitAndLoss(request.Position, request.Position.Open.Quantity, lastPrice, + request.Position.Open.Leverage); + tradingService.UpdatePosition(request.Position); + return request.Position; + } + + var closeRequestedOrders = + isForPaperTrading || (await exchangeService.CancelOrder(account, request.Position.Ticker)); + // Close market var closedPosition = await exchangeService.ClosePosition(account, request.Position, lastPrice, isForPaperTrading); - var closeRequestedOrders = - isForPaperTrading || (await exchangeService.CancelOrder(account, request.Position.Ticker)); if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled)) { diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 420dfec..4eab908 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -323,4 +323,173 @@ public static class TradingBox return (profitLast24h / investmentLast24h) * 100; } + + /// + /// Calculates profit and loss for positions within a specific time range + /// + /// List of positions to analyze + /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) + /// The PnL for positions in the specified range + public static decimal GetPnLInTimeRange(List positions, string timeFilter) + { + // If Total, just return the total PnL + if (timeFilter == "Total") + { + return positions + .Where(p => p.IsFinished() && p.ProfitAndLoss != null) + .Sum(p => p.ProfitAndLoss.Realized); + } + + // Convert time filter to a DateTime + DateTime cutoffDate = DateTime.UtcNow; + + switch (timeFilter) + { + case "24H": + cutoffDate = DateTime.UtcNow.AddHours(-24); + break; + case "3D": + cutoffDate = DateTime.UtcNow.AddDays(-3); + break; + case "1W": + cutoffDate = DateTime.UtcNow.AddDays(-7); + break; + case "1M": + cutoffDate = DateTime.UtcNow.AddMonths(-1); + break; + case "1Y": + cutoffDate = DateTime.UtcNow.AddYears(-1); + break; + } + + // Include positions that were closed within the time range + return positions + .Where(p => p.IsFinished() && p.ProfitAndLoss != null && + (p.Date >= cutoffDate || + (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || + (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || + (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))) + .Sum(p => p.ProfitAndLoss.Realized); + } + + /// + /// Calculates ROI for positions within a specific time range + /// + /// List of positions to analyze + /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) + /// The ROI as a percentage for positions in the specified range + public static decimal GetROIInTimeRange(List positions, string timeFilter) + { + // If no positions, return 0 + if (!positions.Any()) + { + return 0; + } + + // Convert time filter to a DateTime + DateTime cutoffDate = DateTime.UtcNow; + + if (timeFilter != "Total") + { + switch (timeFilter) + { + case "24H": + cutoffDate = DateTime.UtcNow.AddHours(-24); + break; + case "3D": + cutoffDate = DateTime.UtcNow.AddDays(-3); + break; + case "1W": + cutoffDate = DateTime.UtcNow.AddDays(-7); + break; + case "1M": + cutoffDate = DateTime.UtcNow.AddMonths(-1); + break; + case "1Y": + cutoffDate = DateTime.UtcNow.AddYears(-1); + break; + } + } + + // Filter positions in the time range + var filteredPositions = timeFilter == "Total" + ? positions.Where(p => p.IsFinished() && p.ProfitAndLoss != null) + : positions.Where(p => p.IsFinished() && p.ProfitAndLoss != null && + (p.Date >= cutoffDate || + (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || + (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || + (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))); + + // Calculate investment and profit + decimal totalInvestment = filteredPositions.Sum(p => p.Open.Quantity * p.Open.Price); + decimal totalProfit = filteredPositions.Sum(p => p.ProfitAndLoss.Realized); + + // Calculate ROI + if (totalInvestment == 0) + { + return 0; + } + + return (totalProfit / totalInvestment) * 100; + } + + /// + /// Gets the win/loss counts from positions in a specific time range + /// + /// List of positions to analyze + /// Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total) + /// A tuple containing (wins, losses) + public static (int Wins, int Losses) GetWinLossCountInTimeRange(List positions, string timeFilter) + { + // Convert time filter to a DateTime + DateTime cutoffDate = DateTime.UtcNow; + + if (timeFilter != "Total") + { + switch (timeFilter) + { + case "24H": + cutoffDate = DateTime.UtcNow.AddHours(-24); + break; + case "3D": + cutoffDate = DateTime.UtcNow.AddDays(-3); + break; + case "1W": + cutoffDate = DateTime.UtcNow.AddDays(-7); + break; + case "1M": + cutoffDate = DateTime.UtcNow.AddMonths(-1); + break; + case "1Y": + cutoffDate = DateTime.UtcNow.AddYears(-1); + break; + } + } + + // Filter positions in the time range + var filteredPositions = timeFilter == "Total" + ? positions.Where(p => p.IsFinished()) + : positions.Where(p => p.IsFinished() && + (p.Date >= cutoffDate || + (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || + (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || + (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))); + + int wins = 0; + int losses = 0; + + foreach (var position in filteredPositions) + { + if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0) + { + wins++; + } + else + { + losses++; + } + } + + return (wins, losses); + } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs index 8c91b64..c95df09 100644 --- a/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs +++ b/src/Managing.Infrastructure.Web3/Services/Gmx/GmxV2Mappers.cs @@ -162,16 +162,29 @@ internal static class GmxV2Mappers { try { + var direction = MiscExtensions.ParseEnum(gmxPosition.Direction); + var ticker = MiscExtensions.ParseEnum(gmxPosition.Ticker); var position = new Position("", "", - MiscExtensions.ParseEnum(gmxPosition.Direction), - MiscExtensions.ParseEnum(gmxPosition.Ticker), + direction, + ticker, new MoneyManagement(), PositionInitiator.User, gmxPosition.Date, new User()); - position.Open = Map(gmxPosition.Open); - position.TakeProfit1 = Map(gmxPosition.TakeProfit1); - position.StopLoss = Map(gmxPosition.StopLoss); + position.Open = new Trade(position.Date, direction, TradeStatus.Filled, TradeType.Market, ticker, + (decimal)gmxPosition.Quantity, (decimal)gmxPosition.Price, (decimal)gmxPosition.Leverage, + gmxPosition.Open.ExchangeOrderId, ""); + + if (gmxPosition.TakeProfit1 != null) + { + position.TakeProfit1 = Map(gmxPosition.TakeProfit1); + } + + if (gmxPosition.StopLoss != null) + { + position.StopLoss = Map(gmxPosition.StopLoss); + } + position.ProfitAndLoss = new ProfitAndLoss() { Net = (decimal)gmxPosition.Pnl diff --git a/src/Managing.Web3Proxy/src/generated/gmxsdk/types/trade.ts b/src/Managing.Web3Proxy/src/generated/gmxsdk/types/trade.ts index 86d48aa..2dbb2e1 100644 --- a/src/Managing.Web3Proxy/src/generated/gmxsdk/types/trade.ts +++ b/src/Managing.Web3Proxy/src/generated/gmxsdk/types/trade.ts @@ -117,7 +117,7 @@ export type DecreasePositionAmounts = { receiveTokenAmount: bigint; receiveUsd: bigint; - triggerOrderType?: OrderType.LimitDecrease | OrderType.StopLossDecrease; + triggerOrderType?: OrderType.LimitDecrease | OrderType.StopLossDecrease | OrderType.MarketDecrease; triggerThresholdType?: TriggerThresholdType; decreaseSwapType: DecreasePositionSwapType; }; diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index 6edc76a..1e645fc 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -464,7 +464,7 @@ export const closeGmxPositionImpl = async ( indexPrice: 0n, collateralPrice: 0n, acceptablePrice: position.markPrice, - triggerOrderType: OrderType.LimitDecrease, + triggerOrderType: OrderType.MarketDecrease, triggerPrice: position.markPrice, }