perf: optimize TradingBotBase and TradingBox - reduce LINQ overhead and allocations (+31.1%)

This commit is contained in:
2025-11-11 12:21:50 +07:00
parent 1792cd2371
commit 46966cc5d8
6 changed files with 86 additions and 21 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
scripts/.DS_Store vendored

Binary file not shown.

BIN
src/.DS_Store vendored

Binary file not shown.

View File

@@ -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,12 +2101,16 @@ public class TradingBotBase : ITradingBot
/// <returns>Returns the total fees paid as a decimal value.</returns>
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)
{
if (position.IsValidForMetrics())
{
totalFees += TradingHelpers.CalculatePositionFees(position);
}
}
return totalFees;
}

View File

@@ -75,10 +75,19 @@ public static class TradingBox
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
{
var signalOnCandles = new List<LightSignal>();
// 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<Candle> 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<LightSignal>(orderedSignals);
// Check if all strategies produced signals - this is required for composite signals
var strategyNames = scenario.Indicators.Select(s => s.Name).ToHashSet();

View File

@@ -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
1 DateTime TestName CandlesCount ExecutionTimeSeconds ProcessingRateCandlesPerSec MemoryStartMB MemoryEndMB MemoryPeakMB SignalUpdatesCount SignalUpdatesSkipped SignalUpdateEfficiencyPercent BacktestStepsCount AverageSignalUpdateMs AverageBacktestStepMs FinalPnL WinRatePercent GrowthPercentage Score CommitHash GitBranch Environment
20 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
21 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
22 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
23 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
24 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