Files
managing-apps/src/Managing.Application.Tests/BotsTests.cs
2025-08-05 17:53:19 +07:00

1472 lines
62 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Diagnostics;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Backtests;
using Managing.Application.Hubs;
using Managing.Core;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Signals;
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 IBacktester _backtester;
private readonly string _reportPath;
private string _analysePath;
private readonly string _errorsPath;
private readonly string _s = "|";
private List<double> _elapsedTimes { get; set; }
public BotsTests() : base()
{
var backtestRepository = new Mock<IBacktestRepository>().Object;
var discordService = new Mock<IMessengerService>().Object;
var scenarioService = new Mock<IScenarioService>().Object;
var messengerService = new Mock<IMessengerService>().Object;
var kaigenService = new Mock<IKaigenService>().Object;
var hubContext = new Mock<IHubContext<BacktestHub>>().Object;
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
var botService = new Mock<IBotService>().Object;
_backtester = new Backtester(_exchangeService, backtestRepository, backtestLogger,
scenarioService, _accountService.Object, messengerService, kaigenService, hubContext, null);
_elapsedTimes = new List<double>();
// 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);
var localCandles =
FileHelpers.ReadJson<List<Candle>>($"{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, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false);
var json = JsonConvert.SerializeObject(backtestResult, Formatting.None);
File.WriteAllText($"{ticker}-{timeframe}-{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 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")
{
Indicators = new List<IndicatorBase>
{
new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
}
};
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<Tuple<string, int, decimal, decimal, decimal, decimal>>();
var errors = new List<string>();
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = 4
};
var periodRange = new List<int>() { 2, 7 };
var stopLossRange = new List<decimal>() { 0.005m, 0.05m, 0.005m };
var takeProfitRange = new List<decimal>() { 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");
scenario.Indicators = new List<IndicatorBase>
{
new RsiDivergenceIndicatorBase("RsiDiv", (int)i)
};
// -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
}, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, 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
}, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
_ => throw new NotImplementedException(),
};
timer.Stop();
if (backtestResult.FinalPnl > 0
&& (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30
&& backtestResult.Score < 3)
{
var currentResult = new Tuple<string, int, decimal, decimal, decimal, decimal>(
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<Tuple<string, decimal, decimal, decimal, decimal>>();
var errors = new List<string>();
var options = new ParallelOptions()
{
MaxDegreeOfParallelism = 4
};
var stopLossRange = new List<decimal>() { 0.005m, 0.05m, 0.005m };
var takeProfitRange = new List<decimal>() { 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");
scenario.Indicators = new List<IndicatorBase>
{
new MacdCrossIndicatorBase("MacdCross", 12, 26, 9)
};
// -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
}, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, 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
}, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).Result,
_ => throw new NotImplementedException(),
};
if (backtestResult.FinalPnl > 0
&& (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30)
{
var currentResult = new Tuple<string, decimal, decimal, decimal, decimal>(
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<int> periodRange, List<decimal> stopLossRange,
List<decimal> 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<decimal> stopLossRange, List<decimal> 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<ScenarioBacktestResult>();
var errors = new List<string>();
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, DateTime.UtcNow.AddDays(-6),
DateTime.UtcNow, null, false, false).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<ScenarioBacktestResult> results, string fileIdentifier)
{
var summaryPath = GetScenarioSummaryCsvPath(fileIdentifier);
var summary = new List<string>
{
"=== 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<ScenarioBacktestResult> 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<string> { 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<Scenario> GenerateScenarioCombinations()
{
var scenarios = new List<Scenario>();
// 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<StrategyConfiguration> GetAllAvailableStrategies()
{
var strategies = new List<StrategyConfiguration>();
// 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<Scenario> GenerateSingleStrategyScenarios()
{
var scenarios = new List<Scenario>();
var availableStrategies = GetAllAvailableStrategies();
foreach (var strategyConfig in availableStrategies)
{
foreach (var parameterSet in strategyConfig.ParameterSets)
{
var scenario = new Scenario($"{strategyConfig.Name}_{parameterSet.Name}")
{
Indicators = new List<IndicatorBase>
{
new RsiDivergenceIndicatorBase("RsiDiv", (int)parameterSet.Period)
}
};
scenarios.Add(scenario);
}
}
return scenarios;
}
private List<Scenario> GenerateMultiStrategyScenarios(int strategyCount)
{
var scenarios = new List<Scenario>();
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 = new Scenario(scenarioName)
{
Indicators = new List<IndicatorBase>
{
new RsiDivergenceIndicatorBase("RsiDiv", (int)paramCombo.First().parameterSet.Period)
}
};
scenario.LoopbackPeriod = 15;
scenarios.Add(scenario);
}
}
return scenarios;
}
private IEnumerable<IEnumerable<T>> GetCombinations<T>(IEnumerable<T> 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<IEnumerable<T>> CartesianProduct<T>(IEnumerable<IEnumerable<T>> sequences)
{
IEnumerable<IEnumerable<T>> emptyProduct = new[] { Enumerable.Empty<T>() };
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<ParameterSet> GetRsiDivergenceParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetRsiDivergenceConfirmParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetMacdCrossParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetEmaCrossParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetDualEmaCrossParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetSuperTrendParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetChandelierExitParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetStcParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetLaggingStcParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetThreeWhiteSoldiersParameters()
{
var parameters = new List<ParameterSet>();
// Three White Soldiers: Pattern recognition - minimal parameters
parameters.Add(new ParameterSet { Name = "Standard", Period = 3 });
return parameters;
}
private List<ParameterSet> GetSuperTrendCrossEmaParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetEmaTrendParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetStochRsiTrendParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> GetStDevParameters()
{
var parameters = new List<ParameterSet>();
// 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<ParameterSet> 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; }
}
}