Files
managing-apps/src/Managing.Application.Tests/BotsTests.cs
2025-04-30 13:55:40 +07:00

449 lines
20 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.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 = "D:\\BacktestingReports\\backtesting.csv";
private string _analysePath = "D:\\BacktestingReports\\analyse";
private readonly string _errorsPath = "D:\\BacktestingReports\\errorsAnalyse.csv";
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 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);
_elapsedTimes = new List<double>();
}
[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.BuildStrategy(StrategyType.RsiDivergence, "RsiDiv", period: 14);
scenario.AddStrategy(strategy);
var localCandles =
FileHelpers.ReadJson<List<Candle>>($"{ticker.ToString()}-{timeframe.ToString()}-candles.json");
// Act
var backtestResult = await _backtester.RunFlippingBotBacktest(_account, MoneyManagement, ticker, scenario,
timeframe, 1000, new DateTime().AddDays(-3), DateTime.UtcNow,
initialCandles: localCandles.TakeLast(500).ToList());
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.BuildStrategy(StrategyType.RsiDivergence, "RsiDiv", period: 5);
scenario.AddStrategy(strategy);
// Act
var backtestResult = await _backtester.RunScalpingBotBacktest(_account, MoneyManagement, ticker, scenario,
timeframe, 1000, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null);
//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.BuildStrategy(StrategyType.MacdCross, "RsiDiv", fastPeriods: 12,
slowPeriods: 26, signalPeriods: 9);
scenario.AddStrategy(strategy);
var moneyManagement = new MoneyManagement()
{
Leverage = 1,
Timeframe = timeframe,
StopLoss = 0.01m,
TakeProfit = 0.02m
};
// Act
var backtestResult = await _backtester.RunScalpingBotBacktest(_account, moneyManagement, ticker, scenario,
timeframe, 1000, DateTime.UtcNow.AddDays(-6), DateTime.UtcNow, null);
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, StrategyType.Stc, BotType.ScalpingBot)]
//[InlineData(Timeframe.FifteenMinutes, -6, Enums.StrategyType.RsiDivergenceConfirm, Enums.BotType.FlippingBot)]
public void GetBestPeriodRsiForDivergenceFlippingBot(Timeframe timeframe, int days, StrategyType strategyType,
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 = $"{strategyType}-{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.BuildStrategy(strategyType, "RsiDiv", period: i);
scenario.AddStrategy(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.RunScalpingBotBacktest(_account, moneyManagement,
scenario, timeframe, candles, 1000, null).Result,
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(_account, moneyManagement,
scenario, timeframe, candles, 1000, null).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, StrategyType.MacdCross, BotType.FlippingBot)]
[InlineData(Timeframe.OneHour, -30, StrategyType.MacdCross, BotType.ScalpingBot)]
public void GetBestMMForMacdFlippingBot(Timeframe timeframe, int days, StrategyType strategyType,
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 = $"{strategyType}-{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.BuildStrategy(strategyType, "RsiDiv", fastPeriods: 12,
slowPeriods: 26, signalPeriods: 9);
scenario.AddStrategy(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.RunScalpingBotBacktest(_account, moneyManagement,
scenario, timeframe, candles, 1000, null).Result,
BotType.FlippingBot => _backtester.RunFlippingBotBacktest(_account, moneyManagement,
scenario, timeframe, candles, 1000, 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);
}
}
}
}
}