Files
maraphon-app/src/Marathon.Application/UseCases/CompareStrategiesUseCase.cs
T
alexei.dolgolyov 6e12dd73c3 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.
2026-05-29 11:32:01 +03:00

61 lines
2.6 KiB
C#

using Marathon.Application.Abstractions;
using Marathon.Application.Storage;
using Marathon.Domain.Backtesting;
using Microsoft.Extensions.Logging;
namespace Marathon.Application.UseCases;
/// <summary>One saved strategy preset paired with its backtest result over a shared window.</summary>
public sealed record StrategyComparison(Guid StrategyId, string Name, BacktestResult Result);
/// <summary>
/// Runs every saved strategy preset over the same anomaly window and returns their
/// backtest results side by side, so the user can see which staking configuration wins.
/// </summary>
/// <remarks>
/// Delegates to <see cref="RunBacktestUseCase"/> once per preset — the anomaly set is
/// re-loaded per run, which is fine for the handful of presets a user keeps. Keeping the
/// composition at the use-case level (rather than re-implementing candidate loading) means
/// the comparison stays bug-for-bug identical to a single backtest run.
/// </remarks>
public sealed class CompareStrategiesUseCase
{
private readonly ISavedStrategyRepository _strategies;
private readonly RunBacktestUseCase _backtest;
private readonly ILogger<CompareStrategiesUseCase> _logger;
public CompareStrategiesUseCase(
ISavedStrategyRepository strategies,
RunBacktestUseCase backtest,
ILogger<CompareStrategiesUseCase> logger)
{
_strategies = strategies ?? throw new ArgumentNullException(nameof(strategies));
_backtest = backtest ?? throw new ArgumentNullException(nameof(backtest));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Backtests each saved preset over <paramref name="dateRange"/> (null = all graded
/// anomalies). Returns one row per preset in saved (name-ascending) order; empty when
/// the user has saved no strategies.
/// </summary>
public async Task<IReadOnlyList<StrategyComparison>> ExecuteAsync(
DateRange? dateRange, CancellationToken ct = default)
{
var presets = await _strategies.ListAsync(ct).ConfigureAwait(false);
if (presets.Count == 0)
return Array.Empty<StrategyComparison>();
var rows = new List<StrategyComparison>(presets.Count);
foreach (var preset in presets)
{
ct.ThrowIfCancellationRequested();
var result = await _backtest.ExecuteAsync(preset.Strategy, dateRange, ct).ConfigureAwait(false);
rows.Add(new StrategyComparison(preset.Id, preset.Name, result));
}
_logger.LogInformation("CompareStrategiesUseCase: compared {Count} preset(s)", rows.Count);
return rows;
}
}