Trading bot grain (#33)

* Trading bot Grain

* Fix a bit more of the trading bot

* Advance on the tradingbot grain

* Fix build

* Fix db script

* Fix user login

* Fix a bit backtest

* Fix cooldown and backtest

* start fixing bot start

* Fix startup

* Setup local db

* Fix build and update candles and scenario

* Add bot registry

* Add reminder

* Updateing the grains

* fix bootstraping

* Save stats on tick

* Save bot data every tick

* Fix serialization

* fix save bot stats

* Fix get candles

* use dict instead of list for position

* Switch hashset to dict

* Fix a bit

* Fix bot launch and bot view

* add migrations

* Remove the tolist

* Add agent grain

* Save agent summary

* clean

* Add save bot

* Update get bots

* Add get bots

* Fix stop/restart

* fix Update config

* Update scanner table on new backtest saved

* Fix backtestRowDetails.tsx

* Fix agentIndex

* Update agentIndex

* Fix more things

* Update user cache

* Fix

* Fix account load/start/restart/run
This commit is contained in:
Oda
2025-08-04 23:07:06 +02:00
committed by GitHub
parent cd378587aa
commit 082ae8714b
215 changed files with 9562 additions and 14028 deletions

View File

@@ -0,0 +1,232 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Statistics;
using Managing.Infrastructure.Databases.PostgreSql;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Database.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)
{
// Ensure the User entity exists and is saved
if (agentSummary.User != null)
{
var existingUser = await _context.Users
.FirstOrDefaultAsync(u => u.Id == agentSummary.UserId);
if (existingUser == null)
{
// User doesn't exist, save it first
var userEntity = PostgreSqlMappers.Map(agentSummary.User);
await _context.Users.AddAsync(userEntity);
await _context.SaveChangesAsync();
_logger.LogInformation("User created for AgentSummary with ID {UserId}", agentSummary.UserId);
}
}
var existing = await _context.AgentSummaries
.FirstOrDefaultAsync(a => a.UserId == agentSummary.UserId);
if (existing == null)
{
await InsertAsync(agentSummary);
}
else
{
// Update existing record - modify the tracked entity directly
MapToEntity(agentSummary, existing);
existing.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<(IEnumerable<AgentSummary> Results, int TotalCount)> GetPaginatedAsync(
int page,
int pageSize,
SortableFields sortBy,
string sortOrder,
IEnumerable<string>? agentNames = null)
{
// 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));
}
// Get total count before applying pagination
var totalCount = await query.CountAsync();
// Apply sorting
var isDescending = sortOrder.ToLowerInvariant() == "desc";
query = sortBy switch
{
SortableFields.TotalPnL => isDescending
? query.OrderByDescending(a => a.TotalPnL)
: query.OrderBy(a => a.TotalPnL),
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),
_ => 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,
TotalROI = domain.TotalROI,
Wins = domain.Wins,
Losses = domain.Losses,
Runtime = domain.Runtime,
CreatedAt = domain.CreatedAt,
UpdatedAt = domain.UpdatedAt,
ActiveStrategiesCount = domain.ActiveStrategiesCount,
TotalVolume = domain.TotalVolume
};
}
private static void MapToEntity(AgentSummary domain, AgentSummaryEntity entity)
{
entity.UserId = domain.UserId;
entity.AgentName = domain.AgentName;
entity.TotalPnL = domain.TotalPnL;
entity.TotalROI = domain.TotalROI;
entity.Wins = domain.Wins;
entity.Losses = domain.Losses;
entity.Runtime = domain.Runtime;
entity.ActiveStrategiesCount = domain.ActiveStrategiesCount;
entity.TotalVolume = domain.TotalVolume;
}
private static AgentSummary MapToDomain(AgentSummaryEntity entity)
{
return new AgentSummary
{
Id = entity.Id,
UserId = entity.UserId,
AgentName = entity.AgentName,
TotalPnL = entity.TotalPnL,
TotalROI = entity.TotalROI,
Wins = entity.Wins,
Losses = entity.Losses,
Runtime = entity.Runtime,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
ActiveStrategiesCount = entity.ActiveStrategiesCount,
TotalVolume = entity.TotalVolume,
User = PostgreSqlMappers.Map(entity.User)
};
}
}

View File

