Postgres (#30)

* Add postgres

* Migrate users

* Migrate geneticRequest

* Try to fix Concurrent call

* Fix asyncawait

* Fix async and concurrent

* Migrate backtests

* Add cache for user by address

* Fix backtest migration

* Fix not open connection

* Fix backtest command error

* Fix concurrent

* Fix all concurrency

* Migrate TradingRepo

* Fix scenarios

* Migrate statistic repo

* Save botbackup

* Add settings et moneymanagement

* Add bot postgres

* fix a bit more backups

* Fix bot model

* Fix loading backup

* Remove cache market for read positions

* Add workers to postgre

* Fix workers api

* Reduce get Accounts for workers

* Migrate synth to postgre

* Fix backtest saved

* Remove mongodb

* botservice decorrelation

* Fix tradingbot scope call

* fix tradingbot

* fix concurrent

* Fix scope for genetics

* Fix account over requesting

* Fix bundle backtest worker

* fix a lot of things

* fix tab backtest

* Remove optimized moneymanagement

* Add light signal to not use User and too much property

* Make money management lighter

* insert indicators to awaitable

* Migrate add strategies to await

* Refactor scenario and indicator retrieval to use asynchronous methods throughout the application

* add more async await

* Add services

* Fix and clean

* Fix bot a bit

* Fix bot and add message for cooldown

* Remove fees

* Add script to deploy db

* Update dfeeploy script

* fix script

* Add idempotent script and backup

* finish script migration

* Fix did user and agent name on start bot
This commit is contained in:
Oda
2025-07-27 15:42:17 +02:00
committed by GitHub
parent 361bfbf6e8
commit 422fecea7b
294 changed files with 23953 additions and 7272 deletions

View File

@@ -0,0 +1,11 @@
namespace Managing.Infrastructure.Databases.PostgreSql.Configurations;
public class PostgreSqlSettings : IPostgreSqlSettings
{
public string ConnectionString { get; set; } = string.Empty;
}
public interface IPostgreSqlSettings
{
string ConnectionString { get; set; }
}

View File

@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ManagingDbContext>
{
public ManagingDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<ManagingDbContext>();
// Use a default connection string for design-time migrations
// This should match your local PostgreSQL setup
optionsBuilder.UseNpgsql("Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres");
return new ManagingDbContext(optionsBuilder.Options);
}
}

View File

@@ -0,0 +1,20 @@
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
public class AccountEntity
{
public int Id { get; set; }
public string Name { get; set; }
public TradingExchanges Exchange { get; set; }
public AccountType Type { get; set; }
public string? Key { get; set; }
public string? Secret { get; set; }
public int? UserId { get; set; }
// Navigation properties
public UserEntity? User { get; set; }
// Store balances as JSON if needed, or skip them entirely
// public string? BalancesJson { get; set; }
}

View File

