- Added comments to indicate that BacktestCount is not updated directly in the entity, as it is managed independently via IncrementBacktestCountAsync. This change prevents other update operations from overwriting the BacktestCount, ensuring data integrity.
556 lines
22 KiB
C#
556 lines
22 KiB
C#
using Managing.Application.Abstractions.Repositories;
|
|
using Managing.Domain.Statistics;
|
|
using Managing.Infrastructure.Databases.PostgreSql.Entities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using static Managing.Common.Enums;
|
|
|
|
namespace Managing.Infrastructure.Databases.PostgreSql;
|
|
|
|
public class AgentSummaryRepository : IAgentSummaryRepository
|
|
{
|
|
private readonly ManagingDbContext _context;
|
|
private readonly ILogger<AgentSummaryRepository> _logger;
|
|
|
|
public AgentSummaryRepository(ManagingDbContext context, ILogger<AgentSummaryRepository> logger)
|
|
{
|
|
_context = context;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<AgentSummary?> GetByUserIdAsync(int userId)
|
|
{
|
|
var entity = await _context.AgentSummaries
|
|
.Include(a => a.User)
|
|
.FirstOrDefaultAsync(a => a.UserId == userId);
|
|
|
|
return entity != null ? MapToDomain(entity) : null;
|
|
}
|
|
|
|
public async Task<AgentSummary?> GetByAgentNameAsync(string agentName)
|
|
{
|
|
var entity = await _context.AgentSummaries
|
|
.Include(a => a.User)
|
|
.FirstOrDefaultAsync(a => a.AgentName == agentName);
|
|
|
|
return entity != null ? MapToDomain(entity) : null;
|
|
}
|
|
|
|
public async Task<IEnumerable<AgentSummary>> GetAllAsync()
|
|
{
|
|
var entities = await _context.AgentSummaries
|
|
.Include(a => a.User)
|
|
.ToListAsync();
|
|
|
|
return entities.Select(MapToDomain);
|
|
}
|
|
|
|
public async Task InsertAsync(AgentSummary agentSummary)
|
|
{
|
|
var entity = MapToEntity(agentSummary);
|
|
entity.CreatedAt = DateTime.UtcNow;
|
|
entity.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await _context.AgentSummaries.AddAsync(entity);
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("AgentSummary inserted for user {UserId} with agent name {AgentName}",
|
|
agentSummary.UserId, agentSummary.AgentName);
|
|
}
|
|
|
|
public async Task UpdateAsync(AgentSummary agentSummary)
|
|
{
|
|
var entity = await _context.AgentSummaries
|
|
.FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId);
|
|
|
|
if (entity != null)
|
|
{
|
|
MapToEntity(agentSummary, entity);
|
|
entity.UpdatedAt = DateTime.UtcNow;
|
|
|
|
// No need to call Update() since the entity is already being tracked
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("AgentSummary updated for user {UserId} with agent name {AgentName}",
|
|
agentSummary.UserId, agentSummary.AgentName);
|
|
}
|
|
}
|
|
|
|
public async Task SaveOrUpdateAsync(AgentSummary agentSummary)
|
|
{
|
|
// Use the execution strategy to handle retries and transactions
|
|
var strategy = _context.Database.CreateExecutionStrategy();
|
|
await strategy.ExecuteAsync(async () =>
|
|
{
|
|
using var transaction = await _context.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
// Ensure the User entity exists and is saved
|
|
if (agentSummary.User != null)
|
|
{
|
|
var existingUser = await _context.Users
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(u => u.Id == agentSummary.UserId);
|
|
|
|
if (existingUser == null)
|
|
{
|
|
// User doesn't exist, save it first
|
|
var userEntity = PostgreSqlMappers.Map(agentSummary.User);
|
|
userEntity.Id = agentSummary.UserId; // Ensure the ID is set
|
|
await _context.Users.AddAsync(userEntity);
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("User created for AgentSummary with ID {UserId}", agentSummary.UserId);
|
|
}
|
|
}
|
|
|
|
// Check if AgentSummary exists (using AsNoTracking to avoid tracking conflicts)
|
|
var existing = await _context.AgentSummaries
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId);
|
|
|
|
if (existing == null)
|
|
{
|
|
// Insert new AgentSummary
|
|
var entity = MapToEntity(agentSummary);
|
|
entity.CreatedAt = DateTime.UtcNow;
|
|
entity.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await _context.AgentSummaries.AddAsync(entity);
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("AgentSummary inserted for user {UserId} with agent name {AgentName}",
|
|
agentSummary.UserId, agentSummary.AgentName);
|
|
}
|
|
else
|
|
{
|
|
// Update existing record - check if already tracked first
|
|
var entityToUpdate = MapToEntity(agentSummary);
|
|
entityToUpdate.Id = existing.Id; // Preserve the existing ID
|
|
entityToUpdate.CreatedAt = existing.CreatedAt; // Preserve creation date
|
|
entityToUpdate.UpdatedAt = DateTime.UtcNow;
|
|
|
|
// Check if an entity with this key is already being tracked
|
|
var trackedEntity = _context.ChangeTracker.Entries<AgentSummaryEntity>()
|
|
.FirstOrDefault(e => e.Entity.Id == existing.Id);
|
|
|
|
if (trackedEntity != null)
|
|
{
|
|
// Entity is already tracked, update its values
|
|
MapToEntity(agentSummary, trackedEntity.Entity);
|
|
trackedEntity.Entity.UpdatedAt = DateTime.UtcNow;
|
|
trackedEntity.State = EntityState.Modified;
|
|
}
|
|
else
|
|
{
|
|
// Entity is not tracked, use Update method which handles state properly
|
|
_context.AgentSummaries.Update(entityToUpdate);
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("AgentSummary updated for user {UserId} with agent name {AgentName}",
|
|
agentSummary.UserId, agentSummary.AgentName);
|
|
}
|
|
|
|
await transaction.CommitAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
_logger.LogError(ex, "Error saving/updating AgentSummary for user {UserId} with agent name {AgentName}",
|
|
agentSummary.UserId, agentSummary.AgentName);
|
|
throw;
|
|
}
|
|
});
|
|
}
|
|
|
|
public async Task<(IEnumerable<AgentSummary> Results, int TotalCount)> GetPaginatedAsync(
|
|
int page,
|
|
int pageSize,
|
|
SortableFields sortBy,
|
|
string sortOrder,
|
|
IEnumerable<string>? agentNames = null,
|
|
bool showOnlyProfitableAgent = false)
|
|
{
|
|
// Start with base query
|
|
var query = _context.AgentSummaries.Include(a => a.User).AsQueryable();
|
|
|
|
// Apply agent name filtering if specified
|
|
if (agentNames != null && agentNames.Any())
|
|
{
|
|
query = query.Where(a => agentNames.Contains(a.AgentName));
|
|
}
|
|
|
|
// Apply profitable agent filtering if specified
|
|
if (showOnlyProfitableAgent)
|
|
{
|
|
query = query.Where(a => a.TotalROI > 0);
|
|
}
|
|
|
|
// Get total count before applying pagination
|
|
var totalCount = await query.CountAsync();
|
|
|
|
// Apply sorting
|
|
var isDescending = sortOrder.ToLowerInvariant() == "desc";
|
|
query = sortBy switch
|
|
{
|
|
SortableFields.NetPnL => isDescending
|
|
? query.OrderByDescending(a => a.NetPnL)
|
|
: query.OrderBy(a => a.NetPnL),
|
|
SortableFields.TotalROI => isDescending
|
|
? query.OrderByDescending(a => a.TotalROI)
|
|
: query.OrderBy(a => a.TotalROI),
|
|
SortableFields.Wins => isDescending
|
|
? query.OrderByDescending(a => a.Wins)
|
|
: query.OrderBy(a => a.Wins),
|
|
SortableFields.Losses => isDescending
|
|
? query.OrderByDescending(a => a.Losses)
|
|
: query.OrderBy(a => a.Losses),
|
|
SortableFields.AgentName => isDescending
|
|
? query.OrderByDescending(a => a.AgentName)
|
|
: query.OrderBy(a => a.AgentName),
|
|
SortableFields.CreatedAt => isDescending
|
|
? query.OrderByDescending(a => a.CreatedAt)
|
|
: query.OrderBy(a => a.CreatedAt),
|
|
SortableFields.UpdatedAt => isDescending
|
|
? query.OrderByDescending(a => a.UpdatedAt)
|
|
: query.OrderBy(a => a.UpdatedAt),
|
|
SortableFields.TotalVolume => isDescending
|
|
? query.OrderByDescending(a => a.TotalVolume)
|
|
: query.OrderBy(a => a.TotalVolume),
|
|
SortableFields.TotalBalance => isDescending
|
|
? query.OrderByDescending(a => a.TotalBalance)
|
|
: query.OrderBy(a => a.TotalBalance),
|
|
_ => isDescending
|
|
? query.OrderByDescending(a => a.TotalPnL) // Default to TotalPnL desc
|
|
: query.OrderBy(a => a.TotalPnL)
|
|
};
|
|
|
|
// Apply pagination
|
|
var results = await query
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToListAsync();
|
|
|
|
// Map to domain objects
|
|
var domainResults = results.Select(MapToDomain);
|
|
|
|
return (domainResults, totalCount);
|
|
}
|
|
|
|
private static AgentSummaryEntity MapToEntity(AgentSummary domain)
|
|
{
|
|
return new AgentSummaryEntity
|
|
{
|
|
Id = domain.Id,
|
|
UserId = domain.UserId,
|
|
AgentName = domain.AgentName,
|
|
TotalPnL = domain.TotalPnL,
|
|
NetPnL = domain.NetPnL,
|
|
TotalROI = domain.TotalROI,
|
|
Wins = domain.Wins,
|
|
Losses = domain.Losses,
|
|
Runtime = domain.Runtime,
|
|
CreatedAt = domain.CreatedAt,
|
|
UpdatedAt = domain.UpdatedAt,
|
|
ActiveStrategiesCount = domain.ActiveStrategiesCount,
|
|
TotalVolume = domain.TotalVolume,
|
|
TotalBalance = domain.TotalBalance,
|
|
TotalFees = domain.TotalFees,
|
|
BacktestCount = domain.BacktestCount
|
|
};
|
|
}
|
|
|
|
private static void MapToEntity(AgentSummary domain, AgentSummaryEntity entity)
|
|
{
|
|
entity.UserId = domain.UserId;
|
|
entity.AgentName = domain.AgentName;
|
|
entity.TotalPnL = domain.TotalPnL;
|
|
entity.NetPnL = domain.NetPnL;
|
|
entity.TotalROI = domain.TotalROI;
|
|
entity.Wins = domain.Wins;
|
|
entity.Losses = domain.Losses;
|
|
entity.Runtime = domain.Runtime;
|
|
entity.ActiveStrategiesCount = domain.ActiveStrategiesCount;
|
|
entity.TotalVolume = domain.TotalVolume;
|
|
entity.TotalBalance = domain.TotalBalance;
|
|
entity.TotalFees = domain.TotalFees;
|
|
// BacktestCount is NOT updated here - it's managed independently via IncrementBacktestCountAsync
|
|
// This prevents other update operations from overwriting the BacktestCount
|
|
}
|
|
|
|
private static AgentSummary MapToDomain(AgentSummaryEntity entity)
|
|
{
|
|
return new AgentSummary
|
|
{
|
|
Id = entity.Id,
|
|
UserId = entity.UserId,
|
|
AgentName = entity.AgentName,
|
|
TotalPnL = entity.TotalPnL,
|
|
NetPnL = entity.NetPnL,
|
|
TotalROI = entity.TotalROI,
|
|
Wins = entity.Wins,
|
|
Losses = entity.Losses,
|
|
Runtime = entity.Runtime,
|
|
CreatedAt = entity.CreatedAt,
|
|
UpdatedAt = entity.UpdatedAt,
|
|
ActiveStrategiesCount = entity.ActiveStrategiesCount,
|
|
TotalVolume = entity.TotalVolume,
|
|
TotalBalance = entity.TotalBalance,
|
|
TotalFees = entity.TotalFees,
|
|
BacktestCount = entity.BacktestCount,
|
|
User = PostgreSqlMappers.Map(entity.User)
|
|
};
|
|
}
|
|
|
|
public async Task<IEnumerable<AgentSummary>> GetAllAgentWithRunningBots()
|
|
{
|
|
var agentSummaries = await _context.AgentSummaries
|
|
.Include(a => a.User)
|
|
.Where(a => _context.Bots.Any(b => b.UserId == a.UserId && b.Status == BotStatus.Running))
|
|
.ToListAsync();
|
|
|
|
return agentSummaries.Select(MapToDomain);
|
|
}
|
|
|
|
public async Task UpdateAgentNameAsync(int userId, string agentName)
|
|
{
|
|
try
|
|
{
|
|
// First, check if there's already a tracked entity with this key
|
|
var trackedEntity = _context.ChangeTracker.Entries<AgentSummaryEntity>()
|
|
.FirstOrDefault(e => e.Entity.UserId == userId);
|
|
|
|
AgentSummaryEntity? entityToUpdate = null;
|
|
bool wasTracked = false;
|
|
|
|
if (trackedEntity != null)
|
|
{
|
|
// Entity is already tracked, update it directly
|
|
entityToUpdate = trackedEntity.Entity;
|
|
wasTracked = true;
|
|
|
|
_logger.LogInformation("Found tracked entity for user {UserId}. Current agent name: {CurrentAgentName}, New agent name: {NewAgentName}",
|
|
userId, entityToUpdate.AgentName, agentName);
|
|
}
|
|
else
|
|
{
|
|
// Entity is not tracked, fetch it normally
|
|
entityToUpdate = await _context.AgentSummaries
|
|
.FirstOrDefaultAsync(a => a.UserId == userId);
|
|
|
|
if (entityToUpdate == null)
|
|
{
|
|
_logger.LogWarning(
|
|
"No AgentSummary found for user {UserId} when trying to update agent name to {AgentName}",
|
|
userId, agentName);
|
|
return;
|
|
}
|
|
|
|
_logger.LogInformation("Fetched entity for user {UserId}. Current agent name: {CurrentAgentName}, New agent name: {NewAgentName}",
|
|
userId, entityToUpdate.AgentName, agentName);
|
|
}
|
|
|
|
// Check if the agent name is actually different
|
|
if (entityToUpdate.AgentName == agentName)
|
|
{
|
|
_logger.LogInformation("Agent name for user {UserId} is already {AgentName}, no update needed", userId, agentName);
|
|
return;
|
|
}
|
|
|
|
// Update the entity
|
|
var oldAgentName = entityToUpdate.AgentName;
|
|
entityToUpdate.AgentName = agentName;
|
|
entityToUpdate.UpdatedAt = DateTime.UtcNow;
|
|
|
|
// If it wasn't tracked, explicitly mark it as modified
|
|
if (!wasTracked)
|
|
{
|
|
_context.Entry(entityToUpdate).State = EntityState.Modified;
|
|
}
|
|
|
|
// Log the change tracker state before saving
|
|
var modifiedEntries = _context.ChangeTracker.Entries()
|
|
.Where(e => e.State == EntityState.Modified)
|
|
.ToList();
|
|
|
|
_logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count);
|
|
|
|
// Save changes
|
|
var changesSaved = await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Agent name updated for user {UserId} from '{OldAgentName}' to '{NewAgentName}'. Changes saved: {ChangesSaved}",
|
|
userId, oldAgentName, agentName, changesSaved);
|
|
|
|
if (changesSaved == 0)
|
|
{
|
|
_logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error updating agent name for user {UserId} to {AgentName}", userId, agentName);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<int> GetTotalAgentCount()
|
|
{
|
|
return await _context.AgentSummaries.CountAsync();
|
|
}
|
|
|
|
public async Task IncrementBacktestCountAsync(int userId)
|
|
{
|
|
try
|
|
{
|
|
// First, check if there's already a tracked entity with this key
|
|
var trackedEntity = _context.ChangeTracker.Entries<AgentSummaryEntity>()
|
|
.FirstOrDefault(e => e.Entity.UserId == userId);
|
|
|
|
AgentSummaryEntity? entityToUpdate = null;
|
|
bool wasTracked = false;
|
|
|
|
if (trackedEntity != null)
|
|
{
|
|
// Entity is already tracked, update it directly
|
|
entityToUpdate = trackedEntity.Entity;
|
|
wasTracked = true;
|
|
|
|
_logger.LogInformation("Found tracked entity for user {UserId}. Current backtest count: {CurrentCount}",
|
|
userId, entityToUpdate.BacktestCount);
|
|
}
|
|
else
|
|
{
|
|
// Entity is not tracked, fetch it normally
|
|
entityToUpdate = await _context.AgentSummaries
|
|
.FirstOrDefaultAsync(a => a.UserId == userId);
|
|
|
|
if (entityToUpdate == null)
|
|
{
|
|
_logger.LogWarning("No AgentSummary found for user {UserId} when trying to increment backtest count",
|
|
userId);
|
|
return;
|
|
}
|
|
|
|
_logger.LogInformation("Fetched entity for user {UserId}. Current backtest count: {CurrentCount}",
|
|
userId, entityToUpdate.BacktestCount);
|
|
}
|
|
|
|
// Update the entity
|
|
var oldCount = entityToUpdate.BacktestCount;
|
|
var newCount = oldCount + 1;
|
|
entityToUpdate.BacktestCount = newCount;
|
|
entityToUpdate.UpdatedAt = DateTime.UtcNow;
|
|
|
|
// If it wasn't tracked, explicitly mark it as modified
|
|
if (!wasTracked)
|
|
{
|
|
_context.Entry(entityToUpdate).State = EntityState.Modified;
|
|
}
|
|
|
|
// Log the change tracker state before saving
|
|
var modifiedEntries = _context.ChangeTracker.Entries()
|
|
.Where(e => e.State == EntityState.Modified)
|
|
.ToList();
|
|
|
|
_logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count);
|
|
|
|
// Save changes
|
|
var changesSaved = await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Backtest count incremented for user {UserId} from {OldCount} to {NewCount}. Changes saved: {ChangesSaved}",
|
|
userId, oldCount, newCount, changesSaved);
|
|
|
|
if (changesSaved == 0)
|
|
{
|
|
_logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error incrementing backtest count for user {UserId}", userId);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task UpdateTotalBalanceAsync(int userId, decimal totalBalance)
|
|
{
|
|
try
|
|
{
|
|
// First, check if there's already a tracked entity with this key
|
|
var trackedEntity = _context.ChangeTracker.Entries<AgentSummaryEntity>()
|
|
.FirstOrDefault(e => e.Entity.UserId == userId);
|
|
|
|
AgentSummaryEntity? entityToUpdate = null;
|
|
bool wasTracked = false;
|
|
|
|
if (trackedEntity != null)
|
|
{
|
|
// Entity is already tracked, update it directly
|
|
entityToUpdate = trackedEntity.Entity;
|
|
wasTracked = true;
|
|
|
|
_logger.LogInformation("Found tracked entity for user {UserId}. Current total balance: {CurrentBalance}, New total balance: {NewBalance}",
|
|
userId, entityToUpdate.TotalBalance, totalBalance);
|
|
}
|
|
else
|
|
{
|
|
// Entity is not tracked, fetch it normally
|
|
entityToUpdate = await _context.AgentSummaries
|
|
.FirstOrDefaultAsync(a => a.UserId == userId);
|
|
|
|
if (entityToUpdate == null)
|
|
{
|
|
_logger.LogWarning("No AgentSummary found for user {UserId} when trying to update total balance to {TotalBalance}",
|
|
userId, totalBalance);
|
|
return;
|
|
}
|
|
|
|
_logger.LogInformation("Fetched entity for user {UserId}. Current total balance: {CurrentBalance}, New total balance: {NewBalance}",
|
|
userId, entityToUpdate.TotalBalance, totalBalance);
|
|
}
|
|
|
|
// Check if the total balance is actually different
|
|
if (entityToUpdate.TotalBalance == totalBalance)
|
|
{
|
|
_logger.LogInformation("Total balance for user {UserId} is already {TotalBalance}, no update needed", userId, totalBalance);
|
|
return;
|
|
}
|
|
|
|
// Update the entity
|
|
var oldBalance = entityToUpdate.TotalBalance;
|
|
entityToUpdate.TotalBalance = totalBalance;
|
|
entityToUpdate.UpdatedAt = DateTime.UtcNow;
|
|
|
|
// If it wasn't tracked, explicitly mark it as modified
|
|
if (!wasTracked)
|
|
{
|
|
_context.Entry(entityToUpdate).State = EntityState.Modified;
|
|
}
|
|
|
|
// Log the change tracker state before saving
|
|
var modifiedEntries = _context.ChangeTracker.Entries()
|
|
.Where(e => e.State == EntityState.Modified)
|
|
.ToList();
|
|
|
|
_logger.LogInformation("Change tracker has {Count} modified entries before save", modifiedEntries.Count);
|
|
|
|
// Save changes
|
|
var changesSaved = await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Total balance updated for user {UserId} from {OldBalance} to {NewBalance}. Changes saved: {ChangesSaved}",
|
|
userId, oldBalance, totalBalance, changesSaved);
|
|
|
|
if (changesSaved == 0)
|
|
{
|
|
_logger.LogWarning("No changes were saved for user {UserId}. This might indicate a tracking issue.", userId);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error updating total balance for user {UserId} to {TotalBalance}", userId, totalBalance);
|
|
throw;
|
|
}
|
|
}
|
|
} |