@@ -0,0 +1,20 @@
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
public class AgentSummaryEntity
{
public int Id { get; set; }
public int UserId { get; set; }
public string AgentName { get; set; }
public decimal TotalPnL { get; set; }
public decimal TotalROI { get; set; }
public int Wins { get; set; }
public int Losses { get; set; }
public DateTime? Runtime { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public int ActiveStrategiesCount { get; set; }
public decimal TotalVolume { get; set; }
// Navigation property
public UserEntity User { get; set; }
}

View File

@@ -1,36 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("BotBackups")]
public class BotBackupEntity
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Identifier { get; set; }
[MaxLength(255)]
public string? UserName { get; set; }
public int? UserId { get; set; }
// Navigation properties
[ForeignKey("UserId")]
public UserEntity? User { get; set; }
/// <summary>
/// Bot configuration and state data stored as JSON string
/// </summary>
[Column(TypeName = "text")]
public string Data { get; set; }
public BotStatus LastStatus { get; set; }
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Bots")]
public class BotEntity
{
[Key] public Guid Identifier { get; set; }
[Required] [MaxLength(255)] public required string Name { get; set; }
public Ticker Ticker { get; set; }
public int UserId { get; set; }
[Required] [ForeignKey("UserId")] public required UserEntity User { get; set; }
public BotStatus Status { get; set; }
public DateTime CreateDate { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime StartupTime { get; set; }
public int TradeWins { get; set; }
public int TradeLosses { get; set; }
public decimal Pnl { get; set; }
public decimal Roi { get; set; }
public decimal Volume { get; set; }
public decimal Fees { get; set; }
}

View File

@@ -7,55 +7,41 @@ namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Positions")]
public class PositionEntity
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Identifier { get; set; }
[Key] [Required] public Guid Identifier { get; set; }
public DateTime Date { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal ProfitAndLoss { get; set; }
[Column(TypeName = "decimal(18,8)")] public decimal ProfitAndLoss { get; set; }
public TradeDirection OriginDirection { get; set; }
public PositionStatus Status { get; set; }
public Ticker Ticker { get; set; }
public PositionInitiator Initiator { get; set; }
[MaxLength(255)]
public string SignalIdentifier { get; set; }
[MaxLength(255)]
public string AccountName { get; set; }
[MaxLength(255)]
public string? UserName { get; set; }
[MaxLength(255)] public string SignalIdentifier { get; set; }
[MaxLength(255)] public string AccountName { get; set; }
[MaxLength(255)] public string? UserName { get; set; }
// Foreign keys to trades
public int? OpenTradeId { get; set; }
public int? StopLossTradeId { get; set; }
public int? TakeProfit1TradeId { get; set; }
public int? TakeProfit2TradeId { get; set; }
// Money management data stored as JSON
[Column(TypeName = "text")]
public string? MoneyManagementJson { get; set; }
[Column(TypeName = "text")] public string? MoneyManagementJson { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// Navigation properties
[ForeignKey("OpenTradeId")]
public virtual TradeEntity? OpenTrade { get; set; }
[ForeignKey("StopLossTradeId")]
public virtual TradeEntity? StopLossTrade { get; set; }
[ForeignKey("TakeProfit1TradeId")]
public virtual TradeEntity? TakeProfit1Trade { get; set; }
[ForeignKey("TakeProfit2TradeId")]
public virtual TradeEntity? TakeProfit2Trade { get; set; }
}
[ForeignKey("OpenTradeId")] public virtual TradeEntity? OpenTrade { get; set; }
[ForeignKey("StopLossTradeId")] public virtual TradeEntity? StopLossTrade { get; set; }
[ForeignKey("TakeProfit1TradeId")] public virtual TradeEntity? TakeProfit1Trade { get; set; }
[ForeignKey("TakeProfit2TradeId")] public virtual TradeEntity? TakeProfit2Trade { get; set; }
}

View File

@@ -1,10 +1,14 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Users")]
public class UserEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string? AgentName { get; set; }
[Key] public int Id { get; set; }
[Required] [MaxLength(255)] public required string Name { get; set; }
[MaxLength(255)] public string? AgentName { get; set; }
public string? AvatarUrl { get; set; }
public string? TelegramChannel { get; set; }
}
}

View File

