Deserialized variant for bundle backtest

This commit is contained in:
2025-10-23 12:31:30 +07:00
parent a1fe7ed3b3
commit 6bfefc91c8
5 changed files with 231 additions and 88 deletions

View File

@@ -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
}
}
/// <summary>
/// Generates individual backtest requests from variant configuration
/// </summary>
/// <param name="request">The bundle backtest request</param>
/// <param name="accountName">The account name to use for all backtests</param>
/// <returns>List of individual backtest requests</returns>
private List<RunBacktestRequest> GenerateBacktestRequests(RunBundleBacktestRequest request, string accountName)
{
var backtestRequests = new List<RunBacktestRequest>();
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;
}
/// <summary>
/// Retrieves all bundle backtest requests for the authenticated user.
/// </summary>
@@ -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);
}
/// <summary>

View File

@@ -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;
/// <summary>
/// View model for bundle backtest requests with deserialized variant lists for frontend consumption
/// </summary>
public class BundleBacktestRequestViewModel
{
/// <summary>
/// Unique identifier for the bundle backtest request
/// </summary>
[Required]
public Guid RequestId { get; set; }
/// <summary>
/// When the request was created
/// </summary>
[Required]
public DateTime CreatedAt { get; set; }
/// <summary>
/// When the request was completed (if completed)
/// </summary>
public DateTime? CompletedAt { get; set; }
/// <summary>
/// Current status of the bundle backtest request
/// </summary>
[Required]
public BundleBacktestRequestStatus Status { get; set; }
/// <summary>
/// Display name for the bundle backtest request
/// </summary>
[Required]
public string Name { get; set; } = string.Empty;
/// <summary>
/// The universal configuration that applies to all backtests
/// </summary>
[Required]
public BundleBacktestUniversalConfig UniversalConfig { get; set; } = new();
/// <summary>
/// The list of DateTime ranges to test
/// </summary>
[Required]
public List<DateTimeRange> DateTimeRanges { get; set; } = new();
/// <summary>
/// The list of money management variants to test
/// </summary>
[Required]
public List<MoneyManagementVariant> MoneyManagementVariants { get; set; } = new();
/// <summary>
/// The list of ticker variants to test
/// </summary>
[Required]
public List<Ticker> TickerVariants { get; set; } = new();
/// <summary>
/// The results of the bundle backtest execution
/// </summary>
public List<string> Results { get; set; } = new();
/// <summary>
/// Total number of backtests in the bundle
/// </summary>
[Required]
public int TotalBacktests { get; set; }
/// <summary>
/// Number of backtests completed so far
/// </summary>
[Required]
public int CompletedBacktests { get; set; }
/// <summary>
/// Number of backtests that failed
/// </summary>
[Required]
public int FailedBacktests { get; set; }
/// <summary>
/// Progress percentage (0-100)
/// </summary>
public double ProgressPercentage => TotalBacktests > 0 ? (double)CompletedBacktests / TotalBacktests * 100 : 0;
/// <summary>
/// Error message if the request failed
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// Progress information (JSON serialized)
/// </summary>
public string? ProgressInfo { get; set; }
/// <summary>
/// Current backtest being processed
/// </summary>
public string? CurrentBacktest { get; set; }
/// <summary>
/// Estimated time remaining in seconds
/// </summary>
public int? EstimatedTimeRemainingSeconds { get; set; }
/// <summary>
/// Maps a domain model to a view model by deserializing JSON fields
/// </summary>
/// <param name="request">The domain bundle backtest request</param>
/// <returns>A view model with deserialized lists</returns>
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<BundleBacktestUniversalConfig>(request.UniversalConfigJson)
?? new BundleBacktestUniversalConfig();
}
// Deserialize DateTimeRanges
if (!string.IsNullOrEmpty(request.DateTimeRangesJson))
{
viewModel.DateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(request.DateTimeRangesJson)
?? new List<DateTimeRange>();
}
// Deserialize MoneyManagementVariants
if (!string.IsNullOrEmpty(request.MoneyManagementVariantsJson))
{
viewModel.MoneyManagementVariants = JsonSerializer.Deserialize<List<MoneyManagementVariant>>(request.MoneyManagementVariantsJson)
?? new List<MoneyManagementVariant>();
}
// Deserialize TickerVariants
if (!string.IsNullOrEmpty(request.TickerVariantsJson))
{
viewModel.TickerVariants = JsonSerializer.Deserialize<List<Ticker>>(request.TickerVariantsJson)
?? new List<Ticker>();
}
return viewModel;
}
}

View File

@@ -12,23 +12,23 @@ public class WebhookService : IWebhookService
private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration;
private readonly ILogger<WebhookService> _logger;
private readonly string _n8nWebhookUrl;
public WebhookService(HttpClient httpClient, IConfiguration configuration, ILogger<WebhookService> 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)
{

View File

@@ -872,7 +872,7 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<BundleBacktestRequest>(null as any);
}
backtest_GetBundleBacktestRequests(): Promise<BundleBacktestRequest[]> {
backtest_GetBundleBacktestRequests(): Promise<BundleBacktestRequestViewModel[]> {
let url_ = this.baseUrl + "/Backtest/Bundle";
url_ = url_.replace(/[?&]$/, "");
@@ -890,13 +890,13 @@ export class BacktestClient extends AuthorizedApiBase {
});
}
protected processBacktest_GetBundleBacktestRequests(response: Response): Promise<BundleBacktestRequest[]> {
protected processBacktest_GetBundleBacktestRequests(response: Response): Promise<BundleBacktestRequestViewModel[]> {
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<BundleBacktestRequest[]>(null as any);
return Promise.resolve<BundleBacktestRequestViewModel[]>(null as any);
}
backtest_GetBundleBacktestRequest(id: string): Promise<BundleBacktestRequest> {
backtest_GetBundleBacktestRequest(id: string): Promise<BundleBacktestRequestViewModel> {
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<BundleBacktestRequest> {
protected processBacktest_GetBundleBacktestRequest(response: Response): Promise<BundleBacktestRequestViewModel> {
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<BundleBacktestRequest>(null as any);
return Promise.resolve<BundleBacktestRequestViewModel>(null as any);
}
backtest_DeleteBundleBacktestRequest(id: string): Promise<FileResponse> {
@@ -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;

View File

@@ -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;