@@ -0,0 +1,83 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Backtests")]
public class BacktestEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Identifier { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string RequestId { get; set; } = string.Empty;
[Required]
[Column(TypeName = "decimal(18,8)")]
public decimal FinalPnl { get; set; }
[Required]
public int WinRate { get; set; }
[Required]
[Column(TypeName = "decimal(18,8)")]
public decimal GrowthPercentage { get; set; }
[Required]
[Column(TypeName = "decimal(18,8)")]
public decimal HodlPercentage { get; set; }
[Required]
[Column(TypeName = "jsonb")]
public string ConfigJson { get; set; } = string.Empty;
[Required]
[Column(TypeName = "jsonb")]
public string PositionsJson { get; set; } = string.Empty;
[Required]
[Column(TypeName = "jsonb")]
public string SignalsJson { get; set; } = string.Empty;
[Required]
public DateTime StartDate { get; set; }
[Required]
public DateTime EndDate { get; set; }
[Required]
[Column(TypeName = "jsonb")]
public string MoneyManagementJson { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string UserName { get; set; } = string.Empty;
[Column(TypeName = "jsonb")]
public string? StatisticsJson { get; set; }
[Required]
[Column(TypeName = "decimal(18,8)")]
public decimal Fees { get; set; }
[Required]
public double Score { get; set; }
[Column(TypeName = "text")]
public string ScoreMessage { get; set; } = string.Empty;
[Column(TypeName = "text")]
public string? Metadata { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,36 @@
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,70 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Managing.Domain.Backtests;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("BundleBacktestRequests")]
public class BundleBacktestRequestEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string RequestId { get; set; } = string.Empty;
[Required]
[MaxLength(255)]
public string UserName { get; set; } = string.Empty;
// Foreign key to User entity
public int? UserId { get; set; }
// Navigation property to User entity
public UserEntity? User { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? CompletedAt { get; set; }
[Required]
public BundleBacktestRequestStatus Status { get; set; }
[Required]
[Column(TypeName = "text")]
public string BacktestRequestsJson { get; set; } = string.Empty;
[Required]
public int TotalBacktests { get; set; }
[Required]
public int CompletedBacktests { get; set; }
[Required]
public int FailedBacktests { get; set; }
[Column(TypeName = "text")]
public string? ErrorMessage { get; set; }
[Column(TypeName = "text")]
public string? ProgressInfo { get; set; }
[MaxLength(500)]
public string? CurrentBacktest { get; set; }
public int? EstimatedTimeRemainingSeconds { get; set; }
[Required]
[MaxLength(255)]
public string Name { get; set; } = string.Empty;
[Required]
[Column(TypeName = "jsonb")]
public string ResultsJson { get; set; } = "[]";
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("FundingRates")]
public class FundingRateEntity
{
[Key]
public int Id { get; set; }
public Ticker Ticker { get; set; }
public TradingExchanges Exchange { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Rate { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal OpenInterest { get; set; }
public DateTime Date { get; set; }
public TradeDirection Direction { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,38 @@
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
public class GeneticRequestEntity
{
public int Id { get; set; }
public string RequestId { get; set; }
public int? UserId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string Status { get; set; } // GeneticRequestStatus as string
public Ticker Ticker { get; set; }
public Timeframe Timeframe { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public decimal Balance { get; set; }
public int PopulationSize { get; set; }
public int Generations { get; set; }
public double MutationRate { get; set; }
public GeneticSelectionMethod SelectionMethod { get; set; }
public GeneticCrossoverMethod CrossoverMethod { get; set; }
public GeneticMutationMethod MutationMethod { get; set; }
public int ElitismPercentage { get; set; }
public double MaxTakeProfit { get; set; }
public string? EligibleIndicatorsJson { get; set; } // Store List<IndicatorType> as JSON
public double? BestFitness { get; set; }
public string? BestIndividual { get; set; }
public string? ErrorMessage { get; set; }
public string? ProgressInfo { get; set; }
public string? BestChromosome { get; set; }
public double? BestFitnessSoFar { get; set; }
public int CurrentGeneration { get; set; }
// Navigation properties
public UserEntity? User { get; set; }
}

View File

@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Indicators")]
public class IndicatorEntity
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Name { get; set; }
public IndicatorType Type { get; set; }
public Timeframe Timeframe { get; set; }
public SignalType SignalType { get; set; }
public int MinimumHistory { get; set; }
public int? Period { get; set; }
public int? FastPeriods { get; set; }
public int? SlowPeriods { get; set; }
public int? SignalPeriods { get; set; }
public double? Multiplier { get; set; }
public int? StochPeriods { get; set; }
public int? SmoothPeriods { get; set; }
public int? CyclePeriods { get; set; }
[MaxLength(255)]
public string? UserName { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// Navigation property for the many-to-many relationship with scenarios
public virtual ICollection<ScenarioIndicatorEntity> ScenarioIndicators { get; set; } = new List<ScenarioIndicatorEntity>();
}

View File

@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("MoneyManagements")]
public class MoneyManagementEntity
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Name { get; set; }
[Required]
public Timeframe Timeframe { get; set; }
[Required]
[Column(TypeName = "decimal(18,8)")]
public decimal StopLoss { get; set; }
[Required]
[Column(TypeName = "decimal(18,8)")]
public decimal TakeProfit { get; set; }
[Required]
[Column(TypeName = "decimal(18,8)")]
public decimal Leverage { get; set; }
[MaxLength(255)]
public string? UserName { get; set; }
public int? UserId { get; set; }
// Navigation properties
[ForeignKey("UserId")]
public UserEntity? User { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,61 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
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; }
public DateTime Date { 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; }
// 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; }
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; }
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Scenarios")]
public class ScenarioEntity
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Name { get; set; }
public int LoopbackPeriod { get; set; }
[MaxLength(255)]
public string? UserName { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// Navigation property for the many-to-many relationship with indicators
public virtual ICollection<ScenarioIndicatorEntity> ScenarioIndicators { get; set; } = new List<ScenarioIndicatorEntity>();
}

View File

@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("ScenarioIndicators")]
public class ScenarioIndicatorEntity
{
[Key]
public int Id { get; set; }
public int ScenarioId { get; set; }
public int IndicatorId { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation properties
[ForeignKey("ScenarioId")]
public virtual ScenarioEntity Scenario { get; set; } = null!;
[ForeignKey("IndicatorId")]
public virtual IndicatorEntity Indicator { get; set; } = null!;
}

View File

@@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Signals")]
public class SignalEntity
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Identifier { get; set; }
public TradeDirection Direction { get; set; }
public Confidence Confidence { get; set; }
public DateTime Date { get; set; }
public Ticker Ticker { get; set; }
public SignalStatus Status { get; set; }
public Timeframe Timeframe { get; set; }
public IndicatorType Type { get; set; }
public SignalType SignalType { get; set; }
[MaxLength(255)]
public string IndicatorName { get; set; }
[MaxLength(255)]
public string? UserName { get; set; }
// Candle data stored as JSON
[Column(TypeName = "text")]
public string? CandleJson { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("SpotlightOverviews")]
public class SpotlightOverviewEntity
{
[Key]
public int Id { get; set; }
public Guid Identifier { get; set; }
public DateTime DateTime { get; set; }
public int ScenarioCount { get; set; }
/// <summary>
/// JSON column containing the complex nested spotlights data
/// This stores the List<SpotlightDto> as JSON to avoid complex normalization
/// </summary>
[Column(TypeName = "jsonb")]
public string SpotlightsJson { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
public class SynthMinersLeaderboardEntity
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(32)]
public string Asset { get; set; }
[Required]
public int TimeIncrement { get; set; }
public DateTime? SignalDate { get; set; }
[Required]
public bool IsBacktest { get; set; }
[Column(TypeName = "jsonb")]
public string MinersData { get; set; } // JSON serialized List<MinerInfo>
[Required]
[MaxLength(255)]
public string CacheKey { get; set; }
[Required]
public DateTime CreatedAt { get; set; }
}

View File

@@ -0,0 +1,38 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
public class SynthPredictionEntity
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(32)]
public string Asset { get; set; }
[Required]
public int MinerUid { get; set; }
[Required]
public int TimeIncrement { get; set; }
[Required]
public int TimeLength { get; set; }
public DateTime? SignalDate { get; set; }
[Required]
public bool IsBacktest { get; set; }
[Column(TypeName = "jsonb")]
public string PredictionData { get; set; } // JSON serialized MinerPrediction
[Required]
[MaxLength(255)]
public string CacheKey { get; set; }
[Required]
public DateTime CreatedAt { get; set; }
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("TopVolumeTickers")]
public class TopVolumeTickerEntity
{
[Key]
public int Id { get; set; }
public Ticker Ticker { get; set; }
public DateTime Date { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Volume { get; set; }
public int Rank { get; set; }
public TradingExchanges Exchange { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Trades")]
public class TradeEntity
{
[Key]
public int Id { get; set; }
public DateTime Date { get; set; }
public TradeDirection Direction { get; set; }
public TradeStatus Status { get; set; }
public TradeType TradeType { get; set; }
public Ticker Ticker { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Fee { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Quantity { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Price { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Leverage { get; set; }
[MaxLength(255)]
public string? ExchangeOrderId { get; set; }
[Column(TypeName = "text")]
public string? Message { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("Traders")]
public class TraderEntity
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Address { get; set; }
public int Winrate { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Pnl { get; set; }
public int TradeCount { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal AverageWin { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal AverageLoss { get; set; }
[Column(TypeName = "decimal(18,8)")]
public decimal Roi { get; set; }
/// <summary>
/// Indicates whether this is a best trader (true) or bad trader (false)
/// This allows us to use one table for both types
/// </summary>
public bool IsBestTrader { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,10 @@
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
public class UserEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string? AgentName { get; set; }
public string? AvatarUrl { get; set; }
public string? TelegramChannel { get; set; }
}

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("Workers")]
public class WorkerEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public WorkerType WorkerType { get; set; }
[Required]
public DateTime StartTime { get; set; }
public DateTime? LastRunTime { get; set; }
[Required]
public int ExecutionCount { get; set; }
[Required]
public long DelayTicks { get; set; } // TimeSpan is not supported, store as ticks
[Required]
public bool IsActive { get; set; }
}

View File

@@ -0,0 +1,521 @@
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class ManagingDbContext : DbContext
{
public ManagingDbContext(DbContextOptions<ManagingDbContext> options) : base(options)
{
}
public DbSet<AccountEntity> Accounts { get; set; }
public DbSet<UserEntity> Users { get; set; }
public DbSet<GeneticRequestEntity> GeneticRequests { get; set; }
public DbSet<BacktestEntity> Backtests { get; set; }
public DbSet<BundleBacktestRequestEntity> BundleBacktestRequests { get; set; }
// Trading entities
public DbSet<ScenarioEntity> Scenarios { get; set; }
public DbSet<IndicatorEntity> Indicators { get; set; }
public DbSet<ScenarioIndicatorEntity> ScenarioIndicators { get; set; }
public DbSet<SignalEntity> Signals { get; set; }
public DbSet<PositionEntity> Positions { get; set; }
public DbSet<TradeEntity> Trades { get; set; }
// Statistics entities
public DbSet<TopVolumeTickerEntity> TopVolumeTickers { get; set; }
public DbSet<SpotlightOverviewEntity> SpotlightOverviews { get; set; }
public DbSet<TraderEntity> Traders { get; set; }
public DbSet<FundingRateEntity> FundingRates { get; set; }
// Bot entities
public DbSet<BotBackupEntity> BotBackups { get; set; }
// Settings entities
public DbSet<MoneyManagementEntity> MoneyManagements { get; set; }
// Worker entities
public DbSet<WorkerEntity> Workers { get; set; }
public DbSet<SynthMinersLeaderboardEntity> SynthMinersLeaderboards { get; set; }
public DbSet<SynthPredictionEntity> SynthPredictions { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure Account entity
modelBuilder.Entity<AccountEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Key).HasMaxLength(500);
entity.Property(e => e.Secret).HasMaxLength(500);
entity.Property(e => e.Exchange)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.Type)
.IsRequired()
.HasConversion<string>(); // Store enum as string
// Create unique index on account name
entity.HasIndex(e => e.Name).IsUnique();
entity.HasIndex(e => e.Key);
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure User entity
modelBuilder.Entity<UserEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.AgentName).HasMaxLength(255);
entity.Property(e => e.AvatarUrl).HasMaxLength(500);
entity.Property(e => e.TelegramChannel).HasMaxLength(255);
});
// Configure GeneticRequest entity
modelBuilder.Entity<GeneticRequestEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255);
entity.Property(e => e.Status).IsRequired().HasMaxLength(50);
entity.Property(e => e.Ticker)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.Timeframe)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.SelectionMethod)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.CrossoverMethod)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.MutationMethod)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.Balance).HasColumnType("decimal(18,8)");
entity.Property(e => e.BestIndividual).HasMaxLength(4000);
entity.Property(e => e.ErrorMessage).HasMaxLength(2000);
entity.Property(e => e.ProgressInfo).HasMaxLength(4000);
entity.Property(e => e.BestChromosome).HasMaxLength(4000);
entity.Property(e => e.EligibleIndicatorsJson).HasMaxLength(2000);
// Create indexes
entity.HasIndex(e => e.RequestId).IsUnique();
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.CreatedAt);
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure Backtest entity
modelBuilder.Entity<BacktestEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.FinalPnl).HasColumnType("decimal(18,8)");
entity.Property(e => e.GrowthPercentage).HasColumnType("decimal(18,8)");
entity.Property(e => e.HodlPercentage).HasColumnType("decimal(18,8)");
entity.Property(e => e.Fees).HasColumnType("decimal(18,8)");
entity.Property(e => e.ConfigJson).HasColumnType("jsonb");
entity.Property(e => e.PositionsJson).HasColumnType("jsonb");
entity.Property(e => e.SignalsJson).HasColumnType("jsonb");
entity.Property(e => e.MoneyManagementJson).HasColumnType("jsonb");
entity.Property(e => e.StatisticsJson).HasColumnType("jsonb");
entity.Property(e => e.ScoreMessage).HasMaxLength(1000);
entity.Property(e => e.Metadata).HasColumnType("text");
// Create indexes for common queries
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.RequestId);
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.Score);
entity.HasIndex(e => e.FinalPnl);
entity.HasIndex(e => e.WinRate);
entity.HasIndex(e => e.StartDate);
entity.HasIndex(e => e.EndDate);
entity.HasIndex(e => e.CreatedAt);
// Composite indexes for efficient pagination and filtering
entity.HasIndex(e => new { e.UserName, e.Score });
entity.HasIndex(e => new { e.RequestId, e.Score });
});
// Configure BundleBacktestRequest entity
modelBuilder.Entity<BundleBacktestRequestEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.RequestId).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserName).IsRequired().HasMaxLength(255);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Status)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.BacktestRequestsJson).HasColumnType("text");
entity.Property(e => e.ErrorMessage).HasColumnType("text");
entity.Property(e => e.ProgressInfo).HasColumnType("text");
entity.Property(e => e.CurrentBacktest).HasMaxLength(500);
entity.Property(e => e.ResultsJson).HasColumnType("jsonb");
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Create indexes for common queries
entity.HasIndex(e => e.RequestId).IsUnique();
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.CreatedAt);
entity.HasIndex(e => e.CompletedAt);
entity.HasIndex(e => e.UserId);
// Composite index for user queries ordered by creation date
entity.HasIndex(e => new { e.UserName, e.CreatedAt });
});
// Configure Scenario entity
modelBuilder.Entity<ScenarioEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserName).HasMaxLength(255);
// Create indexes
entity.HasIndex(e => e.Name);
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.CreatedAt);
// Composite index for user scenarios
entity.HasIndex(e => new { e.UserName, e.Name });
});
// Configure Indicator entity
modelBuilder.Entity<IndicatorEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Type).IsRequired().HasConversion<string>();
entity.Property(e => e.Timeframe).IsRequired().HasConversion<string>();
entity.Property(e => e.SignalType).IsRequired().HasConversion<string>();
entity.Property(e => e.UserName).HasMaxLength(255);
// Create indexes
entity.HasIndex(e => e.Name);
entity.HasIndex(e => e.Type);
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.CreatedAt);
// Composite index for user indicators
entity.HasIndex(e => new { e.UserName, e.Name });
});
// Configure ScenarioIndicator junction table
modelBuilder.Entity<ScenarioIndicatorEntity>(entity =>
{
entity.HasKey(e => e.Id);
// Configure relationships
entity.HasOne(e => e.Scenario)
.WithMany(s => s.ScenarioIndicators)
.HasForeignKey(e => e.ScenarioId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Indicator)
.WithMany(i => i.ScenarioIndicators)
.HasForeignKey(e => e.IndicatorId)
.OnDelete(DeleteBehavior.Cascade);
// Create indexes
entity.HasIndex(e => e.ScenarioId);
entity.HasIndex(e => e.IndicatorId);
entity.HasIndex(e => new { e.ScenarioId, e.IndicatorId }).IsUnique();
});
// Configure Signal entity
modelBuilder.Entity<SignalEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.Direction).IsRequired().HasConversion<string>();
entity.Property(e => e.Confidence).IsRequired().HasConversion<string>();
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.Timeframe).IsRequired().HasConversion<string>();
entity.Property(e => e.Type).IsRequired().HasConversion<string>();
entity.Property(e => e.SignalType).IsRequired().HasConversion<string>();
entity.Property(e => e.IndicatorName).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserName).HasMaxLength(255);
entity.Property(e => e.CandleJson).HasColumnType("text");
// Create indexes
entity.HasIndex(e => e.Identifier);
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.Date);
entity.HasIndex(e => e.Ticker);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.CreatedAt);
// Composite indexes for common queries
entity.HasIndex(e => new { e.UserName, e.Date });
entity.HasIndex(e => new { e.Identifier, e.Date, e.UserName }).IsUnique();
});
// Configure Position entity
modelBuilder.Entity<PositionEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
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>();
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.Initiator).IsRequired().HasConversion<string>();
entity.Property(e => e.SignalIdentifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.AccountName).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserName).HasMaxLength(255);
entity.Property(e => e.MoneyManagementJson).HasColumnType("text");
// Configure relationships with trades
entity.HasOne(e => e.OpenTrade)
.WithMany()
.HasForeignKey(e => e.OpenTradeId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(e => e.StopLossTrade)
.WithMany()
.HasForeignKey(e => e.StopLossTradeId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(e => e.TakeProfit1Trade)
.WithMany()
.HasForeignKey(e => e.TakeProfit1TradeId)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(e => e.TakeProfit2Trade)
.WithMany()
.HasForeignKey(e => e.TakeProfit2TradeId)
.OnDelete(DeleteBehavior.SetNull);
// Create indexes
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.Initiator);
entity.HasIndex(e => e.Date);
entity.HasIndex(e => e.CreatedAt);
// Composite indexes
entity.HasIndex(e => new { e.UserName, e.Identifier });
});
// Configure Trade entity
modelBuilder.Entity<TradeEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Direction).IsRequired().HasConversion<string>();
entity.Property(e => e.Status).IsRequired().HasConversion<string>();
entity.Property(e => e.TradeType).IsRequired().HasConversion<string>();
entity.Property(e => e.Ticker).IsRequired().HasConversion<string>();
entity.Property(e => e.Fee).HasColumnType("decimal(18,8)");
entity.Property(e => e.Quantity).HasColumnType("decimal(18,8)");
entity.Property(e => e.Price).HasColumnType("decimal(18,8)");
entity.Property(e => e.Leverage).HasColumnType("decimal(18,8)");
entity.Property(e => e.ExchangeOrderId).HasMaxLength(255);
entity.Property(e => e.Message).HasColumnType("text");
// Create indexes
entity.HasIndex(e => e.Date);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.ExchangeOrderId);
entity.HasIndex(e => e.CreatedAt);
});
// Configure TopVolumeTicker entity
modelBuilder.Entity<TopVolumeTickerEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Volume).HasPrecision(18, 8);
// Create indexes
entity.HasIndex(e => e.Ticker);
entity.HasIndex(e => e.Date);
entity.HasIndex(e => e.Exchange);
entity.HasIndex(e => e.Rank);
// Composite indexes for efficient queries
entity.HasIndex(e => new { e.Exchange, e.Date });
entity.HasIndex(e => new { e.Date, e.Rank });
});
// Configure SpotlightOverview entity
modelBuilder.Entity<SpotlightOverviewEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired();
entity.Property(e => e.SpotlightsJson).HasColumnType("jsonb");
// Create indexes
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.DateTime);
entity.HasIndex(e => e.ScenarioCount);
// Composite index for efficient queries
entity.HasIndex(e => new { e.DateTime, e.ScenarioCount });
});
// Configure Trader entity
modelBuilder.Entity<TraderEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Address).IsRequired().HasMaxLength(255);
entity.Property(e => e.Pnl).HasPrecision(18, 8);
entity.Property(e => e.AverageWin).HasPrecision(18, 8);
entity.Property(e => e.AverageLoss).HasPrecision(18, 8);
entity.Property(e => e.Roi).HasPrecision(18, 8);
// Create indexes
entity.HasIndex(e => e.Address);
entity.HasIndex(e => e.IsBestTrader);
entity.HasIndex(e => e.Winrate);
entity.HasIndex(e => e.Pnl);
entity.HasIndex(e => e.Roi);
// Composite indexes for efficient queries
entity.HasIndex(e => new { e.IsBestTrader, e.Winrate });
entity.HasIndex(e => new { e.IsBestTrader, e.Roi });
entity.HasIndex(e => new { e.Address, e.IsBestTrader }).IsUnique();
});
// Configure FundingRate entity
modelBuilder.Entity<FundingRateEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Rate).HasPrecision(18, 8);
entity.Property(e => e.OpenInterest).HasPrecision(18, 8);
// Create indexes
entity.HasIndex(e => e.Ticker);
entity.HasIndex(e => e.Exchange);
entity.HasIndex(e => e.Date);
entity.HasIndex(e => e.Direction);
// Composite indexes for efficient queries
entity.HasIndex(e => new { e.Ticker, e.Exchange });
entity.HasIndex(e => new { e.Exchange, e.Date });
entity.HasIndex(e => new { e.Ticker, e.Exchange, e.Date }).IsUnique();
});
// Configure BotBackup entity
modelBuilder.Entity<BotBackupEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Identifier).IsRequired().HasMaxLength(255);
entity.Property(e => e.UserName).HasMaxLength(255);
entity.Property(e => e.Data).IsRequired().HasColumnType("text");
// Create indexes
entity.HasIndex(e => e.Identifier).IsUnique();
entity.HasIndex(e => e.UserName);
entity.HasIndex(e => e.LastStatus);
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()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure MoneyManagement entity
modelBuilder.Entity<MoneyManagementEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(255);
entity.Property(e => e.Timeframe).IsRequired().HasConversion<string>();
entity.Property(e => e.StopLoss).HasColumnType("decimal(18,8)");
entity.Property(e => e.TakeProfit).HasColumnType("decimal(18,8)");
entity.Property(e => e.Leverage).HasColumnType("decimal(18,8)");
entity.Property(e => e.UserName).HasMaxLength(255);
// Create indexes
entity.HasIndex(e => e.Name);
entity.HasIndex(e => e.UserName);
// Composite index for user money managements
entity.HasIndex(e => new { e.UserName, e.Name });
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure Worker entity
modelBuilder.Entity<WorkerEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.WorkerType).IsRequired().HasConversion<string>();
entity.Property(e => e.StartTime).IsRequired();
entity.Property(e => e.LastRunTime);
entity.Property(e => e.ExecutionCount).IsRequired();
entity.Property(e => e.DelayTicks).IsRequired();
entity.Property(e => e.IsActive).IsRequired();
entity.HasIndex(e => e.WorkerType).IsUnique();
});
// Configure SynthMinersLeaderboard entity
modelBuilder.Entity<SynthMinersLeaderboardEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Asset).IsRequired().HasMaxLength(32);
entity.Property(e => e.TimeIncrement).IsRequired();
entity.Property(e => e.SignalDate);
entity.Property(e => e.IsBacktest).IsRequired();
entity.Property(e => e.MinersData).HasColumnType("jsonb");
entity.Property(e => e.CacheKey).IsRequired().HasMaxLength(255);
entity.Property(e => e.CreatedAt).IsRequired();
entity.HasIndex(e => e.CacheKey).IsUnique();
entity.HasIndex(e => e.CreatedAt);
});
// Configure SynthPrediction entity
modelBuilder.Entity<SynthPredictionEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Asset).IsRequired().HasMaxLength(32);
entity.Property(e => e.MinerUid).IsRequired();
entity.Property(e => e.TimeIncrement).IsRequired();
entity.Property(e => e.TimeLength).IsRequired();
entity.Property(e => e.SignalDate);
entity.Property(e => e.IsBacktest).IsRequired();
entity.Property(e => e.PredictionData).HasColumnType("jsonb");
entity.Property(e => e.CacheKey).IsRequired().HasMaxLength(255);
entity.Property(e => e.CreatedAt).IsRequired();
entity.HasIndex(e => e.CacheKey).IsUnique();
entity.HasIndex(e => e.CreatedAt);
});
}
}

View File

