Add bundle backtest refact + fix whitelist

This commit is contained in:
2025-10-12 14:40:20 +07:00
parent 4543246871
commit 5acc77650f
21 changed files with 2961 additions and 628 deletions

View File

@@ -419,21 +419,30 @@ public class BacktestController : BaseController
/// Creates a bundle backtest request with the specified configurations.
/// This endpoint creates a request that will be processed by a background worker.
/// </summary>
/// <param name="requests">The list of backtest requests to execute.</param>
/// <param name="name">Display name for the bundle (required).</param>
/// <param name="request">The bundle backtest request with variant lists.</param>
/// <returns>The bundle backtest request with ID for tracking progress.</returns>
[HttpPost]
[Route("BacktestBundle")]
public async Task<ActionResult<BundleBacktestRequest>> RunBundle([FromBody] RunBundleBacktestRequest request)
{
if (request?.Requests == null || !request.Requests.Any())
if (request?.UniversalConfig == null)
{
return BadRequest("At least one backtest request is required");
return BadRequest("Universal configuration is required");
}
if (request.Requests.Count > 10)
if (request.DateTimeRanges == null || !request.DateTimeRanges.Any())
{
return BadRequest("Maximum of 10 backtests allowed per bundle request");
return BadRequest("At least one DateTime range is required");
}
if (request.MoneyManagementVariants == null || !request.MoneyManagementVariants.Any())
{
return BadRequest("At least one money management variant is required");
}
if (request.TickerVariants == null || !request.TickerVariants.Any())
{
return BadRequest("At least one ticker variant is required");
}
if (string.IsNullOrWhiteSpace(request.Name))
@@ -441,32 +450,35 @@ public class BacktestController : BaseController
return BadRequest("Bundle name is required");
}
// Calculate total number of backtests
var totalBacktests = request.DateTimeRanges.Count * request.MoneyManagementVariants.Count * request.TickerVariants.Count;
if (totalBacktests > 100)
{
return BadRequest("Maximum of 100 backtests allowed per bundle request");
}
try
{
var user = await GetUser();
// Validate all requests before creating the bundle
foreach (var req in request.Requests)
// Validate universal configuration
if (string.IsNullOrEmpty(request.UniversalConfig.AccountName))
{
if (req?.Config == null)
{
return BadRequest("Invalid request: Configuration is required");
}
return BadRequest("Account name is required in universal configuration");
}
if (string.IsNullOrEmpty(req.Config.AccountName))
{
return BadRequest("Invalid request: Account name is required");
}
if (string.IsNullOrEmpty(request.UniversalConfig.ScenarioName) && request.UniversalConfig.Scenario == null)
{
return BadRequest("Either scenario name or scenario object is required in universal configuration");
}
if (string.IsNullOrEmpty(req.Config.ScenarioName) && req.Config.Scenario == null)
// Validate all money management variants
foreach (var mmVariant in request.MoneyManagementVariants)
{
if (mmVariant.MoneyManagement == null)
{
return BadRequest("Invalid request: Either scenario name or scenario object is required");
}
if (string.IsNullOrEmpty(req.Config.MoneyManagementName) && req.Config.MoneyManagement == null)
{
return BadRequest(
"Invalid request: Either money management name or money management object is required");
return BadRequest("Each money management variant must have a money management object");
}
}
@@ -474,8 +486,11 @@ public class BacktestController : BaseController
var bundleRequest = new BundleBacktestRequest
{
User = user,
BacktestRequestsJson = JsonSerializer.Serialize(request.Requests),
TotalBacktests = request.Requests.Count,
UniversalConfigJson = JsonSerializer.Serialize(request.UniversalConfig),
DateTimeRangesJson = JsonSerializer.Serialize(request.DateTimeRanges),
MoneyManagementVariantsJson = JsonSerializer.Serialize(request.MoneyManagementVariants),
TickerVariantsJson = JsonSerializer.Serialize(request.TickerVariants),
TotalBacktests = totalBacktests,
CompletedBacktests = 0,
FailedBacktests = 0,
Status = BundleBacktestRequestStatus.Pending,
@@ -491,6 +506,72 @@ public class BacktestController : BaseController
}
}
/// <summary>
/// Generates individual backtest requests from variant configuration
/// </summary>
/// <param name="request">The bundle backtest request</param>
/// <returns>List of individual backtest requests</returns>
private List<RunBacktestRequest> GenerateBacktestRequests(RunBundleBacktestRequest request)
{
var backtestRequests = new List<RunBacktestRequest>();
foreach (var dateRange in request.DateTimeRanges)
{
foreach (var mmVariant in request.MoneyManagementVariants)
{
foreach (var ticker in request.TickerVariants)
{
var config = new TradingBotConfigRequest
{
AccountName = request.UniversalConfig.AccountName,
Ticker = ticker,
Timeframe = request.UniversalConfig.Timeframe,
IsForWatchingOnly = request.UniversalConfig.IsForWatchingOnly,
BotTradingBalance = request.UniversalConfig.BotTradingBalance,
Name = $"{request.UniversalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}",
FlipPosition = request.UniversalConfig.FlipPosition,
CooldownPeriod = request.UniversalConfig.CooldownPeriod,
MaxLossStreak = request.UniversalConfig.MaxLossStreak,
Scenario = request.UniversalConfig.Scenario,
ScenarioName = request.UniversalConfig.ScenarioName,
MoneyManagement = mmVariant.MoneyManagement,
MaxPositionTimeHours = request.UniversalConfig.MaxPositionTimeHours,
CloseEarlyWhenProfitable = request.UniversalConfig.CloseEarlyWhenProfitable,
FlipOnlyWhenInProfit = request.UniversalConfig.FlipOnlyWhenInProfit,
UseSynthApi = request.UniversalConfig.UseSynthApi,
UseForPositionSizing = request.UniversalConfig.UseForPositionSizing,
UseForSignalFiltering = request.UniversalConfig.UseForSignalFiltering,
UseForDynamicStopLoss = request.UniversalConfig.UseForDynamicStopLoss
};
var backtestRequest = new RunBacktestRequest
{
Config = config,
StartDate = dateRange.StartDate,
EndDate = dateRange.EndDate,
Balance = request.UniversalConfig.BotTradingBalance,
WatchOnly = request.UniversalConfig.WatchOnly,
Save = request.UniversalConfig.Save,
WithCandles = request.UniversalConfig.WithCandles,
MoneyManagement = mmVariant.MoneyManagement != null ?
new MoneyManagement
{
Name = mmVariant.MoneyManagement.Name,
Timeframe = mmVariant.MoneyManagement.Timeframe,
StopLoss = mmVariant.MoneyManagement.StopLoss,
TakeProfit = mmVariant.MoneyManagement.TakeProfit,
Leverage = mmVariant.MoneyManagement.Leverage
} : null
};
backtestRequests.Add(backtestRequest);
}
}
}
return backtestRequests;
}
/// <summary>
/// Retrieves all bundle backtest requests for the authenticated user.
/// </summary>
@@ -741,35 +822,3 @@ public class BacktestController : BaseController
};
}
}
/// <summary>
/// Request model for running a backtest
/// </summary>
public class RunBacktestRequest
{
/// <summary>
/// The trading bot configuration request to use for the backtest
/// </summary>
public TradingBotConfigRequest Config { get; set; }
/// <summary>
/// The start date for the backtest
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
/// The end date for the backtest
/// </summary>
public DateTime EndDate { get; set; }
/// <summary>
/// Whether to save the backtest results
/// </summary>
public bool Save { get; set; } = false;
/// <summary>
/// Whether to include candles and indicators values in the response.
/// Set to false to reduce response size dramatically.
/// </summary>
public bool WithCandles { get; set; } = false;
}

View File

@@ -1,11 +1,41 @@
using System.ComponentModel.DataAnnotations;
using Managing.Api.Controllers;
using Managing.Domain.Backtests;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for running bundle backtests with variant lists instead of individual requests
/// </summary>
public class RunBundleBacktestRequest
{
[Required] public string Name { get; set; } = string.Empty;
/// <summary>
/// Display name for the bundle backtest
/// </summary>
[Required]
public string Name { get; set; } = string.Empty;
[Required] public List<RunBacktestRequest> Requests { get; set; } = new();
/// <summary>
/// Universal configuration that applies to all backtests in the bundle
/// </summary>
[Required]
public BundleBacktestUniversalConfig UniversalConfig { get; set; } = new();
/// <summary>
/// List of DateTime ranges to test (each range will generate a separate backtest)
/// </summary>
[Required]
public List<DateTimeRange> DateTimeRanges { get; set; } = new();
/// <summary>
/// List of money management configurations to test (each will generate a separate backtest)
/// </summary>
[Required]
public List<MoneyManagementVariant> MoneyManagementVariants { get; set; } = new();
/// <summary>
/// List of tickers to test (each will generate a separate backtest)
/// </summary>
[Required]
public List<Ticker> TickerVariants { get; set; } = new();
}

