diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 4ddcaa1e..33593735 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -419,21 +419,30 @@ public class BacktestController : BaseController /// Creates a bundle backtest request with the specified configurations. /// This endpoint creates a request that will be processed by a background worker. /// - /// The list of backtest requests to execute. - /// Display name for the bundle (required). + /// The bundle backtest request with variant lists. /// The bundle backtest request with ID for tracking progress. [HttpPost] [Route("BacktestBundle")] public async Task> RunBundle([FromBody] RunBundleBacktestRequest request) { - if (request?.Requests == null || !request.Requests.Any()) + if (request?.UniversalConfig == null) { - return BadRequest("At least one backtest request is required"); + return BadRequest("Universal configuration is required"); } - if (request.Requests.Count > 10) + if (request.DateTimeRanges == null || !request.DateTimeRanges.Any()) { - return BadRequest("Maximum of 10 backtests allowed per bundle request"); + return BadRequest("At least one DateTime range is required"); + } + + if (request.MoneyManagementVariants == null || !request.MoneyManagementVariants.Any()) + { + return BadRequest("At least one money management variant is required"); + } + + if (request.TickerVariants == null || !request.TickerVariants.Any()) + { + return BadRequest("At least one ticker variant is required"); } if (string.IsNullOrWhiteSpace(request.Name)) @@ -441,32 +450,35 @@ public class BacktestController : BaseController return BadRequest("Bundle name is required"); } + // Calculate total number of backtests + var totalBacktests = request.DateTimeRanges.Count * request.MoneyManagementVariants.Count * request.TickerVariants.Count; + + if (totalBacktests > 100) + { + return BadRequest("Maximum of 100 backtests allowed per bundle request"); + } + try { var user = await GetUser(); - // Validate all requests before creating the bundle - foreach (var req in request.Requests) + // Validate universal configuration + if (string.IsNullOrEmpty(request.UniversalConfig.AccountName)) { - if (req?.Config == null) - { - return BadRequest("Invalid request: Configuration is required"); - } + return BadRequest("Account name is required in universal configuration"); + } - if (string.IsNullOrEmpty(req.Config.AccountName)) - { - return BadRequest("Invalid request: Account name is required"); - } + if (string.IsNullOrEmpty(request.UniversalConfig.ScenarioName) && request.UniversalConfig.Scenario == null) + { + return BadRequest("Either scenario name or scenario object is required in universal configuration"); + } - if (string.IsNullOrEmpty(req.Config.ScenarioName) && req.Config.Scenario == null) + // Validate all money management variants + foreach (var mmVariant in request.MoneyManagementVariants) + { + if (mmVariant.MoneyManagement == null) { - return BadRequest("Invalid request: Either scenario name or scenario object is required"); - } - - if (string.IsNullOrEmpty(req.Config.MoneyManagementName) && req.Config.MoneyManagement == null) - { - return BadRequest( - "Invalid request: Either money management name or money management object is required"); + return BadRequest("Each money management variant must have a money management object"); } } @@ -474,8 +486,11 @@ public class BacktestController : BaseController var bundleRequest = new BundleBacktestRequest { User = user, - BacktestRequestsJson = JsonSerializer.Serialize(request.Requests), - TotalBacktests = request.Requests.Count, + UniversalConfigJson = JsonSerializer.Serialize(request.UniversalConfig), + DateTimeRangesJson = JsonSerializer.Serialize(request.DateTimeRanges), + MoneyManagementVariantsJson = JsonSerializer.Serialize(request.MoneyManagementVariants), + TickerVariantsJson = JsonSerializer.Serialize(request.TickerVariants), + TotalBacktests = totalBacktests, CompletedBacktests = 0, FailedBacktests = 0, Status = BundleBacktestRequestStatus.Pending, @@ -491,6 +506,72 @@ public class BacktestController : BaseController } } + /// + /// Generates individual backtest requests from variant configuration + /// + /// The bundle backtest request + /// List of individual backtest requests + private List GenerateBacktestRequests(RunBundleBacktestRequest request) + { + var backtestRequests = new List(); + + foreach (var dateRange in request.DateTimeRanges) + { + foreach (var mmVariant in request.MoneyManagementVariants) + { + foreach (var ticker in request.TickerVariants) + { + var config = new TradingBotConfigRequest + { + AccountName = request.UniversalConfig.AccountName, + Ticker = ticker, + Timeframe = request.UniversalConfig.Timeframe, + IsForWatchingOnly = request.UniversalConfig.IsForWatchingOnly, + BotTradingBalance = request.UniversalConfig.BotTradingBalance, + Name = $"{request.UniversalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}", + FlipPosition = request.UniversalConfig.FlipPosition, + CooldownPeriod = request.UniversalConfig.CooldownPeriod, + MaxLossStreak = request.UniversalConfig.MaxLossStreak, + Scenario = request.UniversalConfig.Scenario, + ScenarioName = request.UniversalConfig.ScenarioName, + MoneyManagement = mmVariant.MoneyManagement, + MaxPositionTimeHours = request.UniversalConfig.MaxPositionTimeHours, + CloseEarlyWhenProfitable = request.UniversalConfig.CloseEarlyWhenProfitable, + FlipOnlyWhenInProfit = request.UniversalConfig.FlipOnlyWhenInProfit, + UseSynthApi = request.UniversalConfig.UseSynthApi, + UseForPositionSizing = request.UniversalConfig.UseForPositionSizing, + UseForSignalFiltering = request.UniversalConfig.UseForSignalFiltering, + UseForDynamicStopLoss = request.UniversalConfig.UseForDynamicStopLoss + }; + + var backtestRequest = new RunBacktestRequest + { + Config = config, + StartDate = dateRange.StartDate, + EndDate = dateRange.EndDate, + Balance = request.UniversalConfig.BotTradingBalance, + WatchOnly = request.UniversalConfig.WatchOnly, + Save = request.UniversalConfig.Save, + WithCandles = request.UniversalConfig.WithCandles, + MoneyManagement = mmVariant.MoneyManagement != null ? + new MoneyManagement + { + Name = mmVariant.MoneyManagement.Name, + Timeframe = mmVariant.MoneyManagement.Timeframe, + StopLoss = mmVariant.MoneyManagement.StopLoss, + TakeProfit = mmVariant.MoneyManagement.TakeProfit, + Leverage = mmVariant.MoneyManagement.Leverage + } : null + }; + + backtestRequests.Add(backtestRequest); + } + } + } + + return backtestRequests; + } + /// /// Retrieves all bundle backtest requests for the authenticated user. /// @@ -741,35 +822,3 @@ public class BacktestController : BaseController }; } } - -/// -/// Request model for running a backtest -/// -public class RunBacktestRequest -{ - /// - /// The trading bot configuration request to use for the backtest - /// - public TradingBotConfigRequest Config { get; set; } - - /// - /// The start date for the backtest - /// - public DateTime StartDate { get; set; } - - /// - /// The end date for the backtest - /// - public DateTime EndDate { get; set; } - - /// - /// Whether to save the backtest results - /// - public bool Save { get; set; } = false; - - /// - /// Whether to include candles and indicators values in the response. - /// Set to false to reduce response size dramatically. - /// - public bool WithCandles { get; set; } = false; -} \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs b/src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs index d964f8e7..e34304fc 100644 --- a/src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs +++ b/src/Managing.Api/Models/Requests/RunBundleBacktestRequest.cs @@ -1,11 +1,41 @@ using System.ComponentModel.DataAnnotations; -using Managing.Api.Controllers; +using Managing.Domain.Backtests; +using static Managing.Common.Enums; namespace Managing.Api.Models.Requests; +/// +/// Request model for running bundle backtests with variant lists instead of individual requests +/// public class RunBundleBacktestRequest { - [Required] public string Name { get; set; } = string.Empty; + /// + /// Display name for the bundle backtest + /// + [Required] + public string Name { get; set; } = string.Empty; - [Required] public List Requests { get; set; } = new(); + /// + /// Universal configuration that applies to all backtests in the bundle + /// + [Required] + public BundleBacktestUniversalConfig UniversalConfig { get; set; } = new(); + + /// + /// List of DateTime ranges to test (each range will generate a separate backtest) + /// + [Required] + public List DateTimeRanges { get; set; } = new(); + + /// + /// List of money management configurations to test (each will generate a separate backtest) + /// + [Required] + public List MoneyManagementVariants { get; set; } = new(); + + /// + /// List of tickers to test (each will generate a separate backtest) + /// + [Required] + public List TickerVariants { get; set; } = new(); } \ No newline at end of file diff --git a/src/Managing.Application/Grains/BundleBacktestGrain.cs b/src/Managing.Application/Grains/BundleBacktestGrain.cs index a4252e29..370810f3 100644 --- a/src/Managing.Application/Grains/BundleBacktestGrain.cs +++ b/src/Managing.Application/Grains/BundleBacktestGrain.cs @@ -12,6 +12,7 @@ using Managing.Domain.Strategies; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Orleans.Concurrency; +using static Managing.Common.Enums; namespace Managing.Application.Grains; @@ -103,12 +104,11 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable bundleRequest.Status = BundleBacktestRequestStatus.Running; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); - // Deserialize the backtest requests as strongly-typed objects - var backtestRequests = - JsonSerializer.Deserialize>(bundleRequest.BacktestRequestsJson); - if (backtestRequests == null) + // Generate backtest requests from variant configuration + var backtestRequests = GenerateBacktestRequestsFromVariants(bundleRequest); + if (backtestRequests == null || !backtestRequests.Any()) { - throw new InvalidOperationException("Failed to deserialize backtest requests"); + throw new InvalidOperationException("Failed to generate backtest requests from variants"); } // Process each backtest request sequentially @@ -130,6 +130,90 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable } } + /// + /// Generates individual backtest requests from variant configuration + /// + private List GenerateBacktestRequestsFromVariants(BundleBacktestRequest bundleRequest) + { + try + { + // Deserialize the variant configurations + var universalConfig = JsonSerializer.Deserialize(bundleRequest.UniversalConfigJson); + var dateTimeRanges = JsonSerializer.Deserialize>(bundleRequest.DateTimeRangesJson); + var moneyManagementVariants = JsonSerializer.Deserialize>(bundleRequest.MoneyManagementVariantsJson); + var tickerVariants = JsonSerializer.Deserialize>(bundleRequest.TickerVariantsJson); + + if (universalConfig == null || dateTimeRanges == null || moneyManagementVariants == null || tickerVariants == null) + { + _logger.LogError("Failed to deserialize variant configurations for bundle request {RequestId}", bundleRequest.RequestId); + return new List(); + } + + var backtestRequests = new List(); + + foreach (var dateRange in dateTimeRanges) + { + foreach (var mmVariant in moneyManagementVariants) + { + foreach (var ticker in tickerVariants) + { + var config = new TradingBotConfigRequest + { + AccountName = universalConfig.AccountName, + Ticker = ticker, + Timeframe = universalConfig.Timeframe, + IsForWatchingOnly = universalConfig.IsForWatchingOnly, + BotTradingBalance = universalConfig.BotTradingBalance, + Name = $"{universalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}", + FlipPosition = universalConfig.FlipPosition, + CooldownPeriod = universalConfig.CooldownPeriod, + MaxLossStreak = universalConfig.MaxLossStreak, + Scenario = universalConfig.Scenario, + ScenarioName = universalConfig.ScenarioName, + MoneyManagement = mmVariant.MoneyManagement, + MaxPositionTimeHours = universalConfig.MaxPositionTimeHours, + CloseEarlyWhenProfitable = universalConfig.CloseEarlyWhenProfitable, + FlipOnlyWhenInProfit = universalConfig.FlipOnlyWhenInProfit, + UseSynthApi = universalConfig.UseSynthApi, + UseForPositionSizing = universalConfig.UseForPositionSizing, + UseForSignalFiltering = universalConfig.UseForSignalFiltering, + UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss + }; + + var backtestRequest = new RunBacktestRequest + { + Config = config, + StartDate = dateRange.StartDate, + EndDate = dateRange.EndDate, + Balance = universalConfig.BotTradingBalance, + WatchOnly = universalConfig.WatchOnly, + Save = universalConfig.Save, + WithCandles = false, // Bundle backtests never return candles + MoneyManagement = mmVariant.MoneyManagement != null ? + new MoneyManagement + { + Name = mmVariant.MoneyManagement.Name, + Timeframe = mmVariant.MoneyManagement.Timeframe, + StopLoss = mmVariant.MoneyManagement.StopLoss, + TakeProfit = mmVariant.MoneyManagement.TakeProfit, + Leverage = mmVariant.MoneyManagement.Leverage + } : null + }; + + backtestRequests.Add(backtestRequest); + } + } + } + + return backtestRequests; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating backtest requests from variants for bundle request {RequestId}", bundleRequest.RequestId); + return new List(); + } + } + private async Task ProcessSingleBacktest( IBacktester backtester, RunBacktestRequest runBacktestRequest, @@ -138,10 +222,8 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable { try { - // Get total count from deserialized requests instead of string splitting - var backtestRequests = - JsonSerializer.Deserialize>(bundleRequest.BacktestRequestsJson); - var totalCount = backtestRequests?.Count ?? 0; + // Calculate total count from the variant configuration + var totalCount = bundleRequest.TotalBacktests; // Update current backtest being processed bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}"; diff --git a/src/Managing.Application/Users/UserService.cs b/src/Managing.Application/Users/UserService.cs index 813b25ef..f5e7e90b 100644 --- a/src/Managing.Application/Users/UserService.cs +++ b/src/Managing.Application/Users/UserService.cs @@ -64,8 +64,17 @@ public class UserService : IUserService throw new Exception($"Address {recoveredAddress} not corresponding"); } + User user = null; + // Check if account exist - var user = await _userRepository.GetUserByNameAsync(name); + try + { + user = await _userRepository.GetUserByNameAsync(name); + } + catch (Exception e) + { + Console.WriteLine(e); + } if (user != null) { diff --git a/src/Managing.Application/Workers/BundleBacktestWorker.cs b/src/Managing.Application/Workers/BundleBacktestWorker.cs index 8d35c06a..0c1d08f1 100644 --- a/src/Managing.Application/Workers/BundleBacktestWorker.cs +++ b/src/Managing.Application/Workers/BundleBacktestWorker.cs @@ -93,13 +93,11 @@ public class BundleBacktestWorker : BaseWorker bundleRequest.Status = BundleBacktestRequestStatus.Running; await backtester.UpdateBundleBacktestRequestAsync(bundleRequest); - // Deserialize the backtest requests as strongly-typed objects - var backtestRequests = - JsonSerializer.Deserialize>( - bundleRequest.BacktestRequestsJson); - if (backtestRequests == null) + // Generate backtest requests from the new variant structure + var backtestRequests = GenerateBacktestRequestsFromVariants(bundleRequest); + if (backtestRequests == null || !backtestRequests.Any()) { - throw new InvalidOperationException("Failed to deserialize backtest requests"); + throw new InvalidOperationException("Failed to generate backtest requests from variants"); } // Process each backtest request @@ -298,11 +296,9 @@ public class BundleBacktestWorker : BaseWorker // Use Results property to determine which backtests need to be retried var succeededIds = new HashSet(failedBundle.Results ?? new List()); - // Deserialize the original requests - var originalRequests = - JsonSerializer - .Deserialize>(failedBundle.BacktestRequestsJson); - if (originalRequests == null) continue; + // Generate backtest requests from the new variant structure + var originalRequests = GenerateBacktestRequestsFromVariants(failedBundle); + if (originalRequests == null || !originalRequests.Any()) continue; for (int i = failedBundle.CompletedBacktests; i < originalRequests.Count; i++) { @@ -339,4 +335,88 @@ public class BundleBacktestWorker : BaseWorker } } } + + /// + /// Generates individual backtest requests from variant configuration + /// + private List GenerateBacktestRequestsFromVariants(BundleBacktestRequest bundleRequest) + { + try + { + // Deserialize the variant configurations + var universalConfig = JsonSerializer.Deserialize(bundleRequest.UniversalConfigJson); + var dateTimeRanges = JsonSerializer.Deserialize>(bundleRequest.DateTimeRangesJson); + var moneyManagementVariants = JsonSerializer.Deserialize>(bundleRequest.MoneyManagementVariantsJson); + var tickerVariants = JsonSerializer.Deserialize>(bundleRequest.TickerVariantsJson); + + if (universalConfig == null || dateTimeRanges == null || moneyManagementVariants == null || tickerVariants == null) + { + _logger.LogError("Failed to deserialize variant configurations for bundle request {RequestId}", bundleRequest.RequestId); + return new List(); + } + + var backtestRequests = new List(); + + foreach (var dateRange in dateTimeRanges) + { + foreach (var mmVariant in moneyManagementVariants) + { + foreach (var ticker in tickerVariants) + { + var config = new TradingBotConfigRequest + { + AccountName = universalConfig.AccountName, + Ticker = ticker, + Timeframe = universalConfig.Timeframe, + IsForWatchingOnly = universalConfig.IsForWatchingOnly, + BotTradingBalance = universalConfig.BotTradingBalance, + Name = $"{universalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}", + FlipPosition = universalConfig.FlipPosition, + CooldownPeriod = universalConfig.CooldownPeriod, + MaxLossStreak = universalConfig.MaxLossStreak, + Scenario = universalConfig.Scenario, + ScenarioName = universalConfig.ScenarioName, + MoneyManagement = mmVariant.MoneyManagement, + MaxPositionTimeHours = universalConfig.MaxPositionTimeHours, + CloseEarlyWhenProfitable = universalConfig.CloseEarlyWhenProfitable, + FlipOnlyWhenInProfit = universalConfig.FlipOnlyWhenInProfit, + UseSynthApi = universalConfig.UseSynthApi, + UseForPositionSizing = universalConfig.UseForPositionSizing, + UseForSignalFiltering = universalConfig.UseForSignalFiltering, + UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss + }; + + var backtestRequest = new RunBacktestRequest + { + Config = config, + StartDate = dateRange.StartDate, + EndDate = dateRange.EndDate, + Balance = universalConfig.BotTradingBalance, + WatchOnly = universalConfig.WatchOnly, + Save = universalConfig.Save, + WithCandles = false, // Bundle backtests never return candles + MoneyManagement = mmVariant.MoneyManagement != null ? + new MoneyManagement + { + Name = mmVariant.MoneyManagement.Name, + Timeframe = mmVariant.MoneyManagement.Timeframe, + StopLoss = mmVariant.MoneyManagement.StopLoss, + TakeProfit = mmVariant.MoneyManagement.TakeProfit, + Leverage = mmVariant.MoneyManagement.Leverage + } : null + }; + + backtestRequests.Add(backtestRequest); + } + } + } + + return backtestRequests; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating backtest requests from variants for bundle request {RequestId}", bundleRequest.RequestId); + return new List(); + } + } } \ No newline at end of file diff --git a/src/Managing.Domain/Backtests/BundleBacktestRequest.cs b/src/Managing.Domain/Backtests/BundleBacktestRequest.cs index f0dcf064..17c8f6cd 100644 --- a/src/Managing.Domain/Backtests/BundleBacktestRequest.cs +++ b/src/Managing.Domain/Backtests/BundleBacktestRequest.cs @@ -1,10 +1,11 @@ +#nullable enable using System.ComponentModel.DataAnnotations; using Managing.Domain.Users; namespace Managing.Domain.Backtests; /// -/// Domain model for bundle backtest requests +/// Domain model for bundle backtest requests with variant-based configuration /// public class BundleBacktestRequest { @@ -14,7 +15,10 @@ public class BundleBacktestRequest CreatedAt = DateTime.UtcNow; Status = BundleBacktestRequestStatus.Pending; Results = new List(); - BacktestRequestsJson = string.Empty; + UniversalConfigJson = string.Empty; + DateTimeRangesJson = string.Empty; + MoneyManagementVariantsJson = string.Empty; + TickerVariantsJson = string.Empty; } /// @@ -27,7 +31,10 @@ public class BundleBacktestRequest CreatedAt = DateTime.UtcNow; Status = BundleBacktestRequestStatus.Pending; Results = new List(); - BacktestRequestsJson = string.Empty; + UniversalConfigJson = string.Empty; + DateTimeRangesJson = string.Empty; + MoneyManagementVariantsJson = string.Empty; + TickerVariantsJson = string.Empty; } /// @@ -66,10 +73,28 @@ public class BundleBacktestRequest public string Name { get; set; } /// - /// The list of backtest requests to execute (serialized as JSON) + /// The universal configuration that applies to all backtests (serialized as JSON) /// [Required] - public string BacktestRequestsJson { get; set; } = string.Empty; + public string UniversalConfigJson { get; set; } = string.Empty; + + /// + /// The list of DateTime ranges to test (serialized as JSON) + /// + [Required] + public string DateTimeRangesJson { get; set; } = string.Empty; + + /// + /// The list of money management variants to test (serialized as JSON) + /// + [Required] + public string MoneyManagementVariantsJson { get; set; } = string.Empty; + + /// + /// The list of ticker variants to test (serialized as JSON) + /// + [Required] + public string TickerVariantsJson { get; set; } = string.Empty; /// /// The results of the bundle backtest execution @@ -118,6 +143,7 @@ public class BundleBacktestRequest /// Estimated time remaining in seconds /// public int? EstimatedTimeRemainingSeconds { get; set; } + } /// diff --git a/src/Managing.Domain/Backtests/BundleBacktestUniversalConfig.cs b/src/Managing.Domain/Backtests/BundleBacktestUniversalConfig.cs new file mode 100644 index 00000000..22e266c4 --- /dev/null +++ b/src/Managing.Domain/Backtests/BundleBacktestUniversalConfig.cs @@ -0,0 +1,118 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using static Managing.Common.Enums; + +namespace Managing.Domain.Backtests; + +/// +/// Universal configuration that applies to all backtests in the bundle +/// +public class BundleBacktestUniversalConfig +{ + /// + /// The account name to use for all backtests + /// + [Required] + public string AccountName { get; set; } = string.Empty; + + /// + /// The timeframe for trading decisions + /// + [Required] + public Timeframe Timeframe { get; set; } + + /// + /// Whether this bot is for watching only (no actual trading) + /// + [Required] + public bool IsForWatchingOnly { get; set; } + + /// + /// The initial trading balance for the bot + /// + [Required] + public decimal BotTradingBalance { get; set; } + + /// + /// The name/identifier for this bot + /// + [Required] + public string BotName { get; set; } = string.Empty; + + /// + /// Whether to flip positions + /// + [Required] + public bool FlipPosition { get; set; } + + /// + /// Cooldown period between trades (in candles) + /// + public int? CooldownPeriod { get; set; } + + /// + /// Maximum consecutive losses before stopping the bot + /// + public int MaxLossStreak { get; set; } + + /// + /// The scenario configuration (takes precedence over ScenarioName) + /// + public ScenarioRequest? Scenario { get; set; } + + /// + /// The scenario name to load from database (only used when Scenario is not provided) + /// + public string? ScenarioName { get; set; } + + /// + /// Maximum time in hours that a position can remain open before being automatically closed + /// + public decimal? MaxPositionTimeHours { get; set; } + + /// + /// Whether to close positions early when they become profitable + /// + public bool CloseEarlyWhenProfitable { get; set; } = false; + + /// + /// Whether to only flip positions when the current position is in profit + /// + public bool FlipOnlyWhenInProfit { get; set; } = true; + + /// + /// Whether to use Synth API for predictions and risk assessment + /// + public bool UseSynthApi { get; set; } = false; + + /// + /// Whether to use Synth predictions for position sizing adjustments + /// + public bool UseForPositionSizing { get; set; } = true; + + /// + /// Whether to use Synth predictions for signal filtering + /// + public bool UseForSignalFiltering { get; set; } = true; + + /// + /// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments + /// + public bool UseForDynamicStopLoss { get; set; } = true; + + /// + /// Whether to only watch the backtest without executing trades + /// + public bool WatchOnly { get; set; } = false; + + /// + /// Whether to save the backtest results + /// + public bool Save { get; set; } = false; + + /// + /// Whether to include candles and indicators values in the response. + /// Note: This is always ignored for bundle backtests - candles are never returned. + /// + public bool WithCandles { get; set; } = false; +} diff --git a/src/Managing.Domain/Backtests/DateTimeRange.cs b/src/Managing.Domain/Backtests/DateTimeRange.cs new file mode 100644 index 00000000..cbe595bf --- /dev/null +++ b/src/Managing.Domain/Backtests/DateTimeRange.cs @@ -0,0 +1,22 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; + +namespace Managing.Domain.Backtests; + +/// +/// Represents a date range for backtesting +/// +public class DateTimeRange +{ + /// + /// The start date for the backtest + /// + [Required] + public DateTime StartDate { get; set; } + + /// + /// The end date for the backtest + /// + [Required] + public DateTime EndDate { get; set; } +} diff --git a/src/Managing.Domain/Backtests/MoneyManagementVariant.cs b/src/Managing.Domain/Backtests/MoneyManagementVariant.cs new file mode 100644 index 00000000..4fec86ee --- /dev/null +++ b/src/Managing.Domain/Backtests/MoneyManagementVariant.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace Managing.Domain.Backtests; + +/// +/// Represents a money management variant for backtesting +/// +public class MoneyManagementVariant +{ + /// + /// The money management details + /// + public MoneyManagementRequest MoneyManagement { get; set; } = new(); +} diff --git a/src/Managing.Infrastructure.Database/Migrations/20251012063850_UpdateBundleBacktestRequestToVariants.Designer.cs b/src/Managing.Infrastructure.Database/Migrations/20251012063850_UpdateBundleBacktestRequestToVariants.Designer.cs new file mode 100644 index 00000000..089d28b8 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251012063850_UpdateBundleBacktestRequestToVariants.Designer.cs @@ -0,0 +1,1486 @@ +// +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("20251012063850_UpdateBundleBacktestRequestToVariants")] + partial class UpdateBundleBacktestRequestToVariants + { + /// + 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("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("Metadata") + .HasColumnType("text"); + + b.Property("MoneyManagementJson") + .IsRequired() + .HasColumnType("jsonb"); + + 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("SignalsJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StatisticsJson") + .HasColumnType("jsonb"); + + 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", "Score"); + + 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/20251012063850_UpdateBundleBacktestRequestToVariants.cs b/src/Managing.Infrastructure.Database/Migrations/20251012063850_UpdateBundleBacktestRequestToVariants.cs new file mode 100644 index 00000000..0e7e86d6 --- /dev/null +++ b/src/Managing.Infrastructure.Database/Migrations/20251012063850_UpdateBundleBacktestRequestToVariants.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Managing.Infrastructure.Databases.Migrations +{ + /// + public partial class UpdateBundleBacktestRequestToVariants : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "BacktestRequestsJson", + table: "BundleBacktestRequests", + newName: "UniversalConfigJson"); + + migrationBuilder.AddColumn( + name: "DateTimeRangesJson", + table: "BundleBacktestRequests", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "MoneyManagementVariantsJson", + table: "BundleBacktestRequests", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "TickerVariantsJson", + table: "BundleBacktestRequests", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DateTimeRangesJson", + table: "BundleBacktestRequests"); + + migrationBuilder.DropColumn( + name: "MoneyManagementVariantsJson", + table: "BundleBacktestRequests"); + + migrationBuilder.DropColumn( + name: "TickerVariantsJson", + table: "BundleBacktestRequests"); + + migrationBuilder.RenameColumn( + name: "UniversalConfigJson", + table: "BundleBacktestRequests", + newName: "BacktestRequestsJson"); + } + } +} diff --git a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs index fff06f99..132ff0c3 100644 --- a/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs +++ b/src/Managing.Infrastructure.Database/Migrations/ManagingDbContextModelSnapshot.cs @@ -329,10 +329,6 @@ namespace Managing.Infrastructure.Databases.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("BacktestRequestsJson") - .IsRequired() - .HasColumnType("text"); - b.Property("CompletedAt") .HasColumnType("timestamp with time zone"); @@ -346,6 +342,10 @@ namespace Managing.Infrastructure.Databases.Migrations .HasMaxLength(500) .HasColumnType("character varying(500)"); + b.Property("DateTimeRangesJson") + .IsRequired() + .HasColumnType("text"); + b.Property("ErrorMessage") .HasColumnType("text"); @@ -355,6 +355,10 @@ namespace Managing.Infrastructure.Databases.Migrations b.Property("FailedBacktests") .HasColumnType("integer"); + b.Property("MoneyManagementVariantsJson") + .IsRequired() + .HasColumnType("text"); + b.Property("Name") .IsRequired() .HasMaxLength(255) @@ -375,9 +379,17 @@ namespace Managing.Infrastructure.Databases.Migrations .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"); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BundleBacktestRequestEntity.cs b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BundleBacktestRequestEntity.cs index b18a0352..134e4d83 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/Entities/BundleBacktestRequestEntity.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/Entities/BundleBacktestRequestEntity.cs @@ -30,7 +30,19 @@ public class BundleBacktestRequestEntity [Required] [Column(TypeName = "text")] - public string BacktestRequestsJson { get; set; } = string.Empty; + public string UniversalConfigJson { get; set; } = string.Empty; + + [Required] + [Column(TypeName = "text")] + public string DateTimeRangesJson { get; set; } = string.Empty; + + [Required] + [Column(TypeName = "text")] + public string MoneyManagementVariantsJson { get; set; } = string.Empty; + + [Required] + [Column(TypeName = "text")] + public string TickerVariantsJson { get; set; } = string.Empty; [Required] public int TotalBacktests { get; set; } diff --git a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs index 1e5deb32..8db0838e 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/ManagingDbContext.cs @@ -190,7 +190,10 @@ public class ManagingDbContext : DbContext entity.Property(e => e.Status) .IsRequired() .HasConversion(); // Store enum as string - entity.Property(e => e.BacktestRequestsJson).HasColumnType("text"); + entity.Property(e => e.UniversalConfigJson).HasColumnType("text"); + entity.Property(e => e.DateTimeRangesJson).HasColumnType("text"); + entity.Property(e => e.MoneyManagementVariantsJson).HasColumnType("text"); + entity.Property(e => e.TickerVariantsJson).HasColumnType("text"); entity.Property(e => e.ErrorMessage).HasColumnType("text"); entity.Property(e => e.ProgressInfo).HasColumnType("text"); entity.Property(e => e.CurrentBacktest).HasMaxLength(500); diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs index a6227350..fa7ebec6 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlMappers.cs @@ -367,7 +367,10 @@ public static class PostgreSqlMappers CreatedAt = entity.CreatedAt, CompletedAt = entity.CompletedAt, Status = entity.Status, - BacktestRequestsJson = entity.BacktestRequestsJson, + UniversalConfigJson = entity.UniversalConfigJson, + DateTimeRangesJson = entity.DateTimeRangesJson, + MoneyManagementVariantsJson = entity.MoneyManagementVariantsJson, + TickerVariantsJson = entity.TickerVariantsJson, TotalBacktests = entity.TotalBacktests, CompletedBacktests = entity.CompletedBacktests, FailedBacktests = entity.FailedBacktests, @@ -406,7 +409,10 @@ public static class PostgreSqlMappers CreatedAt = bundleRequest.CreatedAt, CompletedAt = bundleRequest.CompletedAt, Status = bundleRequest.Status, - BacktestRequestsJson = bundleRequest.BacktestRequestsJson, + UniversalConfigJson = bundleRequest.UniversalConfigJson, + DateTimeRangesJson = bundleRequest.DateTimeRangesJson, + MoneyManagementVariantsJson = bundleRequest.MoneyManagementVariantsJson, + TickerVariantsJson = bundleRequest.TickerVariantsJson, TotalBacktests = bundleRequest.TotalBacktests, CompletedBacktests = bundleRequest.CompletedBacktests, FailedBacktests = bundleRequest.FailedBacktests, diff --git a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs index cf27c691..6bba3560 100644 --- a/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs +++ b/src/Managing.Infrastructure.Database/PostgreSql/PostgreSqlUserRepository.cs @@ -12,8 +12,8 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito { private readonly ICacheService _cacheService; - public PostgreSqlUserRepository(ManagingDbContext context, ILogger logger, - SentrySqlMonitoringService sentryMonitoringService, ICacheService cacheService) + public PostgreSqlUserRepository(ManagingDbContext context, ILogger logger, + SentrySqlMonitoringService sentryMonitoringService, ICacheService cacheService) : base(context, logger, sentryMonitoringService) { _cacheService = cacheService; @@ -54,10 +54,10 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito return null; var user = PostgreSqlMappers.Map(userEntity); - + // Cache user for 5 minutes since user data doesn't change frequently _cacheService.SaveValue(cacheKey, user, TimeSpan.FromMinutes(5)); - + return user; } finally @@ -71,20 +71,12 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito { return await ExecuteWithLoggingAsync(async () => { - // Check cache first for frequently accessed users - var cacheKey = fetchAccounts ? $"user_name_with_accounts_{name}" : $"user_name_{name}"; - var cachedUser = _cacheService.GetValue(cacheKey); - if (cachedUser != null) - { - return cachedUser; - } - try { await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context); User user; - + if (fetchAccounts) { // Fetch user with accounts in a single query @@ -99,7 +91,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito throw new InvalidOperationException($"User with name '{name}' not found"); user = PostgreSqlMappers.Map(userEntity); - + // Map accounts using the existing mapper if (userEntity.Accounts != null) { @@ -133,12 +125,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito user = PostgreSqlMappers.Map(userEntity); user.Accounts = new List(); // Initialize empty list } - - // Cache user for 5 minutes since user data doesn't change frequently - // Use shorter cache time when including accounts since accounts change more frequently - var cacheTime = fetchAccounts ? TimeSpan.FromMinutes(2) : TimeSpan.FromMinutes(5); - _cacheService.SaveValue(cacheKey, user, cacheTime); - + return user; } finally @@ -179,10 +166,10 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito .ConfigureAwait(false); var users = userEntities.Select(PostgreSqlMappers.Map).ToList(); - + // Cache all users for 10 minutes since this data changes infrequently _cacheService.SaveValue(cacheKey, users, TimeSpan.FromMinutes(10)); - + return users; } finally @@ -209,7 +196,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito { // Capture old AgentName before updating for cache invalidation oldAgentName = existingUser.AgentName; - + // Update existing user existingUser.AgentName = user.AgentName; existingUser.AvatarUrl = user.AvatarUrl; @@ -229,7 +216,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito // Update the user object with the database-generated ID after save await _context.SaveChangesAsync().ConfigureAwait(false); user.Id = userEntity.Id; - + // Cache the new user var newUserNameCacheKey = $"user_name_{user.Name}"; var newUserAgentCacheKey = $"user_agent_{user.AgentName}"; @@ -238,34 +225,34 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito { _cacheService.SaveValue(newUserAgentCacheKey, user, TimeSpan.FromMinutes(5)); } - + // Invalidate all users cache since we added a new user _cacheService.RemoveValue("all_users"); return; // Exit early since we already saved } await _context.SaveChangesAsync().ConfigureAwait(false); - + // Invalidate cache for updated user - handle both old and new AgentName var nameCacheKey = $"user_name_{user.Name}"; var nameWithAccountsCacheKey = $"user_name_with_accounts_{user.Name}"; _cacheService.RemoveValue(nameCacheKey); _cacheService.RemoveValue(nameWithAccountsCacheKey); - + // Invalidate old AgentName cache if it existed if (!string.IsNullOrEmpty(oldAgentName)) { var oldAgentCacheKey = $"user_agent_{oldAgentName}"; _cacheService.RemoveValue(oldAgentCacheKey); } - + // Invalidate new AgentName cache if it exists if (!string.IsNullOrEmpty(user.AgentName)) { var newAgentCacheKey = $"user_agent_{user.AgentName}"; _cacheService.RemoveValue(newAgentCacheKey); } - + // Invalidate all users cache since we updated a user _cacheService.RemoveValue("all_users"); } diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 26366a42..3414a40a 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -4296,8 +4296,12 @@ export interface RunBacktestRequest { config?: TradingBotConfigRequest | null; startDate?: Date; endDate?: Date; + balance?: number; + watchOnly?: boolean; save?: boolean; withCandles?: boolean; + moneyManagementName?: string | null; + moneyManagement?: MoneyManagement | null; } export interface TradingBotConfigRequest { @@ -4352,6 +4356,10 @@ export interface MoneyManagementRequest { leverage: number; } +export interface MoneyManagement extends LightMoneyManagement { + user?: User | null; +} + export interface BundleBacktestRequest { requestId: string; user: User; @@ -4359,8 +4367,11 @@ export interface BundleBacktestRequest { completedAt?: Date | null; status: BundleBacktestRequestStatus; name: string; - backtestRequestsJson: string; - results?: string[] | null; + universalConfigJson: string; + dateTimeRangesJson: string; + moneyManagementVariantsJson: string; + tickerVariantsJson: string; + results?: string[]; totalBacktests: number; completedBacktests: number; failedBacktests: number; @@ -4381,7 +4392,42 @@ export enum BundleBacktestRequestStatus { export interface RunBundleBacktestRequest { name: string; - requests: RunBacktestRequest[]; + universalConfig: BundleBacktestUniversalConfig; + dateTimeRanges: DateTimeRange[]; + moneyManagementVariants: MoneyManagementVariant[]; + tickerVariants: Ticker[]; +} + +export interface BundleBacktestUniversalConfig { + accountName: string; + timeframe: Timeframe; + isForWatchingOnly: boolean; + botTradingBalance: number; + botName: string; + flipPosition: boolean; + cooldownPeriod?: number | null; + maxLossStreak?: number; + scenario?: ScenarioRequest | null; + scenarioName?: string | null; + maxPositionTimeHours?: number | null; + closeEarlyWhenProfitable?: boolean; + flipOnlyWhenInProfit?: boolean; + useSynthApi?: boolean; + useForPositionSizing?: boolean; + useForSignalFiltering?: boolean; + useForDynamicStopLoss?: boolean; + watchOnly?: boolean; + save?: boolean; + withCandles?: boolean; +} + +export interface DateTimeRange { + startDate: Date; + endDate: Date; +} + +export interface MoneyManagementVariant { + moneyManagement?: MoneyManagementRequest; } export interface GeneticRequest { @@ -4472,10 +4518,6 @@ export interface RunGeneticRequest { eligibleIndicators?: IndicatorType[] | null; } -export interface MoneyManagement extends LightMoneyManagement { - user?: User | null; -} - export interface StartBotRequest { config?: TradingBotConfigRequest | null; } diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 49559ee7..4ddabba9 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -553,8 +553,12 @@ export interface RunBacktestRequest { config?: TradingBotConfigRequest | null; startDate?: Date; endDate?: Date; + balance?: number; + watchOnly?: boolean; save?: boolean; withCandles?: boolean; + moneyManagementName?: string | null; + moneyManagement?: MoneyManagement | null; } export interface TradingBotConfigRequest { @@ -609,6 +613,10 @@ export interface MoneyManagementRequest { leverage: number; } +export interface MoneyManagement extends LightMoneyManagement { + user?: User | null; +} + export interface BundleBacktestRequest { requestId: string; user: User; @@ -616,8 +624,11 @@ export interface BundleBacktestRequest { completedAt?: Date | null; status: BundleBacktestRequestStatus; name: string; - backtestRequestsJson: string; - results?: string[] | null; + universalConfigJson: string; + dateTimeRangesJson: string; + moneyManagementVariantsJson: string; + tickerVariantsJson: string; + results?: string[]; totalBacktests: number; completedBacktests: number; failedBacktests: number; @@ -638,7 +649,42 @@ export enum BundleBacktestRequestStatus { export interface RunBundleBacktestRequest { name: string; - requests: RunBacktestRequest[]; + universalConfig: BundleBacktestUniversalConfig; + dateTimeRanges: DateTimeRange[]; + moneyManagementVariants: MoneyManagementVariant[]; + tickerVariants: Ticker[]; +} + +export interface BundleBacktestUniversalConfig { + accountName: string; + timeframe: Timeframe; + isForWatchingOnly: boolean; + botTradingBalance: number; + botName: string; + flipPosition: boolean; + cooldownPeriod?: number | null; + maxLossStreak?: number; + scenario?: ScenarioRequest | null; + scenarioName?: string | null; + maxPositionTimeHours?: number | null; + closeEarlyWhenProfitable?: boolean; + flipOnlyWhenInProfit?: boolean; + useSynthApi?: boolean; + useForPositionSizing?: boolean; + useForSignalFiltering?: boolean; + useForDynamicStopLoss?: boolean; + watchOnly?: boolean; + save?: boolean; + withCandles?: boolean; +} + +export interface DateTimeRange { + startDate: Date; + endDate: Date; +} + +export interface MoneyManagementVariant { + moneyManagement?: MoneyManagementRequest; } export interface GeneticRequest { @@ -729,10 +775,6 @@ export interface RunGeneticRequest { eligibleIndicators?: IndicatorType[] | null; } -export interface MoneyManagement extends LightMoneyManagement { - user?: User | null; -} - export interface StartBotRequest { config?: TradingBotConfigRequest | null; } diff --git a/src/Managing.WebApp/src/pages/backtestPage/BundleRequestModal.tsx b/src/Managing.WebApp/src/pages/backtestPage/BundleRequestModal.tsx index 1f1b79c4..a7aa506b 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/BundleRequestModal.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/BundleRequestModal.tsx @@ -1,24 +1,281 @@ import React, {useEffect, useRef, useState} from 'react'; -import {BundleBacktestRequest, LightBacktestResponse, Ticker, Timeframe} from '../../generated/ManagingApiTypes'; -import {BacktestClient} from '../../generated/ManagingApi'; +import { + AccountClient, + BacktestClient, + BundleBacktestRequest, + BundleBacktestUniversalConfig, + DataClient, + DateTimeRange, + IndicatorType, + LightBacktestResponse, + MoneyManagementRequest, + MoneyManagementVariant, + RunBundleBacktestRequest, + SignalType, + Ticker, + Timeframe +} from '../../generated/ManagingApi'; import useApiUrlStore from '../../app/store/apiStore'; import Toast from '../../components/mollecules/Toast/Toast'; import {useQuery} from '@tanstack/react-query'; import * as signalR from '@microsoft/signalr'; import AuthorizedApiBase from '../../generated/AuthorizedApiBase'; import BacktestTable from '../../components/organism/Backtest/backtestTable'; +import FormInput from '../../components/mollecules/FormInput/FormInput'; +import CustomScenario from '../../components/organism/CustomScenario/CustomScenario'; +import {useCustomScenario} from '../../app/store/customScenario'; interface BundleRequestModalProps { open: boolean; onClose: () => void; bundle: BundleBacktestRequest | null; + onCreateBundle?: (request: RunBundleBacktestRequest) => void; } -const BundleRequestModal: React.FC = ({ open, onClose, bundle }) => { +const BundleRequestModal: React.FC = ({ + open, + onClose, + bundle, + onCreateBundle +}) => { const { apiUrl } = useApiUrlStore(); const [backtests, setBacktests] = useState([]); const signalRRef = useRef(null); + // Custom scenario hook + const { scenario, setCustomScenario } = useCustomScenario(); + + // Form state for creating new bundle requests + const [strategyName, setStrategyName] = useState(''); + const [selectedAccount, setSelectedAccount] = useState(''); + const [selectedTimeframe, setSelectedTimeframe] = useState(Timeframe.FifteenMinutes); + const [selectedTickers, setSelectedTickers] = useState([]); + const [startingCapital, setStartingCapital] = useState(10000); + + // Advanced parameters state + const [cooldownPeriod, setCooldownPeriod] = useState(0); + const [maxLossStreak, setMaxLossStreak] = useState(0); + const [maxPositionTime, setMaxPositionTime] = useState(0); + const [flipPosition, setFlipPosition] = useState(false); + const [flipOnlyWhenInProfit, setFlipOnlyWhenInProfit] = useState(false); + const [closeEarlyWhenProfitable, setCloseEarlyWhenProfitable] = useState(false); + + // Variant arrays + const [dateTimeRanges, setDateTimeRanges] = useState([ + { startDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), endDate: new Date() } + ]); + const [moneyManagementVariants, setMoneyManagementVariants] = useState([ + { moneyManagement: { name: 'Default', timeframe: Timeframe.FifteenMinutes, stopLoss: 0.01, takeProfit: 0.02, leverage: 1 } } + ]); + + // API clients + const backtestClient = new BacktestClient({} as any, apiUrl); + const accountClient = new AccountClient({} as any, apiUrl); + const dataClient = new DataClient({} as any, apiUrl); + + // Fetch data + const { data: accounts } = useQuery({ + queryFn: () => accountClient.account_GetAccounts(), + queryKey: ['accounts'], + }); + + const { data: tickers } = useQuery({ + queryFn: () => dataClient.data_GetTickers(selectedTimeframe), + queryKey: ['tickers', selectedTimeframe], + enabled: !!selectedTimeframe, + }); + + // Calculate total backtests + const totalBacktests = dateTimeRanges.length * moneyManagementVariants.length * selectedTickers.length; + + // Calculate estimated time for bundle backtests + const calculateEstimatedTime = (): number => { + if (totalBacktests === 0) return 0; + + // Base time per timeframe (in seconds) - much lower realistic values + const timeframeBaseTime: Record = { + [Timeframe.OneMinute]: 0.01, + [Timeframe.FiveMinutes]: 0.02, + [Timeframe.FifteenMinutes]: 0.011, // ~15 seconds for 1,344 candles + [Timeframe.ThirtyMinutes]: 0.03, + [Timeframe.OneHour]: 0.04, + [Timeframe.FourHour]: 0.05, + [Timeframe.OneDay]: 0.1, + }; + + // Calculate total time for all date ranges + let totalTimeSeconds = 0; + + dateTimeRanges.forEach(range => { + const timeDiffMs = range.endDate.getTime() - range.startDate.getTime(); + const timeDiffDays = timeDiffMs / (1000 * 60 * 60 * 24); + + // Get base time for the selected timeframe + const baseTimePerCandle = timeframeBaseTime[selectedTimeframe] || 15; + + // Calculate candles per day based on timeframe + const candlesPerDay: Record = { + [Timeframe.OneMinute]: 1440, // 24 * 60 + [Timeframe.FiveMinutes]: 288, // 24 * 12 + [Timeframe.FifteenMinutes]: 96, // 24 * 4 + [Timeframe.ThirtyMinutes]: 48, // 24 * 2 + [Timeframe.OneHour]: 24, + [Timeframe.FourHour]: 6, + [Timeframe.OneDay]: 1, + }; + + const candlesInRange = timeDiffDays * candlesPerDay[selectedTimeframe]; + const timeForThisRange = candlesInRange * baseTimePerCandle; + + totalTimeSeconds += timeForThisRange; + }); + + // Multiply by number of variants (money management and tickers) + const variantMultiplier = moneyManagementVariants.length * selectedTickers.length; + const totalEstimatedSeconds = totalTimeSeconds * variantMultiplier; + + return Math.ceil(totalEstimatedSeconds); + }; + + const estimatedTimeSeconds = calculateEstimatedTime(); + + // Add date range variant + const addDateTimeRange = () => { + const newRange: DateTimeRange = { + startDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), + endDate: new Date() + }; + setDateTimeRanges(prev => [...prev, newRange]); + }; + + // Remove date range variant + const removeDateTimeRange = (index: number) => { + if (dateTimeRanges.length > 1) { + setDateTimeRanges(prev => prev.filter((_, i) => i !== index)); + } + }; + + // Update date range + const updateDateTimeRange = (index: number, field: 'startDate' | 'endDate', value: Date) => { + setDateTimeRanges(prev => prev.map((range, i) => + i === index ? { ...range, [field]: value } : range + )); + }; + + // Add money management variant + const addMoneyManagementVariant = () => { + const newVariant: MoneyManagementVariant = { + moneyManagement: { + name: `MM ${moneyManagementVariants.length + 1}`, + timeframe: selectedTimeframe, + stopLoss: 0.01, + takeProfit: 0.02, + leverage: 1 + } + }; + setMoneyManagementVariants(prev => [...prev, newVariant]); + }; + + // Remove money management variant + const removeMoneyManagementVariant = (index: number) => { + if (moneyManagementVariants.length > 1) { + setMoneyManagementVariants(prev => prev.filter((_, i) => i !== index)); + } + }; + + // Update money management variant + const updateMoneyManagementVariant = (index: number, field: keyof MoneyManagementRequest, value: any) => { + setMoneyManagementVariants(prev => prev.map((variant, i) => + i === index ? { + ...variant, + moneyManagement: { + ...variant.moneyManagement!, + [field]: value + } + } : variant + )); + }; + + // Handle ticker selection + const handleTickerToggle = (ticker: Ticker) => { + setSelectedTickers(prev => { + const isSelected = prev.includes(ticker); + if (isSelected) { + return prev.filter(t => t !== ticker); + } else { + return [...prev, ticker]; + } + }); + }; + + // Create bundle backtest request + const handleCreateBundle = async () => { + if (!strategyName || !selectedAccount || selectedTickers.length === 0) { + new Toast('Please fill in all required fields', false); + return; + } + + if (!scenario) { + new Toast('Please create a scenario with indicators', false); + return; + } + + const universalConfig: BundleBacktestUniversalConfig = { + accountName: selectedAccount, + timeframe: selectedTimeframe, + isForWatchingOnly: false, + botTradingBalance: startingCapital, + botName: strategyName, + flipPosition: flipPosition, + cooldownPeriod: cooldownPeriod, + maxLossStreak: maxLossStreak, + scenario: scenario ? { + name: scenario.name || 'Custom Scenario', + indicators: (scenario.indicators || []).map(indicator => ({ + name: indicator.name || 'Indicator', + type: indicator.type || IndicatorType.EmaCross, + signalType: indicator.signalType || SignalType.Signal, + period: indicator.period || 14, + fastPeriods: indicator.fastPeriods || 12, + slowPeriods: indicator.slowPeriods || 26, + signalPeriods: indicator.signalPeriods || 9, + multiplier: indicator.multiplier || 3.0, + stochPeriods: indicator.stochPeriods || 14, + smoothPeriods: indicator.smoothPeriods || 3, + cyclePeriods: indicator.cyclePeriods || 10 + })), + loopbackPeriod: scenario.loopbackPeriod || 1 + } : undefined, + maxPositionTimeHours: maxPositionTime > 0 ? maxPositionTime : null, + closeEarlyWhenProfitable: closeEarlyWhenProfitable, + flipOnlyWhenInProfit: flipOnlyWhenInProfit, + useSynthApi: false, + useForPositionSizing: true, + useForSignalFiltering: true, + useForDynamicStopLoss: true, + watchOnly: false, + save: true, + withCandles: false + }; + + const request: RunBundleBacktestRequest = { + name: strategyName, + universalConfig, + dateTimeRanges, + moneyManagementVariants, + tickerVariants: selectedTickers + }; + + try { + await onCreateBundle?.(request); + new Toast('Bundle backtest request created successfully!', true); + onClose(); + } catch (error) { + new Toast('Failed to create bundle backtest request', false); + } + }; + + // Existing bundle viewing logic const { data: queryBacktests, isLoading, @@ -28,8 +285,7 @@ const BundleRequestModal: React.FC = ({ open, onClose, queryKey: ['bundle-backtests', bundle?.requestId], queryFn: async () => { if (!open || !bundle) return []; - const client = new BacktestClient({} as any, apiUrl); - const res = await client.backtest_GetBacktestsByRequestId(bundle.requestId); + const res = await backtestClient.backtest_GetBacktestsByRequestId(bundle.requestId); if (!res) return []; return res.map((b: any) => { // Map enums for ticker and timeframe @@ -61,11 +317,12 @@ const BundleRequestModal: React.FC = ({ open, onClose, enabled: !!open && !!bundle, refetchOnWindowFocus: false, }); + useEffect(() => { if (queryBacktests) setBacktests(queryBacktests); }, [queryBacktests]); - // SignalR live updates + // SignalR live updates for existing bundles useEffect(() => { if (!open || !bundle) return; if (bundle.status !== 'Pending' && bundle.status !== 'Running') return; @@ -132,26 +389,471 @@ const BundleRequestModal: React.FC = ({ open, onClose, }; }, [open, bundle, apiUrl]); - if (!open || !bundle) return null; + if (!open) return null; + // If viewing an existing bundle + if (bundle) { + return ( +
+
+

