Add data + fix positions

This commit is contained in:
2025-04-24 23:48:28 +07:00
parent 1d14d31af2
commit af89121c40
9 changed files with 549 additions and 9 deletions

View File

@@ -8,10 +8,13 @@ using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics; using Managing.Domain.Statistics;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using System.Linq;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Api.Controllers; 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 Positions = strategy.Positions.OrderByDescending(p => p.Date).ToList() // Include sorted positions with most recent first
}; };
} }
/// <summary>
/// Retrieves a summary of platform activity across all agents
/// </summary>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
/// <returns>A summary of platform activity including per-agent statistics</returns>
[HttpGet("GetPlatformSummary")]
public async Task<ActionResult<PlatformSummaryViewModel>> 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<PlatformSummaryViewModel>(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<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
{
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);
}
} }

View File

@@ -0,0 +1,104 @@
namespace Managing.Api.Models.Responses
{
/// <summary>
/// Summary of agent performance and activity
/// </summary>
public class AgentSummaryViewModel
{
/// <summary>
/// Username of the agent
/// </summary>
public string Username { get; set; }
/// <summary>
/// Total profit and loss in USD
/// </summary>
public decimal TotalPnL { get; set; }
/// <summary>
/// Profit and loss in the last 24 hours in USD
/// </summary>
public decimal PnLLast24h { get; set; }
/// <summary>
/// Total return on investment as a percentage
/// </summary>
public decimal TotalROI { get; set; }
/// <summary>
/// Return on investment in the last 24 hours as a percentage
/// </summary>
public decimal ROILast24h { get; set; }
/// <summary>
/// Number of winning trades
/// </summary>
public int Wins { get; set; }
/// <summary>
/// Number of losing trades
/// </summary>
public int Losses { get; set; }
/// <summary>
/// Average win rate as a percentage
/// </summary>
public int AverageWinRate { get; set; }
/// <summary>
/// Number of active strategies for this agent
/// </summary>
public int ActiveStrategiesCount { get; set; }
/// <summary>
/// Total volume traded by this agent in USD
/// </summary>
public decimal TotalVolume { get; set; }
/// <summary>
/// Volume traded in the last 24 hours in USD
/// </summary>
public decimal VolumeLast24h { get; set; }
}
/// <summary>
/// Platform-wide statistics including per-agent summaries
/// </summary>
public class PlatformSummaryViewModel
{
/// <summary>
/// Total number of agents on the platform
/// </summary>
public int TotalAgents { get; set; }
/// <summary>
/// Total number of active strategies across all agents
/// </summary>
public int TotalActiveStrategies { get; set; }
/// <summary>
/// Total platform-wide profit and loss in USD
/// </summary>
public decimal TotalPlatformPnL { get; set; }
/// <summary>
/// Total volume traded across all agents in USD
/// </summary>
public decimal TotalPlatformVolume { get; set; }
/// <summary>
/// Total volume traded across all agents in the last 24 hours in USD
/// </summary>
public decimal TotalPlatformVolumeLast24h { get; set; }
/// <summary>
/// Summaries for each agent
/// </summary>
public List<AgentSummaryViewModel> AgentSummaries { get; set; } = new List<AgentSummaryViewModel>();
/// <summary>
/// Time filter applied to the data
/// </summary>
public string TimeFilter { get; set; } = "Total";
}
}

View File

@@ -0,0 +1,22 @@
using Managing.Application.Abstractions;
using Managing.Domain.Users;
using MediatR;
namespace Managing.Application.ManageBot.Commands
{
/// <summary>
/// Command to retrieve all active agents and their strategies
/// </summary>
public class GetAllAgentsCommand : IRequest<Dictionary<User, List<ITradingBot>>>
{
/// <summary>
/// Optional time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)
/// </summary>
public string TimeFilter { get; }
public GetAllAgentsCommand(string timeFilter = "Total")
{
TimeFilter = timeFilter;
}
}
}

View File

@@ -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
{
/// <summary>
/// Handler for retrieving all agents and their strategies
/// </summary>
public class GetAllAgentsCommandHandler : IRequestHandler<GetAllAgentsCommand, Dictionary<User, List<ITradingBot>>>
{
private readonly IBotService _botService;
private readonly IAccountService _accountService;
public GetAllAgentsCommandHandler(IBotService botService, IAccountService accountService)
{
_botService = botService;
_accountService = accountService;
}
public async Task<Dictionary<User, List<ITradingBot>>> Handle(GetAllAgentsCommand request,
CancellationToken cancellationToken)
{
var result = new Dictionary<User, List<ITradingBot>>();
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<ITradingBot>();
}
result[bot.User].Add(bot);
}
return result;
}
/// <summary>
/// Checks if a bot has had trading activity within the specified time range
/// </summary>
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));
}
}
}

