feat(backtest): optional date-range window

- RunBacktestUseCase gains an ExecuteAsync(strategy, DateRange?, ct) overload that
  pushes the date filter to SQL via IAnomalyRepository.ListByDateRangeAsync; the
  existing no-range overload is preserved. +1 use-case test.
- BacktestForm carries optional From/To (Moscow dates) with From<=To validation and
  a ToDateRange() helper; BacktestService threads it through. Backtest page gains two
  clearable date pickers (empty = all anomalies).
- Localization (en+ru) for the backtest date fields and the settings-validation keys
  (shared resx).
This commit is contained in:
2026-05-29 00:50:43 +03:00
parent d9d92ea8fd
commit e5cd2ab30c
7 changed files with 98 additions and 5 deletions
@@ -1,5 +1,6 @@
using FluentAssertions;
using Marathon.Application.Abstractions;
using Marathon.Application.Storage;
using Marathon.Application.UseCases;
using Marathon.Domain.Backtesting;
using Marathon.Domain.Entities;
@@ -63,6 +64,27 @@ public sealed class RunBacktestUseCaseTests
StakeRule: StakeRule.Flat,
FlatStake: 100m, PercentOfBankroll: 0.02m, KellyFraction: 0.25m);
[Fact]
public async Task Should_LoadByDateRange_When_RangeProvided()
{
var id = new EventId("77777777");
var range = new DateRange(BaseTime.AddDays(-1), BaseTime.AddDays(1));
_anomalies.ListByDateRangeAsync(
Arg.Any<DateTimeOffset?>(), Arg.Any<DateTimeOffset?>(), Arg.Any<CancellationToken>())
.Returns(new[] { MakeAnomaly(id) }.ToList().AsReadOnly());
_events.GetAsync(id, Arg.Any<CancellationToken>()).Returns(MakeEvent(id));
_results.GetAsync(id, Arg.Any<CancellationToken>())
.Returns(new EventResult(id, 0, 2, Side.Side2, DateTimeOffset.UtcNow));
var result = await CreateSut().ExecuteAsync(DefaultStrategy(), range, CancellationToken.None);
await _anomalies.Received(1).ListByDateRangeAsync(
Arg.Any<DateTimeOffset?>(), Arg.Any<DateTimeOffset?>(), Arg.Any<CancellationToken>());
await _anomalies.DidNotReceive().ListAsync(Arg.Any<CancellationToken>());
result.BetsPlaced.Should().BeGreaterThan(0, "the in-range graded anomaly produces a bet");
}
[Fact]
public async Task Should_ReturnEmptyResult_When_NoAnomaliesExist()
{