View File

@@ -12,6 +12,7 @@ using Managing.Domain.Strategies;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Orleans.Concurrency;
using static Managing.Common.Enums;
namespace Managing.Application.Grains;
@@ -103,12 +104,11 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
bundleRequest.Status = BundleBacktestRequestStatus.Running;
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
// Deserialize the backtest requests as strongly-typed objects
var backtestRequests =
JsonSerializer.Deserialize<List<RunBacktestRequest>>(bundleRequest.BacktestRequestsJson);
if (backtestRequests == null)
// Generate backtest requests from variant configuration
var backtestRequests = GenerateBacktestRequestsFromVariants(bundleRequest);
if (backtestRequests == null || !backtestRequests.Any())
{
throw new InvalidOperationException("Failed to deserialize backtest requests");
throw new InvalidOperationException("Failed to generate backtest requests from variants");
}
// Process each backtest request sequentially
@@ -130,6 +130,90 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
}
}
/// <summary>
/// Generates individual backtest requests from variant configuration
/// </summary>
private List<RunBacktestRequest> GenerateBacktestRequestsFromVariants(BundleBacktestRequest bundleRequest)
{
try
{
// Deserialize the variant configurations
var universalConfig = JsonSerializer.Deserialize<BundleBacktestUniversalConfig>(bundleRequest.UniversalConfigJson);
var dateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(bundleRequest.DateTimeRangesJson);
var moneyManagementVariants = JsonSerializer.Deserialize<List<MoneyManagementVariant>>(bundleRequest.MoneyManagementVariantsJson);
var tickerVariants = JsonSerializer.Deserialize<List<Ticker>>(bundleRequest.TickerVariantsJson);
if (universalConfig == null || dateTimeRanges == null || moneyManagementVariants == null || tickerVariants == null)
{
_logger.LogError("Failed to deserialize variant configurations for bundle request {RequestId}", bundleRequest.RequestId);
return new List<RunBacktestRequest>();
}
var backtestRequests = new List<RunBacktestRequest>();
foreach (var dateRange in dateTimeRanges)
{
foreach (var mmVariant in moneyManagementVariants)
{
foreach (var ticker in tickerVariants)
{
var config = new TradingBotConfigRequest
{
AccountName = universalConfig.AccountName,
Ticker = ticker,
Timeframe = universalConfig.Timeframe,
IsForWatchingOnly = universalConfig.IsForWatchingOnly,
BotTradingBalance = universalConfig.BotTradingBalance,
Name = $"{universalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}",
FlipPosition = universalConfig.FlipPosition,
CooldownPeriod = universalConfig.CooldownPeriod,
MaxLossStreak = universalConfig.MaxLossStreak,
Scenario = universalConfig.Scenario,
ScenarioName = universalConfig.ScenarioName,
MoneyManagement = mmVariant.MoneyManagement,
MaxPositionTimeHours = universalConfig.MaxPositionTimeHours,
CloseEarlyWhenProfitable = universalConfig.CloseEarlyWhenProfitable,
FlipOnlyWhenInProfit = universalConfig.FlipOnlyWhenInProfit,
UseSynthApi = universalConfig.UseSynthApi,
UseForPositionSizing = universalConfig.UseForPositionSizing,
UseForSignalFiltering = universalConfig.UseForSignalFiltering,
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss
};
var backtestRequest = new RunBacktestRequest
{
Config = config,
StartDate = dateRange.StartDate,
EndDate = dateRange.EndDate,
Balance = universalConfig.BotTradingBalance,
WatchOnly = universalConfig.WatchOnly,
Save = universalConfig.Save,
WithCandles = false, // Bundle backtests never return candles
MoneyManagement = mmVariant.MoneyManagement != null ?
new MoneyManagement
{
Name = mmVariant.MoneyManagement.Name,
Timeframe = mmVariant.MoneyManagement.Timeframe,
StopLoss = mmVariant.MoneyManagement.StopLoss,
TakeProfit = mmVariant.MoneyManagement.TakeProfit,
Leverage = mmVariant.MoneyManagement.Leverage
} : null
};
backtestRequests.Add(backtestRequest);
}
}
}
return backtestRequests;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating backtest requests from variants for bundle request {RequestId}", bundleRequest.RequestId);
return new List<RunBacktestRequest>();
}
}
private async Task ProcessSingleBacktest(
IBacktester backtester,
RunBacktestRequest runBacktestRequest,
@@ -138,10 +222,8 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
{
try
{
// Get total count from deserialized requests instead of string splitting
var backtestRequests =
JsonSerializer.Deserialize<List<RunBacktestRequest>>(bundleRequest.BacktestRequestsJson);
var totalCount = backtestRequests?.Count ?? 0;
// Calculate total count from the variant configuration
var totalCount = bundleRequest.TotalBacktests;
// Update current backtest being processed
bundleRequest.CurrentBacktest = $"Backtest {index + 1} of {totalCount}";

View File

@@ -64,8 +64,17 @@ public class UserService : IUserService
throw new Exception($"Address {recoveredAddress} not corresponding");
}
User user = null;
// Check if account exist
var user = await _userRepository.GetUserByNameAsync(name);
try
{
user = await _userRepository.GetUserByNameAsync(name);
}
catch (Exception e)
{
Console.WriteLine(e);
}
if (user != null)
{

View File

@@ -93,13 +93,11 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
bundleRequest.Status = BundleBacktestRequestStatus.Running;
await backtester.UpdateBundleBacktestRequestAsync(bundleRequest);
// Deserialize the backtest requests as strongly-typed objects
var backtestRequests =
JsonSerializer.Deserialize<List<RunBacktestRequest>>(
bundleRequest.BacktestRequestsJson);
if (backtestRequests == null)
// Generate backtest requests from the new variant structure
var backtestRequests = GenerateBacktestRequestsFromVariants(bundleRequest);
if (backtestRequests == null || !backtestRequests.Any())
{
throw new InvalidOperationException("Failed to deserialize backtest requests");
throw new InvalidOperationException("Failed to generate backtest requests from variants");
}
// Process each backtest request
@@ -298,11 +296,9 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
// Use Results property to determine which backtests need to be retried
var succeededIds = new HashSet<string>(failedBundle.Results ?? new List<string>());
// Deserialize the original requests
var originalRequests =
JsonSerializer
.Deserialize<List<RunBacktestRequest>>(failedBundle.BacktestRequestsJson);
if (originalRequests == null) continue;
// Generate backtest requests from the new variant structure
var originalRequests = GenerateBacktestRequestsFromVariants(failedBundle);
if (originalRequests == null || !originalRequests.Any()) continue;
for (int i = failedBundle.CompletedBacktests; i < originalRequests.Count; i++)
{
@@ -339,4 +335,88 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
}
}
}
/// <summary>
/// Generates individual backtest requests from variant configuration
/// </summary>
private List<RunBacktestRequest> GenerateBacktestRequestsFromVariants(BundleBacktestRequest bundleRequest)
{
try
{
// Deserialize the variant configurations
var universalConfig = JsonSerializer.Deserialize<BundleBacktestUniversalConfig>(bundleRequest.UniversalConfigJson);
var dateTimeRanges = JsonSerializer.Deserialize<List<DateTimeRange>>(bundleRequest.DateTimeRangesJson);
var moneyManagementVariants = JsonSerializer.Deserialize<List<MoneyManagementVariant>>(bundleRequest.MoneyManagementVariantsJson);
var tickerVariants = JsonSerializer.Deserialize<List<Ticker>>(bundleRequest.TickerVariantsJson);
if (universalConfig == null || dateTimeRanges == null || moneyManagementVariants == null || tickerVariants == null)
{
_logger.LogError("Failed to deserialize variant configurations for bundle request {RequestId}", bundleRequest.RequestId);
return new List<RunBacktestRequest>();
}
var backtestRequests = new List<RunBacktestRequest>();
foreach (var dateRange in dateTimeRanges)
{
foreach (var mmVariant in moneyManagementVariants)
{
foreach (var ticker in tickerVariants)
{
var config = new TradingBotConfigRequest
{
AccountName = universalConfig.AccountName,
Ticker = ticker,
Timeframe = universalConfig.Timeframe,
IsForWatchingOnly = universalConfig.IsForWatchingOnly,
BotTradingBalance = universalConfig.BotTradingBalance,
Name = $"{universalConfig.BotName}_{ticker}_{dateRange.StartDate:yyyyMMdd}_{dateRange.EndDate:yyyyMMdd}",
FlipPosition = universalConfig.FlipPosition,
CooldownPeriod = universalConfig.CooldownPeriod,
MaxLossStreak = universalConfig.MaxLossStreak,
Scenario = universalConfig.Scenario,
ScenarioName = universalConfig.ScenarioName,
MoneyManagement = mmVariant.MoneyManagement,
MaxPositionTimeHours = universalConfig.MaxPositionTimeHours,
CloseEarlyWhenProfitable = universalConfig.CloseEarlyWhenProfitable,
FlipOnlyWhenInProfit = universalConfig.FlipOnlyWhenInProfit,
UseSynthApi = universalConfig.UseSynthApi,
UseForPositionSizing = universalConfig.UseForPositionSizing,
UseForSignalFiltering = universalConfig.UseForSignalFiltering,
UseForDynamicStopLoss = universalConfig.UseForDynamicStopLoss
};
var backtestRequest = new RunBacktestRequest
{
Config = config,
StartDate = dateRange.StartDate,
EndDate = dateRange.EndDate,
Balance = universalConfig.BotTradingBalance,
WatchOnly = universalConfig.WatchOnly,
Save = universalConfig.Save,
WithCandles = false, // Bundle backtests never return candles
MoneyManagement = mmVariant.MoneyManagement != null ?
new MoneyManagement
{
Name = mmVariant.MoneyManagement.Name,
Timeframe = mmVariant.MoneyManagement.Timeframe,
StopLoss = mmVariant.MoneyManagement.StopLoss,
TakeProfit = mmVariant.MoneyManagement.TakeProfit,
Leverage = mmVariant.MoneyManagement.Leverage
} : null
};
backtestRequests.Add(backtestRequest);
}
}
}
return backtestRequests;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating backtest requests from variants for bundle request {RequestId}", bundleRequest.RequestId);
return new List<RunBacktestRequest>();
}
}
}

