From 46966cc5d87a8afbf49d6fee46ea0c0a2fb2d115 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Tue, 11 Nov 2025 12:21:50 +0700 Subject: [PATCH] perf: optimize TradingBotBase and TradingBox - reduce LINQ overhead and allocations (+31.1%) --- .DS_Store | Bin 10244 -> 10244 bytes scripts/.DS_Store | Bin 8196 -> 8196 bytes src/.DS_Store | Bin 12292 -> 12292 bytes .../Bots/TradingBotBase.cs | 74 +++++++++++++++--- .../Shared/Helpers/TradingBox.cs | 31 +++++--- .../performance-benchmarks.csv | 2 + 6 files changed, 86 insertions(+), 21 deletions(-) diff --git a/.DS_Store b/.DS_Store index b062e956dc8ba0cae492e897b62008fcdc546317..3a04a93bc40a291a788f056806cf820052166a51 100644 GIT binary patch delta 21 ZcmZn(XbISGO_Z^9^L5ex{9sC#2>@s;2+IHf delta 21 ZcmZn(XbISGO_VWp^L5ex{9sC#2>@lx2#Wv! diff --git a/scripts/.DS_Store b/scripts/.DS_Store index 7d4b09cc52b9c79040102a54052f1a0f602ed559..e52549c7dd964fc53c4f156c5287aae2daf18251 100644 GIT binary patch delta 1051 zcmd^-%TLr$6vq2i9=A2Z9T^8MI*tZW7(oJ}prYXs9tw()L0$?LTHq>laM~Hfg%cH7 z5|d&y@e!kO>oaamCUIdjvM_8g!G*YVW1=qH=q<&ie}S9ZeCOo*de86N>DuYqr_2ZD z)%B{sE5R*3IA(NCa@`*7(X1iE)VVp*%XP!F`7k$PiVp)y#MH)(g+Oi|W%+E= zzg&bCknZ>(KOg7z)WW)~Y^sLXR)xMKr*zFWjz)DiQD5n>WQY|K(&siBVum=qx?04- z3e`x&;uj4sqf{+ZsHH^CadXvMp;i*AE1hH7pfTpfRjK<3ZB@DwM$F<-(<|1fhlmd) zO7~E7d|V7fDaTJSZV{oRA(ju1?oQ63xvEoakU|fKqNIxUJdZP95SCx9A5#NI`Absm!TJ zzLH)`^U|X9QCgBdNz2k#>AUm`X-G#V2z(v|-QgWZ1r|arP5}_%<%qh<) zcQyn9eOd4eB->FWu#})wV5!D_96${k(Tu}5A|OR@65Z%QFNR=X1f$>rk&QSKNaC`< eG%Y~Q;yN76;Rf#EJ|5uViYvnuuFB8gXT!w=?r!xM}l!rzhu}-`APgOzdfy0_Lg8 z?U1sPkbYA)EY}=04cqG~iG~xFHf|^wlI6Kzq%>NlGUof4zVxtpCT+UYv{I7hf-6FF zbvYwPbV#Zrk8bz0gp#o{@xOo;9de_^`H)ki;p|(y8Om z!^RlZ<@}t+Cc=6%6Nc@WDT{U%+#M~1^^K&)$NBb-g4%)|i!(Opk3OI| zZue=!;%j9K^V`}P>vv9Qw$VLl88&@W@b`0nh=y_%zpYI--IVPYIe>%wmLs}W?HX}i z&z%yDlm=N3OG@;;G(@RsO_;9c^4)s)EA4n2!;P7&@LG5$%nM(Hufl@xP53VS6#gK9 zAVQ!}prQ)ZsKEwoK@4%Uq7#R37(Gbf7>1$4z$j8kV-i!C#syr)6IP#3MY$ z6THAnyh0Xl@c|$43G-OMFD&9W#a~etiMU)W7E8oZG3qUb2Lo#R-VSe-GV6UVKI#2X zqM0NoZ%j!(J-Tczmv#kNBXRO{X zM3EQUNVCjSrGdQMNp{KpUqywSR$J9QQh+Bzz54KGuOt!~popLX>o^!6OC1`~gzacX z3);|*4(#KI5=f#KedxysM>LA#Fk!=iiwR_Ko@2U*OB~X5+<=FhxP!a6j|b0YBiW+= I?c#Iz2SEhqp#T5? diff --git a/src/.DS_Store b/src/.DS_Store index 88317301afc8b2aa14e4b5884a7a411b0c69c2b0..59dd6be18dc67c6a6e9aac518ce8e889682bf57f 100644 GIT binary patch delta 62 zcmZokXi1ph&uG3eU^hRb>}DPT8BRd~2499mhCConXUGK7>63S>DRD9A{RaaEhRv#y M%h)z^EBxdK07DWKO8@`> delta 33 pcmZokXi1ph&uF$WU^hRb%w`?|8P3fal8f0UD=<22=2rO04* !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