Add test for executor

This commit is contained in:
2025-11-11 02:15:57 +07:00
parent d02a07f86b
commit e8e2ec5a43
18 changed files with 81418 additions and 170 deletions

View File

@@ -54,6 +54,7 @@ public class BacktestExecutor
/// <param name="save">Whether to save the backtest result</param>
/// <param name="withCandles">Whether to include candles in the result</param>
/// <param name="requestId">The request ID to associate with this backtest</param>
/// <param name="bundleRequestId">Optional bundle request ID to update with backtest result</param>
/// <param name="metadata">Additional metadata</param>
/// <param name="progressCallback">Optional callback for progress updates (0-100)</param>
/// <returns>The lightweight backtest result</returns>
@@ -64,6 +65,7 @@ public class BacktestExecutor
bool save = false,
bool withCandles = false,
string requestId = null,
Guid? bundleRequestId = null,
object metadata = null,
Func<int, Task> progressCallback = null)
{
@@ -72,6 +74,9 @@ public class BacktestExecutor
throw new Exception("No candle to backtest");
}
// Start performance tracking
var backtestStartTime = DateTime.UtcNow;
// Ensure user has accounts loaded
if (user.Accounts == null || !user.Accounts.Any())
{
@@ -129,23 +134,64 @@ public class BacktestExecutor
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
var initialBalance = config.BotTradingBalance;
var fixedCandles = new HashSet<Candle>();
var lastProgressUpdate = DateTime.UtcNow;
const int progressUpdateIntervalMs = 1000; // Update progress every second
// Pre-allocate and populate candle structures for maximum performance
var orderedCandles = candles.OrderBy(c => c.Date).ToList();
// Process all candles
foreach (var candle in candles)
// Use optimized rolling window approach - TradingBox.GetSignal only needs last 600 candles
const int rollingWindowSize = 600;
var rollingCandles = new LinkedList<Candle>();
var fixedCandles = new HashSet<Candle>(rollingWindowSize);
var candlesProcessed = 0;
var lastProgressUpdate = DateTime.UtcNow;
const int progressUpdateIntervalMs = 5000; // Update progress every 5 seconds to reduce database load
const int walletCheckInterval = 10; // Check wallet balance every N candles instead of every candle
var lastWalletCheck = 0;
var lastWalletBalance = config.BotTradingBalance;
// Process all candles with optimized rolling window approach
foreach (var candle in orderedCandles)
{
// Maintain rolling window efficiently using LinkedList
rollingCandles.AddLast(candle);
fixedCandles.Add(candle);
if (rollingCandles.Count > rollingWindowSize)
{
var removedCandle = rollingCandles.First!.Value;
rollingCandles.RemoveFirst();
fixedCandles.Remove(removedCandle);
}
tradingBot.LastCandle = candle;
// Update signals manually only for backtesting
// Update signals and run trading logic with optimized rolling window
// For backtests, we can optimize by reducing async overhead
await tradingBot.UpdateSignals(fixedCandles);
await tradingBot.Run();
// Run with optimized backtest path (minimize async calls)
await RunOptimizedBacktestStep(tradingBot);
currentCandle++;
candlesProcessed++;
// Update progress callback if provided
// Optimized wallet balance check - only check every N candles and cache result
if (currentCandle - lastWalletCheck >= walletCheckInterval)
{
lastWalletBalance = tradingBot.WalletBalances.Values.LastOrDefault();
lastWalletCheck = currentCandle;
if (lastWalletBalance < Constants.GMX.Config.MinimumPositionAmount)
{
_logger.LogWarning(
"Backtest stopped early: Wallet balance fell below {MinimumPositionAmount} USDC (Current: {CurrentBalance:F2} USDC) at candle {CurrentCandle}/{TotalCandles} from {CandleDate}",
Constants.GMX.Config.MinimumPositionAmount, lastWalletBalance, currentCandle, totalCandles,
candle.Date.ToString("yyyy-MM-dd HH:mm"));
break;
}
}
// Update progress callback if provided (optimized frequency)
var currentPercentage = (currentCandle * 100) / totalCandles;
var timeSinceLastUpdate = (DateTime.UtcNow - lastProgressUpdate).TotalMilliseconds;
if (progressCallback != null && (timeSinceLastUpdate >= progressUpdateIntervalMs || currentPercentage >= lastLoggedPercentage + 10))
@@ -161,7 +207,7 @@ public class BacktestExecutor
lastProgressUpdate = DateTime.UtcNow;
}
// Log progress every 10%
// Log progress every 10% (reduced frequency)
if (currentPercentage >= lastLoggedPercentage + 10)
{
lastLoggedPercentage = currentPercentage;
@@ -169,21 +215,20 @@ public class BacktestExecutor
"Backtest progress: {Percentage}% ({CurrentCandle}/{TotalCandles} candles processed)",
currentPercentage, currentCandle, totalCandles);
}
// Check if wallet balance fell below 10 USDC and break if so
var currentWalletBalance = tradingBot.WalletBalances.Values.LastOrDefault();
if (currentWalletBalance < Constants.GMX.Config.MinimumPositionAmount)
{
_logger.LogWarning(
"Backtest stopped early: Wallet balance fell below {MinimumPositionAmount} USDC (Current: {CurrentBalance:F2} USDC) at candle {CurrentCandle}/{TotalCandles} from {CandleDate}",
Constants.GMX.Config.MinimumPositionAmount, currentWalletBalance, currentCandle, totalCandles,
candle.Date.ToString("yyyy-MM-dd HH:mm"));
break;
}
}
_logger.LogInformation("Backtest processing completed. Calculating final results...");
// Log performance metrics
var backtestEndTime = DateTime.UtcNow;
var totalExecutionTime = backtestEndTime - backtestStartTime;
var candlesPerSecond = totalCandles / totalExecutionTime.TotalSeconds;
_logger.LogInformation(
"Backtest performance metrics: {TotalCandles} candles processed in {ExecutionTime:F2}s ({CandlesPerSecond:F1} candles/sec)",
totalCandles, totalExecutionTime.TotalSeconds, candlesPerSecond);
// Calculate final results (using existing optimized methods)
var finalPnl = tradingBot.GetProfitAndLoss();
var winRate = tradingBot.GetWinRate();
var stats = TradingHelpers.GetStatistics(tradingBot.WalletBalances);
@@ -240,6 +285,12 @@ public class BacktestExecutor
if (save && user != null)
{
await _backtestRepository.InsertBacktestForUserAsync(user, result);
// Update bundle request if provided
if (bundleRequestId.HasValue)
{
await UpdateBundleRequestWithBacktestResult(user, bundleRequestId.Value, result);
}
}
// Send notification if backtest meets criteria
@@ -297,6 +348,118 @@ public class BacktestExecutor
return tradingBot;
}
/// <summary>
/// Optimized backtest step execution - delegate to standard Run but with backtest optimizations
/// </summary>
private async Task RunOptimizedBacktestStep(TradingBotBase tradingBot)
{
// Use the standard Run method but ensure it's optimized for backtests
await tradingBot.Run();
}
/// <summary>
/// Updates bundle request with the completed backtest result
/// </summary>
private async Task UpdateBundleRequestWithBacktestResult(User user, Guid bundleRequestId, Backtest backtest)
{
try
{
using var scope = _scopeFactory.CreateScope();
var backtestRepository = scope.ServiceProvider.GetRequiredService<IBacktestRepository>();
var jobRepository = scope.ServiceProvider.GetRequiredService<IJobRepository>();
var webhookService = scope.ServiceProvider.GetRequiredService<IWebhookService>();
// Get bundle request
var bundleRequest = backtestRepository.GetBundleBacktestRequestByIdForUser(user, bundleRequestId);
if (bundleRequest == null)
{
_logger.LogWarning("Bundle request {BundleRequestId} not found for user {UserId}", bundleRequestId, user.Id);
return;
}
var previousStatus = bundleRequest.Status;
// Get all jobs for this bundle to calculate progress
var jobs = await jobRepository.GetByBundleRequestIdAsync(bundleRequestId);
var completedJobs = jobs.Count(j => j.Status == JobStatus.Completed);
var failedJobs = jobs.Count(j => j.Status == JobStatus.Failed);
var runningJobs = jobs.Count(j => j.Status == JobStatus.Running);
var totalJobs = jobs.Count();
// Update bundle request progress
bundleRequest.CompletedBacktests = completedJobs;
bundleRequest.FailedBacktests = failedJobs;
// Update status based on job states
if (completedJobs + failedJobs == totalJobs)
{
// All jobs completed or failed
if (failedJobs == 0)
{
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
}
else if (completedJobs == 0)
{
bundleRequest.Status = BundleBacktestRequestStatus.Failed;
bundleRequest.ErrorMessage = "All backtests failed";
}
else
{
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
bundleRequest.ErrorMessage = $"{failedJobs} backtests failed";
}
bundleRequest.CompletedAt = DateTime.UtcNow;
bundleRequest.CurrentBacktest = null;
}
else if (runningJobs > 0)
{
// Some jobs still running
bundleRequest.Status = BundleBacktestRequestStatus.Running;
}
// Update results list with the new backtest ID
var resultsList = bundleRequest.Results?.ToList() ?? new List<string>();
if (!resultsList.Contains(backtest.Id))
{
resultsList.Add(backtest.Id);
bundleRequest.Results = resultsList;
}
await backtestRepository.UpdateBundleBacktestRequestAsync(bundleRequest);
// Send webhook notification if bundle request just completed
if (previousStatus != BundleBacktestRequestStatus.Completed &&
bundleRequest.Status == BundleBacktestRequestStatus.Completed &&
!string.IsNullOrEmpty(user.TelegramChannel))
{
var message = $"✅ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) completed successfully. " +
$"Completed: {completedJobs}/{totalJobs} backtests" +
(failedJobs > 0 ? $", Failed: {failedJobs}" : "") +
$". Results: {resultsList.Count} backtest(s) generated.";
await webhookService.SendMessage(message, user.TelegramChannel);
}
else if (previousStatus != BundleBacktestRequestStatus.Failed &&
bundleRequest.Status == BundleBacktestRequestStatus.Failed &&
!string.IsNullOrEmpty(user.TelegramChannel))
{
var message = $"❌ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) failed. " +
$"All {totalJobs} backtests failed. Error: {bundleRequest.ErrorMessage}";
await webhookService.SendMessage(message, user.TelegramChannel);
}
_logger.LogInformation(
"Updated bundle request {BundleRequestId} with backtest {BacktestId}: {Completed}/{Total} completed, {Failed} failed, {Running} running",
bundleRequestId, backtest.Id, completedJobs, totalJobs, failedJobs, runningJobs);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update bundle request {BundleRequestId} with backtest {BacktestId}", bundleRequestId, backtest.Id);
}
}
/// <summary>
/// Sends notification if backtest meets criteria
/// </summary>