@@ -0,0 +1,159 @@
using System.Data;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Microsoft.EntityFrameworkCore;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlAccountRepository : IAccountRepository
{
private readonly ManagingDbContext _context;
private readonly ICacheService _cacheService;
public PostgreSqlAccountRepository(ManagingDbContext context, ICacheService cacheService)
{
_context = context;
_cacheService = cacheService;
}
/// <summary>
/// Ensures the database connection is open before executing queries
/// </summary>
private async Task EnsureConnectionOpenAsync()
{
if (_context.Database.GetDbConnection().State != ConnectionState.Open)
{
await _context.Database.OpenConnectionAsync();
}
}
/// <summary>
/// Safely closes the database connection if it was opened by us
/// </summary>
private async Task SafeCloseConnectionAsync()
{
if (_context.Database.GetDbConnection().State == ConnectionState.Open)
{
await _context.Database.CloseConnectionAsync();
}
}
public void DeleteAccountByName(string name)
{
var accountEntity = _context.Accounts
.AsTracking() // Explicitly enable tracking for delete operations
.FirstOrDefault(a => a.Name == name);
if (accountEntity != null)
{
_context.Accounts.Remove(accountEntity);
_context.SaveChanges();
}
}
public async Task<Account> GetAccountByKeyAsync(string key)
{
try
{
await EnsureConnectionOpenAsync();
var accountEntity = await _context.Accounts
.AsNoTracking()
.Include(a => a.User)
.FirstOrDefaultAsync(a => a.Key == key)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(accountEntity);
}
catch (Exception)
{
// If there's an error, try to reset the connection
await SafeCloseConnectionAsync();
throw;
}
}
public async Task<Account> GetAccountByNameAsync(string name)
{
try
{
var cacheKey = $"account_{name}";
var cachedAccount = _cacheService.GetValue<Account>(cacheKey);
if (cachedAccount != null)
{
return cachedAccount;
}
var accountEntity = await _context.Accounts
.AsNoTracking()
.Include(a => a.User)
.FirstOrDefaultAsync(a => a.Name == name)
.ConfigureAwait(false);
var account = PostgreSqlMappers.Map(accountEntity);
_cacheService.SaveValue(cacheKey, account, TimeSpan.FromHours(1));
return account;
}
catch (Exception ex)
{
// If there's an error, try to reset the connection
throw;
}
finally
{
}
}
public async Task<IEnumerable<Account>> GetAccountsAsync()
{
try
{
await EnsureConnectionOpenAsync();
// Use proper async operations with AsNoTracking for optimal performance
var accountEntities = await _context.Accounts
.AsNoTracking()
.Include(a => a.User)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(accountEntities);
}
catch (Exception)
{
// If there's an error, try to reset the connection
await SafeCloseConnectionAsync();
throw;
}
}
public async Task InsertAccountAsync(Account account)
{
var accountEntity = PostgreSqlMappers.Map(account);
// Handle User relationship - check if user exists or create new one
if (account.User != null)
{
var existingUser = await _context.Users
.AsTracking() // Explicitly enable tracking for this operation
.FirstOrDefaultAsync(u => u.Name == account.User.Name)
.ConfigureAwait(false);
if (existingUser != null)
{
accountEntity.UserId = existingUser.Id;
accountEntity.User = null; // Prevent EF from trying to insert duplicate user
}
else
{
// Let EF handle the new user creation
accountEntity.UserId = null;
}
}
// Balances are not stored in PostgreSQL, they remain in domain logic only
_context.Accounts.Add(accountEntity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,730 @@
using System.Diagnostics;
using Exilion.TradingAtomics;
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlBacktestRepository : IBacktestRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlBacktestRepository(ManagingDbContext context)
{
_context = context;
}
// User-specific operations
public void InsertBacktestForUser(User user, Backtest result)
{
ValidateBacktestData(result);
result.User = user;
var entity = PostgreSqlMappers.Map(result);
_context.Backtests.Add(entity);
_context.SaveChanges();
}
/// <summary>
/// Validates that all numeric fields in the backtest are of the correct type
/// </summary>
private void ValidateBacktestData(Backtest backtest)
{
// Ensure FinalPnl is a valid decimal
if (backtest.FinalPnl.GetType() != typeof(decimal))
{
throw new InvalidOperationException(
$"FinalPnl must be of type decimal, but got {backtest.FinalPnl.GetType().Name}");
}
// Ensure other numeric fields are correct
if (backtest.GrowthPercentage.GetType() != typeof(decimal))
{
throw new InvalidOperationException(
$"GrowthPercentage must be of type decimal, but got {backtest.GrowthPercentage.GetType().Name}");
}
if (backtest.HodlPercentage.GetType() != typeof(decimal))
{
throw new InvalidOperationException(
$"HodlPercentage must be of type decimal, but got {backtest.HodlPercentage.GetType().Name}");
}
if (backtest.Score.GetType() != typeof(double))
{
throw new InvalidOperationException(
$"Score must be of type double, but got {backtest.Score.GetType().Name}");
}
if (backtest.WinRate.GetType() != typeof(int))
{
throw new InvalidOperationException(
$"WinRate must be of type int, but got {backtest.WinRate.GetType().Name}");
}
}
public IEnumerable<Backtest> GetBacktestsByUser(User user)
{
var entities = _context.Backtests
.AsNoTracking()
.Where(b => b.UserName == user.Name)
.ToList();
return entities.Select(PostgreSqlMappers.Map);
}
public async Task<IEnumerable<Backtest>> GetBacktestsByUserAsync(User user)
{
var entities = await _context.Backtests
.AsNoTracking()
.Where(b => b.UserName == user.Name)
.ToListAsync()
.ConfigureAwait(false);
return entities.Select(PostgreSqlMappers.Map);
}
public IEnumerable<Backtest> GetBacktestsByRequestId(string requestId)
{
var entities = _context.Backtests
.AsNoTracking()
.Where(b => b.RequestId == requestId)
.ToList();
return entities.Select(PostgreSqlMappers.Map);
}
public async Task<IEnumerable<Backtest>> GetBacktestsByRequestIdAsync(string requestId)
{
var entities = await _context.Backtests
.AsNoTracking()
.Where(b => b.RequestId == requestId)
.ToListAsync()
.ConfigureAwait(false);
return entities.Select(PostgreSqlMappers.Map);
}
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId,
int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var stopwatch = Stopwatch.StartNew();
var baseQuery = _context.Backtests
.AsNoTracking()
.Where(b => b.RequestId == requestId);
var afterQueryMs = stopwatch.ElapsedMilliseconds;
var totalCount = baseQuery.Count();
var afterCountMs = stopwatch.ElapsedMilliseconds;
// Apply sorting
IQueryable<BacktestEntity> sortedQuery = sortBy.ToLower() switch
{
"score" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.Score)
: baseQuery.OrderBy(b => b.Score),
"finalpnl" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.FinalPnl)
: baseQuery.OrderBy(b => b.FinalPnl),
"winrate" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.WinRate)
: baseQuery.OrderBy(b => b.WinRate),
"growthpercentage" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.GrowthPercentage)
: baseQuery.OrderBy(b => b.GrowthPercentage),
"hodlpercentage" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.HodlPercentage)
: baseQuery.OrderBy(b => b.HodlPercentage),
_ => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.Score)
: baseQuery.OrderBy(b => b.Score)
};
var afterSortMs = stopwatch.ElapsedMilliseconds;
var entities = sortedQuery
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
var afterToListMs = stopwatch.ElapsedMilliseconds;
Console.WriteLine(
$"[PostgreSqlBacktestRepo] Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Sort: {afterSortMs - afterCountMs}ms, ToList: {afterToListMs - afterSortMs}ms, Total: {afterToListMs}ms");
var mappedBacktests = entities.Select(entity => new LightBacktest
{
Id = entity.Identifier,
Config = JsonConvert.DeserializeObject<TradingBotConfig>(entity.ConfigJson),
FinalPnl = entity.FinalPnl,
WinRate = entity.WinRate,
GrowthPercentage = entity.GrowthPercentage,
HodlPercentage = entity.HodlPercentage,
StartDate = entity.StartDate,
EndDate = entity.EndDate,
MaxDrawdown = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)?.MaxDrawdown
: null,
Fees = entity.Fees,
SharpeRatio = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)?.SharpeRatio != null
? (double?)JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson).SharpeRatio
: null
: null,
Score = entity.Score,
ScoreMessage = entity.ScoreMessage ?? string.Empty
});
return (mappedBacktests, totalCount);
}
public async Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByRequestIdPaginatedAsync(
string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var stopwatch = Stopwatch.StartNew();
var baseQuery = _context.Backtests
.AsNoTracking()
.Where(b => b.RequestId == requestId);
var afterQueryMs = stopwatch.ElapsedMilliseconds;
var totalCount = await baseQuery.CountAsync().ConfigureAwait(false);
var afterCountMs = stopwatch.ElapsedMilliseconds;
// Apply sorting
IQueryable<BacktestEntity> sortedQuery = sortBy.ToLower() switch
{
"score" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.Score)
: baseQuery.OrderBy(b => b.Score),
"finalpnl" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.FinalPnl)
: baseQuery.OrderBy(b => b.FinalPnl),
"winrate" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.WinRate)
: baseQuery.OrderBy(b => b.WinRate),
"growthpercentage" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.GrowthPercentage)
: baseQuery.OrderBy(b => b.GrowthPercentage),
"hodlpercentage" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.HodlPercentage)
: baseQuery.OrderBy(b => b.HodlPercentage),
_ => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.Score)
: baseQuery.OrderBy(b => b.Score)
};
var afterSortMs = stopwatch.ElapsedMilliseconds;
var entities = await sortedQuery
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync()
.ConfigureAwait(false);
var afterToListMs = stopwatch.ElapsedMilliseconds;
Console.WriteLine(
$"[PostgreSqlBacktestRepo] Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Sort: {afterSortMs - afterCountMs}ms, ToList: {afterToListMs - afterSortMs}ms, Total: {afterToListMs}ms");
var mappedBacktests = entities.Select(entity => new LightBacktest
{
Id = entity.Identifier,
Config = JsonConvert.DeserializeObject<TradingBotConfig>(entity.ConfigJson),
FinalPnl = entity.FinalPnl,
WinRate = entity.WinRate,
GrowthPercentage = entity.GrowthPercentage,
HodlPercentage = entity.HodlPercentage,
StartDate = entity.StartDate,
EndDate = entity.EndDate,
MaxDrawdown = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)?.MaxDrawdown
: null,
Fees = entity.Fees,
SharpeRatio = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)?.SharpeRatio != null
? (double?)JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson).SharpeRatio
: null
: null,
Score = entity.Score,
ScoreMessage = entity.ScoreMessage ?? string.Empty
});
return (mappedBacktests, totalCount);
}
public Backtest GetBacktestByIdForUser(User user, string id)
{
var entity = _context.Backtests
.AsNoTracking()
.FirstOrDefault(b => b.Identifier == id && b.UserName == user.Name);
return entity != null ? PostgreSqlMappers.Map(entity) : null;
}
public async Task<Backtest> GetBacktestByIdForUserAsync(User user, string id)
{
var entity = await _context.Backtests
.AsNoTracking()
.FirstOrDefaultAsync(b => b.Identifier == id && b.UserName == user.Name)
.ConfigureAwait(false);
return entity != null ? PostgreSqlMappers.Map(entity) : null;
}
public void DeleteBacktestByIdForUser(User user, string id)
{
var entity = _context.Backtests
.AsTracking()
.FirstOrDefault(b => b.Identifier == id && b.UserName == user.Name);
if (entity != null)
{
_context.Backtests.Remove(entity);
_context.SaveChanges();
}
}
public async Task DeleteBacktestByIdForUserAsync(User user, string id)
{
var entity = await _context.Backtests
.AsTracking()
.FirstOrDefaultAsync(b => b.Identifier == id && b.UserName == user.Name)
.ConfigureAwait(false);
if (entity != null)
{
_context.Backtests.Remove(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public void DeleteBacktestsByIdsForUser(User user, IEnumerable<string> ids)
{
var entities = _context.Backtests
.AsTracking()
.Where(b => b.UserName == user.Name && ids.Contains(b.Identifier))
.ToList();
if (entities.Any())
{
_context.Backtests.RemoveRange(entities);
_context.SaveChanges();
}
}
public async Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable<string> ids)
{
var entities = await _context.Backtests
.AsTracking()
.Where(b => b.UserName == user.Name && ids.Contains(b.Identifier))
.ToListAsync()
.ConfigureAwait(false);
if (entities.Any())
{
_context.Backtests.RemoveRange(entities);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public void DeleteAllBacktestsForUser(User user)
{
var entities = _context.Backtests
.AsTracking()
.Where(b => b.UserName == user.Name)
.ToList();
if (entities.Any())
{
_context.Backtests.RemoveRange(entities);
_context.SaveChanges();
}
}
public void DeleteBacktestsByRequestId(string requestId)
{
var entities = _context.Backtests
.AsTracking()
.Where(b => b.RequestId == requestId)
.ToList();
if (entities.Any())
{
_context.Backtests.RemoveRange(entities);
_context.SaveChanges();
}
}
public async Task DeleteBacktestsByRequestIdAsync(string requestId)
{
var entities = await _context.Backtests
.AsTracking()
.Where(b => b.RequestId == requestId)
.ToListAsync()
.ConfigureAwait(false);
if (entities.Any())
{
_context.Backtests.RemoveRange(entities);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public (IEnumerable<LightBacktest> Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page,
int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var stopwatch = Stopwatch.StartNew();
var baseQuery = _context.Backtests
.AsNoTracking()
.Where(b => b.UserName == user.Name);
var afterQueryMs = stopwatch.ElapsedMilliseconds;
var totalCount = baseQuery.Count();
var afterCountMs = stopwatch.ElapsedMilliseconds;
// Apply sorting
IQueryable<BacktestEntity> sortedQuery = sortBy.ToLower() switch
{
"score" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.Score)
: baseQuery.OrderBy(b => b.Score),
"finalpnl" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.FinalPnl)
: baseQuery.OrderBy(b => b.FinalPnl),
"winrate" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.WinRate)
: baseQuery.OrderBy(b => b.WinRate),
"growthpercentage" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.GrowthPercentage)
: baseQuery.OrderBy(b => b.GrowthPercentage),
"hodlpercentage" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.HodlPercentage)
: baseQuery.OrderBy(b => b.HodlPercentage),
_ => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.Score)
: baseQuery.OrderBy(b => b.Score)
};
var afterSortMs = stopwatch.ElapsedMilliseconds;
var entities = sortedQuery
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
var afterToListMs = stopwatch.ElapsedMilliseconds;
Console.WriteLine(
$"[PostgreSqlBacktestRepo] User Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Sort: {afterSortMs - afterCountMs}ms, ToList: {afterToListMs - afterSortMs}ms, Total: {afterToListMs}ms");
var mappedBacktests = entities.Select(entity => new LightBacktest
{
Id = entity.Identifier,
Config = JsonConvert.DeserializeObject<TradingBotConfig>(entity.ConfigJson),
FinalPnl = entity.FinalPnl,
WinRate = entity.WinRate,
GrowthPercentage = entity.GrowthPercentage,
HodlPercentage = entity.HodlPercentage,
StartDate = entity.StartDate,
EndDate = entity.EndDate,
MaxDrawdown = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)?.MaxDrawdown
: null,
Fees = entity.Fees,
SharpeRatio = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)?.SharpeRatio != null
? (double?)JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson).SharpeRatio
: null
: null,
Score = entity.Score,
ScoreMessage = entity.ScoreMessage ?? string.Empty
});
return (mappedBacktests, totalCount);
}
public async Task<(IEnumerable<LightBacktest> Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(
User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc")
{
var stopwatch = Stopwatch.StartNew();
var baseQuery = _context.Backtests
.AsNoTracking()
.Where(b => b.UserName == user.Name);
var afterQueryMs = stopwatch.ElapsedMilliseconds;
var totalCount = await baseQuery.CountAsync().ConfigureAwait(false);
var afterCountMs = stopwatch.ElapsedMilliseconds;
// Apply sorting
IQueryable<BacktestEntity> sortedQuery = sortBy.ToLower() switch
{
"score" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.Score)
: baseQuery.OrderBy(b => b.Score),
"finalpnl" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.FinalPnl)
: baseQuery.OrderBy(b => b.FinalPnl),
"winrate" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.WinRate)
: baseQuery.OrderBy(b => b.WinRate),
"growthpercentage" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.GrowthPercentage)
: baseQuery.OrderBy(b => b.GrowthPercentage),
"hodlpercentage" => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.HodlPercentage)
: baseQuery.OrderBy(b => b.HodlPercentage),
_ => sortOrder == "desc"
? baseQuery.OrderByDescending(b => b.Score)
: baseQuery.OrderBy(b => b.Score)
};
var afterSortMs = stopwatch.ElapsedMilliseconds;
var entities = await sortedQuery
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync()
.ConfigureAwait(false);
var afterToListMs = stopwatch.ElapsedMilliseconds;
Console.WriteLine(
$"[PostgreSqlBacktestRepo] User Query: {afterQueryMs}ms, Count: {afterCountMs - afterQueryMs}ms, Sort: {afterSortMs - afterCountMs}ms, ToList: {afterToListMs - afterSortMs}ms, Total: {afterToListMs}ms");
var mappedBacktests = entities.Select(entity => new LightBacktest
{
Id = entity.Identifier,
Config = JsonConvert.DeserializeObject<TradingBotConfig>(entity.ConfigJson),
FinalPnl = entity.FinalPnl,
WinRate = entity.WinRate,
GrowthPercentage = entity.GrowthPercentage,
HodlPercentage = entity.HodlPercentage,
StartDate = entity.StartDate,
EndDate = entity.EndDate,
MaxDrawdown = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)?.MaxDrawdown
: null,
Fees = entity.Fees,
SharpeRatio = !string.IsNullOrEmpty(entity.StatisticsJson)
? JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson)?.SharpeRatio != null
? (double?)JsonConvert.DeserializeObject<PerformanceMetrics>(entity.StatisticsJson).SharpeRatio
: null
: null,
Score = entity.Score,
ScoreMessage = entity.ScoreMessage ?? string.Empty
});
return (mappedBacktests, totalCount);
}
// Bundle backtest methods
public void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest)
{
bundleRequest.User = user;
var entity = PostgreSqlMappers.Map(bundleRequest);
// Set the UserId by finding the user entity
var userEntity = _context.Users.FirstOrDefault(u => u.Name == user.Name);
if (userEntity != null)
{
entity.UserId = userEntity.Id;
}
_context.BundleBacktestRequests.Add(entity);
_context.SaveChanges();
}
public async Task InsertBundleBacktestRequestForUserAsync(User user, BundleBacktestRequest bundleRequest)
{
bundleRequest.User = user;
var entity = PostgreSqlMappers.Map(bundleRequest);
// Set the UserId by finding the user entity
var userEntity = await _context.Users.FirstOrDefaultAsync(u => u.Name == user.Name);
if (userEntity != null)
{
entity.UserId = userEntity.Id;
}
await _context.BundleBacktestRequests.AddAsync(entity);
await _context.SaveChangesAsync();
}
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByUser(User user)
{
var entities = _context.BundleBacktestRequests
.AsNoTracking()
.Include(b => b.User)
.Where(b => b.UserName == user.Name)
.OrderByDescending(b => b.CreatedAt)
.ToList();
return entities.Select(PostgreSqlMappers.Map);
}
public async Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByUserAsync(User user)
{
var entities = await _context.BundleBacktestRequests
.AsNoTracking()
.Include(b => b.User)
.Where(b => b.UserName == user.Name)
.OrderByDescending(b => b.CreatedAt)
.ToListAsync()
.ConfigureAwait(false);
return entities.Select(PostgreSqlMappers.Map);
}
public BundleBacktestRequest? GetBundleBacktestRequestByIdForUser(User user, string id)
{
var entity = _context.BundleBacktestRequests
.AsNoTracking()
.Include(b => b.User)
.FirstOrDefault(b => b.RequestId == id && b.UserName == user.Name);
return entity != null ? PostgreSqlMappers.Map(entity) : null;
}
public async Task<BundleBacktestRequest?> GetBundleBacktestRequestByIdForUserAsync(User user, string id)
{
var entity = await _context.BundleBacktestRequests
.AsNoTracking()
.Include(b => b.User)
.FirstOrDefaultAsync(b => b.RequestId == id && b.UserName == user.Name)
.ConfigureAwait(false);
return entity != null ? PostgreSqlMappers.Map(entity) : null;
}
public void UpdateBundleBacktestRequest(BundleBacktestRequest bundleRequest)
{
var entity = _context.BundleBacktestRequests
.AsTracking()
.FirstOrDefault(b => b.RequestId == bundleRequest.RequestId);
if (entity != null)
{
// Update the entity properties
entity.Status = bundleRequest.Status;
entity.CompletedAt = bundleRequest.CompletedAt;
entity.CompletedBacktests = bundleRequest.CompletedBacktests;
entity.FailedBacktests = bundleRequest.FailedBacktests;
entity.ErrorMessage = bundleRequest.ErrorMessage;
entity.ProgressInfo = bundleRequest.ProgressInfo;
entity.CurrentBacktest = bundleRequest.CurrentBacktest;
entity.EstimatedTimeRemainingSeconds = bundleRequest.EstimatedTimeRemainingSeconds;
entity.UpdatedAt = DateTime.UtcNow;
// Serialize Results to JSON
if (bundleRequest.Results != null && bundleRequest.Results.Any())
{
try
{
entity.ResultsJson = JsonConvert.SerializeObject(bundleRequest.Results);
}
catch
{
entity.ResultsJson = "[]";
}
}
else
{
entity.ResultsJson = "[]";
}
_context.SaveChanges();
}
}
public async Task UpdateBundleBacktestRequestAsync(BundleBacktestRequest bundleRequest)
{
var entity = await _context.BundleBacktestRequests
.AsTracking()
.FirstOrDefaultAsync(b => b.RequestId == bundleRequest.RequestId)
.ConfigureAwait(false);
if (entity != null)
{
// Update the entity properties
entity.Status = bundleRequest.Status;
entity.CompletedAt = bundleRequest.CompletedAt;
entity.CompletedBacktests = bundleRequest.CompletedBacktests;
entity.FailedBacktests = bundleRequest.FailedBacktests;
entity.ErrorMessage = bundleRequest.ErrorMessage;
entity.ProgressInfo = bundleRequest.ProgressInfo;
entity.CurrentBacktest = bundleRequest.CurrentBacktest;
entity.EstimatedTimeRemainingSeconds = bundleRequest.EstimatedTimeRemainingSeconds;
entity.UpdatedAt = DateTime.UtcNow;
// Serialize Results to JSON
if (bundleRequest.Results != null && bundleRequest.Results.Any())
{
try
{
entity.ResultsJson = JsonConvert.SerializeObject(bundleRequest.Results);
}
catch
{
entity.ResultsJson = "[]";
}
}
else
{
entity.ResultsJson = "[]";
}
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public void DeleteBundleBacktestRequestByIdForUser(User user, string id)
{
var entity = _context.BundleBacktestRequests
.AsTracking()
.FirstOrDefault(b => b.RequestId == id && b.UserName == user.Name);
if (entity != null)
{
_context.BundleBacktestRequests.Remove(entity);
_context.SaveChanges();
}
}
public async Task DeleteBundleBacktestRequestByIdForUserAsync(User user, string id)
{
var entity = await _context.BundleBacktestRequests
.AsTracking()
.FirstOrDefaultAsync(b => b.RequestId == id && b.UserName == user.Name)
.ConfigureAwait(false);
if (entity != null)
{
_context.BundleBacktestRequests.Remove(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public IEnumerable<BundleBacktestRequest> GetBundleBacktestRequestsByStatus(BundleBacktestRequestStatus status)
{
var entities = _context.BundleBacktestRequests
.AsNoTracking()
.Include(b => b.User)
.Where(b => b.Status == status)
.ToList();
return entities.Select(PostgreSqlMappers.Map);
}
public async Task<IEnumerable<BundleBacktestRequest>> GetBundleBacktestRequestsByStatusAsync(BundleBacktestRequestStatus status)
{
var entities = await _context.BundleBacktestRequests
.AsNoTracking()
.Include(b => b.User)
.Where(b => b.Status == status)
.ToListAsync()
.ConfigureAwait(false);
return entities.Select(PostgreSqlMappers.Map);
}
}

View File

@@ -0,0 +1,91 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Bots;
using Microsoft.EntityFrameworkCore;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlBotRepository : IBotRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlBotRepository(ManagingDbContext context)
{
_context = context;
}
public async Task InsertBotAsync(BotBackup bot)
{
bot.CreateDate = DateTime.UtcNow;
var entity = PostgreSqlMappers.Map(bot);
// Set the UserId if user is provided
if (bot.User != null)
{
var userEntity = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Name == bot.User.Name)
.ConfigureAwait(false);
entity.UserId = userEntity?.Id;
}
await _context.BotBackups.AddAsync(entity).ConfigureAwait(false);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task<IEnumerable<BotBackup>> GetBotsAsync()
{
var entities = await _context.BotBackups
.AsNoTracking()
.Include(m => m.User)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
public async Task UpdateBackupBot(BotBackup bot)
{
var existingEntity = await _context.BotBackups
.AsTracking()
.FirstOrDefaultAsync(b => b.Identifier == bot.Identifier)
.ConfigureAwait(false);
if (existingEntity == null)
{
throw new InvalidOperationException($"Bot backup with identifier '{bot.Identifier}' not found");
}
// Update the entity properties
existingEntity.Data = bot.SerializeData(); // Use the serialized data string
existingEntity.LastStatus = bot.LastStatus;
existingEntity.UpdatedAt = DateTime.UtcNow;
existingEntity.UserName = bot.User?.Name;
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task DeleteBotBackup(string identifier)
{
var entity = await _context.BotBackups
.AsTracking()
.FirstOrDefaultAsync(b => b.Identifier == identifier)
.ConfigureAwait(false);
if (entity == null)
{
throw new InvalidOperationException($"Bot backup with identifier '{identifier}' not found");
}
_context.BotBackups.Remove(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task<BotBackup?> GetBotByIdentifierAsync(string identifier)
{
var entity = await _context.BotBackups
.AsNoTracking()
.Include(m => m.User)
.FirstOrDefaultAsync(b => b.Identifier == identifier)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entity);
}
}

View File

@@ -0,0 +1,156 @@
using System.Text.Json;
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Backtests;
using Managing.Domain.Users;
using Microsoft.EntityFrameworkCore;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlGeneticRepository : IGeneticRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlGeneticRepository(ManagingDbContext context)
{
_context = context;
}
public void InsertGeneticRequestForUser(User user, GeneticRequest geneticRequest)
{
geneticRequest.User = user;
var geneticRequestEntity = PostgreSqlMappers.Map(geneticRequest);
// Handle User relationship - check if user exists or create new one
if (user != null)
{
var existingUser = _context.Users
.AsTracking() // Explicitly enable tracking for this operation
.FirstOrDefault(u => u.Name == user.Name);
if (existingUser != null)
{
geneticRequestEntity.UserId = existingUser.Id;
geneticRequestEntity.User = null; // Prevent EF from trying to insert duplicate user
}
else
{
// Let EF handle the new user creation
geneticRequestEntity.UserId = null;
}
}
_context.GeneticRequests.Add(geneticRequestEntity);
_context.SaveChanges();
}
public IEnumerable<GeneticRequest> GetGeneticRequestsByUser(User user)
{
// Use synchronous operations and AsNoTracking to avoid concurrency issues
var geneticRequestEntities = _context.GeneticRequests
.AsNoTracking()
.Include(gr => gr.User)
.Where(gr => gr.User != null && gr.User.Name == user.Name)
.OrderByDescending(gr => gr.CreatedAt)
.ToList();
return PostgreSqlMappers.Map(geneticRequestEntities);
}
public GeneticRequest GetGeneticRequestByIdForUser(User user, string id)
{
// Use synchronous operations and AsNoTracking to avoid concurrency issues
var geneticRequestEntity = _context.GeneticRequests
.AsNoTracking()
.Include(gr => gr.User)
.FirstOrDefault(gr => gr.RequestId == id);
// Check if genetic request exists and belongs to the user
if (geneticRequestEntity != null && geneticRequestEntity.User != null &&
geneticRequestEntity.User.Name == user.Name)
{
return PostgreSqlMappers.Map(geneticRequestEntity);
}
return null;
}
public async Task UpdateGeneticRequestAsync(GeneticRequest geneticRequest)
{
var existingEntity = _context.GeneticRequests
.Include(gr => gr.User)
.FirstOrDefault(gr => gr.RequestId == geneticRequest.RequestId);
if (existingEntity != null)
{
// Update the existing entity with new values
existingEntity.CompletedAt = geneticRequest.CompletedAt;
existingEntity.UpdatedAt = DateTime.UtcNow;
existingEntity.Status = geneticRequest.Status.ToString();
existingEntity.BestFitness = geneticRequest.BestFitness;
existingEntity.BestIndividual = geneticRequest.BestIndividual;
existingEntity.ErrorMessage = geneticRequest.ErrorMessage;
existingEntity.ProgressInfo = geneticRequest.ProgressInfo;
existingEntity.BestChromosome = geneticRequest.BestChromosome;
existingEntity.BestFitnessSoFar = geneticRequest.BestFitnessSoFar;
existingEntity.CurrentGeneration = geneticRequest.CurrentGeneration;
// Update EligibleIndicators JSON
if (geneticRequest.EligibleIndicators != null && geneticRequest.EligibleIndicators.Any())
{
try
{
existingEntity.EligibleIndicatorsJson = JsonSerializer.Serialize(geneticRequest.EligibleIndicators);
}
catch
{
existingEntity.EligibleIndicatorsJson = "[]";
}
}
else
{
existingEntity.EligibleIndicatorsJson = "[]";
}
// Only update the tracked entity, do not attach a new one
await _context.SaveChangesAsync();
}
}
public void DeleteGeneticRequestByIdForUser(User user, string id)
{
var geneticRequestEntity = _context.GeneticRequests
.Include(gr => gr.User)
.FirstOrDefault(gr => gr.RequestId == id);
if (geneticRequestEntity != null && geneticRequestEntity.User != null &&
geneticRequestEntity.User.Name == user.Name)
{
_context.GeneticRequests.Remove(geneticRequestEntity);
_context.SaveChanges();
}
}
public void DeleteAllGeneticRequestsForUser(User user)
{
var geneticRequestEntities = _context.GeneticRequests
.Include(gr => gr.User)
.Where(gr => gr.User != null && gr.User.Name == user.Name)
.ToList();
if (geneticRequestEntities.Any())
{
_context.GeneticRequests.RemoveRange(geneticRequestEntities);
_context.SaveChanges();
}
}
public async Task<List<GeneticRequest>> GetGeneticRequestsAsync(GeneticRequestStatus status)
{
var requests = await _context.GeneticRequests
.AsNoTracking()
.Include(gr => gr.User)
.Where(gr => gr.Status == status.ToString())
.OrderBy(gr => gr.CreatedAt)
.ToListAsync();
return PostgreSqlMappers.Map(requests).ToList();
}
}

View File

@@ -0,0 +1,916 @@
using Exilion.TradingAtomics;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
using Managing.Domain.Users;
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;
public static class PostgreSqlMappers
{
#region Account Mappings
public static Account Map(AccountEntity entity)
{
if (entity == null) return null;
return new Account
{
Name = entity.Name,
Exchange = entity.Exchange,
Type = entity.Type,
Key = entity.Key,
Secret = entity.Secret,
User = entity.User != null ? Map(entity.User) : null,
Balances = new List<Balance>() // Empty list for now, balances handled separately if needed
};
}
public static AccountEntity Map(Account account)
{
if (account == null) return null;
return new AccountEntity
{
Name = account.Name,
Exchange = account.Exchange,
Type = account.Type,
Key = account.Key,
Secret = account.Secret,
User = account.User != null ? Map(account.User) : null
};
}
public static IEnumerable<Account> Map(IEnumerable<AccountEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<Account>();
}
#endregion
#region MoneyManagement Mappings
public static MoneyManagement Map(MoneyManagementEntity entity)
{
if (entity == null) return null;
return new MoneyManagement
{
Name = entity.Name,
Timeframe = entity.Timeframe,
StopLoss = entity.StopLoss,
TakeProfit = entity.TakeProfit,
Leverage = entity.Leverage,
User = entity.User != null ? Map(entity.User) : null
};
}
public static MoneyManagementEntity Map(MoneyManagement moneyManagement)
{
if (moneyManagement == null) return null;
return new MoneyManagementEntity
{
Name = moneyManagement.Name,
Timeframe = moneyManagement.Timeframe,
StopLoss = moneyManagement.StopLoss,
TakeProfit = moneyManagement.TakeProfit,
Leverage = moneyManagement.Leverage,
UserName = moneyManagement.User?.Name,
User = moneyManagement.User != null ? Map(moneyManagement.User) : null
};
}
public static IEnumerable<MoneyManagement> Map(IEnumerable<MoneyManagementEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<MoneyManagement>();
}
public static MoneyManagementEntity Map(LightMoneyManagement lightMoneyManagement)
{
if (lightMoneyManagement == null) return null;
return new MoneyManagementEntity
{
Name = lightMoneyManagement.Name,
Timeframe = lightMoneyManagement.Timeframe,
StopLoss = lightMoneyManagement.StopLoss,
TakeProfit = lightMoneyManagement.TakeProfit,
Leverage = lightMoneyManagement.Leverage
};
}
#endregion
#region User Mappings
public static User Map(UserEntity entity)
{
if (entity == null) return null;
return new User
{
Name = entity.Name,
AgentName = entity.AgentName,
AvatarUrl = entity.AvatarUrl,
TelegramChannel = entity.TelegramChannel
};
}
public static UserEntity Map(User user)
{
if (user == null) return null;
return new UserEntity
{
Name = user.Name,
AgentName = user.AgentName,
AvatarUrl = user.AvatarUrl,
TelegramChannel = user.TelegramChannel
};
}
#endregion
#region GeneticRequest Mappings
public static GeneticRequest Map(GeneticRequestEntity entity)
{
if (entity == null) return null;
var geneticRequest = new GeneticRequest(entity.RequestId)
{
User = entity.User != null ? Map(entity.User) : null,
CreatedAt = entity.CreatedAt,
CompletedAt = entity.CompletedAt,
Status = Enum.Parse<GeneticRequestStatus>(entity.Status),
Ticker = entity.Ticker,
Timeframe = entity.Timeframe,
StartDate = entity.StartDate,
EndDate = entity.EndDate,
Balance = entity.Balance,
PopulationSize = entity.PopulationSize,
Generations = entity.Generations,
MutationRate = entity.MutationRate,
SelectionMethod = entity.SelectionMethod,
CrossoverMethod = entity.CrossoverMethod,
MutationMethod = entity.MutationMethod,
ElitismPercentage = entity.ElitismPercentage,
MaxTakeProfit = entity.MaxTakeProfit,
BestFitness = entity.BestFitness,
BestIndividual = entity.BestIndividual,
ErrorMessage = entity.ErrorMessage,
ProgressInfo = entity.ProgressInfo,
BestChromosome = entity.BestChromosome,
BestFitnessSoFar = entity.BestFitnessSoFar,
CurrentGeneration = entity.CurrentGeneration
};
// Deserialize EligibleIndicators from JSON
if (!string.IsNullOrEmpty(entity.EligibleIndicatorsJson))
{
try
{
geneticRequest.EligibleIndicators = SystemJsonSerializer.Deserialize<List<IndicatorType>>(entity.EligibleIndicatorsJson) ?? new List<IndicatorType>();
}
catch
{
geneticRequest.EligibleIndicators = new List<IndicatorType>();
}
}
return geneticRequest;
}
public static GeneticRequestEntity Map(GeneticRequest geneticRequest)
{
if (geneticRequest == null) return null;
var entity = new GeneticRequestEntity
{
RequestId = geneticRequest.RequestId,
User = geneticRequest.User != null ? Map(geneticRequest.User) : null,
CreatedAt = geneticRequest.CreatedAt,
CompletedAt = geneticRequest.CompletedAt,
UpdatedAt = DateTime.UtcNow,
Status = geneticRequest.Status.ToString(),
Ticker = geneticRequest.Ticker,
Timeframe = geneticRequest.Timeframe,
StartDate = geneticRequest.StartDate,
EndDate = geneticRequest.EndDate,
Balance = geneticRequest.Balance,
PopulationSize = geneticRequest.PopulationSize,
Generations = geneticRequest.Generations,
MutationRate = geneticRequest.MutationRate,
SelectionMethod = geneticRequest.SelectionMethod,
CrossoverMethod = geneticRequest.CrossoverMethod,
MutationMethod = geneticRequest.MutationMethod,
ElitismPercentage = geneticRequest.ElitismPercentage,
MaxTakeProfit = geneticRequest.MaxTakeProfit,
BestFitness = geneticRequest.BestFitness,
BestIndividual = geneticRequest.BestIndividual,
ErrorMessage = geneticRequest.ErrorMessage,
ProgressInfo = geneticRequest.ProgressInfo,
BestChromosome = geneticRequest.BestChromosome,
BestFitnessSoFar = geneticRequest.BestFitnessSoFar,
CurrentGeneration = geneticRequest.CurrentGeneration
};
// Serialize EligibleIndicators to JSON
if (geneticRequest.EligibleIndicators != null && geneticRequest.EligibleIndicators.Any())
{
try
{
entity.EligibleIndicatorsJson = SystemJsonSerializer.Serialize(geneticRequest.EligibleIndicators);
}
catch
{
entity.EligibleIndicatorsJson = "[]";
}
}
else
{
entity.EligibleIndicatorsJson = "[]";
}
return entity;
}
public static IEnumerable<GeneticRequest> Map(IEnumerable<GeneticRequestEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<GeneticRequest>();
}
#endregion
#region Backtest Mappings
public static Backtest Map(BacktestEntity entity)
{
if (entity == null) return null;
// 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)
: null;
var backtest = new Backtest(config, positions, signals)
{
Id = entity.Identifier,
FinalPnl = entity.FinalPnl,
WinRate = entity.WinRate,
GrowthPercentage = entity.GrowthPercentage,
HodlPercentage = entity.HodlPercentage,
StartDate = entity.StartDate,
EndDate = entity.EndDate,
User = new User { Name = entity.UserName },
Statistics = statistics,
Fees = entity.Fees,
Score = entity.Score,
ScoreMessage = entity.ScoreMessage,
RequestId = entity.RequestId,
Metadata = entity.Metadata
};
return backtest;
}
public static BacktestEntity Map(Backtest backtest)
{
if (backtest == null) return null;
return new BacktestEntity
{
Identifier = backtest.Id,
RequestId = backtest.RequestId,
FinalPnl = backtest.FinalPnl,
WinRate = backtest.WinRate,
GrowthPercentage = backtest.GrowthPercentage,
HodlPercentage = backtest.HodlPercentage,
ConfigJson = JsonConvert.SerializeObject(backtest.Config),
PositionsJson = JsonConvert.SerializeObject(backtest.Positions),
SignalsJson = JsonConvert.SerializeObject(backtest.Signals),
StartDate = backtest.StartDate,
EndDate = backtest.EndDate,
MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement),
UserName = backtest.User?.Name ?? string.Empty,
StatisticsJson = backtest.Statistics != null ? JsonConvert.SerializeObject(backtest.Statistics) : null,
Fees = backtest.Fees,
Score = backtest.Score,
ScoreMessage = backtest.ScoreMessage ?? string.Empty,
Metadata = backtest.Metadata?.ToString(),
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
}
public static IEnumerable<Backtest> Map(IEnumerable<BacktestEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<Backtest>();
}
#endregion
#region BundleBacktestRequest Mappings
public static BundleBacktestRequest Map(BundleBacktestRequestEntity entity)
{
if (entity == null) return null;
var bundleRequest = new BundleBacktestRequest(entity.RequestId)
{
User = entity.User != null ? Map(entity.User) : new User { Name = entity.UserName },
CreatedAt = entity.CreatedAt,
CompletedAt = entity.CompletedAt,
Status = entity.Status,
BacktestRequestsJson = entity.BacktestRequestsJson,
TotalBacktests = entity.TotalBacktests,
CompletedBacktests = entity.CompletedBacktests,
FailedBacktests = entity.FailedBacktests,
ErrorMessage = entity.ErrorMessage,
ProgressInfo = entity.ProgressInfo,
CurrentBacktest = entity.CurrentBacktest,
EstimatedTimeRemainingSeconds = entity.EstimatedTimeRemainingSeconds,
Name = entity.Name
};
// Deserialize Results from JSON
if (!string.IsNullOrEmpty(entity.ResultsJson))
{
try
{
bundleRequest.Results = JsonConvert.DeserializeObject<List<string>>(entity.ResultsJson) ?? new List<string>();
}
catch
{
bundleRequest.Results = new List<string>();
}
}
return bundleRequest;
}
public static BundleBacktestRequestEntity Map(BundleBacktestRequest bundleRequest)
{
if (bundleRequest == null) return null;
var entity = new BundleBacktestRequestEntity
{
RequestId = bundleRequest.RequestId,
UserName = bundleRequest.User?.Name ?? string.Empty,
UserId = null, // Will be set by the repository when saving
CreatedAt = bundleRequest.CreatedAt,
CompletedAt = bundleRequest.CompletedAt,
Status = bundleRequest.Status,
BacktestRequestsJson = bundleRequest.BacktestRequestsJson,
TotalBacktests = bundleRequest.TotalBacktests,
CompletedBacktests = bundleRequest.CompletedBacktests,
FailedBacktests = bundleRequest.FailedBacktests,
ErrorMessage = bundleRequest.ErrorMessage,
ProgressInfo = bundleRequest.ProgressInfo,
CurrentBacktest = bundleRequest.CurrentBacktest,
EstimatedTimeRemainingSeconds = bundleRequest.EstimatedTimeRemainingSeconds,
Name = bundleRequest.Name,
UpdatedAt = DateTime.UtcNow
};
// Serialize Results to JSON
if (bundleRequest.Results != null && bundleRequest.Results.Any())
{
try
{
entity.ResultsJson = JsonConvert.SerializeObject(bundleRequest.Results);
}
catch
{
entity.ResultsJson = "[]";
}
}
else
{
entity.ResultsJson = "[]";
}
return entity;
}
public static IEnumerable<BundleBacktestRequest> Map(IEnumerable<BundleBacktestRequestEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<BundleBacktestRequest>();
}
#endregion
#region Trading Mappings
// Scenario mappings
public static Scenario Map(ScenarioEntity entity)
{
if (entity == null) return null;
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
};
}
public static ScenarioEntity Map(Scenario scenario)
{
if (scenario == null) return null;
return new ScenarioEntity
{
Name = scenario.Name,
LoopbackPeriod = scenario.LoopbackPeriod ?? 1,
UserName = scenario.User?.Name
};
}
// Indicator mappings
public static Indicator Map(IndicatorEntity entity)
{
if (entity == null) return null;
return new Indicator(entity.Name, entity.Type)
{
SignalType = entity.SignalType,
MinimumHistory = entity.MinimumHistory,
Period = entity.Period,
FastPeriods = entity.FastPeriods,
SlowPeriods = entity.SlowPeriods,
SignalPeriods = entity.SignalPeriods,
Multiplier = entity.Multiplier,
SmoothPeriods = entity.SmoothPeriods,
StochPeriods = entity.StochPeriods,
CyclePeriods = entity.CyclePeriods,
User = entity.UserName != null ? new User { Name = entity.UserName } : null
};
}
public static IndicatorEntity Map(Indicator indicator)
{
if (indicator == null) return null;
return new IndicatorEntity
{
Name = indicator.Name,
Type = indicator.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
};
}
// Signal mappings
public static Signal Map(SignalEntity entity)
{
if (entity == null) return null;
var candle = !string.IsNullOrEmpty(entity.CandleJson)
? JsonConvert.DeserializeObject<Candle>(entity.CandleJson)
: null;
return new Signal(
entity.Ticker,
entity.Direction,
entity.Confidence,
candle,
entity.Date,
TradingExchanges.Evm, // Default exchange
entity.Type,
entity.SignalType,
entity.IndicatorName,
entity.UserName != null ? new User { Name = entity.UserName } : null)
{
Status = entity.Status
};
}
public static SignalEntity Map(Signal signal)
{
if (signal == null) return null;
return new SignalEntity
{
Identifier = signal.Identifier,
Direction = signal.Direction,
Confidence = signal.Confidence,
Date = signal.Date,
Ticker = signal.Ticker,
Status = signal.Status,
Timeframe = signal.Timeframe,
Type = signal.IndicatorType,
SignalType = signal.SignalType,
IndicatorName = signal.IndicatorName,
UserName = signal.User?.Name,
CandleJson = signal.Candle != null ? JsonConvert.SerializeObject(signal.Candle) : null
};
}
// Position mappings
public static Position Map(PositionEntity entity)
{
if (entity == null) return null;
// Deserialize money management
var moneyManagement = new MoneyManagement(); // Default money management
if (!string.IsNullOrEmpty(entity.MoneyManagementJson))
{
moneyManagement = JsonConvert.DeserializeObject<MoneyManagement>(entity.MoneyManagementJson) ?? new MoneyManagement();
}
var position = new Position(
entity.Identifier,
entity.AccountName,
entity.OriginDirection,
entity.Ticker,
moneyManagement,
entity.Initiator,
entity.Date,
entity.UserName != null ? new User { Name = entity.UserName } : null)
{
Status = entity.Status,
SignalIdentifier = entity.SignalIdentifier
};
// Set ProfitAndLoss with proper type
position.ProfitAndLoss = new ProfitAndLoss { Realized = entity.ProfitAndLoss };
// Map related trades
if (entity.OpenTrade != null)
position.Open = Map(entity.OpenTrade);
if (entity.StopLossTrade != null)
position.StopLoss = Map(entity.StopLossTrade);
if (entity.TakeProfit1Trade != null)
position.TakeProfit1 = Map(entity.TakeProfit1Trade);
if (entity.TakeProfit2Trade != null)
position.TakeProfit2 = Map(entity.TakeProfit2Trade);
return position;
}
public static PositionEntity Map(Position position)
{
if (position == null) return null;
return new PositionEntity
{
Identifier = position.Identifier,
Date = position.Date,
ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0,
OriginDirection = position.OriginDirection,
Status = position.Status,
Ticker = position.Ticker,
Initiator = position.Initiator,
SignalIdentifier = position.SignalIdentifier,
AccountName = position.AccountName,
UserName = position.User?.Name,
MoneyManagementJson = position.MoneyManagement != null ? JsonConvert.SerializeObject(position.MoneyManagement) : null
};
}
// Trade mappings
public static Trade Map(TradeEntity entity)
{
if (entity == null) return null;
return new Trade(
entity.Date,
entity.Direction,
entity.Status,
entity.TradeType,
entity.Ticker,
entity.Quantity,
entity.Price,
entity.Leverage,
entity.ExchangeOrderId,
entity.Message)
{
Fee = entity.Fee
};
}
public static TradeEntity Map(Trade trade)
{
if (trade == null) return null;
return new TradeEntity
{
Date = trade.Date,
Direction = trade.Direction,
Status = trade.Status,
TradeType = trade.TradeType,
Ticker = trade.Ticker,
Fee = trade.Fee,
Quantity = trade.Quantity,
Price = trade.Price,
Leverage = trade.Leverage,
ExchangeOrderId = trade.ExchangeOrderId,
Message = trade.Message
};
}
// 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)
{
return entities?.Select(Map) ?? Enumerable.Empty<Indicator>();
}
public static IEnumerable<Signal> Map(IEnumerable<SignalEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<Signal>();
}
public static IEnumerable<Position> Map(IEnumerable<PositionEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<Position>();
}
#endregion
#region Bot Mappings
// BotBackup mappings
public static BotBackup Map(BotBackupEntity entity)
{
if (entity == null) return null;
var botBackup = new BotBackup
{
Identifier = entity.Identifier,
User = entity.User != null ? Map(entity.User) : null,
LastStatus = entity.LastStatus,
CreateDate = entity.CreateDate
};
// Deserialize the JSON data using the helper method
botBackup.DeserializeData(entity.Data);
return botBackup;
}
public static BotBackupEntity Map(BotBackup botBackup)
{
if (botBackup == null) return null;
return new BotBackupEntity
{
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,
UpdatedAt = DateTime.UtcNow
};
}
public static IEnumerable<BotBackup> Map(IEnumerable<BotBackupEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<BotBackup>();
}
public static IEnumerable<BotBackupEntity> Map(IEnumerable<BotBackup> botBackups)
{
return botBackups?.Select(Map) ?? Enumerable.Empty<BotBackupEntity>();
}
#endregion
#region Statistics Mappings
// TopVolumeTicker mappings
public static TopVolumeTicker Map(TopVolumeTickerEntity entity)
{
if (entity == null) return null;
return new TopVolumeTicker
{
Ticker = entity.Ticker,
Date = entity.Date,
Volume = entity.Volume,
Rank = entity.Rank,
Exchange = entity.Exchange
};
}
public static TopVolumeTickerEntity Map(TopVolumeTicker topVolumeTicker)
{
if (topVolumeTicker == null) return null;
return new TopVolumeTickerEntity
{
Ticker = topVolumeTicker.Ticker,
Date = topVolumeTicker.Date,
Volume = topVolumeTicker.Volume,
Rank = topVolumeTicker.Rank,
Exchange = topVolumeTicker.Exchange
};
}
public static IEnumerable<TopVolumeTicker> Map(IEnumerable<TopVolumeTickerEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<TopVolumeTicker>();
}
// SpotlightOverview mappings
public static SpotlightOverview Map(SpotlightOverviewEntity entity)
{
if (entity == null) return null;
var overview = new SpotlightOverview
{
Identifier = entity.Identifier,
DateTime = entity.DateTime,
ScenarioCount = entity.ScenarioCount,
Spotlights = new List<Spotlight>()
};
// Deserialize the JSON spotlights data
if (!string.IsNullOrEmpty(entity.SpotlightsJson))
{
try
{
overview.Spotlights = SystemJsonSerializer.Deserialize<List<Spotlight>>(entity.SpotlightsJson) ?? new List<Spotlight>();
}
catch (JsonException)
{
// If deserialization fails, return empty list
overview.Spotlights = new List<Spotlight>();
}
}
return overview;
}
public static SpotlightOverviewEntity Map(SpotlightOverview overview)
{
if (overview == null) return null;
var entity = new SpotlightOverviewEntity
{
Identifier = overview.Identifier,
DateTime = overview.DateTime,
ScenarioCount = overview.ScenarioCount
};
// Serialize the spotlights to JSON
if (overview.Spotlights != null)
{
entity.SpotlightsJson = SystemJsonSerializer.Serialize(overview.Spotlights);
}
return entity;
}
public static IEnumerable<SpotlightOverview> Map(IEnumerable<SpotlightOverviewEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<SpotlightOverview>();
}
// Trader mappings
public static Trader Map(TraderEntity entity)
{
if (entity == null) return null;
return new Trader
{
Address = entity.Address,
Winrate = entity.Winrate,
Pnl = entity.Pnl,
TradeCount = entity.TradeCount,
AverageWin = entity.AverageWin,
AverageLoss = entity.AverageLoss,
Roi = entity.Roi
};
}
public static TraderEntity Map(Trader trader, bool isBestTrader)
{
if (trader == null) return null;
return new TraderEntity
{
Address = trader.Address,
Winrate = trader.Winrate,
Pnl = trader.Pnl,
TradeCount = trader.TradeCount,
AverageWin = trader.AverageWin,
AverageLoss = trader.AverageLoss,
Roi = trader.Roi,
IsBestTrader = isBestTrader
};
}
public static IEnumerable<Trader> Map(IEnumerable<TraderEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<Trader>();
}
// FundingRate mappings
public static FundingRate Map(FundingRateEntity entity)
{
if (entity == null) return null;
return new FundingRate
{
Ticker = entity.Ticker,
Exchange = entity.Exchange,
Rate = entity.Rate,
OpenInterest = entity.OpenInterest,
Date = entity.Date,
Direction = entity.Direction
};
}
public static FundingRateEntity Map(FundingRate fundingRate)
{
if (fundingRate == null) return null;
return new FundingRateEntity
{
Ticker = fundingRate.Ticker,
Exchange = fundingRate.Exchange,
Rate = fundingRate.Rate,
OpenInterest = fundingRate.OpenInterest,
Date = fundingRate.Date,
Direction = fundingRate.Direction
};
}
public static IEnumerable<FundingRate> Map(IEnumerable<FundingRateEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<FundingRate>();
}
#endregion
#region Worker Mappings
public static Worker Map(WorkerEntity entity)
{
if (entity == null) return null;
return new Worker
{
WorkerType = entity.WorkerType,
StartTime = entity.StartTime,
LastRunTime = entity.LastRunTime,
ExecutionCount = entity.ExecutionCount,
Delay = TimeSpan.FromTicks(entity.DelayTicks),
IsActive = entity.IsActive
};
}
public static WorkerEntity Map(Worker worker)
{
if (worker == null) return null;
return new WorkerEntity
{
WorkerType = worker.WorkerType,
StartTime = worker.StartTime,
LastRunTime = worker.LastRunTime,
ExecutionCount = worker.ExecutionCount,
DelayTicks = worker.Delay.Ticks,
IsActive = worker.IsActive
};
}
public static IEnumerable<Worker> Map(IEnumerable<WorkerEntity> entities)
{
return entities?.Select(Map) ?? Enumerable.Empty<Worker>();
}
#endregion
}

View File

@@ -0,0 +1,173 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Users;
using Microsoft.EntityFrameworkCore;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlSettingsRepository : ISettingsRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlSettingsRepository(ManagingDbContext context)
{
_context = context;
}
public async Task DeleteMoneyManagementAsync(string name)
{
var moneyManagement = await _context.MoneyManagements
.AsTracking()
.FirstOrDefaultAsync(m => m.Name == name)
.ConfigureAwait(false);
if (moneyManagement != null)
{
_context.MoneyManagements.Remove(moneyManagement);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task DeleteMoneyManagementsAsync()
{
var moneyManagements = await _context.MoneyManagements
.AsTracking()
.ToListAsync()
.ConfigureAwait(false);
_context.MoneyManagements.RemoveRange(moneyManagements);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task<MoneyManagement> GetMoneyManagement(string name)
{
var moneyManagement = await _context.MoneyManagements
.AsNoTracking()
.Include(m => m.User)
.FirstOrDefaultAsync(m => m.Name == name)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(moneyManagement);
}
public async Task<IEnumerable<MoneyManagement>> GetMoneyManagementsAsync()
{
var moneyManagements = await _context.MoneyManagements
.AsNoTracking()
.Include(m => m.User)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(moneyManagements);
}
public async Task InsertMoneyManagement(LightMoneyManagement request, User user)
{
// Check if money management already exists for the same user
var existingMoneyManagement = await _context.MoneyManagements
.AsNoTracking()
.FirstOrDefaultAsync(m => m.Name == request.Name &&
((user == null && m.UserName == null) ||
(user != null && m.UserName == user.Name)))
.ConfigureAwait(false);
if (existingMoneyManagement != null)
{
throw new InvalidOperationException(
$"Money management with name '{request.Name}' already exists for user '{user?.Name}'");
}
var entity = PostgreSqlMappers.Map(request);
entity.UserName = user?.Name;
// Set the UserId if user is provided
if (user != null)
{
var userEntity = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Name == user.Name)
.ConfigureAwait(false);
entity.UserId = userEntity?.Id;
}
await _context.MoneyManagements.AddAsync(entity).ConfigureAwait(false);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task UpdateMoneyManagementAsync(LightMoneyManagement moneyManagement, User user)
{
var entity = await _context.MoneyManagements
.AsTracking()
.FirstOrDefaultAsync(m => m.Name == moneyManagement.Name &&
(user != null && m.UserName == user.Name))
.ConfigureAwait(false);
if (entity != null)
{
entity.Timeframe = moneyManagement.Timeframe;
entity.StopLoss = moneyManagement.StopLoss;
entity.TakeProfit = moneyManagement.TakeProfit;
entity.Leverage = moneyManagement.Leverage;
entity.UserName = user?.Name;
entity.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
// User-specific implementations
public async Task<MoneyManagement> GetMoneyManagementByUser(User user, string name)
{
var moneyManagement = await _context.MoneyManagements
.AsNoTracking()
.Include(m => m.User)
.FirstOrDefaultAsync(m =>
m.Name == name &&
m.UserName != null &&
m.UserName == user.Name)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(moneyManagement);
}
public async Task<IEnumerable<MoneyManagement>> GetMoneyManagementsByUserAsync(User user)
{
var moneyManagements = await _context.MoneyManagements
.AsNoTracking()
.Include(m => m.User)
.Where(m => m.UserName != null && m.UserName == user.Name)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(moneyManagements);
}
public async Task DeleteMoneyManagementByUserAsync(User user, string name)
{
var moneyManagement = await _context.MoneyManagements
.AsTracking()
.FirstOrDefaultAsync(m =>
m.Name == name &&
m.UserName != null &&
m.UserName == user.Name)
.ConfigureAwait(false);
if (moneyManagement != null)
{
_context.MoneyManagements.Remove(moneyManagement);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task DeleteMoneyManagementsByUserAsync(User user)
{
var moneyManagements = await _context.MoneyManagements
.AsTracking()
.Where(m => m.UserName != null && m.UserName == user.Name)
.ToListAsync()
.ConfigureAwait(false);
_context.MoneyManagements.RemoveRange(moneyManagements);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,287 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Statistics;
using Microsoft.EntityFrameworkCore;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlStatisticRepository : IStatisticRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlStatisticRepository(ManagingDbContext context)
{
_context = context;
}
#region TopVolumeTicker Methods
public async Task InsertTopVolumeTicker(TopVolumeTicker topVolumeTicker)
{
var entity = PostgreSqlMappers.Map(topVolumeTicker);
await _context.TopVolumeTickers.AddAsync(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public IList<TopVolumeTicker> GetTopVolumeTickers(DateTime date)
{
return GetTopVolumeTickersAsync(date).Result;
}
public async Task<IList<TopVolumeTicker>> GetTopVolumeTickersAsync(DateTime date)
{
var entities = await _context.TopVolumeTickers
.AsNoTracking()
.Where(t => date < t.Date)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities).ToList();
}
#endregion
#region SpotlightOverview Methods
public async Task SaveSpotligthtOverview(SpotlightOverview overview)
{
var entity = PostgreSqlMappers.Map(overview);
await _context.SpotlightOverviews.AddAsync(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public IList<SpotlightOverview> GetSpotlightOverviews(DateTime date)
{
return GetSpotlightOverviewsAsync(date).Result;
}
public async Task<IList<SpotlightOverview>> GetSpotlightOverviewsAsync(DateTime date)
{
var entities = await _context.SpotlightOverviews
.AsNoTracking()
.Where(o => date.ToUniversalTime() < o.DateTime)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities).ToList();
}
public void UpdateSpotlightOverview(SpotlightOverview overview)
{
UpdateSpotlightOverviewAsync(overview).Wait();
}
public async Task UpdateSpotlightOverviewAsync(SpotlightOverview overview)
{
var existingEntity = await _context.SpotlightOverviews
.AsTracking()
.FirstOrDefaultAsync(o => o.Identifier == overview.Identifier)
.ConfigureAwait(false);
if (existingEntity != null)
{
var updatedEntity = PostgreSqlMappers.Map(overview);
updatedEntity.Id = existingEntity.Id; // Keep the same ID
updatedEntity.CreatedAt = existingEntity.CreatedAt; // Keep original creation time
updatedEntity.UpdatedAt = DateTime.UtcNow;
_context.Entry(existingEntity).CurrentValues.SetValues(updatedEntity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
#endregion
#region BestTrader Methods
public List<Trader> GetBestTraders()
{
return GetBestTradersAsync().Result;
}
public async Task<List<Trader>> GetBestTradersAsync()
{
var entities = await _context.Traders
.AsNoTracking()
.Where(t => t.IsBestTrader)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities).ToList();
}
public void UpdateBestTrader(Trader trader)
{
UpdateBestTraderAsync(trader).Wait();
}
public async Task UpdateBestTraderAsync(Trader trader)
{
var existingEntity = await _context.Traders
.AsTracking()
.FirstOrDefaultAsync(t => t.Address == trader.Address && t.IsBestTrader)
.ConfigureAwait(false);
if (existingEntity != null)
{
var updatedEntity = PostgreSqlMappers.Map(trader, true);
updatedEntity.Id = existingEntity.Id; // Keep the same ID
updatedEntity.CreatedAt = existingEntity.CreatedAt; // Keep original creation time
updatedEntity.UpdatedAt = DateTime.UtcNow;
_context.Entry(existingEntity).CurrentValues.SetValues(updatedEntity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task InsertBestTrader(Trader trader)
{
var entity = PostgreSqlMappers.Map(trader, true);
await _context.Traders.AddAsync(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task RemoveBestTrader(Trader trader)
{
var entity = await _context.Traders
.AsTracking()
.FirstOrDefaultAsync(t => t.Address == trader.Address && t.IsBestTrader)
.ConfigureAwait(false);
if (entity != null)
{
_context.Traders.Remove(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
#endregion
#region BadTrader Methods
public List<Trader> GetBadTraders()
{
return GetBadTradersAsync().Result;
}
public async Task<List<Trader>> GetBadTradersAsync()
{
var entities = await _context.Traders
.AsNoTracking()
.Where(t => !t.IsBestTrader)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities).ToList();
}
public void UpdateBadTrader(Trader trader)
{
UpdateBadTraderAsync(trader).Wait();
}
public async Task UpdateBadTraderAsync(Trader trader)
{
var existingEntity = await _context.Traders
.AsTracking()
.FirstOrDefaultAsync(t => t.Address == trader.Address && !t.IsBestTrader)
.ConfigureAwait(false);
if (existingEntity != null)
{
var updatedEntity = PostgreSqlMappers.Map(trader, false);
updatedEntity.Id = existingEntity.Id; // Keep the same ID
updatedEntity.CreatedAt = existingEntity.CreatedAt; // Keep original creation time
updatedEntity.UpdatedAt = DateTime.UtcNow;
_context.Entry(existingEntity).CurrentValues.SetValues(updatedEntity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task InsertBadTrader(Trader trader)
{
var entity = PostgreSqlMappers.Map(trader, false);
await _context.Traders.AddAsync(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task RemoveBadTrader(Trader trader)
{
var entity = await _context.Traders
.AsTracking()
.FirstOrDefaultAsync(t => t.Address == trader.Address && !t.IsBestTrader)
.ConfigureAwait(false);
if (entity != null)
{
_context.Traders.Remove(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
#endregion
#region FundingRate Methods
public List<FundingRate> GetFundingRates()
{
return GetFundingRatesAsync().Result;
}
public async Task<List<FundingRate>> GetFundingRatesAsync()
{
var entities = await _context.FundingRates
.AsNoTracking()
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities).ToList();
}
public async Task RemoveFundingRate(FundingRate oldRate)
{
var entity = await _context.FundingRates
.AsTracking()
.FirstOrDefaultAsync(r => r.Ticker == oldRate.Ticker && r.Exchange == oldRate.Exchange)
.ConfigureAwait(false);
if (entity != null)
{
_context.FundingRates.Remove(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task InsertFundingRate(FundingRate newRate)
{
var entity = PostgreSqlMappers.Map(newRate);
await _context.FundingRates.AddAsync(entity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public void UpdateFundingRate(FundingRate oldRate, FundingRate newRate)
{
UpdateFundingRateAsync(oldRate, newRate).Wait();
}
public async Task UpdateFundingRateAsync(FundingRate oldRate, FundingRate newRate)
{
var existingEntity = await _context.FundingRates
.AsTracking()
.FirstOrDefaultAsync(r => r.Ticker == oldRate.Ticker && r.Exchange == oldRate.Exchange)
.ConfigureAwait(false);
if (existingEntity != null)
{
var updatedEntity = PostgreSqlMappers.Map(newRate);
updatedEntity.Id = existingEntity.Id; // Keep the same ID
updatedEntity.CreatedAt = existingEntity.CreatedAt; // Keep original creation time
updatedEntity.UpdatedAt = DateTime.UtcNow;
_context.Entry(existingEntity).CurrentValues.SetValues(updatedEntity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
}
#endregion
}

View File

@@ -0,0 +1,223 @@
using System.Text.Json;
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Synth.Models;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlSynthRepository : ISynthRepository
{
private readonly ManagingDbContext _context;
private readonly ILogger<PostgreSqlSynthRepository> _logger;
public PostgreSqlSynthRepository(ManagingDbContext context, ILogger<PostgreSqlSynthRepository> logger)
{
_context = context;
_logger = logger;
}
public async Task<SynthMinersLeaderboard?> GetLeaderboardAsync(string cacheKey)
{
try
{
var entity = await _context.SynthMinersLeaderboards.AsNoTracking().FirstOrDefaultAsync(x => x.CacheKey == cacheKey);
if (entity == null)
{
_logger.LogDebug($"[PG Synth] No leaderboard cache found for key: {cacheKey}");
return null;
}
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(entity.MinersData);
return new SynthMinersLeaderboard
{
Id = entity.Id.ToString(),
Asset = entity.Asset,
TimeIncrement = entity.TimeIncrement,
SignalDate = entity.SignalDate,
IsBacktest = entity.IsBacktest,
Miners = miners ?? new List<MinerInfo>(),
CreatedAt = entity.CreatedAt
};
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error retrieving leaderboard cache for key: {cacheKey}");
return null;
}
}
public async Task SaveLeaderboardAsync(SynthMinersLeaderboard leaderboard)
{
try
{
leaderboard.CreatedAt = DateTime.UtcNow;
var cacheKey = leaderboard.GetCacheKey();
var minersData = JsonSerializer.Serialize(leaderboard.Miners);
var existing = await _context.SynthMinersLeaderboards.FirstOrDefaultAsync(x => x.CacheKey == cacheKey);
if (existing != null)
{
existing.Asset = leaderboard.Asset;
existing.TimeIncrement = leaderboard.TimeIncrement;
existing.SignalDate = leaderboard.SignalDate;
existing.IsBacktest = leaderboard.IsBacktest;
existing.MinersData = minersData;
existing.CreatedAt = leaderboard.CreatedAt;
_context.SynthMinersLeaderboards.Update(existing);
_logger.LogDebug($"[PG Synth] Updated leaderboard in DB for key: {cacheKey}");
}
else
{
var entity = new SynthMinersLeaderboardEntity
{
Id = Guid.NewGuid(),
Asset = leaderboard.Asset,
TimeIncrement = leaderboard.TimeIncrement,
SignalDate = leaderboard.SignalDate,
IsBacktest = leaderboard.IsBacktest,
MinersData = minersData,
CacheKey = cacheKey,
CreatedAt = leaderboard.CreatedAt
};
await _context.SynthMinersLeaderboards.AddAsync(entity);
_logger.LogDebug($"[PG Synth] Saved new leaderboard to DB for key: {cacheKey}");
}
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error saving leaderboard cache for key: {leaderboard.GetCacheKey()}");
}
}
public async Task<List<SynthPrediction>> GetIndividualPredictionsAsync(string asset, int timeIncrement, int timeLength, List<int> minerUids, bool isBacktest, DateTime? signalDate)
{
try
{
var results = new List<SynthPrediction>();
foreach (var minerUid in minerUids)
{
var cacheKey = $"{asset}_{timeIncrement}_{timeLength}_{minerUid}";
if (isBacktest && signalDate.HasValue)
{
cacheKey += $"_backtest_{signalDate.Value:yyyy-MM-dd-HH}";
}
var entity = await _context.SynthPredictions.AsNoTracking().FirstOrDefaultAsync(x => x.CacheKey == cacheKey);
if (entity != null)
{
var prediction = JsonSerializer.Deserialize<MinerPrediction>(entity.PredictionData);
if (prediction != null)
{
results.Add(new SynthPrediction
{
Id = entity.Id.ToString(),
Asset = entity.Asset,
MinerUid = entity.MinerUid,
TimeIncrement = entity.TimeIncrement,
TimeLength = entity.TimeLength,
SignalDate = entity.SignalDate,
IsBacktest = entity.IsBacktest,
Prediction = prediction,
CreatedAt = entity.CreatedAt
});
}
}
}
if (results.Any())
{
_logger.LogDebug($"[PG Synth] Retrieved {results.Count}/{minerUids.Count} individual predictions for {asset}");
}
else
{
_logger.LogDebug($"[PG Synth] No individual predictions found for {asset}");
}
return results;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error retrieving individual predictions cache for asset: {asset}");
return new List<SynthPrediction>();
}
}
public async Task SaveIndividualPredictionAsync(SynthPrediction prediction)
{
try
{
prediction.CreatedAt = DateTime.UtcNow;
var cacheKey = prediction.GetCacheKey();
var predictionData = JsonSerializer.Serialize(prediction.Prediction);
var existing = await _context.SynthPredictions.FirstOrDefaultAsync(x => x.CacheKey == cacheKey).ConfigureAwait(false);
if (existing != null)
{
existing.Asset = prediction.Asset;
existing.MinerUid = prediction.MinerUid;
existing.TimeIncrement = prediction.TimeIncrement;
existing.TimeLength = prediction.TimeLength;
existing.SignalDate = prediction.SignalDate;
existing.IsBacktest = prediction.IsBacktest;
existing.PredictionData = predictionData;
existing.CreatedAt = prediction.CreatedAt;
_context.SynthPredictions.Update(existing);
_logger.LogDebug($"[PG Synth] Updated individual prediction for miner {prediction.MinerUid}");
}
else
{
var entity = new SynthPredictionEntity
{
Id = Guid.NewGuid(),
Asset = prediction.Asset,
MinerUid = prediction.MinerUid,
TimeIncrement = prediction.TimeIncrement,
TimeLength = prediction.TimeLength,
SignalDate = prediction.SignalDate,
IsBacktest = prediction.IsBacktest,
PredictionData = predictionData,
CacheKey = cacheKey,
CreatedAt = prediction.CreatedAt
};
await _context.SynthPredictions.AddAsync(entity).ConfigureAwait(false);
_logger.LogDebug($"[PG Synth] Saved new individual prediction for miner {prediction.MinerUid}");
}
await _context.SaveChangesAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error saving individual prediction cache for miner {prediction.MinerUid}: {ex.Message}");
}
}
public async Task SaveIndividualPredictionsAsync(List<SynthPrediction> predictions)
{
if (!predictions.Any())
return;
try
{
var saveTasks = predictions.Select(SaveIndividualPredictionAsync);
await Task.WhenAll(saveTasks);
_logger.LogInformation($"[PG Synth] Successfully saved {predictions.Count} individual predictions to DB");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error saving batch of {predictions.Count} individual predictions");
}
}
public async Task CleanupOldDataAsync(int retentionDays = 30)
{
try
{
var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays);
var oldLeaderboards = _context.SynthMinersLeaderboards.Where(x => x.CreatedAt < cutoffDate);
_context.SynthMinersLeaderboards.RemoveRange(oldLeaderboards);
var oldPredictions = _context.SynthPredictions.Where(x => x.CreatedAt < cutoffDate);
_context.SynthPredictions.RemoveRange(oldPredictions);
await _context.SaveChangesAsync();
_logger.LogInformation($"[PG Synth] Cleaned up old Synth cache data older than {retentionDays} days");
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error during cleanup of old Synth cache data");
}
}
}

View File

@@ -0,0 +1,444 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlTradingRepository : ITradingRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlTradingRepository(ManagingDbContext context)
{
_context = context;
}
#region Scenario Methods
public async Task DeleteScenarioAsync(string name)
{
var scenario = await _context.Scenarios.FirstOrDefaultAsync(s => s.Name == name);
if (scenario != null)
{
_context.Scenarios.Remove(scenario);
await _context.SaveChangesAsync();
}
}
public Scenario GetScenarioByName(string name)
{
return GetScenarioByNameAsync(name).Result;
}
public async Task<Scenario> GetScenarioByNameAsync(string name)
{
var scenario = await _context.Scenarios
.AsNoTracking()
.Include(s => s.ScenarioIndicators)
.ThenInclude(si => si.Indicator)
.FirstOrDefaultAsync(s => s.Name == name)
.ConfigureAwait(false);
if (scenario == null) return null;
var mappedScenario = PostgreSqlMappers.Map(scenario);
// Map indicators from junction table
mappedScenario.Indicators = scenario.ScenarioIndicators
.Select(si => PostgreSqlMappers.Map(si.Indicator))
.ToList();
return mappedScenario;
}
public IEnumerable<Scenario> GetScenarios()
{
return GetScenariosAsync().Result;
}
public async Task<IEnumerable<Scenario>> GetScenariosAsync()
{
var scenarios = await _context.Scenarios
.AsNoTracking()
.Include(s => s.ScenarioIndicators)
.ThenInclude(si => si.Indicator)
.ToListAsync()
.ConfigureAwait(false);
return scenarios.Select(scenario =>
{
var mappedScenario = PostgreSqlMappers.Map(scenario);
mappedScenario.Indicators = scenario.ScenarioIndicators
.Select(si => PostgreSqlMappers.Map(si.Indicator))
.ToList();
return mappedScenario;
});
}
public async Task InsertScenarioAsync(Scenario scenario)
{
// Check if scenario already exists for the same user
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)));
if (existingScenario != null)
{
throw new InvalidOperationException(
$"Scenario with name '{scenario.Name}' already exists for user '{scenario.User?.Name}'");
}
var scenarioEntity = PostgreSqlMappers.Map(scenario);
_context.Scenarios.Add(scenarioEntity);
await _context.SaveChangesAsync();
// Handle scenario-indicator relationships
if (scenario.Indicators != null && scenario.Indicators.Any())
{
foreach (var indicator in scenario.Indicators)
{
var 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)));
if (indicatorEntity != null)
{
var junction = new ScenarioIndicatorEntity
{
ScenarioId = scenarioEntity.Id,
IndicatorId = indicatorEntity.Id
};
_context.ScenarioIndicators.Add(junction);
}
}
await _context.SaveChangesAsync();
}
}
public async Task UpdateScenarioAsync(Scenario scenario)
{
var entity = _context.Scenarios
.AsTracking()
.FirstOrDefault(s => s.Name == scenario.Name);
if (entity != null)
{
entity.LoopbackPeriod = scenario.LoopbackPeriod ?? 1;
entity.UserName = scenario.User?.Name;
entity.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
#endregion
#region Indicator Methods
public async Task DeleteIndicatorAsync(string name)
{
var indicator = _context.Indicators
.AsTracking()
.FirstOrDefault(i => i.Name == name);
if (indicator != null)
{
_context.Indicators.Remove(indicator);
await _context.SaveChangesAsync();
}
}
public async Task DeleteIndicatorsAsync()
{
var indicators = _context.Indicators.AsTracking().ToList();
_context.Indicators.RemoveRange(indicators);
await _context.SaveChangesAsync();
}
public async Task<IEnumerable<Indicator>> GetIndicatorsAsync()
{
var indicators = await _context.Indicators
.AsNoTracking()
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(indicators);
}
public async Task<IEnumerable<Indicator>> GetStrategiesAsync()
{
var indicators = await _context.Indicators
.AsNoTracking()
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(indicators);
}
public async Task<Indicator> GetStrategyByNameAsync(string name)
{
var indicator = await _context.Indicators
.AsNoTracking()
.FirstOrDefaultAsync(i => i.Name == name)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(indicator);
}
public async Task InsertStrategyAsync(Indicator indicator)
{
// 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)));
if (existingIndicator != null)
{
throw new InvalidOperationException(
$"Indicator with name '{indicator.Name}' already exists for user '{indicator.User?.Name}'");
}
var entity = PostgreSqlMappers.Map(indicator);
_context.Indicators.Add(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateStrategyAsync(Indicator indicator)
{
var entity = _context.Indicators
.AsTracking()
.FirstOrDefault(i => i.Name == indicator.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.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
#endregion
#region Position Methods
public Position GetPositionByIdentifier(string identifier)
{
return GetPositionByIdentifierAsync(identifier).Result;
}
public async Task<Position> GetPositionByIdentifierAsync(string identifier)
{
var position = await _context.Positions
.AsNoTracking()
.Include(p => p.OpenTrade)
.Include(p => p.StopLossTrade)
.Include(p => p.TakeProfit1Trade)
.Include(p => p.TakeProfit2Trade)
.FirstOrDefaultAsync(p => p.Identifier == identifier)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(position);
}
public IEnumerable<Position> GetPositions(PositionInitiator positionInitiator)
{
return GetPositionsAsync(positionInitiator).Result;
}
public async Task<IEnumerable<Position>> GetPositionsAsync(PositionInitiator positionInitiator)
{
var positions = await _context.Positions
.AsNoTracking()
.Include(p => p.OpenTrade)
.Include(p => p.StopLossTrade)
.Include(p => p.TakeProfit1Trade)
.Include(p => p.TakeProfit2Trade)
.Where(p => p.Initiator == positionInitiator)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(positions);
}
public IEnumerable<Position> GetPositionsByStatus(PositionStatus positionStatus)
{
return GetPositionsByStatusAsync(positionStatus).Result;
}
public async Task<IEnumerable<Position>> GetPositionsByStatusAsync(PositionStatus positionStatus)
{
var positions = await _context.Positions
.AsNoTracking()
.Include(p => p.OpenTrade)
.Include(p => p.StopLossTrade)
.Include(p => p.TakeProfit1Trade)
.Include(p => p.TakeProfit2Trade)
.Where(p => p.Status == positionStatus)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(positions);
}
public async Task InsertPositionAsync(Position position)
{
// Check if position already exists for the same user
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)));
if (existingPosition != null)
{
throw new InvalidOperationException(
$"Position with identifier '{position.Identifier}' already exists for user '{position.User?.Name}'");
}
var entity = PostgreSqlMappers.Map(position);
// Handle related trades
if (position.Open != null)
{
var openTrade = PostgreSqlMappers.Map(position.Open);
_context.Trades.Add(openTrade);
await _context.SaveChangesAsync();
entity.OpenTradeId = openTrade.Id;
}
if (position.StopLoss != null)
{
var stopLossTrade = PostgreSqlMappers.Map(position.StopLoss);
_context.Trades.Add(stopLossTrade);
await _context.SaveChangesAsync();
entity.StopLossTradeId = stopLossTrade.Id;
}
if (position.TakeProfit1 != null)
{
var takeProfit1Trade = PostgreSqlMappers.Map(position.TakeProfit1);
_context.Trades.Add(takeProfit1Trade);
await _context.SaveChangesAsync();
entity.TakeProfit1TradeId = takeProfit1Trade.Id;
}
if (position.TakeProfit2 != null)
{
var takeProfit2Trade = PostgreSqlMappers.Map(position.TakeProfit2);
_context.Trades.Add(takeProfit2Trade);
await _context.SaveChangesAsync();
entity.TakeProfit2TradeId = takeProfit2Trade.Id;
}
_context.Positions.Add(entity);
await _context.SaveChangesAsync();
}
public async Task UpdatePositionAsync(Position position)
{
var entity = _context.Positions
.AsTracking()
.FirstOrDefault(p => p.Identifier == position.Identifier);
if (entity != null)
{
entity.Date = position.Date;
entity.ProfitAndLoss = position.ProfitAndLoss?.Realized ?? 0;
entity.Status = position.Status;
entity.SignalIdentifier = position.SignalIdentifier;
entity.MoneyManagementJson = position.MoneyManagement != null
? JsonConvert.SerializeObject(position.MoneyManagement)
: null;
entity.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
#endregion
#region Signal Methods
public IEnumerable<Signal> GetSignalsByUser(User user)
{
return GetSignalsByUserAsync(user).Result;
}
public async Task<IEnumerable<Signal>> GetSignalsByUserAsync(User user)
{
var signals = await _context.Signals
.AsNoTracking()
.Where(s => (user == null && s.UserName == null) ||
(user != null && s.UserName == user.Name))
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(signals);
}
public Signal GetSignalByIdentifier(string identifier, User user = null)
{
return GetSignalByIdentifierAsync(identifier, user).Result;
}
public async Task<Signal> GetSignalByIdentifierAsync(string identifier, User user = null)
{
var signal = await _context.Signals
.AsNoTracking()
.FirstOrDefaultAsync(s => s.Identifier == identifier &&
((user == null && s.UserName == null) ||
(user != null && s.UserName == user.Name)))
.ConfigureAwait(false);
return PostgreSqlMappers.Map(signal);
}
public async Task InsertSignalAsync(Signal signal)
{
// Check if signal already exists with the same identifier, date, and user
var existingSignal = _context.Signals
.AsNoTracking()
.FirstOrDefault(s => s.Identifier == signal.Identifier &&
s.Date == signal.Date &&
((s.UserName == null && signal.User == null) ||
(s.UserName != null && signal.User != null && s.UserName == signal.User.Name)));
if (existingSignal != null)
{
throw new InvalidOperationException(
$"Signal with identifier '{signal.Identifier}' and date '{signal.Date}' already exists for this user");
}
var entity = PostgreSqlMappers.Map(signal);
_context.Signals.Add(entity);
await _context.SaveChangesAsync();
}
#endregion
}

View File

@@ -0,0 +1,116 @@
using System.Data;
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Users;
using Microsoft.EntityFrameworkCore;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlUserRepository : IUserRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlUserRepository(ManagingDbContext context)
{
_context = context;
}
/// <summary>
/// Ensures the database connection is open before executing queries
/// </summary>
private async Task EnsureConnectionOpenAsync()
{
if (_context.Database.GetDbConnection().State != ConnectionState.Open)
{
await _context.Database.OpenConnectionAsync();
}
}
/// <summary>
/// Safely closes the database connection if it was opened by us
/// </summary>
private async Task SafeCloseConnectionAsync()
{
if (_context.Database.GetDbConnection().State == ConnectionState.Open)
{
await _context.Database.CloseConnectionAsync();
}
}
public async Task<User> GetUserByAgentNameAsync(string agentName)
{
try
{
await EnsureConnectionOpenAsync();
var userEntity = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.AgentName == agentName)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(userEntity);
}
catch (Exception)
{
// If there's an error, try to reset the connection
await SafeCloseConnectionAsync();
throw;
}
}
public async Task<User> GetUserByNameAsync(string name)
{
try
{
await EnsureConnectionOpenAsync();
var userEntity = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.Name == name)
.ConfigureAwait(false);
return PostgreSqlMappers.Map(userEntity);
}
catch (Exception)
{
// If there's an error, try to reset the connection
await SafeCloseConnectionAsync();
throw;
}
}
public async Task InsertUserAsync(User user)
{
var userEntity = PostgreSqlMappers.Map(user);
_context.Users.Add(userEntity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task UpdateUser(User user)
{
try
{
var userEntity = await _context.Users
.AsTracking() // Explicitly enable tracking for update operations
.FirstOrDefaultAsync(u => u.Name == user.Name)
.ConfigureAwait(false);
if (userEntity == null)
{
throw new InvalidOperationException($"User with name '{user.Name}' not found");
}
userEntity.AgentName = user.AgentName;
userEntity.AvatarUrl = user.AvatarUrl;
userEntity.TelegramChannel = user.TelegramChannel;
_context.Users.Update(userEntity);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
catch (Exception e)
{
Console.WriteLine(e);
throw new Exception("Cannot update user");
}
}
}

View File

@@ -0,0 +1,66 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Common;
using Managing.Domain.Workers;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlWorkerRepository : IWorkerRepository
{
private readonly ManagingDbContext _context;
public PostgreSqlWorkerRepository(ManagingDbContext context)
{
_context = context;
}
public async Task DisableWorker(Enums.WorkerType workerType)
{
var entity = await GetWorkerEntity(workerType);
if (entity == null) throw new InvalidOperationException($"Worker with type '{workerType}' not found");
entity.IsActive = false;
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task EnableWorker(Enums.WorkerType workerType)
{
var entity = await GetWorkerEntity(workerType);
if (entity == null) throw new InvalidOperationException($"Worker with type '{workerType}' not found");
entity.IsActive = true;
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task<Worker> GetWorkerAsync(Enums.WorkerType workerType)
{
var entity = await GetWorkerEntity(workerType);
return PostgreSqlMappers.Map(entity);
}
public async Task<IEnumerable<Worker>> GetWorkers()
{
var entities = await _context.Workers.AsNoTracking().ToListAsync().ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
}
public async Task InsertWorker(Worker worker)
{
var entity = PostgreSqlMappers.Map(worker);
await _context.Workers.AddAsync(entity).ConfigureAwait(false);
await _context.SaveChangesAsync().ConfigureAwait(false);
}
public async Task UpdateWorker(Enums.WorkerType workerType, int executionCount)
{
var entity = await GetWorkerEntity(workerType);
if (entity == null) throw new InvalidOperationException($"Worker with type '{workerType}' not found");
entity.ExecutionCount = executionCount;
entity.LastRunTime = DateTime.UtcNow;
await _context.SaveChangesAsync().ConfigureAwait(false);
}
private async Task<WorkerEntity?> GetWorkerEntity(Enums.WorkerType workerType)
{
return await _context.Workers.FirstOrDefaultAsync(w => w.WorkerType == workerType).ConfigureAwait(false);
}
}