@@ -29,9 +29,10 @@ public class ManagingDbContext : DbContext
public DbSet<SpotlightOverviewEntity> SpotlightOverviews { get; set; }
public DbSet<TraderEntity> Traders { get; set; }
public DbSet<FundingRateEntity> FundingRates { get; set; }
public DbSet<AgentSummaryEntity> AgentSummaries { get; set; }
// Bot entities
public DbSet<BotBackupEntity> BotBackups { get; set; }
public DbSet<BotEntity> Bots { get; set; }
// Settings entities
public DbSet<MoneyManagementEntity> MoneyManagements { get; set; }
@@ -46,6 +47,10 @@ public class ManagingDbContext : DbContext
{
base.OnModelCreating(modelBuilder);
// Configure schema for Orleans tables (if needed for future organization)
// Orleans tables will remain in the default schema for now
// This can be changed later if needed by configuring specific schemas
// Configure Account entity
modelBuilder.Entity<AccountEntity>(entity =>
{
@@ -280,8 +285,7 @@ public class ManagingDbContext : DbContext
// Configure Position entity
modelBuilder.Entity<PositionEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
entity.HasKey(e => e.Identifier);
entity.Property(e => e.ProfitAndLoss).HasColumnType("decimal(18,8)");
entity.Property(e => e.OriginDirection).IsRequired().HasConversion<string>();
entity.Property(e => e.Status).IsRequired().HasConversion<string>();
@@ -348,7 +352,6 @@ public class ManagingDbContext : DbContext
});
// Configure TopVolumeTicker entity
modelBuilder.Entity<TopVolumeTickerEntity>(entity =>
{
@@ -425,22 +428,26 @@ public class ManagingDbContext : DbContext
});
// Configure BotBackup entity
modelBuilder.Entity<BotBackupEntity>(entity =>
modelBuilder.Entity<BotEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.HasKey(e => e.Identifier);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserName).HasMaxLength(255);
entity.Property(e => e.Data).IsRequired().HasColumnType("text");
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.CreateDate).IsRequired();
entity.Property(e => e.StartupTime).IsRequired();
entity.Property(e => e.TradeWins).IsRequired();
entity.Property(e => e.TradeLosses).IsRequired();
entity.Property(e => e.Pnl).HasPrecision(18, 8);
entity.Property(e => e.Roi).HasPrecision(18, 8);
entity.Property(e => e.Volume).HasPrecision(18, 8);
entity.Property(e => e.Fees).HasPrecision(18, 8);
// Create indexes
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.LastStatus);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.CreateDate);
// Composite index for user bots
entity.HasIndex(e => new { e.UserName, e.CreateDate });
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
@@ -517,5 +524,105 @@ public class ManagingDbContext : DbContext
entity.HasIndex(e => e.CacheKey).IsUnique();
entity.HasIndex(e => e.CreatedAt);
});
// Configure AgentSummary entity
modelBuilder.Entity<AgentSummaryEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.UserId).IsRequired();
entity.Property(e => e.AgentName).IsRequired().HasMaxLength(255);
entity.Property(e => e.TotalPnL).HasColumnType("decimal(18,8)");
entity.Property(e => e.TotalROI).HasColumnType("decimal(18,8)");
entity.Property(e => e.Wins).IsRequired();
entity.Property(e => e.Losses).IsRequired();
entity.Property(e => e.Runtime);
entity.Property(e => e.CreatedAt).IsRequired();
entity.Property(e => e.UpdatedAt).IsRequired();
entity.Property(e => e.ActiveStrategiesCount).IsRequired();
entity.Property(e => e.TotalVolume).HasPrecision(18, 8);
// Create indexes for common queries
entity.HasIndex(e => e.UserId).IsUnique();
entity.HasIndex(e => e.AgentName);
entity.HasIndex(e => e.TotalPnL);
entity.HasIndex(e => e.UpdatedAt);
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
}
/// <summary>
/// Ensures Orleans tables are properly initialized in the database.
/// This method can be called during application startup to verify Orleans infrastructure.
/// </summary>
public async Task EnsureOrleansTablesExistAsync()
{
// Orleans tables are automatically created by the Orleans framework
// when using AdoNetClustering and AdoNetReminderService.
// This method serves as a verification point and can be extended
// for custom Orleans table management if needed.
// For now, we just ensure the database is accessible
await Database.CanConnectAsync();
}
/// <summary>
/// Gets Orleans table statistics for monitoring purposes.
/// This helps track Orleans table sizes and performance.
/// </summary>
public async Task<Dictionary<string, long>> GetOrleansTableStatsAsync()
{
var stats = new Dictionary<string, long>();
// Orleans table names
var orleansTables = new[]
{
"orleansmembershiptable",
"orleansmembershipversiontable",
"orleansquery",
"orleansreminderstable",
"orleansstorage"
};
foreach (var tableName in orleansTables)
{
try
{
var count = await Database.SqlQueryRaw<long>($"SELECT COUNT(*) FROM {tableName}").FirstOrDefaultAsync();
stats[tableName] = count;
}
catch
{
// Table might not exist yet (normal during startup)
stats[tableName] = -1;
}
}
return stats;
}
/// <summary>
/// Database organization strategy:
/// - Application tables: Default schema (public)
/// - Orleans tables: Default schema (public) - managed by Orleans framework
/// - Future consideration: Move Orleans tables to 'orleans' schema if needed
///
/// Benefits of current approach:
/// - Single database simplifies deployment and backup
/// - Orleans tables are automatically managed by the framework
/// - No additional configuration complexity
/// - Easier monitoring and maintenance
/// </summary>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
// Add any additional configuration here if needed
}
}

View File

