using System.Collections; using System.Diagnostics; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Backtesting; using Managing.Application.Bots.Base; using Managing.Application.Hubs; using Managing.Application.ManageBot; using Managing.Core; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Microsoft.AspNetCore.SignalR; using Moq; using Newtonsoft.Json; using Xunit; using static Managing.Common.Enums; namespace Managing.Application.Tests { public class BotsTests : BaseTests { private readonly IBotFactory _botFactory; private readonly IBacktester _backtester; private readonly string _reportPath; private string _analysePath; private readonly string _errorsPath; private readonly string _s = "|"; private List _elapsedTimes { get; set; } public BotsTests() : base() { var backtestRepository = new Mock().Object; var discordService = new Mock().Object; var scenarioService = new Mock().Object; var messengerService = new Mock().Object; var kaigenService = new Mock().Object; var backupBotService = new Mock().Object; var hubContext = new Mock>().Object; var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger(); var backtestLogger = TradingBaseTests.CreateBacktesterLogger(); var botService = new Mock().Object; _botFactory = new BotFactory( _exchangeService, tradingBotLogger, discordService, _accountService.Object, _tradingService.Object, botService, backupBotService); _backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger, scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null); _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] [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -10)] public async Task SwingBot_Should_Return_Positiv_Profit_For_Every_Position(Ticker ticker, Timeframe timeframe, int days) { // Arrange var scenario = new Scenario("FlippingScenario"); var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14); scenario.AddIndicator(strategy); var localCandles = FileHelpers.ReadJson>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json"); var config = new TradingBotConfig { AccountName = _account.Name, MoneyManagement = MoneyManagement, Ticker = ticker, Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = true, Name = "Test", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }; // Act var backtestResult = await _backtester.RunTradingBotBacktest(config, localCandles.TakeLast(500).ToList(), null, false); var json = JsonConvert.SerializeObject(backtestResult, Formatting.None); File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json); // WriteCsvReport(backtestResult.GetStringReport()); // Assert Assert.True(backtestResult.FinalPnl > 0); Assert.True(backtestResult.WinRate >= 30); Assert.True(backtestResult.GrowthPercentage > backtestResult.HodlPercentage); } [Theory] //[InlineData(Enums.Exchanges.Binance, "ADAUSDT", Timeframe.ThirtyMinutes, -5)] //[InlineData(Enums.Exchanges.Binance, "ADAUSDT", Timeframe.FifteenMinutes, -5)] //[InlineData(Enums.Exchanges.Binance, "SOLUSDT", Timeframe.ThirtyMinutes, -4)] //[InlineData(Enums.Exchanges.Binance, "SOLUSDT", Timeframe.FifteenMinutes, -4)] //[InlineData(Enums.Exchanges.Binance, "BTCUSDT", Timeframe.ThirtyMinutes, -4)] //[InlineData(Enums.Exchanges.Binance, "BTCUSDT", Timeframe.FifteenMinutes, -4)] [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -14)] public async Task ScalpingBot_Should_Return_Positiv_Profit_For_Every_Position(Ticker ticker, Timeframe timeframe, int days) { // Arrange var scenario = new Scenario("ScalpingScenario"); var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 5); scenario.AddIndicator(strategy); var config = new TradingBotConfig { AccountName = _account.Name, MoneyManagement = MoneyManagement, Ticker = ticker, Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, Name = "Test", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }; // Act var backtestResult = await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null, false, false); //WriteCsvReport(backtestResult.GetStringReport()); // Assert Assert.True(backtestResult.FinalPnl > 0); Assert.True(backtestResult.WinRate >= 30); Assert.True(backtestResult.GrowthPercentage > backtestResult.HodlPercentage); } [Theory] [InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -8)] public async Task MacdCross_Should_Return_Positiv_Profit_For_Every_Position(Ticker ticker, Timeframe timeframe, int days) { // Arrange var scenario = new Scenario("ScalpingScenario"); var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.MacdCross, "RsiDiv", fastPeriods: 12, slowPeriods: 26, signalPeriods: 9); scenario.AddIndicator(strategy); var moneyManagement = new MoneyManagement() { Leverage = 1, Timeframe = timeframe, StopLoss = 0.01m, TakeProfit = 0.02m }; var config = new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, Name = "Test", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }; // Act var backtestResult = await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null, false, false); // WriteCsvReport(backtestResult.GetStringReport()); // Assert Assert.True(backtestResult.FinalPnl > 0); Assert.True(backtestResult.WinRate >= 30); Assert.True(backtestResult.GrowthPercentage > backtestResult.HodlPercentage); } [Theory] [InlineData(Timeframe.FifteenMinutes, -6, IndicatorType.Stc, BotType.ScalpingBot)] //[InlineData(Timeframe.FifteenMinutes, -6, Enums.StrategyType.RsiDivergenceConfirm, Enums.BotType.FlippingBot)] public void GetBestPeriodRsiForDivergenceFlippingBot(Timeframe timeframe, int days, IndicatorType indicatorType, BotType botType) { var result = new List>(); var errors = new List(); var options = new ParallelOptions() { MaxDegreeOfParallelism = 4 }; var periodRange = new List() { 2, 7 }; var stopLossRange = new List() { 0.005m, 0.05m, 0.005m }; var takeProfitRange = new List() { 0.01m, 0.1m, 0.02m }; var fileIdentifier = $"{indicatorType}-{timeframe}"; var completedTest = 0; var totalTests = GetTotalTrades(periodRange, stopLossRange, takeProfitRange) * Enum.GetNames(typeof(Ticker)).Length; CleanAnalyseFile(fileIdentifier); UpdateProgression(totalTests, completedTest); Parallel.ForEach((Ticker[])Enum.GetValues(typeof(Ticker)), options, ticker => { var candles = _exchangeService .GetCandles(_account, ticker, DateTime.Now.AddDays(Convert.ToDouble(days)), timeframe, true).Result; if (candles == null || candles.Count == 0) return; Parallel.For((long)periodRange[0], periodRange[1], options, i => { var scenario = new Scenario("ScalpingScenario"); var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", period: (int)i); scenario.AddIndicator(strategy); // -0.5 to -5 for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2]) { // +1% to +10% in 1% for (decimal t = takeProfitRange[0]; t < takeProfitRange[1]; t += takeProfitRange[2]) { var moneyManagement = new MoneyManagement() { Leverage = 1, Timeframe = timeframe, StopLoss = s, TakeProfit = t }; try { var timer = new Stopwatch(); timer.Start(); var backtestResult = botType switch { BotType.SimpleBot => throw new NotImplementedException(), BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, Name = "Test", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }, candles, null, false).Result, BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = true, Name = "Test", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }, candles, null, false).Result, _ => throw new NotImplementedException(), }; timer.Stop(); if (backtestResult.FinalPnl > 0 && (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30 && backtestResult.Score < 3) { var currentResult = new Tuple( ticker.ToString(), (int)i, backtestResult.FinalPnl, s, t, backtestResult.GrowthPercentage - backtestResult.HodlPercentage); result.Add(currentResult); } completedTest++; UpdateProgression(totalTests, completedTest, timer.Elapsed.TotalSeconds); } catch (Exception ex) { completedTest++; errors.Add($"{ticker}{_s}{i}{_s}{s}{_s}{t}{_s}{ex.Message}"); } } } }); }); foreach (var r in result) { WriteCsvAnalyse( $"{r.Item1}{_s}{r.Item2}{_s}{r.Item3.ToString("0.000")}{_s}{r.Item4 * 100}{_s}{r.Item5 * 100}{_s}{r.Item6}"); } foreach (var e in errors) { WriteCsvErrors(e); } var bestResult = result.OrderByDescending(b => b.Item3).FirstOrDefault(); WriteCsvAnalyse( $"Best result : {bestResult.Item1} - Rsi Period : {bestResult.Item2} - {bestResult.Item3} - SL : {bestResult.Item4}% - TP : {bestResult.Item5}%"); Assert.True(result.Any()); } [Theory] [InlineData(Timeframe.OneHour, -30, IndicatorType.MacdCross, BotType.FlippingBot)] [InlineData(Timeframe.OneHour, -30, IndicatorType.MacdCross, BotType.ScalpingBot)] public void GetBestMMForMacdFlippingBot(Timeframe timeframe, int days, IndicatorType indicatorType, BotType botType) { var result = new List>(); var errors = new List(); var options = new ParallelOptions() { MaxDegreeOfParallelism = 4 }; var stopLossRange = new List() { 0.005m, 0.05m, 0.005m }; var takeProfitRange = new List() { 0.01m, 0.1m, 0.02m }; var fileIdentifier = $"{indicatorType}-{timeframe}-{botType}"; var completedTest = 0; var totalTests = GetTotalTradeForStopLossTakeProfit(stopLossRange, takeProfitRange) * Enum.GetNames(typeof(Ticker)).Length; CleanAnalyseFile(fileIdentifier); UpdateProgression(totalTests, completedTest); Parallel.ForEach((Ticker[])Enum.GetValues(typeof(Ticker)), options, ticker => { var candles = _exchangeService .GetCandles(_account, ticker, DateTime.Now.AddDays(Convert.ToDouble(days)), timeframe, true).Result; if (candles == null || candles.Count == 0) return; var scenario = new Scenario("ScalpingScenario"); var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", fastPeriods: 12, slowPeriods: 26, signalPeriods: 9); scenario.AddIndicator(strategy); // -0.5 to -5 for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2]) { // +1% to +10% in 1% for (decimal t = takeProfitRange[0]; t < takeProfitRange[1]; t += takeProfitRange[2]) { var timer = new Stopwatch(); timer.Start(); try { var moneyManagement = new MoneyManagement() { Leverage = 1, Timeframe = timeframe, StopLoss = s, TakeProfit = t }; var backtestResult = botType switch { BotType.SimpleBot => throw new NotImplementedException(), BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, Name = "Test", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }, candles, null).Result, BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig { AccountName = _account.Name, MoneyManagement = moneyManagement, Ticker = ticker, Scenario = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = true, Name = "Test", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }, candles, null).Result, _ => throw new NotImplementedException(), }; if (backtestResult.FinalPnl > 0 && (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30) { var currentResult = new Tuple( ticker.ToString(), backtestResult.FinalPnl, s, t, backtestResult.GrowthPercentage - backtestResult.HodlPercentage); result.Add(currentResult); } completedTest++; UpdateProgression(totalTests, completedTest, timer.Elapsed.TotalSeconds); } catch (Exception ex) { completedTest++; errors.Add($"{ticker}{_s}{s}{_s}{t}{_s}{ex.Message}"); } timer.Stop(); } } }); foreach (var r in result) { WriteCsvAnalyse( $"{r.Item1}{_s}{r.Item2.ToString("0.000")}{_s}{r.Item3 * 100}{_s}{r.Item4 * 100}{_s}{r.Item5}"); } foreach (var e in errors) { WriteCsvErrors(e); } var bestResult = result.OrderByDescending(b => b.Item3).FirstOrDefault(); WriteCsvAnalyse( $"Best result : {bestResult.Item1} - Rsi Period : {bestResult.Item2} - {bestResult.Item3} - SL : {bestResult.Item4}% - TP : {bestResult.Item5}%"); Assert.True(result.Any()); } private void WriteCsvReport(string line) { File.AppendAllLines(_reportPath, new[] { line }); } private void WriteCsvAnalyse(string line, string fileIdentifier = null) { if (!string.IsNullOrEmpty(fileIdentifier)) _analysePath += $"-{fileIdentifier}-{DateTime.Now.ToString("dd-MM-HH-mm")}.csv"; File.AppendAllLines(_analysePath, new[] { line }); } private void WriteCsvErrors(string line) { File.AppendAllLines(_errorsPath, new[] { line }); } private void CleanAnalyseFile(string fileIdentifier) { WriteCsvAnalyse("", fileIdentifier); } private decimal GetTotalTrades(List periodRange, List stopLossRange, List takeProfitRange) { var stopLossRangeTotalTest = stopLossRange[1] / stopLossRange[2]; var takeProfitRangeTotalTest = takeProfitRange[1] / takeProfitRange[2]; var totalTrades = GetTotalTradeForStopLossTakeProfit(stopLossRange, takeProfitRange) * stopLossRangeTotalTest * takeProfitRangeTotalTest; return totalTrades; } private decimal GetTotalTradeForStopLossTakeProfit(List stopLossRange, List takeProfitRange) { var stopLossRangeTotalTest = stopLossRange[1] / stopLossRange[2]; var takeProfitRangeTotalTest = takeProfitRange[1] / takeProfitRange[2]; var totalTrades = stopLossRangeTotalTest * takeProfitRangeTotalTest; return totalTrades; } private void UpdateProgression(decimal totalTest, int completedTest, double? elapsed = null) { var timeRemaining = ""; if (elapsed.HasValue && completedTest > 0) { //_elapsedTimes.Add((elapsed.Value / completedTest) * ((double)totalTest - completedTest)); _elapsedTimes.Add(elapsed.Value); //var t = (_elapsedTimes.Average() / completedTest) * ((double)totalTest - completedTest); var t = (_elapsedTimes.Average() * (double)(totalTest - completedTest)); timeRemaining = $" Remaining time: {t} seconds - Estimated end date: {DateTime.Now.AddSeconds(t)}"; } ModifyFirstRow(_analysePath, $"{completedTest}/{totalTest}{timeRemaining}"); } private void ModifyFirstRow(string filepath, string newValue) { ArrayList rows = new ArrayList(); using (StreamReader reader = new StreamReader(filepath)) { string row = null; while ((row = reader.ReadLine()) != null) { rows.Add(row); } } // Determ if the file even contains rows. if (rows.Count > 0) { // Replace the first row. rows[0] = newValue; } else { // Add the new value because there // where no rows found in the file. rows.Add(newValue); } // Write the modified content to the file. using (StreamWriter writer = new StreamWriter(filepath, false)) { foreach (String row in rows) { writer.WriteLine(row); } } } [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.Indicators.Count} strategies)"); foreach (var strategy in scenario.Indicators) { 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 = LightScenario.FromScenario(scenario), Timeframe = timeframe, IsForWatchingOnly = false, BotTradingBalance = 1000, 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 scenarioResult = new ScenarioBacktestResult { ScenarioName = scenario.Name, StrategyCount = scenario.Indicators.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.MaxDrawdown ?? 0), SharpeRatio = (double)(backtestResult.SharpeRatio ?? 0), ExecutionTime = timer.Elapsed.TotalSeconds, StopLoss = standardMoneyManagement.StopLoss, TakeProfit = standardMoneyManagement.TakeProfit, Leverage = standardMoneyManagement.Leverage, Score = backtestResult.Score, }; 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 = IndicatorType.RsiDivergence, Name = "RSI_Divergence", ParameterSets = GetRsiDivergenceParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.RsiDivergenceConfirm, Name = "RSI_Divergence_Confirm", ParameterSets = GetRsiDivergenceConfirmParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.MacdCross, Name = "MACD_Cross", ParameterSets = GetMacdCrossParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.EmaCross, Name = "EMA_Cross", ParameterSets = GetEmaCrossParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.DualEmaCross, Name = "Dual_EMA_Cross", ParameterSets = GetDualEmaCrossParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.SuperTrend, Name = "SuperTrend", ParameterSets = GetSuperTrendParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.ChandelierExit, Name = "Chandelier_Exit", ParameterSets = GetChandelierExitParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.Stc, Name = "STC", ParameterSets = GetStcParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.LaggingStc, Name = "Lagging_STC", ParameterSets = GetLaggingStcParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.ThreeWhiteSoldiers, Name = "Three_White_Soldiers", ParameterSets = GetThreeWhiteSoldiersParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.SuperTrendCrossEma, Name = "SuperTrend_Cross_EMA", ParameterSets = GetSuperTrendCrossEmaParameters() }); // Trend strategies strategies.Add(new StrategyConfiguration { Type = IndicatorType.EmaTrend, Name = "EMA_Trend", ParameterSets = GetEmaTrendParameters() }); strategies.Add(new StrategyConfiguration { Type = IndicatorType.StochRsiTrend, Name = "StochRSI_Trend", ParameterSets = GetStochRsiTrendParameters() }); // Context strategies strategies.Add(new StrategyConfiguration { Type = IndicatorType.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); scenario.LoopbackPeriod = 15; scenarios.Add(scenario); } } 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.BuildIndicator( 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.AddIndicator(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 IndicatorType 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; } } }