Bundle: {bundle.name}

+
+
Request ID: {bundle.requestId}
+
Status: {bundle.status}
+
Created: {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}
+
Completed: {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}
+
+
Backtest Results
+ {isLoading ? ( +
Loading backtests...
+ ) : queryError ? ( +
{(queryError as any)?.message || 'Failed to fetch backtests'}
+ ) : ( + + )} +
+ +
+
+
+ ); + } + + // Create new bundle form return (
-
-

Bundle: {bundle.name}

-
-
Request ID: {bundle.requestId}
-
Status: {bundle.status}
-
Created: {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}
-
Completed: {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}
+
+

Create Bundle Backtest

+ +
+ {/* Left Column - Strategy Configuration */} +
+ {/* Strategy Name */} + + setStrategyName(e.target.value)} + /> + + + {/* Account Selection */} + + + + + {/* Scenario Builder */} +
+

Build your trading scenario

+

+ Create a custom scenario with indicators and parameters for your automated trading strategy. +

+ + +
+ + {/* Asset Selection */} + +

Select what your agent trades.

+
+ {tickers?.map((tickerInfo) => ( + + ))} +
+
+ + {/* Timeframe Selection */} + +

This sets how often your strategy reads and reacts to market data

+ +
+ + {/* Money Management Variants */} +
+

