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