using Managing.Application.Abstractions; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Bots; 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.Extensions.Logging; using static Managing.Common.Enums; namespace Managing.Application.Backtesting { public class Backtester : IBacktester { private readonly IBacktestRepository _backtestRepository; private readonly ILogger _logger; private readonly IExchangeService _exchangeService; private readonly IBotFactory _botFactory; private readonly IScenarioService _scenarioService; private readonly IAccountService _accountService; private readonly IMessengerService _messengerService; private readonly IKaigenService _kaigenService; public Backtester( IExchangeService exchangeService, IBotFactory botFactory, IBacktestRepository backtestRepository, ILogger logger, IScenarioService scenarioService, IAccountService accountService, IMessengerService messengerService, IKaigenService kaigenService) { _exchangeService = exchangeService; _botFactory = botFactory; _backtestRepository = backtestRepository; _logger = logger; _scenarioService = scenarioService; _accountService = accountService; _messengerService = messengerService; _kaigenService = kaigenService; } public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false) { var simplebot = _botFactory.CreateSimpleBot("scenario", workflow); Backtest result = null; if (save && result != null) { // Simple bot backtest not implemented yet, would need user // _backtestRepository.InsertBacktestForUser(null, result); } return result; } /// /// Runs a trading bot backtest with the specified configuration and date range. /// Automatically handles different bot types based on config.BotType. /// /// The trading bot configuration (must include Scenario object or ScenarioName) /// The start date for the backtest /// The end date for the backtest /// The user running the backtest (optional) /// Whether to save the backtest results /// Whether to include candles and indicators values in the response /// The request ID to associate with this backtest (optional) /// Additional metadata to associate with this backtest (optional) /// The backtest results public async Task RunTradingBotBacktest( TradingBotConfig config, DateTime startDate, DateTime endDate, User user = null, bool save = false, bool withCandles = false, string requestId = null, object metadata = null) { string creditRequestId = null; // Debit user credits before starting the backtest if (user != null) { try { creditRequestId = await _kaigenService.DebitUserCreditsAsync(user, 3); _logger.LogInformation("Successfully debited credits for user {UserName} with request ID {RequestId}", user.Name, creditRequestId); } catch (Exception ex) { _logger.LogError(ex, "Failed to debit credits for user {UserName}. Backtest will not proceed.", user.Name); throw new Exception($"Failed to debit credits: {ex.Message}"); } } 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; if (save && user != null) { _backtestRepository.InsertBacktestForUser(user, result); } return result; } catch (Exception ex) { // If backtest fails and we debited credits, attempt to refund if (user != null && !string.IsNullOrEmpty(creditRequestId)) { try { var refundSuccess = await _kaigenService.RefundUserCreditsAsync(creditRequestId, user); if (refundSuccess) { _logger.LogInformation("Successfully refunded credits for user {UserName} after backtest failure", user.Name); } else { _logger.LogError("Failed to refund credits for user {UserName} after backtest failure", user.Name); } } catch (Exception refundEx) { _logger.LogError(refundEx, "Error during refund attempt for user {UserName}", user.Name); } } throw; } } /// /// Runs a trading bot backtest with pre-loaded candles. /// Automatically handles different bot types based on config.BotType. /// /// The trading bot configuration (must include Scenario object or ScenarioName) /// The candles to use for backtesting /// The user running the backtest (optional) /// Whether to include candles and indicators values in the response /// The backtest results public async Task RunTradingBotBacktest( TradingBotConfig config, List candles, User user = null, bool withCandles = false, string requestId = null, object metadata = null) { return await RunBacktestWithCandles(config, candles, user, withCandles, requestId, metadata); } /// /// Core backtesting logic - handles the actual backtest execution with pre-loaded candles /// private async Task RunBacktestWithCandles( TradingBotConfig config, List candles, User user = null, bool withCandles = false, string requestId = null, object metadata = null) { var tradingBot = _botFactory.CreateBacktestTradingBot(config); // 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()) { throw new InvalidOperationException( $"No indicators were loaded for scenario '{config.ScenarioName ?? config.Scenario?.Name}'. " + "This indicates a problem with scenario loading."); } tradingBot.User = user; await tradingBot.LoadAccount(); var result = await GetBacktestingResult(config, tradingBot, candles, user, withCandles, requestId, metadata); if (user != null) { result.User = user; } return result; } private async Task GetAccountFromConfig(TradingBotConfig config) { var account = await _accountService.GetAccount(config.AccountName, false, false); if (account != null) { return account; } return new Account { Name = config.AccountName, Exchange = TradingExchanges.GmxV2 }; } private List GetCandles(Ticker ticker, Timeframe timeframe, DateTime startDate, DateTime endDate) { var candles = _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, startDate, timeframe, endDate).Result; if (candles == null || candles.Count == 0) throw new Exception($"No candles for {ticker} on {timeframe} timeframe"); return candles; } private async Task GetBacktestingResult( TradingBotConfig config, ITradingBot bot, List candles, User user = null, bool withCandles = false, string requestId = null, object metadata = null) { 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); bot.Run(); currentCandle++; // 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(candles); // Only calculate indicators values if withCandles is true Dictionary indicatorsValues = null; if (withCandles) { indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles); } var finalPnl = bot.GetProfitAndLoss(); var winRate = bot.GetWinRate(); var optimizedMoneyManagement = TradingBox.GetBestMoneyManagement(candles, bot.Positions, config.MoneyManagement); var stats = TradingHelpers.GetStatistics(bot.WalletBalances); var growthPercentage = TradingHelpers.GetGrowthFromInitalBalance(config.BotTradingBalance, finalPnl); var hodlPercentage = TradingHelpers.GetHodlPercentage(candles[0], candles.Last()); var fees = bot.GetTotalFees(); var scoringParams = new BacktestScoringParams( sharpeRatio: (double)stats.SharpeRatio, maxDrawdownPc: (double)stats.MaxDrawdownPc, 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: config.BotTradingBalance, startDate: candles[0].Date, endDate: candles.Last().Date, timeframe: config.Timeframe ); var score = BacktestScorer.CalculateTotalScore(scoringParams); // Create backtest result with conditional candles and indicators values var result = new Backtest(config, bot.Positions, bot.Signals.ToList(), withCandles ? candles : new List()) { FinalPnl = finalPnl, WinRate = winRate, GrowthPercentage = growthPercentage, HodlPercentage = hodlPercentage, Fees = fees, WalletBalances = bot.WalletBalances.ToList(), Statistics = stats, OptimizedMoneyManagement = optimizedMoneyManagement, IndicatorsValues = withCandles ? AggregateValues(indicatorsValues, bot.IndicatorsValues) : new Dictionary(), Score = score, Id = Guid.NewGuid().ToString(), RequestId = requestId, Metadata = metadata }; // Send notification if backtest meets criteria await SendBacktestNotificationIfCriteriaMet(result); return result; } private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest) { try { if (backtest.Score > 80) { await _messengerService.SendBacktestNotification(backtest); } } catch (Exception ex) { _logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id); } } private Dictionary AggregateValues( Dictionary indicatorsValues, Dictionary 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(); 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 GetIndicatorsValues(List indicators, List candles) { var indicatorsValues = new Dictionary(); var fixedCandles = new FixedSizeQueue(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 bool DeleteBacktest(string id) { try { _backtestRepository.DeleteBacktestByIdForUser(null, id); return true; } catch (Exception ex) { _logger.LogError(ex.Message); return false; } } public bool DeleteBacktests() { try { _backtestRepository.DeleteAllBacktestsForUser(null); return true; } catch (Exception ex) { _logger.LogError(ex.Message); return false; } } public IEnumerable GetBacktestsByUser(User user) { var backtests = _backtestRepository.GetBacktestsByUser(user).ToList(); return backtests; } public IEnumerable GetBacktestsByRequestId(string requestId) { var backtests = _backtestRepository.GetBacktestsByRequestId(requestId).ToList(); return backtests; } public (IEnumerable Backtests, int TotalCount) GetBacktestsByRequestIdPaginated(string requestId, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var (backtests, totalCount) = _backtestRepository.GetBacktestsByRequestIdPaginated(requestId, page, pageSize, sortBy, sortOrder); return (backtests, totalCount); } public Backtest GetBacktestByIdForUser(User user, string id) { var backtest = _backtestRepository.GetBacktestByIdForUser(user, id); if (backtest == null) return null; if (backtest.Candles == null || backtest.Candles.Count == 0 || backtest.Candles.Count < 10) { try { var account = new Account { Name = backtest.Config.AccountName, Exchange = TradingExchanges.Evm }; var candles = _exchangeService.GetCandlesInflux( account.Exchange, backtest.Config.Ticker, backtest.StartDate, backtest.Config.Timeframe, backtest.EndDate).Result; if (candles != null && candles.Count > 0) { backtest.Candles = candles; } } catch (Exception ex) { _logger.LogError(ex, "Failed to retrieve candles for backtest {Id}", id); } } return backtest; } public bool DeleteBacktestByUser(User user, string id) { try { _backtestRepository.DeleteBacktestByIdForUser(user, id); return true; } catch (Exception ex) { _logger.LogError(ex.Message); return false; } } public bool DeleteBacktestsByIdsForUser(User user, IEnumerable ids) { try { _backtestRepository.DeleteBacktestsByIdsForUser(user, ids); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to delete backtests for user {UserName}", user.Name); return false; } } public bool DeleteBacktestsByUser(User user) { try { _backtestRepository.DeleteAllBacktestsForUser(user); return true; } catch (Exception ex) { _logger.LogError(ex.Message); return false; } } public (IEnumerable Backtests, int TotalCount) GetBacktestsByUserPaginated(User user, int page, int pageSize, string sortBy = "score", string sortOrder = "desc") { var (backtests, totalCount) = _backtestRepository.GetBacktestsByUserPaginated(user, page, pageSize, sortBy, sortOrder); return (backtests, totalCount); } } }