Choose your money management approach(s)

+

+ Select the approach that fits your goals and keep your strategy's risk in check. +

+ + {moneyManagementVariants.map((variant, index) => ( +
+
+
Money Management {index + 1}
+ {moneyManagementVariants.length > 1 && ( + + )} +
+
+ + updateMoneyManagementVariant(index, 'leverage', parseFloat(e.target.value))} + min="1" + step="0.1" + /> + + + updateMoneyManagementVariant(index, 'takeProfit', parseFloat(e.target.value))} + min="0" + step="0.01" + /> + + + updateMoneyManagementVariant(index, 'stopLoss', parseFloat(e.target.value))} + min="0" + step="0.01" + /> + +
+
+ ))} + + +
+
+ + {/* Right Column - Test Period & Backtest */} +
+ {/* Test Period */} +
+

Select the test period

+

+ Pick a historical range to evaluate your strategy. +

+ +
+ + +
+ + {dateTimeRanges.map((range, index) => ( +
+
+
Date Range {index + 1}
+ {dateTimeRanges.length > 1 && ( + + )} +
+
+ + updateDateTimeRange(index, 'startDate', new Date(e.target.value))} + /> + + + updateDateTimeRange(index, 'endDate', new Date(e.target.value))} + /> + +
+
+ ))} +
+ + {/* Advanced Parameters */} +
+ +
+
+

