Files
managing-apps/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBotRepository.cs
cryptooda ae353aa0d5 Implement balance update callback in TradingBotBase for immediate sync
- Removed the private _currentBalance field and replaced it with direct access to Config.BotTradingBalance.
- Added OnBalanceUpdatedCallback to TradingBotBase for immediate synchronization and database saving when the balance is updated.
- Updated LiveTradingBotGrain to set the callback for balance updates, ensuring accurate state management.
- Modified PostgreSqlBotRepository to save the updated bot trading balance during entity updates.
2026-01-09 03:34:35 +07:00

341 lines
12 KiB
C#

using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Bots;
using Microsoft.EntityFrameworkCore;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlBotRepository : IBotRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlBotRepository(ManagingDbContext context)
{
_context = context;
}
public async Task InsertBotAsync(Bot bot)
{
bot.CreateDate = DateTime.UtcNow;
var entity = PostgreSqlMappers.Map(bot);
// Set the UserId if user is provided
if (bot.User != null)
{
var userEntity = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Id == bot.User.Id)
.ConfigureAwait(false);
if (userEntity == null)
{
throw new InvalidOperationException($"User with id '{bot.User.Id}' not found");
}
entity.UserId = userEntity.Id;
}
await _context.Bots.AddAsync(entity).ConfigureAwait(false);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task<IEnumerable<Bot>> GetBotsAsync()
{
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
public async Task UpdateBot(Bot bot)
{
var existingEntity = await _context.Bots
.AsTracking()
.FirstOrDefaultAsync(b => b.Identifier == bot.Identifier)
.ConfigureAwait(false);
if (existingEntity == null)
{
throw new InvalidOperationException($"Bot backup with identifier '{bot.Identifier}' not found");
}
// Update the existing entity properties directly instead of creating a new one
existingEntity.Name = bot.Name;
existingEntity.Ticker = bot.Ticker;
existingEntity.TradingType = bot.TradingType;
existingEntity.Status = bot.Status;
existingEntity.StartupTime = bot.StartupTime;
existingEntity.TradeWins = bot.TradeWins;
existingEntity.TradeLosses = bot.TradeLosses;
existingEntity.Pnl = bot.Pnl;
existingEntity.NetPnL = bot.NetPnL;
existingEntity.Roi = bot.Roi;
existingEntity.Volume = bot.Volume;
existingEntity.Fees = bot.Fees;
existingEntity.UpdatedAt = DateTime.UtcNow;
existingEntity.LongPositionCount = bot.LongPositionCount;
existingEntity.ShortPositionCount = bot.ShortPositionCount;
existingEntity.LastStartTime = bot.LastStartTime;
existingEntity.LastStopTime = bot.LastStopTime;
existingEntity.AccumulatedRunTimeSeconds = bot.AccumulatedRunTimeSeconds;
existingEntity.MasterBotUserId =
bot.MasterBotUserId ?? existingEntity.MasterBotUserId;
existingEntity.BotTradingBalance = bot.BotTradingBalance;
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task DeleteBot(Guid identifier)
{
var entity = await _context.Bots
.AsTracking()
.FirstOrDefaultAsync(b => b.Identifier == identifier)
.ConfigureAwait(false);
if (entity == null)
{
throw new InvalidOperationException($"Bot backup with identifier '{identifier}' not found");
}
_context.Bots.Remove(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task<Bot> GetBotByIdentifierAsync(Guid identifier)
{
var entity = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.FirstOrDefaultAsync(b => b.Identifier == identifier)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entity);
}
public async Task<IEnumerable<Bot>> GetBotsByUserIdAsync(int id)
{
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.Where(b => b.UserId == id)
.ToListAsync()
.ConfigureAwait(false);
// Map entities to domain objects
var bots = PostgreSqlMappers.Map(entities).ToList();
// Attach master bot users to domain objects
foreach (var entity in entities.Where(e => e.MasterBotUser != null))
{
var bot = bots.FirstOrDefault(b => b.MasterBotUserId == entity.MasterBotUserId);
if (bot != null)
{
// Convert UserEntity to User domain object
bot.MasterBotUser = PostgreSqlMappers.Map(entity.MasterBotUser);
}
}
return bots;
}
public async Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status)
{
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.Where(b => b.Status == status)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
public async Task<Bot> GetBotByNameAsync(string name)
{
var entity = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.FirstOrDefaultAsync(b => b.Name == name)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entity);
}
public async Task<Bot> GetBotByUserIdAndNameAsync(int userId, string name)
{
var entity = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.FirstOrDefaultAsync(b => b.UserId == userId && b.Name == name)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entity);
}
public async Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> identifiers)
{
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.Where(b => identifiers.Contains(b.Identifier))
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
finally
{
// Always ensure the connection is closed after the operation
await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context);
}
}
public async Task<(IEnumerable<Bot> Bots, int TotalCount)> GetBotsPaginatedAsync(
int pageNumber,
int pageSize,
BotStatus? status = null,
string? name = null,
string? ticker = null,
string? agentName = null,
decimal? minBalance = null,
decimal? maxBalance = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false)
{
// Build the query with filters
var query = _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.AsQueryable();
// Apply filters
if (status.HasValue)
{
query = query.Where(b => b.Status == status.Value);
}
if (!string.IsNullOrWhiteSpace(name))
{
query = query.Where(b => EF.Functions.ILike(b.Name, $"%{name}%"));
}
if (!string.IsNullOrWhiteSpace(ticker))
{
query = query.Where(b => EF.Functions.ILike(b.Ticker.ToString(), $"%{ticker}%"));
}
if (!string.IsNullOrWhiteSpace(agentName))
{
query = query.Where(b => b.User != null && EF.Functions.ILike(b.User.AgentName, $"%{agentName}%"));
}
// Apply balance filtering if specified
if (minBalance.HasValue)
{
query = query.Where(b => b.BotTradingBalance >= minBalance.Value);
}
if (maxBalance.HasValue)
{
query = query.Where(b => b.BotTradingBalance <= maxBalance.Value);
}
// Apply profitable bots filtering if specified
if (showOnlyProfitable)
{
query = query.Where(b => b.Roi > 0);
}
// Get total count before applying pagination
var totalCount = await query.CountAsync().ConfigureAwait(false);
// Apply sorting
query = sortBy switch
{
BotSortableColumn.Name => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Name)
: query.OrderByDescending(b => b.Name),
BotSortableColumn.Ticker => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Ticker)
: query.OrderByDescending(b => b.Ticker),
BotSortableColumn.Status => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Status)
: query.OrderByDescending(b => b.Status),
BotSortableColumn.StartupTime => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.StartupTime)
: query.OrderByDescending(b => b.StartupTime),
BotSortableColumn.Roi => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Roi)
: query.OrderByDescending(b => b.Roi),
BotSortableColumn.Pnl => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.Pnl)
: query.OrderByDescending(b => b.Pnl),
BotSortableColumn.WinRate => sortDirection == SortDirection.Asc
? query.OrderBy(b =>
(b.TradeWins + b.TradeLosses) > 0 ? (double)b.TradeWins / (b.TradeWins + b.TradeLosses) : 0)
: query.OrderByDescending(b =>
(b.TradeWins + b.TradeLosses) > 0 ? (double)b.TradeWins / (b.TradeWins + b.TradeLosses) : 0),
BotSortableColumn.AgentName => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.User.AgentName)
: query.OrderByDescending(b => b.User.AgentName),
BotSortableColumn.BotTradingBalance => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.BotTradingBalance)
: query.OrderByDescending(b => b.BotTradingBalance),
_ => sortDirection == SortDirection.Asc
? query.OrderBy(b => b.CreateDate)
: query.OrderByDescending(b => b.CreateDate)
};
// Apply pagination
var skip = (pageNumber - 1) * pageSize;
var entities = await query
.Skip(skip)
.Take(pageSize)
.ToListAsync()
.ConfigureAwait(false);
var bots = PostgreSqlMappers.Map(entities);
return (bots, totalCount);
}
public async Task<IEnumerable<Bot>> GetTopBotsByPnLAsync(IEnumerable<BotStatus> statuses, int count = 3)
{
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.Where(b => statuses.Contains(b.Status))
.OrderByDescending(b => b.Pnl)
.Take(count)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
public async Task<IEnumerable<Bot>> GetTopBotsByRoiAsync(IEnumerable<BotStatus> statuses, int count = 3)
{
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.Where(b => statuses.Contains(b.Status))
.OrderByDescending(b => b.Roi)
.Take(count)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
}