diff --git a/src/Managing.Application/Grains/PlatformSummaryGrain.cs b/src/Managing.Application/Grains/PlatformSummaryGrain.cs index 68deed0d..e3353914 100644 --- a/src/Managing.Application/Grains/PlatformSummaryGrain.cs +++ b/src/Managing.Application/Grains/PlatformSummaryGrain.cs @@ -4,6 +4,7 @@ using Managing.Application.Abstractions.Services; using Managing.Application.Orleans; using Managing.Domain.Candles; using Managing.Domain.Shared.Helpers; +using Managing.Domain.Trades; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -49,10 +50,25 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable // Daily reminder - runs at midnight (00:00 UTC) var nextDailyTime = CandleHelpers.GetNextExpectedCandleTime(Timeframe.OneDay, now); var timeUntilNextDay = nextDailyTime - now; + + // Ensure dueTime is never negative - if it is, schedule for next day + if (timeUntilNextDay <= TimeSpan.Zero) + { + timeUntilNextDay = TimeSpan.FromDays(1).Add(TimeSpan.FromMinutes(3)); + _logger.LogWarning("Due time was negative or zero, scheduling reminder for next day instead"); + } + await this.RegisterOrUpdateReminder(_dailySnapshotReminder, timeUntilNextDay, TimeSpan.FromDays(1).Add(TimeSpan.FromMinutes(3))); - _logger.LogInformation("Daily reminder scheduled - Next daily: {NextDaily}", nextDailyTime); + _logger.LogInformation("Daily reminder scheduled - Next daily: {NextDaily}, Due time: {DueTime}", + nextDailyTime, timeUntilNextDay); + + // Wipe daily snapshots except for the first day + await WipeDailySnapshotsExceptFirstAsync(); + + // Fill missing daily snapshots before initial data load + await FillMissingDailySnapshotsAsync(); // Initial data load if state is empty if (_state.State.LastUpdated == default) @@ -81,7 +97,6 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable _logger.LogInformation("Created initial empty daily snapshot for {Date}", today); } - await RefreshDataAsync(); } @@ -125,10 +140,11 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable var positionVolume = TradingHelpers.GetVolumeForPosition(position); totalVolume += positionVolume; - // Add to open interest for active positions only + // Add to open interest for active positions only (only opening volume) if (!position.IsFinished()) { - totalOpenInterest += positionVolume; + var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage; + totalOpenInterest += openingVolume; } // Calculate fees and PnL for all positions @@ -313,4 +329,277 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable var timeSinceLastUpdate = DateTime.UtcNow - _state.State.LastUpdated; return timeSinceLastUpdate > TimeSpan.FromMinutes(5); } + + /// + /// Wipes all daily snapshots except for the first day + /// + private async Task WipeDailySnapshotsExceptFirstAsync() + { + try + { + if (!_state.State.DailySnapshots.Any()) + { + _logger.LogInformation("No daily snapshots to wipe"); + return; + } + + var originalCount = _state.State.DailySnapshots.Count; + + // Keep only the first day snapshot + var firstSnapshot = _state.State.DailySnapshots.OrderBy(s => s.Date).First(); + _state.State.DailySnapshots.Clear(); + _state.State.DailySnapshots.Add(firstSnapshot); + + // Update last snapshot date to the first snapshot date + _state.State.LastSnapshot = firstSnapshot.Date; + + await _state.WriteStateAsync(); + + _logger.LogInformation("Wiped {WipedCount} daily snapshots, kept first snapshot from {FirstDate}", + originalCount - 1, firstSnapshot.Date); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error wiping daily snapshots except first"); + } + } + + /// + /// Fills missing daily snapshots by recalculating them from position data + /// + private async Task FillMissingDailySnapshotsAsync() + { + try + { + _logger.LogInformation("Checking for missing daily snapshots"); + + // Get all positions to calculate missing snapshots + var positions = await _tradingService.GetAllDatabasePositionsAsync(); + + if (!positions.Any()) + { + _logger.LogInformation("No positions found, skipping gap filling"); + return; + } + + // Find the date range we need to cover + var earliestPositionDate = positions.Min(p => p.Date).Date; + var latestPositionDate = positions.Max(p => p.Date).Date; + var today = DateTime.UtcNow.Date; + + // Determine the start date for gap filling + var startDate = _state.State.DailySnapshots.Any() + ? _state.State.DailySnapshots.Max(s => s.Date).AddDays(1) + : earliestPositionDate; + + // Don't go beyond today + var endDate = today > latestPositionDate ? today : latestPositionDate; + + _logger.LogInformation("Gap filling from {StartDate} to {EndDate}", startDate, endDate); + + var missingDates = new List(); + for (var date = startDate; date <= endDate; date = date.AddDays(1)) + { + if (!_state.State.DailySnapshots.Any(s => s.Date.Date == date)) + { + missingDates.Add(date); + } + } + + if (!missingDates.Any()) + { + _logger.LogInformation("No missing daily snapshots found"); + return; + } + + _logger.LogInformation("Found {Count} missing daily snapshots to fill", missingDates.Count); + + // Calculate and add missing snapshots + foreach (var missingDate in missingDates) + { + var snapshot = await CalculateDailySnapshotFromPositionsAsync(positions.ToList(), missingDate); + _state.State.DailySnapshots.Add(snapshot); + + _logger.LogInformation("Created missing daily snapshot for {Date}: Volume={Volume}, PnL={PnL}, Positions={Positions}", + missingDate, snapshot.TotalVolume, snapshot.TotalPnL, snapshot.TotalLifetimePositionCount); + } + + // Sort snapshots by date + _state.State.DailySnapshots.Sort((a, b) => a.Date.CompareTo(b.Date)); + + // Update last snapshot date + if (_state.State.DailySnapshots.Any()) + { + _state.State.LastSnapshot = _state.State.DailySnapshots.Max(s => s.Date); + } + + await _state.WriteStateAsync(); + _logger.LogInformation("Successfully filled {Count} missing daily snapshots", missingDates.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error filling missing daily snapshots"); + } + } + + /// + /// Calculates a daily snapshot from positions for a specific date + /// + /// All positions to analyze + /// The date to calculate the snapshot for + /// A daily snapshot for the specified date + private async Task CalculateDailySnapshotFromPositionsAsync(List positions, DateTime targetDate) + { + var dayStart = targetDate; + var dayEnd = targetDate.AddDays(1); + + // For daily snapshots, we need to consider ALL positions to calculate: + // 1. Volume from trades that occurred on this specific day + // 2. Open interest from positions that were active during this day + // So we'll process all positions and filter the relevant data + + // Calculate metrics for this specific day + var totalVolume = 0m; + var totalFees = 0m; + var totalPnL = 0m; + var maxOpenInterest = 0m; + var totalPositionCount = 0; + + // Calculate open interest at different points during the day to find the maximum + var hourlyOpenInterest = new List(); + + // Check open interest at each hour of the day (0-23) + for (int hour = 0; hour < 24; hour++) + { + var hourDateTime = targetDate.AddHours(hour); + var hourlyOI = 0m; + + foreach (var position in positions) + { + // Check if position was active at this hour + var wasActiveAtThisHour = position.Date <= hourDateTime && + (!position.IsFinished() || + (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date > hourDateTime) || + (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date > hourDateTime) || + (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date > hourDateTime)); + + if (wasActiveAtThisHour) + { + // For open interest, only count the opening volume (not closing trades) + var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage; + hourlyOI += openingVolume; + } + } + + hourlyOpenInterest.Add(hourlyOI); + } + + // Find the maximum open interest during the day + maxOpenInterest = hourlyOpenInterest.Max(); + + foreach (var position in positions) + { + // Calculate volume for trades that occurred on this specific day + var dayVolume = 0m; + + _logger.LogDebug("Checking position {PositionId}: Position.Date={PositionDate}, TargetDate={TargetDate}, Position.Date.Date={PositionDateOnly}", + position.Identifier, position.Date, targetDate, position.Date.Date); + + // Add opening volume if position was opened on this day + // Use more flexible date comparison to handle timezone differences + if (position.Date.Date == targetDate || + (position.Date >= targetDate && position.Date < targetDate.AddDays(1))) + { + var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage; + dayVolume += openingVolume; + _logger.LogDebug("Position {PositionId} opened on {TargetDate}: Opening volume = {OpeningVolume}", + position.Identifier, targetDate, openingVolume); + } + + // Add closing volume if position was closed on this day + if (position.IsFinished()) + { + if (position.StopLoss.Status == TradeStatus.Filled && + (position.StopLoss.Date.Date == targetDate || + (position.StopLoss.Date >= targetDate && position.StopLoss.Date < targetDate.AddDays(1)))) + { + var closingVolume = position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage; + dayVolume += closingVolume; + _logger.LogDebug("Position {PositionId} closed on {TargetDate} via StopLoss: Closing volume = {ClosingVolume}", + position.Identifier, targetDate, closingVolume); + } + if (position.TakeProfit1.Status == TradeStatus.Filled && + (position.TakeProfit1.Date.Date == targetDate || + (position.TakeProfit1.Date >= targetDate && position.TakeProfit1.Date < targetDate.AddDays(1)))) + { + var closingVolume = position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage; + dayVolume += closingVolume; + _logger.LogDebug("Position {PositionId} closed on {TargetDate} via TakeProfit1: Closing volume = {ClosingVolume}", + position.Identifier, targetDate, closingVolume); + } + if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && + (position.TakeProfit2.Date.Date == targetDate || + (position.TakeProfit2.Date >= targetDate && position.TakeProfit2.Date < targetDate.AddDays(1)))) + { + var closingVolume = position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage; + dayVolume += closingVolume; + _logger.LogDebug("Position {PositionId} closed on {TargetDate} via TakeProfit2: Closing volume = {ClosingVolume}", + position.Identifier, targetDate, closingVolume); + } + } + + if (dayVolume > 0) + { + _logger.LogDebug("Position {PositionId} contributed {DayVolume} to {TargetDate} total volume", + position.Identifier, dayVolume, targetDate); + } + + totalVolume += dayVolume; + + // Calculate fees and PnL for positions closed on this day + var wasClosedOnThisDay = position.IsFinished() && ( + (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date.Date == targetDate) || + (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date == targetDate) || + (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date == targetDate) + ); + + if (wasClosedOnThisDay) + { + totalFees += position.CalculateTotalFees(); + totalPnL += position.ProfitAndLoss?.Realized ?? 0; + } + + // Count positions that were active on this day (opened on or before, closed on or after) + var wasActiveOnThisDay = position.Date.Date <= targetDate && + (!position.IsFinished() || + (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date.Date >= targetDate) || + (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date >= targetDate) || + (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date >= targetDate)); + + if (wasActiveOnThisDay) + { + totalPositionCount++; + } + } + + // Get current agent and strategy counts (these are current state, not historical) + var totalAgents = await _agentService.GetTotalAgentCount(); + var totalStrategies = _state.State.TotalActiveStrategies; + + _logger.LogInformation("Calculated daily snapshot for {TargetDate}: TotalVolume={TotalVolume}, MaxOpenInterest={MaxOpenInterest}, TotalPositionCount={TotalPositionCount}", + targetDate, totalVolume, maxOpenInterest, totalPositionCount); + + return new DailySnapshot + { + Date = targetDate, + TotalAgents = totalAgents, + TotalStrategies = totalStrategies, + TotalVolume = totalVolume, + TotalPnL = totalPnL, + NetPnL = totalPnL - totalFees, + TotalOpenInterest = maxOpenInterest, + TotalLifetimePositionCount = totalPositionCount, + TotalPlatformFees = (int)totalFees, + }; + } } \ No newline at end of file