Advanced Parameters

+

+ Refine your strategy with additional limits. +

+
+ + + +
+
+
+ {/* Cooldown Period */} +
+ +
+ + setCooldownPeriod(Math.max(0, parseInt(e.target.value) || 0))} + min="0" + /> + +
+
+ + {/* Max Loss Streak */} +
+ +
+ + setMaxLossStreak(Math.max(0, parseInt(e.target.value) || 0))} + min="0" + /> + +
+
+ + {/* Max Position Time */} +
+ +
+ + setMaxPositionTime(Math.max(0, parseInt(e.target.value) || 0))} + min="0" + /> + +
+
+ + {/* Toggle Switches */} +
+ {/* Position Flipping */} +
+
+
+ +

+ When this switch is on, the bot can flip between long and short positions as signals change - an aggressive style. +

+
+ setFlipPosition(e.target.checked)} + /> +
+
+ + {/* Flip Only When In Profit */} +
+
+
+ +

+ When this switch is on, the bot flips sides only if the current position is profitable, limiting flips during drawdowns. +

+
+ setFlipOnlyWhenInProfit(e.target.checked)} + /> +
+
+ + {/* Close Early When Profitable */} +
+
+
+ +

+ When this switch is on, the bot exits positions as soon as they turn profitable, locking in gains sooner. +