View File

@@ -1,10 +1,11 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Users;
namespace Managing.Domain.Backtests;
/// <summary>
/// Domain model for bundle backtest requests
/// Domain model for bundle backtest requests with variant-based configuration
/// </summary>
public class BundleBacktestRequest
{
@@ -14,7 +15,10 @@ public class BundleBacktestRequest
CreatedAt = DateTime.UtcNow;
Status = BundleBacktestRequestStatus.Pending;
Results = new List<string>();
BacktestRequestsJson = string.Empty;
UniversalConfigJson = string.Empty;
DateTimeRangesJson = string.Empty;
MoneyManagementVariantsJson = string.Empty;
TickerVariantsJson = string.Empty;
}
/// <summary>
@@ -27,7 +31,10 @@ public class BundleBacktestRequest
CreatedAt = DateTime.UtcNow;
Status = BundleBacktestRequestStatus.Pending;
Results = new List<string>();
BacktestRequestsJson = string.Empty;
UniversalConfigJson = string.Empty;
DateTimeRangesJson = string.Empty;
MoneyManagementVariantsJson = string.Empty;
TickerVariantsJson = string.Empty;
}
/// <summary>
@@ -66,10 +73,28 @@ public class BundleBacktestRequest
public string Name { get; set; }
/// <summary>
/// The list of backtest requests to execute (serialized as JSON)
/// The universal configuration that applies to all backtests (serialized as JSON)
/// </summary>
[Required]
public string BacktestRequestsJson { get; set; } = string.Empty;
public string UniversalConfigJson { get; set; } = string.Empty;
/// <summary>
/// The list of DateTime ranges to test (serialized as JSON)
/// </summary>
[Required]
public string DateTimeRangesJson { get; set; } = string.Empty;
/// <summary>
/// The list of money management variants to test (serialized as JSON)
/// </summary>
[Required]
public string MoneyManagementVariantsJson { get; set; } = string.Empty;
/// <summary>
/// The list of ticker variants to test (serialized as JSON)
/// </summary>
[Required]
public string TickerVariantsJson { get; set; } = string.Empty;
/// <summary>
/// The results of the bundle backtest execution
@@ -118,6 +143,7 @@ public class BundleBacktestRequest
/// Estimated time remaining in seconds
/// </summary>
public int? EstimatedTimeRemainingSeconds { get; set; }
}
/// <summary>

View 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;
}

View 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; }
}

View 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();
}

View File

@@ -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");
}
}
}

View File

@@ -329,10 +329,6 @@ namespace Managing.Infrastructure.Databases.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("BacktestRequestsJson")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("timestamp with time zone");
@@ -346,6 +342,10 @@ namespace Managing.Infrastructure.Databases.Migrations
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("DateTimeRangesJson")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
@@ -355,6 +355,10 @@ namespace Managing.Infrastructure.Databases.Migrations
b.Property<int>("FailedBacktests")
.HasColumnType("integer");
b.Property<string>("MoneyManagementVariantsJson")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
@@ -375,9 +379,17 @@ namespace Managing.Infrastructure.Databases.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<string>("TickerVariantsJson")
.IsRequired()
.HasColumnType("text");
b.Property<int>("TotalBacktests")
.HasColumnType("integer");
b.Property<string>("UniversalConfigJson")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");

View File

@@ -30,7 +30,19 @@ public class BundleBacktestRequestEntity
[Required]
[Column(TypeName = "text")]
public string BacktestRequestsJson { get; set; } = string.Empty;
public string UniversalConfigJson { get; set; } = string.Empty;
[Required]
[Column(TypeName = "text")]
public string DateTimeRangesJson { get; set; } = string.Empty;
[Required]
[Column(TypeName = "text")]
public string MoneyManagementVariantsJson { get; set; } = string.Empty;
[Required]
[Column(TypeName = "text")]
public string TickerVariantsJson { get; set; } = string.Empty;
[Required]
public int TotalBacktests { get; set; }

View File

@@ -190,7 +190,10 @@ public class ManagingDbContext : DbContext
entity.Property(e => e.Status)
.IsRequired()
.HasConversion<string>(); // Store enum as string
entity.Property(e => e.BacktestRequestsJson).HasColumnType("text");
entity.Property(e => e.UniversalConfigJson).HasColumnType("text");
entity.Property(e => e.DateTimeRangesJson).HasColumnType("text");
entity.Property(e => e.MoneyManagementVariantsJson).HasColumnType("text");
entity.Property(e => e.TickerVariantsJson).HasColumnType("text");
entity.Property(e => e.ErrorMessage).HasColumnType("text");
entity.Property(e => e.ProgressInfo).HasColumnType("text");
entity.Property(e => e.CurrentBacktest).HasMaxLength(500);

View File

@@ -367,7 +367,10 @@ public static class PostgreSqlMappers
CreatedAt = entity.CreatedAt,
CompletedAt = entity.CompletedAt,
Status = entity.Status,
BacktestRequestsJson = entity.BacktestRequestsJson,
UniversalConfigJson = entity.UniversalConfigJson,
DateTimeRangesJson = entity.DateTimeRangesJson,
MoneyManagementVariantsJson = entity.MoneyManagementVariantsJson,
TickerVariantsJson = entity.TickerVariantsJson,
TotalBacktests = entity.TotalBacktests,
CompletedBacktests = entity.CompletedBacktests,
FailedBacktests = entity.FailedBacktests,
@@ -406,7 +409,10 @@ public static class PostgreSqlMappers
CreatedAt = bundleRequest.CreatedAt,
CompletedAt = bundleRequest.CompletedAt,
Status = bundleRequest.Status,
BacktestRequestsJson = bundleRequest.BacktestRequestsJson,
UniversalConfigJson = bundleRequest.UniversalConfigJson,
DateTimeRangesJson = bundleRequest.DateTimeRangesJson,
MoneyManagementVariantsJson = bundleRequest.MoneyManagementVariantsJson,
TickerVariantsJson = bundleRequest.TickerVariantsJson,
TotalBacktests = bundleRequest.TotalBacktests,
CompletedBacktests = bundleRequest.CompletedBacktests,
FailedBacktests = bundleRequest.FailedBacktests,

View File

