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:
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user