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,
}