@@ -71,14 +71,6 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
{
return await ExecuteWithLoggingAsync(async () =>
{
// Check cache first for frequently accessed users
var cacheKey = fetchAccounts ? $"user_name_with_accounts_{name}" : $"user_name_{name}";
var cachedUser = _cacheService.GetValue<User>(cacheKey);
if (cachedUser != null)
{
return cachedUser;
}
try
{
await PostgreSqlConnectionHelper.EnsureConnectionOpenAsync(_context);
@@ -134,11 +126,6 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
user.Accounts = new List<Account>(); // Initialize empty list
}
// Cache user for 5 minutes since user data doesn't change frequently
// Use shorter cache time when including accounts since accounts change more frequently
var cacheTime = fetchAccounts ? TimeSpan.FromMinutes(2) : TimeSpan.FromMinutes(5);
_cacheService.SaveValue(cacheKey, user, cacheTime);
return user;
}
finally

View File

@@ -4296,8 +4296,12 @@ export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null;
startDate?: Date;
endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean;
withCandles?: boolean;
moneyManagementName?: string | null;
moneyManagement?: MoneyManagement | null;
}
export interface TradingBotConfigRequest {
@@ -4352,6 +4356,10 @@ export interface MoneyManagementRequest {
leverage: number;
}
export interface MoneyManagement extends LightMoneyManagement {
user?: User | null;
}
export interface BundleBacktestRequest {
requestId: string;
user: User;
@@ -4359,8 +4367,11 @@ export interface BundleBacktestRequest {
completedAt?: Date | null;
status: BundleBacktestRequestStatus;
name: string;
backtestRequestsJson: string;
results?: string[] | null;
universalConfigJson: string;
dateTimeRangesJson: string;
moneyManagementVariantsJson: string;
tickerVariantsJson: string;
results?: string[];
totalBacktests: number;
completedBacktests: number;
failedBacktests: number;
@@ -4381,7 +4392,42 @@ export enum BundleBacktestRequestStatus {
export interface RunBundleBacktestRequest {
name: string;
requests: RunBacktestRequest[];
universalConfig: BundleBacktestUniversalConfig;
dateTimeRanges: DateTimeRange[];
moneyManagementVariants: MoneyManagementVariant[];
tickerVariants: Ticker[];
}
export interface BundleBacktestUniversalConfig {
accountName: string;
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botName: string;
flipPosition: boolean;
cooldownPeriod?: number | null;
maxLossStreak?: number;
scenario?: ScenarioRequest | null;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit?: boolean;
useSynthApi?: boolean;
useForPositionSizing?: boolean;
useForSignalFiltering?: boolean;
useForDynamicStopLoss?: boolean;
watchOnly?: boolean;
save?: boolean;
withCandles?: boolean;
}
export interface DateTimeRange {
startDate: Date;
endDate: Date;
}
export interface MoneyManagementVariant {
moneyManagement?: MoneyManagementRequest;
}
export interface GeneticRequest {
@@ -4472,10 +4518,6 @@ export interface RunGeneticRequest {
eligibleIndicators?: IndicatorType[] | null;
}
export interface MoneyManagement extends LightMoneyManagement {
user?: User | null;
}
export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}

View File

@@ -553,8 +553,12 @@ export interface RunBacktestRequest {
config?: TradingBotConfigRequest | null;
startDate?: Date;
endDate?: Date;
balance?: number;
watchOnly?: boolean;
save?: boolean;
withCandles?: boolean;
moneyManagementName?: string | null;
moneyManagement?: MoneyManagement | null;
}
export interface TradingBotConfigRequest {
@@ -609,6 +613,10 @@ export interface MoneyManagementRequest {
leverage: number;
}
export interface MoneyManagement extends LightMoneyManagement {
user?: User | null;
}
export interface BundleBacktestRequest {
requestId: string;
user: User;
@@ -616,8 +624,11 @@ export interface BundleBacktestRequest {
completedAt?: Date | null;
status: BundleBacktestRequestStatus;
name: string;
backtestRequestsJson: string;
results?: string[] | null;
universalConfigJson: string;
dateTimeRangesJson: string;
moneyManagementVariantsJson: string;
tickerVariantsJson: string;
results?: string[];
totalBacktests: number;
completedBacktests: number;
failedBacktests: number;
@@ -638,7 +649,42 @@ export enum BundleBacktestRequestStatus {
export interface RunBundleBacktestRequest {
name: string;
requests: RunBacktestRequest[];
universalConfig: BundleBacktestUniversalConfig;
dateTimeRanges: DateTimeRange[];
moneyManagementVariants: MoneyManagementVariant[];
tickerVariants: Ticker[];
}
export interface BundleBacktestUniversalConfig {
accountName: string;
timeframe: Timeframe;
isForWatchingOnly: boolean;
botTradingBalance: number;
botName: string;
flipPosition: boolean;
cooldownPeriod?: number | null;
maxLossStreak?: number;
scenario?: ScenarioRequest | null;
scenarioName?: string | null;
maxPositionTimeHours?: number | null;
closeEarlyWhenProfitable?: boolean;
flipOnlyWhenInProfit?: boolean;
useSynthApi?: boolean;
useForPositionSizing?: boolean;
useForSignalFiltering?: boolean;
useForDynamicStopLoss?: boolean;
watchOnly?: boolean;
save?: boolean;
withCandles?: boolean;
}
export interface DateTimeRange {
startDate: Date;
endDate: Date;
}
export interface MoneyManagementVariant {
moneyManagement?: MoneyManagementRequest;
}
export interface GeneticRequest {
@@ -729,10 +775,6 @@ export interface RunGeneticRequest {
eligibleIndicators?: IndicatorType[] | null;
}
export interface MoneyManagement extends LightMoneyManagement {
user?: User | null;
}
export interface StartBotRequest {
config?: TradingBotConfigRequest | null;
}

View File

@@ -1,24 +1,281 @@
import React, {useEffect, useRef, useState} from 'react';
import {BundleBacktestRequest, LightBacktestResponse, Ticker, Timeframe} from '../../generated/ManagingApiTypes';
import {BacktestClient} from '../../generated/ManagingApi';
import {
AccountClient,
BacktestClient,
BundleBacktestRequest,
BundleBacktestUniversalConfig,
DataClient,
DateTimeRange,
IndicatorType,
LightBacktestResponse,
MoneyManagementRequest,
MoneyManagementVariant,
RunBundleBacktestRequest,
SignalType,
Ticker,
Timeframe
} from '../../generated/ManagingApi';
import useApiUrlStore from '../../app/store/apiStore';
import Toast from '../../components/mollecules/Toast/Toast';
import {useQuery} from '@tanstack/react-query';
import * as signalR from '@microsoft/signalr';
import AuthorizedApiBase from '../../generated/AuthorizedApiBase';
import BacktestTable from '../../components/organism/Backtest/backtestTable';
import FormInput from '../../components/mollecules/FormInput/FormInput';
import CustomScenario from '../../components/organism/CustomScenario/CustomScenario';
import {useCustomScenario} from '../../app/store/customScenario';
interface BundleRequestModalProps {
open: boolean;
onClose: () => void;
bundle: BundleBacktestRequest | null;
onCreateBundle?: (request: RunBundleBacktestRequest) => void;
}
const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose, bundle }) => {
const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
open,
onClose,
bundle,
onCreateBundle
}) => {
const { apiUrl } = useApiUrlStore();
const [backtests, setBacktests] = useState<LightBacktestResponse[]>([]);
const signalRRef = useRef<any>(null);
// Custom scenario hook
const { scenario, setCustomScenario } = useCustomScenario();
// Form state for creating new bundle requests
const [strategyName, setStrategyName] = useState<string>('');
const [selectedAccount, setSelectedAccount] = useState<string>('');
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(Timeframe.FifteenMinutes);
const [selectedTickers, setSelectedTickers] = useState<Ticker[]>([]);
const [startingCapital, setStartingCapital] = useState<number>(10000);
// Advanced parameters state
const [cooldownPeriod, setCooldownPeriod] = useState<number>(0);
const [maxLossStreak, setMaxLossStreak] = useState<number>(0);
const [maxPositionTime, setMaxPositionTime] = useState<number>(0);
const [flipPosition, setFlipPosition] = useState<boolean>(false);
const [flipOnlyWhenInProfit, setFlipOnlyWhenInProfit] = useState<boolean>(false);
const [closeEarlyWhenProfitable, setCloseEarlyWhenProfitable] = useState<boolean>(false);
// Variant arrays
const [dateTimeRanges, setDateTimeRanges] = useState<DateTimeRange[]>([
{ startDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), endDate: new Date() }
]);
const [moneyManagementVariants, setMoneyManagementVariants] = useState<MoneyManagementVariant[]>([
{ moneyManagement: { name: 'Default', timeframe: Timeframe.FifteenMinutes, stopLoss: 0.01, takeProfit: 0.02, leverage: 1 } }
]);
// API clients
const backtestClient = new BacktestClient({} as any, apiUrl);
const accountClient = new AccountClient({} as any, apiUrl);
const dataClient = new DataClient({} as any, apiUrl);
// Fetch data
const { data: accounts } = useQuery({
queryFn: () => accountClient.account_GetAccounts(),
queryKey: ['accounts'],
});
const { data: tickers } = useQuery({
queryFn: () => dataClient.data_GetTickers(selectedTimeframe),
queryKey: ['tickers', selectedTimeframe],
enabled: !!selectedTimeframe,
});
// Calculate total backtests
const totalBacktests = dateTimeRanges.length * moneyManagementVariants.length * selectedTickers.length;
// Calculate estimated time for bundle backtests
const calculateEstimatedTime = (): number => {
if (totalBacktests === 0) return 0;
// Base time per timeframe (in seconds) - much lower realistic values
const timeframeBaseTime: Record<Timeframe, number> = {
[Timeframe.OneMinute]: 0.01,
[Timeframe.FiveMinutes]: 0.02,
[Timeframe.FifteenMinutes]: 0.011, // ~15 seconds for 1,344 candles
[Timeframe.ThirtyMinutes]: 0.03,
[Timeframe.OneHour]: 0.04,
[Timeframe.FourHour]: 0.05,
[Timeframe.OneDay]: 0.1,
};
// Calculate total time for all date ranges
let totalTimeSeconds = 0;
dateTimeRanges.forEach(range => {
const timeDiffMs = range.endDate.getTime() - range.startDate.getTime();
const timeDiffDays = timeDiffMs / (1000 * 60 * 60 * 24);
// Get base time for the selected timeframe
const baseTimePerCandle = timeframeBaseTime[selectedTimeframe] || 15;
// Calculate candles per day based on timeframe
const candlesPerDay: Record<Timeframe, number> = {
[Timeframe.OneMinute]: 1440, // 24 * 60
[Timeframe.FiveMinutes]: 288, // 24 * 12
[Timeframe.FifteenMinutes]: 96, // 24 * 4
[Timeframe.ThirtyMinutes]: 48, // 24 * 2
[Timeframe.OneHour]: 24,
[Timeframe.FourHour]: 6,
[Timeframe.OneDay]: 1,
};
const candlesInRange = timeDiffDays * candlesPerDay[selectedTimeframe];
const timeForThisRange = candlesInRange * baseTimePerCandle;
totalTimeSeconds += timeForThisRange;
});
// Multiply by number of variants (money management and tickers)
const variantMultiplier = moneyManagementVariants.length * selectedTickers.length;
const totalEstimatedSeconds = totalTimeSeconds * variantMultiplier;
return Math.ceil(totalEstimatedSeconds);
};
const estimatedTimeSeconds = calculateEstimatedTime();
// Add date range variant
const addDateTimeRange = () => {
const newRange: DateTimeRange = {
startDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000),
endDate: new Date()
};
setDateTimeRanges(prev => [...prev, newRange]);
};
// Remove date range variant
const removeDateTimeRange = (index: number) => {
if (dateTimeRanges.length > 1) {
setDateTimeRanges(prev => prev.filter((_, i) => i !== index));
}
};
// Update date range
const updateDateTimeRange = (index: number, field: 'startDate' | 'endDate', value: Date) => {
setDateTimeRanges(prev => prev.map((range, i) =>
i === index ? { ...range, [field]: value } : range
));
};
// Add money management variant
const addMoneyManagementVariant = () => {
const newVariant: MoneyManagementVariant = {
moneyManagement: {
name: `MM ${moneyManagementVariants.length + 1}`,
timeframe: selectedTimeframe,
stopLoss: 0.01,
takeProfit: 0.02,
leverage: 1
}
};
setMoneyManagementVariants(prev => [...prev, newVariant]);
};
// Remove money management variant
const removeMoneyManagementVariant = (index: number) => {
if (moneyManagementVariants.length > 1) {
setMoneyManagementVariants(prev => prev.filter((_, i) => i !== index));
}
};
// Update money management variant
const updateMoneyManagementVariant = (index: number, field: keyof MoneyManagementRequest, value: any) => {
setMoneyManagementVariants(prev => prev.map((variant, i) =>
i === index ? {
...variant,
moneyManagement: {
...variant.moneyManagement!,
[field]: value
}
} : variant
));
};
// Handle ticker selection
const handleTickerToggle = (ticker: Ticker) => {
setSelectedTickers(prev => {
const isSelected = prev.includes(ticker);
if (isSelected) {
return prev.filter(t => t !== ticker);
} else {
return [...prev, ticker];
}
});
};
// Create bundle backtest request
const handleCreateBundle = async () => {
if (!strategyName || !selectedAccount || selectedTickers.length === 0) {
new Toast('Please fill in all required fields', false);
return;
}
if (!scenario) {
new Toast('Please create a scenario with indicators', false);
return;
}
const universalConfig: BundleBacktestUniversalConfig = {
accountName: selectedAccount,
timeframe: selectedTimeframe,
isForWatchingOnly: false,
botTradingBalance: startingCapital,
botName: strategyName,
flipPosition: flipPosition,
cooldownPeriod: cooldownPeriod,
maxLossStreak: maxLossStreak,
scenario: scenario ? {
name: scenario.name || 'Custom Scenario',
indicators: (scenario.indicators || []).map(indicator => ({
name: indicator.name || 'Indicator',
type: indicator.type || IndicatorType.EmaCross,
signalType: indicator.signalType || SignalType.Signal,
period: indicator.period || 14,
fastPeriods: indicator.fastPeriods || 12,
slowPeriods: indicator.slowPeriods || 26,
signalPeriods: indicator.signalPeriods || 9,
multiplier: indicator.multiplier || 3.0,
stochPeriods: indicator.stochPeriods || 14,
smoothPeriods: indicator.smoothPeriods || 3,
cyclePeriods: indicator.cyclePeriods || 10
})),
loopbackPeriod: scenario.loopbackPeriod || 1
} : undefined,
maxPositionTimeHours: maxPositionTime > 0 ? maxPositionTime : null,
closeEarlyWhenProfitable: closeEarlyWhenProfitable,
flipOnlyWhenInProfit: flipOnlyWhenInProfit,
useSynthApi: false,
useForPositionSizing: true,
useForSignalFiltering: true,
useForDynamicStopLoss: true,
watchOnly: false,
save: true,
withCandles: false
};
const request: RunBundleBacktestRequest = {
name: strategyName,
universalConfig,
dateTimeRanges,
moneyManagementVariants,
tickerVariants: selectedTickers
};
try {
await onCreateBundle?.(request);
new Toast('Bundle backtest request created successfully!', true);
onClose();
} catch (error) {
new Toast('Failed to create bundle backtest request', false);
}
};
// Existing bundle viewing logic
const {
data: queryBacktests,
isLoading,
@@ -28,8 +285,7 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
queryKey: ['bundle-backtests', bundle?.requestId],
queryFn: async () => {
if (!open || !bundle) return [];
const client = new BacktestClient({} as any, apiUrl);
const res = await client.backtest_GetBacktestsByRequestId(bundle.requestId);
const res = await backtestClient.backtest_GetBacktestsByRequestId(bundle.requestId);
if (!res) return [];
return res.map((b: any) => {
// Map enums for ticker and timeframe
@@ -61,11 +317,12 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
enabled: !!open && !!bundle,
refetchOnWindowFocus: false,
});
useEffect(() => {
if (queryBacktests) setBacktests(queryBacktests);
}, [queryBacktests]);
// SignalR live updates
// SignalR live updates for existing bundles
useEffect(() => {
if (!open || !bundle) return;
if (bundle.status !== 'Pending' && bundle.status !== 'Running') return;
@@ -132,26 +389,471 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({ open, onClose,
};
}, [open, bundle, apiUrl]);
if (!open || !bundle) return null;
if (!open) return null;
// If viewing an existing bundle
if (bundle) {
return (
<div className="modal modal-open">
<div className="modal-box max-w-4xl">
<h3 className="font-bold text-lg mb-2">Bundle: {bundle.name}</h3>
<div className="mb-2 text-sm">
<div><b>Request ID:</b> <span className="font-mono text-xs">{bundle.requestId}</span></div>
<div><b>Status:</b> <span className={`badge badge-sm ml-1`}>{bundle.status}</span></div>
<div><b>Created:</b> {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}</div>
<div><b>Completed:</b> {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}</div>
</div>
<div className="divider">Backtest Results</div>
{isLoading ? (
<div>Loading backtests...</div>
) : queryError ? (
<div className="text-error">{(queryError as any)?.message || 'Failed to fetch backtests'}</div>
) : (
<BacktestTable list={backtests} />
)}
<div className="modal-action">
<button className="btn" onClick={onClose}>Close</button>
</div>
</div>
</div>
);
}
// Create new bundle form
return (
<div className="modal modal-open">
<div className="modal-box max-w-4xl">
<h3 className="font-bold text-lg mb-2">Bundle: {bundle.name}</h3>
<div className="mb-2 text-sm">
<div><b>Request ID:</b> <span className="font-mono text-xs">{bundle.requestId}</span></div>
<div><b>Status:</b> <span className={`badge badge-sm ml-1`}>{bundle.status}</span></div>
<div><b>Created:</b> {bundle.createdAt ? new Date(bundle.createdAt).toLocaleString() : '-'}</div>
<div><b>Completed:</b> {bundle.completedAt ? new Date(bundle.completedAt).toLocaleString() : '-'}</div>
<div className="modal-box max-w-6xl">
<h3 className="font-bold text-lg mb-6">Create Bundle Backtest</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Left Column - Strategy Configuration */}
<div className="space-y-6">
{/* Strategy Name */}
<FormInput label="Name your strategy" htmlFor="strategyName">
<input
type="text"
className="input input-bordered w-full"
placeholder="Road to $100k..."
value={strategyName}
onChange={(e) => setStrategyName(e.target.value)}
/>
</FormInput>
{/* Account Selection */}
<FormInput label="Select Account" htmlFor="account">
<select
className="select select-bordered w-full"
value={selectedAccount}
onChange={(e) => setSelectedAccount(e.target.value)}
>
<option value="" disabled>Select an account</option>
{accounts?.map((account) => (
<option key={account.name} value={account.name}>
{account.name}
</option>
))}
</select>
</FormInput>
{/* Scenario Builder */}
<div>
<h4 className="font-semibold mb-2">Build your trading scenario</h4>
<p className="text-sm text-gray-600 mb-4">
Create a custom scenario with indicators and parameters for your automated trading strategy.
</p>
<CustomScenario
onCreateScenario={setCustomScenario}
showCustomScenario={true}
/>
</div>
{/* Asset Selection */}
<FormInput label="Select asset(s)" htmlFor="tickers">
<p className="text-sm text-gray-600 mb-2">Select what your agent trades.</p>
<div className="grid grid-cols-3 gap-2 max-h-40 overflow-y-auto">
{tickers?.map((tickerInfo) => (
<button
key={tickerInfo.ticker}
className={`btn btn-sm ${
selectedTickers.includes(tickerInfo.ticker!)
? 'btn-primary'
: 'btn-outline'
}`}
onClick={() => handleTickerToggle(tickerInfo.ticker!)}
>
{tickerInfo.ticker}
</button>
))}
</div>
</FormInput>
{/* Timeframe Selection */}
<FormInput label="Select timeframe" htmlFor="timeframe">
<p className="text-sm text-gray-600 mb-2">This sets how often your strategy reads and reacts to market data</p>
<select
className="select select-bordered w-full"
value={selectedTimeframe}
onChange={(e) => setSelectedTimeframe(e.target.value as Timeframe)}
>
{Object.values(Timeframe).map((tf) => (
<option key={tf} value={tf}>{tf}</option>
))}
</select>
</FormInput>
{/* Money Management Variants */}
<div>
<h4 className="font-semibold mb-2">Choose your money management approach(s)</h4>
<p className="text-sm text-gray-600 mb-4">
Select the approach that fits your goals and keep your strategy's risk in check.
</p>
{moneyManagementVariants.map((variant, index) => (
<div key={index} className="card bg-base-200 p-4 mb-4">
<div className="flex justify-between items-center mb-3">
<h5 className="font-medium">Money Management {index + 1}</h5>
{moneyManagementVariants.length > 1 && (
<button
className="btn btn-error btn-sm"
onClick={() => removeMoneyManagementVariant(index)}
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-3 gap-4">
<FormInput label="Leverage" htmlFor={`leverage-${index}`}>
<input
type="number"
className="input input-bordered w-full"
value={variant.moneyManagement?.leverage || 1}
onChange={(e) => updateMoneyManagementVariant(index, 'leverage', parseFloat(e.target.value))}
min="1"
step="0.1"
/>
</FormInput>
<FormInput label="TP %" htmlFor={`takeProfit-${index}`}>
<input
type="number"
className="input input-bordered w-full"
value={variant.moneyManagement?.takeProfit || 0}
onChange={(e) => updateMoneyManagementVariant(index, 'takeProfit', parseFloat(e.target.value))}
min="0"
step="0.01"
/>
</FormInput>
<FormInput label="SL %" htmlFor={`stopLoss-${index}`}>
<input
type="number"
className="input input-bordered w-full"
value={variant.moneyManagement?.stopLoss || 0}
onChange={(e) => updateMoneyManagementVariant(index, 'stopLoss', parseFloat(e.target.value))}
min="0"
step="0.01"
/>
</FormInput>
</div>
</div>
))}
<button
className="btn btn-outline btn-sm"
onClick={addMoneyManagementVariant}
>
+ Add variant
</button>
</div>
</div>
{/* Right Column - Test Period & Backtest */}
<div className="space-y-6">
{/* Test Period */}
<div>
<h4 className="font-semibold mb-2">Select the test period</h4>
<p className="text-sm text-gray-600 mb-4">
Pick a historical range to evaluate your strategy.
</p>
<div className="flex justify-between items-center mb-4">
<label className="label">Date range</label>
<button
className="btn btn-outline btn-sm"
onClick={addDateTimeRange}
>
+ Add variant
</button>
</div>
{dateTimeRanges.map((range, index) => (
<div key={index} className="card bg-base-200 p-4 mb-4">
<div className="flex justify-between items-center mb-3">
<h5 className="font-medium">Date Range {index + 1}</h5>
{dateTimeRanges.length > 1 && (
<button
className="btn btn-error btn-sm"
onClick={() => removeDateTimeRange(index)}
>
Remove
</button>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<FormInput label="Start Date" htmlFor={`startDate-${index}`}>
<input
type="date"
className="input input-bordered w-full"
value={range.startDate.toISOString().split('T')[0]}
onChange={(e) => updateDateTimeRange(index, 'startDate', new Date(e.target.value))}
/>
</FormInput>
<FormInput label="End Date" htmlFor={`endDate-${index}`}>
<input
type="date"
className="input input-bordered w-full"
value={range.endDate.toISOString().split('T')[0]}
onChange={(e) => updateDateTimeRange(index, 'endDate', new Date(e.target.value))}
/>
</FormInput>
</div>
</div>
))}
</div>
{/* Advanced Parameters */}
<div className="collapse collapse-arrow bg-base-200">
<input type="checkbox" />
<div className="collapse-title font-medium flex items-center justify-between">
<div>
<h4 className="font-semibold">Advanced Parameters</h4>
<p className="text-sm text-gray-600 font-normal">
Refine your strategy with additional limits.
</p>
</div>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
<div className="collapse-content">
<div className="space-y-6">
{/* Cooldown Period */}
<div className="flex items-center justify-between py-3 border-b border-base-300">
<label className="label-text font-medium text-base">Cooldown Period</label>
<div className="flex items-center">
<button
type="button"
className="btn btn-sm btn-outline rounded-r-none"
onClick={() => setCooldownPeriod(Math.max(0, cooldownPeriod - 1))}
>
-
</button>
<input
type="number"
className="input input-bordered rounded-none text-center w-20"
value={cooldownPeriod}
onChange={(e) => setCooldownPeriod(Math.max(0, parseInt(e.target.value) || 0))}
min="0"
/>
<button
type="button"
className="btn btn-sm btn-outline rounded-l-none"
onClick={() => setCooldownPeriod(cooldownPeriod + 1)}
>
+
</button>
</div>
</div>
{/* Max Loss Streak */}
<div className="flex items-center justify-between py-3 border-b border-base-300">
<label className="label-text font-medium text-base">Max Loss Streak</label>
<div className="flex items-center">
<button
type="button"
className="btn btn-sm btn-outline rounded-r-none"
onClick={() => setMaxLossStreak(Math.max(0, maxLossStreak - 1))}
>
-
</button>
<input
type="number"
className="input input-bordered rounded-none text-center w-20"
value={maxLossStreak}
onChange={(e) => setMaxLossStreak(Math.max(0, parseInt(e.target.value) || 0))}
min="0"
/>
<button
type="button"
className="btn btn-sm btn-outline rounded-l-none"
onClick={() => setMaxLossStreak(maxLossStreak + 1)}
>
+
</button>
</div>
</div>
{/* Max Position Time */}
<div className="flex items-center justify-between py-3 border-b border-base-300">
<label className="label-text font-medium text-base">Max Position Time</label>
<div className="flex items-center">
<button
type="button"
className="btn btn-sm btn-outline rounded-r-none"
onClick={() => setMaxPositionTime(Math.max(0, maxPositionTime - 1))}
>
-
</button>
<input
type="number"
className="input input-bordered rounded-none text-center w-20"
value={maxPositionTime}
onChange={(e) => setMaxPositionTime(Math.max(0, parseInt(e.target.value) || 0))}
min="0"
/>
<button
type="button"
className="btn btn-sm btn-outline rounded-l-none"
onClick={() => setMaxPositionTime(maxPositionTime + 1)}
>
+
</button>
</div>
</div>
{/* Toggle Switches */}
<div className="space-y-4">
{/* Position Flipping */}
<div className="form-control">
<div className="flex items-center justify-between">
<div className="flex-1">
<label className="label-text font-medium">Position flipping</label>
<p className="text-sm text-gray-600">
When this switch is on, the bot can flip between long and short positions as signals change - an aggressive style.
</p>
</div>
<input
type="checkbox"
className="toggle toggle-primary"
checked={flipPosition}
onChange={(e) => setFlipPosition(e.target.checked)}
/>
</div>
</div>
{/* Flip Only When In Profit */}
<div className="form-control">
<div className="flex items-center justify-between">
<div className="flex-1">
<label className="label-text font-medium">Flip only when in profit</label>
<p className="text-sm text-gray-600">
When this switch is on, the bot flips sides only if the current position is profitable, limiting flips during drawdowns.
</p>
</div>
<input
type="checkbox"
className="toggle toggle-primary"
checked={flipOnlyWhenInProfit}
onChange={(e) => setFlipOnlyWhenInProfit(e.target.checked)}
/>
</div>
</div>
{/* Close Early When Profitable */}
<div className="form-control">
<div className="flex items-center justify-between">
<div className="flex-1">
<label className="label-text font-medium">Close early when profitable</label>
<p className="text-sm text-gray-600">
When this switch is on, the bot exits positions as soon as they turn profitable, locking in gains sooner.
</p>
</div>
<input
type="checkbox"
className="toggle toggle-primary"
checked={closeEarlyWhenProfitable}
onChange={(e) => setCloseEarlyWhenProfitable(e.target.checked)}
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Starting Capital */}
<FormInput label="Starting Capital" htmlFor="startingCapital">
<input
type="number"
className="input input-bordered w-full"
value={startingCapital}
onChange={(e) => setStartingCapital(parseFloat(e.target.value))}
min="1"
step="0.01"
/>
</FormInput>
{/* Backtest Cart */}
<div className="card bg-base-200 p-6">
<div className="flex items-center mb-4">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" />
</svg>
<h4 className="font-semibold">Backtest Cart</h4>
</div>
<p className="text-sm text-gray-600 mb-4">
A summary of your strategy creation.
</p>
<div className="space-y-2 mb-4">
<div className="flex justify-between">
<span>Total number of backtests</span>
<span className="font-medium">{totalBacktests}</span>
</div>
<div className="flex justify-between">
<span>Total number of credits used</span>
<span className="font-medium flex items-center">
{totalBacktests}
<svg className="w-4 h-4 ml-1 text-orange-500" fill="currentColor" viewBox="0 0 20 20">
<circle cx="10" cy="10" r="8" />
</svg>
</span>
</div>
<div className="flex justify-between">
<span>Estimated time</span>
<span className="font-medium">
{estimatedTimeSeconds < 60
? `${estimatedTimeSeconds}s`
: estimatedTimeSeconds < 3600
? `${Math.ceil(estimatedTimeSeconds / 60)}m`
: `${Math.ceil(estimatedTimeSeconds / 3600)}h ${Math.ceil((estimatedTimeSeconds % 3600) / 60)}m`
}
</span>
</div>
</div>
<button
className="btn btn-primary w-full mb-4"
onClick={handleCreateBundle}
disabled={!strategyName || !selectedAccount || selectedTickers.length === 0 || !scenario}
>
Run Backtest
</button>
<div className="flex justify-between">
<button className="btn btn-outline btn-sm">
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4 4a2 2 0 00-2 2v8a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2H4zm12 12V6H4v10h12zM8 8a1 1 0 000 2h4a1 1 0 100-2H8z" clipRule="evenodd" />
</svg>
Save this backtest
</button>
<button className="btn btn-ghost btn-sm">
Clear all
</button>
</div>
</div>
</div>
</div>
<div className="divider">Backtest Results</div>
{isLoading ? (
<div>Loading backtests...</div>
) : queryError ? (
<div className="text-error">{(queryError as any)?.message || 'Failed to fetch backtests'}</div>
) : (
<BacktestTable list={backtests} />
)}
<div className="modal-action">
<button className="btn" onClick={onClose}>Close</button>
</div>

View File

@@ -1,476 +1,11 @@
import React, {useEffect, useState} from 'react';
import {AccountClient, BacktestClient} from '../../generated/ManagingApi';
import type {
MoneyManagementRequest,
RunBacktestRequest,
ScenarioRequest,
TradingBotConfigRequest,
} from '../../generated/ManagingApiTypes';
import {Ticker, Timeframe} from '../../generated/ManagingApiTypes';
import CustomScenario from '../../components/organism/CustomScenario/CustomScenario';
import {useCustomScenario} from '../../app/store/customScenario';
import useApiUrlStore from '../../app/store/apiStore';
import Toast from '../../components/mollecules/Toast/Toast';
import React from 'react';
import BundleRequestsTable from './bundleRequestsTable';
import {useQuery} from '@tanstack/react-query';
// Placeholder types (replace with your actual types)
type Indicator = { name: string; params?: Record<string, any> };
type MoneyManagementVariant = { leverage: number; tp: number; sl: number };
type TimeRangeVariant = { start: string; end: string };
type Asset = 'BTC' | 'ETH' | 'GMX';
const allAssets: Asset[] = ['BTC', 'ETH', 'GMX'];
const allIndicators: Indicator[] = [
{ name: 'EMA Cross' },
{ name: 'MACD Cross' },
{ name: 'SuperTrend', params: { period: 12, multiplier: 4 } },
{ name: 'EMA Trend' },
{ name: 'Chandelier Exit' },
];
const allTimeframes = [
{ label: '5 minutes', value: '5m' },
{ label: '15 minutes', value: '15m' },
{ label: '1 hour', value: '1h' },
{ label: '4 hours', value: '4h' },
{ label: '1 day', value: '1d' },
];
const tickerMap: Record<string, Ticker> = {
BTC: Ticker.BTC,
ETH: Ticker.ETH,
GMX: Ticker.GMX,
};
const timeframeMap: Record<string, Timeframe> = {
'5m': Timeframe.FiveMinutes,
'15m': Timeframe.FifteenMinutes,
'1h': Timeframe.OneHour,
'4h': Timeframe.FourHour,
'1d': Timeframe.OneDay,
};
const BacktestBundleForm: React.FC = () => {
const {apiUrl} = useApiUrlStore()
// API clients
const accountClient = new AccountClient({}, apiUrl);
// Data fetching
const { data: accounts, isSuccess } = useQuery({
queryFn: async () => {
const fetchedAccounts = await accountClient.account_GetAccounts();
return fetchedAccounts;
},
queryKey: ['accounts'],
});
// Form state
const [strategyName, setStrategyName] = useState('');
const [loopback, setLoopback] = useState(14);
// Remove selectedIndicators, use scenario from store
const [selectedAssets, setSelectedAssets] = useState<Asset[]>([]);
const [timeframe, setTimeframe] = useState('5m');
const [moneyManagementVariants, setMoneyManagementVariants] = useState<MoneyManagementVariant[]>([
{ leverage: 2, tp: 1.5, sl: 1 },
]);
const [timeRangeVariants, setTimeRangeVariants] = useState<TimeRangeVariant[]>([
{ start: '', end: '' },
]);
const [cooldown, setCooldown] = useState(0);
const [maxLossStreak, setMaxLossStreak] = useState(0);
const [maxPositionTime, setMaxPositionTime] = useState(0);
const [positionFlipping, setPositionFlipping] = useState(false);
const [flipOnlyInProfit, setFlipOnlyInProfit] = useState(false);
const [closeEarly, setCloseEarly] = useState(false);
const [startingCapital, setStartingCapital] = useState(10000);
const [accountName, setAccountName] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const { scenario, setCustomScenario } = useCustomScenario();
// Set account name when accounts are loaded
useEffect(() => {
if (accounts && accounts.length > 0 && !accountName) {
setAccountName(accounts[0].name);
}
}, [accounts, accountName]);
// Placeholder for cart summary
const totalBacktests =
moneyManagementVariants.length * timeRangeVariants.length * (selectedAssets.length || 1);
// Handlers (add/remove variants, select assets/indicators, etc.)
// ...
// Generate all combinations of variants using scenario from store
const generateRequests = (): RunBacktestRequest[] => {
const requests: RunBacktestRequest[] = [];
if (!scenario) return requests;
selectedAssets.forEach(asset => {
moneyManagementVariants.forEach(mm => {
timeRangeVariants.forEach(tr => {
const mmReq: MoneyManagementRequest = {
name: `${strategyName}-MM`,
leverage: mm.leverage,
takeProfit: mm.tp,
stopLoss: mm.sl,
timeframe: timeframeMap[timeframe],
};
const config: TradingBotConfigRequest = {
accountName: accountName,
ticker: tickerMap[asset],
timeframe: timeframeMap[timeframe],
isForWatchingOnly: false,
botTradingBalance: startingCapital,
name: `${strategyName} - ${asset}`,
flipPosition: positionFlipping,
cooldownPeriod: cooldown,
maxLossStreak: maxLossStreak,
scenario: scenario as ScenarioRequest,
moneyManagement: mmReq,
maxPositionTimeHours: maxPositionTime,
closeEarlyWhenProfitable: closeEarly,
flipOnlyWhenInProfit: flipOnlyInProfit,
useSynthApi: false,
useForPositionSizing: true,
useForSignalFiltering: true,
useForDynamicStopLoss: true,
};
requests.push({
config,
startDate: tr.start ? new Date(tr.start) : undefined,
endDate: tr.end ? new Date(tr.end) : undefined,
save: false,
withCandles: false,
});
});
});
});
return requests;
};
// API call
const handleRunBundle = async () => {
setLoading(true);
setError(null);
setSuccess(null);
const toast = new Toast('Starting bundle backtest...', true);
try {
const client = new BacktestClient({} as any, apiUrl);
const requests = generateRequests();
if (!strategyName) throw new Error('Strategy name is required');
if (!accountName) throw new Error('Account selection is required');
if (requests.length === 0) throw new Error('No backtest variants to run');
await client.backtest_RunBundle({ name: strategyName, requests });
setSuccess('Bundle backtest started successfully!');
toast.update('success', 'Bundle backtest started successfully!');
} catch (e: any) {
setError(e.message || 'Failed to start bundle backtest');
toast.update('error', e.message || 'Failed to start bundle backtest');
} finally {
setLoading(false);
}
};
return (
<div className="p-10 max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Bundle Backtest</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Left column: Main form */}
<div>
{/* Name your strategy */}
<div className="mb-4">
<label className="label">Name your strategy</label>
<input
className="input input-bordered w-full"
value={strategyName}
onChange={e => setStrategyName(e.target.value)}
placeholder="Road to $100k..."
/>
</div>
{/* Select account */}
<div className="mb-4">
<label className="label">Select account</label>
<select
className="select select-bordered w-full"
value={accountName}
onChange={e => setAccountName(e.target.value)}
disabled={!accounts || accounts.length === 0}
>
{!accounts || accounts.length === 0 ? (
<option value="">Loading accounts...</option>
) : (
accounts.map(account => (
<option key={account.name} value={account.name}>
{account.name}
</option>
))
)}
</select>
</div>
{/* Scenario/Indicators section */}
<div className="mb-4">
<CustomScenario
onCreateScenario={setCustomScenario}
showCustomScenario={true}
/>
</div>
{/* Select asset(s) */}
<div className="mb-4">
<label className="label">Select asset(s)</label>
<div className="flex flex-wrap gap-2">
{allAssets.map(asset => (
<button
key={asset}
className={`btn btn-sm ${selectedAssets.includes(asset) ? 'btn-primary' : 'btn-outline'}`}
onClick={e => {
e.preventDefault();
setSelectedAssets(sel =>
sel.includes(asset)
? sel.filter(a => a !== asset)
: [...sel, asset]
);
}}
>
{asset}
</button>
))}
</div>
</div>
{/* Select timeframe */}
<div className="mb-4">
<label className="label">Select timeframe</label>
<select
className="select select-bordered w-full"
value={timeframe}
onChange={e => setTimeframe(e.target.value)}
>
{allTimeframes.map(tf => (
<option key={tf.value} value={tf.value}>
{tf.label}
</option>
))}
</select>
</div>
{/* Money management variants */}
<div className="mb-4">
<label className="label">Choose your money management approach(s)</label>
{moneyManagementVariants.map((mm, idx) => (
<div key={idx} className="flex gap-2 items-center mb-2">
<input
type="number"
className="input input-bordered w-16"
value={mm.leverage}
onChange={e => {
const v = [...moneyManagementVariants];
v[idx].leverage = Number(e.target.value);
setMoneyManagementVariants(v);
}}
placeholder="Leverage"
min={1}
/>
<input
type="number"
className="input input-bordered w-16"
value={mm.tp}
onChange={e => {
const v = [...moneyManagementVariants];
v[idx].tp = Number(e.target.value);
setMoneyManagementVariants(v);
}}
placeholder="TP %"
min={0}
/>
<input
type="number"
className="input input-bordered w-16"
value={mm.sl}
onChange={e => {
const v = [...moneyManagementVariants];
v[idx].sl = Number(e.target.value);
setMoneyManagementVariants(v);
}}
placeholder="SL %"
min={0}
/>
<button
className="btn btn-xs btn-error"
onClick={e => {
e.preventDefault();
setMoneyManagementVariants(v => v.filter((_, i) => i !== idx));
}}
disabled={moneyManagementVariants.length === 1}
>
</button>
</div>
))}
<button
className="btn btn-sm btn-outline"
onClick={e => {
e.preventDefault();
setMoneyManagementVariants(v => [...v, { leverage: 1, tp: 1, sl: 1 }]);
}}
>
+ Add variant
</button>
</div>
</div>
{/* Right column: Test period, advanced params, capital, cart */}
<div>
{/* Test period variants */}
<div className="mb-4">
<label className="label">Select the test period</label>
{timeRangeVariants.map((tr, idx) => (
<div key={idx} className="flex gap-2 items-center mb-2">
<input
type="date"
className="input input-bordered"
value={tr.start}
onChange={e => {
const v = [...timeRangeVariants];
v[idx].start = e.target.value;
setTimeRangeVariants(v);
}}
/>
<input
type="date"
className="input input-bordered"
value={tr.end}
onChange={e => {
const v = [...timeRangeVariants];
v[idx].end = e.target.value;
setTimeRangeVariants(v);
}}
/>
<button
className="btn btn-xs btn-error"
onClick={e => {
e.preventDefault();
setTimeRangeVariants(v => v.filter((_, i) => i !== idx));
}}
disabled={timeRangeVariants.length === 1}
>
</button>
</div>
))}
<button
className="btn btn-sm btn-outline"
onClick={e => {
e.preventDefault();
setTimeRangeVariants(v => [...v, { start: '', end: '' }]);
}}
>
+ Add variant
</button>
</div>
{/* Advanced Parameters */}
<div className="mb-4">
<div className="collapse collapse-arrow bg-base-200">
<input type="checkbox" />
<div className="collapse-title font-medium">Advanced Parameters</div>
<div className="collapse-content">
<div className="flex gap-2 mb-2">
<input
type="number"
className="input input-bordered w-20"
value={cooldown}
onChange={e => setCooldown(Number(e.target.value))}
placeholder="Cooldown"
/>
<input
type="number"
className="input input-bordered w-20"
value={maxLossStreak}
onChange={e => setMaxLossStreak(Number(e.target.value))}
placeholder="Max Loss Streak"
/>
<input
type="number"
className="input input-bordered w-20"
value={maxPositionTime}
onChange={e => setMaxPositionTime(Number(e.target.value))}
placeholder="Max Position Time"
/>
</div>
<div className="form-control mb-2">
<label className="cursor-pointer label">
<span className="label-text">Position flipping</span>
<input
type="checkbox"
className="toggle"
checked={positionFlipping}
onChange={e => setPositionFlipping(e.target.checked)}
/>
</label>
</div>
<div className="form-control mb-2">
<label className="cursor-pointer label">
<span className="label-text">Flip only when in profit</span>
<input
type="checkbox"
className="toggle"
checked={flipOnlyInProfit}
onChange={e => setFlipOnlyInProfit(e.target.checked)}
/>
</label>
</div>
<div className="form-control mb-2">
<label className="cursor-pointer label">
<span className="label-text">Close early when profitable</span>
<input
type="checkbox"
className="toggle"
checked={closeEarly}
onChange={e => setCloseEarly(e.target.checked)}
/>
</label>
</div>
</div>
</div>
</div>
{/* Starting Capital */}
<div className="mb-4">
<label className="label">Starting Capital</label>
<input
type="number"
className="input input-bordered w-full"
value={startingCapital}
onChange={e => setStartingCapital(Number(e.target.value))}
min={1}
/>
</div>
{/* Backtest Cart */}
<div className="mb-4 bg-base-200 rounded-lg p-4">
<div className="font-bold mb-2">Backtest Cart</div>
<div>Total number of backtests: <span className="font-mono">{totalBacktests}</span></div>
<div>Total number of credits used: <span className="font-mono">{totalBacktests}</span></div>
<div>Estimated time: <span className="font-mono">~ 1 min</span></div>
<button className="btn btn-primary w-full mt-4" onClick={handleRunBundle}>
Run Backtest
</button>
<button className="btn btn-outline w-full mt-2">Save this backtest</button>
<button className="btn btn-ghost w-full mt-2">Clear all</button>
</div>
</div>
</div>
<div className="mt-10">
<BundleRequestsTable />
</div>
<BundleRequestsTable />
</div>
);
};

View File

@@ -148,12 +148,28 @@ const BundleRequestsTable = () => {
return (
<div className="w-full">
<h2 className="text-lg font-bold mb-2">Bundle Backtest Requests</h2>
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-bold">Bundle Backtest Requests</h2>
<button
className="btn btn-primary"
onClick={() => {
setSelectedBundle(null);
setModalOpen(true);
}}
>
Create New Bundle
</button>
</div>
<Table columns={columns} data={data} showPagination={true} />
<BundleRequestModal
open={modalOpen}
onClose={() => setModalOpen(false)}
bundle={selectedBundle}
onCreateBundle={async (request) => {
const client = new BacktestClient({} as any, apiUrl);
await client.backtest_RunBundle(request);
fetchData(); // Refresh the table
}}
/>
</div>
);