Refactoring TradingBotBase.cs + clean architecture (#38)

* Refactoring TradingBotBase.cs + clean architecture

* Fix basic tests

* Fix tests

* Fix workers

* Fix open positions

* Fix closing position stucking the grain

* Fix comments

* Refactor candle handling to use IReadOnlyList for chronological order preservation across various components
This commit is contained in:
Oda
2025-12-01 19:32:06 +07:00
committed by GitHub
parent ab26260f6d
commit 9d536ea49e
74 changed files with 4525 additions and 2350 deletions

View File

@@ -129,7 +129,7 @@ public class BacktestExecutor
/// <returns>The lightweight backtest result</returns>
public async Task<LightBacktest> ExecuteAsync(
TradingBotConfig config,
HashSet<Candle> candles,
IReadOnlyList<Candle> candles,
User user,
bool save = false,
bool withCandles = false,
@@ -166,7 +166,9 @@ public class BacktestExecutor
// Create a fresh TradingBotBase instance for this backtest
var tradingBot = CreateTradingBotInstance(config);
tradingBot.Account = user.Accounts.First();
var account = user.Accounts.First();
account.User = user; // Ensure Account.User is set for backtest
tradingBot.Account = account;
var totalCandles = candles.Count;
var currentCandle = 0;
@@ -220,8 +222,9 @@ public class BacktestExecutor
// The signal calculation depends on rolling window state and cannot be pre-calculated effectively
// Use optimized rolling window approach - TradingBox.GetSignal only needs last 600 candles
// Use List<Candle> directly to preserve chronological order and enable incremental updates
const int RollingWindowSize = 600; // TradingBox.GetSignal only needs last 600 candles
var rollingWindowCandles = new Queue<Candle>(RollingWindowSize);
var rollingWindowCandles = new List<Candle>(RollingWindowSize); // Pre-allocate capacity for performance
var candlesProcessed = 0;
// Signal caching optimization - reduce signal update frequency for better performance
@@ -253,21 +256,20 @@ public class BacktestExecutor
cancellationToken.ThrowIfCancellationRequested();
// Maintain rolling window of last 600 candles to prevent exponential memory growth
rollingWindowCandles.Enqueue(candle);
if (rollingWindowCandles.Count > RollingWindowSize)
// Incremental updates: remove oldest if at capacity, then add newest
// This preserves chronological order and avoids expensive HashSet recreation
if (rollingWindowCandles.Count >= RollingWindowSize)
{
rollingWindowCandles.Dequeue(); // Remove oldest candle
rollingWindowCandles.RemoveAt(0); // Remove oldest candle (O(n) but only 600 items max)
}
rollingWindowCandles.Add(candle); // Add newest candle (O(1) amortized)
tradingBot.LastCandle = candle;
// Run with optimized backtest path (minimize async calls)
var signalUpdateStart = Stopwatch.GetTimestamp();
// Convert rolling window to HashSet for TradingBot.UpdateSignals compatibility
// NOTE: Recreating HashSet each iteration is necessary to maintain correct enumeration order
// Incremental updates break business logic (changes PnL results)
var fixedCandles = new HashSet<Candle>(rollingWindowCandles);
await tradingBot.UpdateSignals(fixedCandles, preCalculatedIndicatorValues);
// Pass List<Candle> directly - no conversion needed, order is preserved
await tradingBot.UpdateSignals(rollingWindowCandles, preCalculatedIndicatorValues);
signalUpdateTotalTime += Stopwatch.GetElapsedTime(signalUpdateStart);
var backtestStepStart = Stopwatch.GetTimestamp();
@@ -542,7 +544,7 @@ public class BacktestExecutor
throw new InvalidOperationException("Bot configuration is not initialized");
}
if (!config.IsForBacktest)
if (config.TradingType != TradingType.BacktestFutures)
{
throw new InvalidOperationException("BacktestExecutor can only be used for backtesting");
}
@@ -550,7 +552,7 @@ public class BacktestExecutor
// Create the trading bot instance
using var scope = _scopeFactory.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
var tradingBot = new BacktestFuturesBot(logger, _scopeFactory, config);
return tradingBot;
}