Fix platform summary + add recalculation of the dailySnapshots

This commit is contained in:
2025-10-03 12:18:40 +07:00
parent ba7a1f87c4
commit 44fd3c6919

View File

@@ -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);
}
/// <summary>
/// Wipes all daily snapshots except for the first day
/// </summary>
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");
}
}
/// <summary>
/// Fills missing daily snapshots by recalculating them from position data
/// </summary>
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<DateTime>();
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");
}
}
/// <summary>
/// Calculates a daily snapshot from positions for a specific date
/// </summary>
/// <param name="positions">All positions to analyze</param>
/// <param name="targetDate">The date to calculate the snapshot for</param>
/// <returns>A daily snapshot for the specified date</returns>
private async Task<DailySnapshot> CalculateDailySnapshotFromPositionsAsync(List<Position> 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<decimal>();
// 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,
};
}
}