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

@@ -53,7 +53,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// <returns>The complete backtest result</returns>
public async Task<LightBacktest> RunBacktestAsync(
TradingBotConfig config,
HashSet<Candle> candles,
IReadOnlyList<Candle> candles,
User user = null,
bool save = false,
bool withCandles = false,
@@ -67,7 +67,9 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create a fresh TradingBotBase instance for this backtest
var tradingBot = await 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;
@@ -81,7 +83,9 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
var initialBalance = config.BotTradingBalance;
var fixedCandles = new HashSet<Candle>();
const int RollingWindowSize = 600; // TradingBox.GetSignal only needs last 600 candles
// Use List<Candle> directly to preserve chronological order and enable incremental updates
var rollingWindowCandles = new List<Candle>(RollingWindowSize); // Pre-allocate capacity for performance
var lastYieldTime = DateTime.UtcNow;
const int yieldIntervalMs = 5000; // Yield control every 5 seconds to prevent timeout
const int candlesPerBatch = 100; // Process in batches to allow Orleans to check for cancellation
@@ -89,11 +93,19 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Process all candles following the exact pattern from GetBacktestingResult
foreach (var candle in candles)
{
fixedCandles.Add(candle);
// Maintain rolling window: remove oldest if at capacity, then add newest
// This preserves chronological order and avoids expensive HashSet recreation
if (rollingWindowCandles.Count >= RollingWindowSize)
{
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;
// Update signals manually only for backtesting
await tradingBot.UpdateSignals(fixedCandles);
// Update signals manually only for backtesting with rolling window
// Pass List<Candle> directly - no conversion needed, order is preserved
await tradingBot.UpdateSignals(rollingWindowCandles);
await tradingBot.Run();
currentCandle++;
@@ -132,11 +144,12 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
_logger.LogInformation("Backtest processing completed. Calculating final results...");
var finalPnl = TradingBox.GetTotalNetPnL(tradingBot.Positions);
var realizedPnl = TradingBox.GetTotalRealizedPnL(tradingBot.Positions); // PnL before fees
var netPnl = TradingBox.GetTotalNetPnL(tradingBot.Positions); // PnL after fees
var winRate = TradingBox.GetWinRate(tradingBot.Positions);
var stats = TradingBox.GetStatistics(tradingBot.WalletBalances);
var growthPercentage =
TradingBox.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, finalPnl);
TradingBox.GetGrowthFromInitalBalance(tradingBot.WalletBalances.FirstOrDefault().Value, netPnl);
var hodlPercentage = TradingBox.GetHodlPercentage(candles.First(), candles.Last());
var fees = TradingBox.GetTotalFees(tradingBot.Positions);
@@ -145,7 +158,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
growthPercentage: (double)growthPercentage,
hodlPercentage: (double)hodlPercentage,
winRate: winRate,
totalPnL: (double)finalPnl,
totalPnL: (double)realizedPnl,
fees: (double)fees,
tradeCount: tradingBot.Positions.Count,
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
@@ -166,7 +179,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
// Create backtest result with conditional candles and indicators values
var result = new Backtest(config, tradingBot.Positions, tradingBot.Signals)
{
FinalPnl = finalPnl,
FinalPnl = realizedPnl, // Realized PnL before fees
WinRate = winRate,
GrowthPercentage = growthPercentage,
HodlPercentage = hodlPercentage,
@@ -180,7 +193,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
StartDate = candles.FirstOrDefault()!.OpenTime,
EndDate = candles.LastOrDefault()!.OpenTime,
InitialBalance = initialBalance,
NetPnl = finalPnl - fees,
NetPnl = netPnl, // Net PnL after fees
};
if (save && user != null)
@@ -233,14 +246,14 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
throw new InvalidOperationException("Bot configuration is not initialized");
}
if (!config.IsForBacktest)
if (config.TradingType != TradingType.BacktestFutures)
{
throw new InvalidOperationException("BacktestTradingBotGrain can only be used for backtesting");
}
// Create the trading bot instance
var logger = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var tradingBot = new TradingBotBase(logger, _scopeFactory, config);
var tradingBot = new BacktestFuturesBot(logger, _scopeFactory, config);
return tradingBot;
}
@@ -284,7 +297,7 @@ public class BacktestTradingBotGrain : Grain, IBacktestTradingBotGrain
/// Gets indicators values (following Backtester.cs pattern)
/// </summary>
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<LightIndicator> indicators,
HashSet<Candle> candles)
IReadOnlyList<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();

View File

@@ -72,7 +72,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
throw new InvalidOperationException("Bot configuration is not properly initialized");
}
if (config.IsForBacktest)
if (config.TradingType == TradingType.BacktestFutures)
{
throw new InvalidOperationException("LiveTradingBotGrain cannot be used for backtesting");
}
@@ -531,7 +531,7 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
using var scope = _scopeFactory.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<TradingBotBase>>();
var streamProvider = this.GetStreamProvider("ManagingStreamProvider");
var tradingBot = new TradingBotBase(logger, _scopeFactory, config, streamProvider);
var tradingBot = new FuturesBot(logger, _scopeFactory, config, streamProvider);
// Load state into the trading bot instance
LoadStateIntoTradingBot(tradingBot);