Add cancellation token support to backtest execution and update progress handling

This commit is contained in:
2025-11-13 18:05:55 +07:00
parent 17d904c445
commit 1f7d914625
4 changed files with 16 additions and 15 deletions

View File

@@ -125,6 +125,7 @@ public class BacktestExecutor
/// <param name="bundleRequestId">Optional bundle request ID to update with backtest result</param> /// <param name="bundleRequestId">Optional bundle request ID to update with backtest result</param>
/// <param name="metadata">Additional metadata</param> /// <param name="metadata">Additional metadata</param>
/// <param name="progressCallback">Optional callback for progress updates (0-100)</param> /// <param name="progressCallback">Optional callback for progress updates (0-100)</param>
/// <param name="cancellationToken">Cancellation token to stop execution</param>
/// <returns>The lightweight backtest result</returns> /// <returns>The lightweight backtest result</returns>
public async Task<LightBacktest> ExecuteAsync( public async Task<LightBacktest> ExecuteAsync(
TradingBotConfig config, TradingBotConfig config,
@@ -135,7 +136,8 @@ public class BacktestExecutor
string requestId = null, string requestId = null,
Guid? bundleRequestId = null, Guid? bundleRequestId = null,
object metadata = null, object metadata = null,
Func<int, Task> progressCallback = null) Func<int, Task> progressCallback = null,
CancellationToken cancellationToken = default)
{ {
if (candles == null || candles.Count == 0) if (candles == null || candles.Count == 0)
{ {
@@ -247,6 +249,9 @@ public class BacktestExecutor
// Process all candles with optimized rolling window approach // Process all candles with optimized rolling window approach
foreach (var candle in orderedCandles) foreach (var candle in orderedCandles)
{ {
// Check for cancellation (timeout or shutdown)
cancellationToken.ThrowIfCancellationRequested();
// Add to HashSet for reuse // Add to HashSet for reuse
fixedCandles.Add(candle); fixedCandles.Add(candle);
tradingBot.LastCandle = candle; tradingBot.LastCandle = candle;

View File

@@ -65,7 +65,8 @@ public class BacktestExecutorAdapter : IBacktester
requestId, requestId,
bundleRequestId: null, bundleRequestId: null,
metadata, metadata,
progressCallback: null); progressCallback: null,
cancellationToken: default);
return result; return result;
} }
@@ -88,7 +89,8 @@ public class BacktestExecutorAdapter : IBacktester
requestId, requestId,
bundleRequestId: null, bundleRequestId: null,
metadata, metadata,
progressCallback: null); progressCallback: null,
cancellationToken: default);
return result; return result;
} }

View File

@@ -1057,7 +1057,9 @@ public class TradingBotFitness : IFitness
requestId: _request.RequestId, requestId: _request.RequestId,
bundleRequestId: null, // Genetic algorithm doesn't use bundle requests bundleRequestId: null, // Genetic algorithm doesn't use bundle requests
metadata: new GeneticBacktestMetadata(_geneticAlgorithm?.GenerationsNumber ?? 0, metadata: new GeneticBacktestMetadata(_geneticAlgorithm?.GenerationsNumber ?? 0,
_request.RequestId) _request.RequestId),
progressCallback: null,
cancellationToken: default
) )
).GetAwaiter().GetResult(); ).GetAwaiter().GetResult();
} }

View File

@@ -267,18 +267,9 @@ public class BacktestComputeWorker : BackgroundService
_jobProgressTrackers.TryAdd(job.Id, progressTracker); _jobProgressTrackers.TryAdd(job.Id, progressTracker);
// Progress callback that only updates in-memory progress (non-blocking) // Progress callback that only updates in-memory progress (non-blocking)
// Timeout is now enforced via CancellationToken, not by throwing in callback
Func<int, Task> progressCallback = (percentage) => Func<int, Task> progressCallback = (percentage) =>
{ {
// Check if job has been running too long
var elapsed = DateTime.UtcNow - jobStartTime;
if (elapsed.TotalMinutes > _options.JobTimeoutMinutes)
{
_logger.LogWarning(
"Job {JobId} has been running for {ElapsedMinutes} minutes, exceeding timeout of {TimeoutMinutes} minutes",
job.Id, elapsed.TotalMinutes, _options.JobTimeoutMinutes);
throw new TimeoutException($"Job exceeded timeout of {_options.JobTimeoutMinutes} minutes");
}
// Update progress in memory only - persistence happens in background // Update progress in memory only - persistence happens in background
progressTracker.UpdateProgress(percentage); progressTracker.UpdateProgress(percentage);
@@ -301,7 +292,8 @@ public class BacktestComputeWorker : BackgroundService
requestId: job.RequestId, requestId: job.RequestId,
bundleRequestId: job.BundleRequestId, bundleRequestId: job.BundleRequestId,
metadata: null, metadata: null,
progressCallback: progressCallback); progressCallback: progressCallback,
cancellationToken: linkedCts.Token);
} }
catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested && !cancellationToken.IsCancellationRequested) catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{ {