Add test for platform summary calculation
This commit is contained in:
@@ -3,6 +3,7 @@ using Managing.Application.Abstractions.Grains;
|
||||
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;
|
||||
@@ -140,172 +141,39 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
|
||||
{
|
||||
_logger.LogInformation("Refreshing platform summary data");
|
||||
|
||||
var positions = await _tradingService.GetAllDatabasePositionsAsync();
|
||||
var positions = (await _tradingService.GetAllDatabasePositionsAsync()).ToList();
|
||||
|
||||
// Get the last daily snapshot date to calculate cumulative volume
|
||||
var lastSnapshotDate = _state.State.DailySnapshots.Any()
|
||||
? _state.State.DailySnapshots.Max(s => s.Date)
|
||||
: DateTime.MinValue;
|
||||
// Use TradingBox to calculate all platform metrics
|
||||
var metrics = TradingBox.CalculatePlatformSummaryMetrics(positions, _state.State.TotalPlatformVolume);
|
||||
|
||||
// Start with the cumulative volume from the last snapshot
|
||||
var cumulativeVolume = _state.State.DailySnapshots.Any()
|
||||
? _state.State.DailySnapshots.OrderByDescending(s => s.Date).First().TotalVolume
|
||||
: 0m;
|
||||
// Update state with calculated metrics
|
||||
_state.State.TotalPlatformVolume = metrics.TotalPlatformVolume;
|
||||
_state.State.TotalPlatformFees = metrics.TotalPlatformFees;
|
||||
_state.State.TotalPlatformPnL = metrics.TotalPlatformPnL;
|
||||
_state.State.NetPnL = metrics.NetPnL;
|
||||
_state.State.OpenInterest = metrics.OpenInterest;
|
||||
_state.State.TotalLifetimePositionCount = metrics.TotalLifetimePositionCount;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Calculating cumulative volume from last snapshot date: {LastSnapshotDate}, Base volume: {CumulativeVolume}",
|
||||
lastSnapshotDate, cumulativeVolume);
|
||||
// Convert string keys to Ticker enum keys
|
||||
_state.State.VolumeByAsset = metrics.VolumeByAsset.ToDictionary(
|
||||
kvp => Enum.Parse<Ticker>(kvp.Key),
|
||||
kvp => kvp.Value);
|
||||
_state.State.PositionCountByAsset = metrics.PositionCountByAsset.ToDictionary(
|
||||
kvp => Enum.Parse<Ticker>(kvp.Key),
|
||||
kvp => kvp.Value);
|
||||
_state.State.PositionCountByDirection = metrics.PositionCountByDirection;
|
||||
|
||||
// Calculate all metrics from positions in a single loop
|
||||
var newVolume = 0m; // Volume from positions after last snapshot
|
||||
var totalVolumeFromAllPositions = 0m; // Total volume calculated from ALL positions (for comparison)
|
||||
var totalFees = 0m;
|
||||
var totalPnL = 0m;
|
||||
var totalOpenInterest = 0m;
|
||||
var totalPositionCount = 0;
|
||||
|
||||
// Clear state dictionaries at the start
|
||||
_state.State.VolumeByAsset.Clear();
|
||||
_state.State.PositionCountByAsset.Clear();
|
||||
_state.State.PositionCountByDirection.Clear();
|
||||
|
||||
foreach (var position in positions)
|
||||
{
|
||||
if (!position.IsValidForMetrics()) continue;
|
||||
|
||||
// Calculate volume using the same logic as daily snapshots for consistency
|
||||
// Opening volume is always counted (for positions opened on or before today)
|
||||
var openVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage;
|
||||
var closingVolume = 0m;
|
||||
|
||||
// Only include closing volume from trades that are filled
|
||||
if (position.Status == PositionStatus.Finished || position.Status == PositionStatus.Flipped)
|
||||
{
|
||||
if (position.StopLoss?.Status == TradeStatus.Filled)
|
||||
{
|
||||
closingVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage;
|
||||
}
|
||||
|
||||
if (position.TakeProfit1?.Status == TradeStatus.Filled)
|
||||
{
|
||||
closingVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage;
|
||||
}
|
||||
|
||||
if (position.TakeProfit2?.Status == TradeStatus.Filled)
|
||||
{
|
||||
closingVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage;
|
||||
}
|
||||
}
|
||||
|
||||
var positionVolume = openVolume + closingVolume;
|
||||
|
||||
// Track total volume from ALL positions (this is the true cumulative volume)
|
||||
totalVolumeFromAllPositions += positionVolume;
|
||||
|
||||
// For cumulative volume: only add volume from positions created AFTER last snapshot
|
||||
// For volume breakdown: include all positions
|
||||
if (position.Date.Date > lastSnapshotDate)
|
||||
{
|
||||
newVolume += positionVolume;
|
||||
_logger.LogInformation(
|
||||
"Position {PositionId} created after last snapshot ({PositionDate} > {LastSnapshotDate}), adding volume: {Volume}",
|
||||
position.Identifier, position.Date.Date, lastSnapshotDate, positionVolume);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Position {PositionId} created before/on last snapshot ({PositionDate} <= {LastSnapshotDate}), skipping volume: {Volume}",
|
||||
position.Identifier, position.Date.Date, lastSnapshotDate, positionVolume);
|
||||
}
|
||||
|
||||
// Calculate breakdown metrics from ALL positions (for current state)
|
||||
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]++;
|
||||
|
||||
// Calculate fees and PnL for all positions
|
||||
totalFees += position.CalculateTotalFees();
|
||||
totalPnL += position.ProfitAndLoss?.Realized ?? 0;
|
||||
|
||||
// Count all positions
|
||||
totalPositionCount++;
|
||||
|
||||
// Position count breakdown by direction - only count open positions
|
||||
if (position.IsOpen())
|
||||
{
|
||||
var openingVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage;
|
||||
totalOpenInterest += openingVolume;
|
||||
|
||||
if (!_state.State.PositionCountByDirection.ContainsKey(direction))
|
||||
{
|
||||
_state.State.PositionCountByDirection[direction] = 0;
|
||||
}
|
||||
|
||||
_state.State.PositionCountByDirection[direction]++;
|
||||
}
|
||||
}
|
||||
|
||||
// CUMULATIVE volume: baseline + new volume since last snapshot
|
||||
var updatedCumulativeVolume = cumulativeVolume + newVolume;
|
||||
|
||||
_logger.LogWarning(
|
||||
"Volume calculation DEBUG: TotalFromAllPositions={TotalFromAll}, LastSnapshotVolume={LastSnapshot}, NewVolumeSinceSnapshot={NewVolume}, CalculatedCumulative={Cumulative}, CurrentState={CurrentState}",
|
||||
totalVolumeFromAllPositions, cumulativeVolume, newVolume, updatedCumulativeVolume,
|
||||
_state.State.TotalPlatformVolume);
|
||||
|
||||
// ISSUE FIX: The correct cumulative volume should be calculated from ALL positions
|
||||
// not just by adding new positions to the last snapshot, because:
|
||||
// 1. Positions might be closed/updated which affects their volume
|
||||
// 2. The daily snapshot might be outdated
|
||||
// We should use the total from all positions, but never let it decrease
|
||||
var correctCumulativeVolume = totalVolumeFromAllPositions;
|
||||
|
||||
// Ensure volume never decreases
|
||||
if (correctCumulativeVolume < _state.State.TotalPlatformVolume)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Calculated volume ({Calculated}) is less than current volume ({Current}). Keeping current value to maintain cumulative nature.",
|
||||
correctCumulativeVolume, _state.State.TotalPlatformVolume);
|
||||
correctCumulativeVolume = _state.State.TotalPlatformVolume;
|
||||
}
|
||||
|
||||
_state.State.TotalPlatformVolume = correctCumulativeVolume;
|
||||
|
||||
_logger.LogInformation("Final volume set to: {FinalVolume}", correctCumulativeVolume);
|
||||
_state.State.TotalPlatformFees = totalFees;
|
||||
_state.State.TotalPlatformPnL = totalPnL;
|
||||
_state.State.NetPnL = totalPnL - totalFees; // Calculate NetPnL
|
||||
_state.State.OpenInterest = totalOpenInterest;
|
||||
_state.State.TotalLifetimePositionCount = totalPositionCount;
|
||||
_state.State.HasPendingChanges = false;
|
||||
|
||||
_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));
|
||||
_logger.LogInformation("Platform summary data refreshed - Volume: {Volume}, PnL: {PnL}, Positions: {Positions}",
|
||||
metrics.TotalPlatformVolume, metrics.TotalPlatformPnL, metrics.TotalLifetimePositionCount);
|
||||
|
||||
_state.State.LastUpdated = DateTime.UtcNow;
|
||||
await RefreshAgentCountAsync();
|
||||
await _state.WriteStateAsync();
|
||||
|
||||
_logger.LogInformation("Platform summary data refreshed successfully - Cumulative volume: {Volume}",
|
||||
updatedCumulativeVolume);
|
||||
metrics.TotalPlatformVolume);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -642,145 +510,34 @@ public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
|
||||
private async Task<DailySnapshot> CalculateDailySnapshotFromPositionsAsync(List<Position> positions,
|
||||
DateTime targetDate)
|
||||
{
|
||||
// Calculate CUMULATIVE metrics: sum of ALL volume/PnL from positions with activity on or before target date
|
||||
var totalVolume = 0m;
|
||||
var totalFees = 0m;
|
||||
var totalPnL = 0m;
|
||||
var currentOpenInterest = 0m;
|
||||
var totalPositionCount = 0;
|
||||
|
||||
_logger.LogInformation("Calculating snapshot for {TargetDate} with {PositionCount} positions",
|
||||
targetDate, positions.Count);
|
||||
|
||||
foreach (var position in positions)
|
||||
{
|
||||
// Only include positions that were OPENED on or before the target date
|
||||
if (position.Date.Date > targetDate)
|
||||
{
|
||||
_logger.LogDebug("Position {PositionId} opened after target date ({PositionDate} > {TargetDate}), skipping",
|
||||
position.Identifier, position.Date.Date, targetDate);
|
||||
continue;
|
||||
}
|
||||
// Filter positions to only include those opened on or before the target date
|
||||
var filteredPositions = positions.Where(p => p.Date.Date <= targetDate).ToList();
|
||||
|
||||
// Count this position in the cumulative count
|
||||
totalPositionCount++;
|
||||
|
||||
// Calculate volume for this position using SQL logic:
|
||||
// OpenVolume = Price * Quantity * Leverage (from opening trade)
|
||||
var openVolume = position.Open.Price * position.Open.Quantity * position.Open.Leverage;
|
||||
var closingVolume = 0m;
|
||||
|
||||
// ClosingVolume = Sum of all filled closing trades (SL, TP1, TP2) that happened on or before target date
|
||||
// IMPORTANT: Only count closing volume from trades that were filled on or BEFORE the target date
|
||||
if (position.Status == PositionStatus.Finished || position.Status == PositionStatus.Flipped)
|
||||
{
|
||||
// Stop Loss volume (if filled and on or before target date)
|
||||
if (position.StopLoss?.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate)
|
||||
{
|
||||
closingVolume += position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage;
|
||||
_logger.LogDebug("Position {PositionId}: Including SL closing volume {Volume} (filled on {Date})",
|
||||
position.Identifier, position.StopLoss.Price * position.StopLoss.Quantity * position.StopLoss.Leverage, position.StopLoss.Date.Date);
|
||||
}
|
||||
else if (position.StopLoss?.Status == TradeStatus.Filled)
|
||||
{
|
||||
_logger.LogDebug("Position {PositionId}: Excluding SL closing volume (filled on {Date} > target {TargetDate})",
|
||||
position.Identifier, position.StopLoss.Date.Date, targetDate);
|
||||
}
|
||||
|
||||
// Take Profit 1 volume (if filled and on or before target date)
|
||||
if (position.TakeProfit1?.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate)
|
||||
{
|
||||
closingVolume += position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage;
|
||||
_logger.LogDebug("Position {PositionId}: Including TP1 closing volume {Volume} (filled on {Date})",
|
||||
position.Identifier, position.TakeProfit1.Price * position.TakeProfit1.Quantity * position.TakeProfit1.Leverage, position.TakeProfit1.Date.Date);
|
||||
}
|
||||
else if (position.TakeProfit1?.Status == TradeStatus.Filled)
|
||||
{
|
||||
_logger.LogDebug("Position {PositionId}: Excluding TP1 closing volume (filled on {Date} > target {TargetDate})",
|
||||
position.Identifier, position.TakeProfit1.Date.Date, targetDate);
|
||||
}
|
||||
|
||||
// Take Profit 2 volume (if filled and on or before target date)
|
||||
if (position.TakeProfit2?.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate)
|
||||
{
|
||||
closingVolume += position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage;
|
||||
_logger.LogDebug("Position {PositionId}: Including TP2 closing volume {Volume} (filled on {Date})",
|
||||
position.Identifier, position.TakeProfit2.Price * position.TakeProfit2.Quantity * position.TakeProfit2.Leverage, position.TakeProfit2.Date.Date);
|
||||
}
|
||||
else if (position.TakeProfit2?.Status == TradeStatus.Filled)
|
||||
{
|
||||
_logger.LogDebug("Position {PositionId}: Excluding TP2 closing volume (filled on {Date} > target {TargetDate})",
|
||||
position.Identifier, position.TakeProfit2.Date.Date, targetDate);
|
||||
}
|
||||
}
|
||||
// For positions that are still open, no closing volume yet
|
||||
else if (position.IsOpen())
|
||||
{
|
||||
_logger.LogDebug("Position {PositionId}: Still open, no closing volume", position.Identifier);
|
||||
}
|
||||
|
||||
// Total volume for this position = opening + closing (only what happened by target date)
|
||||
var positionVolume = openVolume + closingVolume;
|
||||
totalVolume += positionVolume;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Position {PositionId} (opened {OpenDate}): OpenVolume={OpenVol}, ClosingVolume={CloseVol}, Total={Total}",
|
||||
position.Identifier, position.Date.Date, openVolume, closingVolume, positionVolume);
|
||||
|
||||
// Calculate open interest (only for positions that are still open on target date)
|
||||
if (position.IsOpen())
|
||||
{
|
||||
currentOpenInterest += openVolume;
|
||||
}
|
||||
else if (position.Status == PositionStatus.Finished || position.Status == PositionStatus.Flipped)
|
||||
{
|
||||
// Check if position was still open on the target date (closed after target date)
|
||||
var closedAfterTargetDate =
|
||||
(position.StopLoss?.Status == TradeStatus.Filled && position.StopLoss.Date.Date > targetDate) ||
|
||||
(position.TakeProfit1?.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date > targetDate) ||
|
||||
(position.TakeProfit2?.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date > targetDate);
|
||||
|
||||
if (closedAfterTargetDate)
|
||||
{
|
||||
currentOpenInterest += openVolume;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate fees and PnL for FINISHED positions (only if closed on or before target date)
|
||||
if (position.Status == PositionStatus.Finished || position.Status == PositionStatus.Flipped)
|
||||
{
|
||||
var wasClosedByTargetDate =
|
||||
(position.StopLoss?.Status == TradeStatus.Filled && position.StopLoss.Date.Date <= targetDate) ||
|
||||
(position.TakeProfit1?.Status == TradeStatus.Filled && position.TakeProfit1.Date.Date <= targetDate) ||
|
||||
(position.TakeProfit2?.Status == TradeStatus.Filled && position.TakeProfit2.Date.Date <= targetDate);
|
||||
|
||||
if (wasClosedByTargetDate)
|
||||
{
|
||||
totalFees += position.CalculateTotalFees();
|
||||
totalPnL += position.ProfitAndLoss?.Realized ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use TradingBox to calculate metrics for filtered positions
|
||||
var metrics = TradingBox.CalculatePlatformSummaryMetrics(filteredPositions);
|
||||
|
||||
// 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 CUMULATIVE snapshot for {TargetDate}: CumVolume={TotalVolume}, OpenInterest={OpenInterest}, CumPositionCount={TotalPositionCount}, Fees={Fees}, PnL={PnL}",
|
||||
targetDate, totalVolume, currentOpenInterest, totalPositionCount, totalFees, totalPnL);
|
||||
"Calculated CUMULATIVE snapshot for {TargetDate}: Volume={TotalVolume}, OpenInterest={OpenInterest}, PositionCount={TotalPositionCount}, Fees={Fees}, PnL={PnL}",
|
||||
targetDate, metrics.TotalPlatformVolume, metrics.OpenInterest, metrics.TotalLifetimePositionCount, metrics.TotalPlatformFees, metrics.TotalPlatformPnL);
|
||||
|
||||
return new DailySnapshot
|
||||
{
|
||||
Date = targetDate,
|
||||
TotalAgents = totalAgents,
|
||||
TotalStrategies = totalStrategies,
|
||||
TotalVolume = totalVolume,
|
||||
TotalPnL = totalPnL,
|
||||
NetPnL = totalPnL - totalFees,
|
||||
TotalOpenInterest = currentOpenInterest,
|
||||
TotalLifetimePositionCount = totalPositionCount,
|
||||
TotalPlatformFees = (int)totalFees,
|
||||
TotalVolume = metrics.TotalPlatformVolume,
|
||||
TotalPnL = metrics.TotalPlatformPnL,
|
||||
NetPnL = metrics.NetPnL,
|
||||
TotalOpenInterest = metrics.OpenInterest,
|
||||
TotalLifetimePositionCount = metrics.TotalLifetimePositionCount,
|
||||
TotalPlatformFees = (int)metrics.TotalPlatformFees,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user