583 lines
26 KiB
C#
583 lines
26 KiB
C#
using System.Diagnostics;
|
|
using Managing.Application.Abstractions;
|
|
using Managing.Application.Abstractions.Repositories;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Application.Bots;
|
|
using Managing.Common;
|
|
using Managing.Domain.Backtests;
|
|
using Managing.Domain.Bots;
|
|
using Managing.Domain.Candles;
|
|
using Managing.Domain.Shared.Helpers;
|
|
using Managing.Domain.Strategies.Base;
|
|
using Managing.Domain.Users;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
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>
|
|
public class BacktestTelemetry
|
|
{
|
|
public long MemoryUsageAtStart { get; set; }
|
|
public long MemoryUsageAtEnd { get; set; }
|
|
public long PeakMemoryUsage { get; set; }
|
|
public TimeSpan IndicatorPreCalculationTime { get; set; }
|
|
public TimeSpan CandleProcessingTime { get; set; }
|
|
public TimeSpan SignalUpdateTime { get; set; }
|
|
public TimeSpan BacktestStepTime { get; set; }
|
|
public TimeSpan ProgressCallbackTime { get; set; }
|
|
public TimeSpan ResultCalculationTime { get; set; }
|
|
public int TotalCandlesProcessed { get; set; }
|
|
public int TotalSignalUpdates { get; set; }
|
|
public int TotalBacktestSteps { get; set; }
|
|
public int ProgressCallbacksCount { get; set; }
|
|
public Dictionary<string, TimeSpan> OperationBreakdown { get; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Service for executing backtests without Orleans dependencies.
|
|
/// Extracted from BacktestTradingBotGrain to be reusable in compute workers.
|
|
/// </summary>
|
|
public class BacktestExecutor
|
|
{
|
|
private readonly ILogger<BacktestExecutor> _logger;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
private readonly IBacktestRepository _backtestRepository;
|
|
private readonly IScenarioService _scenarioService;
|
|
private readonly IAccountService _accountService;
|
|
private readonly IMessengerService _messengerService;
|
|
private readonly SignalCache _signalCache = new();
|
|
|
|
public BacktestExecutor(
|
|
ILogger<BacktestExecutor> logger,
|
|
IServiceScopeFactory scopeFactory,
|
|
IBacktestRepository backtestRepository,
|
|
IScenarioService scenarioService,
|
|
IAccountService accountService,
|
|
IMessengerService messengerService)
|
|
{
|
|
_logger = logger;
|
|
_scopeFactory = scopeFactory;
|
|
_backtestRepository = backtestRepository;
|
|
_scenarioService = scenarioService;
|
|
_accountService = accountService;
|
|
_messengerService = messengerService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes a backtest with the given configuration and candles.
|
|
/// </summary>
|
|
/// <param name="config">The trading bot configuration</param>
|
|
/// <param name="candles">The candles to use for backtesting</param>
|
|
/// <param name="user">The user running the backtest</param>
|
|
/// <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>
|
|
/// <param name="cancellationToken">Cancellation token to stop execution</param>
|
|
/// <returns>The lightweight backtest result</returns>
|
|
public async Task<LightBacktest> ExecuteAsync(
|
|
TradingBotConfig config,
|
|
HashSet<Candle> candles,
|
|
User user,
|
|
bool save = false,
|
|
bool withCandles = false,
|
|
string requestId = null,
|
|
Guid? bundleRequestId = null,
|
|
object metadata = null,
|
|
Func<int, Task> progressCallback = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (candles == null || candles.Count == 0)
|
|
{
|
|
throw new Exception("No candle to backtest");
|
|
}
|
|
|
|
// Comprehensive telemetry setup
|
|
var backtestStartTime = DateTime.UtcNow;
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var telemetry = new BacktestTelemetry();
|
|
|
|
// Initial memory snapshot
|
|
var initialMemory = GC.GetTotalMemory(false);
|
|
telemetry.MemoryUsageAtStart = initialMemory;
|
|
|
|
_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();
|
|
}
|
|
|
|
// Create a fresh TradingBotBase instance for this backtest
|
|
var tradingBot = CreateTradingBotInstance(config);
|
|
tradingBot.Account = user.Accounts.First();
|
|
|
|
var totalCandles = candles.Count;
|
|
var currentCandle = 0;
|
|
var lastLoggedPercentage = 0;
|
|
|
|
_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);
|
|
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
|
|
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
|
|
var lastWalletCheck = 0;
|
|
var lastWalletBalance = config.BotTradingBalance;
|
|
|
|
var fixedCandles = new HashSet<Candle>();
|
|
|
|
// Track memory usage during processing
|
|
var peakMemory = initialMemory;
|
|
const int memoryCheckInterval = 100; // Check memory every N candles to reduce GC.GetTotalMemory overhead
|
|
var lastMemoryCheck = 0;
|
|
|
|
// Start timing the candle processing loop
|
|
var candleProcessingStart = Stopwatch.GetTimestamp();
|
|
var signalUpdateTotalTime = TimeSpan.Zero;
|
|
var backtestStepTotalTime = TimeSpan.Zero;
|
|
var progressCallbackTotalTime = TimeSpan.Zero;
|
|
|
|
_logger.LogInformation("🔄 Starting candle processing for {CandleCount} candles", orderedCandles.Count);
|
|
|
|
// Process all candles with optimized rolling window approach
|
|
foreach (var candle in orderedCandles)
|
|
{
|
|
// Check for cancellation (timeout or shutdown)
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
// Add to HashSet for reuse
|
|
fixedCandles.Add(candle);
|
|
tradingBot.LastCandle = candle;
|
|
|
|
// Run with optimized backtest path (minimize async calls)
|
|
var backtestStepStart = Stopwatch.GetTimestamp();
|
|
await tradingBot.UpdateSignals(fixedCandles, preCalculatedIndicatorValues);
|
|
await tradingBot.Run();
|
|
|
|
backtestStepTotalTime += Stopwatch.GetElapsedTime(backtestStepStart);
|
|
|
|
telemetry.TotalBacktestSteps++;
|
|
|
|
currentCandle++;
|
|
candlesProcessed++;
|
|
|
|
// 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))
|
|
{
|
|
var progressCallbackStart = Stopwatch.GetTimestamp();
|
|
try
|
|
{
|
|
await progressCallback(currentPercentage);
|
|
telemetry.ProgressCallbacksCount++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Error in progress callback");
|
|
}
|
|
|
|
progressCallbackTotalTime += Stopwatch.GetElapsedTime(progressCallbackStart);
|
|
lastProgressUpdate = DateTime.UtcNow;
|
|
}
|
|
|
|
// Track peak memory usage (reduced frequency to minimize GC overhead)
|
|
if (currentCandle - lastMemoryCheck >= memoryCheckInterval)
|
|
{
|
|
var currentMemory = GC.GetTotalMemory(false);
|
|
if (currentMemory > peakMemory)
|
|
{
|
|
peakMemory = currentMemory;
|
|
}
|
|
|
|
lastMemoryCheck = currentCandle;
|
|
}
|
|
|
|
// Log progress every 10% (reduced frequency)
|
|
if (currentPercentage >= lastLoggedPercentage + 10)
|
|
{
|
|
lastLoggedPercentage = currentPercentage;
|
|
_logger.LogInformation(
|
|
"Backtest progress: {Percentage}% ({CurrentCandle}/{TotalCandles} candles processed)",
|
|
currentPercentage, currentCandle, totalCandles);
|
|
}
|
|
}
|
|
|
|
// Complete candle processing telemetry
|
|
telemetry.CandleProcessingTime = Stopwatch.GetElapsedTime(candleProcessingStart);
|
|
telemetry.SignalUpdateTime = signalUpdateTotalTime;
|
|
telemetry.BacktestStepTime = backtestStepTotalTime;
|
|
telemetry.ProgressCallbackTime = progressCallbackTotalTime;
|
|
telemetry.TotalCandlesProcessed = candlesProcessed;
|
|
|
|
_logger.LogInformation("✅ Backtest processing completed. Calculating final results...");
|
|
|
|
// Start result calculation timing
|
|
var resultCalculationStart = Stopwatch.GetTimestamp();
|
|
|
|
// Calculate final results using static methods from TradingBox
|
|
var realizedPnl = TradingBox.GetTotalRealizedPnL(tradingBot.Positions); // PnL before fees
|
|
var netPnl = TradingBox.GetTotalNetPnL(tradingBot.Positions); // PnL after fees
|
|
var winRate = TradingBox.GetWinRate(tradingBot.Positions);
|
|
var stats = TradingBox.GetStatistics(tradingBot.WalletBalances);
|
|
var growthPercentage =
|
|
TradingBox.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, netPnl);
|
|
var hodlPercentage = TradingBox.GetHodlPercentage(candles.First(), candles.Last());
|
|
|
|
var fees = TradingBox.GetTotalFees(tradingBot.Positions);
|
|
var scoringParams = new BacktestScoringParams(
|
|
sharpeRatio: (double)stats.SharpeRatio,
|
|
growthPercentage: (double)growthPercentage,
|
|
hodlPercentage: (double)hodlPercentage,
|
|
winRate: winRate,
|
|
totalPnL: (double)netPnl,
|
|
fees: (double)fees,
|
|
tradeCount: tradingBot.Positions.Count,
|
|
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
|
|
maxDrawdown: stats.MaxDrawdown,
|
|
initialBalance: tradingBot.WalletBalances.FirstOrDefault().Value,
|
|
tradingBalance: config.BotTradingBalance,
|
|
startDate: candles.First().Date,
|
|
endDate: candles.Last().Date,
|
|
timeframe: config.Timeframe,
|
|
moneyManagement: config.MoneyManagement
|
|
);
|
|
|
|
var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams);
|
|
|
|
// Complete result calculation telemetry
|
|
telemetry.ResultCalculationTime = Stopwatch.GetElapsedTime(resultCalculationStart);
|
|
|
|
// Final memory snapshot
|
|
var finalMemory = GC.GetTotalMemory(false);
|
|
telemetry.MemoryUsageAtEnd = finalMemory;
|
|
telemetry.PeakMemoryUsage = peakMemory;
|
|
|
|
// Generate requestId if not provided
|
|
var finalRequestId = requestId != null ? Guid.Parse(requestId) : Guid.NewGuid();
|
|
|
|
// Create backtest result with conditional candles and indicators values
|
|
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals,
|
|
withCandles ? candles : new HashSet<Candle>())
|
|
{
|
|
FinalPnl = realizedPnl, // Realized PnL before fees
|
|
WinRate = winRate,
|
|
GrowthPercentage = growthPercentage,
|
|
HodlPercentage = hodlPercentage,
|
|
Fees = fees,
|
|
WalletBalances = tradingBot.WalletBalances.ToList(),
|
|
Statistics = stats,
|
|
Score = scoringResult.Score,
|
|
ScoreMessage = scoringResult.GenerateSummaryMessage(),
|
|
Id = Guid.NewGuid().ToString(),
|
|
RequestId = finalRequestId,
|
|
Metadata = metadata,
|
|
StartDate = candles.FirstOrDefault()!.OpenTime,
|
|
EndDate = candles.LastOrDefault()!.OpenTime,
|
|
InitialBalance = initialBalance,
|
|
NetPnl = netPnl, // Net PnL after fees
|
|
};
|
|
|
|
if (save && user != null)
|
|
{
|
|
await _backtestRepository.InsertBacktestForUserAsync(user, result);
|
|
}
|
|
|
|
// Send notification if backtest meets criteria
|
|
await SendBacktestNotificationIfCriteriaMet(result);
|
|
|
|
// Stop overall timing
|
|
stopwatch.Stop();
|
|
var totalExecutionTime = stopwatch.Elapsed;
|
|
|
|
// Comprehensive performance telemetry logging
|
|
_logger.LogInformation("📊 === BACKTEST PERFORMANCE TELEMETRY ===");
|
|
_logger.LogInformation("⏱️ Total Execution Time: {TotalTime:F2}s", totalExecutionTime.TotalSeconds);
|
|
_logger.LogInformation("📈 Candles Processed: {Candles} ({CandlesPerSecond:F1} candles/sec)",
|
|
telemetry.TotalCandlesProcessed, telemetry.TotalCandlesProcessed / totalExecutionTime.TotalSeconds);
|
|
|
|
_logger.LogInformation("💾 Memory Usage: Start={StartMB:F2}MB, End={EndMB:F2}MB, Peak={PeakMB:F2}MB",
|
|
telemetry.MemoryUsageAtStart / 1024.0 / 1024.0,
|
|
telemetry.MemoryUsageAtEnd / 1024.0 / 1024.0,
|
|
telemetry.PeakMemoryUsage / 1024.0 / 1024.0);
|
|
|
|
_logger.LogInformation("⚡ Operation Breakdown:");
|
|
_logger.LogInformation(" • Indicator Pre-calculation: {Time:F2}ms ({Percentage:F1}%)",
|
|
telemetry.IndicatorPreCalculationTime.TotalMilliseconds,
|
|
telemetry.IndicatorPreCalculationTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100);
|
|
_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)",
|
|
telemetry.SignalUpdateTime.TotalMilliseconds,
|
|
telemetry.SignalUpdateTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100,
|
|
telemetry.TotalSignalUpdates,
|
|
signalUpdateSkipCount,
|
|
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,
|
|
telemetry.TotalBacktestSteps);
|
|
_logger.LogInformation(" • Progress Callbacks: {Time:F2}ms ({Percentage:F1}%) - {Count} calls",
|
|
telemetry.ProgressCallbackTime.TotalMilliseconds,
|
|
telemetry.ProgressCallbackTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds * 100,
|
|
telemetry.ProgressCallbacksCount);
|
|
_logger.LogInformation(" • Result Calculation: {Time:F2}ms ({Percentage:F1}%)",
|
|
telemetry.ResultCalculationTime.TotalMilliseconds,
|
|
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;
|
|
|
|
_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));
|
|
|
|
// Identify potential bottlenecks
|
|
var bottlenecks = new List<string>();
|
|
if (telemetry.SignalUpdateTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds > 0.5)
|
|
bottlenecks.Add("Signal Updates");
|
|
if (telemetry.BacktestStepTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds > 0.3)
|
|
bottlenecks.Add("Backtest Steps");
|
|
if (telemetry.IndicatorPreCalculationTime.TotalMilliseconds / totalExecutionTime.TotalMilliseconds > 0.2)
|
|
bottlenecks.Add("Indicator Pre-calculation");
|
|
|
|
if (bottlenecks.Any())
|
|
{
|
|
_logger.LogWarning("⚠️ Performance Bottlenecks Detected: {Bottlenecks}",
|
|
string.Join(", ", bottlenecks));
|
|
}
|
|
|
|
_logger.LogInformation("🎯 Backtest completed successfully - RequestId: {RequestId}", finalRequestId);
|
|
|
|
// Convert Backtest to LightBacktest
|
|
return ConvertToLightBacktest(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advanced signal caching based on indicator update frequency
|
|
/// Instead of hashing candles, we cache signals based on how often indicators need updates
|
|
/// </summary>
|
|
private bool ShouldSkipSignalUpdate(int currentCandleIndex, int totalCandles)
|
|
{
|
|
// RSI and similar indicators don't need to be recalculated every candle
|
|
// For 15-minute candles, we can update signals every 3-5 candles without significant accuracy loss
|
|
const int signalUpdateFrequency = 3; // Update signals every N candles
|
|
|
|
// Always update signals for the first few candles to establish baseline
|
|
if (currentCandleIndex < 10)
|
|
return false;
|
|
|
|
// Always update signals near the end to ensure final trades are calculated
|
|
if (currentCandleIndex > totalCandles - 10)
|
|
return false;
|
|
|
|
// Skip signal updates based on frequency
|
|
return (currentCandleIndex % signalUpdateFrequency) != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pre-calculates all signals for the entire backtest period
|
|
/// This eliminates repeated GetSignal() calls during the backtest loop
|
|
/// </summary>
|
|
/// <summary>
|
|
/// Converts a Backtest to LightBacktest
|
|
/// </summary>
|
|
private LightBacktest ConvertToLightBacktest(Backtest backtest)
|
|
{
|
|
return new LightBacktest
|
|
{
|
|
Id = backtest.Id,
|
|
Config = backtest.Config,
|
|
FinalPnl = backtest.FinalPnl,
|
|
WinRate = backtest.WinRate,
|
|
GrowthPercentage = backtest.GrowthPercentage,
|
|
HodlPercentage = backtest.HodlPercentage,
|
|
StartDate = backtest.StartDate,
|
|
EndDate = backtest.EndDate,
|
|
MaxDrawdown = backtest.Statistics?.MaxDrawdown,
|
|
Fees = backtest.Fees,
|
|
SharpeRatio = (double?)backtest.Statistics?.SharpeRatio,
|
|
Score = backtest.Score,
|
|
ScoreMessage = backtest.ScoreMessage,
|
|
InitialBalance = backtest.InitialBalance,
|
|
NetPnl = backtest.NetPnl
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a TradingBotBase instance for backtesting
|
|
/// </summary>
|
|
private TradingBotBase CreateTradingBotInstance(TradingBotConfig config)
|
|
{
|
|
// Validate configuration for backtesting
|
|
if (config == null)
|
|
{
|
|
throw new InvalidOperationException("Bot configuration is not initialized");
|
|
}
|
|
|
|
if (!config.IsForBacktest)
|
|
{
|
|
throw new InvalidOperationException("BacktestExecutor can only be used for backtesting");
|
|
}
|
|
|
|
// Create the trading bot instance
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
|
|
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
|
|
return tradingBot;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends notification if backtest meets criteria
|
|
/// </summary>
|
|
private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest)
|
|
{
|
|
try
|
|
{
|
|
if (backtest.Score > 60)
|
|
{
|
|
await _messengerService.SendBacktestNotification(backtest);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id);
|
|
}
|
|
}
|
|
} |