+
+ setCloseEarlyWhenProfitable(e.target.checked)} + /> +
+
+
+
+
+
+ + {/* Starting Capital */} + + setStartingCapital(parseFloat(e.target.value))} + min="1" + step="0.01" + /> + + + {/* Backtest Cart */} +
+
+ + + +

Backtest Cart

+
+

+ A summary of your strategy creation. +

+ +
+
+ Total number of backtests + {totalBacktests} +
+
+ Total number of credits used + + {totalBacktests} + + + + +
+
+ Estimated time + + {estimatedTimeSeconds < 60 + ? `${estimatedTimeSeconds}s` + : estimatedTimeSeconds < 3600 + ? `${Math.ceil(estimatedTimeSeconds / 60)}m` + : `${Math.ceil(estimatedTimeSeconds / 3600)}h ${Math.ceil((estimatedTimeSeconds % 3600) / 60)}m` + } + +
+
+ + + +
+ + +
+
+
-
Backtest Results
- {isLoading ? ( -
Loading backtests...
- ) : queryError ? ( -
{(queryError as any)?.message || 'Failed to fetch backtests'}
- ) : ( - - )} +
diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestBundleForm.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestBundleForm.tsx index fb595dec..9eba1065 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestBundleForm.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestBundleForm.tsx @@ -1,476 +1,11 @@ -import React, {useEffect, useState} from 'react'; -import {AccountClient, BacktestClient} from '../../generated/ManagingApi'; -import type { - MoneyManagementRequest, - RunBacktestRequest, - ScenarioRequest, - TradingBotConfigRequest, -} from '../../generated/ManagingApiTypes'; -import {Ticker, Timeframe} from '../../generated/ManagingApiTypes'; -import CustomScenario from '../../components/organism/CustomScenario/CustomScenario'; -import {useCustomScenario} from '../../app/store/customScenario'; -import useApiUrlStore from '../../app/store/apiStore'; -import Toast from '../../components/mollecules/Toast/Toast'; +import React from 'react'; import BundleRequestsTable from './bundleRequestsTable'; -import {useQuery} from '@tanstack/react-query'; - -// Placeholder types (replace with your actual types) -type Indicator = { name: string; params?: Record }; -type MoneyManagementVariant = { leverage: number; tp: number; sl: number }; -type TimeRangeVariant = { start: string; end: string }; - -type Asset = 'BTC' | 'ETH' | 'GMX'; - -const allAssets: Asset[] = ['BTC', 'ETH', 'GMX']; -const allIndicators: Indicator[] = [ - { name: 'EMA Cross' }, - { name: 'MACD Cross' }, - { name: 'SuperTrend', params: { period: 12, multiplier: 4 } }, - { name: 'EMA Trend' }, - { name: 'Chandelier Exit' }, -]; - -const allTimeframes = [ - { label: '5 minutes', value: '5m' }, - { label: '15 minutes', value: '15m' }, - { label: '1 hour', value: '1h' }, - { label: '4 hours', value: '4h' }, - { label: '1 day', value: '1d' }, -]; - -const tickerMap: Record = { - BTC: Ticker.BTC, - ETH: Ticker.ETH, - GMX: Ticker.GMX, -}; -const timeframeMap: Record = { - '5m': Timeframe.FiveMinutes, - '15m': Timeframe.FifteenMinutes, - '1h': Timeframe.OneHour, - '4h': Timeframe.FourHour, - '1d': Timeframe.OneDay, -}; const BacktestBundleForm: React.FC = () => { - const {apiUrl} = useApiUrlStore() - - // API clients - const accountClient = new AccountClient({}, apiUrl); - - // Data fetching - const { data: accounts, isSuccess } = useQuery({ - queryFn: async () => { - const fetchedAccounts = await accountClient.account_GetAccounts(); - return fetchedAccounts; - }, - queryKey: ['accounts'], - }); - - // Form state - const [strategyName, setStrategyName] = useState(''); - const [loopback, setLoopback] = useState(14); - // Remove selectedIndicators, use scenario from store - const [selectedAssets, setSelectedAssets] = useState([]); - const [timeframe, setTimeframe] = useState('5m'); - const [moneyManagementVariants, setMoneyManagementVariants] = useState([ - { leverage: 2, tp: 1.5, sl: 1 }, - ]); - const [timeRangeVariants, setTimeRangeVariants] = useState([ - { start: '', end: '' }, - ]); - const [cooldown, setCooldown] = useState(0); - const [maxLossStreak, setMaxLossStreak] = useState(0); - const [maxPositionTime, setMaxPositionTime] = useState(0); - const [positionFlipping, setPositionFlipping] = useState(false); - const [flipOnlyInProfit, setFlipOnlyInProfit] = useState(false); - const [closeEarly, setCloseEarly] = useState(false); - const [startingCapital, setStartingCapital] = useState(10000); - const [accountName, setAccountName] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - - const { scenario, setCustomScenario } = useCustomScenario(); - - // Set account name when accounts are loaded - useEffect(() => { - if (accounts && accounts.length > 0 && !accountName) { - setAccountName(accounts[0].name); - } - }, [accounts, accountName]); - - // Placeholder for cart summary - const totalBacktests = - moneyManagementVariants.length * timeRangeVariants.length * (selectedAssets.length || 1); - - // Handlers (add/remove variants, select assets/indicators, etc.) - // ... - - // Generate all combinations of variants using scenario from store - const generateRequests = (): RunBacktestRequest[] => { - const requests: RunBacktestRequest[] = []; - if (!scenario) return requests; - selectedAssets.forEach(asset => { - moneyManagementVariants.forEach(mm => { - timeRangeVariants.forEach(tr => { - const mmReq: MoneyManagementRequest = { - name: `${strategyName}-MM`, - leverage: mm.leverage, - takeProfit: mm.tp, - stopLoss: mm.sl, - timeframe: timeframeMap[timeframe], - }; - const config: TradingBotConfigRequest = { - accountName: accountName, - ticker: tickerMap[asset], - timeframe: timeframeMap[timeframe], - isForWatchingOnly: false, - botTradingBalance: startingCapital, - name: `${strategyName} - ${asset}`, - flipPosition: positionFlipping, - cooldownPeriod: cooldown, - maxLossStreak: maxLossStreak, - scenario: scenario as ScenarioRequest, - moneyManagement: mmReq, - maxPositionTimeHours: maxPositionTime, - closeEarlyWhenProfitable: closeEarly, - flipOnlyWhenInProfit: flipOnlyInProfit, - useSynthApi: false, - useForPositionSizing: true, - useForSignalFiltering: true, - useForDynamicStopLoss: true, - }; - requests.push({ - config, - startDate: tr.start ? new Date(tr.start) : undefined, - endDate: tr.end ? new Date(tr.end) : undefined, - save: false, - withCandles: false, - }); - }); - }); - }); - return requests; - }; - - // API call - const handleRunBundle = async () => { - setLoading(true); - setError(null); - setSuccess(null); - const toast = new Toast('Starting bundle backtest...', true); - try { - const client = new BacktestClient({} as any, apiUrl); - const requests = generateRequests(); - if (!strategyName) throw new Error('Strategy name is required'); - if (!accountName) throw new Error('Account selection is required'); - if (requests.length === 0) throw new Error('No backtest variants to run'); - await client.backtest_RunBundle({ name: strategyName, requests }); - setSuccess('Bundle backtest started successfully!'); - toast.update('success', 'Bundle backtest started successfully!'); - } catch (e: any) { - setError(e.message || 'Failed to start bundle backtest'); - toast.update('error', e.message || 'Failed to start bundle backtest'); - } finally { - setLoading(false); - } - }; - return (

