using Managing.Application.Abstractions.Repositories; using Managing.Domain.Scenarios; using Managing.Domain.Strategies; using Managing.Domain.Trades; using Managing.Domain.Users; using Managing.Infrastructure.Databases.PostgreSql.Entities; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using static Managing.Common.Enums; namespace Managing.Infrastructure.Databases.PostgreSql; public class PostgreSqlTradingRepository : ITradingRepository { private readonly ManagingDbContext _context; public PostgreSqlTradingRepository(ManagingDbContext context) { _context = context; } #region Scenario Methods public async Task DeleteScenarioAsync(string name) { var scenario = await _context.Scenarios.FirstOrDefaultAsync(s => s.Name == name); if (scenario != null) { _context.Scenarios.Remove(scenario); await _context.SaveChangesAsync(); } } public Scenario GetScenarioByName(string name) { return GetScenarioByNameAsync(name).Result; } public async Task GetScenarioByNameAsync(string name) { var scenario = await _context.Scenarios .AsNoTracking() .Include(s => s.User) .Include(s => s.ScenarioIndicators) .ThenInclude(si => si.Indicator) .FirstOrDefaultAsync(s => s.Name == name) .ConfigureAwait(false); if (scenario == null) return null; var mappedScenario = PostgreSqlMappers.Map(scenario); // Map indicators from junction table mappedScenario.Indicators = scenario.ScenarioIndicators .Select(si => PostgreSqlMappers.Map(si.Indicator)) .ToList(); return mappedScenario; } public IEnumerable GetScenarios() { return GetScenariosAsync().Result; } public async Task> GetScenariosAsync() { var scenarios = await _context.Scenarios .AsNoTracking() .Include(s => s.ScenarioIndicators) .ThenInclude(si => si.Indicator) .ToListAsync() .ConfigureAwait(false); return scenarios.Select(scenario => { var mappedScenario = PostgreSqlMappers.Map(scenario); mappedScenario.Indicators = scenario.ScenarioIndicators .Select(si => PostgreSqlMappers.Map(si.Indicator)) .ToList(); return mappedScenario; }); } public async Task> GetScenariosByUserAsync(User user) { var userId = user?.Id ?? 0; var scenarios = await _context.Scenarios .AsNoTracking() .Include(s => s.User) .Include(s => s.ScenarioIndicators) .ThenInclude(si => si.Indicator) .Where(s => s.UserId == userId) .ToListAsync() .ConfigureAwait(false); return scenarios.Select(scenario => { var mappedScenario = PostgreSqlMappers.Map(scenario); mappedScenario.Indicators = scenario.ScenarioIndicators .Select(si => PostgreSqlMappers.Map(si.Indicator)) .ToList(); return mappedScenario; }); } public async Task InsertScenarioAsync(Scenario scenario) { var userId = scenario.User?.Id ?? 0; // Check if scenario already exists for the same user var existingScenario = await _context.Scenarios .AsNoTracking() .FirstOrDefaultAsync(s => s.Name == scenario.Name && s.UserId == userId); if (existingScenario != null) { throw new InvalidOperationException( $"Scenario with name '{scenario.Name}' already exists for user '{scenario.User?.Name}'"); } var scenarioEntity = PostgreSqlMappers.Map(scenario); _context.Scenarios.Add(scenarioEntity); await _context.SaveChangesAsync(); // Handle scenario-indicator relationships if (scenario.Indicators != null && scenario.Indicators.Any()) { foreach (var indicator in scenario.Indicators) { var indicatorUserId = indicator.User?.Id ?? 0; var indicatorEntity = await _context.Indicators .AsNoTracking() .FirstOrDefaultAsync(i => i.Name == indicator.Name && i.UserId == indicatorUserId); if (indicatorEntity != null) { var junction = new ScenarioIndicatorEntity { ScenarioId = scenarioEntity.Id, IndicatorId = indicatorEntity.Id }; _context.ScenarioIndicators.Add(junction); } } await _context.SaveChangesAsync(); } } public async Task UpdateScenarioAsync(Scenario scenario) { var entity = _context.Scenarios .AsTracking() .FirstOrDefault(s => s.Name == scenario.Name); if (entity != null) { entity.LoopbackPeriod = scenario.LoopbackPeriod ?? 1; entity.UserId = scenario.User?.Id ?? 0; entity.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); } } #endregion #region Indicator Methods public async Task DeleteIndicatorAsync(string name) { var indicator = _context.Indicators .AsTracking() .FirstOrDefault(i => i.Name == name); if (indicator != null) { _context.Indicators.Remove(indicator); await _context.SaveChangesAsync(); } } public async Task DeleteIndicatorsAsync() { var indicators = _context.Indicators.AsTracking().ToList(); _context.Indicators.RemoveRange(indicators); await _context.SaveChangesAsync(); } public async Task> GetIndicatorsAsync() { var indicators = await _context.Indicators .AsNoTracking() .ToListAsync() .ConfigureAwait(false); return PostgreSqlMappers.Map(indicators); } public async Task> GetStrategiesAsync() { var indicators = await _context.Indicators .AsNoTracking() .ToListAsync() .ConfigureAwait(false); return PostgreSqlMappers.Map(indicators); } public async Task GetStrategyByNameAsync(string name) { var indicator = await _context.Indicators .AsNoTracking() .FirstOrDefaultAsync(i => i.Name == name) .ConfigureAwait(false); return PostgreSqlMappers.Map(indicator); } public async Task InsertIndicatorAsync(IndicatorBase indicatorBase) { var indicatorUserId = indicatorBase.User?.Id ?? 0; // Check if indicator already exists for the same user var existingIndicator = await _context.Indicators .AsNoTracking() .FirstOrDefaultAsync(i => i.Name == indicatorBase.Name && i.UserId == indicatorUserId); if (existingIndicator != null) { throw new InvalidOperationException( $"Indicator with name '{indicatorBase.Name}' already exists for user '{indicatorBase.User?.Name}'"); } var entity = PostgreSqlMappers.Map(indicatorBase); _context.Indicators.Add(entity); await _context.SaveChangesAsync(); } public async Task UpdateStrategyAsync(IndicatorBase indicatorBase) { var entity = _context.Indicators .AsTracking() .FirstOrDefault(i => i.Name == indicatorBase.Name); if (entity != null) { entity.Type = indicatorBase.Type; entity.SignalType = indicatorBase.SignalType; entity.MinimumHistory = indicatorBase.MinimumHistory; entity.Period = indicatorBase.Period; entity.FastPeriods = indicatorBase.FastPeriods; entity.SlowPeriods = indicatorBase.SlowPeriods; entity.SignalPeriods = indicatorBase.SignalPeriods; entity.Multiplier = indicatorBase.Multiplier; entity.SmoothPeriods = indicatorBase.SmoothPeriods; entity.StochPeriods = indicatorBase.StochPeriods; entity.CyclePeriods = indicatorBase.CyclePeriods; entity.UserId = indicatorBase.User?.Id ?? 0; entity.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); } } #endregion #region Position Methods public async Task GetPositionByIdentifierAsync(Guid identifier) { try { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); var position = await _context.Positions .AsNoTracking() .Include(p => p.User) .Include(p => p.OpenTrade) .Include(p => p.StopLossTrade) .Include(p => p.TakeProfit1Trade) .Include(p => p.TakeProfit2Trade) .FirstOrDefaultAsync(p => p.Identifier == identifier) .ConfigureAwait(false); return PostgreSqlMappers.Map(position); } finally { await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); } } public IEnumerable GetPositions(PositionInitiator positionInitiator) { return GetPositionsAsync(positionInitiator).Result; } public async Task> GetPositionsAsync(PositionInitiator positionInitiator) { var positions = await _context.Positions .AsNoTracking() .Include(p => p.User) .Include(p => p.OpenTrade) .Include(p => p.StopLossTrade) .Include(p => p.TakeProfit1Trade) .Include(p => p.TakeProfit2Trade) .Where(p => p.Initiator == positionInitiator) .ToListAsync() .ConfigureAwait(false); return PostgreSqlMappers.Map(positions); } public IEnumerable GetPositionsByStatus(PositionStatus positionStatus) { return GetPositionsByStatusAsync(positionStatus).Result; } public async Task> GetPositionsByStatusAsync(PositionStatus positionStatus) { var positions = await _context.Positions .AsNoTracking() .Include(p => p.User) .Include(p => p.OpenTrade) .Include(p => p.StopLossTrade) .Include(p => p.TakeProfit1Trade) .Include(p => p.TakeProfit2Trade) .Where(p => p.Status == positionStatus) .ToListAsync() .ConfigureAwait(false); return PostgreSqlMappers.Map(positions); } public async Task InsertPositionAsync(Position position) { var positionUserId = position.User?.Id ?? 0; // Check if position already exists for the same user var existingPosition = await _context.Positions .AsNoTracking() .FirstOrDefaultAsync(p => p.Identifier == position.Identifier && p.UserId == positionUserId); if (existingPosition != null) { throw new InvalidOperationException( $"Position with identifier '{position.Identifier}' already exists for user '{position.User?.Name}'"); } var entity = PostgreSqlMappers.Map(position); // Handle related trades if (position.Open != null) { var openTrade = PostgreSqlMappers.Map(position.Open); _context.Trades.Add(openTrade); await _context.SaveChangesAsync(); entity.OpenTradeId = openTrade.Id; } if (position.StopLoss != null) { var stopLossTrade = PostgreSqlMappers.Map(position.StopLoss); _context.Trades.Add(stopLossTrade); await _context.SaveChangesAsync(); entity.StopLossTradeId = stopLossTrade.Id; } if (position.TakeProfit1 != null) { var takeProfit1Trade = PostgreSqlMappers.Map(position.TakeProfit1); _context.Trades.Add(takeProfit1Trade); await _context.SaveChangesAsync(); entity.TakeProfit1TradeId = takeProfit1Trade.Id; } if (position.TakeProfit2 != null) { var takeProfit2Trade = PostgreSqlMappers.Map(position.TakeProfit2); _context.Trades.Add(takeProfit2Trade); await _context.SaveChangesAsync(); entity.TakeProfit2TradeId = takeProfit2Trade.Id; } _context.Positions.Add(entity); await _context.SaveChangesAsync(); } public async Task UpdatePositionAsync(Position position) { var entity = _context.Positions .AsTracking() .FirstOrDefault(p => p.Identifier == position.Identifier); if (entity != null) { entity.Date = position.Date; entity.ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0; entity.UiFees = position.UiFees; entity.GasFees = position.GasFees; entity.Status = position.Status; entity.SignalIdentifier = position.SignalIdentifier; entity.MoneyManagementJson = position.MoneyManagement != null ? JsonConvert.SerializeObject(position.MoneyManagement) : null; entity.UpdatedAt = DateTime.UtcNow; // Update related trades if (position.Open != null) { await UpdateTradeAsync(position.Open); } if (position.StopLoss != null) { await UpdateTradeAsync(position.StopLoss); } if (position.TakeProfit1 != null) { await UpdateTradeAsync(position.TakeProfit1); } if (position.TakeProfit2 != null) { await UpdateTradeAsync(position.TakeProfit2); } await _context.SaveChangesAsync(); } } public async Task UpdateTradeAsync(Trade trade) { var entity = _context.Trades .AsTracking() .FirstOrDefault(t => t.ExchangeOrderId == trade.ExchangeOrderId); if (entity != null) { entity.Date = trade.Date; entity.Direction = trade.Direction; entity.Status = trade.Status; entity.TradeType = trade.TradeType; entity.Ticker = trade.Ticker; entity.Quantity = trade.Quantity; entity.Price = trade.Price; entity.Leverage = trade.Leverage; entity.ExchangeOrderId = trade.ExchangeOrderId; entity.Message = trade.Message; entity.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); } } public async Task> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier) { var positions = await _context.Positions .AsNoTracking() .Include(p => p.User) .Include(p => p.OpenTrade) .Include(p => p.StopLossTrade) .Include(p => p.TakeProfit1Trade) .Include(p => p.TakeProfit2Trade) .Where(p => p.InitiatorIdentifier == initiatorIdentifier) .ToListAsync() .ConfigureAwait(false); return PostgreSqlMappers.Map(positions); } public async Task> GetPositionsByInitiatorIdentifiersAsync( IEnumerable initiatorIdentifiers) { var identifiersList = initiatorIdentifiers.ToList(); if (!identifiersList.Any()) { return Enumerable.Empty(); } var positions = await _context.Positions .AsNoTracking() .Include(p => p.User) .Include(p => p.OpenTrade) .Include(p => p.StopLossTrade) .Include(p => p.TakeProfit1Trade) .Include(p => p.TakeProfit2Trade) .Where(p => identifiersList.Contains(p.InitiatorIdentifier)) .ToListAsync() .ConfigureAwait(false); return PostgreSqlMappers.Map(positions); } public async Task> GetAllPositionsAsync() { var positions = await _context.Positions .AsNoTracking() .Include(p => p.User) .Include(p => p.OpenTrade) .Include(p => p.StopLossTrade) .Include(p => p.TakeProfit1Trade) .Include(p => p.TakeProfit2Trade) .ToListAsync() .ConfigureAwait(false); return PostgreSqlMappers.Map(positions); } public async Task GetGlobalPnLFromPositionsAsync() { try { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); // Calculate total PnL from all finished positions (closed positions) // Only include positions that are Finished or Flipped (closed positions) // Include UiFees and GasFees in the calculation for accurate net PnL var totalPnL = await _context.Positions .AsNoTracking() .Where(p => p.Status == PositionStatus.Finished || p.Status == PositionStatus.Flipped) .SumAsync(p => p.ProfitAndLoss - p.UiFees - p.GasFees) .ConfigureAwait(false); return totalPnL; } finally { await PostgreSqlConnectionHelper.SafeCloseConnectionAsync(_context); } } #endregion #region Signal Methods public IEnumerable GetSignalsByUser(User user) { return GetSignalsByUserAsync(user).Result; } public async Task> GetSignalsByUserAsync(User user) { var userId = user?.Id ?? 0; var signals = await _context.Signals .AsNoTracking() .Include(s => s.User) .Where(s => s.UserId == userId) .ToListAsync() .ConfigureAwait(false); return PostgreSqlMappers.Map(signals); } public Signal GetSignalByIdentifier(string identifier, User user = null) { return GetSignalByIdentifierAsync(identifier, user).Result; } public async Task GetSignalByIdentifierAsync(string identifier, User user = null) { var userId = user?.Id ?? 0; var signal = await _context.Signals .AsNoTracking() .Include(s => s.User) .FirstOrDefaultAsync(s => s.Identifier == identifier && s.UserId == userId) .ConfigureAwait(false); return PostgreSqlMappers.Map(signal); } public async Task InsertSignalAsync(Signal signal) { var signalUserId = signal.User?.Id ?? 0; // Check if signal already exists with the same identifier, date, and user var existingSignal = _context.Signals .AsNoTracking() .FirstOrDefault(s => s.Identifier == signal.Identifier && s.Date == signal.Date && s.UserId == signalUserId); if (existingSignal != null) { throw new InvalidOperationException( $"Signal with identifier '{signal.Identifier}' and date '{signal.Date}' already exists for this user"); } var entity = PostgreSqlMappers.Map(signal); _context.Signals.Add(entity); await _context.SaveChangesAsync(); } public async Task GetStrategyByNameUserAsync(string name, User user) { var userId = user?.Id ?? 0; var indicator = await _context.Indicators .AsNoTracking() .Include(i => i.User) .FirstOrDefaultAsync(i => i.Name == name && i.UserId == userId); return PostgreSqlMappers.Map(indicator); } public async Task GetScenarioByNameUserAsync(string scenarioName, User user) { var userId = user?.Id ?? 0; var scenario = await _context.Scenarios .AsNoTracking() .Include(s => s.User) .FirstOrDefaultAsync(s => s.Name == scenarioName && s.UserId == userId); return PostgreSqlMappers.Map(scenario); } #endregion }