Optimze worker for backtest
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -351,6 +351,13 @@ public class TradingBotBase : ITradingBot
|
||||
|
||||
private async Task ManagePositions()
|
||||
{
|
||||
// Early exit optimization - skip if no positions to manage
|
||||
var hasOpenPositions = Positions.Values.Any(p => !p.IsFinished());
|
||||
var hasWaitingSignals = Signals.Values.Any(s => s.Status == SignalStatus.WaitingForPosition);
|
||||
|
||||
if (!hasOpenPositions && !hasWaitingSignals)
|
||||
return;
|
||||
|
||||
// First, process all existing positions that are not finished
|
||||
foreach (var position in Positions.Values.Where(p => !p.IsFinished()))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user