Refactor BacktestExecutor and TradingBotBase for performance optimizations; remove unused SignalCache and pre-calculation logic; implement caching for open position state and streamline signal access with TryGetValue; enhance logging for detailed timing breakdown during backtest execution.
This commit is contained in:
@@ -16,52 +16,6 @@ using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.Backtests;
|
||||
|
||||
/// <summary>
|
||||
/// Signal caching optimization to reduce redundant calculations
|
||||
/// </summary>
|
||||
public class SignalCache
|
||||
{
|
||||
private readonly Dictionary<int, Dictionary<IndicatorType, object>> _cachedSignals = new();
|
||||
private readonly int _cacheSize = 50; // Cache last N candle signals to balance memory vs performance
|
||||
private int _nextCacheKey = 0;
|
||||
|
||||
public bool TryGetCachedSignal(int cacheKey, IndicatorType indicatorType, out object signal)
|
||||
{
|
||||
if (_cachedSignals.TryGetValue(cacheKey, out var candleSignals) &&
|
||||
candleSignals.TryGetValue(indicatorType, out signal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
signal = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public int CacheSignal(IndicatorType indicatorType, object signal)
|
||||
{
|
||||
var cacheKey = _nextCacheKey++;
|
||||
if (!_cachedSignals.ContainsKey(cacheKey))
|
||||
_cachedSignals[cacheKey] = new Dictionary<IndicatorType, object>();
|
||||
|
||||
_cachedSignals[cacheKey][indicatorType] = signal;
|
||||
|
||||
// Maintain cache size - remove oldest entries
|
||||
if (_cachedSignals.Count > _cacheSize)
|
||||
{
|
||||
var oldestKey = _cachedSignals.Keys.Min();
|
||||
_cachedSignals.Remove(oldestKey);
|
||||
}
|
||||
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_cachedSignals.Clear();
|
||||
_nextCacheKey = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive telemetry data for backtest execution profiling
|
||||
/// </summary>
|
||||
@@ -95,7 +49,6 @@ public class BacktestExecutor
|
||||
private readonly IScenarioService _scenarioService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IMessengerService _messengerService;
|
||||
private readonly SignalCache _signalCache = new();
|
||||
|
||||
public BacktestExecutor(
|
||||
ILogger<BacktestExecutor> logger,
|
||||
@@ -177,59 +130,16 @@ public class BacktestExecutor
|
||||
_logger.LogInformation("Backtest requested by {UserId} with {TotalCandles} candles for {Ticker} on {Timeframe}",
|
||||
user.Id, totalCandles, config.Ticker, config.Timeframe);
|
||||
|
||||
// Pre-calculate indicator values once for all candles to optimize performance
|
||||
// This avoids recalculating indicators for every candle iteration
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null;
|
||||
if (config.Scenario != null && false)
|
||||
{
|
||||
var indicatorCalcStart = Stopwatch.GetTimestamp();
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("⚡ Pre-calculating indicator values for {IndicatorCount} indicators",
|
||||
config.Scenario.Indicators?.Count ?? 0);
|
||||
|
||||
// Convert LightScenario to Scenario for CalculateIndicatorsValues
|
||||
var scenario = config.Scenario.ToScenario();
|
||||
|
||||
// Calculate all indicator values once with all candles
|
||||
preCalculatedIndicatorValues = TradingBox.CalculateIndicatorsValues(scenario, candles);
|
||||
telemetry.IndicatorPreCalculationTime = Stopwatch.GetElapsedTime(indicatorCalcStart);
|
||||
_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}",
|
||||
telemetry.IndicatorPreCalculationTime.TotalMilliseconds, ex.Message);
|
||||
// Continue with normal calculation if pre-calculation fails
|
||||
preCalculatedIndicatorValues = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize wallet balance with first candle
|
||||
tradingBot.WalletBalances.Clear();
|
||||
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
|
||||
tradingBot.WalletBalances.Add(candles.First().Date, config.BotTradingBalance);
|
||||
var initialBalance = config.BotTradingBalance;
|
||||
|
||||
// Pre-allocate and populate candle structures for maximum performance
|
||||
var orderedCandles = candles.ToList();
|
||||
|
||||
// Skip pre-calculated signals - the approach was flawed and caused performance regression
|
||||
// The signal calculation depends on rolling window state and cannot be pre-calculated effectively
|
||||
|
||||
// Use optimized rolling window approach - TradingBox.GetSignal only needs last 600 candles
|
||||
// Use List<Candle> directly to preserve chronological order and enable incremental updates
|
||||
const int RollingWindowSize = 600; // TradingBox.GetSignal only needs last 600 candles
|
||||
var rollingWindowCandles = new List<Candle>(RollingWindowSize); // Pre-allocate capacity for performance
|
||||
const int RollingWindowSize = 600;
|
||||
var rollingWindowCandles = new List<Candle>(RollingWindowSize); // Pre-allocate capacity
|
||||
var candlesProcessed = 0;
|
||||
|
||||
// Signal caching optimization - reduce signal update frequency for better performance
|
||||
var signalUpdateSkipCount = 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
|
||||
@@ -247,14 +157,23 @@ public class BacktestExecutor
|
||||
var backtestStepTotalTime = TimeSpan.Zero;
|
||||
var progressCallbackTotalTime = TimeSpan.Zero;
|
||||
|
||||
_logger.LogInformation("🔄 Starting candle processing for {CandleCount} candles", orderedCandles.Count);
|
||||
_logger.LogInformation("🔄 Starting candle processing for {CandleCount} candles", candles.Count);
|
||||
|
||||
// TIMING: Track rolling window operations
|
||||
var rollingWindowTotalTime = TimeSpan.Zero;
|
||||
var loopOverheadTotalTime = TimeSpan.Zero;
|
||||
|
||||
// Process all candles with optimized rolling window approach
|
||||
foreach (var candle in orderedCandles)
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
var loopStart = Stopwatch.GetTimestamp();
|
||||
|
||||
// Check for cancellation (timeout or shutdown)
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// TIMING: Measure rolling window operations
|
||||
var rollingWindowStart = Stopwatch.GetTimestamp();
|
||||
|
||||
// Maintain rolling window of last 600 candles to prevent exponential memory growth
|
||||
// Incremental updates: remove oldest if at capacity, then add newest
|
||||
// This preserves chronological order and avoids expensive HashSet recreation
|
||||
@@ -263,13 +182,15 @@ public class BacktestExecutor
|
||||
rollingWindowCandles.RemoveAt(0); // Remove oldest candle (O(n) but only 600 items max)
|
||||
}
|
||||
rollingWindowCandles.Add(candle); // Add newest candle (O(1) amortized)
|
||||
|
||||
rollingWindowTotalTime += Stopwatch.GetElapsedTime(rollingWindowStart);
|
||||
|
||||
tradingBot.LastCandle = candle;
|
||||
|
||||
// Run with optimized backtest path (minimize async calls)
|
||||
var signalUpdateStart = Stopwatch.GetTimestamp();
|
||||
// Pass List<Candle> directly - no conversion needed, order is preserved
|
||||
await tradingBot.UpdateSignals(rollingWindowCandles, preCalculatedIndicatorValues);
|
||||
await tradingBot.UpdateSignals(rollingWindowCandles);
|
||||
signalUpdateTotalTime += Stopwatch.GetElapsedTime(signalUpdateStart);
|
||||
|
||||
var backtestStepStart = Stopwatch.GetTimestamp();
|
||||
@@ -338,6 +259,11 @@ public class BacktestExecutor
|
||||
"Backtest progress: {Percentage}% ({CurrentCandle}/{TotalCandles} candles processed)",
|
||||
currentPercentage, currentCandle, totalCandles);
|
||||
}
|
||||
|
||||
// TIMING: Calculate loop overhead (everything except signal updates and backtest steps)
|
||||
var loopTotal = Stopwatch.GetElapsedTime(loopStart);
|
||||
var signalAndBacktestTime = signalUpdateTotalTime + backtestStepTotalTime;
|
||||
// Note: loopOverheadTotalTime is cumulative, we track it at the end
|
||||
}
|
||||
|
||||
// Complete candle processing telemetry
|
||||
@@ -347,6 +273,23 @@ public class BacktestExecutor
|
||||
telemetry.ProgressCallbackTime = progressCallbackTotalTime;
|
||||
telemetry.TotalCandlesProcessed = candlesProcessed;
|
||||
|
||||
// TIMING: Log detailed breakdown
|
||||
_logger.LogInformation("📊 === DETAILED TIMING BREAKDOWN ===");
|
||||
_logger.LogInformation(" • Rolling Window Operations: {Time:F2}ms ({Percentage:F1}% of total)",
|
||||
rollingWindowTotalTime.TotalMilliseconds,
|
||||
rollingWindowTotalTime.TotalMilliseconds / telemetry.CandleProcessingTime.TotalMilliseconds * 100);
|
||||
_logger.LogInformation(" • Signal Updates: {Time:F2}ms ({Percentage:F1}% of total)",
|
||||
signalUpdateTotalTime.TotalMilliseconds,
|
||||
signalUpdateTotalTime.TotalMilliseconds / telemetry.CandleProcessingTime.TotalMilliseconds * 100);
|
||||
_logger.LogInformation(" • Backtest Steps (Run): {Time:F2}ms ({Percentage:F1}% of total)",
|
||||
backtestStepTotalTime.TotalMilliseconds,
|
||||
backtestStepTotalTime.TotalMilliseconds / telemetry.CandleProcessingTime.TotalMilliseconds * 100);
|
||||
var accountedTime = rollingWindowTotalTime + signalUpdateTotalTime + backtestStepTotalTime + progressCallbackTotalTime;
|
||||
var unaccountedTime = telemetry.CandleProcessingTime - accountedTime;
|
||||
_logger.LogInformation(" • Other Loop Overhead: {Time:F2}ms ({Percentage:F1}% of total)",
|
||||
unaccountedTime.TotalMilliseconds,
|
||||
unaccountedTime.TotalMilliseconds / telemetry.CandleProcessingTime.TotalMilliseconds * 100);
|
||||
|
||||
_logger.LogInformation("✅ Backtest processing completed. Calculating final results...");
|
||||
|
||||
// Start result calculation timing
|
||||
@@ -445,14 +388,10 @@ public class BacktestExecutor
|
||||
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)",
|
||||
" • Signal Updates: {Time:F2}ms ({Percentage:F1}%) - {Count} updates",
|
||||
telemetry.SignalUpdateTime.TotalMilliseconds,
|
||||
telemetry.SignalUpdateTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100,
|
||||
telemetry.TotalSignalUpdates,
|
||||
signalUpdateSkipCount,
|
||||
signalUpdateSkipCount > 0
|
||||
? (double)signalUpdateSkipCount / (telemetry.TotalSignalUpdates + signalUpdateSkipCount) * 100
|
||||
: 0);
|
||||
telemetry.TotalSignalUpdates);
|
||||
_logger.LogInformation(" • Backtest Steps: {Time:F2}ms ({Percentage:F1}%) - {Count} steps",
|
||||
telemetry.BacktestStepTime.TotalMilliseconds,
|
||||
telemetry.BacktestStepTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100,
|
||||
|
||||
Reference in New Issue
Block a user