Add bundle backtest refact + fix whitelist
This commit is contained in:
@@ -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.
|
||||
/// </summary>
|
||||
/// <param name="requests">The list of backtest requests to execute.</param>
|
||||
/// <param name="name">Display name for the bundle (required).</param>
|
||||
/// <param name="request">The bundle backtest request with variant lists.</param>
|
||||
/// <returns>The bundle backtest request with ID for tracking progress.</returns>
|
||||
[HttpPost]
|
||||
[Route("BacktestBundle")]
|
||||
public async Task<ActionResult<BundleBacktestRequest>> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates individual backtest requests from variant configuration
|
||||
/// </summary>
|
||||
/// <param name="request">The bundle backtest request</param>
|
||||
/// <returns>List of individual backtest requests</returns>
|
||||
private List<RunBacktestRequest> GenerateBacktestRequests(RunBundleBacktestRequest request)
|
||||
{
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all bundle backtest requests for the authenticated user.
|
||||
/// </summary>
|
||||
@@ -741,35 +822,3 @@ public class BacktestController : BaseController
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for running a backtest
|
||||
/// </summary>
|
||||
public class RunBacktestRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The trading bot configuration request to use for the backtest
|
||||
/// </summary>
|
||||
public TradingBotConfigRequest Config { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The start date for the backtest
|
||||
/// </summary>
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The end date for the backtest
|
||||
/// </summary>
|
||||
public DateTime EndDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to save the backtest results
|
||||
/// </summary>
|
||||
public bool Save { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include candles and indicators values in the response.
|
||||
/// Set to false to reduce response size dramatically.
|
||||
/// </summary>
|
||||
public bool WithCandles { get; set; } = false;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Request model for running bundle backtests with variant lists instead of individual requests
|
||||
/// </summary>
|
||||
public class RunBundleBacktestRequest
|
||||
{
|
||||
[Required] public string Name { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Display name for the bundle backtest
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required] public List<RunBacktestRequest> Requests { get; set; } = new();
|
||||
/// <summary>
|
||||
/// Universal configuration that applies to all backtests in the bundle
|
||||
/// </summary>
|
||||
[Required]
|
||||
public BundleBacktestUniversalConfig UniversalConfig { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of DateTime ranges to test (each range will generate a separate backtest)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<DateTimeRange> DateTimeRanges { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of money management configurations to test (each will generate a separate backtest)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<MoneyManagementVariant> MoneyManagementVariants { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of tickers to test (each will generate a separate backtest)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<Ticker> TickerVariants { get; set; } = new();
|
||||
}
|
||||
@@ -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<List<RunBacktestRequest>>(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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates individual backtest requests from variant configuration
|
||||
/// </summary>
|
||||
private List<RunBacktestRequest> GenerateBacktestRequestsFromVariants(BundleBacktestRequest bundleRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Deserialize the variant configurations
|
||||
var universalConfig = JsonSerializer.Deserialize<BundleBacktestUniversalConfig>(bundleRequest.UniversalConfigJson);
|
||||
var dateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(bundleRequest.DateTimeRangesJson);
|
||||
var moneyManagementVariants = JsonSerializer.Deserialize<List<MoneyManagementVariant>>(bundleRequest.MoneyManagementVariantsJson);
|
||||
var tickerVariants = JsonSerializer.Deserialize<List<Ticker>>(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<RunBacktestRequest>();
|
||||
}
|
||||
|
||||
var backtestRequests = new List<RunBacktestRequest>();
|
||||
|
||||
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<RunBacktestRequest>();
|
||||
}
|
||||
}
|
||||
|
||||
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<List<RunBacktestRequest>>(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}";
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -93,13 +93,11 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
|
||||
bundleRequest.Status = BundleBacktestRequestStatus.Running;
|
||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||
|
||||
// Deserialize the backtest requests as strongly-typed objects
|
||||
var backtestRequests =
|
||||
JsonSerializer.Deserialize<List<RunBacktestRequest>>(
|
||||
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<BundleBacktestWorker>
|
||||
// Use Results property to determine which backtests need to be retried
|
||||
var succeededIds = new HashSet<string>(failedBundle.Results ?? new List<string>());
|
||||
|
||||
// Deserialize the original requests
|
||||
var originalRequests =
|
||||
JsonSerializer
|
||||
.Deserialize<List<RunBacktestRequest>>(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<BundleBacktestWorker>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates individual backtest requests from variant configuration
|
||||
/// </summary>
|
||||
private List<RunBacktestRequest> GenerateBacktestRequestsFromVariants(BundleBacktestRequest bundleRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Deserialize the variant configurations
|
||||
var universalConfig = JsonSerializer.Deserialize<BundleBacktestUniversalConfig>(bundleRequest.UniversalConfigJson);
|
||||
var dateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(bundleRequest.DateTimeRangesJson);
|
||||
var moneyManagementVariants = JsonSerializer.Deserialize<List<MoneyManagementVariant>>(bundleRequest.MoneyManagementVariantsJson);
|
||||
var tickerVariants = JsonSerializer.Deserialize<List<Ticker>>(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<RunBacktestRequest>();
|
||||
}
|
||||
|
||||
var backtestRequests = new List<RunBacktestRequest>();
|
||||
|
||||
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<RunBacktestRequest>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Managing.Domain.Users;
|
||||
|
||||
namespace Managing.Domain.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Domain model for bundle backtest requests
|
||||
/// Domain model for bundle backtest requests with variant-based configuration
|
||||
/// </summary>
|
||||
public class BundleBacktestRequest
|
||||
{
|
||||
@@ -14,7 +15,10 @@ public class BundleBacktestRequest
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
Status = BundleBacktestRequestStatus.Pending;
|
||||
Results = new List<string>();
|
||||
BacktestRequestsJson = string.Empty;
|
||||
UniversalConfigJson = string.Empty;
|
||||
DateTimeRangesJson = string.Empty;
|
||||
MoneyManagementVariantsJson = string.Empty;
|
||||
TickerVariantsJson = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -27,7 +31,10 @@ public class BundleBacktestRequest
|
||||
CreatedAt = DateTime.UtcNow;
|
||||
Status = BundleBacktestRequestStatus.Pending;
|
||||
Results = new List<string>();
|
||||
BacktestRequestsJson = string.Empty;
|
||||
UniversalConfigJson = string.Empty;
|
||||
DateTimeRangesJson = string.Empty;
|
||||
MoneyManagementVariantsJson = string.Empty;
|
||||
TickerVariantsJson = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -66,10 +73,28 @@ public class BundleBacktestRequest
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of backtest requests to execute (serialized as JSON)
|
||||
/// The universal configuration that applies to all backtests (serialized as JSON)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string BacktestRequestsJson { get; set; } = string.Empty;
|
||||
public string UniversalConfigJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The list of DateTime ranges to test (serialized as JSON)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string DateTimeRangesJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The list of money management variants to test (serialized as JSON)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string MoneyManagementVariantsJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The list of ticker variants to test (serialized as JSON)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string TickerVariantsJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The results of the bundle backtest execution
|
||||
@@ -118,6 +143,7 @@ public class BundleBacktestRequest
|
||||
/// Estimated time remaining in seconds
|
||||
/// </summary>
|
||||
public int? EstimatedTimeRemainingSeconds { get; set; }
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
118
src/Managing.Domain/Backtests/BundleBacktestUniversalConfig.cs
Normal file
118
src/Managing.Domain/Backtests/BundleBacktestUniversalConfig.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Domain.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Universal configuration that applies to all backtests in the bundle
|
||||
/// </summary>
|
||||
public class BundleBacktestUniversalConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The account name to use for all backtests
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string AccountName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The timeframe for trading decisions
|
||||
/// </summary>
|
||||
[Required]
|
||||
public Timeframe Timeframe { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this bot is for watching only (no actual trading)
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool IsForWatchingOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The initial trading balance for the bot
|
||||
/// </summary>
|
||||
[Required]
|
||||
public decimal BotTradingBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name/identifier for this bot
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string BotName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to flip positions
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool FlipPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cooldown period between trades (in candles)
|
||||
/// </summary>
|
||||
public int? CooldownPeriod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum consecutive losses before stopping the bot
|
||||
/// </summary>
|
||||
public int MaxLossStreak { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The scenario configuration (takes precedence over ScenarioName)
|
||||
/// </summary>
|
||||
public ScenarioRequest? Scenario { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The scenario name to load from database (only used when Scenario is not provided)
|
||||
/// </summary>
|
||||
public string? ScenarioName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time in hours that a position can remain open before being automatically closed
|
||||
/// </summary>
|
||||
public decimal? MaxPositionTimeHours { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to close positions early when they become profitable
|
||||
/// </summary>
|
||||
public bool CloseEarlyWhenProfitable { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to only flip positions when the current position is in profit
|
||||
/// </summary>
|
||||
public bool FlipOnlyWhenInProfit { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth API for predictions and risk assessment
|
||||
/// </summary>
|
||||
public bool UseSynthApi { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for position sizing adjustments
|
||||
/// </summary>
|
||||
public bool UseForPositionSizing { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for signal filtering
|
||||
/// </summary>
|
||||
public bool UseForSignalFiltering { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
|
||||
/// </summary>
|
||||
public bool UseForDynamicStopLoss { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to only watch the backtest without executing trades
|
||||
/// </summary>
|
||||
public bool WatchOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to save the backtest results
|
||||
/// </summary>
|
||||
public bool Save { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include candles and indicators values in the response.
|
||||
/// Note: This is always ignored for bundle backtests - candles are never returned.
|
||||
/// </summary>
|
||||
public bool WithCandles { get; set; } = false;
|
||||
}
|
||||
22
src/Managing.Domain/Backtests/DateTimeRange.cs
Normal file
22
src/Managing.Domain/Backtests/DateTimeRange.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Managing.Domain.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a date range for backtesting
|
||||
/// </summary>
|
||||
public class DateTimeRange
|
||||
{
|
||||
/// <summary>
|
||||
/// The start date for the backtest
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The end date for the backtest
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DateTime EndDate { get; set; }
|
||||
}
|
||||
13
src/Managing.Domain/Backtests/MoneyManagementVariant.cs
Normal file
13
src/Managing.Domain/Backtests/MoneyManagementVariant.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
namespace Managing.Domain.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a money management variant for backtesting
|
||||
/// </summary>
|
||||
public class MoneyManagementVariant
|
||||
{
|
||||
/// <summary>
|
||||
/// The money management details
|
||||
/// </summary>
|
||||
public MoneyManagementRequest MoneyManagement { get; set; } = new();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Managing.Infrastructure.Databases.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UpdateBundleBacktestRequestToVariants : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "BacktestRequestsJson",
|
||||
table: "BundleBacktestRequests",
|
||||
newName: "UniversalConfigJson");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DateTimeRangesJson",
|
||||
table: "BundleBacktestRequests",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "MoneyManagementVariantsJson",
|
||||
table: "BundleBacktestRequests",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TickerVariantsJson",
|
||||
table: "BundleBacktestRequests",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,10 +329,6 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("BacktestRequestsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -346,6 +342,10 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("DateTimeRangesJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
@@ -355,6 +355,10 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
b.Property<int>("FailedBacktests")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("MoneyManagementVariantsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
@@ -375,9 +379,17 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TickerVariantsJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("TotalBacktests")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("UniversalConfigJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -190,7 +190,10 @@ public class ManagingDbContext : DbContext
|
||||
entity.Property(e => e.Status)
|
||||
.IsRequired()
|
||||
.HasConversion<string>(); // 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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,8 +12,8 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
||||
{
|
||||
private readonly ICacheService _cacheService;
|
||||
|
||||
public PostgreSqlUserRepository(ManagingDbContext context, ILogger<SqlQueryLogger> logger,
|
||||
SentrySqlMonitoringService sentryMonitoringService, ICacheService cacheService)
|
||||
public PostgreSqlUserRepository(ManagingDbContext context, ILogger<SqlQueryLogger> 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<User>(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<Account>(); // 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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<BundleRequestModalProps> = ({ open, onClose, bundle }) => {
|
||||
const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
bundle,
|
||||
onCreateBundle
|
||||
}) => {
|
||||
const { apiUrl } = useApiUrlStore();
|
||||
const [backtests, setBacktests] = useState<LightBacktestResponse[]>([]);
|
||||
const signalRRef = useRef<any>(null);
|
||||
|
||||
// Custom scenario hook
|
||||
const { scenario, setCustomScenario } = useCustomScenario();
|
||||
|
||||
// Form state for creating new bundle requests
|
||||
const [strategyName, setStrategyName] = useState<string>('');
|
||||
const [selectedAccount, setSelectedAccount] = useState<string>('');
|
||||
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(Timeframe.FifteenMinutes);
|
||||
const [selectedTickers, setSelectedTickers] = useState<Ticker[]>([]);
|
||||
const [startingCapital, setStartingCapital] = useState<number>(10000);
|
||||
|
||||
// Advanced parameters state
|
||||
const [cooldownPeriod, setCooldownPeriod] = useState<number>(0);
|
||||
const [maxLossStreak, setMaxLossStreak] = useState<number>(0);
|
||||
const [maxPositionTime, setMaxPositionTime] = useState<number>(0);
|
||||
const [flipPosition, setFlipPosition] = useState<boolean>(false);
|
||||
const [flipOnlyWhenInProfit, setFlipOnlyWhenInProfit] = useState<boolean>(false);
|
||||
const [closeEarlyWhenProfitable, setCloseEarlyWhenProfitable] = useState<boolean>(false);
|
||||
|
||||
// Variant arrays
|
||||
const [dateTimeRanges, setDateTimeRanges] = useState<DateTimeRange[]>([
|
||||
{ startDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), endDate: new Date() }
|
||||
]);
|
||||
const [moneyManagementVariants, setMoneyManagementVariants] = useState<MoneyManagementVariant[]>([
|
||||
{ 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, number> = {
|
||||
[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, number> = {
|
||||
[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<BundleRequestModalProps> = ({ 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<BundleRequestModalProps> = ({ 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<BundleRequestModalProps> = ({ open, onClose,
|
||||
};
|
||||
}, [open, bundle, apiUrl]);
|
||||
|
||||
if (!open || !bundle) return null;
|
||||
if (!open) return null;
|
||||
|
||||
// If viewing an existing bundle
|
||||
if (bundle) {
|
||||
return (
|
||||
<div className="modal modal-open">
|
||||
<div className="modal-box max-w-4xl">
|
||||
<h3 className="font-bold text-lg mb-2">Bundle: {bundle.name}</h3>
|
||||
<div className="mb-2 text-sm">
|
||||
<div><b>Request ID:</b> <span className="font-mono text-xs">{bundle.requestId}</span></div>
|
||||
<div><b>Status:</b> <span className={`badge badge-sm ml-1`}>{bundle.status}</span></div>
|
||||
<div><b>Created:</b> {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}</div>
|
||||
<div><b>Completed:</b> {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}</div>
|
||||
</div>
|
||||
<div className="divider">Backtest Results</div>
|
||||
{isLoading ? (
|
||||
<div>Loading backtests...</div>
|
||||
) : queryError ? (
|
||||
<div className="text-error">{(queryError as any)?.message || 'Failed to fetch backtests'}</div>
|
||||
) : (
|
||||
<BacktestTable list={backtests} />
|
||||
)}
|
||||
<div className="modal-action">
|
||||
<button className="btn" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Create new bundle form
|
||||
return (
|
||||
<div className="modal modal-open">
|
||||
<div className="modal-box max-w-4xl">
|
||||
<h3 className="font-bold text-lg mb-2">Bundle: {bundle.name}</h3>
|
||||
<div className="mb-2 text-sm">
|
||||
<div><b>Request ID:</b> <span className="font-mono text-xs">{bundle.requestId}</span></div>
|
||||
<div><b>Status:</b> <span className={`badge badge-sm ml-1`}>{bundle.status}</span></div>
|
||||
<div><b>Created:</b> {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}</div>
|
||||
<div><b>Completed:</b> {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}</div>
|
||||
<div className="modal-box max-w-6xl">
|
||||
<h3 className="font-bold text-lg mb-6">Create Bundle Backtest</h3>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Left Column - Strategy Configuration */}
|
||||
<div className="space-y-6">
|
||||
{/* Strategy Name */}
|
||||
<FormInput label="Name your strategy" htmlFor="strategyName">
|
||||
<input
|
||||
type="text"
|
||||
className="input input-bordered w-full"
|
||||
placeholder="Road to $100k..."
|
||||
value={strategyName}
|
||||
onChange={(e) => setStrategyName(e.target.value)}
|
||||
/>
|
||||
</FormInput>
|
||||
|
||||
{/* Account Selection */}
|
||||
<FormInput label="Select Account" htmlFor="account">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={selectedAccount}
|
||||
onChange={(e) => setSelectedAccount(e.target.value)}
|
||||
>
|
||||
<option value="" disabled>Select an account</option>
|
||||
{accounts?.map((account) => (
|
||||
<option key={account.name} value={account.name}>
|
||||
{account.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
{/* Scenario Builder */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Build your trading scenario</h4>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Create a custom scenario with indicators and parameters for your automated trading strategy.
|
||||
</p>
|
||||
|
||||
<CustomScenario
|
||||
onCreateScenario={setCustomScenario}
|
||||
showCustomScenario={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Asset Selection */}
|
||||
<FormInput label="Select asset(s)" htmlFor="tickers">
|
||||
<p className="text-sm text-gray-600 mb-2">Select what your agent trades.</p>
|
||||
<div className="grid grid-cols-3 gap-2 max-h-40 overflow-y-auto">
|
||||
{tickers?.map((tickerInfo) => (
|
||||
<button
|
||||
key={tickerInfo.ticker}
|
||||
className={`btn btn-sm ${
|
||||
selectedTickers.includes(tickerInfo.ticker!)
|
||||
? 'btn-primary'
|
||||
: 'btn-outline'
|
||||
}`}
|
||||
onClick={() => handleTickerToggle(tickerInfo.ticker!)}
|
||||
>
|
||||
{tickerInfo.ticker}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</FormInput>
|
||||
|
||||
{/* Timeframe Selection */}
|
||||
<FormInput label="Select timeframe" htmlFor="timeframe">
|
||||
<p className="text-sm text-gray-600 mb-2">This sets how often your strategy reads and reacts to market data</p>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={selectedTimeframe}
|
||||
onChange={(e) => setSelectedTimeframe(e.target.value as Timeframe)}
|
||||
>
|
||||
{Object.values(Timeframe).map((tf) => (
|
||||
<option key={tf} value={tf}>{tf}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormInput>
|
||||
|
||||
{/* Money Management Variants */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Choose your money management approach(s)</h4>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Select the approach that fits your goals and keep your strategy's risk in check.
|
||||
</p>
|
||||
|
||||
{moneyManagementVariants.map((variant, index) => (
|
||||
<div key={index} className="card bg-base-200 p-4 mb-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h5 className="font-medium">Money Management {index + 1}</h5>
|
||||
{moneyManagementVariants.length > 1 && (
|
||||
<button
|
||||
className="btn btn-error btn-sm"
|
||||
onClick={() => removeMoneyManagementVariant(index)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<FormInput label="Leverage" htmlFor={`leverage-${index}`}>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-full"
|
||||
value={variant.moneyManagement?.leverage || 1}
|
||||
onChange={(e) => updateMoneyManagementVariant(index, 'leverage', parseFloat(e.target.value))}
|
||||
min="1"
|
||||
step="0.1"
|
||||
/>
|
||||
</FormInput>
|
||||
<FormInput label="TP %" htmlFor={`takeProfit-${index}`}>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-full"
|
||||
value={variant.moneyManagement?.takeProfit || 0}
|
||||
onChange={(e) => updateMoneyManagementVariant(index, 'takeProfit', parseFloat(e.target.value))}
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</FormInput>
|
||||
<FormInput label="SL %" htmlFor={`stopLoss-${index}`}>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-full"
|
||||
value={variant.moneyManagement?.stopLoss || 0}
|
||||
onChange={(e) => updateMoneyManagementVariant(index, 'stopLoss', parseFloat(e.target.value))}
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
</FormInput>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="btn btn-outline btn-sm"
|
||||
onClick={addMoneyManagementVariant}
|
||||
>
|
||||
+ Add variant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Test Period & Backtest */}
|
||||
<div className="space-y-6">
|
||||
{/* Test Period */}
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">Select the test period</h4>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Pick a historical range to evaluate your strategy.
|
||||
</p>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<label className="label">Date range</label>
|
||||
<button
|
||||
className="btn btn-outline btn-sm"
|
||||
onClick={addDateTimeRange}
|
||||
>
|
||||
+ Add variant
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{dateTimeRanges.map((range, index) => (
|
||||
<div key={index} className="card bg-base-200 p-4 mb-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h5 className="font-medium">Date Range {index + 1}</h5>
|
||||
{dateTimeRanges.length > 1 && (
|
||||
<button
|
||||
className="btn btn-error btn-sm"
|
||||
onClick={() => removeDateTimeRange(index)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormInput label="Start Date" htmlFor={`startDate-${index}`}>
|
||||
<input
|
||||
type="date"
|
||||
className="input input-bordered w-full"
|
||||
value={range.startDate.toISOString().split('T')[0]}
|
||||
onChange={(e) => updateDateTimeRange(index, 'startDate', new Date(e.target.value))}
|
||||
/>
|
||||
</FormInput>
|
||||
<FormInput label="End Date" htmlFor={`endDate-${index}`}>
|
||||
<input
|
||||
type="date"
|
||||
className="input input-bordered w-full"
|
||||
value={range.endDate.toISOString().split('T')[0]}
|
||||
onChange={(e) => updateDateTimeRange(index, 'endDate', new Date(e.target.value))}
|
||||
/>
|
||||
</FormInput>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Advanced Parameters */}
|
||||
<div className="collapse collapse-arrow bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div className="collapse-title font-medium flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-semibold">Advanced Parameters</h4>
|
||||
<p className="text-sm text-gray-600 font-normal">
|
||||
Refine your strategy with additional limits.
|
||||
</p>
|
||||
</div>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="collapse-content">
|
||||
<div className="space-y-6">
|
||||
{/* Cooldown Period */}
|
||||
<div className="flex items-center justify-between py-3 border-b border-base-300">
|
||||
<label className="label-text font-medium text-base">Cooldown Period</label>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline rounded-r-none"
|
||||
onClick={() => setCooldownPeriod(Math.max(0, cooldownPeriod - 1))}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered rounded-none text-center w-20"
|
||||
value={cooldownPeriod}
|
||||
onChange={(e) => setCooldownPeriod(Math.max(0, parseInt(e.target.value) || 0))}
|
||||
min="0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline rounded-l-none"
|
||||
onClick={() => setCooldownPeriod(cooldownPeriod + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Max Loss Streak */}
|
||||
<div className="flex items-center justify-between py-3 border-b border-base-300">
|
||||
<label className="label-text font-medium text-base">Max Loss Streak</label>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline rounded-r-none"
|
||||
onClick={() => setMaxLossStreak(Math.max(0, maxLossStreak - 1))}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered rounded-none text-center w-20"
|
||||
value={maxLossStreak}
|
||||
onChange={(e) => setMaxLossStreak(Math.max(0, parseInt(e.target.value) || 0))}
|
||||
min="0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline rounded-l-none"
|
||||
onClick={() => setMaxLossStreak(maxLossStreak + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Max Position Time */}
|
||||
<div className="flex items-center justify-between py-3 border-b border-base-300">
|
||||
<label className="label-text font-medium text-base">Max Position Time</label>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline rounded-r-none"
|
||||
onClick={() => setMaxPositionTime(Math.max(0, maxPositionTime - 1))}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered rounded-none text-center w-20"
|
||||
value={maxPositionTime}
|
||||
onChange={(e) => setMaxPositionTime(Math.max(0, parseInt(e.target.value) || 0))}
|
||||
min="0"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-outline rounded-l-none"
|
||||
onClick={() => setMaxPositionTime(maxPositionTime + 1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggle Switches */}
|
||||
<div className="space-y-4">
|
||||
{/* Position Flipping */}
|
||||
<div className="form-control">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<label className="label-text font-medium">Position flipping</label>
|
||||
<p className="text-sm text-gray-600">
|
||||
When this switch is on, the bot can flip between long and short positions as signals change - an aggressive style.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary"
|
||||
checked={flipPosition}
|
||||
onChange={(e) => setFlipPosition(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flip Only When In Profit */}
|
||||
<div className="form-control">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<label className="label-text font-medium">Flip only when in profit</label>
|
||||
<p className="text-sm text-gray-600">
|
||||
When this switch is on, the bot flips sides only if the current position is profitable, limiting flips during drawdowns.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary"
|
||||
checked={flipOnlyWhenInProfit}
|
||||
onChange={(e) => setFlipOnlyWhenInProfit(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Close Early When Profitable */}
|
||||
<div className="form-control">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<label className="label-text font-medium">Close early when profitable</label>
|
||||
<p className="text-sm text-gray-600">
|
||||
When this switch is on, the bot exits positions as soon as they turn profitable, locking in gains sooner.
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary"
|
||||
checked={closeEarlyWhenProfitable}
|
||||
onChange={(e) => setCloseEarlyWhenProfitable(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Starting Capital */}
|
||||
<FormInput label="Starting Capital" htmlFor="startingCapital">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-full"
|
||||
value={startingCapital}
|
||||
onChange={(e) => setStartingCapital(parseFloat(e.target.value))}
|
||||
min="1"
|
||||
step="0.01"
|
||||
/>
|
||||
</FormInput>
|
||||
|
||||
{/* Backtest Cart */}
|
||||
<div className="card bg-base-200 p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<h4 className="font-semibold">Backtest Cart</h4>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
A summary of your strategy creation.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex justify-between">
|
||||
<span>Total number of backtests</span>
|
||||
<span className="font-medium">{totalBacktests}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Total number of credits used</span>
|
||||
<span className="font-medium flex items-center">
|
||||
{totalBacktests}
|
||||
<svg className="w-4 h-4 ml-1 text-orange-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<circle cx="10" cy="10" r="8" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Estimated time</span>
|
||||
<span className="font-medium">
|
||||
{estimatedTimeSeconds < 60
|
||||
? `${estimatedTimeSeconds}s`
|
||||
: estimatedTimeSeconds < 3600
|
||||
? `${Math.ceil(estimatedTimeSeconds / 60)}m`
|
||||
: `${Math.ceil(estimatedTimeSeconds / 3600)}h ${Math.ceil((estimatedTimeSeconds % 3600) / 60)}m`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-primary w-full mb-4"
|
||||
onClick={handleCreateBundle}
|
||||
disabled={!strategyName || !selectedAccount || selectedTickers.length === 0 || !scenario}
|
||||
>
|
||||
Run Backtest
|
||||
</button>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button className="btn btn-outline btn-sm">
|
||||
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm12 12V6H4v10h12zM8 8a1 1 0 000 2h4a1 1 0 100-2H8z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Save this backtest
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-sm">
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider">Backtest Results</div>
|
||||
{isLoading ? (
|
||||
<div>Loading backtests...</div>
|
||||
) : queryError ? (
|
||||
<div className="text-error">{(queryError as any)?.message || 'Failed to fetch backtests'}</div>
|
||||
) : (
|
||||
<BacktestTable list={backtests} />
|
||||
)}
|
||||
|
||||
<div className="modal-action">
|
||||
<button className="btn" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
|
||||
@@ -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<string, any> };
|
||||
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<string, Ticker> = {
|
||||
BTC: Ticker.BTC,
|
||||
ETH: Ticker.ETH,
|
||||
GMX: Ticker.GMX,
|
||||
};
|
||||
const timeframeMap: Record<string, Timeframe> = {
|
||||
'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<Asset[]>([]);
|
||||
const [timeframe, setTimeframe] = useState('5m');
|
||||
const [moneyManagementVariants, setMoneyManagementVariants] = useState<MoneyManagementVariant[]>([
|
||||
{ leverage: 2, tp: 1.5, sl: 1 },
|
||||
]);
|
||||
const [timeRangeVariants, setTimeRangeVariants] = useState<TimeRangeVariant[]>([
|
||||
{ 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<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(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 (
|
||||
<div className="p-10 max-w-7xl mx-auto">
|
||||
<h2 className="text-2xl font-bold mb-6">Bundle Backtest</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Left column: Main form */}
|
||||
<div>
|
||||
{/* Name your strategy */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Name your strategy</label>
|
||||
<input
|
||||
className="input input-bordered w-full"
|
||||
value={strategyName}
|
||||
onChange={e => setStrategyName(e.target.value)}
|
||||
placeholder="Road to $100k..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Select account */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Select account</label>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={accountName}
|
||||
onChange={e => setAccountName(e.target.value)}
|
||||
disabled={!accounts || accounts.length === 0}
|
||||
>
|
||||
{!accounts || accounts.length === 0 ? (
|
||||
<option value="">Loading accounts...</option>
|
||||
) : (
|
||||
accounts.map(account => (
|
||||
<option key={account.name} value={account.name}>
|
||||
{account.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Scenario/Indicators section */}
|
||||
<div className="mb-4">
|
||||
<CustomScenario
|
||||
onCreateScenario={setCustomScenario}
|
||||
showCustomScenario={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Select asset(s) */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Select asset(s)</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allAssets.map(asset => (
|
||||
<button
|
||||
key={asset}
|
||||
className={`btn btn-sm ${selectedAssets.includes(asset) ? 'btn-primary' : 'btn-outline'}`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setSelectedAssets(sel =>
|
||||
sel.includes(asset)
|
||||
? sel.filter(a => a !== asset)
|
||||
: [...sel, asset]
|
||||
);
|
||||
}}
|
||||
>
|
||||
{asset}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Select timeframe */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Select timeframe</label>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={timeframe}
|
||||
onChange={e => setTimeframe(e.target.value)}
|
||||
>
|
||||
{allTimeframes.map(tf => (
|
||||
<option key={tf.value} value={tf.value}>
|
||||
{tf.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Money management variants */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Choose your money management approach(s)</label>
|
||||
{moneyManagementVariants.map((mm, idx) => (
|
||||
<div key={idx} className="flex gap-2 items-center mb-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-16"
|
||||
value={mm.leverage}
|
||||
onChange={e => {
|
||||
const v = [...moneyManagementVariants];
|
||||
v[idx].leverage = Number(e.target.value);
|
||||
setMoneyManagementVariants(v);
|
||||
}}
|
||||
placeholder="Leverage"
|
||||
min={1}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-16"
|
||||
value={mm.tp}
|
||||
onChange={e => {
|
||||
const v = [...moneyManagementVariants];
|
||||
v[idx].tp = Number(e.target.value);
|
||||
setMoneyManagementVariants(v);
|
||||
}}
|
||||
placeholder="TP %"
|
||||
min={0}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-16"
|
||||
value={mm.sl}
|
||||
onChange={e => {
|
||||
const v = [...moneyManagementVariants];
|
||||
v[idx].sl = Number(e.target.value);
|
||||
setMoneyManagementVariants(v);
|
||||
}}
|
||||
placeholder="SL %"
|
||||
min={0}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-xs btn-error"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setMoneyManagementVariants(v => v.filter((_, i) => i !== idx));
|
||||
}}
|
||||
disabled={moneyManagementVariants.length === 1}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-sm btn-outline"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setMoneyManagementVariants(v => [...v, { leverage: 1, tp: 1, sl: 1 }]);
|
||||
}}
|
||||
>
|
||||
+ Add variant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column: Test period, advanced params, capital, cart */}
|
||||
<div>
|
||||
{/* Test period variants */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Select the test period</label>
|
||||
{timeRangeVariants.map((tr, idx) => (
|
||||
<div key={idx} className="flex gap-2 items-center mb-2">
|
||||
<input
|
||||
type="date"
|
||||
className="input input-bordered"
|
||||
value={tr.start}
|
||||
onChange={e => {
|
||||
const v = [...timeRangeVariants];
|
||||
v[idx].start = e.target.value;
|
||||
setTimeRangeVariants(v);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
className="input input-bordered"
|
||||
value={tr.end}
|
||||
onChange={e => {
|
||||
const v = [...timeRangeVariants];
|
||||
v[idx].end = e.target.value;
|
||||
setTimeRangeVariants(v);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-xs btn-error"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimeRangeVariants(v => v.filter((_, i) => i !== idx));
|
||||
}}
|
||||
disabled={timeRangeVariants.length === 1}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="btn btn-sm btn-outline"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setTimeRangeVariants(v => [...v, { start: '', end: '' }]);
|
||||
}}
|
||||
>
|
||||
+ Add variant
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Advanced Parameters */}
|
||||
<div className="mb-4">
|
||||
<div className="collapse collapse-arrow bg-base-200">
|
||||
<input type="checkbox" />
|
||||
<div className="collapse-title font-medium">Advanced Parameters</div>
|
||||
<div className="collapse-content">
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-20"
|
||||
value={cooldown}
|
||||
onChange={e => setCooldown(Number(e.target.value))}
|
||||
placeholder="Cooldown"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-20"
|
||||
value={maxLossStreak}
|
||||
onChange={e => setMaxLossStreak(Number(e.target.value))}
|
||||
placeholder="Max Loss Streak"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-20"
|
||||
value={maxPositionTime}
|
||||
onChange={e => setMaxPositionTime(Number(e.target.value))}
|
||||
placeholder="Max Position Time"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control mb-2">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="label-text">Position flipping</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle"
|
||||
checked={positionFlipping}
|
||||
onChange={e => setPositionFlipping(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control mb-2">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="label-text">Flip only when in profit</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle"
|
||||
checked={flipOnlyInProfit}
|
||||
onChange={e => setFlipOnlyInProfit(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-control mb-2">
|
||||
<label className="cursor-pointer label">
|
||||
<span className="label-text">Close early when profitable</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle"
|
||||
checked={closeEarly}
|
||||
onChange={e => setCloseEarly(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Starting Capital */}
|
||||
<div className="mb-4">
|
||||
<label className="label">Starting Capital</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input input-bordered w-full"
|
||||
value={startingCapital}
|
||||
onChange={e => setStartingCapital(Number(e.target.value))}
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Backtest Cart */}
|
||||
<div className="mb-4 bg-base-200 rounded-lg p-4">
|
||||
<div className="font-bold mb-2">Backtest Cart</div>
|
||||
<div>Total number of backtests: <span className="font-mono">{totalBacktests}</span></div>
|
||||
<div>Total number of credits used: <span className="font-mono">{totalBacktests}</span></div>
|
||||
<div>Estimated time: <span className="font-mono">~ 1 min</span></div>
|
||||
<button className="btn btn-primary w-full mt-4" onClick={handleRunBundle}>
|
||||
Run Backtest
|
||||
</button>
|
||||
<button className="btn btn-outline w-full mt-2">Save this backtest</button>
|
||||
<button className="btn btn-ghost w-full mt-2">Clear all</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10">
|
||||
<BundleRequestsTable />
|
||||
</div>
|
||||
<BundleRequestsTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -148,12 +148,28 @@ const BundleRequestsTable = () => {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<h2 className="text-lg font-bold mb-2">Bundle Backtest Requests</h2>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-bold">Bundle Backtest Requests</h2>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setSelectedBundle(null);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Create New Bundle
|
||||
</button>
|
||||
</div>
|
||||
<Table columns={columns} data={data} showPagination={true} />
|
||||
<BundleRequestModal
|
||||
open={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
bundle={selectedBundle}
|
||||
onCreateBundle={async (request) => {
|
||||
const client = new BacktestClient({} as any, apiUrl);
|
||||
await client.backtest_RunBundle(request);
|
||||
fetchData(); // Refresh the table
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user