6e12dd73c3
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.
90 lines
2.9 KiB
C#
90 lines
2.9 KiB
C#
using FluentAssertions;
|
|
using Marathon.Application.UseCases;
|
|
using Marathon.Domain.Backtesting;
|
|
using Marathon.Domain.ValueObjects;
|
|
using Marathon.UI.Services;
|
|
|
|
namespace Marathon.UI.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// Tests the pure <see cref="StrategyComparisonService.BuildVm"/> projection — per-row hit
|
|
/// rate and the single best-by-ROI flag — without the (sealed) use case.
|
|
/// </summary>
|
|
public sealed class StrategyComparisonServiceTests
|
|
{
|
|
private static BacktestResult Result(int bets, int wins, int losses, decimal net, decimal? roi, decimal maxDd = 0m) =>
|
|
new(
|
|
StartingBankroll: 1000m,
|
|
FinalBankroll: 1000m + net,
|
|
NetProfit: net,
|
|
RoiPercent: roi,
|
|
TotalStaked: 0m,
|
|
TotalReturned: 0m,
|
|
MaxDrawdown: maxDd,
|
|
MaxDrawdownPercent: null,
|
|
BetsPlaced: bets,
|
|
Wins: wins,
|
|
Losses: losses,
|
|
Skipped: 0,
|
|
SkippedByThreshold: 0,
|
|
SkippedByDataQuality: 0,
|
|
SkippedByBankroll: 0,
|
|
MaxWinStreak: 0,
|
|
MaxLossStreak: 0,
|
|
Trace: Array.Empty<BacktestTrace>(),
|
|
EventTitles: new Dictionary<EventId, string>());
|
|
|
|
private static StrategyComparison Comp(string name, BacktestResult result) =>
|
|
new(Guid.NewGuid(), name, result);
|
|
|
|
[Fact]
|
|
public void BuildVm_Empty_When_NoComparisons()
|
|
{
|
|
StrategyComparisonService.BuildVm(Array.Empty<StrategyComparison>())
|
|
.Should().Be(StrategyComparisonVm.Empty);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildVm_MarksHighestRoiBest_AndComputesHitRate()
|
|
{
|
|
var comps = new[]
|
|
{
|
|
Comp("Flat", Result(bets: 10, wins: 6, losses: 4, net: 20m, roi: 5m)),
|
|
Comp("Kelly", Result(bets: 10, wins: 7, losses: 3, net: 50m, roi: 12m)),
|
|
Comp("Percent", Result(bets: 10, wins: 5, losses: 5, net: -10m, roi: -3m)),
|
|
};
|
|
|
|
var vm = StrategyComparisonService.BuildVm(comps);
|
|
|
|
vm.Rows.Should().HaveCount(3);
|
|
vm.Rows.Count(r => r.IsBest).Should().Be(1);
|
|
vm.Rows.Single(r => r.IsBest).Name.Should().Be("Kelly");
|
|
vm.Rows.Single(r => r.Name == "Flat").HitRatePercent.Should().Be(60m); // 6/10
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildVm_NoBetsPreset_NotBest_AndNullHitRate()
|
|
{
|
|
var vm = StrategyComparisonService.BuildVm(new[]
|
|
{
|
|
Comp("Idle", Result(bets: 0, wins: 0, losses: 0, net: 0m, roi: null)),
|
|
});
|
|
|
|
vm.Rows.Should().ContainSingle();
|
|
vm.Rows[0].IsBest.Should().BeFalse();
|
|
vm.Rows[0].HitRatePercent.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildVm_Ties_ProduceSingleBest()
|
|
{
|
|
var comps = new[]
|
|
{
|
|
Comp("A", Result(10, 6, 4, 20m, 5m)),
|
|
Comp("B", Result(10, 6, 4, 20m, 5m)),
|
|
};
|
|
|
|
StrategyComparisonService.BuildVm(comps).Rows.Count(r => r.IsBest).Should().Be(1);
|
|
}
|
|
}
|