diff --git a/.DS_Store b/.DS_Store index 9f12458..0f5830b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index 0326478..08d73d3 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -4,7 +4,9 @@ using Managing.Application.Hubs; using Managing.Application.ManageBot.Commands; using Managing.Application.Workers.Abstractions; using Managing.Api.Models.Responses; +using Managing.Domain.Bots; using Managing.Domain.Candles; +using Managing.Domain.Shared.Helpers; using Managing.Domain.Statistics; using MediatR; using Microsoft.AspNetCore.Authorization; @@ -171,25 +173,25 @@ public class DataController : ControllerBase public async Task> GetTopStrategies() { const string cacheKey = "TopStrategies"; - + // Check if the top strategies are already cached var cachedStrategies = _cacheService.GetValue(cacheKey); - + if (cachedStrategies != null) { return Ok(cachedStrategies); } - + // Get active bots var activeBots = await _mediator.Send(new GetActiveBotsCommand()); - + // Calculate PnL for each bot once and store in a list of tuples var botsWithPnL = activeBots .Select(bot => new { Bot = bot, PnL = bot.GetProfitAndLoss() }) .OrderByDescending(item => item.PnL) .Take(3) .ToList(); - + // Map to view model var topStrategies = new TopStrategiesViewModel { @@ -201,10 +203,130 @@ public class DataController : ControllerBase }) .ToList() }; - + // Cache the result for 10 minutes _cacheService.SaveValue(cacheKey, topStrategies, TimeSpan.FromMinutes(10)); - + return Ok(topStrategies); } + + /// + /// Retrieves list of the active strategies for a user with detailed information + /// + /// The agentName to retrieve strategies for + /// A list of detailed strategy information + [HttpGet("GetUserStrategies")] + public async Task>> GetUserStrategies(string agentName) + { + string cacheKey = $"UserStrategies_{agentName}"; + + // Check if the user strategy details are already cached + var cachedDetails = _cacheService.GetValue>(cacheKey); + + if (cachedDetails != null && cachedDetails.Count > 0) + { + return Ok(cachedDetails); + } + + // Get all strategies for the specified user + var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName)); + + // Convert to detailed view model with additional information + var result = userStrategies.Select(strategy => MapStrategyToViewModel(strategy)).ToList(); + + // Cache the results for 5 minutes + _cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5)); + + return Ok(result); + } + + /// + /// Retrieves a specific strategy for a user by strategy name + /// + /// The agent/user name to retrieve the strategy for + /// The name of the strategy to retrieve + /// Detailed information about the requested strategy + [HttpGet("GetUserStrategy")] + public async Task> GetUserStrategy(string agentName, string strategyName) + { + string cacheKey = $"UserStrategy_{agentName}_{strategyName}"; + + // Check if the user strategy details are already cached + var cachedDetails = _cacheService.GetValue(cacheKey); + + if (cachedDetails != null) + { + return Ok(cachedDetails); + } + + // Get the specific strategy for the user + var strategy = await _mediator.Send(new GetUserStrategyCommand(agentName, strategyName)); + + if (strategy == null) + { + return NotFound($"Strategy '{strategyName}' not found for user '{agentName}'"); + } + + // Map the strategy to a view model using the shared method + var result = MapStrategyToViewModel(strategy); + + // Cache the results for 5 minutes + _cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5)); + + return Ok(result); + } + + /// + /// Maps a trading bot to a strategy view model with detailed statistics + /// + /// The trading bot to map + /// A view model with detailed strategy information + private UserStrategyDetailsViewModel MapStrategyToViewModel(ITradingBot strategy) + { + // Get the runtime directly from the bot + TimeSpan runtimeSpan = strategy.GetRuntime(); + + // Get the startup time from the bot's internal property + // If bot is not running, we use MinValue as a placeholder + DateTime startupTime = DateTime.MinValue; + if (strategy is Bot bot && bot.StartupTime != DateTime.MinValue) + { + startupTime = bot.StartupTime; + } + + // Calculate ROI percentage based on PnL relative to account value + decimal pnl = strategy.GetProfitAndLoss(); + + // If we had initial investment amount, we could calculate ROI like: + decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account + decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0; + + // Calculate volume statistics + decimal totalVolume = TradingBox.GetTotalVolumeTraded(strategy.Positions); + decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(strategy.Positions); + + // Calculate win/loss statistics + (int wins, int losses) = TradingBox.GetWinLossCount(strategy.Positions); + + // Calculate ROI for last 24h + decimal roiLast24h = TradingBox.GetLast24HROI(strategy.Positions); + + return new UserStrategyDetailsViewModel + { + Name = strategy.Name, + StrategyName = strategy.ScenarioName, + State = strategy.GetStatus() == BotStatus.Up.ToString() ? "RUNNING" : + strategy.GetStatus() == BotStatus.Down.ToString() ? "STOPPED" : "UNUSED", + PnL = pnl, + ROIPercentage = roi, + ROILast24H = roiLast24h, + Runtime = startupTime, + WinRate = strategy.GetWinRate(), + TotalVolumeTraded = totalVolume, + VolumeLast24H = volumeLast24h, + Wins = wins, + Losses = losses, + Positions = strategy.Positions.OrderByDescending(p => p.Date).ToList() // Include sorted positions with most recent first + }; + } } \ No newline at end of file diff --git a/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs b/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs new file mode 100644 index 0000000..facbab7 --- /dev/null +++ b/src/Managing.Api/Models/Responses/UserStrategyDetailsViewModel.cs @@ -0,0 +1,76 @@ +using Managing.Domain.Trades; +using static Managing.Common.Enums; + +namespace Managing.Api.Models.Responses +{ + /// + /// Detailed information about a user's deployed strategy + /// + public class UserStrategyDetailsViewModel + { + /// + /// Name of the deployed strategy + /// + public string Name { get; set; } + + /// + /// Strategy identifier + /// + public string StrategyName { get; set; } + + /// + /// Current state of the strategy (RUNNING, STOPPED, UNUSED) + /// + public string State { get; set; } + + /// + /// Total profit or loss generated by the strategy in USD + /// + public decimal PnL { get; set; } + + /// + /// Return on investment percentage + /// + public decimal ROIPercentage { get; set; } + + /// + /// Return on investment percentage in the last 24 hours + /// + public decimal ROILast24H { get; set; } + + /// + /// Date and time when the strategy was started + /// + public DateTime Runtime { get; set; } + + /// + /// Average percentage of successful trades + /// + public int WinRate { get; set; } + + /// + /// Total trading volume for all trades + /// + public decimal TotalVolumeTraded { get; set; } + + /// + /// Trading volume in the last 24 hours + /// + public decimal VolumeLast24H { get; set; } + + /// + /// Number of winning trades + /// + public int Wins { get; set; } + + /// + /// Number of losing trades + /// + public int Losses { get; set; } + + /// + /// List of all positions executed by this strategy + /// + public List Positions { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs index cc346b4..b7ce598 100644 --- a/src/Managing.Application/Abstractions/ITradingBot.cs +++ b/src/Managing.Application/Abstractions/ITradingBot.cs @@ -6,6 +6,7 @@ using Managing.Domain.MoneyManagements; using Managing.Domain.Strategies; using Managing.Domain.Strategies.Base; using Managing.Domain.Trades; +using Managing.Domain.Users; using static Managing.Common.Enums; namespace Managing.Application.Abstractions @@ -26,6 +27,7 @@ namespace Managing.Application.Abstractions BotType BotType { get; set; } Dictionary WalletBalances { get; set; } Dictionary StrategiesValues { get; set; } + User User { get; set; } Task Run(); Task ToggleIsForWatchOnly(); diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index f4e5b7e..dbe1063 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -48,6 +48,7 @@ public class TradingBot : Bot, ITradingBot public Scenario Scenario { get; set; } public Dictionary WalletBalances { get; set; } public Dictionary StrategiesValues { get; set; } + public DateTime StartupTime { get; set; } public TradingBot( string accountName, @@ -139,6 +140,8 @@ public class TradingBot : Bot, ITradingBot else { Account = account; + // Set the User property from the account + User = account.User; } } @@ -806,7 +809,8 @@ public class TradingBot : Bot, ITradingBot AccountName = AccountName, IsForWatchingOnly = IsForWatchingOnly, WalletBalances = WalletBalances, - MoneyManagement = MoneyManagement + MoneyManagement = MoneyManagement, + StartupTime = DateTime.Now }; BotService.SaveOrUpdateBotBackup(Name, BotType, JsonConvert.SerializeObject(data)); } @@ -823,6 +827,12 @@ public class TradingBot : Bot, ITradingBot ScenarioName = data.ScenarioName; AccountName = data.AccountName; IsForWatchingOnly = data.IsForWatchingOnly; + + // Restore the startup time if it was previously saved + if (data.StartupTime != DateTime.MinValue) + { + StartupTime = data.StartupTime; + } } /// @@ -883,4 +893,5 @@ public class TradingBotBackup public bool IsForWatchingOnly { get; set; } public Dictionary WalletBalances { get; set; } public MoneyManagement MoneyManagement { get; set; } + public DateTime StartupTime { get; set; } } \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/Commands/GetUserStrategiesCommand.cs b/src/Managing.Application/ManageBot/Commands/GetUserStrategiesCommand.cs new file mode 100644 index 0000000..e226931 --- /dev/null +++ b/src/Managing.Application/ManageBot/Commands/GetUserStrategiesCommand.cs @@ -0,0 +1,18 @@ +using Managing.Application.Abstractions; +using MediatR; + +namespace Managing.Application.ManageBot.Commands +{ + /// + /// Command to retrieve all strategies owned by a specific user + /// + public class GetUserStrategiesCommand : IRequest> + { + public string UserName { get; } + + public GetUserStrategiesCommand(string userName) + { + UserName = userName; + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/Commands/GetUserStrategyCommand.cs b/src/Managing.Application/ManageBot/Commands/GetUserStrategyCommand.cs new file mode 100644 index 0000000..b5a457c --- /dev/null +++ b/src/Managing.Application/ManageBot/Commands/GetUserStrategyCommand.cs @@ -0,0 +1,27 @@ +using Managing.Application.Abstractions; +using MediatR; + +namespace Managing.Application.ManageBot.Commands +{ + /// + /// Command to retrieve a specific strategy owned by a user + /// + public class GetUserStrategyCommand : IRequest + { + /// + /// The username of the agent/user that owns the strategy + /// + public string AgentName { get; } + + /// + /// The name of the strategy/bot to retrieve + /// + public string StrategyName { get; } + + public GetUserStrategyCommand(string agentName, string strategyName) + { + AgentName = agentName; + StrategyName = strategyName; + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/GetUserStrategiesCommandHandler.cs b/src/Managing.Application/ManageBot/GetUserStrategiesCommandHandler.cs new file mode 100644 index 0000000..a4f2796 --- /dev/null +++ b/src/Managing.Application/ManageBot/GetUserStrategiesCommandHandler.cs @@ -0,0 +1,26 @@ +using Managing.Application.Abstractions; +using Managing.Application.ManageBot.Commands; +using MediatR; + +namespace Managing.Application.ManageBot +{ + public class GetUserStrategiesCommandHandler : IRequestHandler> + { + private readonly IBotService _botService; + + public GetUserStrategiesCommandHandler(IBotService botService) + { + _botService = botService; + } + + public Task> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken) + { + var allActiveBots = _botService.GetActiveBots(); + var userBots = allActiveBots + .Where(bot => bot.User != null && bot.User.Name == request.UserName) + .ToList(); + + return Task.FromResult(userBots); + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/GetUserStrategyCommandHandler.cs b/src/Managing.Application/ManageBot/GetUserStrategyCommandHandler.cs new file mode 100644 index 0000000..640ad86 --- /dev/null +++ b/src/Managing.Application/ManageBot/GetUserStrategyCommandHandler.cs @@ -0,0 +1,33 @@ +using Managing.Application.Abstractions; +using Managing.Application.ManageBot.Commands; +using MediatR; + +namespace Managing.Application.ManageBot +{ + /// + /// Handler for retrieving a specific strategy owned by a user + /// + public class GetUserStrategyCommandHandler : IRequestHandler + { + private readonly IBotService _botService; + + public GetUserStrategyCommandHandler(IBotService botService) + { + _botService = botService; + } + + public Task Handle(GetUserStrategyCommand request, CancellationToken cancellationToken) + { + var allActiveBots = _botService.GetActiveBots(); + + // Find the specific strategy that matches both user and strategy name + var strategy = allActiveBots + .FirstOrDefault(bot => + bot.User != null && + bot.User.Name == request.AgentName && + bot.Name == request.StrategyName); + + return Task.FromResult(strategy); + } + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Bots/Bot.cs b/src/Managing.Domain/Bots/Bot.cs index fd8039c..fbf98b2 100644 --- a/src/Managing.Domain/Bots/Bot.cs +++ b/src/Managing.Domain/Bots/Bot.cs @@ -16,6 +16,10 @@ namespace Managing.Domain.Bots public int Interval { get; set; } public BotStatus Status { get; set; } public User User { get; set; } + /// + /// The time when the bot was started + /// + public DateTime StartupTime { get; private set; } private CancellationTokenSource CancellationToken { get; set; } public Bot(string name) @@ -26,11 +30,13 @@ namespace Managing.Domain.Bots CancellationToken = new CancellationTokenSource(); ExecutionCount = 0; Interval = 3000; + StartupTime = DateTime.MinValue; // Initialize with minimum value to indicate it hasn't been started yet } public virtual void Start() { Status = BotStatus.Up; + StartupTime = DateTime.UtcNow; // Record the startup time when the bot is started } public async Task InitWorker(Func action) @@ -76,6 +82,7 @@ namespace Managing.Domain.Bots public void Restart() { Status = BotStatus.Up; + StartupTime = DateTime.UtcNow; // Update the startup time when the bot is restarted } public string GetStatus() @@ -87,6 +94,18 @@ namespace Managing.Domain.Bots { return Name; } + + /// + /// Gets the total runtime of the bot since it was started + /// + /// TimeSpan representing the runtime, or TimeSpan.Zero if the bot is not running + public TimeSpan GetRuntime() + { + if (Status != BotStatus.Up || StartupTime == DateTime.MinValue) + return TimeSpan.Zero; + + return DateTime.UtcNow - StartupTime; + } public abstract void SaveBackup(); public abstract void LoadBackup(BotBackup backup); diff --git a/src/Managing.Domain/Bots/IBot.cs b/src/Managing.Domain/Bots/IBot.cs index 95a240e..b69bc11 100644 --- a/src/Managing.Domain/Bots/IBot.cs +++ b/src/Managing.Domain/Bots/IBot.cs @@ -8,6 +8,11 @@ void Restart(); string GetStatus(); string GetName(); + /// + /// Gets the total runtime of the bot since it was started + /// + /// TimeSpan representing the runtime, or TimeSpan.Zero if the bot is not running + TimeSpan GetRuntime(); void SaveBackup(); void LoadBackup(BotBackup backup); } diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 829f58f..420dfec 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -187,4 +187,140 @@ public static class TradingBox pnl.Realized = pnl.Realized * leverage; return pnl; } + + /// + /// Calculates the total volume traded across all positions + /// + /// List of positions to analyze + /// The total volume traded in decimal + public static decimal GetTotalVolumeTraded(List positions) + { + decimal totalVolume = 0; + + foreach (var position in positions) + { + // Add entry volume + totalVolume += position.Open.Quantity * position.Open.Price; + + // Add exit volumes from stop loss or take profits if they were executed + if (position.StopLoss.Status == TradeStatus.Filled) + { + totalVolume += position.StopLoss.Quantity * position.StopLoss.Price; + } + + if (position.TakeProfit1.Status == TradeStatus.Filled) + { + totalVolume += position.TakeProfit1.Quantity * position.TakeProfit1.Price; + } + + if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled) + { + totalVolume += position.TakeProfit2.Quantity * position.TakeProfit2.Price; + } + } + + return totalVolume; + } + + /// + /// Calculates the volume traded in the last 24 hours + /// + /// List of positions to analyze + /// The volume traded in the last 24 hours in decimal + public static decimal GetLast24HVolumeTraded(List positions) + { + decimal last24hVolume = 0; + DateTime cutoff = DateTime.UtcNow.AddHours(-24); + + foreach (var position in positions) + { + // Check if any part of this position was traded in the last 24 hours + + // Add entry volume if it was within the last 24 hours + if (position.Open.Date >= cutoff) + { + last24hVolume += position.Open.Quantity * position.Open.Price; + } + + // Add exit volumes if they were executed within the last 24 hours + if (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date >= cutoff) + { + last24hVolume += position.StopLoss.Quantity * position.StopLoss.Price; + } + + if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date >= cutoff) + { + last24hVolume += position.TakeProfit1.Quantity * position.TakeProfit1.Price; + } + + if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && + position.TakeProfit2.Date >= cutoff) + { + last24hVolume += position.TakeProfit2.Quantity * position.TakeProfit2.Price; + } + } + + return last24hVolume; + } + + /// + /// Gets the win/loss counts from positions + /// + /// List of positions to analyze + /// A tuple containing (wins, losses) + public static (int Wins, int Losses) GetWinLossCount(List positions) + { + int wins = 0; + int losses = 0; + + foreach (var position in positions) + { + // Only count finished positions + if (position.IsFinished()) + { + if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0) + { + wins++; + } + else + { + losses++; + } + } + } + + return (wins, losses); + } + + /// + /// Calculates the ROI for the last 24 hours + /// + /// List of positions to analyze + /// The ROI for the last 24 hours as a percentage + public static decimal GetLast24HROI(List positions) + { + decimal profitLast24h = 0; + decimal investmentLast24h = 0; + DateTime cutoff = DateTime.UtcNow.AddHours(-24); + + foreach (var position in positions) + { + // Only count positions that were opened or closed within the last 24 hours + if (position.IsFinished() && + (position.Open.Date >= cutoff || + (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date >= cutoff) || + (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date >= cutoff) || + (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date >= cutoff))) + { + profitLast24h += position.ProfitAndLoss != null ? position.ProfitAndLoss.Realized : 0; + investmentLast24h += position.Open.Quantity * position.Open.Price; + } + } + + // Avoid division by zero + if (investmentLast24h == 0) + return 0; + + return (profitLast24h / investmentLast24h) * 100; + } } \ No newline at end of file diff --git a/src/Managing.Web3Proxy/.DS_Store b/src/Managing.Web3Proxy/.DS_Store index b349bd1..5366bf7 100644 Binary files a/src/Managing.Web3Proxy/.DS_Store and b/src/Managing.Web3Proxy/.DS_Store differ