1501 lines
64 KiB
C#
1501 lines
64 KiB
C#
using System.Collections;
|
||
using System.Diagnostics;
|
||
using Managing.Application.Abstractions;
|
||
using Managing.Application.Abstractions.Repositories;
|
||
using Managing.Application.Abstractions.Services;
|
||
using Managing.Application.Backtesting;
|
||
using Managing.Application.Bots.Base;
|
||
using Managing.Core;
|
||
using Managing.Domain.Backtests;
|
||
using Managing.Domain.Bots;
|
||
using Managing.Domain.Candles;
|
||
using Managing.Domain.MoneyManagements;
|
||
using Managing.Domain.Scenarios;
|
||
using Moq;
|
||
using Newtonsoft.Json;
|
||
using Xunit;
|
||
using static Managing.Common.Enums;
|
||
|
||
namespace Managing.Application.Tests
|
||
{
|
||
public class BotsTests : BaseTests
|
||
{
|
||
private readonly IBotFactory _botFactory;
|
||
private readonly IBacktester _backtester;
|
||
private readonly string _reportPath;
|
||
private string _analysePath;
|
||
private readonly string _errorsPath;
|
||
private readonly string _s = "|";
|
||
private List<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 tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
|
||
var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
|
||
var botService = new Mock<IBotService>().Object;
|
||
_botFactory = new BotFactory(
|
||
_exchangeService,
|
||
tradingBotLogger,
|
||
discordService,
|
||
_accountService.Object,
|
||
_tradingService.Object,
|
||
botService);
|
||
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
|
||
scenarioService, _accountService.Object, messengerService, kaigenService);
|
||
_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);
|
||
scenario.AddIndicator(strategy);
|
||
var localCandles =
|
||
FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json");
|
||
|
||
var config = new TradingBotConfig
|
||
{
|
||
AccountName = _account.Name,
|
||
MoneyManagement = MoneyManagement,
|
||
Ticker = ticker,
|
||
Scenario = scenario,
|
||
Timeframe = timeframe,
|
||
IsForWatchingOnly = false,
|
||
BotTradingBalance = 1000,
|
||
IsForBacktest = true,
|
||
CooldownPeriod = 1,
|
||
MaxLossStreak = 0,
|
||
FlipPosition = true,
|
||
Name = "Test",
|
||
FlipOnlyWhenInProfit = true,
|
||
MaxPositionTimeHours = null,
|
||
CloseEarlyWhenProfitable = false
|
||
};
|
||
|
||
// Act
|
||
var backtestResult =
|
||
await _backtester.RunTradingBotBacktest(config, localCandles.TakeLast(500).ToList(), null, false);
|
||
|
||
var json = JsonConvert.SerializeObject(backtestResult, Formatting.None);
|
||
File.WriteAllText($"{ticker.ToString()}-{timeframe.ToString()}-{Guid.NewGuid()}.json", json);
|
||
// WriteCsvReport(backtestResult.GetStringReport());
|
||
|
||
// Assert
|
||
Assert.True(backtestResult.FinalPnl > 0);
|
||
Assert.True(backtestResult.WinRate >= 30);
|
||
Assert.True(backtestResult.GrowthPercentage > backtestResult.HodlPercentage);
|
||
}
|
||
|
||
[Theory]
|
||
//[InlineData(Enums.Exchanges.Binance, "ADAUSDT", Timeframe.ThirtyMinutes, -5)]
|
||
//[InlineData(Enums.Exchanges.Binance, "ADAUSDT", Timeframe.FifteenMinutes, -5)]
|
||
//[InlineData(Enums.Exchanges.Binance, "SOLUSDT", Timeframe.ThirtyMinutes, -4)]
|
||
//[InlineData(Enums.Exchanges.Binance, "SOLUSDT", Timeframe.FifteenMinutes, -4)]
|
||
//[InlineData(Enums.Exchanges.Binance, "BTCUSDT", Timeframe.ThirtyMinutes, -4)]
|
||
//[InlineData(Enums.Exchanges.Binance, "BTCUSDT", Timeframe.FifteenMinutes, -4)]
|
||
[InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -14)]
|
||
public async Task ScalpingBot_Should_Return_Positiv_Profit_For_Every_Position(Ticker ticker,
|
||
Timeframe timeframe,
|
||
int days)
|
||
{
|
||
// Arrange
|
||
var scenario = new Scenario("ScalpingScenario");
|
||
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 5);
|
||
scenario.AddIndicator(strategy);
|
||
|
||
var config = new TradingBotConfig
|
||
{
|
||
AccountName = _account.Name,
|
||
MoneyManagement = MoneyManagement,
|
||
Ticker = ticker,
|
||
Scenario = scenario,
|
||
Timeframe = timeframe,
|
||
IsForWatchingOnly = false,
|
||
BotTradingBalance = 1000,
|
||
IsForBacktest = true,
|
||
CooldownPeriod = 1,
|
||
MaxLossStreak = 0,
|
||
FlipPosition = false,
|
||
Name = "Test",
|
||
FlipOnlyWhenInProfit = true,
|
||
MaxPositionTimeHours = null,
|
||
CloseEarlyWhenProfitable = false
|
||
};
|
||
|
||
// Act
|
||
var backtestResult = await _backtester.RunTradingBotBacktest(config, DateTime.UtcNow.AddDays(-6),
|
||
DateTime.UtcNow, null, false, false);
|
||
//WriteCsvReport(backtestResult.GetStringReport());
|
||
|
||
// Assert
|
||
Assert.True(backtestResult.FinalPnl > 0);
|
||
Assert.True(backtestResult.WinRate >= 30);
|
||
Assert.True(backtestResult.GrowthPercentage > backtestResult.HodlPercentage);
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData(Ticker.BTC, Timeframe.FifteenMinutes, -8)]
|
||
public async Task MacdCross_Should_Return_Positiv_Profit_For_Every_Position(Ticker ticker, Timeframe timeframe,
|
||
int days)
|
||
{
|
||
// Arrange
|
||
var scenario = new Scenario("ScalpingScenario");
|
||
var strategy = ScenarioHelpers.BuildIndicator(IndicatorType.MacdCross, "RsiDiv", fastPeriods: 12,
|
||
slowPeriods: 26, signalPeriods: 9);
|
||
scenario.AddIndicator(strategy);
|
||
|
||
var moneyManagement = new MoneyManagement()
|
||
{
|
||
Leverage = 1,
|
||
Timeframe = timeframe,
|
||
StopLoss = 0.01m,
|
||
TakeProfit = 0.02m
|
||
};
|
||
|
||
var config = new TradingBotConfig
|
||
{
|
||
AccountName = _account.Name,
|
||
MoneyManagement = moneyManagement,
|
||
Ticker = ticker,
|
||
Scenario = 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(periodRange[0], periodRange[1], options, i =>
|
||
{
|
||
var scenario = new Scenario("ScalpingScenario");
|
||
var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", period: i);
|
||
scenario.AddIndicator(strategy);
|
||
|
||
// -0.5 to -5
|
||
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
|
||
{
|
||
// +1% to +10% in 1%
|
||
for (decimal t = takeProfitRange[0]; t < takeProfitRange[1]; t += takeProfitRange[2])
|
||
{
|
||
var moneyManagement = new MoneyManagement()
|
||
{
|
||
Leverage = 1,
|
||
Timeframe = timeframe,
|
||
StopLoss = s,
|
||
TakeProfit = t
|
||
};
|
||
|
||
try
|
||
{
|
||
var timer = new Stopwatch();
|
||
timer.Start();
|
||
|
||
var backtestResult = botType switch
|
||
{
|
||
BotType.SimpleBot => throw new NotImplementedException(),
|
||
BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
|
||
{
|
||
AccountName = _account.Name,
|
||
MoneyManagement = moneyManagement,
|
||
Ticker = ticker,
|
||
Scenario = scenario,
|
||
Timeframe = timeframe,
|
||
IsForWatchingOnly = false,
|
||
BotTradingBalance = 1000,
|
||
IsForBacktest = true,
|
||
CooldownPeriod = 1,
|
||
MaxLossStreak = 0,
|
||
FlipPosition = false,
|
||
Name = "Test",
|
||
FlipOnlyWhenInProfit = true,
|
||
MaxPositionTimeHours = null,
|
||
CloseEarlyWhenProfitable = false
|
||
}, candles, null, false).Result,
|
||
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
|
||
{
|
||
AccountName = _account.Name,
|
||
MoneyManagement = moneyManagement,
|
||
Ticker = ticker,
|
||
Scenario = scenario,
|
||
Timeframe = timeframe,
|
||
IsForWatchingOnly = false,
|
||
BotTradingBalance = 1000,
|
||
IsForBacktest = true,
|
||
CooldownPeriod = 1,
|
||
MaxLossStreak = 0,
|
||
FlipPosition = true,
|
||
Name = "Test",
|
||
FlipOnlyWhenInProfit = true,
|
||
MaxPositionTimeHours = null,
|
||
CloseEarlyWhenProfitable = false
|
||
}, candles, null, false).Result,
|
||
_ => throw new NotImplementedException(),
|
||
};
|
||
timer.Stop();
|
||
|
||
if (backtestResult.FinalPnl > 0
|
||
&& (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30
|
||
&& backtestResult.Statistics.MaxDrawdown < 3)
|
||
{
|
||
var currentResult = new Tuple<string, int, decimal, decimal, decimal, decimal>(
|
||
ticker.ToString(), 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");
|
||
var strategy = ScenarioHelpers.BuildIndicator(indicatorType, "RsiDiv", fastPeriods: 12,
|
||
slowPeriods: 26, signalPeriods: 9);
|
||
scenario.AddIndicator(strategy);
|
||
|
||
// -0.5 to -5
|
||
for (decimal s = stopLossRange[0]; s < stopLossRange[1]; s += stopLossRange[2])
|
||
{
|
||
// +1% to +10% in 1%
|
||
for (decimal t = takeProfitRange[0]; t < takeProfitRange[1]; t += takeProfitRange[2])
|
||
{
|
||
var timer = new Stopwatch();
|
||
timer.Start();
|
||
try
|
||
{
|
||
var moneyManagement = new MoneyManagement()
|
||
{
|
||
Leverage = 1,
|
||
Timeframe = timeframe,
|
||
StopLoss = s,
|
||
TakeProfit = t
|
||
};
|
||
|
||
var backtestResult = botType switch
|
||
{
|
||
BotType.SimpleBot => throw new NotImplementedException(),
|
||
BotType.ScalpingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
|
||
{
|
||
AccountName = _account.Name,
|
||
MoneyManagement = moneyManagement,
|
||
Ticker = ticker,
|
||
Scenario = scenario,
|
||
Timeframe = timeframe,
|
||
IsForWatchingOnly = false,
|
||
BotTradingBalance = 1000,
|
||
IsForBacktest = true,
|
||
CooldownPeriod = 1,
|
||
MaxLossStreak = 0,
|
||
FlipPosition = false,
|
||
Name = "Test",
|
||
FlipOnlyWhenInProfit = true,
|
||
MaxPositionTimeHours = null,
|
||
CloseEarlyWhenProfitable = false
|
||
}, candles, null).Result,
|
||
BotType.FlippingBot => _backtester.RunTradingBotBacktest(new TradingBotConfig
|
||
{
|
||
AccountName = _account.Name,
|
||
MoneyManagement = moneyManagement,
|
||
Ticker = ticker,
|
||
Scenario = scenario,
|
||
Timeframe = timeframe,
|
||
IsForWatchingOnly = false,
|
||
BotTradingBalance = 1000,
|
||
IsForBacktest = true,
|
||
CooldownPeriod = 1,
|
||
MaxLossStreak = 0,
|
||
FlipPosition = true,
|
||
Name = "Test",
|
||
FlipOnlyWhenInProfit = true,
|
||
MaxPositionTimeHours = null,
|
||
CloseEarlyWhenProfitable = false
|
||
}, candles, null).Result,
|
||
_ => throw new NotImplementedException(),
|
||
};
|
||
|
||
if (backtestResult.FinalPnl > 0
|
||
&& (backtestResult.GrowthPercentage - backtestResult.HodlPercentage) > 30
|
||
&& backtestResult.Statistics.MaxDrawdown < 3)
|
||
{
|
||
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 = scenario,
|
||
Timeframe = timeframe,
|
||
IsForWatchingOnly = false,
|
||
BotTradingBalance = 1000,
|
||
IsForBacktest = true,
|
||
CooldownPeriod = 1,
|
||
MaxLossStreak = 0,
|
||
FlipPosition = botType == BotType.FlippingBot,
|
||
Name = $"Test_{scenario.Name}",
|
||
FlipOnlyWhenInProfit = true,
|
||
MaxPositionTimeHours = null,
|
||
CloseEarlyWhenProfitable = false
|
||
};
|
||
|
||
var backtestResult = _backtester.RunTradingBotBacktest(config, candles, null).Result;
|
||
|
||
timer.Stop();
|
||
|
||
var 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,
|
||
maxDrawdown: backtestResult.Statistics?.MaxDrawdown ?? 0,
|
||
initialBalance: config.BotTradingBalance,
|
||
startDate: backtestResult.StartDate,
|
||
endDate: backtestResult.EndDate,
|
||
timeframe: config.Timeframe
|
||
);
|
||
|
||
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.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<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 = BuildScenario($"{strategyConfig.Name}_{parameterSet.Name}",
|
||
new[] { (strategyConfig, parameterSet) });
|
||
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 = BuildScenario(scenarioName, paramCombo);
|
||
scenario.LoopbackPeriod = 15;
|
||
scenarios.Add(scenario);
|
||
}
|
||
}
|
||
|
||
return scenarios;
|
||
}
|
||
|
||
private Scenario BuildScenario(string scenarioName,
|
||
IEnumerable<(StrategyConfiguration strategyConfig, ParameterSet parameterSet)> strategyParams)
|
||
{
|
||
var scenario = new Scenario(scenarioName);
|
||
|
||
foreach (var (strategyConfig, parameterSet) in strategyParams)
|
||
{
|
||
var strategy = ScenarioHelpers.BuildIndicator(
|
||
strategyConfig.Type,
|
||
$"{strategyConfig.Name}_{parameterSet.Name}",
|
||
period: parameterSet.Period,
|
||
fastPeriods: parameterSet.FastPeriods,
|
||
slowPeriods: parameterSet.SlowPeriods,
|
||
signalPeriods: parameterSet.SignalPeriods,
|
||
multiplier: parameterSet.Multiplier,
|
||
stochPeriods: parameterSet.StochPeriods,
|
||
smoothPeriods: parameterSet.SmoothPeriods,
|
||
cyclePeriods: parameterSet.CyclePeriods);
|
||
|
||
scenario.AddIndicator(strategy);
|
||
}
|
||
|
||
return scenario;
|
||
}
|
||
|
||
private IEnumerable<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; }
|
||
}
|
||
} |