feat(backtest): strategy comparison (head-to-head)
New /anomalies/compare page runs every saved strategy preset over the same window and ranks them by ROI — bets, W–L, hit-rate, net, and max drawdown side by side, with the best ROI flagged. Auto-runs on load with an optional date-range refine. - CompareStrategiesUseCase fans RunBacktestUseCase over saved presets (re-loads the anomaly set per preset — fine for the handful a user keeps; stays bug-for-bug identical to a single backtest run). - StrategyComparisonService.BuildVm (pure) computes per-row hit-rate + a single best-by-ROI flag; nav entry + en/ru resx. - 6 tests: use-case fan-out + BuildVm best/tie/no-bets/hit-rate.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
using FluentAssertions;
|
||||
using Marathon.Application.Abstractions;
|
||||
using Marathon.Application.UseCases;
|
||||
using Marathon.Domain.Backtesting;
|
||||
using Marathon.Domain.Entities;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Marathon.Application.Tests.UseCases;
|
||||
|
||||
public sealed class CompareStrategiesUseCaseTests
|
||||
{
|
||||
private readonly ISavedStrategyRepository _strategies = Substitute.For<ISavedStrategyRepository>();
|
||||
private readonly IAnomalyRepository _anomalies = Substitute.For<IAnomalyRepository>();
|
||||
private readonly IEventRepository _events = Substitute.For<IEventRepository>();
|
||||
private readonly IResultRepository _results = Substitute.For<IResultRepository>();
|
||||
|
||||
private CompareStrategiesUseCase CreateSut()
|
||||
{
|
||||
var backtest = new RunBacktestUseCase(_anomalies, _events, _results, NullLogger<RunBacktestUseCase>.Instance);
|
||||
return new CompareStrategiesUseCase(_strategies, backtest, NullLogger<CompareStrategiesUseCase>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Empty_When_NoPresets()
|
||||
{
|
||||
_strategies.ListAsync(Arg.Any<CancellationToken>()).Returns(Array.Empty<SavedStrategy>());
|
||||
|
||||
(await CreateSut().ExecuteAsync(null)).Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunsEachPreset_PreservingNameAndOrder()
|
||||
{
|
||||
_strategies.ListAsync(Arg.Any<CancellationToken>()).Returns(new[]
|
||||
{
|
||||
SavedStrategy.Create("Alpha", BacktestStrategy.Default),
|
||||
SavedStrategy.Create("Beta", BacktestStrategy.Default),
|
||||
});
|
||||
// No anomalies → each backtest returns a 0-bet result; events/results are never queried.
|
||||
_anomalies.ListAsync(Arg.Any<CancellationToken>()).Returns(Array.Empty<Anomaly>());
|
||||
|
||||
var rows = await CreateSut().ExecuteAsync(null);
|
||||
|
||||
rows.Select(r => r.Name).Should().ContainInOrder("Alpha", "Beta");
|
||||
rows.Should().OnlyContain(r => r.Result.BetsPlaced == 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user