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