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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -372,6 +372,109 @@ public class BacktestExecutorTests : BaseTests, IDisposable
|
||||
Console.WriteLine($"✅ Performance test passed: {candlesPerSecond:F1} candles/sec");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteBacktest_With_Two_Scenarios_Should_Show_Performance_Telemetry()
|
||||
{
|
||||
// Arrange - Test with 2 indicators to verify pre-calculated signals optimization works with multiple scenarios
|
||||
var candles =
|
||||
FileHelpers.ReadJson<List<Candle>>("../../../Data/ETH-FifteenMinutes-candles-20:44:15 +00:00-.json");
|
||||
Assert.NotNull(candles);
|
||||
Assert.NotEmpty(candles);
|
||||
|
||||
Console.WriteLine($"DEBUG: Loaded {candles.Count} candles for two-scenarios performance telemetry test");
|
||||
|
||||
var scenario = new Scenario("ETH_TwoScenarios_Backtest");
|
||||
var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
|
||||
var emaCrossIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.EmaCross, "EmaCross", period: 21);
|
||||
scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator, (IndicatorBase)emaCrossIndicator };
|
||||
scenario.LoopbackPeriod = 15; // 15 minutes loopback period as requested
|
||||
|
||||
var config = new TradingBotConfig
|
||||
{
|
||||
AccountName = _account.Name,
|
||||
MoneyManagement = MoneyManagement,
|
||||
Ticker = Ticker.ETH,
|
||||
Scenario = LightScenario.FromScenario(scenario),
|
||||
Timeframe = Timeframe.FifteenMinutes,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = 100000,
|
||||
IsForBacktest = true,
|
||||
CooldownPeriod = 1,
|
||||
MaxLossStreak = 0,
|
||||
FlipPosition = false,
|
||||
Name = "ETH_TwoScenarios_Performance_Test",
|
||||
FlipOnlyWhenInProfit = true,
|
||||
MaxPositionTimeHours = null,
|
||||
CloseEarlyWhenProfitable = false
|
||||
};
|
||||
|
||||
// Track execution time
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
var result = await _backtestExecutor.ExecuteAsync(
|
||||
config,
|
||||
candles.ToHashSet(),
|
||||
_testUser,
|
||||
save: false,
|
||||
withCandles: false,
|
||||
requestId: null,
|
||||
bundleRequestId: null,
|
||||
metadata: null,
|
||||
progressCallback: null);
|
||||
|
||||
var executionTime = DateTime.UtcNow - startTime;
|
||||
|
||||
// Assert - Verify the result is valid
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(Ticker.ETH, result.Config.Ticker);
|
||||
Assert.Equal(100000, result.InitialBalance);
|
||||
Assert.True(result.Score >= 0);
|
||||
|
||||
// Business Logic Baseline Assertions - ensure consistency over time
|
||||
// These values establish the expected baseline for the two-scenarios test
|
||||
const decimal expectedFinalPnl = 2018.27m;
|
||||
const double expectedScore = 19.18;
|
||||
const int expectedWinRatePercent = 40; // 40% win rate
|
||||
const decimal expectedGrowthPercentage = 2.02m;
|
||||
|
||||
// Allow small tolerance for floating-point precision variations
|
||||
const decimal pnlTolerance = 0.01m;
|
||||
const double scoreTolerance = 0.01;
|
||||
const decimal growthTolerance = 0.01m;
|
||||
|
||||
Assert.True(Math.Abs(result.FinalPnl - expectedFinalPnl) <= pnlTolerance,
|
||||
$"Final PnL {result.FinalPnl:F2} differs from expected baseline {expectedFinalPnl:F2} (tolerance: ±{pnlTolerance:F2})");
|
||||
|
||||
Assert.True(Math.Abs(result.Score - expectedScore) <= scoreTolerance,
|
||||
$"Score {result.Score:F2} differs from expected baseline {expectedScore:F2} (tolerance: ±{scoreTolerance:F2})");
|
||||
|
||||
Assert.True(Math.Abs(result.WinRate - expectedWinRatePercent) <= 5,
|
||||
$"Win Rate {result.WinRate}% differs from expected baseline {expectedWinRatePercent}% (tolerance: ±5%)");
|
||||
|
||||
Assert.True(Math.Abs(result.GrowthPercentage - expectedGrowthPercentage) <= growthTolerance,
|
||||
$"Growth {result.GrowthPercentage:F2}% differs from expected baseline {expectedGrowthPercentage:F2}% (tolerance: ±{growthTolerance:F2}%)");
|
||||
|
||||
// Performance metrics
|
||||
var totalCandles = candles.Count;
|
||||
var candlesPerSecond = totalCandles / executionTime.TotalSeconds;
|
||||
|
||||
// Log comprehensive performance metrics
|
||||
Console.WriteLine($"📊 === TWO-SCENARIOS PERFORMANCE TELEMETRY ===");
|
||||
Console.WriteLine($"⏱️ Total Execution Time: {executionTime.TotalSeconds:F2}s");
|
||||
Console.WriteLine($"📈 Candles Processed: {totalCandles} ({candlesPerSecond:F1} candles/sec)");
|
||||
Console.WriteLine($"🎯 Final PnL: {result.FinalPnl:F2} (Expected: {expectedFinalPnl:F2})");
|
||||
Console.WriteLine($"📊 Score: {result.Score:F2} (Expected: {expectedScore:F2})");
|
||||
Console.WriteLine($"📈 Win Rate: {result.WinRate}% (Expected: {expectedWinRatePercent}%)");
|
||||
Console.WriteLine($"📈 Growth: {result.GrowthPercentage:F2}% (Expected: {expectedGrowthPercentage:F2}%)");
|
||||
Console.WriteLine($"🎭 Scenario: {scenario.Name} ({scenario.Indicators.Count} indicators, LoopbackPeriod: {scenario.LoopbackPeriod})");
|
||||
|
||||
// Performance assertion - should be reasonably fast even with 2 indicators
|
||||
Assert.True(candlesPerSecond > 200, $"Expected >200 candles/sec with 2 indicators, got {candlesPerSecond:F1} candles/sec");
|
||||
|
||||
Console.WriteLine($"✅ Two-scenarios performance test passed: {candlesPerSecond:F1} candles/sec with {scenario.Indicators.Count} indicators");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_loggerFactory?.Dispose();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
DateTime,TestName,CandlesCount,ExecutionTimeSeconds,ProcessingRateCandlesPerSec,MemoryStartMB,MemoryEndMB,MemoryPeakMB,SignalUpdatesCount,SignalUpdatesSkipped,SignalUpdateEfficiencyPercent,BacktestStepsCount,AverageSignalUpdateMs,AverageBacktestStepMs,FinalPnL,WinRatePercent,GrowthPercentage,Score,CommitHash,GitBranch,Environment
|
||||
2025-11-11T06:53:40Z,ExecuteBacktest_With_Two_Scenarios_Should_Show_Performance_Telemetry,576037926 576037588,1.52 1.53,3792.6 3758,8,15.26,11.35,23.73,0.0,0,0.0,0.0,0.0,0.0,2018.27,4 000,00,2.02,1919,e810ab60,dev,development
|
||||
2025-11-11T06:58:31Z,ExecuteBacktest_With_Two_Scenarios_Should_Show_Performance_Telemetry,576038904 576038584,1.48 1.49,3890.4 3858,4,15.27,11.03,23.74,0.0,0,0.0,0.0,0.0,0.0,2018.27 (Expected: 2018.27),4 000,00 (Expected: 40,0%),2.02 (Expected: 2.02%),19181918,e810ab60,dev,development
|
||||
2025-11-11T07:03:00Z,ExecuteBacktest_With_Two_Scenarios_Should_Show_Performance_Telemetry,576033954 576033649,1.70 1.71,3395.4 3364,9,15.29,11.00,23.75,0.0,0,0.0,0.0,0.0,0.0,2018.27 (Expected: 2018.27),40 (Expected: 40%),2.02 (Expected: 2.02%),19191918,e810ab60,dev,development
|
||||
|
@@ -33,3 +33,14 @@ DateTime,TestName,CandlesCount,ExecutionTimeSeconds,ProcessingRateCandlesPerSec,
|
||||
2025-11-11T05:50:25Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,0.915,6292.9,15.27,11.04,23.72,770.66,3828,66.5,69.13,0.40,0.01,24560.79,38,24.56,6015,c66f6279,dev,development
|
||||
2025-11-11T05:52:21Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.045,5475.3,15.27,11.30,23.71,907.47,3828,66.5,64.87,0.47,0.01,24560.79,38,24.56,6015,c66f6279,dev,development
|
||||
2025-11-11T05:54:40Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.445,3959.3,15.26,11.11,23.72,1222.26,3828,66.5,111.35,0.63,0.02,24560.79,38,24.56,6015,c66f6279,dev,development
|
||||
2025-11-11T06:10:59Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.22,4683.2,15.26,10.84,23.72,1048.26,3828,66.5,79.79,0.54,0.01,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:15:18Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.85,3102.1,15.78,14.48,24.59,1559.17,3828,66.5,142.94,0.81,0.02,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:16:50Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.58,3629.2,15.26,15.20,24.06,1386.27,3828,66.5,101.01,0.72,0.02,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:22:25Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.445,3966.6,15.26,10.45,24.60,1256.25,3828,66.5,109.62,0.65,0.02,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:23:44Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.265,4544.2,15.26,11.24,23.71,1023.42,3828,66.5,80.77,0.53,0.01,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:41:40Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,0.835,6870.8,15.27,10.21,23.73,720.71,3828,66.5,52.24,0.37,0.01,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:44:52Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.095,5217.4,15.26,11.07,23.72,945.37,3828,66.5,72.77,0.49,0.01,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:45:12Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.07,5356.7,15.26,11.18,23.73,897.94,3828,66.5,91.98,0.46,0.02,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:53:40Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.12,5112.2,15.26,11.35,23.73,927.80,3828,66.5,78.67,0.48,0.01,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T06:58:31Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.55,3699.6,15.27,11.03,23.74,1319.91,3828,66.5,117.22,0.68,0.02,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
2025-11-11T07:03:00Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,2.11,2720.5,15.29,11.00,23.75,1780.10,3828,66.5,145.96,0.92,0.03,24560.79,38,24.56,6015,e810ab60,dev,development
|
||||
|
||||
|
Reference in New Issue
Block a user