Optimze worker for backtest

This commit is contained in:
2025-11-11 03:59:41 +07:00
parent 5a4cb670a5
commit 1d70355617
10 changed files with 138465 additions and 40 deletions

View File

@@ -33,6 +33,7 @@ public class SignalCache
{
return true;
}
signal = null;
return false;
}
@@ -151,13 +152,15 @@ public class BacktestExecutor
var initialMemory = GC.GetTotalMemory(false);
telemetry.MemoryUsageAtStart = initialMemory;
_logger.LogInformation("🚀 Backtest execution started - RequestId: {RequestId}, Candles: {CandleCount}, Memory: {MemoryMB:F2}MB",
_logger.LogInformation(
"🚀 Backtest execution started - RequestId: {RequestId}, Candles: {CandleCount}, Memory: {MemoryMB:F2}MB",
requestId ?? "N/A", candles.Count, initialMemory / 1024.0 / 1024.0);
// Ensure user has accounts loaded
if (user.Accounts == null || !user.Accounts.Any())
{
user.Accounts = (await _accountService.GetAccountsByUserAsync(user, hideSecrets: true, getBalance: false)).ToList();
user.Accounts = (await _accountService.GetAccountsByUserAsync(user, hideSecrets: true, getBalance: false))
.ToList();
}
// Create a fresh TradingBotBase instance for this backtest
@@ -186,24 +189,27 @@ public class BacktestExecutor
var scenario = config.Scenario.ToScenario();
// Calculate all indicator values once with all candles
preCalculatedIndicatorValues = await ServiceScopeHelpers.WithScopedService<ITradingService, Dictionary<IndicatorType, IndicatorsResultBase>>(
_scopeFactory,
async tradingService =>
{
return await tradingService.CalculateIndicatorsValuesAsync(scenario, candles);
});
preCalculatedIndicatorValues = await ServiceScopeHelpers
.WithScopedService<ITradingService, Dictionary<IndicatorType, IndicatorsResultBase>>(
_scopeFactory,
async tradingService =>
{
return await tradingService.CalculateIndicatorsValuesAsync(scenario, candles);
});
// Store pre-calculated values in trading bot for use during signal generation
tradingBot.PreCalculatedIndicatorValues = preCalculatedIndicatorValues;
telemetry.IndicatorPreCalculationTime = Stopwatch.GetElapsedTime(indicatorCalcStart);
_logger.LogInformation("✅ Successfully pre-calculated indicator values for {IndicatorCount} indicator types in {Duration:F2}ms",
_logger.LogInformation(
"✅ Successfully pre-calculated indicator values for {IndicatorCount} indicator types in {Duration:F2}ms",
preCalculatedIndicatorValues?.Count ?? 0, telemetry.IndicatorPreCalculationTime.TotalMilliseconds);
}
catch (Exception ex)
{
telemetry.IndicatorPreCalculationTime = Stopwatch.GetElapsedTime(indicatorCalcStart);
_logger.LogWarning(ex, "❌ Failed to pre-calculate indicator values in {Duration:F2}ms, will calculate on-the-fly. Error: {ErrorMessage}",
_logger.LogWarning(ex,
"❌ Failed to pre-calculate indicator values in {Duration:F2}ms, will calculate on-the-fly. Error: {ErrorMessage}",
telemetry.IndicatorPreCalculationTime.TotalMilliseconds, ex.Message);
// Continue with normal calculation if pre-calculation fails
preCalculatedIndicatorValues = null;
@@ -220,8 +226,7 @@ public class BacktestExecutor
// 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 rollingCandles = new List<Candle>(rollingWindowSize); // Pre-allocate capacity for better performance
var candlesProcessed = 0;
// Signal caching optimization - reduce signal update frequency for better performance
@@ -249,15 +254,13 @@ public class BacktestExecutor
Console.WriteLine("CONSOLE: About to start candle processing loop");
foreach (var candle in orderedCandles)
{
// Maintain rolling window efficiently using LinkedList
rollingCandles.AddLast(candle);
fixedCandles.Add(candle);
// Maintain rolling window efficiently using List
rollingCandles.Add(candle);
if (rollingCandles.Count > rollingWindowSize)
{
var removedCandle = rollingCandles.First!.Value;
rollingCandles.RemoveFirst();
fixedCandles.Remove(removedCandle);
// Remove oldest candle (first element) - O(n) but acceptable for small window
rollingCandles.RemoveAt(0);
}
tradingBot.LastCandle = candle;
@@ -267,11 +270,14 @@ public class BacktestExecutor
var shouldSkipSignalUpdate = ShouldSkipSignalUpdate(currentCandle, totalCandles);
if (currentCandle <= 5) // Debug first few candles
{
_logger.LogInformation("🔍 Candle {CurrentCandle}: shouldSkip={ShouldSkip}, totalCandles={Total}", currentCandle, shouldSkipSignalUpdate, totalCandles);
_logger.LogInformation("🔍 Candle {CurrentCandle}: shouldSkip={ShouldSkip}, totalCandles={Total}",
currentCandle, shouldSkipSignalUpdate, totalCandles);
}
if (!shouldSkipSignalUpdate)
{
// Convert to HashSet only when needed for GetSignal (it expects HashSet)
var fixedCandles = new HashSet<Candle>(rollingCandles);
var signalUpdateStart = Stopwatch.GetTimestamp();
await tradingBot.UpdateSignals(fixedCandles);
signalUpdateTotalTime += Stopwatch.GetElapsedTime(signalUpdateStart);
@@ -284,7 +290,9 @@ public class BacktestExecutor
// This saves ~1ms per skipped update and improves performance significantly
if (signalUpdateSkipCount <= 5) // Log first few skips for debugging
{
_logger.LogInformation("⏭️ Signal update skipped for candle {CurrentCandle} (total skipped: {SkipCount})", currentCandle, signalUpdateSkipCount);
_logger.LogInformation(
"⏭️ Signal update skipped for candle {CurrentCandle} (total skipped: {SkipCount})",
currentCandle, signalUpdateSkipCount);
}
}
@@ -318,7 +326,8 @@ public class BacktestExecutor
// 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))
if (progressCallback != null && (timeSinceLastUpdate >= progressUpdateIntervalMs ||
currentPercentage >= lastLoggedPercentage + 10))
{
var progressCallbackStart = Stopwatch.GetTimestamp();
try
@@ -330,6 +339,7 @@ public class BacktestExecutor
{
_logger.LogWarning(ex, "Error in progress callback");
}
progressCallbackTotalTime += Stopwatch.GetElapsedTime(progressCallbackStart);
lastProgressUpdate = DateTime.UtcNow;
}
@@ -461,12 +471,15 @@ public class BacktestExecutor
_logger.LogInformation(" • Candle Processing: {Time:F2}ms ({Percentage:F1}%)",
telemetry.CandleProcessingTime.TotalMilliseconds,
telemetry.CandleProcessingTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100);
_logger.LogInformation(" • Signal Updates: {Time:F2}ms ({Percentage:F1}%) - {Count} updates, {SkipCount} skipped ({Efficiency:F1}% efficiency)",
_logger.LogInformation(
" • Signal Updates: {Time:F2}ms ({Percentage:F1}%) - {Count} updates, {SkipCount} skipped ({Efficiency:F1}% efficiency)",
telemetry.SignalUpdateTime.TotalMilliseconds,
telemetry.SignalUpdateTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100,
telemetry.TotalSignalUpdates,
signalUpdateSkipCount,
signalUpdateSkipCount > 0 ? (double)signalUpdateSkipCount / (telemetry.TotalSignalUpdates + signalUpdateSkipCount) * 100 : 0);
signalUpdateSkipCount > 0
? (double)signalUpdateSkipCount / (telemetry.TotalSignalUpdates + signalUpdateSkipCount) * 100
: 0);
_logger.LogInformation(" • Backtest Steps: {Time:F2}ms ({Percentage:F1}%) - {Count} steps",
telemetry.BacktestStepTime.TotalMilliseconds,
telemetry.BacktestStepTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100,
@@ -480,16 +493,19 @@ public class BacktestExecutor
telemetry.ResultCalculationTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100);
// Performance insights
var signalUpdateAvg = telemetry.TotalSignalUpdates > 0 ?
telemetry.SignalUpdateTime.TotalMilliseconds / telemetry.TotalSignalUpdates : 0;
var backtestStepAvg = telemetry.TotalBacktestSteps > 0 ?
telemetry.BacktestStepTime.TotalMilliseconds / telemetry.TotalBacktestSteps : 0;
var signalUpdateAvg = telemetry.TotalSignalUpdates > 0
? telemetry.SignalUpdateTime.TotalMilliseconds / telemetry.TotalSignalUpdates
: 0;
var backtestStepAvg = telemetry.TotalBacktestSteps > 0
? telemetry.BacktestStepTime.TotalMilliseconds / telemetry.TotalBacktestSteps
: 0;
_logger.LogInformation("🔍 Performance Insights:");
_logger.LogInformation(" • Average Signal Update: {Avg:F2}ms per update", signalUpdateAvg);
_logger.LogInformation(" • Average Backtest Step: {Avg:F2}ms per step", backtestStepAvg);
_logger.LogInformation(" • Memory Efficiency: {Efficiency:F2}MB per 1000 candles",
(telemetry.PeakMemoryUsage - telemetry.MemoryUsageAtStart) / 1024.0 / 1024.0 / (telemetry.TotalCandlesProcessed / 1000.0));
(telemetry.PeakMemoryUsage - telemetry.MemoryUsageAtStart) / 1024.0 / 1024.0 /
(telemetry.TotalCandlesProcessed / 1000.0));
// Identify potential bottlenecks
var bottlenecks = new List<string>();
@@ -608,7 +624,8 @@ public class BacktestExecutor
var bundleRequest = backtestRepository.GetBundleBacktestRequestByIdForUser(user, bundleRequestId);
if (bundleRequest == null)
{
_logger.LogWarning("Bundle request {BundleRequestId} not found for user {UserId}", bundleRequestId, user.Id);
_logger.LogWarning("Bundle request {BundleRequestId} not found for user {UserId}", bundleRequestId,
user.Id);
return;
}
@@ -643,6 +660,7 @@ public class BacktestExecutor
bundleRequest.Status = BundleBacktestRequestStatus.Completed;
bundleRequest.ErrorMessage = $"{failedJobs} backtests failed";
}
bundleRequest.CompletedAt = DateTime.UtcNow;
bundleRequest.CurrentBacktest = null;
}
@@ -667,10 +685,11 @@ public class BacktestExecutor
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.";
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);
}
@@ -679,7 +698,7 @@ public class BacktestExecutor
!string.IsNullOrEmpty(user.TelegramChannel))
{
var message = $"❌ Bundle backtest '{bundleRequest.Name}' (ID: {bundleRequest.RequestId}) failed. " +
$"All {totalJobs} backtests failed. Error: {bundleRequest.ErrorMessage}";
$"All {totalJobs} backtests failed. Error: {bundleRequest.ErrorMessage}";
await webhookService.SendMessage(message, user.TelegramChannel);
}
@@ -690,7 +709,8 @@ public class BacktestExecutor
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update bundle request {BundleRequestId} with backtest {BacktestId}", bundleRequestId, backtest.Id);
_logger.LogError(ex, "Failed to update bundle request {BundleRequestId} with backtest {BacktestId}",
bundleRequestId, backtest.Id);
}
}
@@ -711,5 +731,4 @@ public class BacktestExecutor
_logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id);
}
}
}
}