@
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>();
|
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,
|
||||||
|
|||||||
@@ -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
|
- [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 (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