From 563f0969d64e3a738613bc99f5b15daeafad277e Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sun, 28 Sep 2025 15:47:27 +0700 Subject: [PATCH] Fix global summary --- .../Grains/PlatformSummaryGrainState.cs | 2 + .../Grains/PlatformSummaryGrain.cs | 326 ++++++++---------- 2 files changed, 149 insertions(+), 179 deletions(-) diff --git a/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs b/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs index 905d7712..78760b78 100644 --- a/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs +++ b/src/Managing.Application.Abstractions/Grains/PlatformSummaryGrainState.cs @@ -64,4 +64,6 @@ public class DailySnapshot [Id(5)] public decimal TotalOpenInterest { get; set; } [Id(6)] public int TotalPositionCount { get; set; } + + [Id(7)] public int TotalPlatformFees { get; set; } } \ No newline at end of file diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs index 76937453..6610827d 100644 --- a/src/Managing.Application/Grains/PlatformSummaryGrain.cs +++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs @@ -2,8 +2,8 @@ using Managing.Application.Abstractions; using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Services; using Managing.Application.Orleans; -using Managing.Domain.Bots; using Managing.Domain.Candles; +using Managing.Domain.Trades; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -70,11 +70,12 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable TotalPnL = 0, TotalOpenInterest = 0, TotalPositionCount = 0, + TotalPlatformFees = 0, }; _state.State.DailySnapshots.Add(initialSnapshot); - _state.State.LastSnapshot = today; - _state.State.LastUpdated = today; + _state.State.LastSnapshot = initialSnapshot.Date; + _state.State.LastUpdated = initialSnapshot.Date; _logger.LogInformation("Created initial empty daily snapshot for {Date}", today); } @@ -99,55 +100,81 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable { _logger.LogInformation("Refreshing platform summary data"); + var positions = await _tradingService.GetAllDatabasePositionsAsync(); var strategies = await _botService.GetBotsAsync(); - // Calculate totals - var totalActiveStrategies = strategies.Count(s => s.Status == BotStatus.Running); + // Calculate all metrics from positions in a single loop + var totalVolume = 0m; + var totalFees = 0m; + var totalPnL = 0m; + var totalOpenInterest = 0m; + var totalPositionCount = 0; - // Calculate volume from strategies - var totalVolume = strategies.Sum(s => s.Volume); + // Clear state dictionaries at the start + _state.State.VolumeByAsset.Clear(); + _state.State.PositionCountByAsset.Clear(); + _state.State.PositionCountByDirection.Clear(); - // Calculate PnL directly from database positions (closed positions only) - var totalPnL = await _tradingService.GetGlobalPnLFromPositionsAsync(); - - // Calculate real open interest and position count from actual positions - var (totalOpenInterest, totalPositionCount) = await CalculatePositionMetricsAsync(); - - // Update state - _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) + foreach (var position in positions) { - _state.State.TotalPlatformVolume = totalVolume; - _logger.LogDebug("Updated volume from strategies: {Volume}", totalVolume); - } - else - { - _logger.LogDebug("Preserving event-updated volume: {Volume}", _state.State.TotalPlatformVolume); + // Calculate volume using the dedicated method + var positionVolume = GetVolumeForPosition(position); + totalVolume += positionVolume; + + // Add to open interest for active positions only + if (!position.IsFinished()) + { + totalOpenInterest += positionVolume; + } + + // Calculate fees and PnL for all positions + totalFees += position.CalculateTotalFees(); + totalPnL += position.ProfitAndLoss?.Realized ?? 0; + + // Count all positions + totalPositionCount++; + + // Calculate breakdown metrics and update state directly + var ticker = position.Ticker; + var direction = position.OriginDirection; + + // Volume breakdown by asset - update state directly + if (!_state.State.VolumeByAsset.ContainsKey(ticker)) + { + _state.State.VolumeByAsset[ticker] = 0; + } + _state.State.VolumeByAsset[ticker] += positionVolume; + + // Position count breakdown by asset - update state directly + if (!_state.State.PositionCountByAsset.ContainsKey(ticker)) + { + _state.State.PositionCountByAsset[ticker] = 0; + } + _state.State.PositionCountByAsset[ticker]++; + + // Position count breakdown by direction - update state directly + if (!_state.State.PositionCountByDirection.ContainsKey(direction)) + { + _state.State.PositionCountByDirection[direction] = 0; + } + _state.State.PositionCountByDirection[direction]++; } _state.State.TotalAgents = await _agentService.GetTotalAgentCount(); + _state.State.TotalPlatformVolume = totalVolume; + _state.State.TotalPlatformFees = totalFees; _state.State.TotalPlatformPnL = totalPnL; _state.State.OpenInterest = totalOpenInterest; _state.State.TotalLifetimePositionCount = totalPositionCount; - _state.State.LastUpdated = DateTime.UtcNow; _state.State.HasPendingChanges = false; - // Update volume breakdown by asset only if volume wasn't updated by events - if (!_state.State.VolumeUpdatedByEvents) - { - UpdateVolumeBreakdown(strategies); - } - else - { - _logger.LogDebug("Preserving event-updated volume breakdown"); - } - - // Update position count breakdown - UpdatePositionCountBreakdown(strategies); + _logger.LogDebug( + "Updated position breakdown from positions: {AssetCount} assets, Long={LongPositions}, Short={ShortPositions}", + _state.State.PositionCountByAsset.Count, + _state.State.PositionCountByDirection.GetValueOrDefault(TradeDirection.Long, 0), + _state.State.PositionCountByDirection.GetValueOrDefault(TradeDirection.Short, 0)); + _state.State.LastUpdated = DateTime.UtcNow; await _state.WriteStateAsync(); _logger.LogInformation("Platform summary data refreshed successfully"); @@ -158,97 +185,39 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } } - private void UpdateVolumeBreakdown(IEnumerable strategies) + /// + /// Calculates the total volume for a position based on its status and filled trades + /// + /// The position to calculate volume for + /// The total volume for the position + private decimal GetVolumeForPosition(Position position) { - _state.State.VolumeByAsset.Clear(); + // Always include the opening trade volume + var totalVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage; - // Group strategies by ticker and sum their volumes - var volumeByAsset = strategies - .Where(s => s.Volume > 0) - .GroupBy(s => s.Ticker) - .ToDictionary(g => g.Key, g => g.Sum(s => s.Volume)); - - foreach (var kvp in volumeByAsset) + // For closed positions, add volume from filled closing trades + if (position.IsFinished()) { - _state.State.VolumeByAsset[kvp.Key] = kvp.Value; - } - - _logger.LogDebug("Updated volume breakdown: {AssetCount} assets with total volume {TotalVolume}", - volumeByAsset.Count, volumeByAsset.Values.Sum()); - } - - private void UpdatePositionCountBreakdown(IEnumerable strategies) - { - _state.State.PositionCountByAsset.Clear(); - _state.State.PositionCountByDirection.Clear(); - - // Use position counts directly from bot statistics - var activeStrategies = strategies.Where(s => s.Status != BotStatus.Saved).ToList(); - - if (activeStrategies.Any()) - { - // Group by asset and sum position counts per asset - var positionsByAsset = activeStrategies - .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 - var totalLongPositions = activeStrategies.Sum(s => s.LongPositionCount); - var totalShortPositions = activeStrategies.Sum(s => s.ShortPositionCount); - - // Update state - foreach (var kvp in positionsByAsset) + // Add Stop Loss volume if filled + if (position.StopLoss?.Status == TradeStatus.Filled) { - _state.State.PositionCountByAsset[kvp.Key] = kvp.Value; + totalVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage; } - _state.State.PositionCountByDirection[TradeDirection.Long] = totalLongPositions; - _state.State.PositionCountByDirection[TradeDirection.Short] = totalShortPositions; - - _logger.LogDebug( - "Updated position breakdown from bot statistics: {AssetCount} assets, Long={LongPositions}, Short={ShortPositions}", - positionsByAsset.Count, totalLongPositions, totalShortPositions); - } - else - { - _logger.LogDebug("No active strategies found for position breakdown"); - } - } - - private async Task<(decimal totalOpenInterest, int totalPositionCount)> CalculatePositionMetricsAsync() - { - try - { - // Get all open positions from all accounts - // Get positions directly from database instead of exchange - var allPositions = (await _tradingService.GetAllDatabasePositionsAsync()).ToList(); - var openPositions = allPositions?.Where(p => !p.IsFinished()); - var totalOpenPositionCount = allPositions.Count(); - - if (openPositions.Any()) + // Add Take Profit 1 volume if filled + if (position.TakeProfit1?.Status == TradeStatus.Filled) { - // Calculate open interest as the sum of leveraged position notional values - // Open interest = sum of (position size * price * leverage) for all open positions - var openInterest = openPositions - .Sum(p => (p.Open.Price * p.Open.Quantity) * p.Open.Leverage); - - _logger.LogDebug( - "Calculated position metrics: {PositionCount} positions, {OpenInterest} leveraged open interest", - totalOpenPositionCount, openInterest); - - return (openInterest, totalOpenPositionCount); + totalVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage; } - else + + // Add Take Profit 2 volume if filled + if (position.TakeProfit2?.Status == TradeStatus.Filled) { - _logger.LogDebug("No open positions found for metrics calculation"); - return (0m, allPositions.Count()); + totalVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage; } } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to calculate position metrics, returning zero values"); - return (0m, 0); - } + + return totalVolume; } // Event handlers for immediate updates @@ -266,7 +235,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable } _state.State.TotalActiveStrategies = newActiveCount; - _state.State.HasPendingChanges = true; await _state.WriteStateAsync(); } catch (Exception ex) @@ -282,34 +250,34 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _logger.LogInformation("Position closed: {PositionId} for {Ticker} with PnL: {PnL}", evt.PositionIdentifier, evt.Ticker, evt.RealizedPnL); - // Validate event data - if (evt == null || evt.PositionIdentifier == Guid.Empty || evt.Ticker == Ticker.Unknown) - { - _logger.LogWarning("Invalid PositionClosedEvent received: {Event}", evt); - return; - } + // // Validate event data + // if (evt == null || evt.PositionIdentifier == Guid.Empty || evt.Ticker == Ticker.Unknown) + // { + // _logger.LogWarning("Invalid PositionClosedEvent received: {Event}", evt); + // return; + // } - _state.State.TotalPlatformVolume += evt.Volume; + // _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 - await RefreshPnLFromDatabaseAsync(); + // // 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 + // await RefreshPnLFromDatabaseAsync(); - // Update volume by asset - var asset = evt.Ticker; - if (!_state.State.VolumeByAsset.ContainsKey(asset)) - { - _state.State.VolumeByAsset[asset] = 0; - } + // // Update volume by asset + // var asset = evt.Ticker; + // if (!_state.State.VolumeByAsset.ContainsKey(asset)) + // { + // _state.State.VolumeByAsset[asset] = 0; + // } - _state.State.VolumeByAsset[asset] += evt.Volume; + // _state.State.VolumeByAsset[asset] += evt.Volume; - // Mark that volume has been updated by events - _state.State.VolumeUpdatedByEvents = true; + // // Mark that volume has been updated by events + // _state.State.VolumeUpdatedByEvents = true; - // Update open interest (subtract the closed position's volume) - _state.State.OpenInterest = Math.Max(0, _state.State.OpenInterest - evt.Volume); + // // Update open interest (subtract the closed position's volume) + // _state.State.OpenInterest = Math.Max(0, _state.State.OpenInterest - evt.Volume); _state.State.HasPendingChanges = true; await _state.WriteStateAsync(); @@ -327,54 +295,54 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _logger.LogInformation("Position opened: {PositionIdentifier} for {Ticker} with volume: {Volume}", evt.PositionIdentifier, evt.Ticker, evt.Volume); - // Validate event data - if (evt == null || evt.Ticker == Ticker.Unknown || evt.Volume <= 0) - { - _logger.LogWarning("Invalid PositionOpenEvent received: {Event}", evt); - return; - } + // // Validate event data + // if (evt == null || evt.Ticker == Ticker.Unknown || evt.Volume <= 0) + // { + // _logger.LogWarning("Invalid PositionOpenEvent received: {Event}", evt); + // return; + // } - // Update platform volume - _state.State.TotalPlatformVolume += evt.Volume; + // // Update platform volume + // _state.State.TotalPlatformVolume += evt.Volume; - // Update volume by asset - var asset = evt.Ticker; - if (!_state.State.VolumeByAsset.ContainsKey(asset)) - { - _state.State.VolumeByAsset[asset] = 0; - } + // // Update volume by asset + // var asset = evt.Ticker; + // if (!_state.State.VolumeByAsset.ContainsKey(asset)) + // { + // _state.State.VolumeByAsset[asset] = 0; + // } - _state.State.VolumeByAsset[asset] += evt.Volume; + // _state.State.VolumeByAsset[asset] += evt.Volume; - // Mark that volume has been updated by events - _state.State.VolumeUpdatedByEvents = true; + // // Mark that volume has been updated by events + // _state.State.VolumeUpdatedByEvents = true; - // Update open interest and position count - // Since this is called only when position is fully open on broker, we always increase counts - _state.State.TotalLifetimePositionCount++; - _state.State.OpenInterest += evt.Volume; + // // Update open interest and position count + // // Since this is called only when position is fully open on broker, we always increase counts + // _state.State.TotalLifetimePositionCount++; + // _state.State.OpenInterest += evt.Volume; - // Update position count by asset - if (!_state.State.PositionCountByAsset.ContainsKey(asset)) - { - _state.State.PositionCountByAsset[asset] = 0; - } + // // Update position count by asset + // if (!_state.State.PositionCountByAsset.ContainsKey(asset)) + // { + // _state.State.PositionCountByAsset[asset] = 0; + // } - _state.State.PositionCountByAsset[asset]++; + // _state.State.PositionCountByAsset[asset]++; - // Update position count by direction - if (!_state.State.PositionCountByDirection.ContainsKey(evt.Direction)) - { - _state.State.PositionCountByDirection[evt.Direction] = 0; - } + // // Update position count by direction + // if (!_state.State.PositionCountByDirection.ContainsKey(evt.Direction)) + // { + // _state.State.PositionCountByDirection[evt.Direction] = 0; + // } - _state.State.PositionCountByDirection[evt.Direction]++; + // _state.State.PositionCountByDirection[evt.Direction]++; - // Update fees if provided - if (evt.Fee > 0) - { - _state.State.TotalPlatformFees += evt.Fee; - } + // // Update fees if provided + // if (evt.Fee > 0) + // { + // _state.State.TotalPlatformFees += evt.Fee; + // } _state.State.HasPendingChanges = true; await _state.WriteStateAsync();