@
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:
+12
@@ -111,6 +111,7 @@ internal static class Program
|
||||
services.AddSingleton<AIModelService>();
|
||||
|
||||
// Trading-Kern
|
||||
services.AddSingleton<DashboardService>();
|
||||
services.AddSingleton<IRiskService, RiskService>();
|
||||
services.AddSingleton<IPortfolioService, PortfolioService>();
|
||||
services.AddSingleton<IExecutionService, ExecutionService>();
|
||||
@@ -150,6 +151,7 @@ internal static class Program
|
||||
{
|
||||
var map = new Dictionary<string, Image>
|
||||
{
|
||||
["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
|
||||
/// <summary>Registriert die Core-Views (Logs, Settings, Workers) bei der Shell.</summary>
|
||||
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
|
||||
{
|
||||
Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10,
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user