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.
61 lines
2.6 KiB
C#
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;
|
|
}
|
|
}
|