@* StrategyCompare — head-to-head backtest of every saved strategy preset. Runs each saved preset (from the Backtest page) over the same window and tables their ROI / hit-rate / net / drawdown side by side, flagging the best ROI. Same editorial-quant tone as Backtest. *@ @page "/anomalies/compare" @using System.Globalization @implements IDisposable @inject IStringLocalizer L @inject IStrategyComparisonService Service @inject NavigationManager Nav @inject ILogger Logger @L["App.Title"] · @L["Nav.Compare"]
@L["Compare.Kicker"]

@L["Compare.Title"]

@L["Compare.Lede"]


@L["Backtest.Field.DateRange.Hint"]
@if (!string.IsNullOrEmpty(_error)) {

@_error

}
@if (_loading) {
@L["Common.Loading"]
} else if (_vm is null || _vm.Rows.Count == 0) {
@L["Common.Empty"]

@L["Compare.Empty"]

@L["Nav.Backtest"]
} else {
@L["Compare.Section.Results"] @_vm.Rows.Count
@foreach (var row in _vm.Rows) { }
@L["Compare.Column.Strategy"] @L["Compare.Column.Bets"] @L["Compare.Column.WinLoss"] @L["Compare.Column.HitRate"] @L["Compare.Column.Net"] @L["Compare.Column.Roi"] @L["Compare.Column.MaxDrawdown"]
@row.Name @if (row.IsBest) { @L["Compare.Best"] } @row.BetsPlaced @row.Wins@row.Losses @FormatPercent(row.HitRatePercent) @FormatSignedDecimal(row.NetProfit, row.BetsPlaced) @FormatSignedPercent(row.RoiPercent) @(row.MaxDrawdown == 0m ? "—" : row.MaxDrawdown.ToString("0.00", CultureInfo.InvariantCulture))
}
@code { private DateTime? _from; private DateTime? _to; private StrategyComparisonVm? _vm; private bool _loading = true; private bool _running; private string? _error; private CancellationTokenSource? _cts; protected override async Task OnInitializedAsync() { await RunCoreAsync(); _loading = false; } private async Task RunAsync() { if (_running) return; await RunCoreAsync(); } private async Task RunCoreAsync() { _error = null; if (_from.HasValue != _to.HasValue) { _error = L["Backtest.Field.DateRange.Hint"].Value; return; } _cts?.Cancel(); _cts?.Dispose(); _cts = new CancellationTokenSource(); var ct = _cts.Token; _running = true; StateHasChanged(); try { var vm = await Service.CompareAsync(_from, _to, ct); if (!ct.IsCancellationRequested) _vm = vm; } catch (OperationCanceledException) { /* superseded */ } catch (Exception ex) { Logger.LogError(ex, "Strategy comparison failed."); _error = L["Backtest.Error.Generic"].Value; } finally { _running = false; StateHasChanged(); } } private static string FormatPercent(decimal? v) => v is null ? "—" : v.Value.ToString("0.0", CultureInfo.InvariantCulture) + "%"; private static string FormatSignedDecimal(decimal value, int betsPlaced) { if (betsPlaced == 0) return "—"; var sign = value > 0m ? "+" : (value < 0m ? "-" : ""); return sign + Math.Abs(value).ToString("0.00", CultureInfo.InvariantCulture); } private static string FormatSignedPercent(decimal? value) { if (value is null) return "—"; var v = value.Value; var sign = v > 0m ? "+" : (v < 0m ? "-" : ""); return sign + Math.Abs(v).ToString("0.0", CultureInfo.InvariantCulture) + "%"; } private static string Tone(decimal value, int betsPlaced) { if (betsPlaced == 0) return "neutral"; if (value > 0m) return "positive"; if (value < 0m) return "negative"; return "neutral"; } private static string RoiTone(decimal? roi) => roi switch { null => "neutral", > 0m => "positive", < 0m => "negative", _ => "neutral", }; public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); } }