Add platform grain
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
using Managing.Api.Models.Requests;
|
using Managing.Api.Extensions;
|
||||||
|
using Managing.Api.Models.Requests;
|
||||||
using Managing.Api.Models.Responses;
|
using Managing.Api.Models.Responses;
|
||||||
|
using Managing.Application.Abstractions.Grains;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.Hubs;
|
using Managing.Application.Hubs;
|
||||||
using Managing.Application.ManageBot.Commands;
|
using Managing.Application.ManageBot.Commands;
|
||||||
@@ -36,6 +38,7 @@ public class DataController : ControllerBase
|
|||||||
private readonly IHubContext<CandleHub> _hubContext;
|
private readonly IHubContext<CandleHub> _hubContext;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly ITradingService _tradingService;
|
private readonly ITradingService _tradingService;
|
||||||
|
private readonly IGrainFactory _grainFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DataController"/> class.
|
/// Initializes a new instance of the <see cref="DataController"/> class.
|
||||||
@@ -47,6 +50,7 @@ public class DataController : ControllerBase
|
|||||||
/// <param name="hubContext">SignalR hub context for real-time communication.</param>
|
/// <param name="hubContext">SignalR hub context for real-time communication.</param>
|
||||||
/// <param name="mediator">Mediator for handling commands and queries.</param>
|
/// <param name="mediator">Mediator for handling commands and queries.</param>
|
||||||
/// <param name="tradingService">Service for trading operations.</param>
|
/// <param name="tradingService">Service for trading operations.</param>
|
||||||
|
/// <param name="grainFactory">Orleans grain factory for accessing grains.</param>
|
||||||
public DataController(
|
public DataController(
|
||||||
IExchangeService exchangeService,
|
IExchangeService exchangeService,
|
||||||
IAccountService accountService,
|
IAccountService accountService,
|
||||||
@@ -55,7 +59,8 @@ public class DataController : ControllerBase
|
|||||||
IAgentService agentService,
|
IAgentService agentService,
|
||||||
IHubContext<CandleHub> hubContext,
|
IHubContext<CandleHub> hubContext,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
ITradingService tradingService)
|
ITradingService tradingService,
|
||||||
|
IGrainFactory grainFactory)
|
||||||
{
|
{
|
||||||
_exchangeService = exchangeService;
|
_exchangeService = exchangeService;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
@@ -65,6 +70,7 @@ public class DataController : ControllerBase
|
|||||||
_hubContext = hubContext;
|
_hubContext = hubContext;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_tradingService = tradingService;
|
_tradingService = tradingService;
|
||||||
|
_grainFactory = grainFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -458,68 +464,31 @@ public class DataController : ControllerBase
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a summary of platform activity across all agents (platform-level data only)
|
/// Retrieves a summary of platform activity across all agents (platform-level data only)
|
||||||
|
/// Uses Orleans grain for efficient caching and real-time updates
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A summary of platform activity without individual agent details</returns>
|
/// <returns>A summary of platform activity without individual agent details</returns>
|
||||||
[HttpGet("GetPlatformSummary")]
|
[HttpGet("GetPlatformSummary")]
|
||||||
public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary()
|
public async Task<ActionResult<PlatformSummaryViewModel>> GetPlatformSummary()
|
||||||
{
|
{
|
||||||
const string cacheKey = "PlatformSummary";
|
try
|
||||||
|
|
||||||
// Check if the platform summary is already cached
|
|
||||||
var cachedSummary = _cacheService.GetValue<PlatformSummaryViewModel>(cacheKey);
|
|
||||||
|
|
||||||
if (cachedSummary != null)
|
|
||||||
{
|
{
|
||||||
return Ok(cachedSummary);
|
// Get the platform summary grain
|
||||||
|
var platformSummaryGrain = _grainFactory.GetGrain<IPlatformSummaryGrain>("platform-summary");
|
||||||
|
|
||||||
|
// Get the platform summary from the grain (handles caching and real-time updates)
|
||||||
|
var abstractionsSummary = await platformSummaryGrain.GetPlatformSummaryAsync();
|
||||||
|
|
||||||
|
// Convert to API ViewModel
|
||||||
|
var summary = abstractionsSummary.ToApiViewModel();
|
||||||
|
|
||||||
|
return Ok(summary);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
// Get all agents and their strategies (without time filter)
|
|
||||||
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand());
|
|
||||||
|
|
||||||
// Create the platform summary
|
|
||||||
var summary = new PlatformSummaryViewModel
|
|
||||||
{
|
{
|
||||||
TotalAgents = agentsWithStrategies.Count,
|
// Log the error and return a fallback response
|
||||||
TotalActiveStrategies = agentsWithStrategies.Values.Sum(list => list.Count)
|
// In production, you might want to return cached data or partial data
|
||||||
};
|
return StatusCode(500, $"Error retrieving platform summary: {ex.Message}");
|
||||||
|
|
||||||
// Calculate total platform metrics
|
|
||||||
decimal totalPlatformPnL = 0;
|
|
||||||
decimal totalPlatformVolume = 0;
|
|
||||||
decimal totalPlatformVolumeLast24h = 0;
|
|
||||||
|
|
||||||
// Calculate totals from all agents
|
|
||||||
foreach (var agent in agentsWithStrategies)
|
|
||||||
{
|
|
||||||
var strategies = agent.Value;
|
|
||||||
|
|
||||||
if (strategies.Count == 0)
|
|
||||||
{
|
|
||||||
continue; // Skip agents with no strategies
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add this calculation into repository for better performance
|
|
||||||
|
|
||||||
var globalPnL = strategies.Sum(s => s.Pnl);
|
|
||||||
var globalVolume = strategies.Sum(s => s.Volume);
|
|
||||||
var globalVolumeLast24h = strategies.Sum(s => s.Volume);
|
|
||||||
|
|
||||||
// Calculate agent metrics for platform totals
|
|
||||||
// Add to platform totals
|
|
||||||
totalPlatformPnL += globalPnL;
|
|
||||||
totalPlatformVolume += globalVolume;
|
|
||||||
totalPlatformVolumeLast24h += globalVolumeLast24h;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the platform totals
|
|
||||||
summary.TotalPlatformPnL = totalPlatformPnL;
|
|
||||||
summary.TotalPlatformVolume = totalPlatformVolume;
|
|
||||||
summary.TotalPlatformVolumeLast24h = totalPlatformVolumeLast24h;
|
|
||||||
|
|
||||||
// Cache the results for 5 minutes
|
|
||||||
_cacheService.SaveValue(cacheKey, summary, TimeSpan.FromMinutes(5));
|
|
||||||
|
|
||||||
return Ok(summary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
40
src/Managing.Api/Extensions/PlatformSummaryExtensions.cs
Normal file
40
src/Managing.Api/Extensions/PlatformSummaryExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Managing.Api.Models.Responses;
|
||||||
|
using AbstractionsPlatformSummaryViewModel = Managing.Application.Abstractions.Models.PlatformSummaryViewModel;
|
||||||
|
|
||||||
|
namespace Managing.Api.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for converting between Platform Summary ViewModels
|
||||||
|
/// </summary>
|
||||||
|
public static class PlatformSummaryExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts from the Abstractions PlatformSummaryViewModel to the API PlatformSummaryViewModel
|
||||||
|
/// </summary>
|
||||||
|
public static PlatformSummaryViewModel ToApiViewModel(this AbstractionsPlatformSummaryViewModel abstractionsModel)
|
||||||
|
{
|
||||||
|
return new PlatformSummaryViewModel
|
||||||
|
{
|
||||||
|
TotalAgents = abstractionsModel.TotalAgents,
|
||||||
|
TotalActiveStrategies = abstractionsModel.TotalActiveStrategies,
|
||||||
|
TotalPlatformPnL = abstractionsModel.TotalPlatformPnL,
|
||||||
|
TotalPlatformVolume = abstractionsModel.TotalPlatformVolume,
|
||||||
|
TotalPlatformVolumeLast24h = abstractionsModel.TotalPlatformVolumeLast24h,
|
||||||
|
TotalOpenInterest = abstractionsModel.TotalOpenInterest,
|
||||||
|
TotalPositionCount = abstractionsModel.TotalPositionCount,
|
||||||
|
AgentsChange24h = abstractionsModel.AgentsChange24h,
|
||||||
|
StrategiesChange24h = abstractionsModel.StrategiesChange24h,
|
||||||
|
PnLChange24h = abstractionsModel.PnLChange24h,
|
||||||
|
VolumeChange24h = abstractionsModel.VolumeChange24h,
|
||||||
|
OpenInterestChange24h = abstractionsModel.OpenInterestChange24h,
|
||||||
|
PositionCountChange24h = abstractionsModel.PositionCountChange24h,
|
||||||
|
VolumeByAsset = abstractionsModel.VolumeByAsset,
|
||||||
|
PositionCountByAsset = abstractionsModel.PositionCountByAsset,
|
||||||
|
PositionCountByDirection = abstractionsModel.PositionCountByDirection.ToDictionary(
|
||||||
|
kvp => kvp.Key.ToString(),
|
||||||
|
kvp => kvp.Value),
|
||||||
|
LastUpdated = abstractionsModel.LastUpdated,
|
||||||
|
Last24HourSnapshot = abstractionsModel.Last24HourSnapshot
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,74 @@ namespace Managing.Api.Models.Responses
|
|||||||
/// Total volume traded across all agents in the last 24 hours in USD
|
/// Total volume traded across all agents in the last 24 hours in USD
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal TotalPlatformVolumeLast24h { get; set; }
|
public decimal TotalPlatformVolumeLast24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total open interest across all positions in USD
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalOpenInterest { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of open positions across all strategies
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPositionCount { get; set; }
|
||||||
|
|
||||||
|
// 24-hour changes
|
||||||
|
/// <summary>
|
||||||
|
/// Change in agent count over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public int AgentsChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in strategy count over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public int StrategiesChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in PnL over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public decimal PnLChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in volume over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public decimal VolumeChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in open interest over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public decimal OpenInterestChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in position count over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public int PositionCountChange24h { get; set; }
|
||||||
|
|
||||||
|
// Breakdowns
|
||||||
|
/// <summary>
|
||||||
|
/// Volume breakdown by asset/ticker
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, decimal> VolumeByAsset { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position count breakdown by asset/ticker
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, int> PositionCountByAsset { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position count breakdown by direction (Long/Short)
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, int> PositionCountByDirection { get; set; } = new();
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
/// <summary>
|
||||||
|
/// When the data was last updated
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastUpdated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the last 24-hour snapshot was taken
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Last24HourSnapshot { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
using Managing.Application.Abstractions.Models;
|
||||||
|
using Orleans;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Grains;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Grain interface for managing platform-wide summary metrics
|
||||||
|
/// </summary>
|
||||||
|
public interface IPlatformSummaryGrain : IGrainWithStringKey
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current platform summary data
|
||||||
|
/// </summary>
|
||||||
|
Task<PlatformSummaryViewModel> GetPlatformSummaryAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces a refresh of all platform data
|
||||||
|
/// </summary>
|
||||||
|
Task RefreshDataAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total volume traded across all strategies
|
||||||
|
/// </summary>
|
||||||
|
Task<decimal> GetTotalVolumeAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total PnL across all strategies
|
||||||
|
/// </summary>
|
||||||
|
Task<decimal> GetTotalPnLAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total open interest across all positions
|
||||||
|
/// </summary>
|
||||||
|
Task<decimal> GetTotalOpenInterest();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total number of open positions
|
||||||
|
/// </summary>
|
||||||
|
Task<int> GetTotalPositionCountAsync();
|
||||||
|
|
||||||
|
// Event handlers for immediate updates
|
||||||
|
Task OnStrategyDeployedAsync(StrategyDeployedEvent evt);
|
||||||
|
Task OnStrategyStoppedAsync(StrategyStoppedEvent evt);
|
||||||
|
Task OnPositionOpenedAsync(PositionOpenedEvent evt);
|
||||||
|
Task OnPositionClosedAsync(PositionClosedEvent evt);
|
||||||
|
Task OnTradeExecutedAsync(TradeExecutedEvent evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for platform metrics events
|
||||||
|
/// </summary>
|
||||||
|
public abstract class PlatformMetricsEvent
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when a new strategy is deployed
|
||||||
|
/// </summary>
|
||||||
|
public class StrategyDeployedEvent : PlatformMetricsEvent
|
||||||
|
{
|
||||||
|
public Guid StrategyId { get; set; }
|
||||||
|
public string AgentName { get; set; } = string.Empty;
|
||||||
|
public string StrategyName { get; set; } = string.Empty;
|
||||||
|
public decimal InitialVolume { get; set; }
|
||||||
|
public decimal InitialPnL { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when a strategy is stopped
|
||||||
|
/// </summary>
|
||||||
|
public class StrategyStoppedEvent : PlatformMetricsEvent
|
||||||
|
{
|
||||||
|
public Guid StrategyId { get; set; }
|
||||||
|
public string AgentName { get; set; } = string.Empty;
|
||||||
|
public string StrategyName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when a new position is opened
|
||||||
|
/// </summary>
|
||||||
|
public class PositionOpenedEvent : PlatformMetricsEvent
|
||||||
|
{
|
||||||
|
public Guid PositionId { get; set; }
|
||||||
|
public Guid StrategyId { get; set; }
|
||||||
|
public string Ticker { get; set; } = string.Empty;
|
||||||
|
public decimal Size { get; set; }
|
||||||
|
public decimal NotionalValue { get; set; }
|
||||||
|
public TradeDirection Direction { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when a position is closed
|
||||||
|
/// </summary>
|
||||||
|
public class PositionClosedEvent : PlatformMetricsEvent
|
||||||
|
{
|
||||||
|
public Guid PositionId { get; set; }
|
||||||
|
public Guid StrategyId { get; set; }
|
||||||
|
public string Ticker { get; set; } = string.Empty;
|
||||||
|
public decimal RealizedPnL { get; set; }
|
||||||
|
public decimal Volume { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when a trade is executed
|
||||||
|
/// </summary>
|
||||||
|
public class TradeExecutedEvent : PlatformMetricsEvent
|
||||||
|
{
|
||||||
|
public Guid TradeId { get; set; }
|
||||||
|
public Guid PositionId { get; set; }
|
||||||
|
public Guid StrategyId { get; set; }
|
||||||
|
public string Ticker { get; set; } = string.Empty;
|
||||||
|
public decimal Volume { get; set; }
|
||||||
|
public decimal PnL { get; set; }
|
||||||
|
public decimal Fee { get; set; }
|
||||||
|
public TradeDirection Direction { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Grains;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// State model for Platform Summary Grain
|
||||||
|
/// </summary>
|
||||||
|
public class PlatformSummaryGrainState
|
||||||
|
{
|
||||||
|
public DateTime LastUpdated { get; set; }
|
||||||
|
public DateTime LastSnapshot { get; set; }
|
||||||
|
public bool HasPendingChanges { get; set; }
|
||||||
|
|
||||||
|
// Current metrics
|
||||||
|
public int TotalAgents { get; set; }
|
||||||
|
public int TotalActiveStrategies { get; set; }
|
||||||
|
public decimal TotalPlatformPnL { get; set; }
|
||||||
|
public decimal TotalPlatformVolume { get; set; }
|
||||||
|
public decimal TotalOpenInterest { get; set; }
|
||||||
|
public int TotalPositionCount { get; set; }
|
||||||
|
|
||||||
|
// 24-hour ago values (for comparison)
|
||||||
|
public int TotalAgents24hAgo { get; set; }
|
||||||
|
public int TotalActiveStrategies24hAgo { get; set; }
|
||||||
|
public decimal TotalPlatformPnL24hAgo { get; set; }
|
||||||
|
public decimal TotalPlatformVolume24hAgo { get; set; }
|
||||||
|
public decimal TotalOpenInterest24hAgo { get; set; }
|
||||||
|
public int TotalPositionCount24hAgo { get; set; }
|
||||||
|
|
||||||
|
// Historical snapshots
|
||||||
|
public List<HourlySnapshot> HourlySnapshots { get; set; } = new();
|
||||||
|
public List<DailySnapshot> DailySnapshots { get; set; } = new();
|
||||||
|
|
||||||
|
// Volume breakdown by asset
|
||||||
|
public Dictionary<string, decimal> VolumeByAsset { get; set; } = new();
|
||||||
|
|
||||||
|
// Position count breakdown
|
||||||
|
public Dictionary<string, int> PositionCountByAsset { get; set; } = new();
|
||||||
|
public Dictionary<TradeDirection, int> PositionCountByDirection { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hourly snapshot of platform metrics
|
||||||
|
/// </summary>
|
||||||
|
public class HourlySnapshot
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public int TotalAgents { get; set; }
|
||||||
|
public int TotalStrategies { get; set; }
|
||||||
|
public decimal TotalVolume { get; set; }
|
||||||
|
public decimal TotalPnL { get; set; }
|
||||||
|
public decimal TotalOpenInterest { get; set; }
|
||||||
|
public int TotalPositionCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Daily snapshot of platform metrics
|
||||||
|
/// </summary>
|
||||||
|
public class DailySnapshot
|
||||||
|
{
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public int TotalAgents { get; set; }
|
||||||
|
public int TotalStrategies { get; set; }
|
||||||
|
public decimal TotalVolume { get; set; }
|
||||||
|
public decimal TotalPnL { get; set; }
|
||||||
|
public decimal TotalOpenInterest { get; set; }
|
||||||
|
public int TotalPositionCount { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Platform-wide statistics without individual agent details
|
||||||
|
/// </summary>
|
||||||
|
public class PlatformSummaryViewModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of agents on the platform
|
||||||
|
/// </summary>
|
||||||
|
public int TotalAgents { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of active strategies across all agents
|
||||||
|
/// </summary>
|
||||||
|
public int TotalActiveStrategies { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total platform-wide profit and loss in USD
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalPlatformPnL { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total volume traded across all agents in USD
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalPlatformVolume { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total volume traded across all agents in the last 24 hours in USD
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalPlatformVolumeLast24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total open interest across all positions in USD
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalOpenInterest { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of open positions across all strategies
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPositionCount { get; set; }
|
||||||
|
|
||||||
|
// 24-hour changes
|
||||||
|
/// <summary>
|
||||||
|
/// Change in agent count over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public int AgentsChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in strategy count over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public int StrategiesChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in PnL over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public decimal PnLChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in volume over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public decimal VolumeChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in open interest over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public decimal OpenInterestChange24h { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change in position count over the last 24 hours
|
||||||
|
/// </summary>
|
||||||
|
public int PositionCountChange24h { get; set; }
|
||||||
|
|
||||||
|
// Breakdowns
|
||||||
|
/// <summary>
|
||||||
|
/// Volume breakdown by asset/ticker
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, decimal> VolumeByAsset { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position count breakdown by asset/ticker
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, int> PositionCountByAsset { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position count breakdown by direction (Long/Short)
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<TradeDirection, int> PositionCountByDirection { get; set; } = new();
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
/// <summary>
|
||||||
|
/// When the data was last updated
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastUpdated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the last 24-hour snapshot was taken
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Last24HourSnapshot { get; set; }
|
||||||
|
}
|
||||||
@@ -633,6 +633,12 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
|||||||
.Sum(p => p.Open.Quantity * p.Open.Price);
|
.Sum(p => p.Open.Quantity * p.Open.Price);
|
||||||
var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0;
|
var roi = totalInvestment > 0 ? (pnl / totalInvestment) * 100 : 0;
|
||||||
|
|
||||||
|
// Calculate long and short position counts
|
||||||
|
var longPositionCount = _tradingBot.Positions.Values
|
||||||
|
.Count(p => p.OriginDirection == TradeDirection.Long);
|
||||||
|
var shortPositionCount = _tradingBot.Positions.Values
|
||||||
|
.Count(p => p.OriginDirection == TradeDirection.Short);
|
||||||
|
|
||||||
// Create complete Bot object with all statistics
|
// Create complete Bot object with all statistics
|
||||||
bot = new Bot
|
bot = new Bot
|
||||||
{
|
{
|
||||||
@@ -648,7 +654,9 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
|||||||
Pnl = pnl,
|
Pnl = pnl,
|
||||||
Roi = roi,
|
Roi = roi,
|
||||||
Volume = volume,
|
Volume = volume,
|
||||||
Fees = fees
|
Fees = fees,
|
||||||
|
LongPositionCount = longPositionCount,
|
||||||
|
ShortPositionCount = shortPositionCount
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,8 +667,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
|
|||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}",
|
"Successfully saved bot statistics for bot {BotId}: Wins={Wins}, Losses={Losses}, PnL={PnL}, ROI={ROI}%, Volume={Volume}, Fees={Fees}, Long={LongPositions}, Short={ShortPositions}",
|
||||||
_state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees);
|
_state.State.Identifier, bot.TradeWins, bot.TradeLosses, bot.Pnl, bot.Roi, bot.Volume, bot.Fees, bot.LongPositionCount, bot.ShortPositionCount);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
416
src/Managing.Application/Grains/PlatformSummaryGrain.cs
Normal file
416
src/Managing.Application/Grains/PlatformSummaryGrain.cs
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
using Managing.Application.Abstractions;
|
||||||
|
using Managing.Application.Abstractions.Grains;
|
||||||
|
using Managing.Application.Abstractions.Models;
|
||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Application.Grains;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Grain for managing platform-wide summary metrics with real-time updates and periodic snapshots
|
||||||
|
/// </summary>
|
||||||
|
public class PlatformSummaryGrain : Grain, IPlatformSummaryGrain, IRemindable
|
||||||
|
{
|
||||||
|
private readonly IPersistentState<PlatformSummaryGrainState> _state;
|
||||||
|
private readonly IBotService _botService;
|
||||||
|
private readonly IAgentService _agentService;
|
||||||
|
private readonly ITradingService _tradingService;
|
||||||
|
private readonly ILogger<PlatformSummaryGrain> _logger;
|
||||||
|
|
||||||
|
private const string _hourlySnapshotReminder = "HourlySnapshot";
|
||||||
|
private const string _dailySnapshotReminder = "DailySnapshot";
|
||||||
|
|
||||||
|
public PlatformSummaryGrain(
|
||||||
|
[PersistentState("platform-summary-state", "platform-summary-store")]
|
||||||
|
IPersistentState<PlatformSummaryGrainState> state,
|
||||||
|
IBotService botService,
|
||||||
|
IAgentService agentService,
|
||||||
|
ITradingService tradingService,
|
||||||
|
ILogger<PlatformSummaryGrain> logger)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_botService = botService;
|
||||||
|
_agentService = agentService;
|
||||||
|
_tradingService = tradingService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnActivateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Platform Summary Grain activated");
|
||||||
|
|
||||||
|
// Set up reminders for periodic snapshots
|
||||||
|
await this.RegisterOrUpdateReminder(_hourlySnapshotReminder,
|
||||||
|
TimeSpan.FromHours(1), TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var nextMidnight = now.Date.AddDays(1);
|
||||||
|
var timeUntilMidnight = nextMidnight - now;
|
||||||
|
await this.RegisterOrUpdateReminder(_dailySnapshotReminder,
|
||||||
|
timeUntilMidnight, TimeSpan.FromDays(1));
|
||||||
|
|
||||||
|
// Initial data load if state is empty
|
||||||
|
if (_state.State.LastUpdated == default)
|
||||||
|
{
|
||||||
|
await RefreshDataAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PlatformSummaryViewModel> GetPlatformSummaryAsync()
|
||||||
|
{
|
||||||
|
// If data is stale or has pending changes, refresh it
|
||||||
|
if (IsDataStale() || _state.State.HasPendingChanges)
|
||||||
|
{
|
||||||
|
await RefreshDataAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapToViewModel(_state.State);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshDataAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Refreshing platform summary data");
|
||||||
|
|
||||||
|
// Get all data in parallel for better performance
|
||||||
|
var agentsTask = _agentService.GetAllAgentSummaries();
|
||||||
|
var strategiesTask = _botService.GetBotsAsync();
|
||||||
|
|
||||||
|
await Task.WhenAll(agentsTask, strategiesTask);
|
||||||
|
|
||||||
|
var agents = await agentsTask;
|
||||||
|
var strategies = await strategiesTask;
|
||||||
|
|
||||||
|
// Calculate totals
|
||||||
|
var totalAgents = agents.Count();
|
||||||
|
var totalActiveStrategies = strategies.Count(s => s.Status == BotStatus.Running);
|
||||||
|
|
||||||
|
// Calculate volume and PnL from strategies
|
||||||
|
var totalVolume = strategies.Sum(s => s.Volume);
|
||||||
|
var totalPnL = strategies.Sum(s => s.Pnl);
|
||||||
|
|
||||||
|
// Calculate real open interest and position count from actual positions
|
||||||
|
var (totalOpenInterest, totalPositionCount) = await CalculatePositionMetricsAsync();
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
_state.State.TotalAgents = totalAgents;
|
||||||
|
_state.State.TotalActiveStrategies = totalActiveStrategies;
|
||||||
|
_state.State.TotalPlatformVolume = totalVolume;
|
||||||
|
_state.State.TotalPlatformPnL = totalPnL;
|
||||||
|
_state.State.TotalOpenInterest = totalOpenInterest;
|
||||||
|
_state.State.TotalPositionCount = totalPositionCount;
|
||||||
|
_state.State.LastUpdated = DateTime.UtcNow;
|
||||||
|
_state.State.HasPendingChanges = false;
|
||||||
|
|
||||||
|
// Update volume breakdown by asset
|
||||||
|
await UpdateVolumeBreakdownAsync(strategies);
|
||||||
|
|
||||||
|
// Update position count breakdown
|
||||||
|
await UpdatePositionCountBreakdownAsync(strategies);
|
||||||
|
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Platform summary data refreshed successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error refreshing platform summary data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateVolumeBreakdownAsync(IEnumerable<Bot> strategies)
|
||||||
|
{
|
||||||
|
_state.State.VolumeByAsset.Clear();
|
||||||
|
|
||||||
|
// Group strategies by ticker and sum their volumes
|
||||||
|
var volumeByAsset = strategies
|
||||||
|
.Where(s => s.Volume > 0)
|
||||||
|
.GroupBy(s => s.Ticker.ToString())
|
||||||
|
.ToDictionary(g => g.Key, g => g.Sum(s => s.Volume));
|
||||||
|
|
||||||
|
foreach (var kvp in volumeByAsset)
|
||||||
|
{
|
||||||
|
_state.State.VolumeByAsset[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Updated volume breakdown: {AssetCount} assets with total volume {TotalVolume}",
|
||||||
|
volumeByAsset.Count, volumeByAsset.Values.Sum());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdatePositionCountBreakdownAsync(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.ToString())
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.State.PositionCountByDirection[TradeDirection.Long] = totalLongPositions;
|
||||||
|
_state.State.PositionCountByDirection[TradeDirection.Short] = totalShortPositions;
|
||||||
|
|
||||||
|
_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
|
||||||
|
var openPositions = await _tradingService.GetBrokerPositions(null);
|
||||||
|
|
||||||
|
if (openPositions?.Any() == true)
|
||||||
|
{
|
||||||
|
var positionCount = openPositions.Count();
|
||||||
|
|
||||||
|
// Calculate open interest as the sum of position notional values
|
||||||
|
// Open interest = sum of (position size * price) for all open positions
|
||||||
|
var openInterest = openPositions
|
||||||
|
.Where(p => p.Open?.Price > 0 && p.Open?.Quantity > 0)
|
||||||
|
.Sum(p => p.Open.Price * p.Open.Quantity);
|
||||||
|
|
||||||
|
_logger.LogDebug("Calculated position metrics: {PositionCount} positions, {OpenInterest} open interest",
|
||||||
|
positionCount, openInterest);
|
||||||
|
|
||||||
|
return (openInterest, positionCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No open positions found for metrics calculation");
|
||||||
|
return (0m, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to calculate position metrics, returning zero values");
|
||||||
|
return (0m, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<decimal> GetTotalVolumeAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(_state.State.TotalPlatformVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<decimal> GetTotalPnLAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(_state.State.TotalPlatformPnL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<decimal> GetTotalOpenInterestAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(_state.State.TotalOpenInterest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> GetTotalPositionCountAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(_state.State.TotalPositionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers for immediate updates
|
||||||
|
public async Task OnStrategyDeployedAsync(StrategyDeployedEvent evt)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Strategy deployed: {StrategyId} - {StrategyName}", evt.StrategyId, evt.StrategyName);
|
||||||
|
|
||||||
|
_state.State.TotalActiveStrategies++;
|
||||||
|
_state.State.HasPendingChanges = true;
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnStrategyStoppedAsync(StrategyStoppedEvent evt)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Strategy stopped: {StrategyId} - {StrategyName}", evt.StrategyId, evt.StrategyName);
|
||||||
|
|
||||||
|
_state.State.TotalActiveStrategies--;
|
||||||
|
_state.State.HasPendingChanges = true;
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnPositionOpenedAsync(PositionOpenedEvent evt)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Position opened: {PositionId} for {Ticker}", evt.PositionId, evt.Ticker);
|
||||||
|
|
||||||
|
_state.State.TotalPositionCount++;
|
||||||
|
_state.State.TotalOpenInterest += evt.NotionalValue;
|
||||||
|
_state.State.HasPendingChanges = true;
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnPositionClosedAsync(PositionClosedEvent evt)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Position closed: {PositionId} for {Ticker} with PnL: {PnL}",
|
||||||
|
evt.PositionId, evt.Ticker, evt.RealizedPnL);
|
||||||
|
|
||||||
|
_state.State.TotalPositionCount--;
|
||||||
|
_state.State.TotalPlatformVolume += evt.Volume;
|
||||||
|
_state.State.TotalPlatformPnL += evt.RealizedPnL;
|
||||||
|
|
||||||
|
// Update volume by asset
|
||||||
|
var asset = evt.Ticker;
|
||||||
|
if (!_state.State.VolumeByAsset.ContainsKey(asset))
|
||||||
|
{
|
||||||
|
_state.State.VolumeByAsset[asset] = 0;
|
||||||
|
}
|
||||||
|
_state.State.VolumeByAsset[asset] += evt.Volume;
|
||||||
|
|
||||||
|
_state.State.HasPendingChanges = true;
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnTradeExecutedAsync(TradeExecutedEvent evt)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Trade executed: {TradeId} for {Ticker} with volume: {Volume}",
|
||||||
|
evt.TradeId, evt.Ticker, evt.Volume);
|
||||||
|
|
||||||
|
_state.State.TotalPlatformVolume += evt.Volume;
|
||||||
|
_state.State.TotalPlatformPnL += evt.PnL;
|
||||||
|
|
||||||
|
// Update volume by asset
|
||||||
|
var asset = evt.Ticker;
|
||||||
|
if (!_state.State.VolumeByAsset.ContainsKey(asset))
|
||||||
|
{
|
||||||
|
_state.State.VolumeByAsset[asset] = 0;
|
||||||
|
}
|
||||||
|
_state.State.VolumeByAsset[asset] += evt.Volume;
|
||||||
|
|
||||||
|
_state.State.HasPendingChanges = true;
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reminder handlers for periodic snapshots
|
||||||
|
public async Task ReceiveReminder(string reminderName, TickStatus status)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Reminder received: {ReminderName}", reminderName);
|
||||||
|
|
||||||
|
switch (reminderName)
|
||||||
|
{
|
||||||
|
case _hourlySnapshotReminder:
|
||||||
|
await TakeHourlySnapshotAsync();
|
||||||
|
break;
|
||||||
|
case _dailySnapshotReminder:
|
||||||
|
await TakeDailySnapshotAsync();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TakeHourlySnapshotAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Taking hourly snapshot");
|
||||||
|
|
||||||
|
var snapshot = new HourlySnapshot
|
||||||
|
{
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
TotalAgents = _state.State.TotalAgents,
|
||||||
|
TotalStrategies = _state.State.TotalActiveStrategies,
|
||||||
|
TotalVolume = _state.State.TotalPlatformVolume,
|
||||||
|
TotalPnL = _state.State.TotalPlatformPnL,
|
||||||
|
TotalOpenInterest = _state.State.TotalOpenInterest,
|
||||||
|
TotalPositionCount = _state.State.TotalPositionCount
|
||||||
|
};
|
||||||
|
|
||||||
|
_state.State.HourlySnapshots.Add(snapshot);
|
||||||
|
|
||||||
|
// Keep only last 24 hours
|
||||||
|
var cutoff = DateTime.UtcNow.AddHours(-24);
|
||||||
|
_state.State.HourlySnapshots.RemoveAll(s => s.Timestamp < cutoff);
|
||||||
|
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TakeDailySnapshotAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Taking daily snapshot");
|
||||||
|
|
||||||
|
// Store 24-hour ago values for comparison
|
||||||
|
_state.State.TotalAgents24hAgo = _state.State.TotalAgents;
|
||||||
|
_state.State.TotalActiveStrategies24hAgo = _state.State.TotalActiveStrategies;
|
||||||
|
_state.State.TotalPlatformPnL24hAgo = _state.State.TotalPlatformPnL;
|
||||||
|
_state.State.TotalPlatformVolume24hAgo = _state.State.TotalPlatformVolume;
|
||||||
|
_state.State.TotalOpenInterest24hAgo = _state.State.TotalOpenInterest;
|
||||||
|
_state.State.TotalPositionCount24hAgo = _state.State.TotalPositionCount;
|
||||||
|
|
||||||
|
// Add daily snapshot
|
||||||
|
var dailySnapshot = new DailySnapshot
|
||||||
|
{
|
||||||
|
Date = DateTime.UtcNow.Date,
|
||||||
|
TotalAgents = _state.State.TotalAgents,
|
||||||
|
TotalStrategies = _state.State.TotalActiveStrategies,
|
||||||
|
TotalVolume = _state.State.TotalPlatformVolume,
|
||||||
|
TotalPnL = _state.State.TotalPlatformPnL,
|
||||||
|
TotalOpenInterest = _state.State.TotalOpenInterest,
|
||||||
|
TotalPositionCount = _state.State.TotalPositionCount
|
||||||
|
};
|
||||||
|
|
||||||
|
_state.State.DailySnapshots.Add(dailySnapshot);
|
||||||
|
|
||||||
|
// Keep only last 30 days
|
||||||
|
var cutoff = DateTime.UtcNow.AddDays(-30);
|
||||||
|
_state.State.DailySnapshots.RemoveAll(s => s.Date < cutoff);
|
||||||
|
|
||||||
|
_state.State.LastSnapshot = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _state.WriteStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDataStale()
|
||||||
|
{
|
||||||
|
var timeSinceLastUpdate = DateTime.UtcNow - _state.State.LastUpdated;
|
||||||
|
return timeSinceLastUpdate > TimeSpan.FromMinutes(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlatformSummaryViewModel MapToViewModel(PlatformSummaryGrainState state)
|
||||||
|
{
|
||||||
|
return new PlatformSummaryViewModel
|
||||||
|
{
|
||||||
|
TotalAgents = state.TotalAgents,
|
||||||
|
TotalActiveStrategies = state.TotalActiveStrategies,
|
||||||
|
TotalPlatformPnL = state.TotalPlatformPnL,
|
||||||
|
TotalPlatformVolume = state.TotalPlatformVolume,
|
||||||
|
TotalPlatformVolumeLast24h = state.TotalPlatformVolume - state.TotalPlatformVolume24hAgo,
|
||||||
|
TotalOpenInterest = state.TotalOpenInterest,
|
||||||
|
TotalPositionCount = state.TotalPositionCount,
|
||||||
|
|
||||||
|
// 24-hour changes
|
||||||
|
AgentsChange24h = state.TotalAgents - state.TotalAgents24hAgo,
|
||||||
|
StrategiesChange24h = state.TotalActiveStrategies - state.TotalActiveStrategies24hAgo,
|
||||||
|
PnLChange24h = state.TotalPlatformPnL - state.TotalPlatformPnL24hAgo,
|
||||||
|
VolumeChange24h = state.TotalPlatformVolume - state.TotalPlatformVolume24hAgo,
|
||||||
|
OpenInterestChange24h = state.TotalOpenInterest - state.TotalOpenInterest24hAgo,
|
||||||
|
PositionCountChange24h = state.TotalPositionCount - state.TotalPositionCount24hAgo,
|
||||||
|
|
||||||
|
// Breakdowns
|
||||||
|
VolumeByAsset = state.VolumeByAsset,
|
||||||
|
PositionCountByAsset = state.PositionCountByAsset,
|
||||||
|
PositionCountByDirection = state.PositionCountByDirection,
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
LastUpdated = state.LastUpdated,
|
||||||
|
Last24HourSnapshot = state.LastSnapshot
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ namespace Managing.Domain.Bots
|
|||||||
public decimal Roi { get; set; }
|
public decimal Roi { get; set; }
|
||||||
public decimal Volume { get; set; }
|
public decimal Volume { get; set; }
|
||||||
public decimal Fees { get; set; }
|
public decimal Fees { get; set; }
|
||||||
|
public int LongPositionCount { get; set; }
|
||||||
|
public int ShortPositionCount { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1428
src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.Designer.cs
generated
Normal file
1428
src/Managing.Infrastructure.Database/Migrations/20250814123925_AddBotPositionCounts.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Managing.Infrastructure.Databases.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBotPositionCounts : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "LongPositionCount",
|
||||||
|
table: "Bots",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "ShortPositionCount",
|
||||||
|
table: "Bots",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LongPositionCount",
|
||||||
|
table: "Bots");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ShortPositionCount",
|
||||||
|
table: "Bots");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,4 +27,6 @@ public class BotEntity
|
|||||||
public decimal Roi { get; set; }
|
public decimal Roi { get; set; }
|
||||||
public decimal Volume { get; set; }
|
public decimal Volume { get; set; }
|
||||||
public decimal Fees { get; set; }
|
public decimal Fees { get; set; }
|
||||||
|
public int LongPositionCount { get; set; }
|
||||||
|
public int ShortPositionCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -688,7 +688,9 @@ public static class PostgreSqlMappers
|
|||||||
Pnl = entity.Pnl,
|
Pnl = entity.Pnl,
|
||||||
Roi = entity.Roi,
|
Roi = entity.Roi,
|
||||||
Volume = entity.Volume,
|
Volume = entity.Volume,
|
||||||
Fees = entity.Fees
|
Fees = entity.Fees,
|
||||||
|
LongPositionCount = entity.LongPositionCount,
|
||||||
|
ShortPositionCount = entity.ShortPositionCount
|
||||||
};
|
};
|
||||||
|
|
||||||
return bot;
|
return bot;
|
||||||
@@ -713,6 +715,8 @@ public static class PostgreSqlMappers
|
|||||||
Roi = bot.Roi,
|
Roi = bot.Roi,
|
||||||
Volume = bot.Volume,
|
Volume = bot.Volume,
|
||||||
Fees = bot.Fees,
|
Fees = bot.Fees,
|
||||||
|
LongPositionCount = bot.LongPositionCount,
|
||||||
|
ShortPositionCount = bot.ShortPositionCount,
|
||||||
UpdatedAt = DateTime.UtcNow
|
UpdatedAt = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user