236 lines
8.6 KiB
C#
236 lines
8.6 KiB
C#
using Managing.Application.Abstractions;
|
|
using Managing.Application.Abstractions.Grains;
|
|
using Managing.Application.Abstractions.Models;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Application.Bots.Models;
|
|
using Managing.Application.Orleans;
|
|
using Managing.Domain.Statistics;
|
|
using Microsoft.Extensions.Logging;
|
|
using static Managing.Common.Enums;
|
|
|
|
namespace Managing.Application.Bots.Grains;
|
|
|
|
/// <summary>
|
|
/// Orleans grain for Agent operations.
|
|
/// Uses custom trading placement with load balancing and built-in fallback.
|
|
/// </summary>
|
|
[TradingPlacement] // Use custom trading placement with load balancing
|
|
public class AgentGrain : Grain, IAgentGrain
|
|
{
|
|
private readonly IPersistentState<AgentGrainState> _state;
|
|
private readonly ILogger<AgentGrain> _logger;
|
|
private readonly IBotService _botService;
|
|
private readonly IAgentService _agentService;
|
|
private readonly IExchangeService _exchangeService;
|
|
private readonly IUserService _userService;
|
|
private readonly IAccountService _accountService;
|
|
private readonly ITradingService _tradingService;
|
|
|
|
public AgentGrain(
|
|
[PersistentState("agent-state", "agent-store")]
|
|
IPersistentState<AgentGrainState> state,
|
|
ILogger<AgentGrain> logger,
|
|
IBotService botService,
|
|
IAgentService agentService,
|
|
IExchangeService exchangeService,
|
|
IUserService userService,
|
|
IAccountService accountService,
|
|
ITradingService tradingService)
|
|
{
|
|
_state = state;
|
|
_logger = logger;
|
|
_botService = botService;
|
|
_agentService = agentService;
|
|
_exchangeService = exchangeService;
|
|
_userService = userService;
|
|
_accountService = accountService;
|
|
_tradingService = tradingService;
|
|
}
|
|
|
|
public override Task OnActivateAsync(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("AgentGrain activated for user {UserId}", this.GetPrimaryKeyLong());
|
|
return base.OnActivateAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task InitializeAsync(int userId, string agentName)
|
|
{
|
|
_state.State.AgentName = agentName;
|
|
await _state.WriteStateAsync();
|
|
|
|
// Create an empty AgentSummary for the new agent
|
|
var emptySummary = new AgentSummary
|
|
{
|
|
UserId = userId,
|
|
AgentName = agentName,
|
|
TotalPnL = 0,
|
|
TotalROI = 0,
|
|
Wins = 0,
|
|
Losses = 0,
|
|
Runtime = null,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow,
|
|
ActiveStrategiesCount = 0,
|
|
TotalVolume = 0,
|
|
TotalBalance = 0
|
|
};
|
|
|
|
await _agentService.SaveOrUpdateAgentSummary(emptySummary);
|
|
_logger.LogInformation("Agent {UserId} initialized with name {AgentName} and empty summary", userId, agentName);
|
|
}
|
|
|
|
public async Task UpdateAgentNameAsync(string agentName)
|
|
{
|
|
_state.State.AgentName = agentName;
|
|
await _state.WriteStateAsync();
|
|
_logger.LogInformation("Agent {UserId} updated with name {AgentName}", this.GetPrimaryKeyLong(), agentName);
|
|
}
|
|
|
|
public async Task OnAgentSummaryUpdateAsync(AgentSummaryUpdateEvent updateEvent)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogInformation("Received agent summary update event for user {UserId}, event type: {EventType}",
|
|
this.GetPrimaryKeyLong(), updateEvent.EventType);
|
|
|
|
// Only update summary if the event is for this agent's bots
|
|
if (_state.State.BotIds.Contains(updateEvent.BotId))
|
|
{
|
|
await UpdateSummary();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error processing agent summary update event for user {UserId}",
|
|
this.GetPrimaryKeyLong());
|
|
}
|
|
}
|
|
|
|
public async Task UpdateSummary()
|
|
{
|
|
try
|
|
{
|
|
// Get all bots for this agent
|
|
var bots = await _botService.GetBotsByIdsAsync(_state.State.BotIds);
|
|
|
|
// Calculate aggregated statistics from bot data
|
|
var totalPnL = bots.Sum(b => b.Pnl);
|
|
var totalWins = bots.Sum(b => b.TradeWins);
|
|
var totalLosses = bots.Sum(b => b.TradeLosses);
|
|
|
|
// Calculate ROI based on total volume traded with proper division by zero handling
|
|
var totalVolume = bots.Sum(b => b.Volume);
|
|
decimal totalROI;
|
|
|
|
if (totalVolume > 0)
|
|
{
|
|
totalROI = (totalPnL / totalVolume) * 100;
|
|
}
|
|
else if (totalVolume == 0 && totalPnL == 0)
|
|
{
|
|
// No trading activity yet
|
|
totalROI = 0;
|
|
}
|
|
else if (totalVolume == 0 && totalPnL != 0)
|
|
{
|
|
// Edge case: PnL exists but no volume (shouldn't happen in normal cases)
|
|
_logger.LogWarning("Agent {UserId} has PnL {PnL} but zero volume", this.GetPrimaryKeyLong(), totalPnL);
|
|
totalROI = 0;
|
|
}
|
|
else
|
|
{
|
|
// Fallback for any other edge cases
|
|
totalROI = 0;
|
|
}
|
|
|
|
// Calculate Runtime based on the farthest date from bot startup times
|
|
DateTime? runtime = null;
|
|
if (bots.Any())
|
|
{
|
|
runtime = bots.Max(b => b.StartupTime);
|
|
}
|
|
|
|
// Calculate total balance (USDC + open positions value)
|
|
decimal totalBalance = 0;
|
|
try
|
|
{
|
|
var userId = (int)this.GetPrimaryKeyLong();
|
|
var user = await _userService.GetUserByIdAsync(userId);
|
|
|
|
if (user != null)
|
|
{
|
|
var userAccounts = await _accountService.GetAccountsByUserAsync(user, hideSecrets: true, true);
|
|
|
|
foreach (var account in userAccounts)
|
|
{
|
|
// Get USDC balance
|
|
var usdcBalances = await _exchangeService.GetBalances(account);
|
|
var usdcBalance = usdcBalances.FirstOrDefault(b => b.TokenName?.ToUpper() == "USDC")?.Amount ??
|
|
0;
|
|
totalBalance += usdcBalance;
|
|
}
|
|
|
|
// Get positions for all bots using their GUIDs as InitiatorIdentifier
|
|
var botPositions =
|
|
await _tradingService.GetPositionsByInitiatorIdentifiersAsync(_state.State.BotIds);
|
|
|
|
foreach (var position in botPositions.Where(p => !p.IsFinished()))
|
|
{
|
|
totalBalance += position.Open.Price * position.Open.Quantity;
|
|
totalBalance += position.ProfitAndLoss?.Realized ?? 0;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error calculating total balance for agent {UserId}", this.GetPrimaryKeyLong());
|
|
totalBalance = 0; // Set to 0 if calculation fails
|
|
}
|
|
|
|
var summary = new AgentSummary
|
|
{
|
|
UserId = (int)this.GetPrimaryKeyLong(),
|
|
AgentName = _state.State.AgentName,
|
|
TotalPnL = totalPnL,
|
|
Wins = totalWins,
|
|
Losses = totalLosses,
|
|
TotalROI = totalROI,
|
|
Runtime = runtime,
|
|
ActiveStrategiesCount = bots.Count(b => b.Status == BotStatus.Running),
|
|
TotalVolume = totalVolume,
|
|
TotalBalance = totalBalance,
|
|
};
|
|
|
|
// Save summary to database
|
|
await _agentService.SaveOrUpdateAgentSummary(summary);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error calculating agent summary for user {UserId}", this.GetPrimaryKeyLong());
|
|
}
|
|
}
|
|
|
|
public async Task RegisterBotAsync(Guid botId)
|
|
{
|
|
if (_state.State.BotIds.Add(botId))
|
|
{
|
|
await _state.WriteStateAsync();
|
|
_logger.LogInformation("Bot {BotId} registered to Agent {UserId}", botId, this.GetPrimaryKeyLong());
|
|
|
|
// Update summary after registering bot
|
|
await UpdateSummary();
|
|
}
|
|
}
|
|
|
|
public async Task UnregisterBotAsync(Guid botId)
|
|
{
|
|
if (_state.State.BotIds.Remove(botId))
|
|
{
|
|
await _state.WriteStateAsync();
|
|
_logger.LogInformation("Bot {BotId} unregistered from Agent {UserId}", botId, this.GetPrimaryKeyLong());
|
|
|
|
// Update summary after unregistering bot
|
|
await UpdateSummary();
|
|
}
|
|
}
|
|
} |