Orlean (#32)
* Start building with orlean * Add missing file * Serialize grain state * Remove grain and proxies * update and add plan * Update a bit * Fix backtest grain * Fix backtest grain * Clean a bit
This commit is contained in:
@@ -1,17 +1,13 @@
|
||||
using Managing.Application.Abstractions;
|
||||
using Managing.Application.Abstractions.Grains;
|
||||
using Managing.Application.Abstractions.Repositories;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Bots;
|
||||
using Managing.Application.Hubs;
|
||||
using Managing.Core.FixedSizedQueue;
|
||||
using Managing.Domain.Accounts;
|
||||
using Managing.Domain.Backtests;
|
||||
using Managing.Domain.Bots;
|
||||
using Managing.Domain.Candles;
|
||||
using Managing.Domain.Scenarios;
|
||||
using Managing.Domain.Shared.Helpers;
|
||||
using Managing.Domain.Strategies;
|
||||
using Managing.Domain.Strategies.Base;
|
||||
using Managing.Domain.Users;
|
||||
using Managing.Domain.Workflows;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
@@ -32,6 +28,7 @@ namespace Managing.Application.Backtesting
|
||||
private readonly IMessengerService _messengerService;
|
||||
private readonly IKaigenService _kaigenService;
|
||||
private readonly IHubContext<BacktestHub> _hubContext;
|
||||
private readonly IGrainFactory _grainFactory;
|
||||
|
||||
public Backtester(
|
||||
IExchangeService exchangeService,
|
||||
@@ -42,7 +39,8 @@ namespace Managing.Application.Backtesting
|
||||
IAccountService accountService,
|
||||
IMessengerService messengerService,
|
||||
IKaigenService kaigenService,
|
||||
IHubContext<BacktestHub> hubContext)
|
||||
IHubContext<BacktestHub> hubContext,
|
||||
IGrainFactory grainFactory)
|
||||
{
|
||||
_exchangeService = exchangeService;
|
||||
_botFactory = botFactory;
|
||||
@@ -53,6 +51,7 @@ namespace Managing.Application.Backtesting
|
||||
_messengerService = messengerService;
|
||||
_kaigenService = kaigenService;
|
||||
_hubContext = hubContext;
|
||||
_grainFactory = grainFactory;
|
||||
}
|
||||
|
||||
public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false)
|
||||
@@ -80,8 +79,8 @@ namespace Managing.Application.Backtesting
|
||||
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
|
||||
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
|
||||
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
|
||||
/// <returns>The backtest results</returns>
|
||||
public async Task<Backtest> RunTradingBotBacktest(
|
||||
/// <returns>The lightweight backtest results</returns>
|
||||
public async Task<LightBacktest> RunTradingBotBacktest(
|
||||
TradingBotConfig config,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
@@ -114,25 +113,7 @@ namespace Managing.Application.Backtesting
|
||||
try
|
||||
{
|
||||
var candles = GetCandles(config.Ticker, config.Timeframe, startDate, endDate);
|
||||
|
||||
var result = await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata);
|
||||
|
||||
// Set start and end dates
|
||||
result.StartDate = startDate;
|
||||
result.EndDate = endDate;
|
||||
|
||||
// Ensure RequestId is set - required for PostgreSQL NOT NULL constraint
|
||||
if (string.IsNullOrEmpty(result.RequestId))
|
||||
{
|
||||
result.RequestId = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
if (save && user != null)
|
||||
{
|
||||
_backtestRepository.InsertBacktestForUser(user, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
return await RunBacktestWithCandles(config, candles, user, save, withCandles, requestId, metadata);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -172,8 +153,10 @@ namespace Managing.Application.Backtesting
|
||||
/// <param name="candles">The candles to use for backtesting</param>
|
||||
/// <param name="user">The user running the backtest (optional)</param>
|
||||
/// <param name="withCandles">Whether to include candles and indicators values in the response</param>
|
||||
/// <returns>The backtest results</returns>
|
||||
public async Task<Backtest> RunTradingBotBacktest(
|
||||
/// <param name="requestId">The request ID to associate with this backtest (optional)</param>
|
||||
/// <param name="metadata">Additional metadata to associate with this backtest (optional)</param>
|
||||
/// <returns>The lightweight backtest results</returns>
|
||||
public async Task<LightBacktest> RunTradingBotBacktest(
|
||||
TradingBotConfig config,
|
||||
List<Candle> candles,
|
||||
User user = null,
|
||||
@@ -181,43 +164,49 @@ namespace Managing.Application.Backtesting
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
return await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata);
|
||||
return await RunBacktestWithCandles(config, candles, user, false, withCandles, requestId, metadata);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core backtesting logic - handles the actual backtest execution with pre-loaded candles
|
||||
/// </summary>
|
||||
private async Task<Backtest> RunBacktestWithCandles(
|
||||
private async Task<LightBacktest> RunBacktestWithCandles(
|
||||
TradingBotConfig config,
|
||||
List<Candle> candles,
|
||||
User user = null,
|
||||
bool save = false,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
{
|
||||
var tradingBot = await _botFactory.CreateBacktestTradingBot(config);
|
||||
// Ensure this is a backtest configuration
|
||||
if (!config.IsForBacktest)
|
||||
{
|
||||
throw new InvalidOperationException("Backtest configuration must have IsForBacktest set to true");
|
||||
}
|
||||
|
||||
// Scenario and indicators should already be loaded in constructor by BotService
|
||||
// This is just a validation check to ensure everything loaded properly
|
||||
if (tradingBot is TradingBot bot && !bot.Indicators.Any())
|
||||
// Validate that scenario and indicators are properly loaded
|
||||
if (config.Scenario == null && string.IsNullOrEmpty(config.ScenarioName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"No indicators were loaded for scenario '{config.ScenarioName ?? config.Scenario?.Name}'. " +
|
||||
"This indicates a problem with scenario loading.");
|
||||
"Backtest configuration must include either Scenario object or ScenarioName");
|
||||
}
|
||||
|
||||
tradingBot.User = user;
|
||||
tradingBot.Account = await GetAccountFromConfig(config);
|
||||
|
||||
var result =
|
||||
await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata);
|
||||
|
||||
if (user != null)
|
||||
if (config.Scenario == null && !string.IsNullOrEmpty(config.ScenarioName))
|
||||
{
|
||||
result.User = user;
|
||||
var fullScenario = await _scenarioService.GetScenarioByNameAndUserAsync(config.ScenarioName, user);
|
||||
config.Scenario = LightScenario.FromScenario(fullScenario);
|
||||
}
|
||||
|
||||
return result;
|
||||
// Create a clean copy of the config to avoid Orleans serialization issues
|
||||
var cleanConfig = CreateCleanConfigForOrleans(config);
|
||||
|
||||
// Create Orleans grain for backtesting
|
||||
var backtestGrain = _grainFactory.GetGrain<IBacktestTradingBotGrain>(Guid.NewGuid());
|
||||
|
||||
// Run the backtest using the Orleans grain and return LightBacktest directly
|
||||
return await backtestGrain.RunBacktestAsync(cleanConfig, candles, user, save, withCandles, requestId,
|
||||
metadata);
|
||||
}
|
||||
|
||||
private async Task<Account> GetAccountFromConfig(TradingBotConfig config)
|
||||
@@ -237,128 +226,16 @@ namespace Managing.Application.Backtesting
|
||||
return candles;
|
||||
}
|
||||
|
||||
private async Task<Backtest> GetBacktestingResult(
|
||||
TradingBotConfig config,
|
||||
ITradingBot bot,
|
||||
List<Candle> candles,
|
||||
User user = null,
|
||||
bool withCandles = false,
|
||||
string requestId = null,
|
||||
object metadata = null)
|
||||
|
||||
/// <summary>
|
||||
/// Creates a clean copy of the trading bot config for Orleans serialization
|
||||
/// Uses LightScenario and LightIndicator to avoid FixedSizeQueue serialization issues
|
||||
/// </summary>
|
||||
private TradingBotConfig CreateCleanConfigForOrleans(TradingBotConfig originalConfig)
|
||||
{
|
||||
if (candles == null || candles.Count == 0)
|
||||
{
|
||||
throw new Exception("No candle to backtest");
|
||||
}
|
||||
|
||||
var totalCandles = candles.Count;
|
||||
var currentCandle = 0;
|
||||
var lastLoggedPercentage = 0;
|
||||
|
||||
_logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}",
|
||||
totalCandles, config.Ticker, config.Timeframe);
|
||||
|
||||
bot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance);
|
||||
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
bot.OptimizedCandles.Enqueue(candle);
|
||||
bot.Candles.Add(candle);
|
||||
await bot.Run();
|
||||
|
||||
currentCandle++;
|
||||
|
||||
// Check if wallet balance fell below 10 USDC and break if so
|
||||
var currentWalletBalance = bot.WalletBalances.Values.LastOrDefault();
|
||||
if (currentWalletBalance < 10m)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Backtest stopped early: Wallet balance fell below 10 USDC (Current: {CurrentBalance:F2} USDC) at candle {CurrentCandle}/{TotalCandles} from {CandleDate}",
|
||||
currentWalletBalance, currentCandle, totalCandles, candle.Date.ToString("yyyy-MM-dd HH:mm"));
|
||||
break;
|
||||
}
|
||||
|
||||
// Log progress every 10% or every 1000 candles, whichever comes first
|
||||
var currentPercentage = (int)((double)currentCandle / totalCandles * 100);
|
||||
var shouldLog = currentPercentage >= lastLoggedPercentage + 10 ||
|
||||
currentCandle % 1000 == 0 ||
|
||||
currentCandle == totalCandles;
|
||||
|
||||
if (shouldLog && currentPercentage > lastLoggedPercentage)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Backtest progress: {CurrentCandle}/{TotalCandles} ({Percentage}%) - Processing candle from {CandleDate}",
|
||||
currentCandle, totalCandles, currentPercentage, candle.Date.ToString("yyyy-MM-dd HH:mm"));
|
||||
lastLoggedPercentage = currentPercentage;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Backtest processing completed. Calculating final results...");
|
||||
|
||||
bot.Candles = new HashSet<Candle>(candles);
|
||||
|
||||
// Only calculate indicators values if withCandles is true
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues = null;
|
||||
if (withCandles)
|
||||
{
|
||||
indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles);
|
||||
}
|
||||
|
||||
var finalPnl = bot.GetProfitAndLoss();
|
||||
var winRate = bot.GetWinRate();
|
||||
var stats = TradingHelpers.GetStatistics(bot.WalletBalances);
|
||||
var growthPercentage =
|
||||
TradingHelpers.GetGrowthFromInitalBalance(bot.WalletBalances.FirstOrDefault().Value, finalPnl);
|
||||
var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last());
|
||||
|
||||
var fees = bot.GetTotalFees();
|
||||
var scoringParams = new BacktestScoringParams(
|
||||
sharpeRatio: (double)stats.SharpeRatio,
|
||||
growthPercentage: (double)growthPercentage,
|
||||
hodlPercentage: (double)hodlPercentage,
|
||||
winRate: winRate,
|
||||
totalPnL: (double)finalPnl,
|
||||
fees: (double)fees,
|
||||
tradeCount: bot.Positions.Count,
|
||||
maxDrawdownRecoveryTime: stats.MaxDrawdownRecoveryTime,
|
||||
maxDrawdown: stats.MaxDrawdown,
|
||||
initialBalance: bot.WalletBalances.FirstOrDefault().Value,
|
||||
tradingBalance: config.BotTradingBalance,
|
||||
startDate: candles[0].Date,
|
||||
endDate: candles.Last().Date,
|
||||
timeframe: config.Timeframe,
|
||||
moneyManagement: config.MoneyManagement
|
||||
);
|
||||
|
||||
var scoringResult = BacktestScorer.CalculateDetailedScore(scoringParams);
|
||||
|
||||
// Create backtest result with conditional candles and indicators values
|
||||
var result = new Backtest(config, bot.Positions, bot.Signals.ToList(),
|
||||
withCandles ? candles : new List<Candle>())
|
||||
{
|
||||
FinalPnl = finalPnl,
|
||||
WinRate = winRate,
|
||||
GrowthPercentage = growthPercentage,
|
||||
HodlPercentage = hodlPercentage,
|
||||
Fees = fees,
|
||||
WalletBalances = bot.WalletBalances.ToList(),
|
||||
Statistics = stats,
|
||||
IndicatorsValues = withCandles
|
||||
? AggregateValues(indicatorsValues, bot.IndicatorsValues)
|
||||
: new Dictionary<IndicatorType, IndicatorsResultBase>(),
|
||||
Score = scoringResult.Score,
|
||||
ScoreMessage = scoringResult.GenerateSummaryMessage(),
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
RequestId = requestId,
|
||||
Metadata = metadata,
|
||||
StartDate = candles.FirstOrDefault()!.OpenTime,
|
||||
EndDate = candles.LastOrDefault()!.OpenTime,
|
||||
};
|
||||
|
||||
// Send notification if backtest meets criteria
|
||||
await SendBacktestNotificationIfCriteriaMet(result);
|
||||
|
||||
return result;
|
||||
// Since we're now using LightScenario in TradingBotConfig, we can just return the original config
|
||||
// The conversion to LightScenario is already done when loading the scenario
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest)
|
||||
@@ -376,56 +253,6 @@ namespace Managing.Application.Backtesting
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> AggregateValues(
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
|
||||
Dictionary<IndicatorType, IndicatorsResultBase> botStrategiesValues)
|
||||
{
|
||||
// Foreach strategy type, only retrieve the values where the strategy is not present already in the bot
|
||||
// Then, add the values to the bot values
|
||||
|
||||
var result = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
foreach (var indicator in indicatorsValues)
|
||||
{
|
||||
// if (!botStrategiesValues.ContainsKey(strategy.Key))
|
||||
// {
|
||||
// result[strategy.Key] = strategy.Value;
|
||||
// }else
|
||||
// {
|
||||
// result[strategy.Key] = botStrategiesValues[strategy.Key];
|
||||
// }
|
||||
|
||||
result[indicator.Key] = indicator.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
|
||||
List<Candle> candles)
|
||||
{
|
||||
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
|
||||
var fixedCandles = new FixedSizeQueue<Candle>(10000);
|
||||
foreach (var candle in candles)
|
||||
{
|
||||
fixedCandles.Enqueue(candle);
|
||||
}
|
||||
|
||||
foreach (var indicator in indicators)
|
||||
{
|
||||
try
|
||||
{
|
||||
var s = ScenarioHelpers.BuildIndicator(indicator, 10000);
|
||||
s.Candles = fixedCandles;
|
||||
indicatorsValues[indicator.Type] = s.GetIndicatorValues();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
return indicatorsValues;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteBacktestAsync(string id)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user