View File

@@ -35,11 +35,26 @@ public class ClosePositionCommandHandler(
? request.ExecutionPrice.GetValueOrDefault() ? request.ExecutionPrice.GetValueOrDefault()
: exchangeService.GetPrice(account, request.Position.Ticker, DateTime.UtcNow); : 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 // Close market
var closedPosition = var closedPosition =
await exchangeService.ClosePosition(account, request.Position, lastPrice, isForPaperTrading); 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)) if (closeRequestedOrders || closedPosition.Status == (TradeStatus.PendingOpen | TradeStatus.Filled))
{ {

View File

@@ -323,4 +323,173 @@ public static class TradingBox
return (profitLast24h / investmentLast24h) * 100; return (profitLast24h / investmentLast24h) * 100;
} }
/// <summary>
/// Calculates profit and loss for positions within a specific time range
/// </summary>
/// <param name="positions">List of positions to analyze</param>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
/// <returns>The PnL for positions in the specified range</returns>
public static decimal GetPnLInTimeRange(List<Position> 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);
}
/// <summary>
/// Calculates ROI for positions within a specific time range
/// </summary>
/// <param name="positions">List of positions to analyze</param>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
/// <returns>The ROI as a percentage for positions in the specified range</returns>
public static decimal GetROIInTimeRange(List<Position> 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;
}
/// <summary>
/// Gets the win/loss counts from positions in a specific time range
/// </summary>
/// <param name="positions">List of positions to analyze</param>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
/// <returns>A tuple containing (wins, losses)</returns>
public static (int Wins, int Losses) GetWinLossCountInTimeRange(List<Position> 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);
}
} }

View File

@@ -162,16 +162,29 @@ internal static class GmxV2Mappers
{ {
try try
{ {
var direction = MiscExtensions.ParseEnum<TradeDirection>(gmxPosition.Direction);
var ticker = MiscExtensions.ParseEnum<Ticker>(gmxPosition.Ticker);
var position = new Position("", "", var position = new Position("", "",
MiscExtensions.ParseEnum<TradeDirection>(gmxPosition.Direction), direction,
MiscExtensions.ParseEnum<Ticker>(gmxPosition.Ticker), ticker,
new MoneyManagement(), new MoneyManagement(),
PositionInitiator.User, PositionInitiator.User,
gmxPosition.Date, gmxPosition.Date,
new User()); new User());
position.Open = Map(gmxPosition.Open); 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); position.TakeProfit1 = Map(gmxPosition.TakeProfit1);
}
if (gmxPosition.StopLoss != null)
{
position.StopLoss = Map(gmxPosition.StopLoss); position.StopLoss = Map(gmxPosition.StopLoss);
}
position.ProfitAndLoss = new ProfitAndLoss() position.ProfitAndLoss = new ProfitAndLoss()
{ {
Net = (decimal)gmxPosition.Pnl Net = (decimal)gmxPosition.Pnl

View File

@@ -117,7 +117,7 @@ export type DecreasePositionAmounts = {
receiveTokenAmount: bigint; receiveTokenAmount: bigint;
receiveUsd: bigint; receiveUsd: bigint;
triggerOrderType?: OrderType.LimitDecrease | OrderType.StopLossDecrease; triggerOrderType?: OrderType.LimitDecrease | OrderType.StopLossDecrease | OrderType.MarketDecrease;
triggerThresholdType?: TriggerThresholdType; triggerThresholdType?: TriggerThresholdType;
decreaseSwapType: DecreasePositionSwapType; decreaseSwapType: DecreasePositionSwapType;
}; };

View File

@@ -464,7 +464,7 @@ export const closeGmxPositionImpl = async (
indexPrice: 0n, indexPrice: 0n,
collateralPrice: 0n, collateralPrice: 0n,
acceptablePrice: position.markPrice, acceptablePrice: position.markPrice,
triggerOrderType: OrderType.LimitDecrease, triggerOrderType: OrderType.MarketDecrease,
triggerPrice: position.markPrice, triggerPrice: position.markPrice,
} }