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;