@@ -1,6 +1,7 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Bots;
using Microsoft.EntityFrameworkCore;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql;
@@ -13,7 +14,7 @@ public class PostgreSqlBotRepository : IBotRepository
_context = context;
}
public async Task InsertBotAsync(BotBackup bot)
public async Task InsertBotAsync(Bot bot)
{
bot.CreateDate = DateTime.UtcNow;
var entity = PostgreSqlMappers.Map(bot);
@@ -22,18 +23,24 @@ public class PostgreSqlBotRepository : IBotRepository
{
var userEntity = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Name == bot.User.Name)
.FirstOrDefaultAsync(u => u.Id == bot.User.Id)
.ConfigureAwait(false);
entity.UserId = userEntity?.Id;
if (userEntity == null)
{
throw new InvalidOperationException($"User with id '{bot.User.Id}' not found");
}
entity.UserId = userEntity.Id;
}
await _context.BotBackups.AddAsync(entity).ConfigureAwait(false);
await _context.Bots.AddAsync(entity).ConfigureAwait(false);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task<IEnumerable<BotBackup>> GetBotsAsync()
public async Task<IEnumerable<Bot>> GetBotsAsync()
{
var entities = await _context.BotBackups
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.ToListAsync()
@@ -42,9 +49,9 @@ public class PostgreSqlBotRepository : IBotRepository
return PostgreSqlMappers.Map(entities);
}
public async Task UpdateBackupBot(BotBackup bot)
public async Task UpdateBot(Bot bot)
{
var existingEntity = await _context.BotBackups
var existingEntity = await _context.Bots
.AsTracking()
.FirstOrDefaultAsync(b => b.Identifier == bot.Identifier)
.ConfigureAwait(false);
@@ -54,18 +61,25 @@ public class PostgreSqlBotRepository : IBotRepository
throw new InvalidOperationException($"Bot backup with identifier '{bot.Identifier}' not found");
}
// Update the entity properties
existingEntity.Data = bot.SerializeData(); // Use the serialized data string
existingEntity.LastStatus = bot.LastStatus;
// Update the existing entity properties directly instead of creating a new one
existingEntity.Name = bot.Name;
existingEntity.Ticker = bot.Ticker;
existingEntity.Status = bot.Status;
existingEntity.StartupTime = bot.StartupTime;
existingEntity.TradeWins = bot.TradeWins;
existingEntity.TradeLosses = bot.TradeLosses;
existingEntity.Pnl = bot.Pnl;
existingEntity.Roi = bot.Roi;
existingEntity.Volume = bot.Volume;
existingEntity.Fees = bot.Fees;
existingEntity.UpdatedAt = DateTime.UtcNow;
existingEntity.UserName = bot.User?.Name;
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task DeleteBotBackup(string identifier)
public async Task DeleteBot(Guid identifier)
{
var entity = await _context.BotBackups
var entity = await _context.Bots
.AsTracking()
.FirstOrDefaultAsync(b => b.Identifier == identifier)
.ConfigureAwait(false);
@@ -75,17 +89,142 @@ public class PostgreSqlBotRepository : IBotRepository
throw new InvalidOperationException($"Bot backup with identifier '{identifier}' not found");
}
_context.BotBackups.Remove(entity);
_context.Bots.Remove(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task<BotBackup?> GetBotByIdentifierAsync(string identifier)
public async Task<Bot> GetBotByIdentifierAsync(Guid identifier)
{
var entity = await _context.BotBackups
var entity = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.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)
.Where(b => b.UserId == id)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
public async Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status)
{
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.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)
.FirstOrDefaultAsync(b => b.Name == name)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entity);
}
public async Task<IEnumerable<Bot>> GetBotsByIdsAsync(IEnumerable<Guid> identifiers)
{
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Where(b => identifiers.Contains(b.Identifier))
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
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,
string sortBy = "CreateDate",
string sortDirection = "Desc")
{
// Build the query with filters
var query = _context.Bots
.AsNoTracking()
.Include(m => m.User)
.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}%"));
}
// Get total count before applying pagination
var totalCount = await query.CountAsync().ConfigureAwait(false);
// Apply sorting
query = sortBy.ToLower() switch
{
"name" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.Name)
: query.OrderByDescending(b => b.Name),
"ticker" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.Ticker)
: query.OrderByDescending(b => b.Ticker),
"status" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.Status)
: query.OrderByDescending(b => b.Status),
"startuptime" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.StartupTime)
: query.OrderByDescending(b => b.StartupTime),
"pnl" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.Pnl)
: query.OrderByDescending(b => b.Pnl),
"winrate" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.TradeWins / (b.TradeWins + b.TradeLosses))
: query.OrderByDescending(b => b.TradeWins / (b.TradeWins + b.TradeLosses)),
"agentname" => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.User.AgentName)
: query.OrderByDescending(b => b.User.AgentName),
_ => sortDirection.ToLower() == "asc"
? query.OrderBy(b => b.CreateDate)
: query.OrderByDescending(b => b.CreateDate) // Default to 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);
}
}

View File

