Fix status and filtered positions for metrics

This commit is contained in:
2025-10-08 18:37:38 +07:00
parent 86dd6849ea
commit 76b087a6e4
8 changed files with 57 additions and 89 deletions

View File

@@ -24,7 +24,6 @@ public interface IBotService
Task<Position> ClosePositionAsync(Guid identifier, Guid positionId); Task<Position> ClosePositionAsync(Guid identifier, Guid positionId);
Task<TradingBotConfig> GetBotConfig(Guid identifier); Task<TradingBotConfig> GetBotConfig(Guid identifier);
Task<IEnumerable<TradingBotConfig>> GetBotConfigsByIdsAsync(IEnumerable<Guid> botIds); Task<IEnumerable<TradingBotConfig>> GetBotConfigsByIdsAsync(IEnumerable<Guid> botIds);
Task<bool> UpdateBotStatisticsAsync(Guid identifier);
Task<bool> SaveBotStatisticsAsync(Bot bot); Task<bool> SaveBotStatisticsAsync(Bot bot);
/// <summary> /// <summary>

View File

@@ -169,7 +169,8 @@ public class AgentGrain : Grain, IAgentGrain
try try
{ {
// Get all positions for this agent's bots as initiator // 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 // Calculate aggregated statistics from position data
var totalPnL = positions.Sum(p => p.ProfitAndLoss?.Realized ?? 0); var totalPnL = positions.Sum(p => p.ProfitAndLoss?.Realized ?? 0);

View File

@@ -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 // Calculate statistics using TradingBox helpers
var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(_tradingBot.Positions); var (tradeWins, tradeLosses) = TradingBox.GetWinLossCount(positionForMetrics);
var pnl = _tradingBot.GetProfitAndLoss(); var pnl = _tradingBot.GetProfitAndLoss();
var fees = _tradingBot.GetTotalFees(); var fees = _tradingBot.GetTotalFees();
var netPnl = pnl - fees; // Net PnL after fees 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) // Calculate ROI based on total investment (Net PnL)
var totalInvestment = _tradingBot.Positions.Values var totalInvestment = positionForMetrics
.Sum(p => p.Open.Quantity * p.Open.Price); .Sum(p => p.Open.Quantity * p.Open.Price);
var roi = totalInvestment > 0 ? (netPnl / totalInvestment) * 100 : 0; var roi = totalInvestment > 0 ? (netPnl / totalInvestment) * 100 : 0;
// Calculate long and short position counts // Calculate long and short position counts
var longPositionCount = _tradingBot.Positions.Values var longPositionCount = positionForMetrics
.Count(p => p.OriginDirection == TradeDirection.Long); .Count(p => p.OriginDirection == TradeDirection.Long);
var shortPositionCount = _tradingBot.Positions.Values var shortPositionCount = positionForMetrics
.Count(p => p.OriginDirection == TradeDirection.Short); .Count(p => p.OriginDirection == TradeDirection.Short);
// Create complete Bot object with all statistics // Create complete Bot object with all statistics

View File

@@ -277,7 +277,7 @@ public class TradingBotBase : ITradingBot
private async Task ManagePositions() private async Task ManagePositions()
{ {
// First, process all existing positions that are not finished // 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]; var signalForPosition = Signals[position.SignalIdentifier];
if (signalForPosition == null) if (signalForPosition == null)
@@ -307,15 +307,17 @@ public class TradingBotBase : ITradingBot
} }
// Second, process all finished positions to ensure they are updated in the database // 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())) foreach (var position in Positions.Values.Where(p => p.IsFinished()))
{ {
try try
{ {
var positionInDatabase = await ServiceScopeHelpers.WithScopedService<ITradingService, Position>(_scopeFactory, async tradingService => var positionInDatabase = await ServiceScopeHelpers.WithScopedService<ITradingService, Position>(
{ _scopeFactory,
return await tradingService.GetPositionByIdentifierAsync(position.Identifier); async tradingService =>
}); {
return await tradingService.GetPositionByIdentifierAsync(position.Identifier);
});
if (positionInDatabase != null && positionInDatabase.Status != position.Status) if (positionInDatabase != null && positionInDatabase.Status != position.Status)
{ {
@@ -323,7 +325,6 @@ public class TradingBotBase : ITradingBot
await LogInformation( await LogInformation(
$"💾 Database Update\nPosition: `{position.Identifier}`\nStatus: `{position.Status}`\nUpdated in database"); $"💾 Database Update\nPosition: `{position.Identifier}`\nStatus: `{position.Status}`\nUpdated in database");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -425,11 +426,11 @@ public class TradingBotBase : ITradingBot
{ {
var previousPositionStatus = internalPosition.Status; var previousPositionStatus = internalPosition.Status;
// Position found on the broker, means the position is filled // Position found on the broker, means the position is filled
var brokerNetPnL = brokerPosition.GetNetPnL(); var brokerPnlBeforeFees = brokerPosition.GetPnLBeforeFees();
UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL); UpdatePositionPnl(positionForSignal.Identifier, brokerPnlBeforeFees);
var totalFees = internalPosition.GasFees + internalPosition.UiFees; var totalFees = internalPosition.GasFees + internalPosition.UiFees;
var netPnl = brokerNetPnL - totalFees; var netPnl = brokerPnlBeforeFees - totalFees;
internalPosition.ProfitAndLoss = new ProfitAndLoss { Realized = brokerNetPnL, Net = netPnl }; internalPosition.ProfitAndLoss = new ProfitAndLoss { Realized = brokerPnlBeforeFees, Net = netPnl };
internalPosition.Status = PositionStatus.Filled; internalPosition.Status = PositionStatus.Filled;
await SetPositionStatus(internalPosition.SignalIdentifier, 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"); $"✅ Position Found on Broker\nPosition is already open on broker\nUpdating position status to Filled");
// Calculate net PnL after fees for broker position // Calculate net PnL after fees for broker position
var brokerNetPnL = brokerPosition.GetNetPnL(); var brokerNetPnL = brokerPosition.GetPnLBeforeFees();
UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL); UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL);
// Update Open trade status when position is found on broker with 2 orders // Update Open trade status when position is found on broker with 2 orders
@@ -603,7 +604,7 @@ public class TradingBotBase : ITradingBot
else else
{ {
await LogWarning( 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 // 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 // Don't call HandleClosedPosition as that would incorrectly add volume/PnL
@@ -623,7 +624,8 @@ public class TradingBotBase : ITradingBot
{ {
lastCandle = Config.IsForBacktest lastCandle = Config.IsForBacktest
? LastCandle ? 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; 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}`"); $"📊 Position Status Change\nPosition: `{position.Ticker}`\nDirection: `{position.OriginDirection}`\nNew Status: `{positionStatus}`");
// Update Open trade status when position becomes Filled // Update Open trade status when position becomes Filled
if (positionStatus == PositionStatus.Filled && position.Open != null) if (positionStatus == PositionStatus.Filled)
{ {
position.Open.SetStatus(TradeStatus.Filled); position.Open.SetStatus(TradeStatus.Filled);
} }
@@ -1745,8 +1747,8 @@ public class TradingBotBase : ITradingBot
public decimal GetProfitAndLoss() public decimal GetProfitAndLoss()
{ {
// Calculate net PnL after deducting fees for each position // Calculate net PnL after deducting fees for each position
var netPnl = Positions.Values.Where(p => p.ProfitAndLoss != null) var netPnl = Positions.Values.Where(p => p.IsValidForMetrics() && p.ProfitAndLoss != null)
.Sum(p => p.GetNetPnL()); .Sum(p => p.GetPnLBeforeFees());
return netPnl; return netPnl;
} }
@@ -1760,7 +1762,7 @@ public class TradingBotBase : ITradingBot
{ {
decimal totalFees = 0; 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); totalFees += TradingHelpers.CalculatePositionFees(position);
} }

View File

@@ -10,7 +10,6 @@ using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Indicators; using Managing.Domain.Indicators;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -277,53 +276,6 @@ namespace Managing.Application.ManageBot
return await grain.ClosePositionAsync(positionId); return await grain.ClosePositionAsync(positionId);
} }
public async Task<bool> UpdateBotStatisticsAsync(Guid identifier)
{
try
{
var grain = _grainFactory.GetGrain<ILiveTradingBotGrain>(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<bool> SaveBotStatisticsAsync(Bot bot) public async Task<bool> SaveBotStatisticsAsync(Bot bot)
{ {
try try

View File

@@ -91,10 +91,10 @@ public class MessengerService : IMessengerService
private string BuildClosePositionMessage(Position position) private string BuildClosePositionMessage(Position position)
{ {
return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" + return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" +
$"Open Price : {position.Open.Price} \n" + $"Open Price : {position.Open.Price} \n" +
$"Closing Price : {position.Open.Price} \n" + $"Closing Price : {position.Open.Price} \n" +
$"Quantity :{position.Open.Quantity} \n" + $"Quantity :{position.Open.Quantity} \n" +
$"PNL : {position.ProfitAndLoss.Realized} $"; $"PNL : {position.ProfitAndLoss.Realized} $";
} }
public async Task SendMessage(string message, string telegramChannel) public async Task SendMessage(string message, string telegramChannel)
@@ -114,21 +114,23 @@ public class MessengerService : IMessengerService
if (position.Open != null) 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){ if (position.StopLoss != null)
message += $"\nStop Loss: {position.StopLoss.Quantity:F5} @ {position.StopLoss.Price:F5}"; {
message += $"\nStop Loss: {position.StopLoss.Price:F5}";
} }
if (position.TakeProfit1 != null){ if (position.TakeProfit1 != null)
message += $"\nTake Profit 1: {position.TakeProfit1.Quantity:F5} @ {position.TakeProfit1.Price:F5}"; {
message += $"\nTake Profit 1: {position.TakeProfit1.Price:F5}";
} }
if (position.ProfitAndLoss != null) if (position.ProfitAndLoss != null)
{ {
var pnlEmoji = position.ProfitAndLoss.Realized >= 0 ? "✅" : "❌"; var pnlEmoji = position.ProfitAndLoss.Net >= 0 ? "✅" : "❌";
message += $"\nPnL: {pnlEmoji} ${position.ProfitAndLoss.Realized:F5}"; message += $"\nPnL: {pnlEmoji} ${position.ProfitAndLoss.Net:F5}";
} }
return message; return message;

View File

@@ -454,11 +454,11 @@ public static class TradingBox
/// </summary> /// </summary>
/// <param name="positions">List of positions to analyze</param> /// <param name="positions">List of positions to analyze</param>
/// <returns>The total volume traded in decimal</returns> /// <returns>The total volume traded in decimal</returns>
public static decimal GetTotalVolumeTraded(Dictionary<Guid, Position> positions) public static decimal GetTotalVolumeTraded(List<Position> positions)
{ {
decimal totalVolume = 0; decimal totalVolume = 0;
foreach (var position in positions.Values) foreach (var position in positions)
{ {
// Add entry volume // Add entry volume
totalVolume += position.Open.Quantity * position.Open.Price; totalVolume += position.Open.Quantity * position.Open.Price;
@@ -530,12 +530,12 @@ public static class TradingBox
/// </summary> /// </summary>
/// <param name="positions">List of positions to analyze</param> /// <param name="positions">List of positions to analyze</param>
/// <returns>A tuple containing (wins, losses)</returns> /// <returns>A tuple containing (wins, losses)</returns>
public static (int Wins, int Losses) GetWinLossCount(Dictionary<Guid, Position> positions) public static (int Wins, int Losses) GetWinLossCount(List<Position> positions)
{ {
int wins = 0; int wins = 0;
int losses = 0; int losses = 0;
foreach (var position in positions.Values) foreach (var position in positions)
{ {
if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0) if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0)
{ {

View File

@@ -141,7 +141,7 @@ namespace Managing.Domain.Trades
/// Gets the net PnL after deducting fees /// Gets the net PnL after deducting fees
/// </summary> /// </summary>
/// <returns>The net PnL after fees</returns> /// <returns>The net PnL after fees</returns>
public decimal GetNetPnL() public decimal GetPnLBeforeFees()
{ {
if (ProfitAndLoss?.Realized == null) if (ProfitAndLoss?.Realized == null)
{ {
@@ -151,6 +151,16 @@ namespace Managing.Domain.Trades
return ProfitAndLoss.Realized; return ProfitAndLoss.Realized;
} }
public decimal GetNetPnl()
{
if (ProfitAndLoss?.Net == null)
{
return 0;
}
return ProfitAndLoss.Realized - UiFees - GasFees;
}
/// <summary> /// <summary>
/// Updates the UI fees for this position /// Updates the UI fees for this position
/// </summary> /// </summary>