502 lines
22 KiB
C#
502 lines
22 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Managing.Application.Abstractions;
|
|
using Managing.Application.Abstractions.Repositories;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Application.Backtests;
|
|
using Managing.Application.Bots;
|
|
using Managing.Application.Tests;
|
|
using Managing.Core;
|
|
using Managing.Domain.Accounts;
|
|
using Managing.Domain.Backtests;
|
|
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.Workers.Tests;
|
|
|
|
public class BacktestExecutorTests : BaseTests, IDisposable
|
|
{
|
|
private readonly BacktestExecutor _backtestExecutor;
|
|
private readonly Mock<IServiceScopeFactory> _scopeFactory;
|
|
private readonly Mock<IBacktestRepository> _backtestRepository;
|
|
private readonly Mock<IScenarioService> _scenarioService;
|
|
private readonly Mock<IMessengerService> _messengerService;
|
|
private readonly User _testUser;
|
|
private readonly ILoggerFactory _loggerFactory;
|
|
|
|
public BacktestExecutorTests() : base()
|
|
{
|
|
// Setup mock dependencies
|
|
_backtestRepository = new Mock<IBacktestRepository>();
|
|
_scenarioService = new Mock<IScenarioService>();
|
|
_messengerService = new Mock<IMessengerService>();
|
|
|
|
// Setup service scope factory
|
|
_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);
|
|
|
|
mockServiceProvider.Setup(x => x.GetService(typeof(IBotService)))
|
|
.Returns(new Mock<IBotService>().Object);
|
|
|
|
mockServiceProvider.Setup(x => x.GetService(typeof(IMessengerService)))
|
|
.Returns(_messengerService.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 logger factory for telemetry visibility
|
|
_loggerFactory = LoggerFactory.Create(builder =>
|
|
{
|
|
builder.AddConsole();
|
|
builder.SetMinimumLevel(LogLevel.Debug); // Enable debug for troubleshooting
|
|
});
|
|
|
|
// Create BacktestExecutor instance with console logger
|
|
var logger = _loggerFactory.CreateLogger<BacktestExecutor>();
|
|
_backtestExecutor = new BacktestExecutor(
|
|
logger,
|
|
_scopeFactory.Object,
|
|
_backtestRepository.Object,
|
|
_scenarioService.Object,
|
|
_accountService.Object,
|
|
_messengerService.Object);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteBacktest_With_ETH_FifteenMinutes_Data_Should_Return_LightBacktest()
|
|
{
|
|
Console.WriteLine("TEST START: ExecuteBacktest_With_ETH_FifteenMinutes_Data_Should_Return_LightBacktest");
|
|
// 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
|
|
var result = await _backtestExecutor.ExecuteAsync(
|
|
config,
|
|
candles.ToHashSet(),
|
|
_testUser,
|
|
save: false,
|
|
withCandles: false,
|
|
requestId: null,
|
|
bundleRequestId: null,
|
|
metadata: null,
|
|
progressCallback: null);
|
|
|
|
// Output the result to console for review
|
|
var json = JsonConvert.SerializeObject(new
|
|
{
|
|
result.FinalPnl,
|
|
result.WinRate,
|
|
result.GrowthPercentage,
|
|
result.HodlPercentage,
|
|
result.Fees,
|
|
result.NetPnl,
|
|
result.MaxDrawdown,
|
|
result.SharpeRatio,
|
|
result.Score,
|
|
result.InitialBalance,
|
|
StartDate = result.StartDate.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
EndDate = result.EndDate.ToString("yyyy-MM-dd HH:mm:ss")
|
|
}, Formatting.Indented);
|
|
|
|
Console.WriteLine("BacktestExecutor Results:");
|
|
Console.WriteLine(json);
|
|
|
|
// Debug: Verify telemetry is working
|
|
Console.WriteLine($"DEBUG: Test completed successfully with {result.WinRate}% win rate");
|
|
|
|
// Assert - Validate specific backtest results
|
|
Assert.NotNull(result);
|
|
Assert.IsType<LightBacktest>(result);
|
|
|
|
// Validate key metrics - Updated after bug fix in executor
|
|
Assert.Equal(1000.0m, result.InitialBalance);
|
|
Assert.Equal(44.343999999999999999999999991m, result.FinalPnl);
|
|
Assert.Equal(31, result.WinRate);
|
|
Assert.Equal(4.43440000000000000000000m, result.GrowthPercentage);
|
|
Assert.Equal(-0.67091284426766023865867781m, result.HodlPercentage);
|
|
Assert.Equal(86.64864600000000000000000000m, result.Fees);
|
|
Assert.Equal(-42.304646000000000000000000009m, result.NetPnl);
|
|
Assert.Equal(119.8400000000000000000000000m, result.MaxDrawdown);
|
|
Assert.True(Math.Abs((double)(result.SharpeRatio ?? 0) - 0.01080949889674031) < 0.01,
|
|
$"SharpeRatio mismatch: expected ~0.01080949889674031, got {result.SharpeRatio}");
|
|
Assert.True(Math.Abs(result.Score - 12.402462484050353) < 0.001,
|
|
$"Score {result.Score} should be within 0.001 of expected value 12.402462484050353");
|
|
|
|
// Validate dates
|
|
Assert.Equal(new DateTime(2025, 10, 14, 12, 0, 0), result.StartDate);
|
|
Assert.Equal(new DateTime(2025, 10, 24, 11, 45, 0), result.EndDate);
|
|
Assert.True(result.StartDate < result.EndDate);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task LongBacktest_ETH_RSI()
|
|
{
|
|
// Arrange
|
|
var candles =
|
|
FileHelpers.ReadJson<List<Candle>>("../../../Data/ETH-FifteenMinutes-candles-20:44:15 +00:00-.json");
|
|
Assert.NotNull(candles);
|
|
Assert.NotEmpty(candles);
|
|
|
|
// Use more candles for performance testing (first 5000 candles)
|
|
candles = candles.Take(5000).ToList();
|
|
Console.WriteLine($"DEBUG: Loaded {candles.Count} candles for backtest");
|
|
|
|
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 = 100000, // Increased balance for testing more candles
|
|
IsForBacktest = true,
|
|
CooldownPeriod = 1,
|
|
MaxLossStreak = 0,
|
|
FlipPosition = false,
|
|
Name = "ETH_FifteenMinutes_Test_Second",
|
|
FlipOnlyWhenInProfit = true,
|
|
MaxPositionTimeHours = null,
|
|
CloseEarlyWhenProfitable = false
|
|
};
|
|
|
|
// Act
|
|
var result = await _backtestExecutor.ExecuteAsync(
|
|
config,
|
|
candles.ToHashSet(),
|
|
_testUser,
|
|
save: false,
|
|
withCandles: false,
|
|
requestId: null,
|
|
bundleRequestId: null,
|
|
metadata: null,
|
|
progressCallback: null);
|
|
|
|
// Output the result to console for review
|
|
var json = JsonConvert.SerializeObject(new
|
|
{
|
|
result.FinalPnl,
|
|
result.WinRate,
|
|
result.GrowthPercentage,
|
|
result.HodlPercentage,
|
|
result.Fees,
|
|
result.NetPnl,
|
|
result.MaxDrawdown,
|
|
result.SharpeRatio,
|
|
result.Score,
|
|
result.InitialBalance,
|
|
StartDate = result.StartDate.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
EndDate = result.EndDate.ToString("yyyy-MM-dd HH:mm:ss")
|
|
}, Formatting.Indented);
|
|
|
|
Console.WriteLine("BacktestExecutor Results (Second File):");
|
|
Console.WriteLine(json);
|
|
|
|
// Assert - Validate specific backtest results
|
|
Assert.NotNull(result);
|
|
Assert.IsType<LightBacktest>(result);
|
|
|
|
// Validate key metrics - Updated after bug fix in executor
|
|
Assert.Equal(100000.0m, result.InitialBalance);
|
|
Assert.Equal(-31899.032000000000000000000000m, result.FinalPnl);
|
|
Assert.Equal(21, result.WinRate);
|
|
Assert.Equal(-31.8990320000000000000000m, result.GrowthPercentage);
|
|
Assert.Equal(-12.86812721679866545042180006m, result.HodlPercentage);
|
|
Assert.Equal(25875.444102000000000000000000m, result.Fees);
|
|
Assert.Equal(-57774.476102000000000000000000m, result.NetPnl);
|
|
Assert.Equal(37030.256000000000000000000000m, result.MaxDrawdown);
|
|
Assert.True(Math.Abs((double)(result.SharpeRatio ?? 0) - (-0.024119163190349627)) < 0.01,
|
|
$"SharpeRatio mismatch: expected ~-0.024119163190349627, got {result.SharpeRatio}"); // Use tolerance for floating point precision
|
|
Assert.Equal((double)0.0m, result.Score);
|
|
|
|
// Validate dates - Updated to match actual results from ETH-FifteenMinutes-candles-20:44:15 +00:00-.json
|
|
Assert.Equal(new DateTime(2025, 9, 11, 20, 45, 0), result.StartDate);
|
|
Assert.Equal(new DateTime(2025, 11, 2, 22, 30, 0), result.EndDate);
|
|
Assert.True(result.StartDate < result.EndDate);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Telemetry_ETH_RSI()
|
|
{
|
|
// Arrange - Use the large dataset for performance testing
|
|
var candles =
|
|
FileHelpers.ReadJson<List<Candle>>("../../../Data/ETH-FifteenMinutes-candles-20:44:15 +00:00-.json");
|
|
Assert.NotNull(candles);
|
|
Assert.NotEmpty(candles);
|
|
|
|
Console.WriteLine($"DEBUG: Loaded {candles.Count} candles for performance telemetry test");
|
|
|
|
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 = 100000,
|
|
IsForBacktest = true,
|
|
CooldownPeriod = 1,
|
|
MaxLossStreak = 0,
|
|
FlipPosition = false,
|
|
Name = "ETH_FifteenMinutes_Performance_Test",
|
|
FlipOnlyWhenInProfit = true,
|
|
MaxPositionTimeHours = null,
|
|
CloseEarlyWhenProfitable = false
|
|
};
|
|
|
|
// Track execution time
|
|
var startTime = DateTime.UtcNow;
|
|
|
|
// Act
|
|
var result = await _backtestExecutor.ExecuteAsync(
|
|
config,
|
|
candles.ToHashSet(),
|
|
_testUser,
|
|
save: false,
|
|
withCandles: false,
|
|
requestId: null,
|
|
bundleRequestId: null,
|
|
metadata: null,
|
|
progressCallback: null);
|
|
|
|
var endTime = DateTime.UtcNow;
|
|
var totalExecutionTime = (endTime - startTime).TotalSeconds;
|
|
|
|
// Output performance metrics
|
|
Console.WriteLine("═══════════════════════════════════════════════════════════");
|
|
Console.WriteLine("📊 PERFORMANCE TELEMETRY TEST RESULTS");
|
|
Console.WriteLine("═══════════════════════════════════════════════════════════");
|
|
Console.WriteLine($"📈 Total Candles Processed: {candles.Count:N0}");
|
|
Console.WriteLine($"⏱️ Total Execution Time: {totalExecutionTime:F2}s");
|
|
Console.WriteLine($"🚀 Processing Rate: {candles.Count / totalExecutionTime:F1} candles/sec");
|
|
Console.WriteLine($"💾 Memory per 1000 candles: ~{(16.80 - 12.06) / (candles.Count / 1000.0):F2}MB");
|
|
Console.WriteLine();
|
|
Console.WriteLine("📋 Backtest Results Summary:");
|
|
Console.WriteLine($" • Final PnL: {result.FinalPnl:F2}");
|
|
Console.WriteLine($" • Win Rate: {result.WinRate}%");
|
|
Console.WriteLine($" • Growth: {result.GrowthPercentage:F2}%");
|
|
Console.WriteLine($" • Fees: {result.Fees:F2}");
|
|
Console.WriteLine($" • Net PnL: {result.NetPnl:F2}");
|
|
Console.WriteLine($" • Max Drawdown: {result.MaxDrawdown:F2}");
|
|
Console.WriteLine($" • Sharpe Ratio: {result.SharpeRatio:F4}");
|
|
Console.WriteLine($" • Score: {result.Score:F2}");
|
|
Console.WriteLine($" • Start Date: {result.StartDate:yyyy-MM-dd HH:mm:ss}");
|
|
Console.WriteLine($" • End Date: {result.EndDate:yyyy-MM-dd HH:mm:ss}");
|
|
Console.WriteLine("═══════════════════════════════════════════════════════════");
|
|
|
|
// Assert - Validate basic results
|
|
Assert.NotNull(result);
|
|
Assert.IsType<LightBacktest>(result);
|
|
Assert.True(result.StartDate < result.EndDate);
|
|
Assert.True(totalExecutionTime > 0);
|
|
|
|
// Performance assertions - ensure we're processing at a reasonable rate
|
|
var candlesPerSecond = candles.Count / totalExecutionTime;
|
|
Assert.True(candlesPerSecond > 500, $"Expected >500 candles/sec, got {candlesPerSecond:F1} candles/sec");
|
|
|
|
Console.WriteLine($"✅ Performance test passed: {candlesPerSecond:F1} candles/sec");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Telemetry_ETH_RSI_EMACROSS()
|
|
{
|
|
// Arrange - Test with 2 indicators to verify pre-calculated signals optimization works with multiple scenarios
|
|
var candles =
|
|
FileHelpers.ReadJson<List<Candle>>("../../../Data/ETH-FifteenMinutes-candles-20:44:15 +00:00-.json");
|
|
Assert.NotNull(candles);
|
|
Assert.NotEmpty(candles);
|
|
|
|
Console.WriteLine($"DEBUG: Loaded {candles.Count} candles for two-scenarios performance telemetry test");
|
|
|
|
var scenario = new Scenario("ETH_TwoScenarios_Backtest");
|
|
var rsiDivIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.RsiDivergence, "RsiDiv", period: 14);
|
|
var emaCrossIndicator = ScenarioHelpers.BuildIndicator(IndicatorType.EmaCross, "EmaCross", period: 21);
|
|
scenario.Indicators = new List<IndicatorBase> { (IndicatorBase)rsiDivIndicator, (IndicatorBase)emaCrossIndicator };
|
|
scenario.LoopbackPeriod = 15; // 15 minutes loopback period as requested
|
|
|
|
var config = new TradingBotConfig
|
|
{
|
|
AccountName = _account.Name,
|
|
MoneyManagement = MoneyManagement,
|
|
Ticker = Ticker.ETH,
|
|
Scenario = LightScenario.FromScenario(scenario),
|
|
Timeframe = Timeframe.FifteenMinutes,
|
|
IsForWatchingOnly = false,
|
|
BotTradingBalance = 100000,
|
|
IsForBacktest = true,
|
|
CooldownPeriod = 1,
|
|
MaxLossStreak = 0,
|
|
FlipPosition = false,
|
|
Name = "ETH_TwoScenarios_Performance_Test",
|
|
FlipOnlyWhenInProfit = true,
|
|
MaxPositionTimeHours = null,
|
|
CloseEarlyWhenProfitable = false
|
|
};
|
|
|
|
// Track execution time
|
|
var startTime = DateTime.UtcNow;
|
|
|
|
// Act
|
|
var result = await _backtestExecutor.ExecuteAsync(
|
|
config,
|
|
candles.ToHashSet(),
|
|
_testUser,
|
|
save: false,
|
|
withCandles: false,
|
|
requestId: null,
|
|
bundleRequestId: null,
|
|
metadata: null,
|
|
progressCallback: null);
|
|
|
|
var executionTime = DateTime.UtcNow - startTime;
|
|
|
|
// Assert - Verify the result is valid
|
|
Assert.NotNull(result);
|
|
Assert.Equal(Ticker.ETH, result.Config.Ticker);
|
|
Assert.Equal(100000, result.InitialBalance);
|
|
Assert.True(result.Score >= 0); // Score can be 0 or positive
|
|
|
|
// Output the result to console for review
|
|
var json = JsonConvert.SerializeObject(new
|
|
{
|
|
result.FinalPnl,
|
|
result.WinRate,
|
|
result.GrowthPercentage,
|
|
result.HodlPercentage,
|
|
result.Fees,
|
|
result.NetPnl,
|
|
result.MaxDrawdown,
|
|
result.SharpeRatio,
|
|
result.Score,
|
|
result.InitialBalance,
|
|
StartDate = result.StartDate.ToString("yyyy-MM-dd HH:mm:ss"),
|
|
EndDate = result.EndDate.ToString("yyyy-MM-dd HH:mm:ss")
|
|
}, Formatting.Indented);
|
|
|
|
Console.WriteLine("Two-Scenarios Backtest Results:");
|
|
Console.WriteLine(json);
|
|
|
|
// Business Logic Baseline Assertions - Updated after bug fix in executor
|
|
// These values establish the expected baseline for the two-scenarios test
|
|
const decimal expectedFinalPnl = -34137.424000000000000000000000m;
|
|
const double expectedScore = 0.0;
|
|
const int expectedWinRatePercent = 20; // 20% win rate
|
|
const decimal expectedGrowthPercentage = -34.1374240000000000000000m;
|
|
|
|
// Allow small tolerance for floating-point precision variations
|
|
const decimal pnlTolerance = 0.01m;
|
|
const double scoreTolerance = 0.01;
|
|
const decimal growthTolerance = 0.01m;
|
|
|
|
Assert.True(Math.Abs(result.FinalPnl - expectedFinalPnl) <= pnlTolerance,
|
|
$"Final PnL {result.FinalPnl:F2} differs from expected baseline {expectedFinalPnl:F2} (tolerance: ±{pnlTolerance:F2})");
|
|
|
|
Assert.True(Math.Abs(result.Score - expectedScore) <= scoreTolerance,
|
|
$"Score {result.Score:F2} differs from expected baseline {expectedScore:F2} (tolerance: ±{scoreTolerance:F2})");
|
|
|
|
Assert.True(Math.Abs(result.WinRate - expectedWinRatePercent) <= 5,
|
|
$"Win Rate {result.WinRate}% differs from expected baseline {expectedWinRatePercent}% (tolerance: ±5%)");
|
|
|
|
Assert.True(Math.Abs(result.GrowthPercentage - expectedGrowthPercentage) <= growthTolerance,
|
|
$"Growth {result.GrowthPercentage:F2}% differs from expected baseline {expectedGrowthPercentage:F2}% (tolerance: ±{growthTolerance:F2}%)");
|
|
|
|
// Performance metrics
|
|
var totalCandles = candles.Count;
|
|
var candlesPerSecond = totalCandles / executionTime.TotalSeconds;
|
|
|
|
// Log comprehensive performance metrics
|
|
Console.WriteLine($"📊 === TWO-SCENARIOS PERFORMANCE TELEMETRY ===");
|
|
Console.WriteLine($"⏱️ Total Execution Time: {executionTime.TotalSeconds:F2}s");
|
|
Console.WriteLine($"📈 Candles Processed: {totalCandles} ({candlesPerSecond:F1} candles/sec)");
|
|
Console.WriteLine($"🎯 Final PnL: {result.FinalPnl:F2} (Expected: {expectedFinalPnl:F2})");
|
|
Console.WriteLine($"📊 Score: {result.Score:F2} (Expected: {expectedScore:F2})");
|
|
Console.WriteLine($"📈 Win Rate: {result.WinRate}% (Expected: {expectedWinRatePercent}%)");
|
|
Console.WriteLine($"📈 Growth: {result.GrowthPercentage:F2}% (Expected: {expectedGrowthPercentage:F2}%)");
|
|
Console.WriteLine($"🎭 Scenario: {scenario.Name} ({scenario.Indicators.Count} indicators, LoopbackPeriod: {scenario.LoopbackPeriod})");
|
|
|
|
// Performance assertion - should be reasonably fast even with 2 indicators
|
|
Assert.True(candlesPerSecond > 200, $"Expected >200 candles/sec with 2 indicators, got {candlesPerSecond:F1} candles/sec");
|
|
|
|
Console.WriteLine($"✅ Two-scenarios performance test passed: {candlesPerSecond:F1} candles/sec with {scenario.Indicators.Count} indicators");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_loggerFactory?.Dispose();
|
|
}
|
|
} |