diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 10f5f22a..081e3164 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Managing.Api.Models.Requests; using Managing.Application.Abstractions.Services; +using Managing.Application.Abstractions.Shared; using Managing.Application.Hubs; using Managing.Domain.Backtests; using Managing.Domain.Bots; @@ -10,6 +11,7 @@ using Managing.Domain.Strategies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using static Managing.Common.Enums; using MoneyManagementRequest = Managing.Domain.Backtests.MoneyManagementRequest; namespace Managing.Api.Controllers; @@ -231,8 +233,17 @@ public class BacktestController : BaseController public async Task> GetBacktestsPaginated( int page = 1, int pageSize = 50, - string sortBy = "score", - string sortOrder = "desc") + BacktestSortableColumn sortBy = BacktestSortableColumn.Score, + string sortOrder = "desc", + [FromQuery] double? scoreMin = null, + [FromQuery] double? scoreMax = null, + [FromQuery] int? winrateMin = null, + [FromQuery] int? winrateMax = null, + [FromQuery] decimal? maxDrawdownMax = null, + [FromQuery] string? tickers = null, + [FromQuery] string? indicators = null, + [FromQuery] double? durationMinDays = null, + [FromQuery] double? durationMaxDays = null) { var user = await GetUser(); @@ -251,8 +262,65 @@ public class BacktestController : BaseController return BadRequest("Sort order must be 'asc' or 'desc'"); } + // Validate score and winrate ranges [0,100] + if (scoreMin.HasValue && (scoreMin < 0 || scoreMin > 100)) + { + return BadRequest("scoreMin must be between 0 and 100"); + } + if (scoreMax.HasValue && (scoreMax < 0 || scoreMax > 100)) + { + return BadRequest("scoreMax must be between 0 and 100"); + } + if (winrateMin.HasValue && (winrateMin < 0 || winrateMin > 100)) + { + return BadRequest("winrateMin must be between 0 and 100"); + } + if (winrateMax.HasValue && (winrateMax < 0 || winrateMax > 100)) + { + return BadRequest("winrateMax must be between 0 and 100"); + } + + if (scoreMin.HasValue && scoreMax.HasValue && scoreMin > scoreMax) + { + return BadRequest("scoreMin must be less than or equal to scoreMax"); + } + if (winrateMin.HasValue && winrateMax.HasValue && winrateMin > winrateMax) + { + return BadRequest("winrateMin must be less than or equal to winrateMax"); + } + if (maxDrawdownMax.HasValue && maxDrawdownMax < 0) + { + return BadRequest("maxDrawdownMax must be greater than or equal to 0"); + } + + // Parse multi-selects if provided (comma-separated). Currently unused until repository wiring. + var tickerList = string.IsNullOrWhiteSpace(tickers) + ? Array.Empty() + : tickers.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var indicatorList = string.IsNullOrWhiteSpace(indicators) + ? Array.Empty() + : indicators.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var filter = new BacktestsFilter + { + ScoreMin = scoreMin, + ScoreMax = scoreMax, + WinrateMin = winrateMin, + WinrateMax = winrateMax, + MaxDrawdownMax = maxDrawdownMax, + Tickers = tickerList, + Indicators = indicatorList, + DurationMin = durationMinDays.HasValue ? TimeSpan.FromDays(durationMinDays.Value) : (TimeSpan?)null, + DurationMax = durationMaxDays.HasValue ? TimeSpan.FromDays(durationMaxDays.Value) : (TimeSpan?)null + }; + var (backtests, totalCount) = - await _backtester.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder); + await _backtester.GetBacktestsByUserPaginatedAsync( + user, + page, + pageSize, + sortBy, + sortOrder, + filter); var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); var response = new PaginatedBacktestsResponse diff --git a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs index 4063df62..eda2bb96 100644 --- a/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs +++ b/src/Managing.Application.Abstractions/Repositories/IBacktestRepository.cs @@ -1,4 +1,6 @@ -using Managing.Domain.Backtests; +using Managing.Application.Abstractions.Shared; +using Managing.Common; +using Managing.Domain.Backtests; using Managing.Domain.Users; namespace Managing.Application.Abstractions.Repositories; @@ -18,11 +20,21 @@ public interface IBacktestRepository int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); - (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, - int pageSize, string sortBy = "score", string sortOrder = "desc"); + (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated( + User user, + int page, + int pageSize, + Enums.BacktestSortableColumn sortBy = Enums.BacktestSortableColumn.Score, + string sortOrder = "desc", + BacktestsFilter? filter = null); - Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user, int page, - int pageSize, string sortBy = "score", string sortOrder = "desc"); + Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync( + User user, + int page, + int pageSize, + Enums.BacktestSortableColumn sortBy = Enums.BacktestSortableColumn.Score, + string sortOrder = "desc", + BacktestsFilter? filter = null); Task GetBacktestByIdForUserAsync(User user, string id); Task DeleteBacktestByIdForUserAsync(User user, string id); diff --git a/src/Managing.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs index b89baba0..f1b4d085 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -1,4 +1,6 @@ -using Managing.Domain.Backtests; +using Managing.Application.Abstractions.Shared; +using Managing.Common; +using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Users; @@ -64,9 +66,22 @@ namespace Managing.Application.Abstractions.Services Task DeleteBacktestByUserAsync(User user, string id); Task DeleteBacktestsByIdsForUserAsync(User user, IEnumerable ids); bool DeleteBacktestsByUser(User user); + (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated( + User user, + int page, + int pageSize, + Enums.BacktestSortableColumn sortBy, + string sortOrder = "desc", + BacktestsFilter? filter = null); + + Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync( + User user, + int page, + int pageSize, + Enums.BacktestSortableColumn sortBy, + string sortOrder = "desc", + BacktestsFilter? filter = null); Task DeleteBacktestsByRequestIdAsync(Guid requestId); - (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); - Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc"); // Bundle backtest methods void InsertBundleBacktestRequestForUser(User user, BundleBacktestRequest bundleRequest); diff --git a/src/Managing.Application.Abstractions/Shared/BacktestsFilter.cs b/src/Managing.Application.Abstractions/Shared/BacktestsFilter.cs new file mode 100644 index 00000000..93712a53 --- /dev/null +++ b/src/Managing.Application.Abstractions/Shared/BacktestsFilter.cs @@ -0,0 +1,16 @@ +namespace Managing.Application.Abstractions.Shared; + +public class BacktestsFilter +{ + public double? ScoreMin { get; set; } + public double? ScoreMax { get; set; } + public int? WinrateMin { get; set; } + public int? WinrateMax { get; set; } + public decimal? MaxDrawdownMax { get; set; } + public IEnumerable? Tickers { get; set; } + public IEnumerable? Indicators { get; set; } + public TimeSpan? DurationMin { get; set; } + public TimeSpan? DurationMax { get; set; } +} + + diff --git a/src/Managing.Application/Backtests/Backtester.cs b/src/Managing.Application/Backtests/Backtester.cs index ec8e52fe..8d1c2faa 100644 --- a/src/Managing.Application/Backtests/Backtester.cs +++ b/src/Managing.Application/Backtests/Backtester.cs @@ -2,6 +2,7 @@ using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; +using Managing.Application.Abstractions.Shared; using Managing.Application.Hubs; using Managing.Core; using Managing.Domain.Accounts; @@ -415,19 +416,29 @@ namespace Managing.Application.Backtests } } - public (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, - int pageSize, string sortBy = "score", string sortOrder = "desc") + public (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated( + User user, + int page, + int pageSize, + BacktestSortableColumn sortBy, + string sortOrder = "desc", + BacktestsFilter? filter = null) { var (backtests, totalCount) = - _backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder); + _backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder, filter); return (backtests, totalCount); } public async Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync( - User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") + User user, + int page, + int pageSize, + BacktestSortableColumn sortBy, + string sortOrder = "desc", + BacktestsFilter? filter = null) { var (backtests, totalCount) = - await _backtestRepository.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder); + await _backtestRepository.GetBacktestsByUserPaginatedAsync(user, page, pageSize, sortBy, sortOrder, filter); return (backtests, totalCount); } diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index e28f766b..cd02d742 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -504,6 +504,25 @@ public static class Enums TotalBalance } + /// + /// Sortable columns for backtests pagination endpoints + /// + public enum BacktestSortableColumn + { + Score, + FinalPnl, + WinRate, + GrowthPercentage, + HodlPercentage, + Duration, + Timeframe, + IndicatorsCount, + MaxDrawdown, + Fees, + SharpeRatio, + Ticker + } + /// /// Event types for agent summary updates /// diff --git a/src/Managing.Domain/Backtests/LightBacktest.cs b/src/Managing.Domain/Backtests/LightBacktest.cs index a0bdbbf0..997403e3 100644 --- a/src/Managing.Domain/Backtests/LightBacktest.cs +++ b/src/Managing.Domain/Backtests/LightBacktest.cs @@ -24,4 +24,5 @@ public class LightBacktest [Id(11)] public double Score { get; set; } [Id(12)] public string ScoreMessage { get; set; } = string.Empty; [Id(13)] public object Metadata { get; set; } + [Id(14)] public string Ticker { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/Migrations/20251014092228_BacktestFiltersDenormalization.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251014092228_BacktestFiltersDenormalization.Designer.cs new file mode 100644 index 00000000..74797be0 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251014092228_BacktestFiltersDenormalization.Designer.cs @@ -0,0 +1,1536 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20251014092228_BacktestFiltersDenormalization")] + partial class BacktestFiltersDenormalization + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BacktestCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalFees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName") + .IsUnique(); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorsCount") + .HasColumnType("integer"); + + b.Property("IndicatorsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxDrawdown") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("MaxDrawdownRecoveryTime") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SharpeRatio") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("Ticker") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Timeframe") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "IndicatorsCount"); + + b.HasIndex("UserId", "IndicatorsCsv"); + + b.HasIndex("UserId", "Name"); + + b.HasIndex("UserId", "Score"); + + b.HasIndex("UserId", "Ticker"); + + b.HasIndex("UserId", "Timeframe"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("AccumulatedRunTimeSeconds") + .HasColumnType("bigint"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LastStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStopTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DateTimeRangesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("MoneyManagementVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TickerVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UniversalConfigJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("GasFees") + .HasColumnType("decimal(18,8)"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("NetPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UiFees") + .HasColumnType("decimal(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Navigation("Accounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251014092228_BacktestFiltersDenormalization.cs b/src/Managing.Infrastructure.Database/Migrations/20251014092228_BacktestFiltersDenormalization.cs new file mode 100644 index 00000000..64ba8edd --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251014092228_BacktestFiltersDenormalization.cs @@ -0,0 +1,165 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class BacktestFiltersDenormalization : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Duration", + table: "Backtests", + type: "interval", + nullable: false, + defaultValue: new TimeSpan(0, 0, 0, 0, 0)); + + migrationBuilder.AddColumn( + name: "IndicatorsCount", + table: "Backtests", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "IndicatorsCsv", + table: "Backtests", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "MaxDrawdown", + table: "Backtests", + type: "numeric(18,8)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "MaxDrawdownRecoveryTime", + table: "Backtests", + type: "interval", + nullable: false, + defaultValue: new TimeSpan(0, 0, 0, 0, 0)); + + migrationBuilder.AddColumn( + name: "Name", + table: "Backtests", + type: "character varying(255)", + maxLength: 255, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "SharpeRatio", + table: "Backtests", + type: "numeric(18,8)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "Ticker", + table: "Backtests", + type: "character varying(32)", + maxLength: 32, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Timeframe", + table: "Backtests", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_Backtests_UserId_IndicatorsCount", + table: "Backtests", + columns: new[] { "UserId", "IndicatorsCount" }); + + migrationBuilder.CreateIndex( + name: "IX_Backtests_UserId_IndicatorsCsv", + table: "Backtests", + columns: new[] { "UserId", "IndicatorsCsv" }); + + migrationBuilder.CreateIndex( + name: "IX_Backtests_UserId_Name", + table: "Backtests", + columns: new[] { "UserId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_Backtests_UserId_Ticker", + table: "Backtests", + columns: new[] { "UserId", "Ticker" }); + + migrationBuilder.CreateIndex( + name: "IX_Backtests_UserId_Timeframe", + table: "Backtests", + columns: new[] { "UserId", "Timeframe" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Backtests_UserId_IndicatorsCount", + table: "Backtests"); + + migrationBuilder.DropIndex( + name: "IX_Backtests_UserId_IndicatorsCsv", + table: "Backtests"); + + migrationBuilder.DropIndex( + name: "IX_Backtests_UserId_Name", + table: "Backtests"); + + migrationBuilder.DropIndex( + name: "IX_Backtests_UserId_Ticker", + table: "Backtests"); + + migrationBuilder.DropIndex( + name: "IX_Backtests_UserId_Timeframe", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "Duration", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "IndicatorsCount", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "IndicatorsCsv", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "MaxDrawdown", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "MaxDrawdownRecoveryTime", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "Name", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "SharpeRatio", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "Ticker", + table: "Backtests"); + + migrationBuilder.DropColumn( + name: "Timeframe", + table: "Backtests"); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251014092447_RemoveIndicatorIndexes.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251014092447_RemoveIndicatorIndexes.Designer.cs new file mode 100644 index 00000000..c474ba97 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251014092447_RemoveIndicatorIndexes.Designer.cs @@ -0,0 +1,1532 @@ +// +using System; +using Managing.Infrastructure.Databases.PostgreSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + [DbContext(typeof(ManagingDbContext))] + [Migration("20251014092447_RemoveIndicatorIndexes")] + partial class RemoveIndicatorIndexes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Exchange") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGmxInitialized") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Key") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Secret") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActiveStrategiesCount") + .HasColumnType("integer"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("BacktestCount") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Losses") + .HasColumnType("integer"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Runtime") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalBalance") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalFees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("TotalPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalROI") + .HasColumnType("decimal(18,8)"); + + b.Property("TotalVolume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Wins") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AgentName") + .IsUnique(); + + b.HasIndex("TotalPnL"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("AgentSummaries"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasColumnType("decimal(18,8)"); + + b.Property("FinalPnl") + .HasColumnType("decimal(18,8)"); + + b.Property("GrowthPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("HodlPercentage") + .HasColumnType("decimal(18,8)"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorsCount") + .HasColumnType("integer"); + + b.Property("IndicatorsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxDrawdown") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("MaxDrawdownRecoveryTime") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PositionsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Score") + .HasColumnType("double precision"); + + b.Property("ScoreMessage") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("text"); + + b.Property("SharpeRatio") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + b.Property("Ticker") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Timeframe") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("WinRate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("RequestId"); + + b.HasIndex("Score"); + + b.HasIndex("UserId"); + + b.HasIndex("RequestId", "Score"); + + b.HasIndex("UserId", "Name"); + + b.HasIndex("UserId", "Score"); + + b.HasIndex("UserId", "Ticker"); + + b.HasIndex("UserId", "Timeframe"); + + b.ToTable("Backtests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("AccumulatedRunTimeSeconds") + .HasColumnType("bigint"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Fees") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("LastStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastStopTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LongPositionCount") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("NetPnL") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.Property("ShortPositionCount") + .HasColumnType("integer"); + + b.Property("StartupTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("TradeLosses") + .HasColumnType("integer"); + + b.Property("TradeWins") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("numeric(18,8)"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("Bots"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedBacktests") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentBacktest") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DateTimeRangesJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("EstimatedTimeRemainingSeconds") + .HasColumnType("integer"); + + b.Property("FailedBacktests") + .HasColumnType("integer"); + + b.Property("MoneyManagementVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProgressInfo") + .HasColumnType("text"); + + b.Property("RequestId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("ResultsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TickerVariantsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("TotalBacktests") + .HasColumnType("integer"); + + b.Property("UniversalConfigJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "CreatedAt"); + + b.ToTable("BundleBacktestRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.FundingRateEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .HasColumnType("integer"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("OpenInterest") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Rate") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Exchange", "Date"); + + b.HasIndex("Ticker", "Exchange"); + + b.HasIndex("Ticker", "Exchange", "Date") + .IsUnique(); + + b.ToTable("FundingRates"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(18,8)"); + + b.Property("BestChromosome") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("BestFitness") + .HasColumnType("double precision"); + + b.Property("BestFitnessSoFar") + .HasColumnType("double precision"); + + b.Property("BestIndividual") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CrossoverMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentGeneration") + .HasColumnType("integer"); + + b.Property("EligibleIndicatorsJson") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ElitismPercentage") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Generations") + .HasColumnType("integer"); + + b.Property("MaxTakeProfit") + .HasColumnType("double precision"); + + b.Property("MutationMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("MutationRate") + .HasColumnType("double precision"); + + b.Property("PopulationSize") + .HasColumnType("integer"); + + b.Property("ProgressInfo") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SelectionMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("RequestId") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("UserId"); + + b.ToTable("GeneticRequests"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CyclePeriods") + .HasColumnType("integer"); + + b.Property("FastPeriods") + .HasColumnType("integer"); + + b.Property("MinimumHistory") + .HasColumnType("integer"); + + b.Property("Multiplier") + .HasColumnType("double precision"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Period") + .HasColumnType("integer"); + + b.Property("SignalPeriods") + .HasColumnType("integer"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("SlowPeriods") + .HasColumnType("integer"); + + b.Property("SmoothPeriods") + .HasColumnType("integer"); + + b.Property("StochPeriods") + .HasColumnType("integer"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Indicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,8)"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.Property("UserName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserName"); + + b.HasIndex("UserName", "Name"); + + b.ToTable("MoneyManagements"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.Property("Identifier") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("GasFees") + .HasColumnType("decimal(18,8)"); + + b.Property("Initiator") + .IsRequired() + .HasColumnType("text"); + + b.Property("InitiatorIdentifier") + .HasColumnType("uuid"); + + b.Property("MoneyManagementJson") + .HasColumnType("text"); + + b.Property("NetPnL") + .HasColumnType("decimal(18,8)"); + + b.Property("OpenTradeId") + .HasColumnType("integer"); + + b.Property("OriginDirection") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProfitAndLoss") + .HasColumnType("decimal(18,8)"); + + b.Property("SignalIdentifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StopLossTradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit1TradeId") + .HasColumnType("integer"); + + b.Property("TakeProfit2TradeId") + .HasColumnType("integer"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("UiFees") + .HasColumnType("decimal(18,8)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Identifier"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("InitiatorIdentifier"); + + b.HasIndex("OpenTradeId"); + + b.HasIndex("Status"); + + b.HasIndex("StopLossTradeId"); + + b.HasIndex("TakeProfit1TradeId"); + + b.HasIndex("TakeProfit2TradeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Identifier"); + + b.ToTable("Positions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LoopbackPeriod") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Name"); + + b.ToTable("Scenarios"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IndicatorId") + .HasColumnType("integer"); + + b.Property("ScenarioId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IndicatorId"); + + b.HasIndex("ScenarioId", "IndicatorId") + .IsUnique(); + + b.ToTable("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CandleJson") + .HasColumnType("text"); + + b.Property("Confidence") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("IndicatorName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SignalType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Identifier"); + + b.HasIndex("Status"); + + b.HasIndex("Ticker"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "Date"); + + b.HasIndex("Identifier", "Date", "UserId") + .IsUnique(); + + b.ToTable("Signals"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SpotlightOverviewEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("uuid"); + + b.Property("ScenarioCount") + .HasColumnType("integer"); + + b.Property("SpotlightsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DateTime"); + + b.HasIndex("Identifier") + .IsUnique(); + + b.HasIndex("DateTime", "ScenarioCount"); + + b.ToTable("SpotlightOverviews"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthMinersLeaderboardEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinersData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthMinersLeaderboards"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SynthPredictionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Asset") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CacheKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBacktest") + .HasColumnType("boolean"); + + b.Property("MinerUid") + .HasColumnType("integer"); + + b.Property("PredictionData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SignalDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeIncrement") + .HasColumnType("integer"); + + b.Property("TimeLength") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CacheKey") + .IsUnique(); + + b.ToTable("SynthPredictions"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TopVolumeTickerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Exchange") + .HasColumnType("integer"); + + b.Property("Rank") + .HasColumnType("integer"); + + b.Property("Ticker") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Volume") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("Exchange"); + + b.HasIndex("Ticker"); + + b.HasIndex("Date", "Rank"); + + b.HasIndex("Exchange", "Date"); + + b.ToTable("TopVolumeTickers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Direction") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExchangeOrderId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Leverage") + .HasColumnType("decimal(18,8)"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("decimal(18,8)"); + + b.Property("Quantity") + .HasColumnType("decimal(18,8)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Ticker") + .IsRequired() + .HasColumnType("text"); + + b.Property("TradeType") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("ExchangeOrderId"); + + b.HasIndex("Status"); + + b.ToTable("Trades"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.TraderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AverageLoss") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("AverageWin") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBestTrader") + .HasColumnType("boolean"); + + b.Property("Pnl") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("Roi") + .HasPrecision(18, 8) + .HasColumnType("decimal(18,8)"); + + b.Property("TradeCount") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Winrate") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Address"); + + b.HasIndex("IsBestTrader"); + + b.HasIndex("Pnl"); + + b.HasIndex("Roi"); + + b.HasIndex("Winrate"); + + b.HasIndex("Address", "IsBestTrader") + .IsUnique(); + + b.HasIndex("IsBestTrader", "Roi"); + + b.HasIndex("IsBestTrader", "Winrate"); + + b.ToTable("Traders"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TelegramChannel") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("AgentName"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.WorkerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DelayTicks") + .HasColumnType("bigint"); + + b.Property("ExecutionCount") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastRunTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkerType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("WorkerType") + .IsUnique(); + + b.ToTable("Workers"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AccountEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany("Accounts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.AgentSummaryEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BacktestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BotEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.BundleBacktestRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.GeneticRequestEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.PositionEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "OpenTrade") + .WithMany() + .HasForeignKey("OpenTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "StopLossTrade") + .WithMany() + .HasForeignKey("StopLossTradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit1Trade") + .WithMany() + .HasForeignKey("TakeProfit1TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.TradeEntity", "TakeProfit2Trade") + .WithMany() + .HasForeignKey("TakeProfit2TradeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("OpenTrade"); + + b.Navigation("StopLossTrade"); + + b.Navigation("TakeProfit1Trade"); + + b.Navigation("TakeProfit2Trade"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioIndicatorEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", "Indicator") + .WithMany("ScenarioIndicators") + .HasForeignKey("IndicatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", "Scenario") + .WithMany("ScenarioIndicators") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Indicator"); + + b.Navigation("Scenario"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.SignalEntity", b => + { + b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.IndicatorEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.ScenarioEntity", b => + { + b.Navigation("ScenarioIndicators"); + }); + + modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", b => + { + b.Navigation("Accounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251014092447_RemoveIndicatorIndexes.cs b/src/Managing.Infrastructure.Database/Migrations/20251014092447_RemoveIndicatorIndexes.cs new file mode 100644 index 00000000..48be48d9 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251014092447_RemoveIndicatorIndexes.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class RemoveIndicatorIndexes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Backtests_UserId_IndicatorsCount", + table: "Backtests"); + + migrationBuilder.DropIndex( + name: "IX_Backtests_UserId_IndicatorsCsv", + table: "Backtests"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_Backtests_UserId_IndicatorsCount", + table: "Backtests", + columns: new[] { "UserId", "IndicatorsCount" }); + + migrationBuilder.CreateIndex( + name: "IX_Backtests_UserId_IndicatorsCsv", + table: "Backtests", + columns: new[] { "UserId", "IndicatorsCsv" }); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index 132ff0c3..e6f46757 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -157,6 +157,11 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + b.Property("EndDate") .HasColumnType("timestamp with time zone"); @@ -177,6 +182,23 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(255) .HasColumnType("character varying(255)"); + b.Property("IndicatorsCount") + .HasColumnType("integer"); + + b.Property("IndicatorsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("MaxDrawdown") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + + b.Property("MaxDrawdownRecoveryTime") + .ValueGeneratedOnAdd() + .HasColumnType("interval") + .HasDefaultValue(new TimeSpan(0, 0, 0, 0, 0)); + b.Property("Metadata") .HasColumnType("text"); @@ -184,6 +206,11 @@ namespace Managing.Infrastructure.Databases.Migrations .IsRequired() .HasColumnType("jsonb"); + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("PositionsJson") .IsRequired() .HasColumnType("jsonb"); @@ -200,6 +227,11 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(1000) .HasColumnType("text"); + b.Property("SharpeRatio") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,8)") + .HasDefaultValue(0m); + b.Property("SignalsJson") .IsRequired() .HasColumnType("jsonb"); @@ -210,6 +242,14 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("StatisticsJson") .HasColumnType("jsonb"); + b.Property("Ticker") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Timeframe") + .HasColumnType("integer"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); @@ -232,8 +272,14 @@ namespace Managing.Infrastructure.Databases.Migrations b.HasIndex("RequestId", "Score"); + b.HasIndex("UserId", "Name"); + b.HasIndex("UserId", "Score"); + b.HasIndex("UserId", "Ticker"); + + b.HasIndex("UserId", "Timeframe"); + b.ToTable("Backtests"); }); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BacktestEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BacktestEntity.cs index 2b8499a2..c7a1125d 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BacktestEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BacktestEntity.cs @@ -36,6 +36,28 @@ public class BacktestEntity [Column(TypeName = "jsonb")] public string ConfigJson { get; set; } = string.Empty; + // Denormalized bot/backtest name (e.g., "MyBundleTest #3") for sorting/filtering + [Required] + [MaxLength(255)] + public string Name { get; set; } = string.Empty; + + // Denormalized ticker string for fast filtering/sorting + [Required] + [MaxLength(32)] + public string Ticker { get; set; } = string.Empty; + + // Stored timeframe as enum numeric value for direct sorting/filtering + [Required] + public int Timeframe { get; set; } + + // Comma-separated indicator types for filtering, e.g., "EMA_CROSS,MACD_CROSS" + [Column(TypeName = "text")] + public string IndicatorsCsv { get; set; } = string.Empty; + + // Number of indicators used in the scenario for sorting/filtering + [Required] + public int IndicatorsCount { get; set; } + [Required] [Column(TypeName = "jsonb")] public string PositionsJson { get; set; } = string.Empty; @@ -50,6 +72,10 @@ public class BacktestEntity [Required] public DateTime EndDate { get; set; } + // Precomputed for filtering: EndDate - StartDate + [Required] + public TimeSpan Duration { get; set; } + [Required] [Column(TypeName = "jsonb")] public string MoneyManagementJson { get; set; } = string.Empty; @@ -63,6 +89,19 @@ public class BacktestEntity [Column(TypeName = "jsonb")] public string? StatisticsJson { get; set; } + // Extracted metrics for efficient querying/indexing + [Required] + [Column(TypeName = "decimal(18,8)")] + public decimal SharpeRatio { get; set; } + + [Required] + [Column(TypeName = "decimal(18,8)")] + public decimal MaxDrawdown { get; set; } + + // PostgreSQL maps TimeSpan to interval + [Required] + public TimeSpan MaxDrawdownRecoveryTime { get; set; } + [Required] [Column(TypeName = "decimal(18,8)")] public decimal Fees { get; set; } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs index 8db0838e..5dbcdf18 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs @@ -156,10 +156,19 @@ public class ManagingDbContext : DbContext 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.Name).IsRequired().HasMaxLength(255); + entity.Property(e => e.Ticker).HasMaxLength(32); + entity.Property(e => e.Timeframe).IsRequired(); + entity.Property(e => e.IndicatorsCsv).HasColumnType("text"); + entity.Property(e => e.IndicatorsCount).IsRequired(); 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.SharpeRatio).HasColumnType("decimal(18,8)").HasDefaultValue(0m); + entity.Property(e => e.MaxDrawdown).HasColumnType("decimal(18,8)").HasDefaultValue(0m); + entity.Property(e => e.MaxDrawdownRecoveryTime).HasDefaultValue(TimeSpan.Zero); + entity.Property(e => e.Duration).HasDefaultValue(TimeSpan.Zero); entity.Property(e => e.ScoreMessage).HasMaxLength(1000); entity.Property(e => e.Metadata).HasColumnType("text"); @@ -177,7 +186,10 @@ public class ManagingDbContext : DbContext // Composite indexes for efficient pagination and filtering entity.HasIndex(e => new { e.UserId, e.Score }); + entity.HasIndex(e => new { e.UserId, e.Name }); entity.HasIndex(e => new { e.RequestId, e.Score }); + entity.HasIndex(e => new { e.UserId, e.Ticker }); + entity.HasIndex(e => new { e.UserId, e.Timeframe }); }); // Configure BundleBacktestRequest entity diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs index 50db7e3a..26c23987 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlBacktestRepository.cs @@ -1,12 +1,14 @@ using System.Diagnostics; using Exilion.TradingAtomics; using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Shared; using Managing.Domain.Backtests; using Managing.Domain.Bots; 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; @@ -377,8 +379,13 @@ public class PostgreSqlBacktestRepository : IBacktestRepository } } - public (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, - int pageSize, string sortBy = "score", string sortOrder = "desc") + public (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated( + User user, + int page, + int pageSize, + BacktestSortableColumn sortBy = BacktestSortableColumn.Score, + string sortOrder = "desc", + BacktestsFilter? filter = null) { var stopwatch = Stopwatch.StartNew(); @@ -386,28 +393,83 @@ public class PostgreSqlBacktestRepository : IBacktestRepository .AsNoTracking() .Where(b => b.UserId == user.Id); + if (filter != null) + { + if (filter.ScoreMin.HasValue) + baseQuery = baseQuery.Where(b => b.Score >= filter.ScoreMin.Value); + if (filter.ScoreMax.HasValue) + baseQuery = baseQuery.Where(b => b.Score <= filter.ScoreMax.Value); + if (filter.WinrateMin.HasValue) + baseQuery = baseQuery.Where(b => b.WinRate >= filter.WinrateMin.Value); + if (filter.WinrateMax.HasValue) + baseQuery = baseQuery.Where(b => b.WinRate <= filter.WinrateMax.Value); + if (filter.MaxDrawdownMax.HasValue) + baseQuery = baseQuery.Where(b => b.MaxDrawdown <= filter.MaxDrawdownMax.Value); + + if (filter.Tickers != null && filter.Tickers.Any()) + { + var tickerArray = filter.Tickers.ToArray(); + baseQuery = baseQuery.Where(b => tickerArray.Contains(b.Ticker)); + } + + if (filter.Indicators != null && filter.Indicators.Any()) + { + foreach (var ind in filter.Indicators) + { + var token = "," + ind + ","; + baseQuery = baseQuery.Where(b => ("," + b.IndicatorsCsv + ",").Contains(token)); + } + } + + if (filter.DurationMin.HasValue) + baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value); + if (filter.DurationMax.HasValue) + baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value); + } + var afterQueryMs = stopwatch.ElapsedMilliseconds; var totalCount = baseQuery.Count(); var afterCountMs = stopwatch.ElapsedMilliseconds; // Apply sorting - IQueryable sortedQuery = sortBy.ToLower() switch + IQueryable sortedQuery = sortBy switch { - "score" => sortOrder == "desc" + BacktestSortableColumn.Score => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score), - "finalpnl" => sortOrder == "desc" + BacktestSortableColumn.FinalPnl => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.FinalPnl) : baseQuery.OrderBy(b => b.FinalPnl), - "winrate" => sortOrder == "desc" + BacktestSortableColumn.WinRate => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.WinRate) : baseQuery.OrderBy(b => b.WinRate), - "growthpercentage" => sortOrder == "desc" + BacktestSortableColumn.GrowthPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.GrowthPercentage) : baseQuery.OrderBy(b => b.GrowthPercentage), - "hodlpercentage" => sortOrder == "desc" + BacktestSortableColumn.HodlPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.HodlPercentage) : baseQuery.OrderBy(b => b.HodlPercentage), + BacktestSortableColumn.Duration => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.Duration) + : baseQuery.OrderBy(b => b.Duration), + BacktestSortableColumn.Timeframe => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.Timeframe) + : baseQuery.OrderBy(b => b.Timeframe), + BacktestSortableColumn.IndicatorsCount => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.IndicatorsCount) + : baseQuery.OrderBy(b => b.IndicatorsCount), + BacktestSortableColumn.MaxDrawdown => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.MaxDrawdown) + : baseQuery.OrderBy(b => b.MaxDrawdown), + BacktestSortableColumn.Fees => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.Fees) + : baseQuery.OrderBy(b => b.Fees), + BacktestSortableColumn.SharpeRatio => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.SharpeRatio) + : baseQuery.OrderBy(b => b.SharpeRatio), + BacktestSortableColumn.Ticker => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.Ticker) + : baseQuery.OrderBy(b => b.Ticker), _ => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score) @@ -427,21 +489,16 @@ public class PostgreSqlBacktestRepository : IBacktestRepository { Id = entity.Identifier, Config = JsonConvert.DeserializeObject(entity.ConfigJson), + Ticker = entity.Ticker, 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(entity.StatisticsJson)?.MaxDrawdown - : null, + MaxDrawdown = entity.MaxDrawdown, Fees = entity.Fees, - SharpeRatio = !string.IsNullOrEmpty(entity.StatisticsJson) - ? JsonConvert.DeserializeObject(entity.StatisticsJson)?.SharpeRatio != null - ? (double?)JsonConvert.DeserializeObject(entity.StatisticsJson).SharpeRatio - : null - : null, + SharpeRatio = (double?)entity.SharpeRatio, Score = entity.Score, ScoreMessage = entity.ScoreMessage ?? string.Empty }); @@ -450,36 +507,95 @@ public class PostgreSqlBacktestRepository : IBacktestRepository } public async Task<(IEnumerable Backtests, int TotalCount)> GetBacktestsByUserPaginatedAsync( - User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") + User user, + int page, + int pageSize, + BacktestSortableColumn sortBy = BacktestSortableColumn.Score, + string sortOrder = "desc", + BacktestsFilter? filter = null) { var stopwatch = Stopwatch.StartNew(); var baseQuery = _context.Backtests .AsNoTracking() .Where(b => b.UserId == user.Id); + if (filter != null) + { + if (filter.ScoreMin.HasValue) + baseQuery = baseQuery.Where(b => b.Score >= filter.ScoreMin.Value); + if (filter.ScoreMax.HasValue) + baseQuery = baseQuery.Where(b => b.Score <= filter.ScoreMax.Value); + if (filter.WinrateMin.HasValue) + baseQuery = baseQuery.Where(b => b.WinRate >= filter.WinrateMin.Value); + if (filter.WinrateMax.HasValue) + baseQuery = baseQuery.Where(b => b.WinRate <= filter.WinrateMax.Value); + if (filter.MaxDrawdownMax.HasValue) + baseQuery = baseQuery.Where(b => b.MaxDrawdown <= filter.MaxDrawdownMax.Value); + + if (filter.Tickers != null && filter.Tickers.Any()) + { + var tickerArray = filter.Tickers.ToArray(); + baseQuery = baseQuery.Where(b => tickerArray.Contains(b.Ticker)); + } + + if (filter.Indicators != null && filter.Indicators.Any()) + { + foreach (var ind in filter.Indicators) + { + var token = "," + ind + ","; + baseQuery = baseQuery.Where(b => ("," + b.IndicatorsCsv + ",").Contains(token)); + } + } + + if (filter.DurationMin.HasValue) + baseQuery = baseQuery.Where(b => b.Duration >= filter.DurationMin.Value); + if (filter.DurationMax.HasValue) + baseQuery = baseQuery.Where(b => b.Duration <= filter.DurationMax.Value); + } var afterQueryMs = stopwatch.ElapsedMilliseconds; var totalCount = await baseQuery.CountAsync().ConfigureAwait(false); var afterCountMs = stopwatch.ElapsedMilliseconds; // Apply sorting - IQueryable sortedQuery = sortBy.ToLower() switch + IQueryable sortedQuery = sortBy switch { - "score" => sortOrder == "desc" + BacktestSortableColumn.Score => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score), - "finalpnl" => sortOrder == "desc" + BacktestSortableColumn.FinalPnl => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.FinalPnl) : baseQuery.OrderBy(b => b.FinalPnl), - "winrate" => sortOrder == "desc" + BacktestSortableColumn.WinRate => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.WinRate) : baseQuery.OrderBy(b => b.WinRate), - "growthpercentage" => sortOrder == "desc" + BacktestSortableColumn.GrowthPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.GrowthPercentage) : baseQuery.OrderBy(b => b.GrowthPercentage), - "hodlpercentage" => sortOrder == "desc" + BacktestSortableColumn.HodlPercentage => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.HodlPercentage) : baseQuery.OrderBy(b => b.HodlPercentage), + BacktestSortableColumn.Duration => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.Duration) + : baseQuery.OrderBy(b => b.Duration), + BacktestSortableColumn.Timeframe => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.Timeframe) + : baseQuery.OrderBy(b => b.Timeframe), + BacktestSortableColumn.IndicatorsCount => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.IndicatorsCount) + : baseQuery.OrderBy(b => b.IndicatorsCount), + BacktestSortableColumn.MaxDrawdown => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.MaxDrawdown) + : baseQuery.OrderBy(b => b.MaxDrawdown), + BacktestSortableColumn.Fees => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.Fees) + : baseQuery.OrderBy(b => b.Fees), + BacktestSortableColumn.SharpeRatio => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.SharpeRatio) + : baseQuery.OrderBy(b => b.SharpeRatio), + BacktestSortableColumn.Ticker => sortOrder == "desc" + ? baseQuery.OrderByDescending(b => b.Ticker) + : baseQuery.OrderBy(b => b.Ticker), _ => sortOrder == "desc" ? baseQuery.OrderByDescending(b => b.Score) : baseQuery.OrderBy(b => b.Score) @@ -506,15 +622,9 @@ public class PostgreSqlBacktestRepository : IBacktestRepository HodlPercentage = entity.HodlPercentage, StartDate = entity.StartDate, EndDate = entity.EndDate, - MaxDrawdown = !string.IsNullOrEmpty(entity.StatisticsJson) - ? JsonConvert.DeserializeObject(entity.StatisticsJson)?.MaxDrawdown - : null, + MaxDrawdown = entity.MaxDrawdown, Fees = entity.Fees, - SharpeRatio = !string.IsNullOrEmpty(entity.StatisticsJson) - ? JsonConvert.DeserializeObject(entity.StatisticsJson)?.SharpeRatio != null - ? (double?)JsonConvert.DeserializeObject(entity.StatisticsJson).SharpeRatio - : null - : null, + SharpeRatio = (double?)entity.SharpeRatio, Score = entity.Score, ScoreMessage = entity.ScoreMessage ?? string.Empty }); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index fa7ebec6..ea2ae5a5 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -332,13 +332,22 @@ public static class PostgreSqlMappers GrowthPercentage = backtest.GrowthPercentage, HodlPercentage = backtest.HodlPercentage, ConfigJson = JsonConvert.SerializeObject(backtest.Config), + Name = backtest.Config?.Name ?? string.Empty, + Ticker = backtest.Config?.Ticker.ToString() ?? string.Empty, + Timeframe = (int)backtest.Config.Timeframe, + IndicatorsCsv = string.Join(',', backtest.Config.Scenario.Indicators.Select(i => i.Type.ToString())), + IndicatorsCount = backtest.Config.Scenario.Indicators.Count, PositionsJson = JsonConvert.SerializeObject(backtest.Positions.Values.ToList()), SignalsJson = JsonConvert.SerializeObject(backtest.Signals.Values.ToList()), StartDate = backtest.StartDate, EndDate = backtest.EndDate, + Duration = backtest.EndDate - backtest.StartDate, MoneyManagementJson = JsonConvert.SerializeObject(backtest.Config?.MoneyManagement), UserId = backtest.User?.Id ?? 0, StatisticsJson = backtest.Statistics != null ? JsonConvert.SerializeObject(backtest.Statistics) : null, + SharpeRatio = backtest.Statistics?.SharpeRatio ?? 0m, + MaxDrawdown = backtest.Statistics?.MaxDrawdown ?? 0m, + MaxDrawdownRecoveryTime = backtest.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero, Fees = backtest.Fees, Score = backtest.Score, ScoreMessage = backtest.ScoreMessage ?? string.Empty, @@ -976,4 +985,13 @@ public static class PostgreSqlMappers } #endregion + + private static int? ExtractBundleIndex(string name) + { + if (string.IsNullOrWhiteSpace(name)) return null; + var hashIndex = name.LastIndexOf('#'); + if (hashIndex < 0 || hashIndex + 1 >= name.Length) return null; + var numberPart = name.Substring(hashIndex + 1).Trim(); + return int.TryParse(numberPart, out var n) ? n : (int?)null; + } } \ No newline at end of file diff --git a/src/Managing.WebApp/package.json b/src/Managing.WebApp/package.json index 9ac3ed36..f8c4a0e7 100644 --- a/src/Managing.WebApp/package.json +++ b/src/Managing.WebApp/package.json @@ -22,6 +22,7 @@ "@privy-io/wagmi": "^1.0.3", "@tailwindcss/typography": "^0.5.0", "@tanstack/react-query": "^5.67.1", + "@tanstack/react-table": "^8.21.3", "@wagmi/chains": "^0.2.9", "@wagmi/connectors": "^5.7.3", "@wagmi/core": "^2.17.0", diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index bb3103bc..217936d1 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -5,7 +5,7 @@ import {useExpanded, useFilters, usePagination, useSortBy, useTable,} from 'reac import useApiUrlStore from '../../../app/store/apiStore' import useBacktestStore from '../../../app/store/backtestStore' import type {Backtest, LightBacktestResponse} from '../../../generated/ManagingApi' -import {BacktestClient} from '../../../generated/ManagingApi' +import {BacktestClient, BacktestSortableColumn, IndicatorType} from '../../../generated/ManagingApi' import {ConfigDisplayModal, IndicatorsDisplay, SelectColumnFilter} from '../../mollecules' import {UnifiedTradingModal} from '../index' import Toast from '../../mollecules/Toast/Toast' @@ -72,15 +72,15 @@ const ServerSortableTable = ({

