Refactor BacktestExecutor and TradingBotBase for performance optimizations; remove unused SignalCache and pre-calculation logic; implement caching for open position state and streamline signal access with TryGetValue; enhance logging for detailed timing breakdown during backtest execution.

This commit is contained in:
2025-12-20 10:05:07 +07:00
parent 415845ed5a
commit e9b4878ffa
5 changed files with 91 additions and 120 deletions

View File

@@ -47,6 +47,9 @@ public abstract class TradingBotBase : ITradingBot
public Guid Identifier { get; set; } = Guid.Empty;
public Candle LastCandle { get; set; }
public DateTime? LastPositionClosingTime { get; set; }
// OPTIMIZATION 2: Cache open position state to avoid expensive Positions.Any() calls
private bool _hasOpenPosition = false;
public TradingBotBase(
ILogger<TradingBotBase> logger,
@@ -264,12 +267,13 @@ public abstract class TradingBotBase : ITradingBot
protected virtual async Task UpdateSignalsCore(IReadOnlyList<Candle> candles,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null)
{
// OPTIMIZATION 2: Use cached open position state instead of expensive Positions.Any() call
// 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 (!Config.FlipPosition && _hasOpenPosition)
{
Logger.LogDebug(
$"Skipping signal update: Position open and flip disabled. Open positions: {Positions.Count(p => p.Value.IsOpen())}");
$"Skipping signal update: Position open and flip disabled.");
return;
}
@@ -338,20 +342,19 @@ public abstract class TradingBotBase : ITradingBot
protected async Task ManagePositions()
{
// Early exit optimization - skip if no positions to manage
// Optimized: Use for loop to avoid multiple iterations
bool hasOpenPositions = false;
// OPTIMIZATION 6: Combine early exit checks and collect unfinished positions in one pass
// Collect unfinished positions in first iteration to avoid LINQ Where() later
var unfinishedPositions = new List<Position>();
foreach (var position in Positions.Values)
{
if (!position.IsFinished())
{
hasOpenPositions = true;
break;
unfinishedPositions.Add(position);
}
}
bool hasWaitingSignals = false;
if (!hasOpenPositions) // Only check signals if no open positions
if (unfinishedPositions.Count == 0) // Only check signals if no open positions
{
foreach (var signal in Signals.Values)
{
@@ -363,14 +366,14 @@ public abstract class TradingBotBase : ITradingBot
}
}
if (!hasOpenPositions && !hasWaitingSignals)
if (unfinishedPositions.Count == 0 && !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 unfinishedPositions)
{
var signalForPosition = Signals[position.SignalIdentifier];
if (signalForPosition == null)
// OPTIMIZATION 3: Use TryGetValue instead of direct dictionary access
if (!Signals.TryGetValue(position.SignalIdentifier, out var signalForPosition))
{
await LogInformation(
$"🔍 Signal Recovery\nSignal not found for position `{position.Identifier}`\nRecreating signal from position data...");
@@ -873,7 +876,13 @@ public abstract class TradingBotBase : ITradingBot
if (openedPosition != null)
{
var previousSignal = Signals[openedPosition.SignalIdentifier];
// OPTIMIZATION 3: Use TryGetValue instead of direct dictionary access
if (!Signals.TryGetValue(openedPosition.SignalIdentifier, out var previousSignal))
{
// Signal not found, expire new signal and return
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
return null;
}
if (openedPosition.OriginDirection == signal.Direction)
{
@@ -908,6 +917,9 @@ public abstract class TradingBotBase : ITradingBot
{
// Add position to internal collection before any status updates
Positions[position.Identifier] = position;
// OPTIMIZATION 2: Update cached open position state
_hasOpenPosition = true;
if (position.Open.Status != TradeStatus.Cancelled && position.Status != PositionStatus.Rejected)
{
@@ -1229,6 +1241,9 @@ public abstract class TradingBotBase : ITradingBot
SkipCandleBasedCalculation:
await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished);
// OPTIMIZATION 2: Update cached open position state after closing position
_hasOpenPosition = Positions.Values.Any(p => p.IsOpen());
// Update position in database with all trade changes
if (TradingBox.IsLiveTrading(Config.TradingType))
@@ -1413,9 +1428,10 @@ public abstract class TradingBotBase : ITradingBot
protected void SetSignalStatus(string signalIdentifier, SignalStatus signalStatus)
{
if (Signals.ContainsKey(signalIdentifier) && Signals[signalIdentifier].Status != signalStatus)
// OPTIMIZATION 4: Use TryGetValue instead of ContainsKey + direct access (single lookup)
if (Signals.TryGetValue(signalIdentifier, out var signal) && signal.Status != signalStatus)
{
Signals[signalIdentifier].Status = signalStatus;
signal.Status = signalStatus;
Logger.LogDebug($"Signal {signalIdentifier} is now {signalStatus}");
}
}
@@ -1470,6 +1486,13 @@ public abstract class TradingBotBase : ITradingBot
{
try
{
// OPTIMIZATION 1: Early return for backtest - skip all logging and validation
if (TradingBox.IsBacktestTrading(Config.TradingType))
{
Signals.Add(signal.Identifier, signal);
return;
}
// Set signal status based on configuration
if (Config.IsForWatchingOnly || (ExecutionCount < 1 && TradingBox.IsLiveTrading(Config.TradingType)))
{