@@ -3,6 +3,7 @@ using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Statistics;
@@ -13,7 +14,6 @@ using Managing.Domain.Workers;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Newtonsoft.Json;
using static Managing.Common.Enums;
using SystemJsonSerializer = System.Text.Json.JsonSerializer;
namespace Managing.Infrastructure.Databases.PostgreSql;
@@ -125,7 +125,8 @@ public static class PostgreSqlMappers
Name = entity.Name,
AgentName = entity.AgentName,
AvatarUrl = entity.AvatarUrl,
TelegramChannel = entity.TelegramChannel
TelegramChannel = entity.TelegramChannel,
Id = entity.Id // Assuming Id is the primary key for UserEntity
};
}
@@ -183,7 +184,9 @@ public static class PostgreSqlMappers
{
try
{
geneticRequest.EligibleIndicators = SystemJsonSerializer.Deserialize<List<IndicatorType>>(entity.EligibleIndicatorsJson) ?? new List<IndicatorType>();
geneticRequest.EligibleIndicators =
SystemJsonSerializer.Deserialize<List<IndicatorType>>(entity.EligibleIndicatorsJson) ??
new List<IndicatorType>();
}
catch
{
@@ -263,10 +266,12 @@ public static class PostgreSqlMappers
// Deserialize JSON fields using MongoMappers for compatibility
var config = JsonConvert.DeserializeObject<TradingBotConfig>(entity.ConfigJson);
var positions = JsonConvert.DeserializeObject<List<Position>>(entity.PositionsJson) ?? new List<Position>();
var signals = JsonConvert.DeserializeObject<List<LightSignal>>(entity.SignalsJson) ?? new List<LightSignal>();
var statistics = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)
var positionsList = JsonConvert.DeserializeObject<List<Position>>(entity.PositionsJson) ?? new List<Position>();
var positions = positionsList.ToDictionary(p => p.Identifier, p => p);
var signalsList = JsonConvert.DeserializeObject<List<LightSignal>>(entity.SignalsJson) ?? new List<LightSignal>();
var signals = signalsList.ToDictionary(s => s.Identifier, s => s);
var statistics = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)
: null;
var backtest = new Backtest(config, positions, signals)
@@ -303,8 +308,8 @@ public static class PostgreSqlMappers
GrowthPercentage = backtest.GrowthPercentage,
HodlPercentage = backtest.HodlPercentage,
ConfigJson = JsonConvert.SerializeObject(backtest.Config),
PositionsJson = JsonConvert.SerializeObject(backtest.Positions),
SignalsJson = JsonConvert.SerializeObject(backtest.Signals),
PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList()),
SignalsJson = JsonConvert.SerializeObject(backtest.Signals.Values.ToList()),
StartDate = backtest.StartDate,
EndDate = backtest.EndDate,
MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement),
@@ -354,7 +359,8 @@ public static class PostgreSqlMappers
{
try
{
bundleRequest.Results = JsonConvert.DeserializeObject<List<string>>(entity.ResultsJson) ?? new List<string>();
bundleRequest.Results = JsonConvert.DeserializeObject<List<string>>(entity.ResultsJson) ??
new List<string>();
}
catch
{
@@ -426,7 +432,7 @@ public static class PostgreSqlMappers
return new Scenario(entity.Name, entity.LoopbackPeriod)
{
User = entity.UserName != null ? new User { Name = entity.UserName } : null,
Indicators = new List<Indicator>() // Will be populated separately when needed
Indicators = new List<IndicatorBase>() // Will be populated separately when needed
};
}
@@ -443,11 +449,11 @@ public static class PostgreSqlMappers
}
// Indicator mappings
public static Indicator Map(IndicatorEntity entity)
public static IndicatorBase Map(IndicatorEntity entity)
{
if (entity == null) return null;
return new Indicator(entity.Name, entity.Type)
return new IndicatorBase(entity.Name, entity.Type)
{
SignalType = entity.SignalType,
MinimumHistory = entity.MinimumHistory,
@@ -463,26 +469,26 @@ public static class PostgreSqlMappers
};
}
public static IndicatorEntity Map(Indicator indicator)
public static IndicatorEntity Map(IndicatorBase indicatorBase)
{
if (indicator == null) return null;
if (indicatorBase == null) return null;
return new IndicatorEntity
{
Name = indicator.Name,
Type = indicator.Type,
Name = indicatorBase.Name,
Type = indicatorBase.Type,
Timeframe = Timeframe.FifteenMinutes, // Default timeframe
SignalType = indicator.SignalType,
MinimumHistory = indicator.MinimumHistory,
Period = indicator.Period,
FastPeriods = indicator.FastPeriods,
SlowPeriods = indicator.SlowPeriods,
SignalPeriods = indicator.SignalPeriods,
Multiplier = indicator.Multiplier,
SmoothPeriods = indicator.SmoothPeriods,
StochPeriods = indicator.StochPeriods,
CyclePeriods = indicator.CyclePeriods,
UserName = indicator.User?.Name
SignalType = indicatorBase.SignalType,
MinimumHistory = indicatorBase.MinimumHistory,
Period = indicatorBase.Period,
FastPeriods = indicatorBase.FastPeriods,
SlowPeriods = indicatorBase.SlowPeriods,
SignalPeriods = indicatorBase.SignalPeriods,
Multiplier = indicatorBase.Multiplier,
SmoothPeriods = indicatorBase.SmoothPeriods,
StochPeriods = indicatorBase.StochPeriods,
CyclePeriods = indicatorBase.CyclePeriods,
UserName = indicatorBase.User?.Name
};
}
@@ -491,8 +497,8 @@ public static class PostgreSqlMappers
{
if (entity == null) return null;
var candle = !string.IsNullOrEmpty(entity.CandleJson)
? JsonConvert.DeserializeObject<Candle>(entity.CandleJson)
var candle = !string.IsNullOrEmpty(entity.CandleJson)
? JsonConvert.DeserializeObject<Candle>(entity.CandleJson)
: null;
return new Signal(
@@ -541,7 +547,8 @@ public static class PostgreSqlMappers
var moneyManagement = new MoneyManagement(); // Default money management
if (!string.IsNullOrEmpty(entity.MoneyManagementJson))
{
moneyManagement = JsonConvert.DeserializeObject<MoneyManagement>(entity.MoneyManagementJson) ?? new MoneyManagement();
moneyManagement = JsonConvert.DeserializeObject<MoneyManagement>(entity.MoneyManagementJson) ??
new MoneyManagement();
}
var position = new Position(
@@ -590,7 +597,9 @@ public static class PostgreSqlMappers
SignalIdentifier = position.SignalIdentifier,
AccountName = position.AccountName,
UserName = position.User?.Name,
MoneyManagementJson = position.MoneyManagement != null ? JsonConvert.SerializeObject(position.MoneyManagement) : null
MoneyManagementJson = position.MoneyManagement != null
? JsonConvert.SerializeObject(position.MoneyManagement)
: null
};
}
@@ -636,16 +645,15 @@ public static class PostgreSqlMappers
}
// Collection mappings
public static IEnumerable<Scenario> Map(IEnumerable<ScenarioEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<Scenario>();
}
public static IEnumerable<Indicator> Map(IEnumerable<IndicatorEntity> entities)
public static IEnumerable<IndicatorBase> Map(IEnumerable<IndicatorEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<Indicator>();
return entities?.Select(Map) ?? Enumerable.Empty<IndicatorBase>();
}
public static IEnumerable<Signal> Map(IEnumerable<SignalEntity> entities)
@@ -663,48 +671,57 @@ public static class PostgreSqlMappers
#region Bot Mappings
// BotBackup mappings
public static BotBackup Map(BotBackupEntity entity)
public static Bot Map(BotEntity entity)
{
if (entity == null) return null;
var botBackup = new BotBackup
var bot = new Bot
{
Identifier = entity.Identifier,
User = entity.User != null ? Map(entity.User) : null,
LastStatus = entity.LastStatus,
CreateDate = entity.CreateDate
Status = entity.Status,
CreateDate = entity.CreateDate,
Name = entity.Name,
Ticker = entity.Ticker,
StartupTime = entity.StartupTime,
TradeWins = entity.TradeWins,
TradeLosses = entity.TradeLosses,
Pnl = entity.Pnl,
Roi = entity.Roi,
Volume = entity.Volume,
Fees = entity.Fees
};
// Deserialize the JSON data using the helper method
botBackup.DeserializeData(entity.Data);
return botBackup;
return bot;
}
public static BotBackupEntity Map(BotBackup botBackup)
public static BotEntity Map(Bot bot)
{
if (botBackup == null) return null;
if (bot == null) return null;
return new BotBackupEntity
return new BotEntity
{
Identifier = botBackup.Identifier,
UserName = botBackup.User?.Name,
User = botBackup.User != null ? Map(botBackup.User) : null,
Data = botBackup.SerializeData(), // Serialize the data using the helper method
LastStatus = botBackup.LastStatus,
CreateDate = botBackup.CreateDate,
Identifier = bot.Identifier,
UserId = bot.User.Id,
User = bot.User != null ? Map(bot.User) : null,
Status = bot.Status,
CreateDate = bot.CreateDate,
Name = bot.Name,
Ticker = bot.Ticker,
StartupTime = bot.StartupTime,
TradeWins = bot.TradeWins,
TradeLosses = bot.TradeLosses,
Pnl = bot.Pnl,
Roi = bot.Roi,
Volume = bot.Volume,
Fees = bot.Fees,
UpdatedAt = DateTime.UtcNow
};
}
public static IEnumerable<BotBackup> Map(IEnumerable<BotBackupEntity> entities)
public static IEnumerable<Bot> Map(IEnumerable<BotEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<BotBackup>();
}
public static IEnumerable<BotBackupEntity> Map(IEnumerable<BotBackup> botBackups)
{
return botBackups?.Select(Map) ?? Enumerable.Empty<BotBackupEntity>();
return entities?.Select(Map) ?? Enumerable.Empty<Bot>();
}
#endregion
@@ -763,7 +780,8 @@ public static class PostgreSqlMappers
{
try
{
overview.Spotlights = SystemJsonSerializer.Deserialize<List<Spotlight>>(entity.SpotlightsJson) ?? new List<Spotlight>();
overview.Spotlights = SystemJsonSerializer.Deserialize<List<Spotlight>>(entity.SpotlightsJson) ??
new List<Spotlight>();
}
catch (JsonException)
{
@@ -913,4 +931,4 @@ public static class PostgreSqlMappers
}
#endregion
}
}

