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.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user