From f512a0877208cd6793791fa733a365d24f614e6f Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Fri, 29 May 2026 11:43:58 +0300 Subject: [PATCH] feat(home): surface forward-test P&L on the dashboard Adds a forward-test (paper-trading) net-P&L tile to the Home dashboard, shown only once the worker has opened or settled any paper bet. Reuses the existing IPaperTradingService aggregation (settled-only net + open count) so there is one definition of the figure, and makes the H4 forward-test result visible from the landing page instead of only its own route. - DashboardSummary gains Paper{OpenCount,SettledCount,NetProfit,RoiPercent} (default-valued, so existing constructions are unaffected) + HasPaperTrades; DashboardSummaryService injects IPaperTradingService; Home tile + en/ru resx. --- src/Marathon.UI/Pages/Home.razor | 20 +++++++++++++++++++ .../Resources/SharedResource.en.resx | 2 ++ .../Resources/SharedResource.ru.resx | 2 ++ .../Services/DashboardSummaryService.cs | 12 ++++++++++- .../Services/IDashboardSummaryService.cs | 9 ++++++++- 5 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/Marathon.UI/Pages/Home.razor b/src/Marathon.UI/Pages/Home.razor index b4ff3d5..529ed1a 100644 --- a/src/Marathon.UI/Pages/Home.razor +++ b/src/Marathon.UI/Pages/Home.razor @@ -24,6 +24,10 @@ Delta="@AnomaliesDelta" Anomaly="true" /> + @if (_summary.HasPaperTrades) + { + + }
@@ -114,6 +118,22 @@ ? string.Format(CultureInfo.CurrentCulture, L["Home.Stat.NewToday"], _summary.AnomaliesToday) : null; + // Forward-test headline: settled-only net P&L, with open-bet count as the delta line. + private string PaperNet + { + get + { + if (_summary.PaperSettledCount == 0) return "—"; + var v = _summary.PaperNetProfit; + var sign = v > 0m ? "+" : (v < 0m ? "-" : ""); + return sign + Math.Abs(v).ToString("0.00", CultureInfo.InvariantCulture); + } + } + + private string? PaperDelta => _summary.PaperOpenCount > 0 + ? string.Format(CultureInfo.CurrentCulture, L["Home.Stat.ForwardOpen"], _summary.PaperOpenCount) + : null; + protected override async Task OnInitializedAsync() { try diff --git a/src/Marathon.UI/Resources/SharedResource.en.resx b/src/Marathon.UI/Resources/SharedResource.en.resx index ef32bc4..fa7c163 100644 --- a/src/Marathon.UI/Resources/SharedResource.en.resx +++ b/src/Marathon.UI/Resources/SharedResource.en.resx @@ -74,6 +74,8 @@ Snapshots today Anomalies flagged Sports covered + Forward-test P&L + {0} open Latest signals Capture pipeline Schedule capture (`/su`) diff --git a/src/Marathon.UI/Resources/SharedResource.ru.resx b/src/Marathon.UI/Resources/SharedResource.ru.resx index da96b23..2e9be4c 100644 --- a/src/Marathon.UI/Resources/SharedResource.ru.resx +++ b/src/Marathon.UI/Resources/SharedResource.ru.resx @@ -77,6 +77,8 @@ Снимков сегодня Аномалий найдено Видов спорта + P&L форвард-теста + {0} открытых Свежий поток Конвейер сбора Сбор расписания (`/su`) diff --git a/src/Marathon.UI/Services/DashboardSummaryService.cs b/src/Marathon.UI/Services/DashboardSummaryService.cs index f414199..f53d374 100644 --- a/src/Marathon.UI/Services/DashboardSummaryService.cs +++ b/src/Marathon.UI/Services/DashboardSummaryService.cs @@ -18,6 +18,7 @@ public sealed class DashboardSummaryService : IDashboardSummaryService private readonly ISnapshotRepository _snapshots; private readonly IAnomalyRepository _anomalies; private readonly IAnomalyBrowsingService _anomalyBrowsing; + private readonly IPaperTradingService _paperTrading; private readonly IOptionsMonitor _workers; public DashboardSummaryService( @@ -25,12 +26,14 @@ public sealed class DashboardSummaryService : IDashboardSummaryService ISnapshotRepository snapshots, IAnomalyRepository anomalies, IAnomalyBrowsingService anomalyBrowsing, + IPaperTradingService paperTrading, IOptionsMonitor workers) { _events = events ?? throw new ArgumentNullException(nameof(events)); _snapshots = snapshots ?? throw new ArgumentNullException(nameof(snapshots)); _anomalies = anomalies ?? throw new ArgumentNullException(nameof(anomalies)); _anomalyBrowsing = anomalyBrowsing ?? throw new ArgumentNullException(nameof(anomalyBrowsing)); + _paperTrading = paperTrading ?? throw new ArgumentNullException(nameof(paperTrading)); _workers = workers ?? throw new ArgumentNullException(nameof(workers)); } @@ -54,6 +57,9 @@ public sealed class DashboardSummaryService : IDashboardSummaryService var w = _workers.CurrentValue; + // Reuse the forward-test aggregation (settled-only P&L) for the headline tile. + var paper = await _paperTrading.GetAsync(ct).ConfigureAwait(false); + return new DashboardSummary( EventsTracked: eventsTracked, SnapshotsToday: snapshotsToday, @@ -64,7 +70,11 @@ public sealed class DashboardSummaryService : IDashboardSummaryService ScheduleStatus: StageStatus(w.UpcomingPollerEnabled, eventsTracked > 0), SnapshotStatus: StageStatus(w.LivePollerEnabled, snapshotsToday > 0), DetectorStatus: StageStatus(w.AnomalyDetectionEnabled, anomaliesTotal > 0), - ExportStatus: eventsTracked > 0 ? "ok" : "idle"); + ExportStatus: eventsTracked > 0 ? "ok" : "idle", + PaperOpenCount: paper.OpenCount, + PaperSettledCount: paper.SettledCount, + PaperNetProfit: paper.NetProfit, + PaperRoiPercent: paper.RoiPercent); } // Maps a worker stage to the PipelineStep token: disabled → idle, enabled but diff --git a/src/Marathon.UI/Services/IDashboardSummaryService.cs b/src/Marathon.UI/Services/IDashboardSummaryService.cs index 5d07ceb..6f9655a 100644 --- a/src/Marathon.UI/Services/IDashboardSummaryService.cs +++ b/src/Marathon.UI/Services/IDashboardSummaryService.cs @@ -25,8 +25,15 @@ public sealed record DashboardSummary( string ScheduleStatus, string SnapshotStatus, string DetectorStatus, - string ExportStatus) + string ExportStatus, + int PaperOpenCount = 0, + int PaperSettledCount = 0, + decimal PaperNetProfit = 0m, + decimal? PaperRoiPercent = null) { + /// True once the forward-test worker has opened or settled any paper bet. + public bool HasPaperTrades => PaperOpenCount > 0 || PaperSettledCount > 0; + /// True once anything has been captured — gates the welcome/empty state. public bool HasAnyData => EventsTracked > 0 || AnomaliesTotal > 0;