Fix status and filtered positions for metrics
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user