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;