Fix global summary

This commit is contained in:
2025-09-28 15:47:27 +07:00
parent c71716d5c2
commit 563f0969d6
2 changed files with 149 additions and 179 deletions

View File

@@ -64,4 +64,6 @@ public class DailySnapshot
[Id(5)] public decimal TotalOpenInterest { get; set; } [Id(5)] public decimal TotalOpenInterest { get; set; }
[Id(6)] public int TotalPositionCount { get; set; } [Id(6)] public int TotalPositionCount { get; set; }
[Id(7)] public int TotalPlatformFees { get; set; }
} }

View File

@@ -2,8 +2,8 @@ using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Orleans; using Managing.Application.Orleans;
using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Trades;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -70,11 +70,12 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
TotalPnL = 0, TotalPnL = 0,
TotalOpenInterest = 0, TotalOpenInterest = 0,
TotalPositionCount = 0, TotalPositionCount = 0,
TotalPlatformFees = 0,
}; };
_state.State.DailySnapshots.Add(initialSnapshot); _state.State.DailySnapshots.Add(initialSnapshot);
_state.State.LastSnapshot = today; _state.State.LastSnapshot = initialSnapshot.Date;
_state.State.LastUpdated = today; _state.State.LastUpdated = initialSnapshot.Date;
_logger.LogInformation("Created initial empty daily snapshot for {Date}", today); _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"); _logger.LogInformation("Refreshing platform summary data");
var positions = await _tradingService.GetAllDatabasePositionsAsync();
var strategies = await _botService.GetBotsAsync(); var strategies = await _botService.GetBotsAsync();
// Calculate totals // Calculate all metrics from positions in a single loop
var totalActiveStrategies = strategies.Count(s => s.Status == BotStatus.Running); var totalVolume = 0m;
var totalFees = 0m;
var totalPnL = 0m;
var totalOpenInterest = 0m;
var totalPositionCount = 0;
// Calculate volume from strategies // Clear state dictionaries at the start
var totalVolume = strategies.Sum(s => s.Volume); _state.State.VolumeByAsset.Clear();
_state.State.PositionCountByAsset.Clear();
_state.State.PositionCountByDirection.Clear();
// Calculate PnL directly from database positions (closed positions only) foreach (var position in positions)
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)
{ {
_state.State.TotalPlatformVolume = totalVolume; // Calculate volume using the dedicated method
_logger.LogDebug("Updated volume from strategies: {Volume}", totalVolume); var positionVolume = GetVolumeForPosition(position);
} totalVolume += positionVolume;
else
{ // Add to open interest for active positions only
_logger.LogDebug("Preserving event-updated volume: {Volume}", _state.State.TotalPlatformVolume); 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.TotalAgents = await _agentService.GetTotalAgentCount();
_state.State.TotalPlatformVolume = totalVolume;
_state.State.TotalPlatformFees = totalFees;
_state.State.TotalPlatformPnL = totalPnL; _state.State.TotalPlatformPnL = totalPnL;
_state.State.OpenInterest = totalOpenInterest; _state.State.OpenInterest = totalOpenInterest;
_state.State.TotalLifetimePositionCount = totalPositionCount; _state.State.TotalLifetimePositionCount = totalPositionCount;
_state.State.LastUpdated = DateTime.UtcNow;
_state.State.HasPendingChanges = false; _state.State.HasPendingChanges = false;
// Update volume breakdown by asset only if volume wasn't updated by events _logger.LogDebug(
if (!_state.State.VolumeUpdatedByEvents) "Updated position breakdown from positions: {AssetCount} assets, Long={LongPositions}, Short={ShortPositions}",
{ _state.State.PositionCountByAsset.Count,
UpdateVolumeBreakdown(strategies); _state.State.PositionCountByDirection.GetValueOrDefault(TradeDirection.Long, 0),
} _state.State.PositionCountByDirection.GetValueOrDefault(TradeDirection.Short, 0));
else
{
_logger.LogDebug("Preserving event-updated volume breakdown");
}
// Update position count breakdown
UpdatePositionCountBreakdown(strategies);
_state.State.LastUpdated = DateTime.UtcNow;
await _state.WriteStateAsync(); await _state.WriteStateAsync();
_logger.LogInformation("Platform summary data refreshed successfully"); _logger.LogInformation("Platform summary data refreshed successfully");
@@ -158,97 +185,39 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
} }
} }
private void UpdateVolumeBreakdown(IEnumerable<Bot> strategies) /// <summary>
/// Calculates the total volume for a position based on its status and filled trades
/// </summary>
/// <param name="position">The position to calculate volume for</param>
/// <returns>The total volume for the position</returns>
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 // For closed positions, add volume from filled closing trades
var volumeByAsset = strategies if (position.IsFinished())
.Where(s => s.Volume > 0)
.GroupBy(s => s.Ticker)
.ToDictionary(g => g.Key, g => g.Sum(s => s.Volume));
foreach (var kvp in volumeByAsset)
{ {
_state.State.VolumeByAsset[kvp.Key] = kvp.Value; // Add Stop Loss volume if filled
} if (position.StopLoss?.Status == TradeStatus.Filled)
_logger.LogDebug("Updated volume breakdown: {AssetCount} assets with total volume {TotalVolume}",
volumeByAsset.Count, volumeByAsset.Values.Sum());
}
private void UpdatePositionCountBreakdown(IEnumerable<Bot> 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)
{ {
_state.State.PositionCountByAsset[kvp.Key] = kvp.Value; totalVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage;
} }
_state.State.PositionCountByDirection[TradeDirection.Long] = totalLongPositions; // Add Take Profit 1 volume if filled
_state.State.PositionCountByDirection[TradeDirection.Short] = totalShortPositions; if (position.TakeProfit1?.Status == TradeStatus.Filled)
_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())
{ {
// Calculate open interest as the sum of leveraged position notional values totalVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage;
// 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);
} }
else
// Add Take Profit 2 volume if filled
if (position.TakeProfit2?.Status == TradeStatus.Filled)
{ {
_logger.LogDebug("No open positions found for metrics calculation"); totalVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage;
return (0m, allPositions.Count());
} }
} }
catch (Exception ex)
{ return totalVolume;
_logger.LogWarning(ex, "Failed to calculate position metrics, returning zero values");
return (0m, 0);
}
} }
// Event handlers for immediate updates // Event handlers for immediate updates
@@ -266,7 +235,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
} }
_state.State.TotalActiveStrategies = newActiveCount; _state.State.TotalActiveStrategies = newActiveCount;
_state.State.HasPendingChanges = true;
await _state.WriteStateAsync(); await _state.WriteStateAsync();
} }
catch (Exception ex) catch (Exception ex)
@@ -282,34 +250,34 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
_logger.LogInformation("Position closed: {PositionId} for {Ticker} with PnL: {PnL}", _logger.LogInformation("Position closed: {PositionId} for {Ticker} with PnL: {PnL}",
evt.PositionIdentifier, evt.Ticker, evt.RealizedPnL); evt.PositionIdentifier, evt.Ticker, evt.RealizedPnL);
// Validate event data // // Validate event data
if (evt == null || evt.PositionIdentifier == Guid.Empty || evt.Ticker == Ticker.Unknown) // if (evt == null || evt.PositionIdentifier == Guid.Empty || evt.Ticker == Ticker.Unknown)
{ // {
_logger.LogWarning("Invalid PositionClosedEvent received: {Event}", evt); // _logger.LogWarning("Invalid PositionClosedEvent received: {Event}", evt);
return; // return;
} // }
_state.State.TotalPlatformVolume += evt.Volume; // _state.State.TotalPlatformVolume += evt.Volume;
// PnL is now calculated directly from database positions, not from events // // PnL is now calculated directly from database positions, not from events
// This ensures accuracy and prevents double-counting issues // // This ensures accuracy and prevents double-counting issues
// Refresh PnL from database to get the latest accurate value // // Refresh PnL from database to get the latest accurate value
await RefreshPnLFromDatabaseAsync(); // await RefreshPnLFromDatabaseAsync();
// Update volume by asset // // Update volume by asset
var asset = evt.Ticker; // var asset = evt.Ticker;
if (!_state.State.VolumeByAsset.ContainsKey(asset)) // if (!_state.State.VolumeByAsset.ContainsKey(asset))
{ // {
_state.State.VolumeByAsset[asset] = 0; // _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 // // Mark that volume has been updated by events
_state.State.VolumeUpdatedByEvents = true; // _state.State.VolumeUpdatedByEvents = true;
// Update open interest (subtract the closed position's volume) // // Update open interest (subtract the closed position's volume)
_state.State.OpenInterest = Math.Max(0, _state.State.OpenInterest - evt.Volume); // _state.State.OpenInterest = Math.Max(0, _state.State.OpenInterest - evt.Volume);
_state.State.HasPendingChanges = true; _state.State.HasPendingChanges = true;
await _state.WriteStateAsync(); await _state.WriteStateAsync();
@@ -327,54 +295,54 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
_logger.LogInformation("Position opened: {PositionIdentifier} for {Ticker} with volume: {Volume}", _logger.LogInformation("Position opened: {PositionIdentifier} for {Ticker} with volume: {Volume}",
evt.PositionIdentifier, evt.Ticker, evt.Volume); evt.PositionIdentifier, evt.Ticker, evt.Volume);
// Validate event data // // Validate event data
if (evt == null || evt.Ticker == Ticker.Unknown || evt.Volume <= 0) // if (evt == null || evt.Ticker == Ticker.Unknown || evt.Volume <= 0)
{ // {
_logger.LogWarning("Invalid PositionOpenEvent received: {Event}", evt); // _logger.LogWarning("Invalid PositionOpenEvent received: {Event}", evt);
return; // return;
} // }
// Update platform volume // // Update platform volume
_state.State.TotalPlatformVolume += evt.Volume; // _state.State.TotalPlatformVolume += evt.Volume;
// Update volume by asset // // Update volume by asset
var asset = evt.Ticker; // var asset = evt.Ticker;
if (!_state.State.VolumeByAsset.ContainsKey(asset)) // if (!_state.State.VolumeByAsset.ContainsKey(asset))
{ // {
_state.State.VolumeByAsset[asset] = 0; // _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 // // Mark that volume has been updated by events
_state.State.VolumeUpdatedByEvents = true; // _state.State.VolumeUpdatedByEvents = true;
// Update open interest and position count // // Update open interest and position count
// Since this is called only when position is fully open on broker, we always increase counts // // Since this is called only when position is fully open on broker, we always increase counts
_state.State.TotalLifetimePositionCount++; // _state.State.TotalLifetimePositionCount++;
_state.State.OpenInterest += evt.Volume; // _state.State.OpenInterest += evt.Volume;
// Update position count by asset // // Update position count by asset
if (!_state.State.PositionCountByAsset.ContainsKey(asset)) // if (!_state.State.PositionCountByAsset.ContainsKey(asset))
{ // {
_state.State.PositionCountByAsset[asset] = 0; // _state.State.PositionCountByAsset[asset] = 0;
} // }
_state.State.PositionCountByAsset[asset]++; // _state.State.PositionCountByAsset[asset]++;
// Update position count by direction // // Update position count by direction
if (!_state.State.PositionCountByDirection.ContainsKey(evt.Direction)) // if (!_state.State.PositionCountByDirection.ContainsKey(evt.Direction))
{ // {
_state.State.PositionCountByDirection[evt.Direction] = 0; // _state.State.PositionCountByDirection[evt.Direction] = 0;
} // }
_state.State.PositionCountByDirection[evt.Direction]++; // _state.State.PositionCountByDirection[evt.Direction]++;
// Update fees if provided // // Update fees if provided
if (evt.Fee > 0) // if (evt.Fee > 0)
{ // {
_state.State.TotalPlatformFees += evt.Fee; // _state.State.TotalPlatformFees += evt.Fee;
} // }
_state.State.HasPendingChanges = true; _state.State.HasPendingChanges = true;
await _state.WriteStateAsync(); await _state.WriteStateAsync();