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:
@@ -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>();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user