View File

@@ -86,8 +86,8 @@ public class PostgreSqlTradingRepository : ITradingRepository
var existingScenario = await _context.Scenarios
.AsNoTracking()
.FirstOrDefaultAsync(s => s.Name == scenario.Name &&
((scenario.User == null && s.UserName == null) ||
(scenario.User != null && s.UserName == scenario.User.Name)));
((scenario.User == null && s.UserName == null) ||
(scenario.User != null && s.UserName == scenario.User.Name)));
if (existingScenario != null)
{
@@ -107,8 +107,8 @@ public class PostgreSqlTradingRepository : ITradingRepository
var indicatorEntity = await _context.Indicators
.AsNoTracking()
.FirstOrDefaultAsync(i => i.Name == indicator.Name &&
((indicator.User == null && i.UserName == null) ||
(indicator.User != null && i.UserName == indicator.User.Name)));
((indicator.User == null && i.UserName == null) ||
(indicator.User != null && i.UserName == indicator.User.Name)));
if (indicatorEntity != null)
{
@@ -120,6 +120,7 @@ public class PostgreSqlTradingRepository : ITradingRepository
_context.ScenarioIndicators.Add(junction);
}
}
await _context.SaveChangesAsync();
}
}
@@ -135,7 +136,7 @@ public class PostgreSqlTradingRepository : ITradingRepository
entity.LoopbackPeriod = scenario.LoopbackPeriod ?? 1;
entity.UserName = scenario.User?.Name;
entity.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
@@ -149,7 +150,7 @@ public class PostgreSqlTradingRepository : ITradingRepository
var indicator = _context.Indicators
.AsTracking()
.FirstOrDefault(i => i.Name == name);
if (indicator != null)
{
_context.Indicators.Remove(indicator);
@@ -164,7 +165,7 @@ public class PostgreSqlTradingRepository : ITradingRepository
await _context.SaveChangesAsync();
}
public async Task<IEnumerable<Indicator>> GetIndicatorsAsync()
public async Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync()
{
var indicators = await _context.Indicators
.AsNoTracking()
@@ -174,7 +175,7 @@ public class PostgreSqlTradingRepository : ITradingRepository
return PostgreSqlMappers.Map(indicators);
}
public async Task<IEnumerable<Indicator>> GetStrategiesAsync()
public async Task<IEnumerable<IndicatorBase>> GetStrategiesAsync()
{
var indicators = await _context.Indicators
.AsNoTracking()
@@ -183,7 +184,7 @@ public class PostgreSqlTradingRepository : ITradingRepository
return PostgreSqlMappers.Map(indicators);
}
public async Task<Indicator> GetStrategyByNameAsync(string name)
public async Task<IndicatorBase> GetStrategyByNameAsync(string name)
{
var indicator = await _context.Indicators
.AsNoTracking()
@@ -193,48 +194,48 @@ public class PostgreSqlTradingRepository : ITradingRepository
return PostgreSqlMappers.Map(indicator);
}
public async Task InsertStrategyAsync(Indicator indicator)
public async Task InsertIndicatorAsync(IndicatorBase indicatorBase)
{
// Check if indicator already exists for the same user
var existingIndicator = await _context.Indicators
.AsNoTracking()
.FirstOrDefaultAsync(i => i.Name == indicator.Name &&
((indicator.User == null && i.UserName == null) ||
(indicator.User != null && i.UserName == indicator.User.Name)));
.FirstOrDefaultAsync(i => i.Name == indicatorBase.Name &&
((indicatorBase.User == null && i.UserName == null) ||
(indicatorBase.User != null && i.UserName == indicatorBase.User.Name)));
if (existingIndicator != null)
{
throw new InvalidOperationException(
$"Indicator with name '{indicator.Name}' already exists for user '{indicator.User?.Name}'");
$"Indicator with name '{indicatorBase.Name}' already exists for user '{indicatorBase.User?.Name}'");
}
var entity = PostgreSqlMappers.Map(indicator);
var entity = PostgreSqlMappers.Map(indicatorBase);
_context.Indicators.Add(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateStrategyAsync(Indicator indicator)
public async Task UpdateStrategyAsync(IndicatorBase indicatorBase)
{
var entity = _context.Indicators
.AsTracking()
.FirstOrDefault(i => i.Name == indicator.Name);
.FirstOrDefault(i => i.Name == indicatorBase.Name);
if (entity != null)
{
entity.Type = indicator.Type;
entity.SignalType = indicator.SignalType;
entity.MinimumHistory = indicator.MinimumHistory;
entity.Period = indicator.Period;
entity.FastPeriods = indicator.FastPeriods;
entity.SlowPeriods = indicator.SlowPeriods;
entity.SignalPeriods = indicator.SignalPeriods;
entity.Multiplier = indicator.Multiplier;
entity.SmoothPeriods = indicator.SmoothPeriods;
entity.StochPeriods = indicator.StochPeriods;
entity.CyclePeriods = indicator.CyclePeriods;
entity.UserName = indicator.User?.Name;
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.UserName = indicatorBase.User?.Name;
entity.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
@@ -242,15 +243,9 @@ public class PostgreSqlTradingRepository : ITradingRepository
#endregion
#region Position Methods
public Position GetPositionByIdentifier(string identifier)
{
return GetPositionByIdentifierAsync(identifier).Result;
}
public async Task<Position> GetPositionByIdentifierAsync(string identifier)
public async Task<Position> GetPositionByIdentifierAsync(Guid identifier)
{
var position = await _context.Positions
.AsNoTracking()
@@ -310,8 +305,8 @@ public class PostgreSqlTradingRepository : ITradingRepository
var existingPosition = await _context.Positions
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Identifier == position.Identifier &&
((position.User == null && p.UserName == null) ||
(position.User != null && p.UserName == position.User.Name)));
((position.User == null && p.UserName == null) ||
(position.User != null && p.UserName == position.User.Name)));
if (existingPosition != null)
{
@@ -320,7 +315,7 @@ public class PostgreSqlTradingRepository : ITradingRepository
}
var entity = PostgreSqlMappers.Map(position);
// Handle related trades
if (position.Open != null)
{
@@ -370,11 +365,11 @@ public class PostgreSqlTradingRepository : ITradingRepository
entity.ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0;
entity.Status = position.Status;
entity.SignalIdentifier = position.SignalIdentifier;
entity.MoneyManagementJson = position.MoneyManagement != null
? JsonConvert.SerializeObject(position.MoneyManagement)
entity.MoneyManagementJson = position.MoneyManagement != null
? JsonConvert.SerializeObject(position.MoneyManagement)
: null;
entity.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
@@ -393,7 +388,7 @@ public class PostgreSqlTradingRepository : ITradingRepository
var signals = await _context.Signals
.AsNoTracking()
.Where(s => (user == null && s.UserName == null) ||
(user != null && s.UserName == user.Name))
(user != null && s.UserName == user.Name))
.ToListAsync()
.ConfigureAwait(false);
@@ -410,8 +405,8 @@ public class PostgreSqlTradingRepository : ITradingRepository
var signal = await _context.Signals
.AsNoTracking()
.FirstOrDefaultAsync(s => s.Identifier == identifier &&
((user == null && s.UserName == null) ||
(user != null && s.UserName == user.Name)))
((user == null && s.UserName == null) ||
(user != null && s.UserName == user.Name)))
.ConfigureAwait(false);
return PostgreSqlMappers.Map(signal);
@@ -423,9 +418,9 @@ public class PostgreSqlTradingRepository : ITradingRepository
var existingSignal = _context.Signals
.AsNoTracking()
.FirstOrDefault(s => s.Identifier == signal.Identifier &&
s.Date == signal.Date &&
((s.UserName == null && signal.User == null) ||
(s.UserName != null && signal.User != null && s.UserName == signal.User.Name)));
s.Date == signal.Date &&
((s.UserName == null && signal.User == null) ||
(s.UserName != null && signal.User != null && s.UserName == signal.User.Name)));
if (existingSignal != null)
{
@@ -438,7 +433,25 @@ public class PostgreSqlTradingRepository : ITradingRepository
await _context.SaveChangesAsync();
}
public async Task<IndicatorBase> GetStrategyByNameUserAsync(string name, User user)
{
var indicator = await _context.Indicators
.AsNoTracking()
.FirstOrDefaultAsync(i => i.Name == name &&
((user == null && i.UserName == null) ||
(user != null && i.UserName == user.Name)));
return PostgreSqlMappers.Map(indicator);
}
public async Task<Scenario> GetScenarioByNameUserAsync(string scenarioName, User user)
{
var scenario = await _context.Scenarios
.AsNoTracking()
.FirstOrDefaultAsync(s => s.Name == scenarioName &&
((user == null && s.UserName == null) ||
(user != null && s.UserName == user.Name)));
return PostgreSqlMappers.Map(scenario);
}
#endregion
}
}