diff --git a/.cursor/rules/fullstack.mdc b/.cursor/rules/fullstack.mdc
index 6e1238b3..4fd682cb 100644
--- a/.cursor/rules/fullstack.mdc
+++ b/.cursor/rules/fullstack.mdc
@@ -97,4 +97,4 @@ Key Principles
- when dividing, make sure variable is not zero
- to test a single ts test you can run : npm run test:single test/plugins/test-name-file.test.tsx
- do not implement business logic on the controller, keep the business logic for Service files
-
+ - When adding new property to and Orleans state, always add the property after the last one and increment the id
diff --git a/src/Managing.Api/Controllers/AdminController.cs b/src/Managing.Api/Controllers/AdminController.cs
index ff36fa78..32899017 100644
--- a/src/Managing.Api/Controllers/AdminController.cs
+++ b/src/Managing.Api/Controllers/AdminController.cs
@@ -263,5 +263,94 @@ public class AdminController : BaseController
RelatedBacktestsDeleted = backtestsDeleted
});
}
+
+ ///
+ /// Retrieves paginated users for admin users.
+ /// This endpoint returns all users with all their properties.
+ ///
+ /// Page number (defaults to 1)
+ /// Number of items per page (defaults to 50, max 100)
+ /// Field to sort by (defaults to "Id")
+ /// Sort order - "asc" or "desc" (defaults to "desc")
+ /// Filter by user name contains
+ /// Filter by owner address contains
+ /// Filter by agent name contains
+ /// Filter by telegram channel contains
+ /// A paginated list of users.
+ [HttpGet]
+ [Route("Users/Paginated")]
+ public async Task> GetUsersPaginated(
+ [FromQuery] int page = 1,
+ [FromQuery] int pageSize = 50,
+ [FromQuery] UserSortableColumn sortBy = UserSortableColumn.Id,
+ [FromQuery] string sortOrder = "desc",
+ [FromQuery] string? userNameContains = null,
+ [FromQuery] string? ownerAddressContains = null,
+ [FromQuery] string? agentNameContains = null,
+ [FromQuery] string? telegramChannelContains = null)
+ {
+ if (!await IsUserAdmin())
+ {
+ _logger.LogWarning("Non-admin user attempted to access admin users endpoint");
+ return StatusCode(403, new { error = "Only admin users can access this endpoint" });
+ }
+
+ if (page < 1)
+ {
+ return BadRequest("Page must be greater than 0");
+ }
+
+ if (pageSize < 1 || pageSize > 100)
+ {
+ return BadRequest("Page size must be between 1 and 100");
+ }
+
+ if (sortOrder != "asc" && sortOrder != "desc")
+ {
+ return BadRequest("Sort order must be 'asc' or 'desc'");
+ }
+
+ // Build filter
+ var filter = new UsersFilter
+ {
+ UserNameContains = string.IsNullOrWhiteSpace(userNameContains) ? null : userNameContains.Trim(),
+ OwnerAddressContains = string.IsNullOrWhiteSpace(ownerAddressContains) ? null : ownerAddressContains.Trim(),
+ AgentNameContains = string.IsNullOrWhiteSpace(agentNameContains) ? null : agentNameContains.Trim(),
+ TelegramChannelContains = string.IsNullOrWhiteSpace(telegramChannelContains) ? null : telegramChannelContains.Trim()
+ };
+
+ var (users, totalCount) =
+ await _userService.GetUsersPaginatedAsync(
+ page,
+ pageSize,
+ sortBy,
+ sortOrder,
+ filter);
+
+ var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
+
+ var response = new PaginatedUsersResponse
+ {
+ Users = users.Select(u => new UserListItemResponse
+ {
+ Id = u.Id,
+ Name = u.Name,
+ AgentName = u.AgentName,
+ AvatarUrl = u.AvatarUrl,
+ TelegramChannel = u.TelegramChannel,
+ OwnerWalletAddress = u.OwnerWalletAddress,
+ IsAdmin = u.IsAdmin,
+ LastConnectionDate = u.LastConnectionDate
+ }),
+ TotalCount = totalCount,
+ CurrentPage = page,
+ PageSize = pageSize,
+ TotalPages = totalPages,
+ HasNextPage = page < totalPages,
+ HasPreviousPage = page > 1
+ };
+
+ return Ok(response);
+ }
}
diff --git a/src/Managing.Api/Models/Responses/PaginatedBundleBacktestRequestsResponse.cs b/src/Managing.Api/Models/Responses/PaginatedBundleBacktestRequestsResponse.cs
index e6ee1beb..a95bf0a8 100644
--- a/src/Managing.Api/Models/Responses/PaginatedBundleBacktestRequestsResponse.cs
+++ b/src/Managing.Api/Models/Responses/PaginatedBundleBacktestRequestsResponse.cs
@@ -42,6 +42,47 @@ public class PaginatedBundleBacktestRequestsResponse
public bool HasPreviousPage { get; set; }
}
+///
+/// Response model for paginated users
+///
+public class PaginatedUsersResponse
+{
+ ///
+ /// The list of users for the current page
+ ///
+ public IEnumerable Users { get; set; } = new List();
+
+ ///
+ /// Total number of users across all pages
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// Current page number
+ ///
+ public int CurrentPage { get; set; }
+
+ ///
+ /// Number of items per page
+ ///
+ public int PageSize { get; set; }
+
+ ///
+ /// Total number of pages
+ ///
+ public int TotalPages { get; set; }
+
+ ///
+ /// Whether there are more pages available
+ ///
+ public bool HasNextPage { get; set; }
+
+ ///
+ /// Whether there are previous pages available
+ ///
+ public bool HasPreviousPage { get; set; }
+}
+
///
/// Response model for a bundle backtest request list item (summary view)
///
@@ -65,4 +106,19 @@ public class BundleBacktestRequestListItemResponse
public int? EstimatedTimeRemainingSeconds { get; set; }
}
+///
+/// Response model for a user list item (summary view)
+///
+public class UserListItemResponse
+{
+ public int Id { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public string AgentName { get; set; } = string.Empty;
+ public string AvatarUrl { get; set; } = string.Empty;
+ public string TelegramChannel { get; set; } = string.Empty;
+ public string OwnerWalletAddress { get; set; } = string.Empty;
+ public bool IsAdmin { get; set; }
+ public DateTimeOffset? LastConnectionDate { get; set; }
+}
+
diff --git a/src/Managing.Application.Abstractions/Repositories/IUserRepository.cs b/src/Managing.Application.Abstractions/Repositories/IUserRepository.cs
index 614edd15..e6f15ecd 100644
--- a/src/Managing.Application.Abstractions/Repositories/IUserRepository.cs
+++ b/src/Managing.Application.Abstractions/Repositories/IUserRepository.cs
@@ -1,4 +1,6 @@
-using Managing.Domain.Users;
+using Managing.Application.Abstractions.Shared;
+using Managing.Domain.Users;
+using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Repositories;
@@ -8,5 +10,6 @@ public interface IUserRepository
Task GetUserByNameAsync(string name, bool fetchAccounts = false);
Task GetUserByIdAsync(int userId);
Task> GetAllUsersAsync();
+ Task<(IEnumerable Users, int TotalCount)> GetUsersPaginatedAsync(int page, int pageSize, UserSortableColumn sortBy, string sortOrder, UsersFilter filter);
Task SaveOrUpdateUserAsync(User user);
}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Services/IUserService.cs b/src/Managing.Application.Abstractions/Services/IUserService.cs
index 4c99dec1..a11a8f27 100644
--- a/src/Managing.Application.Abstractions/Services/IUserService.cs
+++ b/src/Managing.Application.Abstractions/Services/IUserService.cs
@@ -1,4 +1,6 @@
-using Managing.Domain.Users;
+using Managing.Application.Abstractions.Shared;
+using Managing.Domain.Users;
+using static Managing.Common.Enums;
namespace Managing.Application.Abstractions.Services;
@@ -13,4 +15,5 @@ public interface IUserService
Task GetUserByAgentName(string agentName);
Task GetUserByIdAsync(int userId);
Task> GetAllUsersAsync();
+ Task<(IEnumerable Users, int TotalCount)> GetUsersPaginatedAsync(int page, int pageSize, UserSortableColumn sortBy, string sortOrder, UsersFilter filter);
}
\ No newline at end of file
diff --git a/src/Managing.Application.Abstractions/Shared/BundleBacktestRequestsFilter.cs b/src/Managing.Application.Abstractions/Shared/BundleBacktestRequestsFilter.cs
index 6f3fd733..ce598d2d 100644
--- a/src/Managing.Application.Abstractions/Shared/BundleBacktestRequestsFilter.cs
+++ b/src/Managing.Application.Abstractions/Shared/BundleBacktestRequestsFilter.cs
@@ -68,4 +68,30 @@ public class BundleBacktestRequestsFilter
public DateTime? CreatedAtTo { get; set; }
}
+///
+/// Filter model for users
+///
+public class UsersFilter
+{
+ ///
+ /// Filter by user name contains (case-insensitive)
+ ///
+ public string? UserNameContains { get; set; }
+
+ ///
+ /// Filter by owner address contains (case-insensitive)
+ ///
+ public string? OwnerAddressContains { get; set; }
+
+ ///
+ /// Filter by agent name contains (case-insensitive)
+ ///
+ public string? AgentNameContains { get; set; }
+
+ ///
+ /// Filter by telegram channel contains (case-insensitive)
+ ///
+ public string? TelegramChannelContains { get; set; }
+}
+
diff --git a/src/Managing.Application/Users/UserService.cs b/src/Managing.Application/Users/UserService.cs
index ecad1f7f..25b39c17 100644
--- a/src/Managing.Application/Users/UserService.cs
+++ b/src/Managing.Application/Users/UserService.cs
@@ -2,11 +2,12 @@
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
-using Managing.Common;
+using Managing.Application.Abstractions.Shared;
using Managing.Domain.Accounts;
using Managing.Domain.Users;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static Managing.Common.Enums;
namespace Managing.Application.Users;
@@ -113,6 +114,10 @@ public class UserService : IUserService
await _userRepository.SaveOrUpdateUserAsync(user);
}
+ // Update last connection date
+ user.LastConnectionDate = DateTimeOffset.UtcNow;
+ await _userRepository.SaveOrUpdateUserAsync(user);
+
return user;
}
else
@@ -132,8 +137,8 @@ public class UserService : IUserService
{
Name = $"{name}-embedded",
Key = recoveredAddress,
- Exchange = Enums.TradingExchanges.Evm,
- Type = Enums.AccountType.Privy
+ Exchange = TradingExchanges.Evm,
+ Type = AccountType.Privy
});
user.Accounts = new List()
@@ -158,6 +163,10 @@ public class UserService : IUserService
// Don't throw here to avoid breaking the user creation process
}
}
+
+ // Update last connection date for new user
+ user.LastConnectionDate = DateTimeOffset.UtcNow;
+ await _userRepository.SaveOrUpdateUserAsync(user);
}
return user;
@@ -313,4 +322,9 @@ public class UserService : IUserService
return user;
}
+
+ public async Task<(IEnumerable Users, int TotalCount)> GetUsersPaginatedAsync(int page, int pageSize, UserSortableColumn sortBy, string sortOrder, UsersFilter filter)
+ {
+ return await _userRepository.GetUsersPaginatedAsync(page, pageSize, sortBy, sortOrder, filter);
+ }
}
\ No newline at end of file
diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs
index c521677c..43810ebe 100644
--- a/src/Managing.Common/Enums.cs
+++ b/src/Managing.Common/Enums.cs
@@ -544,6 +544,17 @@ public static class Enums
UpdatedAt
}
+ ///
+ /// Sortable columns for user queries
+ ///
+ public enum UserSortableColumn
+ {
+ Id,
+ Name,
+ OwnerWalletAddress,
+ AgentName
+ }
+
///
/// Event types for agent summary updates
///
diff --git a/src/Managing.Domain/Users/User.cs b/src/Managing.Domain/Users/User.cs
index b9599e48..e301f7c7 100644
--- a/src/Managing.Domain/Users/User.cs
+++ b/src/Managing.Domain/Users/User.cs
@@ -21,4 +21,6 @@ public class User
[Id(6)] public string OwnerWalletAddress { get; set; } = string.Empty;
[Id(7)] public bool IsAdmin { get; set; } = false;
+
+ [Id(8)] public DateTimeOffset? LastConnectionDate { get; set; }
}
\ No newline at end of file
diff --git a/src/Managing.Infrastructure.Database/Migrations/20251117115213_AddLastConnectionDateToUsers.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251117115213_AddLastConnectionDateToUsers.Designer.cs
new file mode 100644
index 00000000..91c88cb6
--- /dev/null
+++ b/src/Managing.Infrastructure.Database/Migrations/20251117115213_AddLastConnectionDateToUsers.Designer.cs
@@ -0,0 +1,1726 @@
+//
+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("20251117115213_AddLastConnectionDateToUsers")]
+ partial class AddLastConnectionDateToUsers
+ {
+ ///
+ 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("InitialBalance")
+ .HasColumnType("decimal(18,8)");
+
+ 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("NetPnl")
+ .HasColumnType("decimal(18,8)");
+
+ 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.Property("Version")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
+ b.HasKey("Id");
+
+ b.HasIndex("RequestId")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("UserId", "CreatedAt");
+
+ b.HasIndex("UserId", "Name", "Version");
+
+ 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.JobEntity", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("AssignedWorkerId")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("BundleRequestId")
+ .HasColumnType("uuid");
+
+ b.Property("CompletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ConfigJson")
+ .IsRequired()
+ .HasColumnType("jsonb");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EndDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ErrorMessage")
+ .HasColumnType("text");
+
+ b.Property("FailureCategory")
+ .HasColumnType("integer");
+
+ b.Property("GeneticRequestId")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("IsRetryable")
+ .HasColumnType("boolean");
+
+ b.Property("JobType")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("LastHeartbeat")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MaxRetries")
+ .HasColumnType("integer");
+
+ b.Property("Priority")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("ProgressPercentage")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("RequestId")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("ResultJson")
+ .HasColumnType("jsonb");
+
+ b.Property("RetryAfter")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RetryCount")
+ .HasColumnType("integer");
+
+ b.Property("StartDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("StartedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property("UserId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BundleRequestId")
+ .HasDatabaseName("idx_bundle_request");
+
+ b.HasIndex("GeneticRequestId")
+ .HasDatabaseName("idx_genetic_request");
+
+ b.HasIndex("AssignedWorkerId", "Status")
+ .HasDatabaseName("idx_assigned_worker");
+
+ b.HasIndex("UserId", "Status")
+ .HasDatabaseName("idx_user_status");
+
+ b.HasIndex("Status", "JobType", "Priority", "CreatedAt")
+ .HasDatabaseName("idx_status_jobtype_priority_created");
+
+ b.ToTable("Jobs", (string)null);
+ });
+
+ 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