diff --git a/Program.cs b/Program.cs index 2863757..065f160 100644 --- a/Program.cs +++ b/Program.cs @@ -111,6 +111,7 @@ internal static class Program services.AddSingleton(); // Trading-Kern + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -150,6 +151,7 @@ internal static class Program { var map = new Dictionary { + ["core.dashboard"] = Properties.Resources.dashboard, ["core.workers"] = Properties.Resources.system_time, ["core.logs"] = Properties.Resources.error_log, ["core.settings"] = Properties.Resources.setting_tools, @@ -163,6 +165,16 @@ internal static class Program /// Registriert die Core-Views (Logs, Settings, Workers) bei der Shell. 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(), + sp.GetRequiredService(), + sp.GetServices(), + sp.GetRequiredService(), + sp.GetServices()) + }); uiHost.RegisterView(new ModuleView { Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10, diff --git a/UI/Views/DashboardView.cs b/UI/Views/DashboardView.cs new file mode 100644 index 0000000..401a64c --- /dev/null +++ b/UI/Views/DashboardView.cs @@ -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; + +/// Core-View: Gesamtüberblick (Trading-Modus, aggregierte Kennzahlen, geladene Module). +public sealed class DashboardView : Form +{ + private readonly DashboardService _dashboard; + private readonly SettingsService _settings; + private readonly IReadOnlyList _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 modules, + IConfiguration config, + IEnumerable 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}"; + } + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0ef5fd8..409b576 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 - [ ] **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 -- [ ] Bestehende Unit-Tests portieren; `--smoke-ui`; EF-InMemory-Tests wo sinnvoll +### R7 – Feinschliff ✅ +- [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 (R1–R7) +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. --- diff --git a/src/IBKRTrader.Core/Trading/DashboardService.cs b/src/IBKRTrader.Core/Trading/DashboardService.cs new file mode 100644 index 0000000..8465ec9 --- /dev/null +++ b/src/IBKRTrader.Core/Trading/DashboardService.cs @@ -0,0 +1,26 @@ +using IBKRTrader.Core.Persistence.Ef; +using Microsoft.EntityFrameworkCore; + +namespace IBKRTrader.Core.Trading; + +/// Aggregierte Kennzahlen über alle Module für das Dashboard. +public sealed record DashboardSnapshot(int OpenPositions, decimal TotalExposure, int TotalTrades); + +/// Liest aggregierte Portfolio-Kennzahlen (EF Core) für die Dashboard-Ansicht. +public sealed class DashboardService +{ + private readonly IDbContextFactory _dbf; + + public DashboardService(IDbContextFactory dbf) => _dbf = dbf; + + public async Task 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); + } +} diff --git a/tests/IBKRTrader.Tests/Trading/DashboardServiceTests.cs b/tests/IBKRTrader.Tests/Trading/DashboardServiceTests.cs new file mode 100644 index 0000000..1bb894d --- /dev/null +++ b/tests/IBKRTrader.Tests/Trading/DashboardServiceTests.cs @@ -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 o) : IDbContextFactory + { + public CoreDbContext CreateDbContext() => new(o); + } + + private static (DashboardService sut, IDbContextFactory factory) Create() + { + var opts = new DbContextOptionsBuilder() + .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); + } +}