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.