diff --git a/.DS_Store b/.DS_Store index b062e956..3a04a93b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/scripts/.DS_Store b/scripts/.DS_Store index 7d4b09cc..e52549c7 100644 Binary files a/scripts/.DS_Store and b/scripts/.DS_Store differ diff --git a/src/.DS_Store b/src/.DS_Store index 88317301..59dd6be1 100644 Binary files a/src/.DS_Store and b/src/.DS_Store differ diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index f019a431..e07f4ad4 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -352,15 +352,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); + // Optimized: Use for loop to avoid multiple iterations + bool hasOpenPositions = false; + foreach (var position in Positions.Values) + { + if (!position.IsFinished()) + { + hasOpenPositions = true; + break; + } + } + + bool hasWaitingSignals = false; + if (!hasOpenPositions) // Only check signals if no open positions + { + 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())) + // Optimized: Inline the filter to avoid LINQ overhead + foreach (var position in Positions.Values) { + if (position.IsFinished()) + continue; + var signalForPosition = Signals[position.SignalIdentifier]; if (signalForPosition == null) { @@ -2030,20 +2055,41 @@ 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(); + // Optimized: Single iteration instead of multiple LINQ queries + int succeededPositions = 0; + int totalPositions = 0; - if (total == 0) + foreach (var position in Positions.Values) + { + if (position.IsValidForMetrics()) + { + totalPositions++; + if (position.IsInProfit()) + { + succeededPositions++; + } + } + } + + if (totalPositions == 0) return 0; - return (succeededPositions * 100) / total; + return (succeededPositions * 100) / totalPositions; } 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()); + // Optimized: Single iteration instead of LINQ chaining + decimal netPnl = 0; + + foreach (var position in Positions.Values) + { + if (position.IsValidForMetrics() && position.ProfitAndLoss != null) + { + netPnl += position.GetPnLBeforeFees(); + } + } + return netPnl; } @@ -2055,11 +2101,15 @@ public class TradingBotBase : ITradingBot /// Returns the total fees paid as a decimal value. public decimal GetTotalFees() { + // Optimized: Avoid LINQ Where overhead, inline the check decimal totalFees = 0; - 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; diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 4fee2d4a..3b0fe887 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -75,10 +75,19 @@ public static class TradingBox Dictionary preCalculatedIndicatorValues) { var signalOnCandles = new List(); - // Optimize list creation - avoid redundant allocations - var limitedCandles = newCandles.Count <= 600 - ? newCandles.OrderBy(c => c.Date).ToList() - : newCandles.OrderBy(c => c.Date).TakeLast(600).ToList(); + // Optimize list creation - avoid redundant allocations and multiple ordering + List limitedCandles; + if (newCandles.Count <= 600) + { + // For small sets, just order once + limitedCandles = newCandles.OrderBy(c => c.Date).ToList(); + } + else + { + // For large sets, use more efficient approach: sort then take last + var sorted = newCandles.OrderBy(c => c.Date).ToList(); + limitedCandles = sorted.Skip(sorted.Count - 600).ToList(); + } foreach (var indicator in lightScenario.Indicators) { @@ -97,7 +106,8 @@ public static class TradingBox signals = indicatorInstance.Run(newCandles); } - if (signals == null || signals.Count() == 0) + // Optimized: Use Count property instead of Count() LINQ method + if (signals == null || signals.Count == 0) { // For trend and context strategies, lack of signal might be meaningful // Signal strategies are expected to be sparse, so we continue @@ -112,10 +122,11 @@ public static class TradingBox continue; } - // Ensure limitedCandles is ordered chronologically - var orderedCandles = limitedCandles.OrderBy(c => c.Date).ToList(); + // Optimized: limitedCandles is already ordered, no need to re-order var loopback = loopbackPeriod.HasValue && loopbackPeriod > 1 ? loopbackPeriod.Value : 1; - var candleLoopback = orderedCandles.TakeLast(loopback).ToList(); + var candleLoopback = limitedCandles.Count > loopback + ? limitedCandles.Skip(limitedCandles.Count - loopback).ToList() + : limitedCandles; if (!candleLoopback.Any()) { @@ -174,7 +185,9 @@ public static class TradingBox return signalOnCandles.Single(); } - signalOnCandles = signalOnCandles.OrderBy(s => s.Date).ToHashSet(); + // Optimized: Sort only if needed, then convert to HashSet + var orderedSignals = signalOnCandles.OrderBy(s => s.Date).ToList(); + signalOnCandles = new HashSet(orderedSignals); // Check if all strategies produced signals - this is required for composite signals var strategyNames = scenario.Indicators.Select(s => s.Name).ToHashSet(); diff --git a/src/Managing.Workers.Tests/performance-benchmarks.csv b/src/Managing.Workers.Tests/performance-benchmarks.csv index a5d0486b..229d1b56 100644 --- a/src/Managing.Workers.Tests/performance-benchmarks.csv +++ b/src/Managing.Workers.Tests/performance-benchmarks.csv @@ -20,3 +20,5 @@ DateTime,TestName,CandlesCount,ExecutionTimeSeconds,ProcessingRateCandlesPerSec, 2025-11-11T04:57:14Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.99,2883.5,15.26,13.73,25.11,1589.82,3828,33.2,258.98,0.21,0.04,24560.79,38,24.56,6015,2a0fbf9b,dev,development 2025-11-11T04:59:09Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,2.695,2127.6,15.26,13.64,24.65,2283.69,3828,33.2,209.33,0.30,0.04,24560.79,38,24.56,6015,2a0fbf9b,dev,development 2025-11-11T05:13:30Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,2.49,2300.8,15.27,13.68,25.14,2085.01,3828,33.2,232.91,0.27,0.04,24560.79,38,24.56,6015,2a0fbf9b,dev,development +2025-11-11T05:18:07Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.325,4316.5,15.25,13.83,24.63,1119.29,3828,33.2,112.94,0.15,0.02,24560.79,38,24.56,6015,1792cd23,dev,development +2025-11-11T05:21:03Z,ExecuteBacktest_With_Large_Dataset_Should_Show_Performance_Telemetry,5760,1.015,5659.9,15.27,10.17,24.65,886.92,3828,33.2,58.10,0.12,0.01,24560.79,38,24.56,6015,1792cd23,dev,development