Add test to check if backtest behavior changed

This commit is contained in:
2025-10-24 19:08:10 +07:00
parent fc4369a008
commit 38e6998ff3
12 changed files with 11797 additions and 38 deletions

View File

@@ -0,0 +1,238 @@
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<IServiceScopeFactory> _scopeFactory;
private readonly User _testUser;
public BacktestTests() : base()
{
// Setup mock dependencies for BacktestTradingBotGrain
var backtestRepository = new Mock<IBacktestRepository>().Object;
var grainLogger = new Mock<ILogger<BacktestTradingBotGrain>>().Object;
// Setup service scope factory to return ALL TradingBotBase dependencies
_scopeFactory = new Mock<IServiceScopeFactory>();
var mockScope = new Mock<IServiceScope>();
var mockServiceProvider = new Mock<IServiceProvider>();
// Setup TradingBotBase logger
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
mockServiceProvider.Setup(x => x.GetService(typeof(ILogger<TradingBotBase>)))
.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<IBotService>();
mockServiceProvider.Setup(x => x.GetService(typeof(IBotService)))
.Returns(mockBotService.Object);
var mockMessengerService = new Mock<IMessengerService>();
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> { _account }
};
// Create BacktestTradingBotGrain instance directly
_backtestGrain = new BacktestTradingBotGrain(grainLogger, _scopeFactory.Object, backtestRepository);
}
/// <summary>
/// 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.
/// </summary>
[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<List<Candle>>($"Data/{fileName}");
Assert.NotNull(readBack);
Assert.Equal(candleList.Count, readBack.Count);
}
/// <summary>
/// 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.
/// </summary>
[Fact]
public async Task ETH_FifteenMinutes_Backtest_Should_Return_Consistent_Results()
{
// Arrange
var candles = FileHelpers.ReadJson<List<Candle>>("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> { (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(-105.45m, Math.Round(backtestResult.FinalPnl, 2));
Assert.Equal(-210.40m, Math.Round(backtestResult.NetPnl, 2));
Assert.Equal(104.94m, Math.Round(backtestResult.Fees, 2));
Assert.Equal(1000.0m, backtestResult.InitialBalance);
// Performance metrics
Assert.Equal(31, backtestResult.WinRate);
Assert.Equal(-10.55m, Math.Round(backtestResult.GrowthPercentage, 2));
Assert.Equal(-0.67m, Math.Round(backtestResult.HodlPercentage, 2));
// Risk metrics
Assert.Equal(243.84m, 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");
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,15 +8,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.TestPlatform.AdapterUtilities" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.7"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"/>
<PackageReference Include="Microsoft.TestPlatform.AdapterUtilities" Version="17.9.0"/>
<PackageReference Include="Moq" Version="4.20.70"/>
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1"/>
<PackageReference Include="MSTest.TestFramework" Version="3.3.1"/>
<PackageReference Include="xunit" Version="2.8.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -24,23 +24,26 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Managing.Api\Managing.Api.csproj" />
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj" />
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj" />
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Exchanges\Managing.Infrastructure.Exchanges.csproj" />
<ProjectReference Include="..\Managing.Infrastructure.Tests\Managing.Infrastructure.Tests.csproj" />
<ProjectReference Include="..\Managing.Api\Managing.Api.csproj"/>
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj"/>
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Exchanges\Managing.Infrastructure.Exchanges.csproj"/>
<ProjectReference Include="..\Managing.Infrastructure.Tests\Managing.Infrastructure.Tests.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Data\" />
<Folder Include="Data\"/>
</ItemGroup>
<ItemGroup>
<None Update="Data\candles.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Data\ETH-FifteenMinutes-candles.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>