{(() => { - // Map backend field names to column IDs for comparison - const backendToColumnMapping: { [key: string]: string } = { - 'score': 'score', - 'finalpnl': 'finalPnl', - 'winrate': 'winRate', - 'growthpercentage': 'growthPercentage', - 'hodlpercentage': 'hodlPercentage', + // Map enum sortable fields to table column ids + const enumToColumnMapping: { [key in BacktestSortableColumn]?: string } = { + [BacktestSortableColumn.Score]: 'score', + [BacktestSortableColumn.FinalPnl]: 'finalPnl', + [BacktestSortableColumn.WinRate]: 'winRate', + [BacktestSortableColumn.GrowthPercentage]: 'growthPercentage', + [BacktestSortableColumn.HodlPercentage]: 'hodlPercentage', }; - const columnId = backendToColumnMapping[currentSort?.sortBy || ''] || currentSort?.sortBy; + const columnId = enumToColumnMapping[currentSort?.sortBy as BacktestSortableColumn] || currentSort?.sortBy; return currentSort?.sortBy && columnId === column.id ? ( currentSort.sortOrder === 'desc' ? ( @@ -133,14 +133,36 @@ interface BacktestTableProps { list: LightBacktestResponse[] | undefined isFetching?: boolean displaySummary?: boolean - onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void - currentSort?: { sortBy: string; sortOrder: 'asc' | 'desc' } + onSortChange?: (sortBy: BacktestSortableColumn, sortOrder: 'asc' | 'desc') => void + currentSort?: { sortBy: BacktestSortableColumn; sortOrder: 'asc' | 'desc' } onBacktestDeleted?: () => void // Callback when a backtest is deleted + onFiltersChange?: (filters: { + scoreMin?: number | null + scoreMax?: number | null + winrateMin?: number | null + winrateMax?: number | null + maxDrawdownMax?: number | null + tickers?: string[] | null + indicators?: string[] | null + durationMinDays?: number | null + durationMaxDays?: number | null + }) => void + filters?: { + scoreMin?: number | null + scoreMax?: number | null + winrateMin?: number | null + winrateMax?: number | null + maxDrawdownMax?: number | null + tickers?: string[] | null + indicators?: string[] | null + durationMinDays?: number | null + durationMaxDays?: number | null + } } -const BacktestTable: React.FC = ({list, isFetching, onSortChange, currentSort, onBacktestDeleted}) => { +const BacktestTable: React.FC = ({list, isFetching, onSortChange, currentSort, onBacktestDeleted, onFiltersChange, filters}) => { const [rows, setRows] = useState([]) const {apiUrl} = useApiUrlStore() const {removeBacktest} = useBacktestStore() @@ -157,20 +179,75 @@ const BacktestTable: React.FC = ({list, isFetching, onSortCh const [showConfigDisplayModal, setShowConfigDisplayModal] = useState(false) const [selectedBacktestForConfigView, setSelectedBacktestForConfigView] = useState(null) + // Filters sidebar state + const [isFilterOpen, setIsFilterOpen] = useState(false) + const [scoreMin, setScoreMin] = useState(0) + const [scoreMax, setScoreMax] = useState(100) + const [winMin, setWinMin] = useState(0) + const [winMax, setWinMax] = useState(100) + const [maxDrawdownMax, setMaxDrawdownMax] = useState('') + const [tickersInput, setTickersInput] = useState('') + const [selectedIndicators, setSelectedIndicators] = useState([]) + const [durationMinDays, setDurationMinDays] = useState(null) + const [durationMaxDays, setDurationMaxDays] = useState(null) + + const applyFilters = () => { + if (!onFiltersChange) return + onFiltersChange({ + scoreMin, + scoreMax, + winrateMin: winMin, + winrateMax: winMax, + maxDrawdownMax: maxDrawdownMax === '' ? null : Number(maxDrawdownMax), + tickers: tickersInput ? tickersInput.split(',').map(s => s.trim()).filter(Boolean) : null, + indicators: selectedIndicators.length ? selectedIndicators : null, + durationMinDays, + durationMaxDays, + }) + setIsFilterOpen(false) + } + + const clearDuration = () => { + setDurationMinDays(null) + setDurationMaxDays(null) + } + + const toggleIndicator = (name: string) => { + setSelectedIndicators(prev => prev.includes(name) + ? prev.filter(i => i !== name) + : [...prev, name]) + } + + const clearIndicators = () => setSelectedIndicators([]) + + // Sync incoming filters prop to local sidebar state + useEffect(() => { + if (!filters) return + if (typeof filters.scoreMin === 'number') setScoreMin(filters.scoreMin) + if (typeof filters.scoreMax === 'number') setScoreMax(filters.scoreMax) + if (typeof filters.winrateMin === 'number') setWinMin(filters.winrateMin) + if (typeof filters.winrateMax === 'number') setWinMax(filters.winrateMax) + if (typeof filters.maxDrawdownMax === 'number') setMaxDrawdownMax(filters.maxDrawdownMax) + setTickersInput(filters.tickers && filters.tickers.length ? filters.tickers.join(',') : '') + setSelectedIndicators(filters.indicators ? [...filters.indicators] : []) + setDurationMinDays(filters.durationMinDays ?? null) + setDurationMaxDays(filters.durationMaxDays ?? null) + }, [filters]) + // Handle sort change const handleSortChange = (columnId: string, sortOrder: 'asc' | 'desc') => { if (!onSortChange) return; - // Map column IDs to backend field names - const sortByMapping: { [key: string]: string } = { - 'score': 'score', - 'finalPnl': 'finalpnl', - 'winRate': 'winrate', - 'growthPercentage': 'growthpercentage', - 'hodlPercentage': 'hodlpercentage', + // Map column IDs to BacktestSortableColumn enum + const sortByMapping: { [key: string]: BacktestSortableColumn } = { + 'score': BacktestSortableColumn.Score, + 'finalPnl': BacktestSortableColumn.FinalPnl, + 'winRate': BacktestSortableColumn.WinRate, + 'growthPercentage': BacktestSortableColumn.GrowthPercentage, + 'hodlPercentage': BacktestSortableColumn.HodlPercentage, }; - const backendSortBy = sortByMapping[columnId] || 'score'; + const backendSortBy = sortByMapping[columnId] || BacktestSortableColumn.Score; onSortChange(backendSortBy, sortOrder); }; @@ -486,6 +563,11 @@ const BacktestTable: React.FC = ({list, isFetching, onSortCh ) : ( <> + {/* Filters toggle button */} +
+ +
+ = ({list, isFetching, onSortCh currentSort={currentSort} /> + {/* Right sidebar filter panel */} + {isFilterOpen && ( + <> +
setIsFilterOpen(false)}>
+
+
+

Filters

+ +
+ + {/* Score range */} +
+
Score between
+
+ setScoreMin(Math.min(100, Math.max(0, Number(e.target.value))))} /> + to + setScoreMax(Math.min(100, Math.max(0, Number(e.target.value))))} /> +
+
+ + {/* Tickers */} +
+
Tickers
+ setTickersInput(e.target.value)} + /> +
+ + {/* Max Drawdown */} +
+
Max Drawdown (max)
+ { + const v = e.target.value + if (v === '') setMaxDrawdownMax('') + else setMaxDrawdownMax(Number(v)) + }} /> +
+ + {/* Indicators (enum selection) */} +
+
+ Indicators + {selectedIndicators.length > 0 && ( + + )} +
+
+ {Object.values(IndicatorType).map((ind) => ( + + ))} +
+
+ + {/* Winrate */} +
+
Winrate between
+
+ setWinMin(Math.min(100, Math.max(0, Number(e.target.value))))} /> + to + setWinMax(Math.min(100, Math.max(0, Number(e.target.value))))} /> +
+
+ + {/* Duration */} +
+
Duration
+
+ + + + + + +
+
+ +
+ + +
+
+ + )} + {/* Bot Configuration Modal */} {selectedBacktest && ( (null as any); } - backtest_GetBacktestsPaginated(page: number | undefined, pageSize: number | undefined, sortBy: string | null | undefined, sortOrder: string | null | undefined): Promise { + backtest_GetBacktestsPaginated(page: number | undefined, pageSize: number | undefined, sortBy: BacktestSortableColumn | undefined, sortOrder: string | null | undefined, scoreMin: number | null | undefined, scoreMax: number | null | undefined, winrateMin: number | null | undefined, winrateMax: number | null | undefined, maxDrawdownMax: number | null | undefined, tickers: string | null | undefined, indicators: string | null | undefined, durationMinDays: number | null | undefined, durationMaxDays: number | null | undefined): Promise { let url_ = this.baseUrl + "/Backtest/Paginated?"; if (page === null) throw new Error("The parameter 'page' cannot be null."); @@ -675,10 +675,30 @@ export class BacktestClient extends AuthorizedApiBase { throw new Error("The parameter 'pageSize' cannot be null."); else if (pageSize !== undefined) url_ += "pageSize=" + encodeURIComponent("" + pageSize) + "&"; - if (sortBy !== undefined && sortBy !== null) + if (sortBy === null) + throw new Error("The parameter 'sortBy' cannot be null."); + else if (sortBy !== undefined) url_ += "sortBy=" + encodeURIComponent("" + sortBy) + "&"; if (sortOrder !== undefined && sortOrder !== null) url_ += "sortOrder=" + encodeURIComponent("" + sortOrder) + "&"; + if (scoreMin !== undefined && scoreMin !== null) + url_ += "scoreMin=" + encodeURIComponent("" + scoreMin) + "&"; + if (scoreMax !== undefined && scoreMax !== null) + url_ += "scoreMax=" + encodeURIComponent("" + scoreMax) + "&"; + if (winrateMin !== undefined && winrateMin !== null) + url_ += "winrateMin=" + encodeURIComponent("" + winrateMin) + "&"; + if (winrateMax !== undefined && winrateMax !== null) + url_ += "winrateMax=" + encodeURIComponent("" + winrateMax) + "&"; + if (maxDrawdownMax !== undefined && maxDrawdownMax !== null) + url_ += "maxDrawdownMax=" + encodeURIComponent("" + maxDrawdownMax) + "&"; + if (tickers !== undefined && tickers !== null) + url_ += "tickers=" + encodeURIComponent("" + tickers) + "&"; + if (indicators !== undefined && indicators !== null) + url_ += "indicators=" + encodeURIComponent("" + indicators) + "&"; + if (durationMinDays !== undefined && durationMinDays !== null) + url_ += "durationMinDays=" + encodeURIComponent("" + durationMinDays) + "&"; + if (durationMaxDays !== undefined && durationMaxDays !== null) + url_ += "durationMaxDays=" + encodeURIComponent("" + durationMaxDays) + "&"; url_ = url_.replace(/[?&]$/, ""); let options_: RequestInit = { @@ -4275,6 +4295,21 @@ export interface LightBacktestResponse { scoreMessage: string; } +export enum BacktestSortableColumn { + Score = "Score", + FinalPnl = "FinalPnl", + WinRate = "WinRate", + GrowthPercentage = "GrowthPercentage", + HodlPercentage = "HodlPercentage", + Duration = "Duration", + Timeframe = "Timeframe", + IndicatorsCount = "IndicatorsCount", + MaxDrawdown = "MaxDrawdown", + Fees = "Fees", + SharpeRatio = "SharpeRatio", + Ticker = "Ticker", +} + export interface LightBacktest { id?: string | null; config?: TradingBotConfig | null; @@ -4290,6 +4325,7 @@ export interface LightBacktest { score?: number; scoreMessage?: string | null; metadata?: any | null; + ticker?: string | null; } export interface RunBacktestRequest { @@ -4305,7 +4341,7 @@ export interface RunBacktestRequest { } export interface TradingBotConfigRequest { - accountName: string; + accountName?: string | null; ticker: Ticker; timeframe: Timeframe; isForWatchingOnly: boolean; diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 4ddabba9..14af56de 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -532,6 +532,21 @@ export interface LightBacktestResponse { scoreMessage: string; } +export enum BacktestSortableColumn { + Score = "Score", + FinalPnl = "FinalPnl", + WinRate = "WinRate", + GrowthPercentage = "GrowthPercentage", + HodlPercentage = "HodlPercentage", + Duration = "Duration", + Timeframe = "Timeframe", + IndicatorsCount = "IndicatorsCount", + MaxDrawdown = "MaxDrawdown", + Fees = "Fees", + SharpeRatio = "SharpeRatio", + Ticker = "Ticker", +} + export interface LightBacktest { id?: string | null; config?: TradingBotConfig | null; @@ -547,6 +562,7 @@ export interface LightBacktest { score?: number; scoreMessage?: string | null; metadata?: any | null; + ticker?: string | null; } export interface RunBacktestRequest { @@ -562,7 +578,7 @@ export interface RunBacktestRequest { } export interface TradingBotConfigRequest { - accountName: string; + accountName?: string | null; ticker: Ticker; timeframe: Timeframe; isForWatchingOnly: boolean; @@ -656,7 +672,6 @@ export interface RunBundleBacktestRequest { } export interface BundleBacktestUniversalConfig { - accountName: string; timeframe: Timeframe; isForWatchingOnly: boolean; botTradingBalance: number; diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx index ad8d1e20..e29f4b70 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestScanner.tsx @@ -8,7 +8,7 @@ import {Loader, Slider} from '../../components/atoms' import {Modal, Toast} from '../../components/mollecules' import {BacktestTable, UnifiedTradingModal} from '../../components/organism' import type {LightBacktestResponse} from '../../generated/ManagingApi' -import {BacktestClient} from '../../generated/ManagingApi' +import {BacktestClient, BacktestSortableColumn} from '../../generated/ManagingApi' const PAGE_SIZE = 50 @@ -21,11 +21,24 @@ const BacktestScanner: React.FC = () => { score: 50 }) const [currentPage, setCurrentPage] = useState(1) - const [currentSort, setCurrentSort] = useState<{ sortBy: string; sortOrder: 'asc' | 'desc' }>({ - sortBy: 'score', + const [currentSort, setCurrentSort] = useState<{ sortBy: BacktestSortableColumn; sortOrder: 'asc' | 'desc' }>({ + sortBy: BacktestSortableColumn.Score, sortOrder: 'desc' }) + // Filters state coming from BacktestTable sidebar + const [filters, setFilters] = useState<{ + scoreMin?: number | null + scoreMax?: number | null + winrateMin?: number | null + winrateMax?: number | null + maxDrawdownMax?: number | null + tickers?: string[] | null + indicators?: string[] | null + durationMinDays?: number | null + durationMaxDays?: number | null + }>({}) + const { apiUrl } = useApiUrlStore() const queryClient = useQueryClient() const backtestClient = new BacktestClient({}, apiUrl) @@ -37,13 +50,22 @@ const BacktestScanner: React.FC = () => { error, refetch } = useQuery({ - queryKey: ['backtests', currentPage, currentSort.sortBy, currentSort.sortOrder], + queryKey: ['backtests', currentPage, currentSort.sortBy, currentSort.sortOrder, filters], queryFn: async () => { const response = await backtestClient.backtest_GetBacktestsPaginated( - currentPage, - PAGE_SIZE, - currentSort.sortBy, - currentSort.sortOrder + currentPage, + PAGE_SIZE, + currentSort.sortBy, + currentSort.sortOrder, + filters.scoreMin ?? null, + filters.scoreMax ?? null, + filters.winrateMin ?? null, + filters.winrateMax ?? null, + filters.maxDrawdownMax ?? null, + (filters.tickers && filters.tickers.length ? filters.tickers.join(',') : null), + (filters.indicators && filters.indicators.length ? filters.indicators.join(',') : null), + filters.durationMinDays ?? null, + filters.durationMaxDays ?? null, ) return { backtests: (response.backtests as LightBacktestResponse[]) || [], @@ -176,11 +198,27 @@ const BacktestScanner: React.FC = () => { } // Sorting handler - const handleSortChange = (sortBy: string, sortOrder: 'asc' | 'desc') => { + const handleSortChange = (sortBy: BacktestSortableColumn, sortOrder: 'asc' | 'desc') => { setCurrentSort({ sortBy, sortOrder }) setCurrentPage(1) } + // Filters handler from BacktestTable + const handleFiltersChange = (newFilters: { + scoreMin?: number | null + scoreMax?: number | null + winrateMin?: number | null + winrateMax?: number | null + maxDrawdownMax?: number | null + tickers?: string[] | null + indicators?: string[] | null + durationMinDays?: number | null + durationMaxDays?: number | null + }) => { + setFilters(newFilters) + setCurrentPage(1) + } + // Pagination handler const handlePageChange = (newPage: number) => { if (newPage < 1 || (totalPages && newPage > totalPages)) return @@ -207,11 +245,56 @@ const BacktestScanner: React.FC = () => { + {/* Selected filters summary */} +
+ {( + (filters.scoreMin !== undefined && filters.scoreMin !== null) || + (filters.scoreMax !== undefined && filters.scoreMax !== null) || + (filters.winrateMin !== undefined && filters.winrateMin !== null) || + (filters.winrateMax !== undefined && filters.winrateMax !== null) || + (filters.maxDrawdownMax !== undefined && filters.maxDrawdownMax !== null) || + (filters.tickers && filters.tickers.length) || + (filters.indicators && filters.indicators.length) || + (filters.durationMinDays !== undefined && filters.durationMinDays !== null) || + (filters.durationMaxDays !== undefined && filters.durationMaxDays !== null) + ) ? ( +
+ Active filters: + {filters.scoreMin !== undefined && filters.scoreMin !== null && ( +
Score ≥ {filters.scoreMin}
+ )} + {filters.scoreMax !== undefined && filters.scoreMax !== null && ( +
Score ≤ {filters.scoreMax}
+ )} + {filters.winrateMin !== undefined && filters.winrateMin !== null && ( +
Winrate ≥ {filters.winrateMin}%
+ )} + {filters.winrateMax !== undefined && filters.winrateMax !== null && ( +
Winrate ≤ {filters.winrateMax}%
+ )} + {filters.maxDrawdownMax !== undefined && filters.maxDrawdownMax !== null && ( +
Max DD ≤ {filters.maxDrawdownMax}
+ )} + {filters.tickers && filters.tickers.length > 0 && ( +
Tickers: {filters.tickers.join(', ')}
+ )} + {filters.indicators && filters.indicators.length > 0 && ( +
Indicators: {filters.indicators.join(', ')}
+ )} + {(filters.durationMinDays !== undefined && filters.durationMinDays !== null) || (filters.durationMaxDays !== undefined && filters.durationMaxDays !== null) ? ( +
Duration: {filters.durationMinDays ?? 0}–{filters.durationMaxDays ?? '∞'} days
+ ) : null} +
+ ) : null} +
+ { // Invalidate backtest queries when a backtest is deleted queryClient.invalidateQueries({ queryKey: ['backtests'] })