Bundle Backtest

-
- {/* Left column: Main form */} -
- {/* Name your strategy */} -
- - setStrategyName(e.target.value)} - placeholder="Road to $100k..." - /> -
- - {/* Select account */} -
- - -
- - {/* Scenario/Indicators section */} -
- -
- - {/* Select asset(s) */} -
- -
- {allAssets.map(asset => ( - - ))} -
-
- - {/* Select timeframe */} -
- - -
- - {/* Money management variants */} -
- - {moneyManagementVariants.map((mm, idx) => ( -
- { - const v = [...moneyManagementVariants]; - v[idx].leverage = Number(e.target.value); - setMoneyManagementVariants(v); - }} - placeholder="Leverage" - min={1} - /> - { - const v = [...moneyManagementVariants]; - v[idx].tp = Number(e.target.value); - setMoneyManagementVariants(v); - }} - placeholder="TP %" - min={0} - /> - { - const v = [...moneyManagementVariants]; - v[idx].sl = Number(e.target.value); - setMoneyManagementVariants(v); - }} - placeholder="SL %" - min={0} - /> - -
- ))} - -
-
- - {/* Right column: Test period, advanced params, capital, cart */} -
- {/* Test period variants */} -
- - {timeRangeVariants.map((tr, idx) => ( -
- { - const v = [...timeRangeVariants]; - v[idx].start = e.target.value; - setTimeRangeVariants(v); - }} - /> - { - const v = [...timeRangeVariants]; - v[idx].end = e.target.value; - setTimeRangeVariants(v); - }} - /> - -
- ))} - -
- - {/* Advanced Parameters */} -
-
- -
Advanced Parameters
-
-
- setCooldown(Number(e.target.value))} - placeholder="Cooldown" - /> - setMaxLossStreak(Number(e.target.value))} - placeholder="Max Loss Streak" - /> - setMaxPositionTime(Number(e.target.value))} - placeholder="Max Position Time" - /> -
-
- -
-
- -
-
- -
-
-
-
- - {/* Starting Capital */} -
- - setStartingCapital(Number(e.target.value))} - min={1} - /> -
- - {/* Backtest Cart */} -
-
Backtest Cart
-
Total number of backtests: {totalBacktests}
-
Total number of credits used: {totalBacktests}
-
Estimated time: ~ 1 min
- - - -
-
-
-
- -
+
); }; diff --git a/src/Managing.WebApp/src/pages/backtestPage/bundleRequestsTable.tsx b/src/Managing.WebApp/src/pages/backtestPage/bundleRequestsTable.tsx index 46dcee78..cdc0d817 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/bundleRequestsTable.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/bundleRequestsTable.tsx @@ -148,12 +148,28 @@ const BundleRequestsTable = () => { return (
-

Bundle Backtest Requests

+
+

Bundle Backtest Requests

+ +
setModalOpen(false)} bundle={selectedBundle} + onCreateBundle={async (request) => { + const client = new BacktestClient({} as any, apiUrl); + await client.backtest_RunBundle(request); + fetchData(); // Refresh the table + }} /> );