diff --git a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs index 9fc935b..296ad7b 100644 --- a/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs +++ b/src/Managing.Application.Abstractions/Grains/IPlatformSummaryGrain.cs @@ -71,12 +71,6 @@ public class StrategyDeployedEvent : PlatformMetricsEvent [Id(3)] public string StrategyName { get; set; } = string.Empty; - - [Id(4)] - public decimal InitialVolume { get; set; } - - [Id(5)] - public decimal InitialPnL { get; set; } } /// @@ -105,18 +99,12 @@ public class PositionOpenedEvent : PlatformMetricsEvent public Guid PositionId { get; set; } [Id(2)] - public Guid StrategyId { get; set; } + public Ticker Ticker { get; set; } [Id(3)] - public string Ticker { get; set; } = string.Empty; + public decimal Volume { get; set; } [Id(4)] - public decimal Size { get; set; } - - [Id(5)] - public decimal NotionalValue { get; set; } - - [Id(6)] public TradeDirection Direction { get; set; } } @@ -130,16 +118,16 @@ public class PositionClosedEvent : PlatformMetricsEvent public Guid PositionId { get; set; } [Id(2)] - public Guid StrategyId { get; set; } + public Ticker Ticker { get; set; } [Id(3)] - public string Ticker { get; set; } = string.Empty; - - [Id(4)] public decimal RealizedPnL { get; set; } - [Id(5)] + [Id(4)] public decimal Volume { get; set; } + + [Id(5)] + public decimal InitialVolume { get; set; } } /// @@ -158,7 +146,7 @@ public class TradeExecutedEvent : PlatformMetricsEvent public Guid StrategyId { get; set; } [Id(4)] - public string Ticker { get; set; } = string.Empty; + public Ticker Ticker { get; set; } [Id(5)] public decimal Volume { get; set; } diff --git a/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs b/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs index 990ca99..b80cfd5 100644 --- a/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs +++ b/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs @@ -65,11 +65,11 @@ public class PlatformSummaryGrainState // Volume breakdown by asset [Id(17)] - public Dictionary VolumeByAsset { get; set; } = new(); + public Dictionary VolumeByAsset { get; set; } = new(); // Position count breakdown [Id(18)] - public Dictionary PositionCountByAsset { get; set; } = new(); + public Dictionary PositionCountByAsset { get; set; } = new(); [Id(19)] public Dictionary PositionCountByDirection { get; set; } = new(); diff --git a/src/Managing.Application.Abstractions/Models/PlatformSummaryViewModel.cs b/src/Managing.Application.Abstractions/Models/PlatformSummaryViewModel.cs index 05126f5..bdfdef7 100644 --- a/src/Managing.Application.Abstractions/Models/PlatformSummaryViewModel.cs +++ b/src/Managing.Application.Abstractions/Models/PlatformSummaryViewModel.cs @@ -93,13 +93,13 @@ public class PlatformSummaryViewModel /// Volume breakdown by asset/ticker /// [Id(13)] - public required Dictionary VolumeByAsset { get; set; } + public required Dictionary VolumeByAsset { get; set; } /// /// Position count breakdown by asset/ticker /// [Id(14)] - public required Dictionary PositionCountByAsset { get; set; } + public required Dictionary PositionCountByAsset { get; set; } /// /// Position count breakdown by direction (Long/Short) diff --git a/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs b/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs index ccc3b16..34fac44 100644 --- a/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs +++ b/src/Managing.Application/Bots/Grains/LiveBotRegistryGrain.cs @@ -1,3 +1,4 @@ +using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -13,14 +14,17 @@ 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) + ILogger logger, + IBotService botService) { _state = state; _logger = logger; + _botService = botService; } public override async Task OnActivateAsync(CancellationToken cancellationToken) @@ -60,6 +64,9 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain _logger.LogInformation( "Bot {Identifier} registered successfully for user {UserId}. Total bots: {TotalBots}, Active bots: {ActiveBots}", identifier, userId, _state.State.TotalBotsCount, _state.State.ActiveBotsCount); + + // Notify platform summary grain about strategy deployment + await NotifyStrategyDeployedAsync(identifier, userId); } catch (Exception ex) { @@ -94,6 +101,9 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain _logger.LogInformation( "Bot {Identifier} unregistered successfully from user {UserId}. Total bots: {TotalBots}", identifier, entryToRemove.UserId, _state.State.TotalBotsCount); + + // Notify platform summary grain about strategy stopped + await NotifyStrategyStoppedAsync(identifier, entryToRemove.UserId); } catch (Exception ex) { @@ -176,4 +186,66 @@ public class LiveBotRegistryGrain : Grain, ILiveBotRegistryGrain return Task.FromResult(entry.Status); } + + private async Task NotifyStrategyDeployedAsync(Guid identifier, int userId) + { + try + { + // Get bot details for the event + var bot = await _botService.GetBotByIdentifier(identifier); + if (bot != null) + { + var platformGrain = GrainFactory.GetGrain("platform-summary"); + + var deployedEvent = new StrategyDeployedEvent + { + StrategyId = identifier, + AgentName = bot.User.AgentName, + StrategyName = bot.Name + }; + + await platformGrain.OnStrategyDeployedAsync(deployedEvent); + _logger.LogDebug("Notified platform summary about strategy deployment: {StrategyName}", bot.Name); + } + else + { + _logger.LogWarning("Could not find bot {Identifier} to notify platform summary", identifier); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to notify platform summary about strategy deployment for bot {Identifier}", identifier); + } + } + + private async Task NotifyStrategyStoppedAsync(Guid identifier, int userId) + { + try + { + // Get bot details for the event + var bot = await _botService.GetBotByIdentifier(identifier); + if (bot != null) + { + var platformGrain = GrainFactory.GetGrain("platform-summary"); + + var stoppedEvent = new StrategyStoppedEvent + { + StrategyId = identifier, + AgentName = bot.User?.Name ?? $"User-{userId}", + StrategyName = bot.Name + }; + + await platformGrain.OnStrategyStoppedAsync(stoppedEvent); + _logger.LogDebug("Notified platform summary about strategy stopped: {StrategyName}", bot.Name); + } + else + { + _logger.LogWarning("Could not find bot {Identifier} to notify platform summary", identifier); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to notify platform summary about strategy stopped for bot {Identifier}", identifier); + } + } } \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index 75d3232..3e23a99 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -377,6 +377,27 @@ public class TradingBotBase : ITradingBot if (position.Status.Equals(PositionStatus.New)) { await SetPositionStatus(position.SignalIdentifier, PositionStatus.Filled); + + // Notify platform summary about the executed trade + try + { + await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory => + { + var platformGrain = grainFactory.GetGrain("platform-summary"); + var tradeExecutedEvent = new TradeExecutedEvent + { + TradeId = position.Identifier, + Ticker = position.Ticker, + Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage + }; + + await platformGrain.OnTradeExecutedAsync(tradeExecutedEvent); + }); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to notify platform summary about trade execution for position {PositionId}", position.Identifier); + } } position = brokerPosition; diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs index c2068a3..2b94187 100644 --- a/src/Managing.Application/Grains/PlatformSummaryGrain.cs +++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs @@ -100,10 +100,10 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _state.State.HasPendingChanges = false; // Update volume breakdown by asset - await UpdateVolumeBreakdownAsync(strategies); + UpdateVolumeBreakdown(strategies); // Update position count breakdown - await UpdatePositionCountBreakdownAsync(strategies); + UpdatePositionCountBreakdown(strategies); await _state.WriteStateAsync(); @@ -115,14 +115,14 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } } - private async Task UpdateVolumeBreakdownAsync(IEnumerable strategies) + private void UpdateVolumeBreakdown(IEnumerable strategies) { _state.State.VolumeByAsset.Clear(); // Group strategies by ticker and sum their volumes var volumeByAsset = strategies .Where(s => s.Volume > 0) - .GroupBy(s => s.Ticker.ToString()) + .GroupBy(s => s.Ticker) .ToDictionary(g => g.Key, g => g.Sum(s => s.Volume)); foreach (var kvp in volumeByAsset) @@ -134,7 +134,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable volumeByAsset.Count, volumeByAsset.Values.Sum()); } - private async Task UpdatePositionCountBreakdownAsync(IEnumerable strategies) + private void UpdatePositionCountBreakdown(IEnumerable strategies) { _state.State.PositionCountByAsset.Clear(); _state.State.PositionCountByDirection.Clear(); @@ -146,7 +146,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { // Group by asset and sum position counts per asset var positionsByAsset = activeStrategies - .GroupBy(s => s.Ticker.ToString()) + .GroupBy(s => s.Ticker) .ToDictionary(g => g.Key, g => g.Sum(b => b.LongPositionCount + b.ShortPositionCount)); // Sum long and short position counts across all bots @@ -252,7 +252,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _logger.LogInformation("Position opened: {PositionId} for {Ticker}", evt.PositionId, evt.Ticker); _state.State.TotalPositionCount++; - _state.State.TotalOpenInterest += evt.NotionalValue; _state.State.HasPendingChanges = true; await _state.WriteStateAsync(); } @@ -274,6 +273,7 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } _state.State.VolumeByAsset[asset] += evt.Volume; + _state.State.TotalOpenInterest -= evt.InitialVolume; _state.State.HasPendingChanges = true; await _state.WriteStateAsync(); @@ -285,7 +285,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable evt.TradeId, evt.Ticker, evt.Volume); _state.State.TotalPlatformVolume += evt.Volume; - _state.State.TotalPlatformPnL += evt.PnL; // Update volume by asset var asset = evt.Ticker; @@ -402,8 +401,8 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable PositionCountChange24h = state.TotalPositionCount - state.TotalPositionCount24hAgo, // Breakdowns - VolumeByAsset = state.VolumeByAsset ?? new Dictionary(), - PositionCountByAsset = state.PositionCountByAsset ?? new Dictionary(), + VolumeByAsset = state.VolumeByAsset ?? new Dictionary(), + PositionCountByAsset = state.PositionCountByAsset ?? new Dictionary(), PositionCountByDirection = state.PositionCountByDirection ?? new Dictionary(), // Metadata diff --git a/src/Managing.Application/Trading/Handlers/ClosePositionCommandHandler.cs b/src/Managing.Application/Trading/Handlers/ClosePositionCommandHandler.cs index 39f9fa4..fecec70 100644 --- a/src/Managing.Application/Trading/Handlers/ClosePositionCommandHandler.cs +++ b/src/Managing.Application/Trading/Handlers/ClosePositionCommandHandler.cs @@ -1,4 +1,5 @@ using Managing.Application.Abstractions; +using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.Trading.Commands; using Managing.Domain.Shared.Helpers; @@ -12,6 +13,7 @@ public class ClosePositionCommandHandler( IExchangeService exchangeService, IAccountService accountService, ITradingService tradingService, + IGrainFactory? grainFactory = null, ILogger logger = null) : ICommandHandler { @@ -68,6 +70,30 @@ public class ClosePositionCommandHandler( if (!request.IsForBacktest) await tradingService.UpdatePositionAsync(request.Position); + + // Notify platform summary about the closed position + try + { + var platformGrain = grainFactory?.GetGrain("platform-summary"); + if (platformGrain != null) + { + var positionClosedEvent = new PositionClosedEvent + { + PositionId = request.Position.Identifier, + Ticker = request.Position.Ticker, + RealizedPnL = request.Position.ProfitAndLoss?.Realized ?? 0, + Volume = closedPosition.Quantity * lastPrice * request.Position.Open.Leverage, + InitialVolume = request.Position.Open.Quantity * request.Position.Open.Price * request.Position.Open.Leverage + }; + + await platformGrain.OnPositionClosedAsync(positionClosedEvent); + } + } + catch (Exception ex) + { + SentrySdk.CaptureException(ex); + logger?.LogError(ex, "Failed to notify platform summary about position closure for position {PositionId}", request.Position.Identifier); + } } return request.Position; @@ -77,6 +103,7 @@ public class ClosePositionCommandHandler( logger?.LogError(ex, "Error closing position: {Message} \n Stacktrace : {StackTrace}", ex.Message, ex.StackTrace); + SentrySdk.CaptureException(ex); throw; } } diff --git a/src/Managing.Application/Trading/Handlers/OpenPositionCommandHandler.cs b/src/Managing.Application/Trading/Handlers/OpenPositionCommandHandler.cs index a8cda12..2eb32fa 100644 --- a/src/Managing.Application/Trading/Handlers/OpenPositionCommandHandler.cs +++ b/src/Managing.Application/Trading/Handlers/OpenPositionCommandHandler.cs @@ -1,4 +1,5 @@ using Managing.Application.Abstractions; +using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.Trading.Commands; using Managing.Common; @@ -11,20 +12,14 @@ namespace Managing.Application.Trading.Handlers public class OpenPositionCommandHandler( IExchangeService exchangeService, IAccountService accountService, - ITradingService tradingService) + ITradingService tradingService, + IGrainFactory? grainFactory = null) : ICommandHandler { public async Task Handle(OpenPositionRequest request) { var account = await accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false); - if (!request.IsForPaperTrading) - { - // var cancelOrderResult = await exchangeService.CancelOrder(account, request.Ticker); - // if (!cancelOrderResult) - // { - // throw new Exception($"Not able to close all orders for {request.Ticker}"); - // } - } + var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator; var position = new Position(Guid.NewGuid(), request.AccountName, request.Direction, @@ -108,6 +103,29 @@ namespace Managing.Application.Trading.Handlers if (!request.IsForPaperTrading) { await tradingService.InsertPositionAsync(position); + + // Notify platform summary about the opened position + try + { + var platformGrain = grainFactory?.GetGrain("platform-summary"); + if (platformGrain != null) + { + var positionOpenedEvent = new PositionOpenedEvent + { + PositionId = position.Identifier, + Ticker = position.Ticker, + Volume = position.Open.Price * position.Open.Quantity * position.Open.Leverage, + Direction = position.OriginDirection + }; + + await platformGrain.OnPositionOpenedAsync(positionOpenedEvent); + } + } + catch (Exception) + { + // Log error but don't fail the position creation + // This is a non-critical notification + } } return position;