Fix global summary
This commit is contained in:
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user