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(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.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<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
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<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)
// 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();