Add benchmark + fix bundle that should be completed

This commit is contained in:
2025-11-11 11:35:48 +07:00
parent 47911c28f1
commit 567de2e5ee
3 changed files with 271 additions and 134 deletions

View File

@@ -45,9 +45,9 @@ else
exit 1 exit 1
fi fi
# Extract performance metrics from the output # Extract performance metrics from the output - use more robust parsing
CANDLES_COUNT=$(echo "$TEST_OUTPUT" | grep "📈 Total Candles Processed:" | sed 's/.*: //' | sed 's// /' | tr -d ',' | xargs) 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' | 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) PROCESSING_RATE=$(echo "$TEST_OUTPUT" | grep "🚀 Processing Rate:" | sed 's/.*: //' | sed 's/ candles\/sec//' | sed 's/,/./g' | xargs)
# Extract memory metrics # Extract memory metrics
@@ -58,23 +58,23 @@ MEMORY_PEAK=$(echo "$MEMORY_LINE" | sed 's/.*Peak=//' | sed 's/MB.*//' | xargs)
# Extract signal update metrics # Extract signal update metrics
SIGNAL_LINE=$(echo "$TEST_OUTPUT" | grep "• Signal Updates:") 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_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) SIGNAL_EFFICIENCY=$(echo "$SIGNAL_LINE" | grep -o "[0-9.]*% efficiency" | sed 's/% efficiency//' | xargs)
# Extract backtest steps # Extract backtest steps
BACKTEST_LINE=$(echo "$TEST_OUTPUT" | grep "• 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 # Extract timing metrics
AVG_SIGNAL_UPDATE=$(echo "$TEST_OUTPUT" | grep "• Average Signal Update:" | sed 's/.*Average Signal Update: //' | 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.*//' | 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 # 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) 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) 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: //' | xargs) SCORE=$(echo "$TEST_OUTPUT" | grep "• Score:" | sed 's/.*Score: //' | sed 's/[^0-9.-]//g' | xargs)
# Set defaults for missing values # Set defaults for missing values
CANDLES_COUNT=${CANDLES_COUNT:-0} CANDLES_COUNT=${CANDLES_COUNT:-0}

View File

@@ -51,6 +51,19 @@ public class TradingBotBase : ITradingBot
/// </summary> /// </summary>
public Dictionary<IndicatorType, IndicatorsResultBase> PreCalculatedIndicatorValues { get; set; } public Dictionary<IndicatorType, IndicatorsResultBase> 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( public TradingBotBase(
ILogger<TradingBotBase> logger, ILogger<TradingBotBase> logger,
@@ -70,7 +83,7 @@ public class TradingBotBase : ITradingBot
public async Task Start(BotStatus previousStatus) public async Task Start(BotStatus previousStatus)
{ {
if (!Config.IsForBacktest) if (!IsForBacktest)
{ {
// Start async initialization in the background without blocking // Start async initialization in the background without blocking
try try
@@ -94,17 +107,8 @@ public class TradingBotBase : ITradingBot
switch (previousStatus) switch (previousStatus)
{ {
case BotStatus.Saved: case BotStatus.Saved:
var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList(); var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString());
var startupMessage = $"🚀 Bot Started Successfully\n\n" + 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";
$"📊 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); await LogInformation(startupMessage);
break; break;
@@ -172,7 +176,7 @@ public class TradingBotBase : ITradingBot
public async Task LoadAccount() public async Task LoadAccount()
{ {
if (Config.IsForBacktest) return; if (IsForBacktest) return;
await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService => await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService =>
{ {
var account = await accountService.GetAccountByAccountName(Config.AccountName, false, false); var account = await accountService.GetAccountByAccountName(Config.AccountName, false, false);
@@ -186,7 +190,7 @@ public class TradingBotBase : ITradingBot
/// </summary> /// </summary>
public async Task VerifyAndUpdateBalance() public async Task VerifyAndUpdateBalance()
{ {
if (Config.IsForBacktest) return; if (IsForBacktest) return;
if (Account == null) if (Account == null)
{ {
Logger.LogWarning("Cannot verify balance: Account is null"); Logger.LogWarning("Cannot verify balance: Account is null");
@@ -233,41 +237,86 @@ public class TradingBotBase : ITradingBot
public async Task Run() public async Task Run()
{ {
// Update signals for live trading only // Fast path for backtests - skip live trading operations
if (!Config.IsForBacktest) if (IsForBacktest)
{ {
await UpdateSignals(); if (!IsForWatchingOnly)
await LoadLastCandle();
}
if (!Config.IsForWatchingOnly)
await ManagePositions(); await ManagePositions();
UpdateWalletBalances(); UpdateWalletBalances();
if (!Config.IsForBacktest) return;
{ }
// Live trading path
await UpdateSignals();
await LoadLastCandle();
if (!IsForWatchingOnly)
await ManagePositions();
UpdateWalletBalances();
ExecutionCount++; 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( Logger.LogInformation(
"Bot Status {Name} - ServerDate: {ServerDate}, LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Executions: {ExecutionCount}, Positions: {PositionCount}", "Bot Status {Name} - ServerDate: {ServerDate}, LastCandleDate: {LastCandleDate}, Signals: {SignalCount}, Executions: {ExecutionCount}, Positions: {PositionCount}",
Config.Name, DateTime.UtcNow, LastCandle?.Date, Signals.Count, ExecutionCount, Positions.Count); Config.Name, serverDate, lastCandleDate, signalCount, ExecutionCount, positionCount);
// Optimize position logging - build string efficiently
if (positionCount > 0)
{
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, Logger.LogInformation("[{Name}] Internal Positions : {Position}", Config.Name,
string.Join(", ", string.Join(", ", positionStrings));
Positions.Values.Select(p => $"{p.SignalIdentifier} - Status: {p.Status}")));
} }
} }
public async Task UpdateSignals(HashSet<Candle> candles = null) public async Task UpdateSignals(HashSet<Candle> 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 // 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 // This prevents unnecessary indicator calculations when we can't act on signals anyway
if (!Config.FlipPosition && Positions.Any(p => p.Value.IsOpen())) if (!FlipPosition)
{
var hasOpenPosition = false;
foreach (var position in Positions.Values)
{
if (position.IsOpen())
{
hasOpenPosition = true;
break;
}
}
if (hasOpenPosition)
{ {
Logger.LogDebug( 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. Open positions: {Positions.Count(p => p.Value.IsOpen())}");
return; return;
} }
}
// Check if we're in cooldown period for any direction // Check if we're in cooldown period for any direction
if (await IsInCooldownPeriodAsync()) if (await IsInCooldownPeriodAsync())
@@ -276,16 +325,6 @@ public class TradingBotBase : ITradingBot
return; return;
} }
if (Config.IsForBacktest && candles != null)
{
var backtestSignal =
TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod,
PreCalculatedIndicatorValues);
if (backtestSignal == null) return;
await AddSignal(backtestSignal);
}
else
{
await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory => await ServiceScopeHelpers.WithScopedService<IGrainFactory>(_scopeFactory, async grainFactory =>
{ {
var scenarioRunnerGrain = grainFactory.GetGrain<IScenarioRunnerGrain>(Guid.NewGuid()); var scenarioRunnerGrain = grainFactory.GetGrain<IScenarioRunnerGrain>(Guid.NewGuid());
@@ -294,7 +333,6 @@ public class TradingBotBase : ITradingBot
await AddSignal(signal); await AddSignal(signal);
}); });
} }
}
private async Task<LightSignal> RecreateSignalFromPosition(Position position) private async Task<LightSignal> RecreateSignalFromPosition(Position position)
{ {
@@ -352,17 +390,40 @@ public class TradingBotBase : ITradingBot
private async Task ManagePositions() private async Task ManagePositions()
{ {
// Early exit optimization - skip if no positions to manage // Early exit optimization - skip if no positions to manage
var hasOpenPositions = Positions.Values.Any(p => !p.IsFinished()); var hasOpenPositions = false;
var hasWaitingSignals = Signals.Values.Any(s => s.Status == SignalStatus.WaitingForPosition); 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) if (!hasOpenPositions && !hasWaitingSignals)
return; return;
// First, process all existing positions that are not finished // 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 (position.IsFinished()) continue;
if (signalForPosition == null)
if (!Signals.TryGetValue(position.SignalIdentifier, out var signalForPosition))
{ {
await LogInformation( await LogInformation(
$"🔍 Signal Recovery\nSignal not found for position `{position.Identifier}`\nRecreating signal from position data..."); $"🔍 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 // Then, open positions for signals waiting for a position open
// But first, check if we already have a position for any of these signals foreach (var signal in Signals.Values)
var signalsWaitingForPosition = Signals.Values.Where(s => s.Status == SignalStatus.WaitingForPosition);
foreach (var signal in signalsWaitingForPosition)
{ {
if (signal.Status != SignalStatus.WaitingForPosition) continue;
if (LastCandle != null && signal.Date < LastCandle.Date) if (LastCandle != null && signal.Date < LastCandle.Date)
{ {
await LogWarning( await LogWarning(
@@ -432,16 +491,17 @@ public class TradingBotBase : ITradingBot
return; return;
} }
if (!WalletBalances.ContainsKey(date)) // Optimize: Use TryGetValue instead of ContainsKey + First()
if (!WalletBalances.TryGetValue(date, out _))
{ {
var previousBalance = WalletBalances.First().Value; // Cache the calculation to avoid repeated computation
WalletBalances[date] = previousBalance + GetProfitAndLoss(); 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) private async Task UpdatePosition(LightSignal signal, Position positionForSignal)
{
try
{ {
// Skip processing if position is already canceled or rejected (never filled) // Skip processing if position is already canceled or rejected (never filled)
if (positionForSignal.Status == PositionStatus.Canceled || if (positionForSignal.Status == PositionStatus.Canceled ||
@@ -452,27 +512,27 @@ public class TradingBotBase : ITradingBot
return; return;
} }
try
{
// Fast path for backtests - simplified position handling
if (IsForBacktest)
{
await UpdatePositionForBacktest(signal, positionForSignal);
return;
}
Position internalPosition = null; Position internalPosition = null;
var brokerPositions = await ServiceScopeHelpers.WithScopedService<ITradingService, List<Position>>( var brokerPositions = await ServiceScopeHelpers.WithScopedService<ITradingService, List<Position>>(
_scopeFactory, async tradingService => _scopeFactory, async tradingService =>
{ {
internalPosition = Config.IsForBacktest internalPosition = await tradingService.GetPositionByIdentifierAsync(positionForSignal.Identifier);
? positionForSignal
: await tradingService.GetPositionByIdentifierAsync(positionForSignal.Identifier);
if (Config.IsForBacktest)
{
return new List<Position> { internalPosition };
}
else
{
return await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Position>>( return await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Position>>(
_scopeFactory, _scopeFactory,
async exchangeService => async exchangeService =>
{ {
return [.. await exchangeService.GetBrokerPositions(Account)]; return [.. await exchangeService.GetBrokerPositions(Account)];
}); });
}
}); });
if (!Config.IsForBacktest) if (!Config.IsForBacktest)
@@ -963,6 +1023,30 @@ public class TradingBotBase : ITradingBot
} }
} }
/// <summary>
/// Optimized position update method for backtests - skips live trading overhead
/// </summary>
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) private async Task UpdatePositionDatabase(Position position)
{ {
await ServiceScopeHelpers.WithScopedService<ITradingService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<ITradingService>(_scopeFactory,
@@ -1131,20 +1215,20 @@ public class TradingBotBase : ITradingBot
private async Task<bool> CanOpenPosition(LightSignal signal) private async Task<bool> CanOpenPosition(LightSignal signal)
{ {
// Early return if we're in backtest mode and haven't executed yet // Fast path for backtests - skip live trading checks
// TODO : check if its a startup cycle if (IsForBacktest)
if (!Config.IsForBacktest && ExecutionCount == 0) {
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"); await LogInformation("⏳ Bot Not Ready\nCannot open position\nBot hasn't executed first cycle yet");
return false; return false;
} }
// Check if we're in backtest mode
if (Config.IsForBacktest)
{
return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal);
}
// Check broker positions for live trading // Check broker positions for live trading
var canOpenPosition = await CheckBrokerPositions(); var canOpenPosition = await CheckBrokerPositions();
if (!canOpenPosition) if (!canOpenPosition)
@@ -1158,18 +1242,15 @@ public class TradingBotBase : ITradingBot
decimal currentPrice = 0; decimal currentPrice = 0;
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, async exchangeService => await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, async exchangeService =>
{ {
currentPrice = Config.IsForBacktest currentPrice = await exchangeService.GetCurrentPrice(Account, Config.Ticker);
? LastCandle?.Close ?? 0
: await exchangeService.GetCurrentPrice(Account, Config.Ticker);
}); });
bool synthRisk = false; bool synthRisk = false;
await ServiceScopeHelpers.WithScopedService<ITradingService>(_scopeFactory, async tradingService => await ServiceScopeHelpers.WithScopedService<ITradingService>(_scopeFactory, async tradingService =>
{ {
synthRisk = await tradingService.AssessSynthPositionRiskAsync(Config.Ticker, signal.Direction, synthRisk = await tradingService.AssessSynthPositionRiskAsync(Config.Ticker, signal.Direction,
currentPrice, currentPrice,
Config, Config.IsForBacktest); Config, false);
}); });
if (!synthRisk) if (!synthRisk)
{ {
@@ -1184,38 +1265,69 @@ public class TradingBotBase : ITradingBot
private async Task<bool> CheckLossStreak(LightSignal signal) private async Task<bool> CheckLossStreak(LightSignal signal)
{ {
// If MaxLossStreak is 0, there's no limit // If MaxLossStreak is 0, there's no limit
if (Config.MaxLossStreak <= 0) if (MaxLossStreak <= 0)
{ {
return true; return true;
} }
// Get the last N finished positions regardless of direction // Optimize: Pre-allocate array and use manual sorting for better performance
var recentPositions = Positions var maxStreak = MaxLossStreak;
.Values var recentPositions = new Position[maxStreak];
.Where(p => p.IsFinished()) var count = 0;
.OrderByDescending(p => p.Open.Date)
.Take(Config.MaxLossStreak) // Collect recent finished positions manually for better performance
.ToList(); 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 we don't have enough positions to form a streak, we can open
if (recentPositions.Count < Config.MaxLossStreak) if (count < maxStreak)
{ {
return true; return true;
} }
// Check if all recent positions were losses // 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) if (!allLosses)
{ {
return true; return true;
} }
// If we have a loss streak, check if the last position was in the same direction as the signal // 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) if (lastPosition.OriginDirection == signal.Direction)
{ {
await LogWarning( 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; return false;
} }
@@ -2030,8 +2142,21 @@ public class TradingBotBase : ITradingBot
public int GetWinRate() public int GetWinRate()
{ {
var succeededPositions = Positions.Values.Where(p => p.IsValidForMetrics()).Count(p => p.IsInProfit()); // Optimize: Single pass through positions
var total = Positions.Values.Where(p => p.IsValidForMetrics()).Count(); var succeededPositions = 0;
var total = 0;
foreach (var position in Positions.Values)
{
if (position.IsValidForMetrics())
{
total++;
if (position.IsInProfit())
{
succeededPositions++;
}
}
}
if (total == 0) if (total == 0)
return 0; return 0;
@@ -2041,9 +2166,15 @@ public class TradingBotBase : ITradingBot
public decimal GetProfitAndLoss() public decimal GetProfitAndLoss()
{ {
// Calculate net PnL after deducting fees for each position // Optimize: Manual loop instead of LINQ for better performance
var netPnl = Positions.Values.Where(p => p.IsValidForMetrics() && p.ProfitAndLoss != null) var netPnl = 0m;
.Sum(p => p.GetPnLBeforeFees()); foreach (var position in Positions.Values)
{
if (position.IsValidForMetrics() && position.ProfitAndLoss != null)
{
netPnl += position.GetPnLBeforeFees();
}
}
return netPnl; return netPnl;
} }
@@ -2055,12 +2186,16 @@ public class TradingBotBase : ITradingBot
/// <returns>Returns the total fees paid as a decimal value.</returns> /// <returns>Returns the total fees paid as a decimal value.</returns>
public decimal GetTotalFees() 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)
{
if (position.IsValidForMetrics())
{ {
totalFees += TradingHelpers.CalculatePositionFees(position); totalFees += TradingHelpers.CalculatePositionFees(position);
} }
}
return totalFees; return totalFees;
} }
@@ -2580,8 +2715,8 @@ public class TradingBotBase : ITradingBot
// Calculate cooldown end time based on last position closing time // Calculate cooldown end time based on last position closing time
var baseIntervalSeconds = CandleHelpers.GetBaseIntervalInSeconds(Config.Timeframe); var baseIntervalSeconds = CandleHelpers.GetBaseIntervalInSeconds(Config.Timeframe);
var cooldownEndTime = LastPositionClosingTime.Value.AddSeconds(baseIntervalSeconds * Config.CooldownPeriod); var cooldownEndTime = LastPositionClosingTime.Value.AddSeconds(baseIntervalSeconds * CooldownPeriod);
var isInCooldown = (Config.IsForBacktest ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime; var isInCooldown = (IsForBacktest ? LastCandle.Date : DateTime.UtcNow) < cooldownEndTime;
if (isInCooldown) if (isInCooldown)
{ {

View File

@@ -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 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-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,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: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,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: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,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: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,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: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: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
1 DateTime,TestName,CandlesCount,ExecutionTimeSeconds,ProcessingRateCandlesPerSec,MemoryStartMB,MemoryEndMB,MemoryPeakMB,SignalUpdatesCount,SignalUpdatesSkipped,SignalUpdateEfficiencyPercent,BacktestStepsCount,AverageSignalUpdateMs,AverageBacktestStepMs,FinalPnL,WinRatePercent,GrowthPercentage,Score,CommitHash,GitBranch,Environment DateTime TestName CandlesCount ExecutionTimeSeconds ProcessingRateCandlesPerSec MemoryStartMB MemoryEndMB MemoryPeakMB SignalUpdatesCount SignalUpdatesSkipped SignalUpdateEfficiencyPercent BacktestStepsCount AverageSignalUpdateMs AverageBacktestStepMs FinalPnL WinRatePercent GrowthPercentage Score CommitHash GitBranch Environment
2 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-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
3 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 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
4 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 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
5 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 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
6 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 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
7 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: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
8 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
9 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