R7: Dashboard-View + DashboardService (Abschluss Feinschliff)

- Core/Trading/DashboardService: aggregiert Positionen/Exposure/Trades via EF (DashboardSnapshot)
- UI/Views/DashboardView: Trading-Modus, aggregierte Kennzahlen, geladene Module (+ Aktivierungs-Status)
- core.dashboard-View registriert (Order 5) mit dashboard-Icon
- Tests: DashboardService (leer + Aggregation, InMemory) -> 58/58 gruen; smoke-ui deckt alle 5 Views ab

Kurskorrektur R1-R7 abgeschlossen: IBKRTrader folgt dem PolytraderSharp-Konzept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-28 20:01:55 +02:00
parent 9effc43206
commit e6bffdb70d
5 changed files with 202 additions and 2 deletions
+12
View File
@@ -111,6 +111,7 @@ internal static class Program
services.AddSingleton<AIModelService>(); services.AddSingleton<AIModelService>();
// Trading-Kern // Trading-Kern
services.AddSingleton<DashboardService>();
services.AddSingleton<IRiskService, RiskService>(); services.AddSingleton<IRiskService, RiskService>();
services.AddSingleton<IPortfolioService, PortfolioService>(); services.AddSingleton<IPortfolioService, PortfolioService>();
services.AddSingleton<IExecutionService, ExecutionService>(); services.AddSingleton<IExecutionService, ExecutionService>();
@@ -150,6 +151,7 @@ internal static class Program
{ {
var map = new Dictionary<string, Image> var map = new Dictionary<string, Image>
{ {
["core.dashboard"] = Properties.Resources.dashboard,
["core.workers"] = Properties.Resources.system_time, ["core.workers"] = Properties.Resources.system_time,
["core.logs"] = Properties.Resources.error_log, ["core.logs"] = Properties.Resources.error_log,
["core.settings"] = Properties.Resources.setting_tools, ["core.settings"] = Properties.Resources.setting_tools,
@@ -163,6 +165,16 @@ internal static class Program
/// <summary>Registriert die Core-Views (Logs, Settings, Workers) bei der Shell.</summary> /// <summary>Registriert die Core-Views (Logs, Settings, Workers) bei der Shell.</summary>
private static void RegisterCoreViews(IModuleUiHost uiHost, IServiceProvider sp) private static void RegisterCoreViews(IModuleUiHost uiHost, IServiceProvider sp)
{ {
uiHost.RegisterView(new ModuleView
{
Id = "core.dashboard", Title = "Dashboard", Group = "Core", Order = 5,
CreateForm = () => new DashboardView(
sp.GetRequiredService<DashboardService>(),
sp.GetRequiredService<SettingsService>(),
sp.GetServices<IModule>(),
sp.GetRequiredService<IConfiguration>(),
sp.GetServices<IWorker>())
});
uiHost.RegisterView(new ModuleView uiHost.RegisterView(new ModuleView
{ {
Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10, Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10,
+98
View File
@@ -0,0 +1,98 @@
using IBKRTrader.Core.Modularity;
using IBKRTrader.Core.Settings;
using IBKRTrader.Core.Trading;
using IBKRTrader.Core.Workers;
using Microsoft.Extensions.Configuration;
namespace IBKRTrader.UI.Views;
/// <summary>Core-View: Gesamtüberblick (Trading-Modus, aggregierte Kennzahlen, geladene Module).</summary>
public sealed class DashboardView : Form
{
private readonly DashboardService _dashboard;
private readonly SettingsService _settings;
private readonly IReadOnlyList<IModule> _modules;
private readonly IConfiguration _config;
private readonly int _workerCount;
private readonly Label _lblMode = new() { AutoSize = true, Location = new Point(20, 20), Font = new Font("Segoe UI", 13f, FontStyle.Bold) };
private readonly Label _lblStats = new() { AutoSize = true, Location = new Point(20, 60) };
private readonly Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 90), ForeColor = SystemColors.GrayText };
private readonly DataGridView _modulesGrid = new()
{
Location = new Point(20, 130),
Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right,
ReadOnly = true,
AllowUserToAddRows = false,
RowHeadersVisible = false,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill
};
public DashboardView(
DashboardService dashboard,
SettingsService settings,
IEnumerable<IModule> modules,
IConfiguration config,
IEnumerable<IWorker> workers)
{
_dashboard = dashboard;
_settings = settings;
_modules = modules.ToList();
_config = config;
_workerCount = workers.Count();
Text = "Dashboard";
Width = 900;
Height = 560;
StartPosition = FormStartPosition.CenterScreen;
MinimumSize = new Size(600, 400);
var modLabel = new Label { Text = "Geladene Module:", Location = new Point(20, 108), AutoSize = true };
var refresh = new Button { Text = "Aktualisieren", Location = new Point(760, 18), Width = 110, Anchor = AnchorStyles.Top | AnchorStyles.Right };
refresh.Click += async (_, _) => await RefreshAsync();
_modulesGrid.Size = new Size(ClientSize.Width - 40, ClientSize.Height - 150);
Controls.Add(_lblMode);
Controls.Add(_lblStats);
Controls.Add(_lblStatus);
Controls.Add(modLabel);
Controls.Add(_modulesGrid);
Controls.Add(refresh);
}
protected override async void OnShown(EventArgs e)
{
base.OnShown(e);
await RefreshAsync();
}
private async Task RefreshAsync()
{
var t = _settings.Settings.Trading;
_lblMode.Text = $"Trading: {t.Mode} {(t.TradingEnabled ? "AKTIV" : "inaktiv")}";
_lblMode.ForeColor = t.TradingEnabled ? Color.SeaGreen : SystemColors.GrayText;
_modulesGrid.DataSource = _modules
.Select(m => new
{
Modul = m.Name,
Präfix = m.DbPrefix,
Status = m.GetActivationBlocker(_config) ?? "aktivierbar"
})
.ToList();
try
{
var snap = await _dashboard.GetSnapshotAsync();
_lblStats.Text = $"Offene Positionen: {snap.OpenPositions} | Exposure: {snap.TotalExposure:N2} | " +
$"Trades gesamt: {snap.TotalTrades} | Worker/Services: {_workerCount}";
_lblStatus.Text = $"Aktualisiert: {DateTime.Now:HH:mm:ss}";
}
catch (Exception ex)
{
_lblStats.Text = $"Kennzahlen n/v | Worker/Services: {_workerCount}";
_lblStatus.Text = $"DB nicht erreichbar: {ex.Message}";
}
}
}
+13 -2
View File
@@ -121,8 +121,19 @@ Pin `new MariaDbServerVersion(new Version(11, 8, 6))`. Verbindung aus `appsettin
- [x] **5 SecretProtection-Tests** (Round-Trip, Idempotenz, Passthrough, Tamper/Key-Fehler) → 56/56 grün - [x] **5 SecretProtection-Tests** (Round-Trip, Idempotenz, Passthrough, Tamper/Key-Fehler) → 56/56 grün
- [ ] **Offen (Nutzer-Aktion):** geleaktes DB-Passwort rotieren (liegt in Git-Historie via `grundregeln.md`, Commit `ebeb035`); EF-Schema per `dotnet ef database update` auf die DB anwenden - [ ] **Offen (Nutzer-Aktion):** geleaktes DB-Passwort rotieren (liegt in Git-Historie via `grundregeln.md`, Commit `ebeb035`); EF-Schema per `dotnet ef database update` auf die DB anwenden
### R7 Tests + Feinschliff ### R7 Feinschliff
- [ ] Bestehende Unit-Tests portieren; `--smoke-ui`; EF-InMemory-Tests wo sinnvoll - [x] Core-**Dashboard-View** (Gesamtüberblick: Trading-Modus, aggregierte Kennzahlen, geladene Module) + Icon
- [x] `DashboardService` (aggregiert Positionen/Exposure/Trades via EF) + **2 InMemory-Tests** → 58/58 grün
- [x] Tests durchgehend portiert; `--smoke-ui` deckt alle Views ab
- [ ] **Optional/später:** echter `IbkrBrokerClient` gegen Paper-Gateway (braucht laufendes Client-Portal-Gateway); IBKR-Account-Credentials mit `EncryptedStringConverter` speichern
---
## Kurskorrektur abgeschlossen (R1R7)
IBKRTrader entspricht jetzt strukturell dem PolytraderSharp-Konzept: Multi-Projekt (Core + Modul + App + Tests),
Generic Host + `IHostedService`, `IConfiguration`, `IModule`/`ModuleView`/`ShellUiHost`, EF Core (extern migriert),
Trading-Kern (Risk/Execution/Portfolio, `NullBroker`-Default), CongressTrading-Strategie, Security (Master-Key/AES-GCM).
**Offen für später:** echte IBKR-Broker-Anbindung (Paper-Gateway), DB-Passwort-Rotation, EF-Schema anwenden.
--- ---
@@ -0,0 +1,26 @@
using IBKRTrader.Core.Persistence.Ef;
using Microsoft.EntityFrameworkCore;
namespace IBKRTrader.Core.Trading;
/// <summary>Aggregierte Kennzahlen über alle Module für das Dashboard.</summary>
public sealed record DashboardSnapshot(int OpenPositions, decimal TotalExposure, int TotalTrades);
/// <summary>Liest aggregierte Portfolio-Kennzahlen (EF Core) für die Dashboard-Ansicht.</summary>
public sealed class DashboardService
{
private readonly IDbContextFactory<CoreDbContext> _dbf;
public DashboardService(IDbContextFactory<CoreDbContext> dbf) => _dbf = dbf;
public async Task<DashboardSnapshot> GetSnapshotAsync(CancellationToken ct = default)
{
await using var db = await _dbf.CreateDbContextAsync(ct);
var positions = await db.Positions.ToListAsync(ct);
var trades = await db.TradeHistory.CountAsync(ct);
return new DashboardSnapshot(
OpenPositions: positions.Count,
TotalExposure: positions.Sum(p => p.Quantity * p.AvgPrice),
TotalTrades: trades);
}
}
@@ -0,0 +1,53 @@
using FluentAssertions;
using IBKRTrader.Core.Persistence.Ef;
using IBKRTrader.Core.Persistence.Entities;
using IBKRTrader.Core.Trading;
using Microsoft.EntityFrameworkCore;
namespace IBKRTrader.Tests.Trading;
[Trait("cat", "unit")]
public class DashboardServiceTests
{
private sealed class Factory(DbContextOptions<CoreDbContext> o) : IDbContextFactory<CoreDbContext>
{
public CoreDbContext CreateDbContext() => new(o);
}
private static (DashboardService sut, IDbContextFactory<CoreDbContext> factory) Create()
{
var opts = new DbContextOptionsBuilder<CoreDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var factory = new Factory(opts);
return (new DashboardService(factory), factory);
}
[Fact]
public async Task EmptyDatabase_ReturnsZeros()
{
var (sut, _) = Create();
var snap = await sut.GetSnapshotAsync();
snap.Should().Be(new DashboardSnapshot(0, 0m, 0));
}
[Fact]
public async Task AggregatesPositionsAndTrades()
{
var (sut, factory) = Create();
await using (var db = await factory.CreateDbContextAsync())
{
db.Positions.Add(new CorePosition { Module = "CT", Symbol = "AAPL", Quantity = 5, AvgPrice = 100m });
db.Positions.Add(new CorePosition { Module = "CT", Symbol = "MSFT", Quantity = 2, AvgPrice = 200m });
db.TradeHistory.Add(new CoreTrade { Module = "CT", Symbol = "AAPL", Action = "BUY", Quantity = 5, Price = 100m });
await db.SaveChangesAsync();
}
var snap = await sut.GetSnapshotAsync();
snap.OpenPositions.Should().Be(2);
snap.TotalExposure.Should().Be(900m); // 5*100 + 2*200
snap.TotalTrades.Should().Be(1);
}
}