Add test to check if backtest behavior changed
This commit is contained in:
238
src/Managing.Application.Tests/BacktestTests.cs
Normal file
238
src/Managing.Application.Tests/BacktestTests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user