From 6bfefc91c8c02eda36add1b6b3ccae89aa57da36 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Thu, 23 Oct 2025 12:31:30 +0700 Subject: [PATCH] Deserialized variant for bundle backtest --- .../Controllers/BacktestController.cs | 73 +------- .../BundleBacktestRequestViewModel.cs | 170 ++++++++++++++++++ .../Shared/WebhookService.cs | 18 +- .../src/generated/ManagingApi.ts | 37 +++- .../src/generated/ManagingApiTypes.ts | 21 +++ 5 files changed, 231 insertions(+), 88 deletions(-) create mode 100644 src/Managing.Api/Models/Responses/BundleBacktestRequestViewModel.cs diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 14bf30d6..13f081bd 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Managing.Api.Models.Requests; +using Managing.Api.Models.Responses; using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Shared; using Managing.Application.Hubs; @@ -677,75 +678,6 @@ public class BacktestController : BaseController } } - /// - /// Generates individual backtest requests from variant configuration - /// - /// The bundle backtest request - /// The account name to use for all backtests - /// List of individual backtest requests - private List GenerateBacktestRequests(RunBundleBacktestRequest request, string accountName) - { - 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 = 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. /// @@ -781,7 +713,8 @@ public class BacktestController : BaseController return NotFound($"Bundle backtest request with ID {id} not found or doesn't belong to the current user."); } - return Ok(bundleRequest); + var viewModel = BundleBacktestRequestViewModel.FromDomain(bundleRequest); + return Ok(viewModel); } /// diff --git a/src/Managing.Api/Models/Responses/BundleBacktestRequestViewModel.cs b/src/Managing.Api/Models/Responses/BundleBacktestRequestViewModel.cs new file mode 100644 index 00000000..dd7c8d7d --- /dev/null +++ b/src/Managing.Api/Models/Responses/BundleBacktestRequestViewModel.cs @@ -0,0 +1,170 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Managing.Domain.Backtests; +using static Managing.Common.Enums; + +namespace Managing.Api.Models.Responses; + +/// +/// View model for bundle backtest requests with deserialized variant lists for frontend consumption +/// +public class BundleBacktestRequestViewModel +{ + /// + /// Unique identifier for the bundle backtest request + /// + [Required] + public Guid RequestId { get; set; } + + /// + /// When the request was created + /// + [Required] + public DateTime CreatedAt { get; set; } + + /// + /// When the request was completed (if completed) + /// + public DateTime? CompletedAt { get; set; } + + /// + /// Current status of the bundle backtest request + /// + [Required] + public BundleBacktestRequestStatus Status { get; set; } + + /// + /// Display name for the bundle backtest request + /// + [Required] + public string Name { get; set; } = string.Empty; + + /// + /// The universal configuration that applies to all backtests + /// + [Required] + public BundleBacktestUniversalConfig UniversalConfig { get; set; } = new(); + + /// + /// The list of DateTime ranges to test + /// + [Required] + public List DateTimeRanges { get; set; } = new(); + + /// + /// The list of money management variants to test + /// + [Required] + public List MoneyManagementVariants { get; set; } = new(); + + /// + /// The list of ticker variants to test + /// + [Required] + public List TickerVariants { get; set; } = new(); + + /// + /// The results of the bundle backtest execution + /// + public List Results { get; set; } = new(); + + /// + /// Total number of backtests in the bundle + /// + [Required] + public int TotalBacktests { get; set; } + + /// + /// Number of backtests completed so far + /// + [Required] + public int CompletedBacktests { get; set; } + + /// + /// Number of backtests that failed + /// + [Required] + public int FailedBacktests { get; set; } + + /// + /// Progress percentage (0-100) + /// + public double ProgressPercentage => TotalBacktests > 0 ? (double)CompletedBacktests / TotalBacktests * 100 : 0; + + /// + /// Error message if the request failed + /// + public string? ErrorMessage { get; set; } + + /// + /// Progress information (JSON serialized) + /// + public string? ProgressInfo { get; set; } + + /// + /// Current backtest being processed + /// + public string? CurrentBacktest { get; set; } + + /// + /// Estimated time remaining in seconds + /// + public int? EstimatedTimeRemainingSeconds { get; set; } + + /// + /// Maps a domain model to a view model by deserializing JSON fields + /// + /// The domain bundle backtest request + /// A view model with deserialized lists + public static BundleBacktestRequestViewModel FromDomain(BundleBacktestRequest request) + { + var viewModel = new BundleBacktestRequestViewModel + { + RequestId = request.RequestId, + CreatedAt = request.CreatedAt, + CompletedAt = request.CompletedAt, + Status = request.Status, + Name = request.Name, + Results = request.Results, + TotalBacktests = request.TotalBacktests, + CompletedBacktests = request.CompletedBacktests, + FailedBacktests = request.FailedBacktests, + ErrorMessage = request.ErrorMessage, + ProgressInfo = request.ProgressInfo, + CurrentBacktest = request.CurrentBacktest, + EstimatedTimeRemainingSeconds = request.EstimatedTimeRemainingSeconds + }; + + // Deserialize UniversalConfig + if (!string.IsNullOrEmpty(request.UniversalConfigJson)) + { + viewModel.UniversalConfig = JsonSerializer.Deserialize(request.UniversalConfigJson) + ?? new BundleBacktestUniversalConfig(); + } + + // Deserialize DateTimeRanges + if (!string.IsNullOrEmpty(request.DateTimeRangesJson)) + { + viewModel.DateTimeRanges = JsonSerializer.Deserialize>(request.DateTimeRangesJson) + ?? new List(); + } + + // Deserialize MoneyManagementVariants + if (!string.IsNullOrEmpty(request.MoneyManagementVariantsJson)) + { + viewModel.MoneyManagementVariants = JsonSerializer.Deserialize>(request.MoneyManagementVariantsJson) + ?? new List(); + } + + // Deserialize TickerVariants + if (!string.IsNullOrEmpty(request.TickerVariantsJson)) + { + viewModel.TickerVariants = JsonSerializer.Deserialize>(request.TickerVariantsJson) + ?? new List(); + } + + return viewModel; + } +} + diff --git a/src/Managing.Application/Shared/WebhookService.cs b/src/Managing.Application/Shared/WebhookService.cs index dd6a454c..ffe35273 100644 --- a/src/Managing.Application/Shared/WebhookService.cs +++ b/src/Managing.Application/Shared/WebhookService.cs @@ -12,23 +12,23 @@ public class WebhookService : IWebhookService private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly ILogger _logger; + private readonly string _n8nWebhookUrl; public WebhookService(HttpClient httpClient, IConfiguration configuration, ILogger logger) { _httpClient = httpClient; _configuration = configuration; _logger = logger; + _n8nWebhookUrl = _configuration["N8n:WebhookUrl"] ?? string.Empty; } public async Task SendTradeNotification(User user, string message, bool isBadBehavior = false) { try { - // Get the n8n webhook URL from configuration - var webhookUrl = _configuration["N8n:WebhookUrl"]; - if (string.IsNullOrEmpty(webhookUrl)) + if (string.IsNullOrEmpty(user.TelegramChannel)) { - _logger.LogWarning("N8n webhook URL not configured, skipping webhook notification"); + _logger.LogWarning("No telegram channel configured"); return; } @@ -43,7 +43,7 @@ public class WebhookService : IWebhookService }; // Send the webhook notification - var response = await _httpClient.PostAsJsonAsync(webhookUrl, payload); + var response = await _httpClient.PostAsJsonAsync(_n8nWebhookUrl, payload); if (response.IsSuccessStatusCode) { @@ -64,11 +64,9 @@ public class WebhookService : IWebhookService { try { - // Get the n8n webhook URL from configuration - var webhookUrl = _configuration["N8n:WebhookUrl"]; - if (string.IsNullOrEmpty(webhookUrl)) + if (string.IsNullOrEmpty(telegramChannel)) { - _logger.LogWarning("N8n webhook URL not configured, skipping webhook message"); + _logger.LogWarning("No telegram channel configured"); return; } @@ -82,7 +80,7 @@ public class WebhookService : IWebhookService }; // Send the webhook notification - var response = await _httpClient.PostAsJsonAsync(webhookUrl, payload); + var response = await _httpClient.PostAsJsonAsync(_n8nWebhookUrl, payload); if (response.IsSuccessStatusCode) { diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index ed6792ab..14055095 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -872,7 +872,7 @@ export class BacktestClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - backtest_GetBundleBacktestRequests(): Promise { + backtest_GetBundleBacktestRequests(): Promise { let url_ = this.baseUrl + "/Backtest/Bundle"; url_ = url_.replace(/[?&]$/, ""); @@ -890,13 +890,13 @@ export class BacktestClient extends AuthorizedApiBase { }); } - protected processBacktest_GetBundleBacktestRequests(response: Response): Promise { + protected processBacktest_GetBundleBacktestRequests(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200) { return response.text().then((_responseText) => { let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BundleBacktestRequest[]; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BundleBacktestRequestViewModel[]; return result200; }); } else if (status !== 200 && status !== 204) { @@ -904,10 +904,10 @@ export class BacktestClient extends AuthorizedApiBase { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } - backtest_GetBundleBacktestRequest(id: string): Promise { + backtest_GetBundleBacktestRequest(id: string): Promise { let url_ = this.baseUrl + "/Backtest/Bundle/{id}"; if (id === undefined || id === null) throw new Error("The parameter 'id' must be defined."); @@ -928,13 +928,13 @@ export class BacktestClient extends AuthorizedApiBase { }); } - protected processBacktest_GetBundleBacktestRequest(response: Response): Promise { + protected processBacktest_GetBundleBacktestRequest(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200) { return response.text().then((_responseText) => { let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BundleBacktestRequest; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as BundleBacktestRequestViewModel; return result200; }); } else if (status !== 200 && status !== 204) { @@ -942,7 +942,7 @@ export class BacktestClient extends AuthorizedApiBase { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } backtest_DeleteBundleBacktestRequest(id: string): Promise { @@ -4536,6 +4536,27 @@ export interface MoneyManagementVariant { moneyManagement?: MoneyManagementRequest; } +export interface BundleBacktestRequestViewModel { + requestId: string; + createdAt: Date; + completedAt?: Date | null; + status: BundleBacktestRequestStatus; + name: string; + universalConfig: BundleBacktestUniversalConfig; + dateTimeRanges: DateTimeRange[]; + moneyManagementVariants: MoneyManagementVariant[]; + tickerVariants: Ticker[]; + results?: string[]; + totalBacktests: number; + completedBacktests: number; + failedBacktests: number; + progressPercentage?: number; + errorMessage?: string | null; + progressInfo?: string | null; + currentBacktest?: string | null; + estimatedTimeRemainingSeconds?: number | null; +} + export interface GeneticRequest { requestId: string; user: User; diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index 65eedafc..165f81ba 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -711,6 +711,27 @@ export interface MoneyManagementVariant { moneyManagement?: MoneyManagementRequest; } +export interface BundleBacktestRequestViewModel { + requestId: string; + createdAt: Date; + completedAt?: Date | null; + status: BundleBacktestRequestStatus; + name: string; + universalConfig: BundleBacktestUniversalConfig; + dateTimeRanges: DateTimeRange[]; + moneyManagementVariants: MoneyManagementVariant[]; + tickerVariants: Ticker[]; + results?: string[]; + totalBacktests: number; + completedBacktests: number; + failedBacktests: number; + progressPercentage?: number; + errorMessage?: string | null; + progressInfo?: string | null; + currentBacktest?: string | null; + estimatedTimeRemainingSeconds?: number | null; +} + export interface GeneticRequest { requestId: string; user: User;