Add precalculated signals list + multi scenario test

This commit is contained in:
2025-11-11 14:05:09 +07:00
parent e810ab60ce
commit 903413692c
7 changed files with 359 additions and 17 deletions

View File

@@ -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>

View File

@@ -259,6 +259,11 @@ public class TradingBotBase : ITradingBot
}
public async Task UpdateSignals(HashSet<Candle> candles = null)
{
await UpdateSignals(candles, null);
}
public async Task UpdateSignals(HashSet<Candle> candles, Dictionary<DateTime, LightSignal> preCalculatedSignals = null)
{
// Skip indicator checking if flipping is disabled and there's an open position
// This prevents unnecessary indicator calculations when we can't act on signals anyway
@@ -276,13 +281,29 @@ public class TradingBotBase : ITradingBot
return;
}
if (Config.IsForBacktest && candles != null)
if (Config.IsForBacktest)
{
var backtestSignal =
TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
LightSignal backtestSignal;
if (preCalculatedSignals != null && LastCandle != null && preCalculatedSignals.TryGetValue(LastCandle.Date, out backtestSignal))
{
// Use pre-calculated signal - fast path
if (backtestSignal == null) return;
await AddSignal(backtestSignal);
}
else if (candles != null)
{
// Fallback to original calculation if no pre-calculated signals available
backtestSignal = TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
PreCalculatedIndicatorValues);
if (backtestSignal == null) return;
await AddSignal(backtestSignal);
if (backtestSignal == null) return;
await AddSignal(backtestSignal);
}
else
{
// No candles provided - skip signal update
return;
}
}
else
{
@@ -375,7 +396,7 @@ public class TradingBotBase : ITradingBot
}
}
}
if (!hasOpenPositions && !hasWaitingSignals)
return;