Add precalculated signals list + multi scenario test
This commit is contained in:
@@ -8,6 +8,8 @@ using Managing.Core;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Indicators;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Users;
|
||||
@@ -224,6 +226,29 @@ public class BacktestExecutor
|
||||
// Pre-allocate and populate candle structures for maximum performance
|
||||
var orderedCandles = candles.OrderBy(c => c.Date).ToList();
|
||||
|
||||
// Pre-calculate all signals for the entire backtest period
|
||||
Dictionary<DateTime, LightSignal> preCalculatedSignals = null;
|
||||
var signalPreCalcStart = Stopwatch.GetTimestamp();
|
||||
if (config.Scenario != null && preCalculatedIndicatorValues != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
preCalculatedSignals = PreCalculateAllSignals(orderedCandles, config.Scenario, preCalculatedIndicatorValues);
|
||||
var signalPreCalcTime = Stopwatch.GetElapsedTime(signalPreCalcStart);
|
||||
_logger.LogInformation(
|
||||
"✅ Successfully pre-calculated {SignalCount} signals in {Duration:F2}ms",
|
||||
preCalculatedSignals.Count, signalPreCalcTime.TotalMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var signalPreCalcTime = Stopwatch.GetElapsedTime(signalPreCalcStart);
|
||||
_logger.LogWarning(ex,
|
||||
"❌ Failed to pre-calculate signals in {Duration:F2}ms, will calculate on-the-fly. Error: {ErrorMessage}",
|
||||
signalPreCalcTime.TotalMilliseconds, ex.Message);
|
||||
preCalculatedSignals = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Use optimized rolling window approach - TradingBox.GetSignal only needs last 600 candles
|
||||
const int rollingWindowSize = 600;
|
||||
var rollingCandles = new List<Candle>(rollingWindowSize); // Pre-allocate capacity for better performance
|
||||
@@ -276,9 +301,23 @@ public class BacktestExecutor
|
||||
|
||||
if (!shouldSkipSignalUpdate)
|
||||
{
|
||||
// Reuse the pre-allocated HashSet instead of creating new one
|
||||
// Use pre-calculated signals for maximum performance
|
||||
var signalUpdateStart = Stopwatch.GetTimestamp();
|
||||
await tradingBot.UpdateSignals(fixedCandlesHashSet);
|
||||
|
||||
if (preCalculatedSignals != null && preCalculatedSignals.TryGetValue(candle.Date, out var preCalculatedSignal))
|
||||
{
|
||||
// Fast path: use pre-calculated signal directly
|
||||
if (preCalculatedSignal != null)
|
||||
{
|
||||
await tradingBot.AddSignal(preCalculatedSignal);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: calculate signal on-the-fly (shouldn't happen in optimized path)
|
||||
await tradingBot.UpdateSignals(fixedCandlesHashSet);
|
||||
}
|
||||
|
||||
signalUpdateTotalTime += Stopwatch.GetElapsedTime(signalUpdateStart);
|
||||
telemetry.TotalSignalUpdates++;
|
||||
}
|
||||
@@ -339,10 +378,10 @@ public class BacktestExecutor
|
||||
// Track peak memory usage (reduced frequency to minimize GC overhead)
|
||||
if (currentCandle - lastMemoryCheck >= memoryCheckInterval)
|
||||
{
|
||||
var currentMemory = GC.GetTotalMemory(false);
|
||||
if (currentMemory > peakMemory)
|
||||
{
|
||||
peakMemory = currentMemory;
|
||||
var currentMemory = GC.GetTotalMemory(false);
|
||||
if (currentMemory > peakMemory)
|
||||
{
|
||||
peakMemory = currentMemory;
|
||||
}
|
||||
lastMemoryCheck = currentCandle;
|
||||
}
|
||||
@@ -546,6 +585,51 @@ public class BacktestExecutor
|
||||
return (currentCandleIndex % signalUpdateFrequency) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-calculates all signals for the entire backtest period
|
||||
/// This eliminates repeated GetSignal() calls during the backtest loop
|
||||
/// </summary>
|
||||
private Dictionary<DateTime, LightSignal> PreCalculateAllSignals(
|
||||
List<Candle> orderedCandles,
|
||||
LightScenario scenario,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
|
||||
{
|
||||
var signals = new Dictionary<DateTime, LightSignal>();
|
||||
var previousSignals = new Dictionary<string, LightSignal>();
|
||||
const int rollingWindowSize = 600;
|
||||
|
||||
_logger.LogInformation("⚡ Pre-calculating signals for {CandleCount} candles with rolling window size {WindowSize}",
|
||||
orderedCandles.Count, rollingWindowSize);
|
||||
|
||||
for (int i = 0; i < orderedCandles.Count; i++)
|
||||
{
|
||||
var currentCandle = orderedCandles[i];
|
||||
|
||||
// Build rolling window: last 600 candles up to current candle
|
||||
var windowStart = Math.Max(0, i - rollingWindowSize + 1);
|
||||
var windowCandles = orderedCandles.Skip(windowStart).Take(i - windowStart + 1).ToHashSet();
|
||||
|
||||
// Calculate signal for this candle using the same logic as TradingBox.GetSignal
|
||||
var signal = TradingBox.GetSignal(
|
||||
windowCandles,
|
||||
scenario,
|
||||
previousSignals,
|
||||
scenario?.LoopbackPeriod ?? 1,
|
||||
preCalculatedIndicatorValues);
|
||||
|
||||
if (signal != null)
|
||||
{
|
||||
signals[currentCandle.Date] = signal;
|
||||
previousSignals[signal.Identifier] = signal;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("✅ Pre-calculated {SignalCount} signals for {CandleCount} candles",
|
||||
signals.Count, orderedCandles.Count);
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Backtest to LightBacktest
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user