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.
|
/// Creates a bundle backtest request with the specified configurations.
|
||||||
/// This endpoint creates a request that will be processed by a background worker.
|
/// This endpoint creates a request that will be processed by a background worker.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requests">The list of backtest requests to execute.</param>
|
/// <param name="request">The bundle backtest request with variant lists.</param>
|
||||||
/// <param name="name">Display name for the bundle (required).</param>
|
|
||||||
/// <returns>The bundle backtest request with ID for tracking progress.</returns>
|
/// <returns>The bundle backtest request with ID for tracking progress.</returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("BacktestBundle")]
|
[Route("BacktestBundle")]
|
||||||
public async Task<ActionResult<BundleBacktestRequest>> RunBundle([FromBody] RunBundleBacktestRequest request)
|
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))
|
if (string.IsNullOrWhiteSpace(request.Name))
|
||||||
@@ -441,32 +450,35 @@ public class BacktestController : BaseController
|
|||||||
return BadRequest("Bundle name is required");
|
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
|
try
|
||||||
{
|
{
|
||||||
var user = await GetUser();
|
var user = await GetUser();
|
||||||
|
|
||||||
// Validate all requests before creating the bundle
|
// Validate universal configuration
|
||||||
foreach (var req in request.Requests)
|
if (string.IsNullOrEmpty(request.UniversalConfig.AccountName))
|
||||||
{
|
{
|
||||||
if (req?.Config == null)
|
return BadRequest("Account name is required in universal configuration");
|
||||||
{
|
|
||||||
return BadRequest("Invalid request: Configuration is required");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(req.Config.AccountName))
|
if (string.IsNullOrEmpty(request.UniversalConfig.ScenarioName) && request.UniversalConfig.Scenario == null)
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid request: Account name is required");
|
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)
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid request: Either scenario name or scenario object is required");
|
if (mmVariant.MoneyManagement == null)
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(req.Config.MoneyManagementName) && req.Config.MoneyManagement == null)
|
|
||||||
{
|
{
|
||||||
return BadRequest(
|
return BadRequest("Each money management variant must have a money management object");
|
||||||
"Invalid request: Either money management name or money management object is required");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,8 +486,11 @@ public class BacktestController : BaseController
|
|||||||
var bundleRequest = new BundleBacktestRequest
|
var bundleRequest = new BundleBacktestRequest
|
||||||
{
|
{
|
||||||
User = user,
|
User = user,
|
||||||
BacktestRequestsJson = JsonSerializer.Serialize(request.Requests),
|
UniversalConfigJson = JsonSerializer.Serialize(request.UniversalConfig),
|
||||||
TotalBacktests = request.Requests.Count,
|
DateTimeRangesJson = JsonSerializer.Serialize(request.DateTimeRanges),
|
||||||
|
MoneyManagementVariantsJson = JsonSerializer.Serialize(request.MoneyManagementVariants),
|
||||||
|
TickerVariantsJson = JsonSerializer.Serialize(request.TickerVariants),
|
||||||
|
TotalBacktests = totalBacktests,
|
||||||
CompletedBacktests = 0,
|
CompletedBacktests = 0,
|
||||||
FailedBacktests = 0,
|
FailedBacktests = 0,
|
||||||
Status = BundleBacktestRequestStatus.Pending,
|
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>
|
/// <summary>
|
||||||
/// Retrieves all bundle backtest requests for the authenticated user.
|
/// Retrieves all bundle backtest requests for the authenticated user.
|
||||||
/// </summary>
|
/// </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 System.ComponentModel.DataAnnotations;
|
||||||
using Managing.Api.Controllers;
|
using Managing.Domain.Backtests;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Api.Models.Requests;
|
namespace Managing.Api.Models.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model for running bundle backtests with variant lists instead of individual requests
|
||||||
|
/// </summary>
|
||||||
public class RunBundleBacktestRequest
|
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.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Orleans.Concurrency;
|
using Orleans.Concurrency;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Application.Grains;
|
namespace Managing.Application.Grains;
|
||||||
|
|
||||||
@@ -103,12 +104,11 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
|
|||||||
bundleRequest.Status = BundleBacktestRequestStatus.Running;
|
bundleRequest.Status = BundleBacktestRequestStatus.Running;
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||||
|
|
||||||
// Deserialize the backtest requests as strongly-typed objects
|
// Generate backtest requests from variant configuration
|
||||||
var backtestRequests =
|
var backtestRequests = GenerateBacktestRequestsFromVariants(bundleRequest);
|
||||||
JsonSerializer.Deserialize<List<RunBacktestRequest>>(bundleRequest.BacktestRequestsJson);
|
if (backtestRequests == null || !backtestRequests.Any())
|
||||||
if (backtestRequests == null)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to deserialize backtest requests");
|
throw new InvalidOperationException("Failed to generate backtest requests from variants");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each backtest request sequentially
|
// 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(
|
private async Task ProcessSingleBacktest(
|
||||||
IBacktester backtester,
|
IBacktester backtester,
|
||||||
RunBacktestRequest runBacktestRequest,
|
RunBacktestRequest runBacktestRequest,
|
||||||
@@ -138,10 +222,8 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Get total count from deserialized requests instead of string splitting
|
// Calculate total count from the variant configuration
|
||||||
var backtestRequests =
|
var totalCount = bundleRequest.TotalBacktests;
|
||||||
JsonSerializer.Deserialize<List<RunBacktestRequest>>(bundleRequest.BacktestRequestsJson);
|
|
||||||
var totalCount = backtestRequests?.Count ?? 0;
|
|
||||||
|
|
||||||
// Update current backtest being processed
|
// Update current backtest being processed
|
||||||
bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}";
|
bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}";
|
||||||
|
|||||||
@@ -64,8 +64,17 @@ public class UserService : IUserService
|
|||||||
throw new Exception($"Address {recoveredAddress} not corresponding");
|
throw new Exception($"Address {recoveredAddress} not corresponding");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
User user = null;
|
||||||
|
|
||||||
// Check if account exist
|
// 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)
|
if (user != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -93,13 +93,11 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
|
|||||||
bundleRequest.Status = BundleBacktestRequestStatus.Running;
|
bundleRequest.Status = BundleBacktestRequestStatus.Running;
|
||||||
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
|
||||||
|
|
||||||
// Deserialize the backtest requests as strongly-typed objects
|
// Generate backtest requests from the new variant structure
|
||||||
var backtestRequests =
|
var backtestRequests = GenerateBacktestRequestsFromVariants(bundleRequest);
|
||||||
JsonSerializer.Deserialize<List<RunBacktestRequest>>(
|
if (backtestRequests == null || !backtestRequests.Any())
|
||||||
bundleRequest.BacktestRequestsJson);
|
|
||||||
if (backtestRequests == null)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Failed to deserialize backtest requests");
|
throw new InvalidOperationException("Failed to generate backtest requests from variants");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each backtest request
|
// Process each backtest request
|
||||||
@@ -298,11 +296,9 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
|
|||||||
// Use Results property to determine which backtests need to be retried
|
// Use Results property to determine which backtests need to be retried
|
||||||
var succeededIds = new HashSet<string>(failedBundle.Results ?? new List<string>());
|
var succeededIds = new HashSet<string>(failedBundle.Results ?? new List<string>());
|
||||||
|
|
||||||
// Deserialize the original requests
|
// Generate backtest requests from the new variant structure
|
||||||
var originalRequests =
|
var originalRequests = GenerateBacktestRequestsFromVariants(failedBundle);
|
||||||
JsonSerializer
|
if (originalRequests == null || !originalRequests.Any()) continue;
|
||||||
.Deserialize<List<RunBacktestRequest>>(failedBundle.BacktestRequestsJson);
|
|
||||||
if (originalRequests == null) continue;
|
|
||||||
|
|
||||||
for (int i = failedBundle.CompletedBacktests; i < originalRequests.Count; i++)
|
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 System.ComponentModel.DataAnnotations;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
|
|
||||||
namespace Managing.Domain.Backtests;
|
namespace Managing.Domain.Backtests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Domain model for bundle backtest requests
|
/// Domain model for bundle backtest requests with variant-based configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BundleBacktestRequest
|
public class BundleBacktestRequest
|
||||||
{
|
{
|
||||||
@@ -14,7 +15,10 @@ public class BundleBacktestRequest
|
|||||||
CreatedAt = DateTime.UtcNow;
|
CreatedAt = DateTime.UtcNow;
|
||||||
Status = BundleBacktestRequestStatus.Pending;
|
Status = BundleBacktestRequestStatus.Pending;
|
||||||
Results = new List<string>();
|
Results = new List<string>();
|
||||||
BacktestRequestsJson = string.Empty;
|
UniversalConfigJson = string.Empty;
|
||||||
|
DateTimeRangesJson = string.Empty;
|
||||||
|
MoneyManagementVariantsJson = string.Empty;
|
||||||
|
TickerVariantsJson = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -27,7 +31,10 @@ public class BundleBacktestRequest
|
|||||||
CreatedAt = DateTime.UtcNow;
|
CreatedAt = DateTime.UtcNow;
|
||||||
Status = BundleBacktestRequestStatus.Pending;
|
Status = BundleBacktestRequestStatus.Pending;
|
||||||
Results = new List<string>();
|
Results = new List<string>();
|
||||||
BacktestRequestsJson = string.Empty;
|
UniversalConfigJson = string.Empty;
|
||||||
|
DateTimeRangesJson = string.Empty;
|
||||||
|
MoneyManagementVariantsJson = string.Empty;
|
||||||
|
TickerVariantsJson = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -66,10 +73,28 @@ public class BundleBacktestRequest
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of backtest requests to execute (serialized as JSON)
|
/// The universal configuration that applies to all backtests (serialized as JSON)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[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>
|
/// <summary>
|
||||||
/// The results of the bundle backtest execution
|
/// The results of the bundle backtest execution
|
||||||
@@ -118,6 +143,7 @@ public class BundleBacktestRequest
|
|||||||
/// Estimated time remaining in seconds
|
/// Estimated time remaining in seconds
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EstimatedTimeRemainingSeconds { get; set; }
|
public int? EstimatedTimeRemainingSeconds { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
b.Property<string>("BacktestRequestsJson")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("CompletedAt")
|
b.Property<DateTime?>("CompletedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@@ -346,6 +342,10 @@ namespace Managing.Infrastructure.Databases.Migrations
|
|||||||
.HasMaxLength(500)
|
.HasMaxLength(500)
|
||||||
.HasColumnType("character varying(500)");
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("DateTimeRangesJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("ErrorMessage")
|
b.Property<string>("ErrorMessage")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
@@ -355,6 +355,10 @@ namespace Managing.Infrastructure.Databases.Migrations
|
|||||||
b.Property<int>("FailedBacktests")
|
b.Property<int>("FailedBacktests")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("MoneyManagementVariantsJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(255)
|
||||||
@@ -375,9 +379,17 @@ namespace Managing.Infrastructure.Databases.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("TickerVariantsJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<int>("TotalBacktests")
|
b.Property<int>("TotalBacktests")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("UniversalConfigJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
b.Property<DateTime>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,19 @@ public class BundleBacktestRequestEntity
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column(TypeName = "text")]
|
[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]
|
[Required]
|
||||||
public int TotalBacktests { get; set; }
|
public int TotalBacktests { get; set; }
|
||||||
|
|||||||
@@ -190,7 +190,10 @@ public class ManagingDbContext : DbContext
|
|||||||
entity.Property(e => e.Status)
|
entity.Property(e => e.Status)
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasConversion<string>(); // Store enum as string
|
.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.ErrorMessage).HasColumnType("text");
|
||||||
entity.Property(e => e.ProgressInfo).HasColumnType("text");
|
entity.Property(e => e.ProgressInfo).HasColumnType("text");
|
||||||
entity.Property(e => e.CurrentBacktest).HasMaxLength(500);
|
entity.Property(e => e.CurrentBacktest).HasMaxLength(500);
|
||||||
|
|||||||
@@ -367,7 +367,10 @@ public static class PostgreSqlMappers
|
|||||||
CreatedAt = entity.CreatedAt,
|
CreatedAt = entity.CreatedAt,
|
||||||
CompletedAt = entity.CompletedAt,
|
CompletedAt = entity.CompletedAt,
|
||||||
Status = entity.Status,
|
Status = entity.Status,
|
||||||
BacktestRequestsJson = entity.BacktestRequestsJson,
|
UniversalConfigJson = entity.UniversalConfigJson,
|
||||||
|
DateTimeRangesJson = entity.DateTimeRangesJson,
|
||||||
|
MoneyManagementVariantsJson = entity.MoneyManagementVariantsJson,
|
||||||
|
TickerVariantsJson = entity.TickerVariantsJson,
|
||||||
TotalBacktests = entity.TotalBacktests,
|
TotalBacktests = entity.TotalBacktests,
|
||||||
CompletedBacktests = entity.CompletedBacktests,
|
CompletedBacktests = entity.CompletedBacktests,
|
||||||
FailedBacktests = entity.FailedBacktests,
|
FailedBacktests = entity.FailedBacktests,
|
||||||
@@ -406,7 +409,10 @@ public static class PostgreSqlMappers
|
|||||||
CreatedAt = bundleRequest.CreatedAt,
|
CreatedAt = bundleRequest.CreatedAt,
|
||||||
CompletedAt = bundleRequest.CompletedAt,
|
CompletedAt = bundleRequest.CompletedAt,
|
||||||
Status = bundleRequest.Status,
|
Status = bundleRequest.Status,
|
||||||
BacktestRequestsJson = bundleRequest.BacktestRequestsJson,
|
UniversalConfigJson = bundleRequest.UniversalConfigJson,
|
||||||
|
DateTimeRangesJson = bundleRequest.DateTimeRangesJson,
|
||||||
|
MoneyManagementVariantsJson = bundleRequest.MoneyManagementVariantsJson,
|
||||||
|
TickerVariantsJson = bundleRequest.TickerVariantsJson,
|
||||||
TotalBacktests = bundleRequest.TotalBacktests,
|
TotalBacktests = bundleRequest.TotalBacktests,
|
||||||
CompletedBacktests = bundleRequest.CompletedBacktests,
|
CompletedBacktests = bundleRequest.CompletedBacktests,
|
||||||
FailedBacktests = bundleRequest.FailedBacktests,
|
FailedBacktests = bundleRequest.FailedBacktests,
|
||||||
|
|||||||
@@ -71,14 +71,6 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
|||||||
{
|
{
|
||||||
return await ExecuteWithLoggingAsync(async () =>
|
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
|
try
|
||||||
{
|
{
|
||||||
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
|
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
|
||||||
@@ -134,11 +126,6 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
|||||||
user.Accounts = new List<Account>(); // Initialize empty list
|
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;
|
return user;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -4296,8 +4296,12 @@ export interface RunBacktestRequest {
|
|||||||
config?: TradingBotConfigRequest | null;
|
config?: TradingBotConfigRequest | null;
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
|
balance?: number;
|
||||||
|
watchOnly?: boolean;
|
||||||
save?: boolean;
|
save?: boolean;
|
||||||
withCandles?: boolean;
|
withCandles?: boolean;
|
||||||
|
moneyManagementName?: string | null;
|
||||||
|
moneyManagement?: MoneyManagement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TradingBotConfigRequest {
|
export interface TradingBotConfigRequest {
|
||||||
@@ -4352,6 +4356,10 @@ export interface MoneyManagementRequest {
|
|||||||
leverage: number;
|
leverage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MoneyManagement extends LightMoneyManagement {
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BundleBacktestRequest {
|
export interface BundleBacktestRequest {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
user: User;
|
user: User;
|
||||||
@@ -4359,8 +4367,11 @@ export interface BundleBacktestRequest {
|
|||||||
completedAt?: Date | null;
|
completedAt?: Date | null;
|
||||||
status: BundleBacktestRequestStatus;
|
status: BundleBacktestRequestStatus;
|
||||||
name: string;
|
name: string;
|
||||||
backtestRequestsJson: string;
|
universalConfigJson: string;
|
||||||
results?: string[] | null;
|
dateTimeRangesJson: string;
|
||||||
|
moneyManagementVariantsJson: string;
|
||||||
|
tickerVariantsJson: string;
|
||||||
|
results?: string[];
|
||||||
totalBacktests: number;
|
totalBacktests: number;
|
||||||
completedBacktests: number;
|
completedBacktests: number;
|
||||||
failedBacktests: number;
|
failedBacktests: number;
|
||||||
@@ -4381,7 +4392,42 @@ export enum BundleBacktestRequestStatus {
|
|||||||
|
|
||||||
export interface RunBundleBacktestRequest {
|
export interface RunBundleBacktestRequest {
|
||||||
name: string;
|
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 {
|
export interface GeneticRequest {
|
||||||
@@ -4472,10 +4518,6 @@ export interface RunGeneticRequest {
|
|||||||
eligibleIndicators?: IndicatorType[] | null;
|
eligibleIndicators?: IndicatorType[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MoneyManagement extends LightMoneyManagement {
|
|
||||||
user?: User | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StartBotRequest {
|
export interface StartBotRequest {
|
||||||
config?: TradingBotConfigRequest | null;
|
config?: TradingBotConfigRequest | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -553,8 +553,12 @@ export interface RunBacktestRequest {
|
|||||||
config?: TradingBotConfigRequest | null;
|
config?: TradingBotConfigRequest | null;
|
||||||
startDate?: Date;
|
startDate?: Date;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
|
balance?: number;
|
||||||
|
watchOnly?: boolean;
|
||||||
save?: boolean;
|
save?: boolean;
|
||||||
withCandles?: boolean;
|
withCandles?: boolean;
|
||||||
|
moneyManagementName?: string | null;
|
||||||
|
moneyManagement?: MoneyManagement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TradingBotConfigRequest {
|
export interface TradingBotConfigRequest {
|
||||||
@@ -609,6 +613,10 @@ export interface MoneyManagementRequest {
|
|||||||
leverage: number;
|
leverage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MoneyManagement extends LightMoneyManagement {
|
||||||
|
user?: User | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BundleBacktestRequest {
|
export interface BundleBacktestRequest {
|
||||||
requestId: string;
|
requestId: string;
|
||||||
user: User;
|
user: User;
|
||||||
@@ -616,8 +624,11 @@ export interface BundleBacktestRequest {
|
|||||||
completedAt?: Date | null;
|
completedAt?: Date | null;
|
||||||
status: BundleBacktestRequestStatus;
|
status: BundleBacktestRequestStatus;
|
||||||
name: string;
|
name: string;
|
||||||
backtestRequestsJson: string;
|
universalConfigJson: string;
|
||||||
results?: string[] | null;
|
dateTimeRangesJson: string;
|
||||||
|
moneyManagementVariantsJson: string;
|
||||||
|
tickerVariantsJson: string;
|
||||||
|
results?: string[];
|
||||||
totalBacktests: number;
|
totalBacktests: number;
|
||||||
completedBacktests: number;
|
completedBacktests: number;
|
||||||
failedBacktests: number;
|
failedBacktests: number;
|
||||||
@@ -638,7 +649,42 @@ export enum BundleBacktestRequestStatus {
|
|||||||
|
|
||||||
export interface RunBundleBacktestRequest {
|
export interface RunBundleBacktestRequest {
|
||||||
name: string;
|
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 {
|
export interface GeneticRequest {
|
||||||
@@ -729,10 +775,6 @@ export interface RunGeneticRequest {
|
|||||||
eligibleIndicators?: IndicatorType[] | null;
|
eligibleIndicators?: IndicatorType[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MoneyManagement extends LightMoneyManagement {
|
|
||||||
user?: User | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StartBotRequest {
|
export interface StartBotRequest {
|
||||||
config?: TradingBotConfigRequest | null;
|
config?: TradingBotConfigRequest | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,281 @@
|
|||||||
import React, {useEffect, useRef, useState} from 'react';
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
import {BundleBacktestRequest, LightBacktestResponse, Ticker, Timeframe} from '../../generated/ManagingApiTypes';
|
import {
|
||||||
import {BacktestClient} from '../../generated/ManagingApi';
|
AccountClient,
|
||||||
|
BacktestClient,
|
||||||
|
BundleBacktestRequest,
|
||||||
|
BundleBacktestUniversalConfig,
|
||||||
|
DataClient,
|
||||||
|
DateTimeRange,
|
||||||
|
IndicatorType,
|
||||||
|
LightBacktestResponse,
|
||||||
|
MoneyManagementRequest,
|
||||||
|
MoneyManagementVariant,
|
||||||
|
RunBundleBacktestRequest,
|
||||||
|
SignalType,
|
||||||
|
Ticker,
|
||||||
|
Timeframe
|
||||||
|
} from '../../generated/ManagingApi';
|
||||||
import useApiUrlStore from '../../app/store/apiStore';
|
import useApiUrlStore from '../../app/store/apiStore';
|
||||||
import Toast from '../../components/mollecules/Toast/Toast';
|
import Toast from '../../components/mollecules/Toast/Toast';
|
||||||
import {useQuery} from '@tanstack/react-query';
|
import {useQuery} from '@tanstack/react-query';
|
||||||
import * as signalR from '@microsoft/signalr';
|
import * as signalR from '@microsoft/signalr';
|
||||||
import AuthorizedApiBase from '../../generated/AuthorizedApiBase';
|
import AuthorizedApiBase from '../../generated/AuthorizedApiBase';
|
||||||
import BacktestTable from '../../components/organism/Backtest/backtestTable';
|
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 {
|
interface BundleRequestModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
bundle: BundleBacktestRequest | null;
|
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 { apiUrl } = useApiUrlStore();
|
||||||
const [backtests, setBacktests] = useState<LightBacktestResponse[]>([]);
|
const [backtests, setBacktests] = useState<LightBacktestResponse[]>([]);
|
||||||
const signalRRef = useRef<any>(null);
|
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 {
|
const {
|
||||||
data: queryBacktests,
|
data: queryBacktests,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -28,8 +285,7 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
|
|||||||
queryKey: ['bundle-backtests', bundle?.requestId],
|
queryKey: ['bundle-backtests', bundle?.requestId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!open || !bundle) return [];
|
if (!open || !bundle) return [];
|
||||||
const client = new BacktestClient({} as any, apiUrl);
|
const res = await backtestClient.backtest_GetBacktestsByRequestId(bundle.requestId);
|
||||||
const res = await client.backtest_GetBacktestsByRequestId(bundle.requestId);
|
|
||||||
if (!res) return [];
|
if (!res) return [];
|
||||||
return res.map((b: any) => {
|
return res.map((b: any) => {
|
||||||
// Map enums for ticker and timeframe
|
// Map enums for ticker and timeframe
|
||||||
@@ -61,11 +317,12 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
|
|||||||
enabled: !!open && !!bundle,
|
enabled: !!open && !!bundle,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryBacktests) setBacktests(queryBacktests);
|
if (queryBacktests) setBacktests(queryBacktests);
|
||||||
}, [queryBacktests]);
|
}, [queryBacktests]);
|
||||||
|
|
||||||
// SignalR live updates
|
// SignalR live updates for existing bundles
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !bundle) return;
|
if (!open || !bundle) return;
|
||||||
if (bundle.status !== 'Pending' && bundle.status !== 'Running') return;
|
if (bundle.status !== 'Pending' && bundle.status !== 'Running') return;
|
||||||
@@ -132,8 +389,10 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
|
|||||||
};
|
};
|
||||||
}, [open, bundle, apiUrl]);
|
}, [open, bundle, apiUrl]);
|
||||||
|
|
||||||
if (!open || !bundle) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
|
// If viewing an existing bundle
|
||||||
|
if (bundle) {
|
||||||
return (
|
return (
|
||||||
<div className="modal modal-open">
|
<div className="modal modal-open">
|
||||||
<div className="modal-box max-w-4xl">
|
<div className="modal-box max-w-4xl">
|
||||||
@@ -152,6 +411,449 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
|
|||||||
) : (
|
) : (
|
||||||
<BacktestTable list={backtests} />
|
<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-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="modal-action">
|
<div className="modal-action">
|
||||||
<button className="btn" onClick={onClose}>Close</button>
|
<button className="btn" onClick={onClose}>Close</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,477 +1,12 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React 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 BundleRequestsTable from './bundleRequestsTable';
|
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 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 (
|
return (
|
||||||
<div className="p-10 max-w-7xl mx-auto">
|
<div className="p-10 max-w-7xl mx-auto">
|
||||||
<h2 className="text-2xl font-bold mb-6">Bundle Backtest</h2>
|
<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 />
|
<BundleRequestsTable />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -148,12 +148,28 @@ const BundleRequestsTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<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} />
|
<Table columns={columns} data={data} showPagination={true} />
|
||||||
<BundleRequestModal
|
<BundleRequestModal
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
onClose={() => setModalOpen(false)}
|
onClose={() => setModalOpen(false)}
|
||||||
bundle={selectedBundle}
|
bundle={selectedBundle}
|
||||||
|
onCreateBundle={async (request) => {
|
||||||
|
const client = new BacktestClient({} as any, apiUrl);
|
||||||
|
await client.backtest_RunBundle(request);
|
||||||
|
fetchData(); // Refresh the table
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user