From 76b087a6e411c287e0b5d0cd72bf0f6b2515ae41 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Wed, 8 Oct 2025 18:37:38 +0700 Subject: [PATCH] Fix status and filtered positions for metrics --- .../Abstractions/IBotService.cs | 1 - .../Bots/Grains/AgentGrain.cs | 3 +- .../Bots/Grains/LiveTradingBotGrain.cs | 12 +++-- .../Bots/TradingBotBase.cs | 38 ++++++++------- .../ManageBot/BotService.cs | 48 ------------------- .../Shared/MessengerService.cs | 24 +++++----- .../Shared/Helpers/TradingBox.cs | 8 ++-- src/Managing.Domain/Trades/Position.cs | 12 ++++- 8 files changed, 57 insertions(+), 89 deletions(-) diff --git a/src/Managing.Application/Abstractions/IBotService.cs b/src/Managing.Application/Abstractions/IBotService.cs index 572b9fd9..2ebb717b 100644 --- a/src/Managing.Application/Abstractions/IBotService.cs +++ b/src/Managing.Application/Abstractions/IBotService.cs @@ -24,7 +24,6 @@ public interface IBotService Task ClosePositionAsync(Guid identifier, Guid positionId); Task GetBotConfig(Guid identifier); Task> GetBotConfigsByIdsAsync(IEnumerable botIds); - Task UpdateBotStatisticsAsync(Guid identifier); Task SaveBotStatisticsAsync(Bot bot); /// diff --git a/src/Managing.Application/Bots/Grains/AgentGrain.cs b/src/Managing.Application/Bots/Grains/AgentGrain.cs index 63e71641..6831c78b 100644 --- a/src/Managing.Application/Bots/Grains/AgentGrain.cs +++ b/src/Managing.Application/Bots/Grains/AgentGrain.cs @@ -169,7 +169,8 @@ public class AgentGrain : Grain, IAgentGrain try { // Get all positions for this agent's bots as initiator - var positions = (await _tradingService.GetPositionByUserIdAsync((int)this.GetPrimaryKeyLong())).ToList(); + var positions = (await _tradingService.GetPositionByUserIdAsync((int)this.GetPrimaryKeyLong())) + .Where(p => p.IsValidForMetrics()).ToList(); // Calculate aggregated statistics from position data var totalPnL = positions.Sum(p => p.ProfitAndLoss?.Realized ?? 0); diff --git a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs index a27b289f..b6d64ac1 100644 --- a/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveTradingBotGrain.cs @@ -791,22 +791,24 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable } } + var positionForMetrics = _tradingBot.Positions.Where(p => p.Value.IsValidForMetrics()) + .Select(p => p.Value).ToList(); // Calculate statistics using TradingBox helpers - var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions); + var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(positionForMetrics); var pnl = _tradingBot.GetProfitAndLoss(); var fees = _tradingBot.GetTotalFees(); var netPnl = pnl - fees; // Net PnL after fees - var volume = TradingBox.GetTotalVolumeTraded(_tradingBot.Positions); + var volume = TradingBox.GetTotalVolumeTraded(positionForMetrics); // Calculate ROI based on total investment (Net PnL) - var totalInvestment = _tradingBot.Positions.Values + var totalInvestment = positionForMetrics .Sum(p => p.Open.Quantity * p.Open.Price); var roi = totalInvestment > 0 ? (netPnl / totalInvestment) * 100 : 0; // Calculate long and short position counts - var longPositionCount = _tradingBot.Positions.Values + var longPositionCount = positionForMetrics .Count(p => p.OriginDirection == TradeDirection.Long); - var shortPositionCount = _tradingBot.Positions.Values + var shortPositionCount = positionForMetrics .Count(p => p.OriginDirection == TradeDirection.Short); // Create complete Bot object with all statistics diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index 4c6bdb2e..1fa962fa 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -277,7 +277,7 @@ public class TradingBotBase : ITradingBot private async Task ManagePositions() { // First, process all existing positions that are not finished - foreach (var position in Positions.Values.Where(p => p.IsOpen())) + foreach (var position in Positions.Values.Where(p => !p.IsFinished())) { var signalForPosition = Signals[position.SignalIdentifier]; if (signalForPosition == null) @@ -307,15 +307,17 @@ public class TradingBotBase : ITradingBot } // Second, process all finished positions to ensure they are updated in the database - // This should be removed in the future, when we have a better way to handle positions + // TODO : This should be removed in the future, when we have a better way to handle positions foreach (var position in Positions.Values.Where(p => p.IsFinished())) { try { - var positionInDatabase = await ServiceScopeHelpers.WithScopedService(_scopeFactory, async tradingService => - { - return await tradingService.GetPositionByIdentifierAsync(position.Identifier); - }); + var positionInDatabase = await ServiceScopeHelpers.WithScopedService( + _scopeFactory, + async tradingService => + { + return await tradingService.GetPositionByIdentifierAsync(position.Identifier); + }); if (positionInDatabase != null && positionInDatabase.Status != position.Status) { @@ -323,7 +325,6 @@ public class TradingBotBase : ITradingBot await LogInformation( $"💾 Database Update\nPosition: `{position.Identifier}`\nStatus: `{position.Status}`\nUpdated in database"); } - } catch (Exception ex) { @@ -425,11 +426,11 @@ public class TradingBotBase : ITradingBot { var previousPositionStatus = internalPosition.Status; // Position found on the broker, means the position is filled - var brokerNetPnL = brokerPosition.GetNetPnL(); - UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL); + var brokerPnlBeforeFees = brokerPosition.GetPnLBeforeFees(); + UpdatePositionPnl(positionForSignal.Identifier, brokerPnlBeforeFees); var totalFees = internalPosition.GasFees + internalPosition.UiFees; - var netPnl = brokerNetPnL - totalFees; - internalPosition.ProfitAndLoss = new ProfitAndLoss { Realized = brokerNetPnL, Net = netPnl }; + var netPnl = brokerPnlBeforeFees - totalFees; + internalPosition.ProfitAndLoss = new ProfitAndLoss { Realized = brokerPnlBeforeFees, Net = netPnl }; internalPosition.Status = PositionStatus.Filled; await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Filled); @@ -561,7 +562,7 @@ public class TradingBotBase : ITradingBot $"✅ Position Found on Broker\nPosition is already open on broker\nUpdating position status to Filled"); // Calculate net PnL after fees for broker position - var brokerNetPnL = brokerPosition.GetNetPnL(); + var brokerNetPnL = brokerPosition.GetPnLBeforeFees(); UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL); // Update Open trade status when position is found on broker with 2 orders @@ -603,7 +604,7 @@ public class TradingBotBase : ITradingBot else { await LogWarning( - $"❌ Position Never Filled\nNo position on exchange and no orders\nSignal: `{signal.Identifier}`\nPosition was never filled and will be marked as canceled."); + $"❌ Position Never Filled\nNo position on exchange and no orders\nPosition was never filled and will be marked as canceled."); // Position was never filled (still in New status), so just mark it as canceled // Don't call HandleClosedPosition as that would incorrectly add volume/PnL @@ -623,7 +624,8 @@ public class TradingBotBase : ITradingBot { lastCandle = Config.IsForBacktest ? LastCandle - : await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow); + : await exchangeService.GetCandle(Account, Config.Ticker, + DateTime.UtcNow); }); var currentTime = Config.IsForBacktest ? lastCandle.Date : DateTime.UtcNow; @@ -1686,7 +1688,7 @@ public class TradingBotBase : ITradingBot $"📊 Position Status Change\nPosition: `{position.Ticker}`\nDirection: `{position.OriginDirection}`\nNew Status: `{positionStatus}`"); // Update Open trade status when position becomes Filled - if (positionStatus == PositionStatus.Filled && position.Open != null) + if (positionStatus == PositionStatus.Filled) { position.Open.SetStatus(TradeStatus.Filled); } @@ -1745,8 +1747,8 @@ public class TradingBotBase : ITradingBot public decimal GetProfitAndLoss() { // Calculate net PnL after deducting fees for each position - var netPnl = Positions.Values.Where(p => p.ProfitAndLoss != null) - .Sum(p => p.GetNetPnL()); + var netPnl = Positions.Values.Where(p => p.IsValidForMetrics() && p.ProfitAndLoss != null) + .Sum(p => p.GetPnLBeforeFees()); return netPnl; } @@ -1760,7 +1762,7 @@ public class TradingBotBase : ITradingBot { decimal totalFees = 0; - foreach (var position in Positions.Values.Where(p => p.Open.Price > 0 && p.Open.Quantity > 0)) + foreach (var position in Positions.Values.Where(p => p.IsValidForMetrics())) { totalFees += TradingHelpers.CalculatePositionFees(position); } diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs index b5dbace7..a35eee52 100644 --- a/src/Managing.Application/ManageBot/BotService.cs +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -10,7 +10,6 @@ using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Indicators; using Managing.Domain.Scenarios; -using Managing.Domain.Shared.Helpers; using Managing.Domain.Trades; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -277,53 +276,6 @@ namespace Managing.Application.ManageBot return await grain.ClosePositionAsync(positionId); } - public async Task UpdateBotStatisticsAsync(Guid identifier) - { - try - { - var grain = _grainFactory.GetGrain(identifier); - var botData = await grain.GetBotDataAsync(); - - // Get the current bot from database - var existingBot = await _botRepository.GetBotByIdentifierAsync(identifier); - if (existingBot == null) - { - _tradingBotLogger.LogWarning("Bot {Identifier} not found in database for statistics update", - identifier); - return false; - } - - // Calculate statistics using TradingBox helpers - var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(botData.Positions); - var pnl = botData.ProfitAndLoss; - var fees = botData.Positions.Values.Sum(p => p.CalculateTotalFees()); - var volume = TradingBox.GetTotalVolumeTraded(botData.Positions); - - // Calculate ROI based on total investment - var totalInvestment = botData.Positions.Values - .Where(p => p.IsValidForMetrics()) - .Sum(p => p.Open.Quantity * p.Open.Price); - var netPnl = pnl - fees; - var roi = totalInvestment > 0 ? (netPnl / totalInvestment) * 100 : 0; - - // Update bot statistics - existingBot.TradeWins = tradeWins; - existingBot.TradeLosses = tradeLosses; - existingBot.Pnl = pnl; - existingBot.Roi = roi; - existingBot.Volume = volume; - existingBot.Fees = fees; - - // Use the new SaveBotStatisticsAsync method - return await SaveBotStatisticsAsync(existingBot); - } - catch (Exception e) - { - _tradingBotLogger.LogError(e, "Error updating bot statistics for {Identifier}", identifier); - return false; - } - } - public async Task SaveBotStatisticsAsync(Bot bot) { try diff --git a/src/Managing.Application/Shared/MessengerService.cs b/src/Managing.Application/Shared/MessengerService.cs index 92249582..72f45351 100644 --- a/src/Managing.Application/Shared/MessengerService.cs +++ b/src/Managing.Application/Shared/MessengerService.cs @@ -91,10 +91,10 @@ public class MessengerService : IMessengerService private string BuildClosePositionMessage(Position position) { return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" + - $"Open Price : {position.Open.Price} \n" + - $"Closing Price : {position.Open.Price} \n" + - $"Quantity :{position.Open.Quantity} \n" + - $"PNL : {position.ProfitAndLoss.Realized} $"; + $"Open Price : {position.Open.Price} \n" + + $"Closing Price : {position.Open.Price} \n" + + $"Quantity :{position.Open.Quantity} \n" + + $"PNL : {position.ProfitAndLoss.Realized} $"; } public async Task SendMessage(string message, string telegramChannel) @@ -114,21 +114,23 @@ public class MessengerService : IMessengerService if (position.Open != null) { - message += $"\nOpen Trade: {position.Open.Quantity:F5} @ {position.Open.Price:F5}"; + message += $"\nOpen Trade: {position.Open.Price:F5}"; } - if (position.StopLoss != null){ - message += $"\nStop Loss: {position.StopLoss.Quantity:F5} @ {position.StopLoss.Price:F5}"; + if (position.StopLoss != null) + { + message += $"\nStop Loss: {position.StopLoss.Price:F5}"; } - if (position.TakeProfit1 != null){ - message += $"\nTake Profit 1: {position.TakeProfit1.Quantity:F5} @ {position.TakeProfit1.Price:F5}"; + if (position.TakeProfit1 != null) + { + message += $"\nTake Profit 1: {position.TakeProfit1.Price:F5}"; } if (position.ProfitAndLoss != null) { - var pnlEmoji = position.ProfitAndLoss.Realized >= 0 ? "✅" : "❌"; - message += $"\nPnL: {pnlEmoji} ${position.ProfitAndLoss.Realized:F5}"; + var pnlEmoji = position.ProfitAndLoss.Net >= 0 ? "✅" : "❌"; + message += $"\nPnL: {pnlEmoji} ${position.ProfitAndLoss.Net:F5}"; } return message; diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index aa39a417..4ac65dc1 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -454,11 +454,11 @@ public static class TradingBox /// /// List of positions to analyze /// The total volume traded in decimal - public static decimal GetTotalVolumeTraded(Dictionary positions) + public static decimal GetTotalVolumeTraded(List positions) { decimal totalVolume = 0; - foreach (var position in positions.Values) + foreach (var position in positions) { // Add entry volume totalVolume += position.Open.Quantity * position.Open.Price; @@ -530,12 +530,12 @@ public static class TradingBox /// /// List of positions to analyze /// A tuple containing (wins, losses) - public static (int Wins, int Losses) GetWinLossCount(Dictionary positions) + public static (int Wins, int Losses) GetWinLossCount(List positions) { int wins = 0; int losses = 0; - foreach (var position in positions.Values) + foreach (var position in positions) { if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0) { diff --git a/src/Managing.Domain/Trades/Position.cs b/src/Managing.Domain/Trades/Position.cs index b9bfe58a..092807fd 100644 --- a/src/Managing.Domain/Trades/Position.cs +++ b/src/Managing.Domain/Trades/Position.cs @@ -141,7 +141,7 @@ namespace Managing.Domain.Trades /// Gets the net PnL after deducting fees /// /// The net PnL after fees - public decimal GetNetPnL() + public decimal GetPnLBeforeFees() { if (ProfitAndLoss?.Realized == null) { @@ -151,6 +151,16 @@ namespace Managing.Domain.Trades return ProfitAndLoss.Realized; } + public decimal GetNetPnl() + { + if (ProfitAndLoss?.Net == null) + { + return 0; + } + + return ProfitAndLoss.Realized - UiFees - GasFees; + } + /// /// Updates the UI fees for this position ///