From e4f4d078b201d66fd7b86d789adbf7b3d56e64de Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sun, 15 Jun 2025 18:46:41 +0700 Subject: [PATCH] Test strategy combo --- .../Controllers/BacktestController.cs | 6 +- .../Services/IBacktester.cs | 50 +- src/Managing.Application.Tests/BotsTests.cs | 959 +++++++++++++++++- .../StatisticService.cs | 5 +- .../Abstractions/ITradingBot.cs | 1 + .../Backtesting/Backtester.cs | 133 +-- src/Managing.Application/Bots/TradingBot.cs | 14 + src/Managing.Domain/Bots/TradingBotConfig.cs | 13 +- 8 files changed, 1024 insertions(+), 157 deletions(-) diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 28161d0..027c4f9 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -169,7 +169,8 @@ public class BacktestController : BaseController MaxPositionTimeHours = request.Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, FlipPosition = request.Config.FlipPosition, - Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}", + Name = request.Config.Name ?? + $"Backtest-{request.Config.ScenarioName}-{DateTime.UtcNow:yyyyMMdd-HHmmss}", CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable }; @@ -185,8 +186,7 @@ public class BacktestController : BaseController request.StartDate, request.EndDate, user, - request.Save, - null); + request.Save); break; } diff --git a/src/Managing.Application.Abstractions/Services/IBacktester.cs b/src/Managing.Application.Abstractions/Services/IBacktester.cs index 2ef352d..26c3333 100644 --- a/src/Managing.Application.Abstractions/Services/IBacktester.cs +++ b/src/Managing.Application.Abstractions/Services/IBacktester.cs @@ -8,68 +8,38 @@ namespace Managing.Application.Abstractions.Services public interface IBacktester { /// - /// Runs a unified trading bot backtest with the specified configuration and date range. - /// Automatically handles ScalpingBot and FlippingBot behavior based on config.BotType. + /// 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 + /// 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 + /// The user running the backtest (optional) /// Whether to save the backtest results - /// Optional pre-loaded candles /// The backtest results Task RunTradingBotBacktest( TradingBotConfig config, DateTime startDate, DateTime endDate, User user = null, - bool save = false, - List? initialCandles = null); + bool save = false); /// - /// Runs a unified trading bot backtest with pre-loaded candles. - /// Automatically handles ScalpingBot and FlippingBot behavior based on config.BotType. + /// Runs a trading bot backtest with pre-loaded candles. + /// Automatically handles different bot types based on config.BotType. /// - /// The trading bot configuration + /// The trading bot configuration (must include Scenario object or ScenarioName) /// The candles to use for backtesting - /// The user running the backtest + /// The user running the backtest (optional) /// The backtest results Task RunTradingBotBacktest( TradingBotConfig config, List candles, User user = null); - // Legacy methods - maintained for backward compatibility - Task RunScalpingBotBacktest( - TradingBotConfig config, - DateTime startDate, - DateTime endDate, - User user = null, - bool save = false, - List? initialCandles = null); - - Task RunFlippingBotBacktest( - TradingBotConfig config, - DateTime startDate, - DateTime endDate, - User user = null, - bool save = false, - List? initialCandles = null); - + // Additional methods for backtest management bool DeleteBacktest(string id); bool DeleteBacktests(); - - Task RunScalpingBotBacktest( - TradingBotConfig config, - List candles, - User user = null); - - Task RunFlippingBotBacktest( - TradingBotConfig config, - List candles, - User user = null); - - // User-specific operations Task> GetBacktestsByUser(User user); Backtest GetBacktestByIdForUser(User user, string id); bool DeleteBacktestByUser(User user, string id); diff --git a/src/Managing.Application.Tests/BotsTests.cs b/src/Managing.Application.Tests/BotsTests.cs index 0ed957b..438b861 100644 --- a/src/Managing.Application.Tests/BotsTests.cs +++ b/src/Managing.Application.Tests/BotsTests.cs @@ -6,6 +6,7 @@ using Managing.Application.Abstractions.Services; using Managing.Application.Backtesting; using Managing.Application.Bots.Base; using Managing.Core; +using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.MoneyManagements; @@ -21,9 +22,9 @@ namespace Managing.Application.Tests { private readonly IBotFactory _botFactory; private readonly IBacktester _backtester; - private readonly string _reportPath = "D:\\BacktestingReports\\backtesting.csv"; - private string _analysePath = "D:\\BacktestingReports\\analyse"; - private readonly string _errorsPath = "D:\\BacktestingReports\\errorsAnalyse.csv"; + private readonly string _reportPath; + private string _analysePath; + private readonly string _errorsPath; private readonly string _s = "|"; private List _elapsedTimes { get; set; } @@ -45,6 +46,12 @@ namespace Managing.Application.Tests _backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger, scenarioService, _accountService.Object); _elapsedTimes = new List(); + + // Initialize cross-platform file paths + var reportsDirectory = GetBacktestReportsDirectory(); + _reportPath = Path.Combine(reportsDirectory, "backtesting.csv"); + _analysePath = Path.Combine(reportsDirectory, "analyse"); + _errorsPath = Path.Combine(reportsDirectory, "errorsAnalyse.csv"); } [Theory] @@ -64,7 +71,7 @@ namespace Managing.Application.Tests AccountName = _account.Name, MoneyManagement = MoneyManagement, Ticker = ticker, - ScenarioName = scenario.Name, + Scenario = scenario, Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, @@ -80,7 +87,7 @@ namespace Managing.Application.Tests }; // Act - var backtestResult = await _backtester.RunFlippingBotBacktest(config, localCandles.TakeLast(500).ToList()); + var backtestResult = await _backtester.RunTradingBotBacktest(config, localCandles.TakeLast(500).ToList()); var json = JsonConvert.SerializeObject(backtestResult, Formatting.None); File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json); @@ -114,7 +121,7 @@ namespace Managing.Application.Tests AccountName = _account.Name, MoneyManagement = MoneyManagement, Ticker = ticker, - ScenarioName = scenario.Name, + Scenario = scenario, Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, @@ -130,7 +137,8 @@ namespace Managing.Application.Tests }; // Act - var backtestResult = await _backtester.RunScalpingBotBacktest(config, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null, false, null); + var backtestResult = await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6), + DateTime.UtcNow, null, false); //WriteCsvReport(backtestResult.GetStringReport()); // Assert @@ -163,7 +171,7 @@ namespace Managing.Application.Tests AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, - ScenarioName = scenario.Name, + Scenario = scenario, Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, @@ -179,7 +187,8 @@ namespace Managing.Application.Tests }; // Act - var backtestResult = await _backtester.RunScalpingBotBacktest(config, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null, false, null); + var backtestResult = await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6), + DateTime.UtcNow, null, false); WriteCsvReport(backtestResult.GetStringReport()); // Assert @@ -248,12 +257,12 @@ namespace Managing.Application.Tests var backtestResult = botType switch { BotType.SimpleBot => throw new NotImplementedException(), - BotType.ScalpingBot => _backtester.RunScalpingBotBacktest(new TradingBotConfig + BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, - ScenarioName = scenario.Name, + Scenario = scenario, Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, @@ -267,12 +276,12 @@ namespace Managing.Application.Tests MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }, candles, null).Result, - BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig + BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, - ScenarioName = scenario.Name, + Scenario = scenario, Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, @@ -390,12 +399,12 @@ namespace Managing.Application.Tests var backtestResult = botType switch { BotType.SimpleBot => throw new NotImplementedException(), - BotType.ScalpingBot => _backtester.RunScalpingBotBacktest(new TradingBotConfig + BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, - ScenarioName = scenario.Name, + Scenario = scenario, Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, @@ -409,12 +418,12 @@ namespace Managing.Application.Tests MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }, candles, null).Result, - BotType.FlippingBot => _backtester.RunFlippingBotBacktest(new TradingBotConfig + BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, - ScenarioName = scenario.Name, + Scenario = scenario, Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, @@ -570,5 +579,921 @@ namespace Managing.Application.Tests } } } + + [Theory] + [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -10)] + public async Task ListScenarios(Ticker ticker, Timeframe timeframe, int days) + { + // Arrange + var scenarios = GenerateScenarioCombinations(); + Console.WriteLine($"Total scenarios generated: {scenarios.Count}"); + + // Display breakdown by strategy count + var breakdown = CalculateScenarioBreakdown(); + Console.WriteLine("\n=== SCENARIO BREAKDOWN ==="); + Console.WriteLine($"Single strategy scenarios: {breakdown.SingleStrategy}"); + Console.WriteLine($"Two strategy scenarios: {breakdown.TwoStrategy}"); + Console.WriteLine($"Three strategy scenarios: {breakdown.ThreeStrategy}"); + Console.WriteLine($"Four strategy scenarios: {breakdown.FourStrategy}"); + Console.WriteLine($"Total estimated scenarios: {breakdown.Total}"); + + // Display some example scenarios + Console.WriteLine("\n=== EXAMPLE SCENARIOS ==="); + foreach (var scenario in scenarios.Take(3)) + { + Console.WriteLine($"Scenario: {scenario.Name} ({scenario.Strategies.Count} strategies)"); + foreach (var strategy in scenario.Strategies) + { + Console.WriteLine($" - {strategy.Name} (Type: {strategy.Type})"); + } + + Console.WriteLine(); + } + } + + [Theory] + [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -10, BotType.ScalpingBot)] + // [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -10, BotType.FlippingBot)] + public async Task ComprehensiveScenarioBacktest(Ticker ticker, Timeframe timeframe, int days, BotType botType) + { + // Arrange + var scenarios = GenerateScenarioCombinations(); + var results = new List(); + var errors = new List(); + var options = new ParallelOptions { MaxDegreeOfParallelism = 4 }; + + // Standard money management for filtering best strategies + var standardMoneyManagement = CreateStandardMoneyManagement(timeframe); + + var fileIdentifier = $"ScenarioBacktest_{ticker}_{timeframe}_{botType}_{DateTime.Now:yyyyMMdd_HHmmss}"; + var completedTests = 0; + var totalTests = scenarios.Count; + + // Initialize CSV with headers + InitializeScenarioBacktestCsv(fileIdentifier); + UpdateScenarioProgression(totalTests, completedTests, fileIdentifier); + + Console.WriteLine($"Starting comprehensive backtest of {totalTests} scenarios..."); + Console.WriteLine($"Ticker: {ticker} | Timeframe: {timeframe} | Days: {days} | BotType: {botType}"); + + // Get candles for backtesting + var candles = + await _exchangeService.GetCandles(_account, ticker, DateTime.Now.AddDays(days), timeframe, true); + if (candles == null || candles.Count == 0) + { + Console.WriteLine("No candles available for backtesting"); + return; + } + + Console.WriteLine($"Loaded {candles.Count} candles for backtesting"); + + // Process scenarios in parallel + Parallel.ForEach(scenarios, options, scenario => + { + try + { + var timer = new Stopwatch(); + timer.Start(); + + var config = new TradingBotConfig + { + AccountName = _account.Name, + MoneyManagement = standardMoneyManagement, + Ticker = ticker, + Scenario = scenario, + Timeframe = timeframe, + IsForWatchingOnly = false, + BotTradingBalance = 1000, + BotType = botType, + IsForBacktest = true, + CooldownPeriod = 1, + MaxLossStreak = 0, + FlipPosition = botType == BotType.FlippingBot, + Name = $"Test_{scenario.Name}", + FlipOnlyWhenInProfit = true, + MaxPositionTimeHours = null, + CloseEarlyWhenProfitable = false + }; + + var backtestResult = _backtester.RunTradingBotBacktest(config, candles, null).Result; + + timer.Stop(); + + var scoringParams = new BacktestScoringParams( + sharpeRatio: (double)(backtestResult.Statistics?.SharpeRatio ?? 0), + maxDrawdownPc: (double)(backtestResult.Statistics?.MaxDrawdownPc ?? 0), + growthPercentage: (double)backtestResult.GrowthPercentage, + hodlPercentage: (double)backtestResult.HodlPercentage, + winRate: backtestResult.WinRate / 100.0, // Convert percentage to decimal + totalPnL: (double)backtestResult.FinalPnl, + fees: (double)backtestResult.Fees, + tradeCount: backtestResult.Positions?.Count ?? 0, + maxDrawdownRecoveryTime: backtestResult.Statistics?.MaxDrawdownRecoveryTime ?? TimeSpan.Zero + ); + + var scenarioResult = new ScenarioBacktestResult + { + ScenarioName = scenario.Name, + StrategyCount = scenario.Strategies.Count, + Ticker = ticker.ToString(), + BotType = botType.ToString(), + FinalPnl = backtestResult.FinalPnl, + WinRate = backtestResult.WinRate, + GrowthPercentage = backtestResult.GrowthPercentage, + HodlPercentage = backtestResult.HodlPercentage, + OutperformanceVsHodl = backtestResult.GrowthPercentage - backtestResult.HodlPercentage, + MaxDrawdown = (double)(backtestResult.Statistics?.MaxDrawdown ?? 0), + TotalTrades = backtestResult.Positions?.Count ?? 0, + SharpeRatio = (double)(backtestResult.Statistics?.SharpeRatio ?? 0), + ExecutionTime = timer.Elapsed.TotalSeconds, + StopLoss = standardMoneyManagement.StopLoss, + TakeProfit = standardMoneyManagement.TakeProfit, + Leverage = standardMoneyManagement.Leverage, + Score = BacktestScorer.CalculateTotalScore(scoringParams) + }; + + results.Add(scenarioResult); + WriteScenarioBacktestResult(scenarioResult, fileIdentifier); + + Interlocked.Increment(ref completedTests); + UpdateScenarioProgression(totalTests, completedTests, fileIdentifier, timer.Elapsed.TotalSeconds); + } + catch (Exception ex) + { + Interlocked.Increment(ref completedTests); + var error = $"{scenario.Name}{_s}{ticker}{_s}{ex.Message}"; + errors.Add(error); + WriteScenarioError(error, fileIdentifier); + UpdateScenarioProgression(totalTests, completedTests, fileIdentifier); + } + }); + + // Write summary and analysis + WriteSummaryAnalysis(results, fileIdentifier); + WriteTopPerformingScenarios(results, fileIdentifier); + + Console.WriteLine($"\nBacktest completed! Results written to: {fileIdentifier}"); + Console.WriteLine($"Total scenarios tested: {results.Count}"); + Console.WriteLine($"Errors encountered: {errors.Count}"); + + // Display top 5 performing scenarios + var topScenarios = results + .Where(r => r.FinalPnl > 0 && r.OutperformanceVsHodl > 10) + .OrderByDescending(r => r.Score) + .Take(5); + + Console.WriteLine("\n=== TOP 5 PERFORMING SCENARIOS ==="); + foreach (var scenario in topScenarios) + { + Console.WriteLine($"Scenario: {scenario.ScenarioName}"); + Console.WriteLine( + $" Score: {scenario.Score:F2} | PnL: ${scenario.FinalPnl:F2} | WinRate: {scenario.WinRate}% | Outperformance: {scenario.OutperformanceVsHodl:F2}%"); + Console.WriteLine($" Strategies: {scenario.StrategyCount}"); + Console.WriteLine(); + } + + Assert.True(results.Any(r => r.FinalPnl > 0), "At least one scenario should be profitable"); + } + + private MoneyManagement CreateStandardMoneyManagement(Timeframe timeframe) + { + // Optimized money management for strategy filtering + // These values are designed to be balanced - not too aggressive, not too conservative + return new MoneyManagement + { + Name = "StandardForFiltering", + Timeframe = timeframe, + Leverage = 2m, // Moderate leverage for meaningful results + StopLoss = 0.012m, // 1.2% stop loss - tight enough to limit losses, loose enough to avoid noise + TakeProfit = 0.024m, // 2.4% take profit - 2:1 reward/risk ratio + }; + } + + private void InitializeScenarioBacktestCsv(string fileIdentifier) + { + var csvPath = GetScenarioBacktestCsvPath(fileIdentifier); + var headers = + "ScenarioName|StrategyCount|Ticker|BotType|FinalPnl|WinRate|GrowthPercentage|HodlPercentage|OutperformanceVsHodl|MaxDrawdown|TotalTrades|SharpeRatio|StopLoss|TakeProfit|Leverage|Score"; + File.WriteAllText(csvPath, headers + Environment.NewLine); + } + + private void WriteScenarioBacktestResult(ScenarioBacktestResult result, string fileIdentifier) + { + var csvPath = GetScenarioBacktestCsvPath(fileIdentifier); + + // Write data directly with pipe separator - ensure exact field order matching headers + var line = + $"{result.ScenarioName}|{result.StrategyCount}|{result.Ticker}|{result.BotType}|{result.FinalPnl:F2}|{result.WinRate}|{result.GrowthPercentage:F2}|{result.HodlPercentage:F2}|{result.OutperformanceVsHodl:F2}|{result.MaxDrawdown:F2}|{result.TotalTrades}|{result.SharpeRatio:F3}|{result.StopLoss:F3}|{result.TakeProfit:F3}|{result.Leverage:F1}|{result.Score:F2}"; + + File.AppendAllText(csvPath, line + Environment.NewLine); + } + + private void WriteScenarioError(string error, string fileIdentifier) + { + var errorPath = GetScenarioErrorCsvPath(fileIdentifier); + // Add CSV header if file doesn't exist + if (!File.Exists(errorPath)) + { + File.WriteAllText(errorPath, "ScenarioName|Ticker|ErrorMessage" + Environment.NewLine); + } + + File.AppendAllText(errorPath, error + Environment.NewLine); + } + + private void UpdateScenarioProgression(int totalTests, int completedTests, string fileIdentifier, + double? elapsed = null) + { + var timeRemaining = ""; + if (elapsed.HasValue && completedTests > 0) + { + _elapsedTimes.Add(elapsed.Value); + var avgTime = _elapsedTimes.Average(); + var estimatedRemaining = avgTime * (totalTests - completedTests); + timeRemaining = + $" | Remaining: {estimatedRemaining:F0}s | ETA: {DateTime.Now.AddSeconds(estimatedRemaining):HH:mm:ss}"; + } + + var progressText = + $"{completedTests}/{totalTests} ({(completedTests * 100.0 / totalTests):F1}%){timeRemaining}"; + + // Output progress to console only + Console.WriteLine($"Progress: {progressText}"); + } + + private void WriteSummaryAnalysis(List results, string fileIdentifier) + { + var summaryPath = GetScenarioSummaryCsvPath(fileIdentifier); + var summary = new List + { + "=== COMPREHENSIVE SCENARIO BACKTEST SUMMARY ===", + $"Total Scenarios Tested: {results.Count}", + $"Profitable Scenarios: {results.Count(r => r.FinalPnl > 0)} ({results.Count(r => r.FinalPnl > 0) * 100.0 / results.Count:F1}%)", + $"Scenarios Outperforming HODL: {results.Count(r => r.OutperformanceVsHodl > 0)} ({results.Count(r => r.OutperformanceVsHodl > 0) * 100.0 / results.Count:F1}%)", + "", + "=== PERFORMANCE METRICS ===", + $"Average PnL: ${results.Average(r => r.FinalPnl):F2}", + $"Best PnL: ${results.Max(r => r.FinalPnl):F2}", + $"Worst PnL: ${results.Min(r => r.FinalPnl):F2}", + $"Average Win Rate: {results.Average(r => r.WinRate):F1}%", + $"Average Outperformance vs HODL: {results.Average(r => r.OutperformanceVsHodl):F2}%", + $"Average Max Drawdown: {results.Average(r => r.MaxDrawdown):F2}%", + "", + "=== STRATEGY ANALYSIS ===", + $"Single Strategy Scenarios: {results.Count(r => r.StrategyCount == 1)}", + $"Two Strategy Scenarios: {results.Count(r => r.StrategyCount == 2)}", + $"Three Strategy Scenarios: {results.Count(r => r.StrategyCount == 3)}", + $"Four Strategy Scenarios: {results.Count(r => r.StrategyCount == 4)}" + }; + + File.WriteAllLines(summaryPath, summary); + } + + private void WriteTopPerformingScenarios(List results, string fileIdentifier) + { + var topPath = GetScenarioTopPerformersCsvPath(fileIdentifier); + var headers = + "Rank|ScenarioName|Score|FinalPnl|WinRate|OutperformanceVsHodl|MaxDrawdown|StrategyCount"; + + var topPerformers = results + .Where(r => r.FinalPnl > 0 && r.OutperformanceVsHodl > 5) // Filter for meaningful performance + .OrderByDescending(r => r.Score) + .Take(50) // Top 50 performers + .Select((r, index) => + $"{index + 1}|{r.ScenarioName}|{r.Score:F2}|{r.FinalPnl:F2}|{r.WinRate}|{r.OutperformanceVsHodl:F2}|{r.MaxDrawdown:F2}|{r.StrategyCount}") + .ToList(); + + var content = new List { headers }; + content.AddRange(topPerformers); + File.WriteAllLines(topPath, content); + } + + private string GetScenarioBacktestCsvPath(string fileIdentifier) => + Path.Combine(GetBacktestReportsDirectory(), $"{fileIdentifier}_results.csv"); + + private string GetScenarioErrorCsvPath(string fileIdentifier) => + Path.Combine(GetBacktestReportsDirectory(), $"{fileIdentifier}_errors.csv"); + + private string GetScenarioSummaryCsvPath(string fileIdentifier) => + Path.Combine(GetBacktestReportsDirectory(), $"{fileIdentifier}_summary.txt"); + + private string GetScenarioTopPerformersCsvPath(string fileIdentifier) => + Path.Combine(GetBacktestReportsDirectory(), $"{fileIdentifier}_top_performers.csv"); + + private string GetBacktestReportsDirectory() + { + // Get the project root directory (where the .sln file should be) + var currentDirectory = Directory.GetCurrentDirectory(); + var projectRoot = FindProjectRoot(currentDirectory); + + var reportsDirectory = Path.Combine(projectRoot, "BacktestingReports"); + + // Create directory if it doesn't exist + if (!Directory.Exists(reportsDirectory)) + { + Directory.CreateDirectory(reportsDirectory); + } + + return reportsDirectory; + } + + private string FindProjectRoot(string startDirectory) + { + var directory = new DirectoryInfo(startDirectory); + + // Look for .sln file or src directory to identify project root + while (directory != null) + { + if (directory.GetFiles("*.sln").Any() || + directory.GetDirectories("src").Any()) + { + return directory.FullName; + } + + directory = directory.Parent; + } + + // Fallback to current directory if project root not found + return startDirectory; + } + + private ScenarioBreakdown CalculateScenarioBreakdown() + { + var availableStrategies = GetAllAvailableStrategies(); + var totalStrategies = availableStrategies.Count; + + // Calculate parameter combinations for each strategy + var avgParametersPerStrategy = availableStrategies.Average(s => s.ParameterSets.Count); + + // Single strategy scenarios (each strategy × its parameter sets) + var singleStrategy = availableStrategies.Sum(s => s.ParameterSets.Count); + + // Multi-strategy scenarios (combinations × parameter combinations) + var twoStrategy = CalculateMultiStrategyScenarios(totalStrategies, 2, avgParametersPerStrategy); + var threeStrategy = CalculateMultiStrategyScenarios(totalStrategies, 3, avgParametersPerStrategy); + var fourStrategy = CalculateMultiStrategyScenarios(totalStrategies, 4, avgParametersPerStrategy); + + return new ScenarioBreakdown + { + SingleStrategy = singleStrategy, + TwoStrategy = twoStrategy, + ThreeStrategy = threeStrategy, + FourStrategy = fourStrategy, + Total = singleStrategy + twoStrategy + threeStrategy + fourStrategy + }; + } + + private int CalculateMultiStrategyScenarios(int totalStrategies, int strategyCount, double avgParameters) + { + // Calculate combinations: C(n,k) = n! / (k!(n-k)!) + var combinations = CalculateCombinations(totalStrategies, strategyCount); + + // Each combination has parameter variations (simplified to average) + var parameterCombinations = Math.Pow(avgParameters, strategyCount); + + return (int)(combinations * parameterCombinations); + } + + private int CalculateCombinations(int n, int k) + { + if (k > n) return 0; + if (k == 0 || k == n) return 1; + + // Calculate C(n,k) = n! / (k!(n-k)!) + int result = 1; + for (int i = 1; i <= k; i++) + { + result = result * (n - i + 1) / i; + } + + return result; + } + + private List GenerateScenarioCombinations() + { + var scenarios = new List(); + + // Single strategy scenarios + scenarios.AddRange(GenerateSingleStrategyScenarios()); + + // Two strategy combinations + scenarios.AddRange(GenerateMultiStrategyScenarios(2)); + // + // // Three strategy combinations + // scenarios.AddRange(GenerateMultiStrategyScenarios(3)); + // + // // Four strategy combinations + // scenarios.AddRange(GenerateMultiStrategyScenarios(4)); + + return scenarios; + } + + private List GetAllAvailableStrategies() + { + var strategies = new List(); + + // Signal strategies + strategies.Add(new StrategyConfiguration + { + Type = StrategyType.RsiDivergence, Name = "RSI_Divergence", ParameterSets = GetRsiDivergenceParameters() + }); + strategies.Add(new StrategyConfiguration + { + Type = StrategyType.RsiDivergenceConfirm, Name = "RSI_Divergence_Confirm", + ParameterSets = GetRsiDivergenceConfirmParameters() + }); + strategies.Add(new StrategyConfiguration + { Type = StrategyType.MacdCross, Name = "MACD_Cross", ParameterSets = GetMacdCrossParameters() }); + strategies.Add(new StrategyConfiguration + { Type = StrategyType.EmaCross, Name = "EMA_Cross", ParameterSets = GetEmaCrossParameters() }); + strategies.Add(new StrategyConfiguration + { + Type = StrategyType.DualEmaCross, Name = "Dual_EMA_Cross", ParameterSets = GetDualEmaCrossParameters() + }); + strategies.Add(new StrategyConfiguration + { Type = StrategyType.SuperTrend, Name = "SuperTrend", ParameterSets = GetSuperTrendParameters() }); + strategies.Add(new StrategyConfiguration + { + Type = StrategyType.ChandelierExit, Name = "Chandelier_Exit", + ParameterSets = GetChandelierExitParameters() + }); + strategies.Add(new StrategyConfiguration + { Type = StrategyType.Stc, Name = "STC", ParameterSets = GetStcParameters() }); + strategies.Add(new StrategyConfiguration + { Type = StrategyType.LaggingStc, Name = "Lagging_STC", ParameterSets = GetLaggingStcParameters() }); + strategies.Add(new StrategyConfiguration + { + Type = StrategyType.ThreeWhiteSoldiers, Name = "Three_White_Soldiers", + ParameterSets = GetThreeWhiteSoldiersParameters() + }); + strategies.Add(new StrategyConfiguration + { + Type = StrategyType.SuperTrendCrossEma, Name = "SuperTrend_Cross_EMA", + ParameterSets = GetSuperTrendCrossEmaParameters() + }); + + // Trend strategies + strategies.Add(new StrategyConfiguration + { Type = StrategyType.EmaTrend, Name = "EMA_Trend", ParameterSets = GetEmaTrendParameters() }); + strategies.Add(new StrategyConfiguration + { + Type = StrategyType.StochRsiTrend, Name = "StochRSI_Trend", ParameterSets = GetStochRsiTrendParameters() + }); + + // Context strategies + strategies.Add(new StrategyConfiguration + { Type = StrategyType.StDev, Name = "Standard_Deviation", ParameterSets = GetStDevParameters() }); + + return strategies; + } + + private List GenerateSingleStrategyScenarios() + { + var scenarios = new List(); + var availableStrategies = GetAllAvailableStrategies(); + + foreach (var strategyConfig in availableStrategies) + { + foreach (var parameterSet in strategyConfig.ParameterSets) + { + var scenario = BuildScenario($"{strategyConfig.Name}_{parameterSet.Name}", + new[] { (strategyConfig, parameterSet) }); + scenarios.Add(scenario); + } + } + + return scenarios; + } + + private List GenerateMultiStrategyScenarios(int strategyCount) + { + var scenarios = new List(); + var availableStrategies = GetAllAvailableStrategies(); + + // Generate all combinations of strategies with the specified count + var strategyCombinations = GetCombinations(availableStrategies, strategyCount); + + foreach (var strategyCombination in strategyCombinations) + { + // For each strategy combination, generate parameter combinations + var parameterCombinations = GetParameterCombinations(strategyCombination.ToArray()); + + foreach (var paramCombo in parameterCombinations) + { + var scenarioName = string.Join("_", + paramCombo.Select(p => $"{p.strategyConfig.Name}_{p.parameterSet.Name}")); + var scenario = BuildScenario(scenarioName, paramCombo); + scenarios.Add(scenario); + scenario.LoopbackPeriod = 15; + } + } + + return scenarios; + } + + private Scenario BuildScenario(string scenarioName, + IEnumerable<(StrategyConfiguration strategyConfig, ParameterSet parameterSet)> strategyParams) + { + var scenario = new Scenario(scenarioName); + + foreach (var (strategyConfig, parameterSet) in strategyParams) + { + var strategy = ScenarioHelpers.BuildStrategy( + strategyConfig.Type, + $"{strategyConfig.Name}_{parameterSet.Name}", + period: parameterSet.Period, + fastPeriods: parameterSet.FastPeriods, + slowPeriods: parameterSet.SlowPeriods, + signalPeriods: parameterSet.SignalPeriods, + multiplier: parameterSet.Multiplier, + stochPeriods: parameterSet.StochPeriods, + smoothPeriods: parameterSet.SmoothPeriods, + cyclePeriods: parameterSet.CyclePeriods); + + scenario.AddStrategy(strategy); + } + + return scenario; + } + + private IEnumerable> GetCombinations(IEnumerable elements, int k) + { + return k == 0 + ? new[] { new T[0] } + : elements.SelectMany((e, i) => + GetCombinations(elements.Skip(i + 1), k - 1).Select(c => (new[] { e }).Concat(c))); + } + + private List<(StrategyConfiguration strategyConfig, ParameterSet parameterSet)[]> GetParameterCombinations( + StrategyConfiguration[] strategies) + { + var result = new List<(StrategyConfiguration, ParameterSet)[]>(); + + // Generate all parameter combinations for each strategy in the combination + var allParameterSets = strategies.Select(s => s.ParameterSets.ToList()).ToArray(); + + // Create cartesian product of all parameter sets + var combinations = CartesianProduct(allParameterSets); + + foreach (var combination in combinations) + { + var parameterCombination = + strategies.Zip(combination, (strategy, paramSet) => (strategy, paramSet)).ToArray(); + result.Add(parameterCombination); + } + + return result; + } + + private IEnumerable> CartesianProduct(IEnumerable> sequences) + { + IEnumerable> emptyProduct = new[] { Enumerable.Empty() }; + return sequences.Aggregate( + emptyProduct, + (accumulator, sequence) => + from accseq in accumulator + from item in sequence + select accseq.Concat(new[] { item })); + } + + // Parameter set definitions for each strategy type - using financially meaningful ranges + private List GetRsiDivergenceParameters() + { + var parameters = new List(); + + // RSI Divergence: Most effective periods for crypto/forex + var periods = new[] { 10, 14, 18 }; // Avoid too fast (noise) or too slow (lag) + + foreach (var period in periods) + { + parameters.Add(new ParameterSet { Name = $"RSI_{period}", Period = period }); + } + + return parameters; + } + + private List GetRsiDivergenceConfirmParameters() + { + var parameters = new List(); + + // RSI Confirmation: Slightly different periods to avoid redundancy + var periods = new[] { 12, 16, 20 }; + + foreach (var period in periods) + { + parameters.Add(new ParameterSet { Name = $"RSIConf_{period}", Period = period }); + } + + return parameters; + } + + private List GetMacdCrossParameters() + { + var parameters = new List(); + + // MACD: Proven combinations for different market speeds + var configs = new[] + { + new { Fast = 8, Slow = 21, Signal = 5, Speed = "Fast" }, // Aggressive for scalping + new { Fast = 12, Slow = 26, Signal = 9, Speed = "Standard" }, // Classic MACD + new { Fast = 16, Slow = 30, Signal = 12, Speed = "Slow" } // Conservative for trends + }; + + foreach (var config in configs) + { + parameters.Add(new ParameterSet + { + Name = $"MACD_{config.Speed}", + FastPeriods = config.Fast, + SlowPeriods = config.Slow, + SignalPeriods = config.Signal + }); + } + + return parameters; + } + + private List GetEmaCrossParameters() + { + var parameters = new List(); + + // EMA Cross: Standard institutional levels + var periods = new[] { 20, 50, 100 }; // Skip 200 as it's too slow for most crypto timeframes + + foreach (var period in periods) + { + parameters.Add(new ParameterSet { Name = $"EMA_{period}", Period = period }); + } + + return parameters; + } + + private List GetDualEmaCrossParameters() + { + var parameters = new List(); + + // Dual EMA: Non-overlapping combinations to avoid correlation + var configs = new[] + { + new { Fast = 8, Slow = 21, Speed = "Fast" }, + new { Fast = 12, Slow = 26, Speed = "Medium" }, + new { Fast = 20, Slow = 50, Speed = "Slow" } + }; + + foreach (var config in configs) + { + parameters.Add(new ParameterSet + { + Name = $"DualEMA_{config.Speed}", + FastPeriods = config.Fast, + SlowPeriods = config.Slow + }); + } + + return parameters; + } + + private List GetSuperTrendParameters() + { + var parameters = new List(); + + // SuperTrend: Balanced between sensitivity and noise reduction + var configs = new[] + { + new { Period = 10, Multiplier = 2.5, Type = "Sensitive" }, // Quick reactions + new { Period = 14, Multiplier = 3.0, Type = "Balanced" }, // Standard setting + new { Period = 18, Multiplier = 3.5, Type = "Conservative" } // Reduced noise + }; + + foreach (var config in configs) + { + parameters.Add(new ParameterSet + { + Name = $"ST_{config.Type}", + Period = config.Period, + Multiplier = config.Multiplier + }); + } + + return parameters; + } + + private List GetChandelierExitParameters() + { + var parameters = new List(); + + // Chandelier Exit: Different lookback periods for volatility + var configs = new[] + { + new { Period = 14, Multiplier = 2.5, Type = "Short" }, // Quick exits + new { Period = 22, Multiplier = 3.0, Type = "Standard" }, // Classic setting + new { Period = 30, Multiplier = 3.5, Type = "Long" } // Patient exits + }; + + foreach (var config in configs) + { + parameters.Add(new ParameterSet + { + Name = $"Chandelier_{config.Type}", + Period = config.Period, + Multiplier = config.Multiplier + }); + } + + return parameters; + } + + private List GetStcParameters() + { + var parameters = new List(); + + // STC: Optimized for different market cycles + var configs = new[] + { + new { Cycle = 8, Fast = 21, Slow = 45, Type = "Fast" }, // Short cycles + new { Cycle = 10, Fast = 23, Slow = 50, Type = "Standard" }, // Default + new { Cycle = 12, Fast = 25, Slow = 55, Type = "Smooth" } // Longer cycles + }; + + foreach (var config in configs) + { + parameters.Add(new ParameterSet + { + Name = $"STC_{config.Type}", + CyclePeriods = config.Cycle, + FastPeriods = config.Fast, + SlowPeriods = config.Slow + }); + } + + return parameters; + } + + private List GetLaggingStcParameters() + { + var parameters = new List(); + + // Lagging STC: Slightly different from regular STC to avoid redundancy + var configs = new[] + { + new { Cycle = 9, Fast = 22, Slow = 48, Type = "Fast" }, + new { Cycle = 11, Fast = 24, Slow = 52, Type = "Standard" }, + new { Cycle = 13, Fast = 26, Slow = 56, Type = "Smooth" } + }; + + foreach (var config in configs) + { + parameters.Add(new ParameterSet + { + Name = $"LaggingSTC_{config.Type}", + CyclePeriods = config.Cycle, + FastPeriods = config.Fast, + SlowPeriods = config.Slow + }); + } + + return parameters; + } + + private List GetThreeWhiteSoldiersParameters() + { + var parameters = new List(); + + // Three White Soldiers: Pattern recognition - minimal parameters + parameters.Add(new ParameterSet { Name = "Standard", Period = 3 }); + + return parameters; + } + + private List GetSuperTrendCrossEmaParameters() + { + var parameters = new List(); + + // SuperTrend Cross EMA: Combined indicator parameters + var configs = new[] + { + new { Period = 12, Multiplier = 2.8, Type = "Aggressive" }, + new { Period = 14, Multiplier = 3.0, Type = "Standard" }, + new { Period = 16, Multiplier = 3.2, Type = "Conservative" } + }; + + foreach (var config in configs) + { + parameters.Add(new ParameterSet + { + Name = $"STCrossEMA_{config.Type}", + Period = config.Period, + Multiplier = config.Multiplier + }); + } + + return parameters; + } + + private List GetEmaTrendParameters() + { + var parameters = new List(); + + // EMA Trend: Long-term trend identification + var periods = new[] { 50, 100, 200 }; // Institutional levels + + foreach (var period in periods) + { + parameters.Add(new ParameterSet { Name = $"EMATrend_{period}", Period = period }); + } + + return parameters; + } + + private List GetStochRsiTrendParameters() + { + var parameters = new List(); + + // StochRSI: Optimized for momentum detection + var configs = new[] + { + new { Period = 12, Stoch = 12, Signal = 3, Smooth = 1, Type = "Fast" }, + new { Period = 14, Stoch = 14, Signal = 3, Smooth = 1, Type = "Standard" }, + new { Period = 16, Stoch = 16, Signal = 5, Smooth = 3, Type = "Smooth" } + }; + + foreach (var config in configs) + { + parameters.Add(new ParameterSet + { + Name = $"StochRSI_{config.Type}", + Period = config.Period, + StochPeriods = config.Stoch, + SignalPeriods = config.Signal, + SmoothPeriods = config.Smooth + }); + } + + return parameters; + } + + private List GetStDevParameters() + { + var parameters = new List(); + + // Standard Deviation: Volatility context periods + var periods = new[] { 14, 20, 26 }; // Short to medium-term volatility + + foreach (var period in periods) + { + parameters.Add(new ParameterSet { Name = $"StDev_{period}", Period = period }); + } + + return parameters; + } + } + + public class StrategyConfiguration + { + public StrategyType Type { get; set; } + public string Name { get; set; } + public List ParameterSets { get; set; } = new(); + } + + public class ParameterSet + { + public string Name { get; set; } + public int? Period { get; set; } + public int? FastPeriods { get; set; } + public int? SlowPeriods { get; set; } + public int? SignalPeriods { get; set; } + public double? Multiplier { get; set; } + public int? StochPeriods { get; set; } + public int? SmoothPeriods { get; set; } + public int? CyclePeriods { get; set; } + } + + public class ScenarioBreakdown + { + public int SingleStrategy { get; set; } + public int TwoStrategy { get; set; } + public int ThreeStrategy { get; set; } + public int FourStrategy { get; set; } + public int Total { get; set; } + } + + public class ScenarioBacktestResult + { + public string ScenarioName { get; set; } + public int StrategyCount { get; set; } + public string Ticker { get; set; } + public string BotType { get; set; } + public decimal FinalPnl { get; set; } + public int WinRate { get; set; } + public decimal GrowthPercentage { get; set; } + public decimal HodlPercentage { get; set; } + public decimal OutperformanceVsHodl { get; set; } + public double MaxDrawdown { get; set; } + public int TotalTrades { get; set; } + public double SharpeRatio { get; set; } + public double ExecutionTime { get; set; } + public decimal StopLoss { get; set; } + public decimal TakeProfit { get; set; } + public decimal Leverage { get; set; } + public double Score { get; set; } } } \ No newline at end of file diff --git a/src/Managing.Application.Workers/StatisticService.cs b/src/Managing.Application.Workers/StatisticService.cs index a182122..4fad64b 100644 --- a/src/Managing.Application.Workers/StatisticService.cs +++ b/src/Managing.Application.Workers/StatisticService.cs @@ -283,13 +283,12 @@ public class StatisticService : IStatisticService CloseEarlyWhenProfitable = false }; - var backtest = await _backtester.RunScalpingBotBacktest( + var backtest = await _backtester.RunTradingBotBacktest( config, DateTime.Now.AddDays(-7), DateTime.Now, null, - false, - null); + false); return backtest.Signals; } diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs index b6d7bd4..55866c5 100644 --- a/src/Managing.Application/Abstractions/ITradingBot.cs +++ b/src/Managing.Application/Abstractions/ITradingBot.cs @@ -34,6 +34,7 @@ namespace Managing.Application.Abstractions decimal GetTotalFees(); void LoadStrategies(IEnumerable strategies); void LoadScenario(string scenarioName); + void LoadScenario(Scenario scenario); void UpdateStrategiesValues(); Task LoadAccount(); Task OpenPositionManually(TradeDirection direction); diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index 3b8ef7a..fce36be 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -56,47 +56,32 @@ namespace Managing.Application.Backtesting } /// - /// Runs a unified trading bot backtest with the specified configuration and date range. - /// Automatically handles ScalpingBot and FlippingBot behavior based on config.BotType. + /// 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 + /// 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 + /// The user running the backtest (optional) /// Whether to save the backtest results - /// Optional pre-loaded candles /// The backtest results public async Task RunTradingBotBacktest( TradingBotConfig config, DateTime startDate, DateTime endDate, User user = null, - bool save = false, - List? initialCandles = null) + bool save = false) { var account = await GetAccountFromConfig(config); + var candles = GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate); - // Set FlipPosition based on BotType - config.FlipPosition = config.BotType == BotType.FlippingBot; + var result = await RunBacktestWithCandles(config, candles, user); - var tradingBot = _botFactory.CreateBacktestTradingBot(config); - tradingBot.LoadScenario(config.ScenarioName); - tradingBot.User = user; - await tradingBot.LoadAccount(); - - var candles = initialCandles ?? GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate); - var result = GetBacktestingResult(config, tradingBot, candles); - - if (user != null) - { - result.User = user; - } - // Set start and end dates result.StartDate = startDate; result.EndDate = endDate; - if (save) + if (save && user != null) { _backtestRepository.InsertBacktestForUser(user, result); } @@ -105,25 +90,48 @@ namespace Managing.Application.Backtesting } /// - /// Runs a unified trading bot backtest with pre-loaded candles. - /// Automatically handles ScalpingBot and FlippingBot behavior based on config.BotType. + /// Runs a trading bot backtest with pre-loaded candles. + /// Automatically handles different bot types based on config.BotType. /// - /// The trading bot configuration + /// The trading bot configuration (must include Scenario object or ScenarioName) /// The candles to use for backtesting - /// The user running the backtest + /// The user running the backtest (optional) /// The backtest results public async Task RunTradingBotBacktest( TradingBotConfig config, List candles, User user = null) { - var account = await GetAccountFromConfig(config); - + return await RunBacktestWithCandles(config, candles, user); + } + + /// + /// Core backtesting logic - handles the actual backtest execution with pre-loaded candles + /// + private async Task RunBacktestWithCandles( + TradingBotConfig config, + List candles, + User user = null) + { // Set FlipPosition based on BotType config.FlipPosition = config.BotType == BotType.FlippingBot; var tradingBot = _botFactory.CreateBacktestTradingBot(config); - tradingBot.LoadScenario(config.ScenarioName); + + // Load scenario - prefer Scenario object over ScenarioName + if (config.Scenario != null) + { + tradingBot.LoadScenario(config.Scenario); + } + else if (!string.IsNullOrEmpty(config.ScenarioName)) + { + tradingBot.LoadScenario(config.ScenarioName); + } + else + { + throw new ArgumentException("Either Scenario object or ScenarioName must be provided in TradingBotConfig"); + } + tradingBot.User = user; await tradingBot.LoadAccount(); @@ -137,73 +145,25 @@ namespace Managing.Application.Backtesting return result; } - // Legacy methods - maintained for backward compatibility - public async Task RunScalpingBotBacktest( - TradingBotConfig config, - DateTime startDate, - DateTime endDate, - User user = null, - bool save = false, - List? initialCandles = null) - { - config.BotType = BotType.ScalpingBot; // Ensure correct type - return await RunTradingBotBacktest(config, startDate, endDate, user, save, initialCandles); - } - - public async Task RunFlippingBotBacktest( - TradingBotConfig config, - DateTime startDate, - DateTime endDate, - User user = null, - bool save = false, - List? initialCandles = null) - { - config.BotType = BotType.FlippingBot; // Ensure correct type - return await RunTradingBotBacktest(config, startDate, endDate, user, save, initialCandles); - } - - public async Task RunScalpingBotBacktest( - TradingBotConfig config, - List candles, - User user = null) - { - config.BotType = BotType.ScalpingBot; // Ensure correct type - return await RunTradingBotBacktest(config, candles, user); - } - - public async Task RunFlippingBotBacktest( - TradingBotConfig config, - List candles, - User user = null) - { - config.BotType = BotType.FlippingBot; // Ensure correct type - return await RunTradingBotBacktest(config, candles, user); - } - private async Task GetAccountFromConfig(TradingBotConfig config) { - // Use the account service to get the actual account var account = await _accountService.GetAccount(config.AccountName, false, false); if (account != null) { return account; } - // Fallback: create a basic account structure if not found return new Account { Name = config.AccountName, - Exchange = TradingExchanges.GmxV2 // Default exchange, should be configurable + Exchange = TradingExchanges.GmxV2 }; } private List GetCandles(Account account, Ticker ticker, Timeframe timeframe, DateTime startDate, DateTime endDate) { - List candles; - - // Use specific date range - candles = _exchangeService.GetCandlesInflux(account.Exchange, ticker, + var candles = _exchangeService.GetCandlesInflux(account.Exchange, ticker, startDate, timeframe, endDate).Result; if (candles == null || candles.Count == 0) @@ -326,13 +286,10 @@ namespace Managing.Application.Backtesting return strategiesValues; } - public bool DeleteBacktest(string id) { try { - // Since we no longer have a general DeleteBacktestById method in the repository, - // this should be implemented using DeleteBacktestByIdForUser with null _backtestRepository.DeleteBacktestByIdForUser(null, id); return true; } @@ -347,8 +304,6 @@ namespace Managing.Application.Backtesting { try { - // Since we no longer have a general DeleteAllBacktests method in the repository, - // this should be implemented using DeleteAllBacktestsForUser with null _backtestRepository.DeleteAllBacktestsForUser(null); return true; } @@ -363,10 +318,8 @@ namespace Managing.Application.Backtesting { var backtests = _backtestRepository.GetBacktestsByUser(user).ToList(); - // For each backtest, ensure candles are loaded foreach (var backtest in backtests) { - // If the backtest has no candles or only a few sample candles, retrieve them if (backtest.Candles == null || backtest.Candles.Count == 0 || backtest.Candles.Count < 10) { try @@ -386,7 +339,6 @@ namespace Managing.Application.Backtesting catch (Exception ex) { _logger.LogError(ex, "Failed to retrieve candles for backtest {Id}", backtest.Id); - // Continue with the next backtest if there's an error } } } @@ -396,22 +348,18 @@ namespace Managing.Application.Backtesting public Backtest GetBacktestByIdForUser(User user, string id) { - // Get the backtest from the repository var backtest = _backtestRepository.GetBacktestByIdForUser(user, id); if (backtest == null) return null; - // If the backtest has no candles or only a few sample candles, retrieve them if (backtest.Candles == null || backtest.Candles.Count == 0 || backtest.Candles.Count < 10) { try { - // Get the account var account = new Account { Name = backtest.Config.AccountName, Exchange = TradingExchanges.Evm }; - // Use the stored start and end dates to retrieve candles var candles = _exchangeService.GetCandlesInflux( account.Exchange, backtest.Config.Ticker, @@ -427,7 +375,6 @@ namespace Managing.Application.Backtesting catch (Exception ex) { _logger.LogError(ex, "Failed to retrieve candles for backtest {Id}", id); - // Return the backtest without candles if there's an error } } diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index df59277..9d32198 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -145,6 +145,20 @@ public class TradingBot : Bot, ITradingBot } } + public void LoadScenario(Scenario scenario) + { + if (scenario == null) + { + Logger.LogWarning("Null scenario provided"); + Stop(); + } + else + { + Scenario = scenario; + LoadStrategies(ScenarioHelpers.GetStrategiesFromScenario(scenario)); + } + } + public void LoadStrategies(IEnumerable strategies) { foreach (var strategy in strategies) diff --git a/src/Managing.Domain/Bots/TradingBotConfig.cs b/src/Managing.Domain/Bots/TradingBotConfig.cs index dce250f..2bdae94 100644 --- a/src/Managing.Domain/Bots/TradingBotConfig.cs +++ b/src/Managing.Domain/Bots/TradingBotConfig.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Managing.Domain.MoneyManagements; +using Managing.Domain.Scenarios; using static Managing.Common.Enums; namespace Managing.Domain.Bots; @@ -9,7 +10,6 @@ public class TradingBotConfig [Required] public string AccountName { get; set; } [Required] public MoneyManagement MoneyManagement { get; set; } [Required] public Ticker Ticker { get; set; } - [Required] public string ScenarioName { get; set; } [Required] public Timeframe Timeframe { get; set; } [Required] public bool IsForWatchingOnly { get; set; } [Required] public decimal BotTradingBalance { get; set; } @@ -20,6 +20,17 @@ public class TradingBotConfig [Required] public bool FlipPosition { get; set; } [Required] public string Name { get; set; } + /// + /// The scenario object containing all strategies. When provided, this takes precedence over ScenarioName. + /// This allows running backtests without requiring scenarios to be saved in the database. + /// + public Scenario Scenario { get; set; } + + /// + /// The scenario name to load from database. Only used when Scenario object is not provided. + /// + public string ScenarioName { get; set; } + /// /// Maximum time in hours that a position can remain open before being automatically closed. /// If null, time-based position closure is disabled.