diff --git a/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs b/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
index 5f8cdb7e..de39d0ae 100644
--- a/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
+++ b/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs
@@ -10,6 +10,7 @@ namespace Managing.Application.Abstractions.Grains
///
/// The ID of the user (used as grain key).
/// The display name of the agent.
+ [OneWay]
Task InitializeAsync(int userId, string agentName);
///
diff --git a/src/Managing.Application/Bots/Grains/AgentGrain.cs b/src/Managing.Application/Bots/Grains/AgentGrain.cs
index 32c78061..22c57512 100644
--- a/src/Managing.Application/Bots/Grains/AgentGrain.cs
+++ b/src/Managing.Application/Bots/Grains/AgentGrain.cs
@@ -5,9 +5,12 @@ using Managing.Application.Abstractions.Services;
using Managing.Application.Bots.Models;
using Managing.Application.Orleans;
using Managing.Common;
+using Managing.Core;
using Managing.Core.Exceptions;
using Managing.Domain.Statistics;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Orleans.Concurrency;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Grains;
@@ -27,6 +30,7 @@ public class AgentGrain : Grain, IAgentGrain
private readonly IUserService _userService;
private readonly IAccountService _accountService;
private readonly ITradingService _tradingService;
+ private readonly IServiceScopeFactory _scopeFactory;
public AgentGrain(
[PersistentState("agent-state", "agent-store")]
@@ -37,7 +41,8 @@ public class AgentGrain : Grain, IAgentGrain
IExchangeService exchangeService,
IUserService userService,
IAccountService accountService,
- ITradingService tradingService)
+ ITradingService tradingService,
+ IServiceScopeFactory scopeFactory)
{
_state = state;
_logger = logger;
@@ -47,6 +52,7 @@ public class AgentGrain : Grain, IAgentGrain
_userService = userService;
_accountService = accountService;
_tradingService = tradingService;
+ _scopeFactory = scopeFactory;
}
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}",
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();
}
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}",
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();
}
catch (Exception ex)
@@ -157,13 +136,6 @@ public class AgentGrain : Grain, IAgentGrain
_logger.LogInformation("Position updated event received for user {UserId}, position: {PositionId}",
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();
}
catch (Exception ex)
@@ -176,6 +148,7 @@ public class AgentGrain : Grain, IAgentGrain
///
/// Updates the agent summary by recalculating from position data (used for initialization or manual refresh)
///
+ [OneWay]
private async Task UpdateSummary()
{
try
@@ -185,35 +158,20 @@ public class AgentGrain : Grain, IAgentGrain
// Calculate aggregated statistics from position data
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());
// Calculate wins/losses from position PnL
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;
-
- if (totalVolume > 0)
+ var totalROI = totalVolume switch
{
- totalROI = (totalPnL / totalVolume) * 100;
- }
- else if (totalVolume == 0 && totalPnL == 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;
- }
+ > 0 => (totalPnL / totalVolumeWithoutLeverage) * 100,
+ >= 0 => 0,
+ _ => 0
+ };
// Calculate Runtime based on the earliest position date
DateTime? runtime = null;
@@ -236,9 +194,8 @@ public class AgentGrain : Grain, IAgentGrain
foreach (var account in userAccounts)
{
// Get USDC balance
- var usdcBalances = await _exchangeService.GetBalances(account);
- var usdcBalance = usdcBalances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC")?.Amount ??
- 0;
+ var usdcBalances = await GetOrRefreshBalanceDataAsync(account.Name);
+ var usdcBalance = usdcBalances?.UsdcValue ?? 0;
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)
- var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds);
+ var bots = await ServiceScopeHelpers.WithScopedService> (_scopeFactory, async (grainFactory) => {
+ var registry = grainFactory.GetGrain(0);
+ return await registry.GetBotsForUser((int)this.GetPrimaryKeyLong());
+ });
+
var activeStrategiesCount = bots.Count(b => b.Status == BotStatus.Running);
var summary = new AgentSummary
@@ -293,14 +254,8 @@ public class AgentGrain : Grain, IAgentGrain
{
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();
_logger.LogInformation("Bot {BotId} registered to Agent {UserId}", botId, this.GetPrimaryKeyLong());
-
- // Update summary after registering bot
await UpdateSummary();
}
}
@@ -309,14 +264,8 @@ public class AgentGrain : Grain, IAgentGrain
{
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();
_logger.LogInformation("Bot {BotId} unregistered from Agent {UserId}", botId, this.GetPrimaryKeyLong());
-
- // Update summary after unregistering bot
await UpdateSummary();
}
}
diff --git a/src/Managing.Application/Bots/Models/AgentGrainState.cs b/src/Managing.Application/Bots/Models/AgentGrainState.cs
index e44565af..568499f8 100644
--- a/src/Managing.Application/Bots/Models/AgentGrainState.cs
+++ b/src/Managing.Application/Bots/Models/AgentGrainState.cs
@@ -24,49 +24,6 @@ namespace Managing.Application.Bots.Models
///
[Id(4)]
public CachedBalanceData? CachedBalanceData { get; set; } = null;
-
- // Event-driven metrics for real-time updates
- ///
- /// Total PnL calculated from position events
- ///
- [Id(5)]
- public decimal TotalPnL { get; set; } = 0;
-
- ///
- /// Total volume calculated from position events
- ///
- [Id(6)]
- public decimal TotalVolume { get; set; } = 0;
-
- ///
- /// Total wins count from closed positions
- ///
- [Id(7)]
- public int Wins { get; set; } = 0;
-
- ///
- /// Total losses count from closed positions
- ///
- [Id(8)]
- public int Losses { get; set; } = 0;
-
- ///
- /// Total fees paid from position events
- ///
- [Id(9)]
- public decimal TotalFees { get; set; } = 0;
-
- ///
- /// Active strategies count (updated by bot registration/unregistration)
- ///
- [Id(10)]
- public int ActiveStrategiesCount { get; set; } = 0;
-
- ///
- /// Last time the summary was updated
- ///
- [Id(11)]
- public DateTime LastSummaryUpdate { get; set; } = DateTime.UtcNow;
}
///
diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs
index 4bf61b9b..e6279b47 100644
--- a/src/Managing.Application/Bots/TradingBotBase.cs
+++ b/src/Managing.Application/Bots/TradingBotBase.cs
@@ -388,7 +388,6 @@ public class TradingBotBase : ITradingBot
}
});
- NotificationEventType eventType = NotificationEventType.PositionUpdated;
if (!Config.IsForBacktest)
{
var brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker);
@@ -404,12 +403,13 @@ public class TradingBotBase : ITradingBot
internalPosition.Open.SetStatus(TradeStatus.Filled);
positionForSignal.Open.SetStatus(TradeStatus.Filled);
- eventType = NotificationEventType.PositionOpened;
await UpdatePositionDatabase(internalPosition);
if (previousPositionStatus != PositionStatus.Filled && internalPosition.Status == PositionStatus.Filled)
{
+ await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened, internalPosition);
+ }else{
await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionUpdated, internalPosition);
}
}
@@ -507,10 +507,6 @@ public class TradingBotBase : ITradingBot
}
await SetPositionStatus(signal.Identifier, PositionStatus.Filled);
-
- // Notify platform summary about the executed trade
- await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened,
- internalPosition);
}
else
{
@@ -890,10 +886,6 @@ public class TradingBotBase : ITradingBot
async messengerService => { await messengerService.SendPosition(position); });
}
- // Notify AgentGrain about position opening
- await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened,
- position);
-
Logger.LogInformation($"Position requested");
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
if (!Config.IsForBacktest)
{
- await ServiceScopeHelpers.WithScopedService(_scopeFactory,
- async tradingService => { await tradingService.UpdatePositionAsync(position); });
+ await UpdatePositionDatabase(position);
+ await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionClosed, position);
}
// 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)
{
- Positions[identifier].ProfitAndLoss = new ProfitAndLoss()
+ if (Positions[identifier].ProfitAndLoss == null)
{
- Realized = realized
- };
+ Positions[identifier].ProfitAndLoss = new ProfitAndLoss(){
+ Realized = realized
+ };
+ }else{
+ Positions[identifier].ProfitAndLoss.Realized = realized;
+ }
}
private void SetSignalStatus(string signalIdentifier, SignalStatus signalStatus)
@@ -2130,6 +2126,7 @@ public class TradingBotBase : ITradingBot
};
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}",
position.Identifier);