using Managing.Application.Abstractions; using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Bots; using Managing.Application.Bots.Grains; using Managing.Core; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Scenarios; using Managing.Domain.Strategies; using Managing.Domain.Users; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using Newtonsoft.Json; using Xunit; using static Managing.Common.Enums; namespace Managing.Application.Tests; public class BacktestTests : BaseTests { private readonly BacktestTradingBotGrain _backtestGrain; private readonly Mock _scopeFactory; private readonly User _testUser; public BacktestTests() : base() { // Setup mock dependencies for BacktestTradingBotGrain var backtestRepository = new Mock().Object; var grainLogger = new Mock>().Object; // Setup service scope factory to return ALL TradingBotBase dependencies _scopeFactory = new Mock(); var mockScope = new Mock(); var mockServiceProvider = new Mock(); // Setup TradingBotBase logger var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger(); mockServiceProvider.Setup(x => x.GetService(typeof(ILogger))) .Returns(tradingBotLogger); // Setup all services that TradingBotBase might need mockServiceProvider.Setup(x => x.GetService(typeof(IExchangeService))) .Returns(_exchangeService); mockServiceProvider.Setup(x => x.GetService(typeof(IAccountService))) .Returns(_accountService.Object); mockServiceProvider.Setup(x => x.GetService(typeof(ITradingService))) .Returns(_tradingService.Object); mockServiceProvider.Setup(x => x.GetService(typeof(IMoneyManagementService))) .Returns(_moneyManagementService.Object); // Setup additional services that might be needed var mockBotService = new Mock(); mockServiceProvider.Setup(x => x.GetService(typeof(IBotService))) .Returns(mockBotService.Object); var mockMessengerService = new Mock(); mockServiceProvider.Setup(x => x.GetService(typeof(IMessengerService))) .Returns(mockMessengerService.Object); mockScope.Setup(x => x.ServiceProvider).Returns(mockServiceProvider.Object); _scopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object); // Create test user with account _testUser = new User { Id = 1, Name = "Test User", Accounts = new List { _account } }; // Create BacktestTradingBotGrain instance directly _backtestGrain = new BacktestTradingBotGrain(grainLogger, _scopeFactory.Object, backtestRepository); } /// /// Helper test to fetch candles from ExchangeService and save them to Data folder. /// This test is useful for generating or updating test data files. /// Skip this test in normal test runs by commenting out the [Fact] attribute. /// [Fact(Skip = "Run manually to generate test data")] public async Task GenerateTestData_FetchAndSaveETHCandles() { // Arrange var ticker = Ticker.ETH; var timeframe = Timeframe.FifteenMinutes; var daysBack = -10; // Fetch last 30 days of data var startDate = DateTime.UtcNow.AddDays(daysBack); var endDate = DateTime.UtcNow; // Act - Fetch candles from exchange var candles = await _exchangeService.GetCandles(_account, ticker, startDate, timeframe); Assert.NotNull(candles); Assert.NotEmpty(candles); // Convert to list and sort by date var candleList = candles.OrderBy(c => c.Date).ToList(); // Serialize to JSON var json = JsonConvert.SerializeObject(candleList, Formatting.Indented); // Determine the output path - save to source Data folder, not bin folder // Navigate from bin folder to source: bin/Debug/net8.0 -> ../../.. var assemblyLocation = typeof(BacktestTests).Assembly.Location; var binDirectory = Path.GetDirectoryName(assemblyLocation); var projectDirectory = Path.GetFullPath(Path.Combine(binDirectory, "..", "..", "..")); var dataDirectory = Path.Combine(projectDirectory, "Data"); // Ensure Data directory exists if (!Directory.Exists(dataDirectory)) { Directory.CreateDirectory(dataDirectory); } var fileName = $"{ticker}-{timeframe}-candles.json"; var filePath = Path.Combine(dataDirectory, fileName); // Save to file File.WriteAllText(filePath, json); // Output information Console.WriteLine($"✅ Successfully saved {candleList.Count} candles"); Console.WriteLine($"📁 File Location: {filePath}"); Console.WriteLine($"📂 Data Directory: {dataDirectory}"); Console.WriteLine( $"📊 Date Range: {candleList.First().Date:yyyy-MM-dd HH:mm:ss} to {candleList.Last().Date:yyyy-MM-dd HH:mm:ss}"); Console.WriteLine($"📈 Price Range: ${candleList.Min(c => c.Low):F2} - ${candleList.Max(c => c.High):F2}"); // Assert Assert.True(File.Exists(filePath), $"File should exist at {filePath}"); // Verify we can read it back var readBack = FileHelpers.ReadJson>($"Data/{fileName}"); Assert.NotNull(readBack); Assert.Equal(candleList.Count, readBack.Count); } /// /// Main backtest test that verifies consistent behavior of TradingBotBase. /// Note: Run GenerateTestData_FetchAndSaveETHCandles first if the data file doesn't exist. /// After the first run, update the assertions with the actual expected values to ensure /// future changes to TradingBotBase don't break the backtest behavior. /// [Fact] public async Task ETH_FifteenMinutes_Backtest_Should_Return_Consistent_Results() { // Arrange var candles = FileHelpers.ReadJson>("Data/ETH-FifteenMinutes-candles.json"); Assert.NotNull(candles); Assert.NotEmpty(candles); var scenario = new Scenario("ETH_BacktestScenario"); var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14); scenario.Indicators = new List { (IndicatorBase)rsiDivIndicator }; scenario.LoopbackPeriod = 15; var config = new TradingBotConfig { AccountName = _account.Name, MoneyManagement = MoneyManagement, Ticker = Ticker.ETH, Scenario = LightScenario.FromScenario(scenario), Timeframe = Timeframe.FifteenMinutes, IsForWatchingOnly = false, BotTradingBalance = 1000, IsForBacktest = true, CooldownPeriod = 1, MaxLossStreak = 0, FlipPosition = false, Name = "ETH_FifteenMinutes_Test", FlipOnlyWhenInProfit = true, MaxPositionTimeHours = null, CloseEarlyWhenProfitable = false }; // Act - Call BacktestTradingBotGrain directly (no Orleans needed) var backtestResult = await _backtestGrain.RunBacktestAsync( config, candles.ToHashSet(), _testUser, save: false, withCandles: false); // Output the result to console for review var json = JsonConvert.SerializeObject(new { backtestResult.FinalPnl, backtestResult.WinRate, backtestResult.GrowthPercentage, backtestResult.HodlPercentage, backtestResult.Fees, backtestResult.NetPnl, backtestResult.MaxDrawdown, backtestResult.SharpeRatio, backtestResult.Score, backtestResult.InitialBalance, StartDate = backtestResult.StartDate.ToString("yyyy-MM-dd HH:mm:ss"), EndDate = backtestResult.EndDate.ToString("yyyy-MM-dd HH:mm:ss") }, Formatting.Indented); Console.WriteLine("Backtest Results:"); Console.WriteLine(json); // Assert - Verify consistent backtest behavior // These values ensure that changes to TradingBotBase don't break the backtest logic Assert.NotNull(backtestResult); // Financial metrics - using decimal precision Assert.Equal(-106.56m, Math.Round(backtestResult.FinalPnl, 2)); Assert.Equal(-187.36m, Math.Round(backtestResult.NetPnl, 2)); Assert.Equal(80.80m, Math.Round(backtestResult.Fees, 2)); Assert.Equal(1000.0m, backtestResult.InitialBalance); // Performance metrics Assert.Equal(31, backtestResult.WinRate); Assert.Equal(-10.66m, Math.Round(backtestResult.GrowthPercentage, 2)); Assert.Equal(-0.67m, Math.Round(backtestResult.HodlPercentage, 2)); // Risk metrics Assert.Equal(247.62m, Math.Round(backtestResult.MaxDrawdown.Value, 2)); Assert.Equal(-0.021, Math.Round(backtestResult.SharpeRatio.Value, 3)); Assert.Equal(0.0, backtestResult.Score); // Date range validation Assert.Equal(new DateTime(2025, 10, 14, 12, 0, 0), backtestResult.StartDate); Assert.Equal(new DateTime(2025, 10, 24, 11, 45, 0), backtestResult.EndDate); // Additional validation - strategy didn't outperform HODL Assert.True(backtestResult.GrowthPercentage < backtestResult.HodlPercentage, "Strategy underperformed HODL as expected for this test scenario"); } }