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,4 +1,5 @@
using Marathon.Application.Abstractions;
using Marathon.Application.Storage;
using Marathon.Domain.AnomalyDetection;
using Marathon.Domain.Backtesting;
using Marathon.Domain.Entities;
@@ -46,17 +47,32 @@ public sealed class RunBacktestUseCase
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<BacktestResult> ExecuteAsync(
/// <summary>Runs the backtest over every graded anomaly (no date filter).</summary>
public Task<BacktestResult> ExecuteAsync(
BacktestStrategy strategy,
CancellationToken ct = default)
=> ExecuteAsync(strategy, dateRange: null, ct);
/// <summary>
/// Runs the backtest over anomalies detected within <paramref name="dateRange"/>
/// (inclusive); pass <c>null</c> to include every graded anomaly. The date filter
/// is pushed to SQL via <see cref="IAnomalyRepository.ListByDateRangeAsync"/>.
/// </summary>
public async Task<BacktestResult> ExecuteAsync(
BacktestStrategy strategy,
DateRange? dateRange,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(strategy);
_logger.LogInformation(
"RunBacktestUseCase: started — bankroll={Bankroll}, minScore={MinScore}, stakeRule={Rule}",
strategy.StartingBankroll, strategy.MinScore, strategy.StakeRule);
"RunBacktestUseCase: started — bankroll={Bankroll}, minScore={MinScore}, stakeRule={Rule}, range={Range}",
strategy.StartingBankroll, strategy.MinScore, strategy.StakeRule,
dateRange is null ? "all" : $"{dateRange.From:O}..{dateRange.To:O}");
var anomalies = await _anomalies.ListAsync(ct).ConfigureAwait(false);
var anomalies = dateRange is null
? await _anomalies.ListAsync(ct).ConfigureAwait(false)
: await _anomalies.ListByDateRangeAsync(dateRange.From, dateRange.To, ct).ConfigureAwait(false);
if (anomalies.Count == 0)
{
_logger.LogInformation("RunBacktestUseCase: no anomalies — empty result");