Fix agent grain calculation
This commit is contained in:
@@ -10,6 +10,7 @@ namespace Managing.Application.Abstractions.Grains
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">The ID of the user (used as grain key).</param>
|
/// <param name="userId">The ID of the user (used as grain key).</param>
|
||||||
/// <param name="agentName">The display name of the agent.</param>
|
/// <param name="agentName">The display name of the agent.</param>
|
||||||
|
[OneWay]
|
||||||
Task InitializeAsync(int userId, string agentName);
|
Task InitializeAsync(int userId, string agentName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ using Managing.Application.Abstractions.Services;
|
|||||||
using Managing.Application.Bots.Models;
|
using Managing.Application.Bots.Models;
|
||||||
using Managing.Application.Orleans;
|
using Managing.Application.Orleans;
|
||||||
using Managing.Common;
|
using Managing.Common;
|
||||||
|
using Managing.Core;
|
||||||
using Managing.Core.Exceptions;
|
using Managing.Core.Exceptions;
|
||||||
using Managing.Domain.Statistics;
|
using Managing.Domain.Statistics;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Orleans.Concurrency;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.Bots.Grains;
|
namespace Managing.Application.Bots.Grains;
|
||||||
@@ -27,6 +30,7 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly ITradingService _tradingService;
|
private readonly ITradingService _tradingService;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
public AgentGrain(
|
public AgentGrain(
|
||||||
[PersistentState("agent-state", "agent-store")]
|
[PersistentState("agent-state", "agent-store")]
|
||||||
@@ -37,7 +41,8 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
IExchangeService exchangeService,
|
IExchangeService exchangeService,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
ITradingService tradingService)
|
ITradingService tradingService,
|
||||||
|
IServiceScopeFactory scopeFactory)
|
||||||
{
|
{
|
||||||
_state = state;
|
_state = state;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -47,6 +52,7 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
_userService = userService;
|
_userService = userService;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_tradingService = tradingService;
|
_tradingService = tradingService;
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task OnActivateAsync(CancellationToken cancellationToken)
|
public override Task OnActivateAsync(CancellationToken cancellationToken)
|
||||||
@@ -98,14 +104,6 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
_logger.LogInformation("Position opened event received for user {UserId}, position: {PositionId}",
|
_logger.LogInformation("Position opened event received for user {UserId}, position: {PositionId}",
|
||||||
this.GetPrimaryKeyLong(), evt.PositionIdentifier);
|
this.GetPrimaryKeyLong(), evt.PositionIdentifier);
|
||||||
|
|
||||||
// Update event-driven metrics
|
|
||||||
_state.State.TotalVolume += evt.Volume;
|
|
||||||
_state.State.TotalFees += evt.Fee;
|
|
||||||
_state.State.LastSummaryUpdate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await _state.WriteStateAsync();
|
|
||||||
|
|
||||||
// Update the agent summary with the new data
|
|
||||||
await UpdateSummary();
|
await UpdateSummary();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -122,25 +120,6 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
_logger.LogInformation("Position closed event received for user {UserId}, position: {PositionId}, PnL: {PnL}",
|
_logger.LogInformation("Position closed event received for user {UserId}, position: {PositionId}, PnL: {PnL}",
|
||||||
this.GetPrimaryKeyLong(), evt.PositionIdentifier, evt.RealizedPnL);
|
this.GetPrimaryKeyLong(), evt.PositionIdentifier, evt.RealizedPnL);
|
||||||
|
|
||||||
// Update event-driven metrics
|
|
||||||
_state.State.TotalPnL += evt.RealizedPnL;
|
|
||||||
_state.State.TotalVolume += evt.Volume;
|
|
||||||
|
|
||||||
// Update wins/losses count
|
|
||||||
if (evt.RealizedPnL > 0)
|
|
||||||
{
|
|
||||||
_state.State.Wins++;
|
|
||||||
}
|
|
||||||
else if (evt.RealizedPnL < 0)
|
|
||||||
{
|
|
||||||
_state.State.Losses++;
|
|
||||||
}
|
|
||||||
|
|
||||||
_state.State.LastSummaryUpdate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await _state.WriteStateAsync();
|
|
||||||
|
|
||||||
// Update the agent summary with the new data
|
|
||||||
await UpdateSummary();
|
await UpdateSummary();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -157,13 +136,6 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
_logger.LogInformation("Position updated event received for user {UserId}, position: {PositionId}",
|
_logger.LogInformation("Position updated event received for user {UserId}, position: {PositionId}",
|
||||||
this.GetPrimaryKeyLong(), evt.PositionIdentifier);
|
this.GetPrimaryKeyLong(), evt.PositionIdentifier);
|
||||||
|
|
||||||
// Update event-driven metrics for PnL changes
|
|
||||||
// Note: This is for real-time PnL updates, not cumulative like closed positions
|
|
||||||
_state.State.LastSummaryUpdate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await _state.WriteStateAsync();
|
|
||||||
|
|
||||||
// Update the agent summary with the new data
|
|
||||||
await UpdateSummary();
|
await UpdateSummary();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -176,6 +148,7 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the agent summary by recalculating from position data (used for initialization or manual refresh)
|
/// Updates the agent summary by recalculating from position data (used for initialization or manual refresh)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[OneWay]
|
||||||
private async Task UpdateSummary()
|
private async Task UpdateSummary()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -185,35 +158,20 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
|
|
||||||
// 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);
|
||||||
var totalVolume = positions.Sum(p => p.Open.Price * p.Open.Quantity);
|
var totalVolume = positions.Sum(p => p.Open.Price * p.Open.Quantity * p.Open.Leverage);
|
||||||
|
var totalVolumeWithoutLeverage = positions.Sum(p => p.Open.Price * p.Open.Quantity);
|
||||||
var totalFees = positions.Sum(p => p.CalculateTotalFees());
|
var totalFees = positions.Sum(p => p.CalculateTotalFees());
|
||||||
|
|
||||||
// Calculate wins/losses from position PnL
|
// Calculate wins/losses from position PnL
|
||||||
var totalWins = positions.Count(p => (p.ProfitAndLoss?.Realized ?? 0) > 0);
|
var totalWins = positions.Count(p => (p.ProfitAndLoss?.Realized ?? 0) > 0);
|
||||||
var totalLosses = positions.Count(p => (p.ProfitAndLoss?.Realized ?? 0) < 0);
|
var totalLosses = positions.Count(p => (p.ProfitAndLoss?.Realized ?? 0) <= 0);
|
||||||
|
|
||||||
decimal totalROI;
|
var totalROI = totalVolume switch
|
||||||
|
|
||||||
if (totalVolume > 0)
|
|
||||||
{
|
{
|
||||||
totalROI = (totalPnL / totalVolume) * 100;
|
> 0 => (totalPnL / totalVolumeWithoutLeverage) * 100,
|
||||||
}
|
>= 0 => 0,
|
||||||
else if (totalVolume == 0 && totalPnL == 0)
|
_ => 0
|
||||||
{
|
};
|
||||||
// No trading activity yet
|
|
||||||
totalROI = 0;
|
|
||||||
}
|
|
||||||
else if (totalVolume == 0 && totalPnL != 0)
|
|
||||||
{
|
|
||||||
// Edge case: PnL exists but no volume (shouldn't happen in normal cases)
|
|
||||||
_logger.LogWarning("Agent {UserId} has PnL {PnL} but zero volume", this.GetPrimaryKeyLong(), totalPnL);
|
|
||||||
totalROI = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Fallback for any other edge cases
|
|
||||||
totalROI = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate Runtime based on the earliest position date
|
// Calculate Runtime based on the earliest position date
|
||||||
DateTime? runtime = null;
|
DateTime? runtime = null;
|
||||||
@@ -236,9 +194,8 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
foreach (var account in userAccounts)
|
foreach (var account in userAccounts)
|
||||||
{
|
{
|
||||||
// Get USDC balance
|
// Get USDC balance
|
||||||
var usdcBalances = await _exchangeService.GetBalances(account);
|
var usdcBalances = await GetOrRefreshBalanceDataAsync(account.Name);
|
||||||
var usdcBalance = usdcBalances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC")?.Amount ??
|
var usdcBalance = usdcBalances?.UsdcValue ?? 0;
|
||||||
0;
|
|
||||||
totalBalance += usdcBalance;
|
totalBalance += usdcBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +217,11 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get active strategies count from bot data (this is still needed for running bots)
|
// Get active strategies count from bot data (this is still needed for running bots)
|
||||||
var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds);
|
var bots = await ServiceScopeHelpers.WithScopedService<IGrainFactory, List<BotRegistryEntry>> (_scopeFactory, async (grainFactory) => {
|
||||||
|
var registry = grainFactory.GetGrain<ILiveBotRegistryGrain>(0);
|
||||||
|
return await registry.GetBotsForUser((int)this.GetPrimaryKeyLong());
|
||||||
|
});
|
||||||
|
|
||||||
var activeStrategiesCount = bots.Count(b => b.Status == BotStatus.Running);
|
var activeStrategiesCount = bots.Count(b => b.Status == BotStatus.Running);
|
||||||
|
|
||||||
var summary = new AgentSummary
|
var summary = new AgentSummary
|
||||||
@@ -293,14 +254,8 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
{
|
{
|
||||||
if (_state.State.BotIds.Add(botId))
|
if (_state.State.BotIds.Add(botId))
|
||||||
{
|
{
|
||||||
// Update active strategies count
|
|
||||||
_state.State.ActiveStrategiesCount = _state.State.BotIds.Count;
|
|
||||||
_state.State.LastSummaryUpdate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await _state.WriteStateAsync();
|
await _state.WriteStateAsync();
|
||||||
_logger.LogInformation("Bot {BotId} registered to Agent {UserId}", botId, this.GetPrimaryKeyLong());
|
_logger.LogInformation("Bot {BotId} registered to Agent {UserId}", botId, this.GetPrimaryKeyLong());
|
||||||
|
|
||||||
// Update summary after registering bot
|
|
||||||
await UpdateSummary();
|
await UpdateSummary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,14 +264,8 @@ public class AgentGrain : Grain, IAgentGrain
|
|||||||
{
|
{
|
||||||
if (_state.State.BotIds.Remove(botId))
|
if (_state.State.BotIds.Remove(botId))
|
||||||
{
|
{
|
||||||
// Update active strategies count
|
|
||||||
_state.State.ActiveStrategiesCount = _state.State.BotIds.Count;
|
|
||||||
_state.State.LastSummaryUpdate = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await _state.WriteStateAsync();
|
await _state.WriteStateAsync();
|
||||||
_logger.LogInformation("Bot {BotId} unregistered from Agent {UserId}", botId, this.GetPrimaryKeyLong());
|
_logger.LogInformation("Bot {BotId} unregistered from Agent {UserId}", botId, this.GetPrimaryKeyLong());
|
||||||
|
|
||||||
// Update summary after unregistering bot
|
|
||||||
await UpdateSummary();
|
await UpdateSummary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,49 +24,6 @@ namespace Managing.Application.Bots.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Id(4)]
|
[Id(4)]
|
||||||
public CachedBalanceData? CachedBalanceData { get; set; } = null;
|
public CachedBalanceData? CachedBalanceData { get; set; } = null;
|
||||||
|
|
||||||
// Event-driven metrics for real-time updates
|
|
||||||
/// <summary>
|
|
||||||
/// Total PnL calculated from position events
|
|
||||||
/// </summary>
|
|
||||||
[Id(5)]
|
|
||||||
public decimal TotalPnL { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Total volume calculated from position events
|
|
||||||
/// </summary>
|
|
||||||
[Id(6)]
|
|
||||||
public decimal TotalVolume { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Total wins count from closed positions
|
|
||||||
/// </summary>
|
|
||||||
[Id(7)]
|
|
||||||
public int Wins { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Total losses count from closed positions
|
|
||||||
/// </summary>
|
|
||||||
[Id(8)]
|
|
||||||
public int Losses { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Total fees paid from position events
|
|
||||||
/// </summary>
|
|
||||||
[Id(9)]
|
|
||||||
public decimal TotalFees { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Active strategies count (updated by bot registration/unregistration)
|
|
||||||
/// </summary>
|
|
||||||
[Id(10)]
|
|
||||||
public int ActiveStrategiesCount { get; set; } = 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Last time the summary was updated
|
|
||||||
/// </summary>
|
|
||||||
[Id(11)]
|
|
||||||
public DateTime LastSummaryUpdate { get; set; } = DateTime.UtcNow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -388,7 +388,6 @@ public class TradingBotBase : ITradingBot
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
NotificationEventType eventType = NotificationEventType.PositionUpdated;
|
|
||||||
if (!Config.IsForBacktest)
|
if (!Config.IsForBacktest)
|
||||||
{
|
{
|
||||||
var brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker);
|
var brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker);
|
||||||
@@ -404,12 +403,13 @@ public class TradingBotBase : ITradingBot
|
|||||||
|
|
||||||
internalPosition.Open.SetStatus(TradeStatus.Filled);
|
internalPosition.Open.SetStatus(TradeStatus.Filled);
|
||||||
positionForSignal.Open.SetStatus(TradeStatus.Filled);
|
positionForSignal.Open.SetStatus(TradeStatus.Filled);
|
||||||
eventType = NotificationEventType.PositionOpened;
|
|
||||||
|
|
||||||
await UpdatePositionDatabase(internalPosition);
|
await UpdatePositionDatabase(internalPosition);
|
||||||
|
|
||||||
if (previousPositionStatus != PositionStatus.Filled && internalPosition.Status == PositionStatus.Filled)
|
if (previousPositionStatus != PositionStatus.Filled && internalPosition.Status == PositionStatus.Filled)
|
||||||
{
|
{
|
||||||
|
await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened, internalPosition);
|
||||||
|
}else{
|
||||||
await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionUpdated, internalPosition);
|
await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionUpdated, internalPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,10 +507,6 @@ public class TradingBotBase : ITradingBot
|
|||||||
}
|
}
|
||||||
|
|
||||||
await SetPositionStatus(signal.Identifier, PositionStatus.Filled);
|
await SetPositionStatus(signal.Identifier, PositionStatus.Filled);
|
||||||
|
|
||||||
// Notify platform summary about the executed trade
|
|
||||||
await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened,
|
|
||||||
internalPosition);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -890,10 +886,6 @@ public class TradingBotBase : ITradingBot
|
|||||||
async messengerService => { await messengerService.SendPosition(position); });
|
async messengerService => { await messengerService.SendPosition(position); });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify AgentGrain about position opening
|
|
||||||
await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened,
|
|
||||||
position);
|
|
||||||
|
|
||||||
Logger.LogInformation($"Position requested");
|
Logger.LogInformation($"Position requested");
|
||||||
return position; // Return the created position without adding to list
|
return position; // Return the created position without adding to list
|
||||||
}
|
}
|
||||||
@@ -1355,8 +1347,8 @@ public class TradingBotBase : ITradingBot
|
|||||||
// Update position in database with all trade changes
|
// Update position in database with all trade changes
|
||||||
if (!Config.IsForBacktest)
|
if (!Config.IsForBacktest)
|
||||||
{
|
{
|
||||||
await ServiceScopeHelpers.WithScopedService<ITradingService>(_scopeFactory,
|
await UpdatePositionDatabase(position);
|
||||||
async tradingService => { await tradingService.UpdatePositionAsync(position); });
|
await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionClosed, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the last position closing time for cooldown period tracking
|
// Update the last position closing time for cooldown period tracking
|
||||||
@@ -1474,10 +1466,14 @@ public class TradingBotBase : ITradingBot
|
|||||||
|
|
||||||
private void UpdatePositionPnl(Guid identifier, decimal realized)
|
private void UpdatePositionPnl(Guid identifier, decimal realized)
|
||||||
{
|
{
|
||||||
Positions[identifier].ProfitAndLoss = new ProfitAndLoss()
|
if (Positions[identifier].ProfitAndLoss == null)
|
||||||
{
|
{
|
||||||
|
Positions[identifier].ProfitAndLoss = new ProfitAndLoss(){
|
||||||
Realized = realized
|
Realized = realized
|
||||||
};
|
};
|
||||||
|
}else{
|
||||||
|
Positions[identifier].ProfitAndLoss.Realized = realized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetSignalStatus(string signalIdentifier, SignalStatus signalStatus)
|
private void SetSignalStatus(string signalIdentifier, SignalStatus signalStatus)
|
||||||
@@ -2130,6 +2126,7 @@ public class TradingBotBase : ITradingBot
|
|||||||
};
|
};
|
||||||
|
|
||||||
await agentGrain.OnPositionUpdatedAsync(positionUpdatedEvent);
|
await agentGrain.OnPositionUpdatedAsync(positionUpdatedEvent);
|
||||||
|
// No need to notify platform grain, it will be notified when position is closed or opened only
|
||||||
|
|
||||||
Logger.LogDebug("Sent position updated event to both grains for position {PositionId}",
|
Logger.LogDebug("Sent position updated event to both grains for position {PositionId}",
|
||||||
position.Identifier);
|
position.Identifier);
|
||||||
|
|||||||
Reference in New Issue
Block a user