From d432549d26445684053676ba0a581def82b645df Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sat, 27 Sep 2025 22:20:12 +0700 Subject: [PATCH] Clean and update event --- .../Grains/IPlatformSummaryGrain.cs | 31 +--- .../Models/AgentSummaryUpdateEvent.cs | 2 +- .../Services/ITradingService.cs | 1 - .../AgentGrainTests.cs | 4 +- .../Abstractions/Grains/IAgentGrain.cs | 29 +-- .../Bots/Grains/AgentGrain.cs | 128 +++++++++++--- .../Bots/Grains/LiveBotRegistryGrain.cs | 10 +- .../Bots/Models/AgentGrainState.cs | 43 +++++ .../Bots/TradingBotBase.cs | 167 ++++++------------ .../Grains/PlatformSummaryGrain.cs | 39 +--- .../Trading/TradingService.cs | 28 +-- src/Managing.Common/Enums.cs | 10 +- src/Managing.Domain/Trades/Position.cs | 2 +- 13 files changed, 255 insertions(+), 239 deletions(-) diff --git a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs index f724b459..38d828a6 100644 --- a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs +++ b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs @@ -19,31 +19,6 @@ public interface IPlatformSummaryGrain : IGrainWithStringKey /// Task RefreshDataAsync(); - /// - /// Gets the total volume traded across all strategies - /// - Task GetTotalVolumeAsync(); - - /// - /// Gets the total PnL across all strategies - /// - Task GetTotalPnLAsync(); - - /// - /// Gets the total open interest across all positions - /// - Task GetTotalOpenInterest(); - - /// - /// Gets the total number of open positions - /// - Task GetTotalPositionCountAsync(); - - /// - /// Gets the total platform fees - /// - Task GetTotalFeesAsync(); - // Event handlers for immediate updates /// /// Updates the active strategy count @@ -96,4 +71,10 @@ public class PositionOpenEvent : PlatformMetricsEvent [Id(4)] public TradeDirection Direction { get; set; } [Id(5)] public Guid PositionIdentifier { get; set; } +} + +[GenerateSerializer] +public class PositionUpdatedEvent : PlatformMetricsEvent +{ + [Id(1)] public Guid PositionIdentifier { get; set; } } \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs b/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs index 1a88b1a9..74807e7e 100644 --- a/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs +++ b/src/Managing.Application.Abstractions/Models/AgentSummaryUpdateEvent.cs @@ -11,7 +11,7 @@ public class AgentSummaryUpdateEvent { [Id(0)] public Guid BotId { get; set; } - [Id(1)] public AgentSummaryEventType EventType { get; set; } + [Id(1)] public NotificationEventType EventType { get; set; } [Id(2)] public DateTime Timestamp { get; set; } = DateTime.UtcNow; diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs index 2deba19c..cc7d8c30 100644 --- a/src/Managing.Application.Abstractions/Services/ITradingService.cs +++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs @@ -19,7 +19,6 @@ public interface ITradingService Task GetScenarioByNameAsync(string scenario); Task InsertPositionAsync(Position position); Task UpdatePositionAsync(Position position); - Task UpdateTradeAsync(Trade trade); Task GetIndicatorByNameAsync(string strategy); Task InsertScenarioAsync(Scenario scenario); Task InsertIndicatorAsync(IndicatorBase indicatorBase); diff --git a/src/Managing.Application.Tests/AgentGrainTests.cs b/src/Managing.Application.Tests/AgentGrainTests.cs index 5f59cfee..70790e37 100644 --- a/src/Managing.Application.Tests/AgentGrainTests.cs +++ b/src/Managing.Application.Tests/AgentGrainTests.cs @@ -51,7 +51,7 @@ public class AgentGrainTests var updateEvent = new AgentSummaryUpdateEvent { BotId = botId, - EventType = AgentSummaryEventType.PositionOpened, + EventType = NotificationEventType.PositionOpened, Timestamp = DateTime.UtcNow }; @@ -76,7 +76,7 @@ public class AgentGrainTests var updateEvent = new AgentSummaryUpdateEvent { BotId = Guid.NewGuid(), // Different bot ID - EventType = AgentSummaryEventType.PositionOpened, + EventType = NotificationEventType.PositionOpened, Timestamp = DateTime.UtcNow }; diff --git a/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs b/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs index d27b5e7f..5f8cdb7e 100644 --- a/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs +++ b/src/Managing.Application/Abstractions/Grains/IAgentGrain.cs @@ -1,4 +1,3 @@ -using Managing.Application.Abstractions.Models; using Managing.Application.Bots.Models; using Orleans.Concurrency; @@ -20,28 +19,38 @@ namespace Managing.Application.Abstractions.Grains [OneWay] Task UpdateAgentNameAsync(string agentName); - /// - /// Generates a summary of the agent's stats for the AgentRegistryGrain. - /// - [OneWay] - Task UpdateSummary(); - /// /// Registers a new bot with this agent. /// + [OneWay] Task RegisterBotAsync(Guid botId); /// /// Unregisters a bot from this agent. /// + [OneWay] Task UnregisterBotAsync(Guid botId); /// - /// Handles stream notifications for agent summary updates. + /// Handles position opened events for real-time agent summary updates. /// - /// The update event from the stream] + /// The position opened event [OneWay] - Task OnAgentSummaryUpdateAsync(AgentSummaryUpdateEvent updateEvent); + Task OnPositionOpenedAsync(PositionOpenEvent evt); + + /// + /// Handles position closed events for real-time agent summary updates. + /// + /// The position closed event + [OneWay] + Task OnPositionClosedAsync(PositionClosedEvent evt); + + /// + /// Handles position update events for real-time PnL and status updates. + /// + /// The position update event + [OneWay] + Task OnPositionUpdatedAsync(PositionUpdatedEvent evt); /// /// Coordinates ETH balance checking and swapping for all bots under this agent. diff --git a/src/Managing.Application/Bots/Grains/AgentGrain.cs b/src/Managing.Application/Bots/Grains/AgentGrain.cs index b0ac92d6..32c78061 100644 --- a/src/Managing.Application/Bots/Grains/AgentGrain.cs +++ b/src/Managing.Application/Bots/Grains/AgentGrain.cs @@ -1,6 +1,6 @@ +#nullable enable using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; -using Managing.Application.Abstractions.Models; using Managing.Application.Abstractions.Services; using Managing.Application.Bots.Models; using Managing.Application.Orleans; @@ -91,40 +91,107 @@ public class AgentGrain : Grain, IAgentGrain _logger.LogInformation("Agent {UserId} updated with name {AgentName}", this.GetPrimaryKeyLong(), agentName); } - public async Task OnAgentSummaryUpdateAsync(AgentSummaryUpdateEvent updateEvent) + public async Task OnPositionOpenedAsync(PositionOpenEvent evt) { try { - _logger.LogInformation("Received agent summary update event for user {UserId}, event type: {EventType}", - this.GetPrimaryKeyLong(), updateEvent.EventType); + _logger.LogInformation("Position opened event received for user {UserId}, position: {PositionId}", + this.GetPrimaryKeyLong(), evt.PositionIdentifier); - // Only update summary if the event is for this agent's bots - if (_state.State.BotIds.Contains(updateEvent.BotId)) - { - await UpdateSummary(); - } + // 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) { - _logger.LogError(ex, "Error processing agent summary update event for user {UserId}", + _logger.LogError(ex, "Error processing position opened event for user {UserId}", this.GetPrimaryKeyLong()); } } - public async Task UpdateSummary() + public async Task OnPositionClosedAsync(PositionClosedEvent evt) { try { - // Get all bots for this agent - var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds); + _logger.LogInformation("Position closed event received for user {UserId}, position: {PositionId}, PnL: {PnL}", + this.GetPrimaryKeyLong(), evt.PositionIdentifier, evt.RealizedPnL); - // Calculate aggregated statistics from bot data - var totalPnL = bots.Sum(b => b.Pnl); - var totalWins = bots.Sum(b => b.TradeWins); - var totalLosses = bots.Sum(b => b.TradeLosses); + // 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) + { + _logger.LogError(ex, "Error processing position closed event for user {UserId}", + this.GetPrimaryKeyLong()); + } + } + + public async Task OnPositionUpdatedAsync(PositionUpdatedEvent evt) + { + try + { + _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) + { + _logger.LogError(ex, "Error processing position updated event for user {UserId}", + this.GetPrimaryKeyLong()); + } + } + + /// + /// Updates the agent summary by recalculating from position data (used for initialization or manual refresh) + /// + private async Task UpdateSummary() + { + try + { + // Get all positions for this agent's bots as initiator + var positions = await _tradingService.GetPositionsByInitiatorIdentifiersAsync(_state.State.BotIds); + + // 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 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); - // Calculate ROI based on total volume traded with proper division by zero handling - var totalVolume = bots.Sum(b => b.Volume); decimal totalROI; if (totalVolume > 0) @@ -148,11 +215,11 @@ public class AgentGrain : Grain, IAgentGrain totalROI = 0; } - // Calculate Runtime based on the farthest date from bot startup times + // Calculate Runtime based on the earliest position date DateTime? runtime = null; - if (bots.Any()) + if (positions.Any()) { - runtime = bots.Max(b => b.StartupTime); + runtime = positions.Min(p => p.Date); } // Calculate total balance (USDC + open positions value) @@ -192,6 +259,10 @@ public class AgentGrain : Grain, IAgentGrain totalBalance = 0; // Set to 0 if calculation fails } + // Get active strategies count from bot data (this is still needed for running bots) + var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds); + var activeStrategiesCount = bots.Count(b => b.Status == BotStatus.Running); + var summary = new AgentSummary { UserId = (int)this.GetPrimaryKeyLong(), @@ -201,13 +272,16 @@ public class AgentGrain : Grain, IAgentGrain Losses = totalLosses, TotalROI = totalROI, Runtime = runtime, - ActiveStrategiesCount = bots.Count(b => b.Status == BotStatus.Running), + ActiveStrategiesCount = activeStrategiesCount, TotalVolume = totalVolume, TotalBalance = totalBalance, }; // Save summary to database await _agentService.SaveOrUpdateAgentSummary(summary); + + _logger.LogDebug("Updated agent summary from position data for user {UserId}: PnL={PnL}, Volume={Volume}, Wins={Wins}, Losses={Losses}", + this.GetPrimaryKeyLong(), totalPnL, totalVolume, totalWins, totalLosses); } catch (Exception ex) { @@ -219,6 +293,10 @@ 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()); @@ -231,6 +309,10 @@ 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()); diff --git a/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs b/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs index d00c3dd5..56b558aa 100644 --- a/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs @@ -1,4 +1,3 @@ -using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Orleans; using Microsoft.Extensions.Logging; @@ -17,17 +16,14 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain { private readonly IPersistentState _state; private readonly ILogger _logger; - private readonly IBotService _botService; public LiveBotRegistryGrain( [PersistentState("bot-registry", "registry-store")] IPersistentState state, - ILogger logger, - IBotService botService) + ILogger logger) { _state = state; _logger = logger; - _botService = botService; } public override async Task OnActivateAsync(CancellationToken cancellationToken) @@ -196,8 +192,8 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain { var platformGrain = GrainFactory.GetGrain("platform-summary"); await platformGrain.UpdateActiveStrategyCountAsync(_state.State.ActiveBotsCount); - - _logger.LogDebug("Notified platform summary about active strategy count change. New count: {ActiveCount}", + + _logger.LogDebug("Notified platform summary about active strategy count change. New count: {ActiveCount}", _state.State.ActiveBotsCount); } catch (Exception ex) diff --git a/src/Managing.Application/Bots/Models/AgentGrainState.cs b/src/Managing.Application/Bots/Models/AgentGrainState.cs index 568499f8..e44565af 100644 --- a/src/Managing.Application/Bots/Models/AgentGrainState.cs +++ b/src/Managing.Application/Bots/Models/AgentGrainState.cs @@ -24,6 +24,49 @@ 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 754d9f92..4bf61b9b 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -1,6 +1,5 @@ using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; -using Managing.Application.Abstractions.Models; using Managing.Application.Abstractions.Services; using Managing.Application.Trading.Commands; using Managing.Application.Trading.Handlers; @@ -95,10 +94,6 @@ public class TradingBotBase : ITradingBot $"📢 I'll notify you when signals are triggered."; await LogInformation(startupMessage); - - // Notify AgentGrain about bot startup - await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.BotStarted, - $"Bot: {Config.Name}, Ticker: {Config.Ticker}"); break; case BotStatus.Running: @@ -393,36 +388,29 @@ public class TradingBotBase : ITradingBot } }); + NotificationEventType eventType = NotificationEventType.PositionUpdated; if (!Config.IsForBacktest) { var brokerPosition = brokerPositions.FirstOrDefault(p => p.Ticker == Config.Ticker); if (brokerPosition != null) { - // Calculate net PnL after fees for broker position + var previousPositionStatus = internalPosition.Status; + // Position found on the broker, means the position is filled var brokerNetPnL = brokerPosition.GetNetPnL(); UpdatePositionPnl(positionForSignal.Identifier, brokerNetPnL); internalPosition.ProfitAndLoss = new ProfitAndLoss { Realized = brokerNetPnL }; internalPosition.Status = PositionStatus.Filled; + await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Filled); - // Update Open trade status when position is found on broker - if (internalPosition.Open != null) + internalPosition.Open.SetStatus(TradeStatus.Filled); + positionForSignal.Open.SetStatus(TradeStatus.Filled); + eventType = NotificationEventType.PositionOpened; + + await UpdatePositionDatabase(internalPosition); + + if (previousPositionStatus != PositionStatus.Filled && internalPosition.Status == PositionStatus.Filled) { - internalPosition.Open.SetStatus(TradeStatus.Filled); - } - - // Also update the position in the bot's positions dictionary - if (positionForSignal.Open != null) - { - positionForSignal.Open.SetStatus(TradeStatus.Filled); - } - - if (internalPosition.Status.Equals(PositionStatus.New)) - { - await SetPositionStatus(internalPosition.SignalIdentifier, PositionStatus.Filled); - - // Notify platform summary about the executed trade - await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionOpened, - $"Position found on broker: {internalPosition.Identifier}", internalPosition); + await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionUpdated, internalPosition); } } else @@ -431,18 +419,6 @@ public class TradingBotBase : ITradingBot if (internalPosition.Status.Equals(PositionStatus.Filled)) { internalPosition.Status = PositionStatus.Finished; - - // Update Open trade status when position becomes Finished - if (internalPosition.Open != null) - { - internalPosition.Open.SetStatus(TradeStatus.Filled); - } - - // Also update the position in the bot's positions dictionary - if (positionForSignal.Open != null) - { - positionForSignal.Open.SetStatus(TradeStatus.Filled); - } } } } @@ -496,6 +472,7 @@ public class TradingBotBase : ITradingBot } else if (ordersCount == 2) { + // TODO: This should never happen, but just in case // Check if position is already open on broker with 2 orders await LogInformation( $"🔍 **Checking Broker Position**\nPosition has exactly `{orders.Count()}` open orders\nChecking if position is already open on broker..."); @@ -532,8 +509,7 @@ public class TradingBotBase : ITradingBot await SetPositionStatus(signal.Identifier, PositionStatus.Filled); // Notify platform summary about the executed trade - await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionOpened, - $"Position found on broker with 2 orders: {internalPosition.Identifier}", + await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened, internalPosition); } else @@ -560,8 +536,7 @@ public class TradingBotBase : ITradingBot { await HandleClosedPosition(positionForSignal); } - else if (internalPosition.Status == PositionStatus.Filled || - internalPosition.Status == PositionStatus.PartiallyFilled) + else if (internalPosition.Status == PositionStatus.Filled) { Candle lastCandle = null; await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService => @@ -600,6 +575,7 @@ public class TradingBotBase : ITradingBot } } + // For backtest and to make sure position is closed based on SL and TP if (positionForSignal.OriginDirection == TradeDirection.Long) { if (positionForSignal.StopLoss.Price >= lastCandle.Low) @@ -779,36 +755,7 @@ public class TradingBotBase : ITradingBot } } - if (!Config.IsForBacktest) - { - // Update position in database with broker data - await ServiceScopeHelpers.WithScopedService(_scopeFactory, async tradingService => - { - // Update the internal position with broker data - internalPosition.Status = PositionStatus.Filled; - // Apply fees to the internal position PnL before saving - if (internalPosition.ProfitAndLoss != null) - { - var totalFees = internalPosition.CalculateTotalFees(); - internalPosition.ProfitAndLoss.Realized = internalPosition.ProfitAndLoss.Realized - totalFees; - } - - // Update Open trade status when position is updated to Filled - if (internalPosition.Open != null) - { - internalPosition.Open.SetStatus(TradeStatus.Filled); - } - - // Also update the position in the bot's positions dictionary - if (positionForSignal.Open != null) - { - positionForSignal.Open.SetStatus(TradeStatus.Filled); - } - - // Save updated position to database - await tradingService.UpdatePositionAsync(internalPosition); - }); - } + } catch (Exception ex) { @@ -818,6 +765,14 @@ public class TradingBotBase : ITradingBot } } + private async Task UpdatePositionDatabase(Position position) + { + await ServiceScopeHelpers.WithScopedService(_scopeFactory, async tradingService => + { + await tradingService.UpdatePositionAsync(position); + }); + } + private async Task OpenPosition(LightSignal signal) { Logger.LogInformation($"Opening position for {signal.Identifier}"); @@ -936,8 +891,8 @@ public class TradingBotBase : ITradingBot } // Notify AgentGrain about position opening - await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionOpened, - $"Signal: {signal.Identifier}", position); + await NotifyAgentAndPlatformGrainAsync(NotificationEventType.PositionOpened, + position); Logger.LogInformation($"Position requested"); return position; // Return the created position without adding to list @@ -1418,13 +1373,6 @@ public class TradingBotBase : ITradingBot string.Format("💰 **Balance Updated**\nNew bot trading balance: `${0:F2}`", Config.BotTradingBalance)); } - - // Notify AgentGrain about position closing - var pnlInfo = position.ProfitAndLoss?.Realized != null - ? string.Format("PnL: {0:F2}", position.ProfitAndLoss.Realized) - : "PnL: Unknown"; - await NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType.PositionClosed, - string.Format("Signal: {0}, {1}", position.SignalIdentifier, pnlInfo), position); } else { @@ -1555,7 +1503,7 @@ public class TradingBotBase : ITradingBot public decimal GetProfitAndLoss() { // Calculate net PnL after deducting fees for each position - var netPnl = Positions.Values.Where(p => p.ProfitAndLoss != null && p.IsFinished()) + var netPnl = Positions.Values.Where(p => p.ProfitAndLoss != null) .Sum(p => p.GetNetPnL()); return netPnl; } @@ -2120,13 +2068,12 @@ public class TradingBotBase : ITradingBot } /// - /// Notifies both AgentGrain and PlatformSummaryGrain about bot events + /// Notifies both AgentGrain and PlatformSummaryGrain about bot events using unified event data /// - /// The type of event (e.g., PositionOpened, PositionClosed) - /// Optional additional context data + /// The type of event (e.g., PositionOpened, PositionClosed, PositionUpdated) /// Optional position data for platform summary events - private async Task NotifyAgentAndPlatformGrainAsync(AgentSummaryEventType eventType, string additionalData = null, - Position position = null) + private async Task NotifyAgentAndPlatformGrainAsync(NotificationEventType eventType, + Position position) { if (Config.IsForBacktest) { @@ -2137,33 +2084,13 @@ public class TradingBotBase : ITradingBot { await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory => { - // Notify AgentGrain (user-specific metrics) - if (Account?.User != null) - { - var agentGrain = grainFactory.GetGrain(Account.User.Id); - - var updateEvent = new AgentSummaryUpdateEvent - { - BotId = Identifier, - EventType = eventType, - Timestamp = DateTime.UtcNow, - AdditionalData = additionalData - }; - - await agentGrain.OnAgentSummaryUpdateAsync(updateEvent); - Logger.LogDebug("Sent agent notification: {EventType} for bot {BotId}", eventType, Identifier); - } - - // Notify PlatformSummaryGrain (platform-wide metrics) + var agentGrain = grainFactory.GetGrain(Account.User.Id); var platformGrain = grainFactory.GetGrain("platform-summary"); + // Create unified event objects based on event type switch (eventType) { - case AgentSummaryEventType.PositionOpened when position != null: - // Position opening is now handled by TradeExecutedEvent in PublishTradeExecutedEventAsync - Logger.LogDebug( - "Position opened notification sent via TradeExecutedEvent for position {PositionId}", - position.Identifier); + case NotificationEventType.PositionOpened: var positionOpenEvent = new PositionOpenEvent { PositionIdentifier = position.Identifier, @@ -2172,10 +2099,15 @@ public class TradingBotBase : ITradingBot Fee = position.GasFees + position.UiFees, Direction = position.OriginDirection }; + + await agentGrain.OnPositionOpenedAsync(positionOpenEvent); await platformGrain.OnPositionOpenAsync(positionOpenEvent); + + Logger.LogDebug("Sent position opened event to both grains for position {PositionId}", + position.Identifier); break; - case AgentSummaryEventType.PositionClosed when position != null: + case NotificationEventType.PositionClosed: var positionClosedEvent = new PositionClosedEvent { PositionIdentifier = position.Identifier, @@ -2183,8 +2115,23 @@ public class TradingBotBase : ITradingBot RealizedPnL = position.ProfitAndLoss?.Realized ?? 0, Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage, }; + + await agentGrain.OnPositionClosedAsync(positionClosedEvent); await platformGrain.OnPositionClosedAsync(positionClosedEvent); - Logger.LogDebug("Sent platform position closed notification for position {PositionId}", + + Logger.LogDebug("Sent position closed event to both grains for position {PositionId}", + position.Identifier); + break; + + case NotificationEventType.PositionUpdated: + var positionUpdatedEvent = new PositionUpdatedEvent + { + PositionIdentifier = position.Identifier, + }; + + await agentGrain.OnPositionUpdatedAsync(positionUpdatedEvent); + + Logger.LogDebug("Sent position updated event to both grains for position {PositionId}", position.Identifier); break; } diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs index 8bb7624a..8f6bd6e6 100644 --- a/src/Managing.Application/Grains/PlatformSummaryGrain.cs +++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs @@ -71,13 +71,13 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable TotalOpenInterest = 0, TotalPositionCount = 0, }; - + _state.State.DailySnapshots.Add(initialSnapshot); _state.State.LastSnapshot = today; _state.State.LastUpdated = today; _logger.LogInformation("Created initial empty daily snapshot for {Date}", today); } - + await RefreshDataAsync(); } } @@ -108,7 +108,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable // Calculate volume from strategies var totalVolume = strategies.Sum(s => s.Volume); - + // Calculate PnL directly from database positions (closed positions only) var totalPnL = await _tradingService.GetGlobalPnLFromPositionsAsync(); @@ -118,7 +118,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable // Update state _state.State.TotalAgents = totalAgents; _state.State.TotalActiveStrategies = totalActiveStrategies; - + // Only update volume if it hasn't been updated by events recently // This preserves real-time volume updates from position events if (!_state.State.VolumeUpdatedByEvents) @@ -130,7 +130,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { _logger.LogDebug("Preserving event-updated volume: {Volume}", _state.State.TotalPlatformVolume); } - + _state.State.TotalPlatformPnL = totalPnL; _state.State.OpenInterest = totalOpenInterest; _state.State.TotalPositionCount = totalPositionCount; @@ -254,31 +254,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } } - public Task GetTotalVolumeAsync() - { - return Task.FromResult(_state.State.TotalPlatformVolume); - } - - public Task GetTotalPnLAsync() - { - return Task.FromResult(_state.State.TotalPlatformPnL); - } - - public Task GetTotalOpenInterest() - { - return Task.FromResult(_state.State.OpenInterest); - } - - public Task GetTotalPositionCountAsync() - { - return Task.FromResult(_state.State.TotalPositionCount); - } - - public Task GetTotalFeesAsync() - { - return Task.FromResult(_state.State.TotalPlatformFees); - } - // Event handlers for immediate updates public async Task UpdateActiveStrategyCountAsync(int newActiveCount) { @@ -303,7 +278,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } } - public async Task OnPositionClosedAsync(PositionClosedEvent evt) { try @@ -319,7 +293,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } _state.State.TotalPlatformVolume += evt.Volume; - + // PnL is now calculated directly from database positions, not from events // This ensures accuracy and prevents double-counting issues // Refresh PnL from database to get the latest accurate value @@ -427,7 +401,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } } - private async Task TakeDailySnapshotAsync() { _logger.LogInformation("Taking daily snapshot"); diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs index 939905e5..bc48989c 100644 --- a/src/Managing.Application/Trading/TradingService.cs +++ b/src/Managing.Application/Trading/TradingService.cs @@ -178,27 +178,11 @@ public class TradingService : ITradingService return position; } - public async Task UpdatePositionAsync(Position position) { await _tradingRepository.UpdatePositionAsync(position); } - public async Task UpdateTradeAsync(Trade trade) - { - await _tradingRepository.UpdateTradeAsync(trade); - } - - public async Task> GetPositionsAsync() - { - var positions = new List(); - positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.New)); - positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.Filled)); - positions.AddRange(await GetPositionsByStatusAsync(PositionStatus.PartiallyFilled)); - return positions; - } - - public async Task WatchTrader() { var availableTickers = new List { Ticker.BTC, Ticker.ETH, Ticker.UNI, Ticker.LINK }; @@ -268,7 +252,8 @@ public class TradingService : ITradingService return await _tradingRepository.GetPositionsByInitiatorIdentifierAsync(initiatorIdentifier); } - public async Task> GetPositionsByInitiatorIdentifiersAsync(IEnumerable initiatorIdentifiers) + public async Task> GetPositionsByInitiatorIdentifiersAsync( + IEnumerable initiatorIdentifiers) { return await _tradingRepository.GetPositionsByInitiatorIdentifiersAsync(initiatorIdentifiers); } @@ -385,10 +370,11 @@ public class TradingService : ITradingService var account = await _accountRepository.GetAccountByKeyAsync(publicAddress); if (account != null && account.IsGmxInitialized) { - _logger.LogInformation("Account with address {PublicAddress} is already initialized for GMX", publicAddress); - return new PrivyInitAddressResponse - { - Success = true, + _logger.LogInformation("Account with address {PublicAddress} is already initialized for GMX", + publicAddress); + return new PrivyInitAddressResponse + { + Success = true, Address = publicAddress, IsAlreadyInitialized = true }; diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index a1dee9d4..4ea695d3 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -205,10 +205,9 @@ public static class Enums Canceled = 1, Rejected = 2, Updating = 3, - PartiallyFilled = 4, - Filled = 5, - Flipped = 6, - Finished = 7 + Filled = 4, + Flipped = 5, + Finished = 6 } public enum PositionInitiator @@ -492,10 +491,11 @@ public static class Enums /// /// Event types for agent summary updates /// - public enum AgentSummaryEventType + public enum NotificationEventType { BotStarted, PositionOpened, PositionClosed, + PositionUpdated } } \ No newline at end of file diff --git a/src/Managing.Domain/Trades/Position.cs b/src/Managing.Domain/Trades/Position.cs index c8cdb64f..2304f155 100644 --- a/src/Managing.Domain/Trades/Position.cs +++ b/src/Managing.Domain/Trades/Position.cs @@ -105,7 +105,7 @@ namespace Managing.Domain.Trades return 0; } - return ProfitAndLoss.Realized - CalculateTotalFees(); + return ProfitAndLoss.Realized; } ///