From 567de2e5eee5fb822e166d94d2de655e17917a15 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Tue, 11 Nov 2025 11:35:48 +0700 Subject: [PATCH] Add benchmark + fix bundle that should be completed --- scripts/benchmark-backtest-performance.sh | 20 +- .../Bots/TradingBotBase.cs | 373 ++++++++++++------ .../performance-benchmarks.csv | 12 +- 3 files changed, 271 insertions(+), 134 deletions(-) diff --git a/scripts/benchmark-backtest-performance.sh b/scripts/benchmark-backtest-performance.sh index 7207bfdc..60d7c801 100755 --- a/scripts/benchmark-backtest-performance.sh +++ b/scripts/benchmark-backtest-performance.sh @@ -45,9 +45,9 @@ else exit 1 fi -# Extract performance metrics from the output -CANDLES_COUNT=$(echo "$TEST_OUTPUT" | grep "šŸ“ˆ Total Candles Processed:" | sed 's/.*: //' | sed 's/ / /' | tr -d ',' | xargs) -EXECUTION_TIME=$(echo "$TEST_OUTPUT" | grep "ā±ļø Total Execution Time:" | sed 's/.*: //' | sed 's/s//' | sed 's/,/./g' | xargs) +# Extract performance metrics from the output - use more robust parsing +CANDLES_COUNT=$(echo "$TEST_OUTPUT" | grep "šŸ“ˆ Total Candles Processed:" | sed 's/.*: //' | sed 's/[^0-9]//g' | xargs) +EXECUTION_TIME=$(echo "$TEST_OUTPUT" | grep "ā±ļø Total Execution Time:" | sed 's/.*: //' | sed 's/s//' | sed 's/,/./g' | awk '{print $NF}' | xargs | awk -F' ' '{if (NF==2) print ($1+$2)/2; else print $1}') PROCESSING_RATE=$(echo "$TEST_OUTPUT" | grep "šŸš€ Processing Rate:" | sed 's/.*: //' | sed 's/ candles\/sec//' | sed 's/,/./g' | xargs) # Extract memory metrics @@ -58,23 +58,23 @@ MEMORY_PEAK=$(echo "$MEMORY_LINE" | sed 's/.*Peak=//' | sed 's/MB.*//' | xargs) # Extract signal update metrics SIGNAL_LINE=$(echo "$TEST_OUTPUT" | grep "• Signal Updates:") -SIGNAL_UPDATES=$(echo "$SIGNAL_LINE" | sed 's/.*Signal Updates: //' | sed 's/ms.*//' | xargs) +SIGNAL_UPDATES=$(echo "$SIGNAL_LINE" | sed 's/.*Signal Updates: //' | sed 's/ms.*//' | sed 's/,/./g' | xargs) SIGNAL_SKIPPED=$(echo "$SIGNAL_LINE" | grep -o "[0-9,]* skipped" | sed 's/ skipped//' | tr -d ',' | xargs) SIGNAL_EFFICIENCY=$(echo "$SIGNAL_LINE" | grep -o "[0-9.]*% efficiency" | sed 's/% efficiency//' | xargs) # Extract backtest steps BACKTEST_LINE=$(echo "$TEST_OUTPUT" | grep "• Backtest Steps:") -BACKTEST_STEPS=$(echo "$BACKTEST_LINE" | sed 's/.*Backtest Steps: //' | sed 's/ms.*//' | xargs) +BACKTEST_STEPS=$(echo "$BACKTEST_LINE" | sed 's/.*Backtest Steps: //' | sed 's/ms.*//' | sed 's/,/./g' | xargs) # Extract timing metrics -AVG_SIGNAL_UPDATE=$(echo "$TEST_OUTPUT" | grep "• Average Signal Update:" | sed 's/.*Average Signal Update: //' | sed 's/ms.*//' | xargs) -AVG_BACKTEST_STEP=$(echo "$TEST_OUTPUT" | grep "• Average Backtest Step:" | sed 's/.*Average Backtest Step: //' | sed 's/ms.*//' | xargs) +AVG_SIGNAL_UPDATE=$(echo "$TEST_OUTPUT" | grep "• Average Signal Update:" | sed 's/.*Average Signal Update: //' | sed 's/ms.*//' | sed 's/,/./g' | xargs) +AVG_BACKTEST_STEP=$(echo "$TEST_OUTPUT" | grep "• Average Backtest Step:" | sed 's/.*Average Backtest Step: //' | sed 's/ms.*//' | sed 's/,/./g' | xargs) # Extract trading results -FINAL_PNL=$(echo "$TEST_OUTPUT" | grep "• Final PnL:" | sed 's/.*Final PnL: //' | xargs) +FINAL_PNL=$(echo "$TEST_OUTPUT" | grep "• Final PnL:" | sed 's/.*Final PnL: //' | sed 's/,/./g' | xargs) WIN_RATE=$(echo "$TEST_OUTPUT" | grep "• Win Rate:" | sed 's/.*Win Rate: //' | sed 's/%//' | xargs) -GROWTH_PERCENTAGE=$(echo "$TEST_OUTPUT" | grep "• Growth:" | sed 's/.*Growth: //' | sed 's/%//' | xargs) -SCORE=$(echo "$TEST_OUTPUT" | grep "• Score:" | sed 's/.*Score: //' | xargs) +GROWTH_PERCENTAGE=$(echo "$TEST_OUTPUT" | grep "• Growth:" | sed 's/.*Growth: //' | sed 's/%//' | sed 's/,/./g' | xargs) +SCORE=$(echo "$TEST_OUTPUT" | grep "• Score:" | sed 's/.*Score: //' | sed 's/[^0-9.-]//g' | xargs) # Set defaults for missing values CANDLES_COUNT=${CANDLES_COUNT:-0} diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index f019a431..13b66cf6 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -51,6 +51,19 @@ public class TradingBotBase : ITradingBot /// public Dictionary PreCalculatedIndicatorValues { get; set; } + // Cached properties for performance optimization + private bool? _isForBacktest; + private bool? _isForWatchingOnly; + private int? _maxLossStreak; + private int? _cooldownPeriod; + private bool? _flipPosition; + + private bool IsForBacktest => _isForBacktest ??= Config.IsForBacktest; + private bool IsForWatchingOnly => _isForWatchingOnly ??= Config.IsForWatchingOnly; + private int MaxLossStreak => _maxLossStreak ??= Config.MaxLossStreak; + private int CooldownPeriod => _cooldownPeriod ??= Config.CooldownPeriod; + private bool FlipPosition => _flipPosition ??= Config.FlipPosition; + public TradingBotBase( ILogger logger, @@ -70,7 +83,7 @@ public class TradingBotBase : ITradingBot public async Task Start(BotStatus previousStatus) { - if (!Config.IsForBacktest) + if (!IsForBacktest) { // Start async initialization in the background without blocking try @@ -94,17 +107,8 @@ public class TradingBotBase : ITradingBot switch (previousStatus) { case BotStatus.Saved: - var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList(); - var startupMessage = $"šŸš€ Bot Started Successfully\n\n" + - $"šŸ“Š Trading Setup:\n" + - $"šŸŽÆ Ticker: `{Config.Ticker}`\n" + - $"ā° Timeframe: `{Config.Timeframe}`\n" + - $"šŸŽ® Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" + - $"šŸ’° Balance: `${Config.BotTradingBalance:F2}`\n" + - $"šŸ‘€ Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" + - $"šŸ“ˆ Active Indicators: `{string.Join(", ", indicatorNames)}`\n\n" + - $"āœ… Ready to monitor signals and execute trades\n" + - $"šŸ“¢ Notifications will be sent when positions are triggered"; + var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()); + var startupMessage = $"šŸš€ Bot Started Successfully\n\nšŸ“Š Trading Setup:\nšŸŽÆ Ticker: `{Config.Ticker}`\nā° Timeframe: `{Config.Timeframe}`\nšŸŽ® Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\nšŸ’° Balance: `${Config.BotTradingBalance:F2}`\nšŸ‘€ Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\nšŸ“ˆ Active Indicators: `{string.Join(", ", indicatorNames)}`\n\nāœ… Ready to monitor signals and execute trades\nšŸ“¢ Notifications will be sent when positions are triggered"; await LogInformation(startupMessage); break; @@ -172,7 +176,7 @@ public class TradingBotBase : ITradingBot public async Task LoadAccount() { - if (Config.IsForBacktest) return; + if (IsForBacktest) return; await ServiceScopeHelpers.WithScopedService(_scopeFactory, async accountService => { var account = await accountService.GetAccountByAccountName(Config.AccountName, false, false); @@ -186,7 +190,7 @@ public class TradingBotBase : ITradingBot /// public async Task VerifyAndUpdateBalance() { - if (Config.IsForBacktest) return; + if (IsForBacktest) return; if (Account == null) { Logger.LogWarning("Cannot verify balance: Account is null"); @@ -233,40 +237,85 @@ public class TradingBotBase : ITradingBot public async Task Run() { - // Update signals for live trading only - if (!Config.IsForBacktest) + // Fast path for backtests - skip live trading operations + if (IsForBacktest) { - await UpdateSignals(); - await LoadLastCandle(); + if (!IsForWatchingOnly) + await ManagePositions(); + + UpdateWalletBalances(); + return; } - if (!Config.IsForWatchingOnly) + // Live trading path + await UpdateSignals(); + await LoadLastCandle(); + + if (!IsForWatchingOnly) await ManagePositions(); UpdateWalletBalances(); - if (!Config.IsForBacktest) + + ExecutionCount++; + + // Optimized logging - cache frequently used values + var serverDate = DateTime.UtcNow; + var lastCandleDate = LastCandle?.Date; + var signalCount = Signals.Count; + var positionCount = Positions.Count; + + Logger.LogInformation( + "Bot Status {Name} - ServerDate: {ServerDate}, LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Executions: {ExecutionCount}, Positions: {PositionCount}", + Config.Name, serverDate, lastCandleDate, signalCount, ExecutionCount, positionCount); + + // Optimize position logging - build string efficiently + if (positionCount > 0) { - ExecutionCount++; - - Logger.LogInformation( - "Bot Status {Name} - ServerDate: {ServerDate}, LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Executions: {ExecutionCount}, Positions: {PositionCount}", - Config.Name, DateTime.UtcNow, LastCandle?.Date, Signals.Count, ExecutionCount, Positions.Count); - + var positionStrings = new string[positionCount]; + var index = 0; + foreach (var position in Positions.Values) + { + positionStrings[index++] = $"{position.SignalIdentifier} - Status: {position.Status}"; + } Logger.LogInformation("[{Name}] Internal Positions : {Position}", Config.Name, - string.Join(", ", - Positions.Values.Select(p => $"{p.SignalIdentifier} - Status: {p.Status}"))); + string.Join(", ", positionStrings)); } } public async Task UpdateSignals(HashSet candles = null) { + // Fast path for backtests - skip live trading checks + if (IsForBacktest && candles != null) + { + var backtestSignal = + TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod, + PreCalculatedIndicatorValues); + if (backtestSignal == null) return; + await AddSignal(backtestSignal); + return; + } + + // Live trading path with checks // 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 - if (!Config.FlipPosition && Positions.Any(p => p.Value.IsOpen())) + if (!FlipPosition) { - Logger.LogDebug( - $"Skipping signal update: Position open and flip disabled. Open positions: {Positions.Count(p => p.Value.IsOpen())}"); - return; + var hasOpenPosition = false; + foreach (var position in Positions.Values) + { + if (position.IsOpen()) + { + hasOpenPosition = true; + break; + } + } + + if (hasOpenPosition) + { + Logger.LogDebug( + $"Skipping signal update: Position open and flip disabled. Open positions: {Positions.Count(p => p.Value.IsOpen())}"); + return; + } } // Check if we're in cooldown period for any direction @@ -276,24 +325,13 @@ public class TradingBotBase : ITradingBot return; } - if (Config.IsForBacktest && candles != null) + await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory => { - var backtestSignal = - TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod, - PreCalculatedIndicatorValues); - if (backtestSignal == null) return; - await AddSignal(backtestSignal); - } - else - { - await ServiceScopeHelpers.WithScopedService(_scopeFactory, async grainFactory => - { - var scenarioRunnerGrain = grainFactory.GetGrain(Guid.NewGuid()); - var signal = await scenarioRunnerGrain.GetSignals(Config, Signals, Account.Exchange, LastCandle); - if (signal == null) return; - await AddSignal(signal); - }); - } + var scenarioRunnerGrain = grainFactory.GetGrain(Guid.NewGuid()); + var signal = await scenarioRunnerGrain.GetSignals(Config, Signals, Account.Exchange, LastCandle); + if (signal == null) return; + await AddSignal(signal); + }); } private async Task RecreateSignalFromPosition(Position position) @@ -352,17 +390,40 @@ public class TradingBotBase : ITradingBot private async Task ManagePositions() { // Early exit optimization - skip if no positions to manage - var hasOpenPositions = Positions.Values.Any(p => !p.IsFinished()); - var hasWaitingSignals = Signals.Values.Any(s => s.Status == SignalStatus.WaitingForPosition); - + var hasOpenPositions = false; + var hasWaitingSignals = false; + + // Optimize: Use foreach instead of LINQ for better performance + foreach (var position in Positions.Values) + { + if (!position.IsFinished()) + { + hasOpenPositions = true; + break; + } + } + + if (!hasOpenPositions) + { + foreach (var signal in Signals.Values) + { + if (signal.Status == SignalStatus.WaitingForPosition) + { + hasWaitingSignals = true; + break; + } + } + } + if (!hasOpenPositions && !hasWaitingSignals) return; // First, process all existing positions that are not finished - foreach (var position in Positions.Values.Where(p => !p.IsFinished())) + foreach (var position in Positions.Values) { - var signalForPosition = Signals[position.SignalIdentifier]; - if (signalForPosition == null) + if (position.IsFinished()) continue; + + if (!Signals.TryGetValue(position.SignalIdentifier, out var signalForPosition)) { await LogInformation( $"šŸ” Signal Recovery\nSignal not found for position `{position.Identifier}`\nRecreating signal from position data..."); @@ -389,11 +450,9 @@ public class TradingBotBase : ITradingBot } // Then, open positions for signals waiting for a position open - // But first, check if we already have a position for any of these signals - var signalsWaitingForPosition = Signals.Values.Where(s => s.Status == SignalStatus.WaitingForPosition); - - foreach (var signal in signalsWaitingForPosition) + foreach (var signal in Signals.Values) { + if (signal.Status != SignalStatus.WaitingForPosition) continue; if (LastCandle != null && signal.Date < LastCandle.Date) { await LogWarning( @@ -432,23 +491,33 @@ public class TradingBotBase : ITradingBot return; } - if (!WalletBalances.ContainsKey(date)) + // Optimize: Use TryGetValue instead of ContainsKey + First() + if (!WalletBalances.TryGetValue(date, out _)) { - var previousBalance = WalletBalances.First().Value; - WalletBalances[date] = previousBalance + GetProfitAndLoss(); + // Cache the calculation to avoid repeated computation + var profitAndLoss = GetProfitAndLoss(); + var previousBalance = WalletBalances.Count > 0 ? WalletBalances.First().Value : Config.BotTradingBalance; + WalletBalances[date] = previousBalance + profitAndLoss; } } private async Task UpdatePosition(LightSignal signal, Position positionForSignal) { + // Skip processing if position is already canceled or rejected (never filled) + if (positionForSignal.Status == PositionStatus.Canceled || + positionForSignal.Status == PositionStatus.Rejected) + { + await LogDebug( + $"Skipping update for position {positionForSignal.Identifier} - status is {positionForSignal.Status} (never filled)"); + return; + } + try { - // Skip processing if position is already canceled or rejected (never filled) - if (positionForSignal.Status == PositionStatus.Canceled || - positionForSignal.Status == PositionStatus.Rejected) + // Fast path for backtests - simplified position handling + if (IsForBacktest) { - await LogDebug( - $"Skipping update for position {positionForSignal.Identifier} - status is {positionForSignal.Status} (never filled)"); + await UpdatePositionForBacktest(signal, positionForSignal); return; } @@ -456,23 +525,14 @@ public class TradingBotBase : ITradingBot var brokerPositions = await ServiceScopeHelpers.WithScopedService>( _scopeFactory, async tradingService => { - internalPosition = Config.IsForBacktest - ? positionForSignal - : await tradingService.GetPositionByIdentifierAsync(positionForSignal.Identifier); + internalPosition = await tradingService.GetPositionByIdentifierAsync(positionForSignal.Identifier); - if (Config.IsForBacktest) - { - return new List { internalPosition }; - } - else - { - return await ServiceScopeHelpers.WithScopedService>( - _scopeFactory, - async exchangeService => - { - return [.. await exchangeService.GetBrokerPositions(Account)]; - }); - } + return await ServiceScopeHelpers.WithScopedService>( + _scopeFactory, + async exchangeService => + { + return [.. await exchangeService.GetBrokerPositions(Account)]; + }); }); if (!Config.IsForBacktest) @@ -963,6 +1023,30 @@ public class TradingBotBase : ITradingBot } } + /// + /// Optimized position update method for backtests - skips live trading overhead + /// + private async Task UpdatePositionForBacktest(LightSignal signal, Position positionForSignal) + { + // For backtests, positions are filled immediately + if (positionForSignal.Status == PositionStatus.New) + { + positionForSignal.Status = PositionStatus.Filled; + await SetPositionStatus(signal.Identifier, PositionStatus.Filled); + SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen); + } + else if (positionForSignal.Status == PositionStatus.Filled) + { + // Handle position closing logic for backtests + await HandleClosedPosition(positionForSignal); + } + else if (positionForSignal.Status == PositionStatus.Finished || + positionForSignal.Status == PositionStatus.Flipped) + { + await HandleClosedPosition(positionForSignal); + } + } + private async Task UpdatePositionDatabase(Position position) { await ServiceScopeHelpers.WithScopedService(_scopeFactory, @@ -1131,20 +1215,20 @@ public class TradingBotBase : ITradingBot private async Task CanOpenPosition(LightSignal signal) { - // Early return if we're in backtest mode and haven't executed yet - // TODO : check if its a startup cycle - if (!Config.IsForBacktest && ExecutionCount == 0) + // Fast path for backtests - skip live trading checks + if (IsForBacktest) + { + return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal); + } + + // Live trading path + // Early return if we haven't executed yet + if (ExecutionCount == 0) { await LogInformation("ā³ Bot Not Ready\nCannot open position\nBot hasn't executed first cycle yet"); return false; } - // Check if we're in backtest mode - if (Config.IsForBacktest) - { - return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal); - } - // Check broker positions for live trading var canOpenPosition = await CheckBrokerPositions(); if (!canOpenPosition) @@ -1158,18 +1242,15 @@ public class TradingBotBase : ITradingBot decimal currentPrice = 0; await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService => { - currentPrice = Config.IsForBacktest - ? LastCandle?.Close ?? 0 - : await exchangeService.GetCurrentPrice(Account, Config.Ticker); + currentPrice = await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); - bool synthRisk = false; await ServiceScopeHelpers.WithScopedService(_scopeFactory, async tradingService => { synthRisk = await tradingService.AssessSynthPositionRiskAsync(Config.Ticker, signal.Direction, currentPrice, - Config, Config.IsForBacktest); + Config, false); }); if (!synthRisk) { @@ -1184,38 +1265,69 @@ public class TradingBotBase : ITradingBot private async Task CheckLossStreak(LightSignal signal) { // If MaxLossStreak is 0, there's no limit - if (Config.MaxLossStreak <= 0) + if (MaxLossStreak <= 0) { return true; } - // Get the last N finished positions regardless of direction - var recentPositions = Positions - .Values - .Where(p => p.IsFinished()) - .OrderByDescending(p => p.Open.Date) - .Take(Config.MaxLossStreak) - .ToList(); + // Optimize: Pre-allocate array and use manual sorting for better performance + var maxStreak = MaxLossStreak; + var recentPositions = new Position[maxStreak]; + var count = 0; + + // Collect recent finished positions manually for better performance + foreach (var position in Positions.Values) + { + if (!position.IsFinished()) continue; + + // Simple insertion sort by date (descending) + var insertIndex = 0; + while (insertIndex < count && recentPositions[insertIndex].Open.Date > position.Open.Date) + { + insertIndex++; + } + + if (insertIndex < maxStreak) + { + // Shift elements + for (var i = Math.Min(count, maxStreak - 1); i > insertIndex; i--) + { + recentPositions[i] = recentPositions[i - 1]; + } + + recentPositions[insertIndex] = position; + if (count < maxStreak) count++; + } + } // If we don't have enough positions to form a streak, we can open - if (recentPositions.Count < Config.MaxLossStreak) + if (count < maxStreak) { return true; } // Check if all recent positions were losses - var allLosses = recentPositions.All(p => p.ProfitAndLoss?.Realized < 0); + var allLosses = true; + for (var i = 0; i < count; i++) + { + if (recentPositions[i].ProfitAndLoss?.Realized >= 0) + { + allLosses = false; + break; + } + } + if (!allLosses) { return true; } // If we have a loss streak, check if the last position was in the same direction as the signal - var lastPosition = recentPositions.First(); + var lastPosition = recentPositions[0]; // First element is most recent due to descending sort if (lastPosition.OriginDirection == signal.Direction) { await LogWarning( - $"šŸ”„ Loss Streak Limit\nCannot open position\nMax loss streak: `{Config.MaxLossStreak}` reached\nšŸ“‰ Last `{recentPositions.Count}` trades were losses\nšŸŽÆ Last position: `{lastPosition.OriginDirection}`\nWaiting for opposite direction signal"); + $"šŸ”„ Loss Streak Limit\nCannot open position\nMax loss streak: `{maxStreak}` reached\nšŸ“‰ Last `{count}` trades were losses\nšŸŽÆ Last position: `{lastPosition.OriginDirection}`\nWaiting for opposite direction signal"); return false; } @@ -2030,8 +2142,21 @@ public class TradingBotBase : ITradingBot public int GetWinRate() { - var succeededPositions = Positions.Values.Where(p => p.IsValidForMetrics()).Count(p => p.IsInProfit()); - var total = Positions.Values.Where(p => p.IsValidForMetrics()).Count(); + // Optimize: Single pass through positions + var succeededPositions = 0; + var total = 0; + + foreach (var position in Positions.Values) + { + if (position.IsValidForMetrics()) + { + total++; + if (position.IsInProfit()) + { + succeededPositions++; + } + } + } if (total == 0) return 0; @@ -2041,9 +2166,15 @@ public class TradingBotBase : ITradingBot public decimal GetProfitAndLoss() { - // Calculate net PnL after deducting fees for each position - var netPnl = Positions.Values.Where(p => p.IsValidForMetrics() && p.ProfitAndLoss != null) - .Sum(p => p.GetPnLBeforeFees()); + // Optimize: Manual loop instead of LINQ for better performance + var netPnl = 0m; + foreach (var position in Positions.Values) + { + if (position.IsValidForMetrics() && position.ProfitAndLoss != null) + { + netPnl += position.GetPnLBeforeFees(); + } + } return netPnl; } @@ -2055,11 +2186,15 @@ public class TradingBotBase : ITradingBot /// Returns the total fees paid as a decimal value. public decimal GetTotalFees() { - decimal totalFees = 0; + // Optimize: Manual loop instead of LINQ + var totalFees = 0m; - foreach (var position in Positions.Values.Where(p => p.IsValidForMetrics())) + foreach (var position in Positions.Values) { - totalFees += TradingHelpers.CalculatePositionFees(position); + if (position.IsValidForMetrics()) + { + totalFees += TradingHelpers.CalculatePositionFees(position); + } } return totalFees; @@ -2580,8 +2715,8 @@ public class TradingBotBase : ITradingBot // Calculate cooldown end time based on last position closing time var baseIntervalSeconds = CandleHelpers.GetBaseIntervalInSeconds(Config.Timeframe); - var cooldownEndTime = LastPositionClosingTime.Value.AddSeconds(baseIntervalSeconds * Config.CooldownPeriod); - var isInCooldown = (Config.IsForBacktest ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime; + var cooldownEndTime = LastPositionClosingTime.Value.AddSeconds(baseIntervalSeconds * CooldownPeriod); + var isInCooldown = (IsForBacktest ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime; if (isInCooldown) { diff --git a/src/Managing.Workers.Tests/performance-benchmarks.csv b/src/Managing.Workers.Tests/performance-benchmarks.csv index 544ea723..410950ef 100644 --- a/src/Managing.Workers.Tests/performance-benchmarks.csv +++ b/src/Managing.Workers.Tests/performance-benchmarks.csv @@ -1,7 +1,9 @@ DateTime,TestName,CandlesCount,ExecutionTimeSeconds,ProcessingRateCandlesPerSec,MemoryStartMB,MemoryEndMB,MemoryPeakMB,SignalUpdatesCount,SignalUpdatesSkipped,SignalUpdateEfficiencyPercent,BacktestStepsCount,AverageSignalUpdateMs,AverageBacktestStepMs,FinalPnL,WinRatePercent,GrowthPercentage,Score,CommitHash,GitBranch,Environment 2025-11-11T12:00:00Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,2.15,2684.8,16.05,23.90,24.24,7706,3814,33.1,5760,0.26,0.01,4010.63,28,4.01,3.34,initial,dev,development -2025-11-11T04:13:28Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5,2.50,0,Start=15.26,End=24.11,Peak=23.66,0,0,0,0,0.28,0.03,401063,28,401,334,14bc98d5,dev,development -2025-11-11T04:14:08Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5 760,2.542.57,2244.2,15.27,24.08,23.72,2207.52,3814,33.1,200.48,0.29,0.03,4010,63,28,4,01,3,34,14bc98d5,dev,development -2025-11-11T04:14:39Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5 760,2.73 2.75,2091.2,15.26,24.36,23.99,2102.66,3814,33.1,372.82,0.27,0.06,4010,63,28,4,01,3,34,b0b757b1,dev,development -2025-11-11T04:16:43Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5 760,1.86 1.88,3061.1,15.26,23.95,23.67,1600.09,3814,33.1,115.52,0.21,0.02,4010,63,28,4,01,3,34,e5caf1cd,dev,development -2025-11-11T04:26:29Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5 760,1.19 1.20,4782.4,15.26,18.01,23.47,1068.51,3814,33.1,53.69,0.14,0.01,-2431,04,54,-2,43,0,00,14d101b6,dev,development +2025-11-11T04:14:08Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,2.54,2244.2,15.27,24.08,23.72,2207.52,3814,33.1,200.48,0.29,0.03,4010.63,28,4.01,3.34,14bc98d5,dev,development +2025-11-11T04:14:39Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,2.73,2091.2,15.26,24.36,23.99,2102.66,3814,33.1,372.82,0.27,0.06,4010.63,28,4.01,3.34,b0b757b1,dev,development +2025-11-11T04:16:43Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.87,3061.1,15.26,23.95,23.67,1600.09,3814,33.1,115.52,0.21,0.02,4010.63,28,4.01,3.34,e5caf1cd,dev,development +2025-11-11T04:26:29Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.20,4782.4,15.26,18.01,23.47,1068.51,3814,33.1,53.69,0.14,0.01,-2431.04,54,-2.43,0.00,14d101b6,dev,development +2025-11-11T04:31:06Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,2.14 2.17,2658.3,15.28,17.89,23.73,1875.99,3814,33.1,123.31,0.24,0.02,-2431.04,54,-2.43,0.00,47911c28,dev,development +2025-11-11T04:32:55Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.41 1.43,40376,15.26,17.79,23.47,1186.69,3814,33.1,90.22,0.15,0.02,-243104,54,-243,000,47911c28,dev,development +2025-11-11T04:34:42Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,2.61 2.63,2186.0,15.26,17.85,23.73,2329.99,3814,33.1,134.43,0.30,0.02,-2431.04,54,-2.43,0.00,47911c28,dev,development