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);