P1c+P4: Module und Tests auf net8.0 - WinForms nur noch in PolyTrader.App

Variante B (Entscheidung Richard): Modul-UI entfernt statt in Zwischenprojekte
ausgelagert. Avalonia ist plattformuebergreifend, die neuen Ansichten kommen spaeter
direkt in die Modul-Projekte zurueck - kein Zwischenschritt, keine Wegwerfarbeit.

- 23 WinForms-Dateien aus den 4 Modulen entfernt (Spezifikation steht in
  docs/UI-SPEZIFIKATION-WinForms.md, Originalcode im Tag winforms-final).
- RegisterUi ist jetzt je Modul ein dokumentierter No-Op: View-ID, Titel, Gruppe,
  Order und der Tab-Aufbau stehen als XML-Doku drin, damit der Avalonia-Nachbau
  die stabilen IDs und die Struktur uebernimmt.
- Alle 4 Modulprojekte + Testprojekt: net8.0 statt net8.0-windows, UseWindowsForms raus.
- P4 vorgezogen (war durch den Testprojekt-Wechsel faellig): PDFsharp-MigraDoc-GDI
  -> PDFsharp-MigraDoc (Core-Build). Der Core-Build findet keine Systemschriften,
  daher neu Logic/PdfFontResolver.cs: durchsucht die Schriftverzeichnisse des OS nach
  Segoe UI/DejaVu/Liberation/Noto/Arial/FreeSans. Keine Schriftdateien im Repo noetig;
  fehlt auf Linux alles, kommt eine klare Meldung mit apt-Hinweis statt eines
  kryptischen Renderer-Fehlers.

Verifiziert: Core, alle 4 Module und das Testprojekt publishen fuer linux-x64, und
zwar ohne ein einziges Windows-spezifisches Paket in den deps.json. 442 Tests gruen
(inkl. PDF-Rendering) auf net8.0. Windows-App laeuft weiter mit den Core-Fenstern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-06 17:32:12 +02:00
co-authored by Claude Opus 5
parent 0a26b8563d
commit 53546ceeeb
34 changed files with 243 additions and 5175 deletions
@@ -49,21 +49,22 @@ namespace PolyTrader.Modules.Accounting
services.AddHostedService(sp => sp.GetRequiredService<AccountingIngestService>());
}
public void RegisterUi(IModuleUiHost host, System.IServiceProvider services)
/// <summary>
/// Aktuell ohne Ansicht: Die WinForms-UI wurde mit der Linux-Portierung entfernt, die
/// Avalonia-Ansicht folgt (Stufe L3/L4). Die Fachlogik dieses Moduls laeuft davon
/// unabhaengig weiter die Shell zeigt schlicht kein Fenster fuer das Modul an.
///
/// <para><b>Beim Nachbau zu erhalten</b> (Spezifikation: docs/UI-SPEZIFIKATION-WinForms.md,
/// Originalcode: Git-Tag <c>winforms-final</c>):</para>
/// <list type="bullet">
/// <item>View-ID <c>accounting.main</c> (stabil Launcher-Button und Symbol haengen daran)</item>
/// <item>Titel <c>Accounting</c>, Gruppe <c>Accounting</c>, Order <c>400</c></item>
/// <item>Ein Fenster fuers ganze Modul: AccountingMainForm mit Tabs: Uebersicht+BWA / Ledger / Abruf+Status</item>
/// </list>
/// </summary>
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
{
host.RegisterView(new ModuleView
{
Id = "accounting.main",
Title = "Accounting",
Group = "Accounting",
Order = 400,
CreateView = () =>
{
var form = new Ui.AccountingMainForm();
form.Initialize(services);
return form;
}
});
// Bewusst leer, bis die Avalonia-Ansicht steht (siehe Doku oben).
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -28,9 +28,15 @@ namespace PolyTrader.Modules.Accounting.Logic
decimal V(decimal usdc) => Math.Round(usdc * currencyFactor, 2, MidpointRounding.AwayFromZero);
string M(decimal usdc) => V(usdc).ToString("N2") + " " + currencyCode;
// Schriftauflösung sicherstellen: der PDFsharp-Core-Build findet ohne Resolver keine
// Systemschriften (siehe PdfFontResolver). Idempotent, kostet nach dem ersten Mal nichts.
PdfFontResolver.EnsureRegistered();
var doc = new Document();
doc.Info.Title = "Buchhalterische Abrechnung";
var style = doc.Styles["Normal"];
// Wunschschrift; der Resolver weicht auf eine verfügbare aus, wenn es sie nicht gibt
// (auf Linux z.B. DejaVu Sans).
style.Font.Name = "Segoe UI";
style.Font.Size = 9;
@@ -0,0 +1,151 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using PdfSharp.Fonts;
namespace PolyTrader.Modules.Accounting.Logic
{
/// <summary>
/// Schriftauflösung für den PDF-Export. Nötig, seit der plattformneutrale PDFsharp-Core-Build
/// verwendet wird: der frühere <c>-GDI</c>-Build zog Systemschriften über
/// <c>System.Drawing.Common</c>, das seit .NET 7 Windows-only ist. Der Core-Build bringt keine
/// eigene Schriftsuche mit und braucht deshalb diesen Resolver.
///
/// <para>Der Resolver durchsucht die üblichen Schriftverzeichnisse des Betriebssystems nach einer
/// Kandidatenliste (Windows: Segoe UI/Arial, Linux: DejaVu/Liberation/Noto). Damit sind keine
/// Schriftdateien im Repository nötig und die Lizenzfrage der Schriften bleibt beim System.</para>
///
/// <para><b>Voraussetzung auf Linux:</b> mindestens eine der Kandidatenschriften muss installiert
/// sein. Auf Desktop-Distributionen ist das der Normalfall; auf schlanken Servern/Containern
/// genügt <c>apt install fonts-dejavu-core</c> (bzw. <c>fonts-liberation</c>). Fehlt jede Schrift,
/// wirft <see cref="EnsureRegistered"/> beim ersten Export eine Meldung mit genau diesem Hinweis
/// besser als eine kryptische Meldung aus dem Renderer-Inneren.</para>
/// </summary>
public sealed class PdfFontResolver : IFontResolver
{
/// <summary>Bevorzugte Schriftfamilien in Reihenfolge erste gefundene gewinnt.</summary>
private static readonly string[] PreferredFamilies =
{
"Segoe UI", "DejaVu Sans", "Liberation Sans", "Noto Sans", "Arial", "FreeSans"
};
/// <summary>Dateinamens-Kandidaten je Familie und Schnitt (klein geschrieben, ohne Endung).</summary>
private static readonly Dictionary<string, (string Regular, string Bold)> FileNames = new(StringComparer.OrdinalIgnoreCase)
{
["Segoe UI"] = ("segoeui", "segoeuib"),
["DejaVu Sans"] = ("DejaVuSans", "DejaVuSans-Bold"),
["Liberation Sans"] = ("LiberationSans-Regular", "LiberationSans-Bold"),
["Noto Sans"] = ("NotoSans-Regular", "NotoSans-Bold"),
["Arial"] = ("arial", "arialbd"),
["FreeSans"] = ("FreeSans", "FreeSansBold"),
};
private static readonly ConcurrentDictionary<string, byte[]> Cache = new();
private static readonly object RegisterLock = new();
private static bool _registered;
/// <summary>
/// Registriert den Resolver einmalig global. Muss vor dem ersten Rendern laufen; mehrfache
/// Aufrufe sind unschädlich (PDFsharp erlaubt nur eine Zuweisung pro Prozess).
/// </summary>
public static void EnsureRegistered()
{
if (_registered) return;
lock (RegisterLock)
{
if (_registered) return;
if (FindAnyAvailableFamily() == null)
{
throw new InvalidOperationException(
"Keine geeignete Schriftart gefunden. Der PDF-Export braucht mindestens eine von: " +
string.Join(", ", PreferredFamilies) + ". " +
"Auf schlanken Linux-Systemen nachinstallieren, z.B. 'apt install fonts-dejavu-core'.");
}
GlobalFontSettings.FontResolver = new PdfFontResolver();
_registered = true;
}
}
/// <summary>Erste verfügbare Familie aus <see cref="PreferredFamilies"/>, sonst <c>null</c>.</summary>
internal static string? FindAnyAvailableFamily() =>
PreferredFamilies.FirstOrDefault(f => LocateFile(f, bold: false) != null);
public FontResolverInfo? ResolveTypeface(string familyName, bool isBold, bool isItalic)
{
// Angeforderte Familie bevorzugen, sonst die erste verfügbare (z.B. "Segoe UI" auf Linux).
string? family = LocateFile(familyName, isBold) != null ? familyName : FindAnyAvailableFamily();
if (family == null) return null;
// Kursiv wird bewusst nicht aufgelöst: der Bericht nutzt es nicht, und ein fehlender
// Kursiv-Schnitt wäre sonst ein harter Fehler statt einer kleinen Abweichung.
return new FontResolverInfo($"{family}|{(isBold ? "b" : "r")}");
}
public byte[]? GetFont(string faceName)
{
return Cache.GetOrAdd(faceName, key =>
{
var parts = key.Split('|');
string family = parts[0];
bool bold = parts.Length > 1 && parts[1] == "b";
string? path = LocateFile(family, bold) ?? LocateFile(family, bold: false);
return path != null ? File.ReadAllBytes(path) : Array.Empty<byte>();
});
}
/// <summary>Sucht die Datei zu Familie + Schnitt in allen Schriftverzeichnissen des Systems.</summary>
private static string? LocateFile(string family, bool bold)
{
if (!FileNames.TryGetValue(family, out var names)) return null;
string stem = bold ? names.Bold : names.Regular;
foreach (string dir in FontDirectories())
{
if (!Directory.Exists(dir)) continue;
foreach (string ext in new[] { ".ttf", ".otf" })
{
try
{
// Rekursiv, weil Linux die Schriften in Unterordnern ablegt (truetype/dejavu/…).
var hit = Directory.EnumerateFiles(dir, stem + ext, SearchOption.AllDirectories)
.FirstOrDefault();
if (hit != null) return hit;
}
catch (UnauthorizedAccessException) { /* einzelne Ordner ohne Zugriff überspringen */ }
catch (IOException) { /* z.B. defekter Symlink */ }
}
}
return null;
}
/// <summary>Schriftverzeichnisse je Plattform.</summary>
private static IEnumerable<string> FontDirectories()
{
if (OperatingSystem.IsWindows())
{
yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts");
yield break;
}
if (OperatingSystem.IsMacOS())
{
yield return "/System/Library/Fonts";
yield return "/Library/Fonts";
yield break;
}
// Linux/Unix
yield return "/usr/share/fonts";
yield return "/usr/local/share/fonts";
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(home))
{
yield return Path.Combine(home, ".fonts");
yield return Path.Combine(home, ".local", "share", "fonts");
}
}
}
}
@@ -1,11 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\PolyTrader.Core\PolyTrader.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="PDFsharp-MigraDoc-GDI" Version="6.2.4" />
<!-- Core-Build statt -GDI: der GDI-Build braucht System.Drawing.Common und ist damit
Windows-only. Der Core-Build findet dafuer keine Systemschriften von selbst -
siehe Logic/PdfFontResolver.cs. -->
<PackageReference Include="PDFsharp-MigraDoc" Version="6.2.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
@@ -21,11 +24,12 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<!-- Linux-Portierung: plattformneutral. Die WinForms-UI dieses Moduls (Ingest, BWA, Export)
wurde entfernt; die Avalonia-Ansicht kommt spaeter direkt hierher zurueck Avalonia ist
im Gegensatz zu WinForms plattformuebergreifend und braucht kein eigenes UI-Projekt. -->
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Modul trägt eigene WinForms-UI bei (Ledger/Status; BWA/Steuer/Export folgen A-2..A-4). -->
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
</Project>
@@ -1,609 +0,0 @@
namespace PolyTrader.Modules.Accounting.Ui
{
partial class AccountingMainForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AccountingMainForm));
tabControlAcc = new TabControl();
tabUebersicht = new TabPage();
dgvMonthly = new DataGridView();
flpKpis = new FlowLayoutPanel();
lblKpiNet = new Label();
lblKpiClosing = new Label();
lblKpiDeposits = new Label();
lblKpiWithdrawals = new Label();
lblKpiFees = new Label();
lblKpiRewards = new Label();
lblKpiVolume = new Label();
lblKpiTrades = new Label();
pnlOverviewTop = new Panel();
btnExportPdf = new Button();
btnExportCsv = new Button();
btnCalc = new Button();
cbCurrency = new ComboBox();
lblWaehrung = new Label();
dtTo = new DateTimePicker();
lblBis = new Label();
dtFrom = new DateTimePicker();
lblVon = new Label();
cbOvAccount = new ComboBox();
lblOvKonto = new Label();
tabLedger = new TabPage();
dgvLedger = new DataGridView();
toolStripLedger = new ToolStrip();
lblLedgerKonto = new ToolStripLabel();
cbLedgerAccount = new ToolStripComboBox();
sepLedger = new ToolStripSeparator();
btnLedgerRefresh = new ToolStripButton();
tabStatus = new TabPage();
dgvRuns = new DataGridView();
toolStripStatus = new ToolStrip();
lblStatusKonto = new ToolStripLabel();
cbStatusAccount = new ToolStripComboBox();
sepStatus = new ToolStripSeparator();
btnIncremental = new ToolStripButton();
btnBackfill = new ToolStripButton();
sepStatus2 = new ToolStripSeparator();
btnStatusRefresh = new ToolStripButton();
lblAccStatus = new Label();
tabControlAcc.SuspendLayout();
tabUebersicht.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvMonthly).BeginInit();
flpKpis.SuspendLayout();
pnlOverviewTop.SuspendLayout();
tabLedger.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvLedger).BeginInit();
toolStripLedger.SuspendLayout();
tabStatus.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dgvRuns).BeginInit();
toolStripStatus.SuspendLayout();
SuspendLayout();
//
// tabControlAcc
//
tabControlAcc.Controls.Add(tabUebersicht);
tabControlAcc.Controls.Add(tabLedger);
tabControlAcc.Controls.Add(tabStatus);
tabControlAcc.Dock = DockStyle.Fill;
tabControlAcc.Location = new Point(0, 0);
tabControlAcc.Margin = new Padding(4, 5, 4, 5);
tabControlAcc.Name = "tabControlAcc";
tabControlAcc.SelectedIndex = 0;
tabControlAcc.Size = new Size(1571, 1030);
tabControlAcc.TabIndex = 0;
//
// tabUebersicht
//
tabUebersicht.Controls.Add(dgvMonthly);
tabUebersicht.Controls.Add(flpKpis);
tabUebersicht.Controls.Add(pnlOverviewTop);
tabUebersicht.Location = new Point(4, 34);
tabUebersicht.Margin = new Padding(4, 5, 4, 5);
tabUebersicht.Name = "tabUebersicht";
tabUebersicht.Padding = new Padding(4, 5, 4, 5);
tabUebersicht.Size = new Size(1563, 992);
tabUebersicht.TabIndex = 0;
tabUebersicht.Text = "Übersicht / BWA";
tabUebersicht.UseVisualStyleBackColor = true;
//
// dgvMonthly
//
dgvMonthly.AllowUserToAddRows = false;
dgvMonthly.AllowUserToDeleteRows = false;
dgvMonthly.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvMonthly.Dock = DockStyle.Fill;
dgvMonthly.Location = new Point(4, 222);
dgvMonthly.Margin = new Padding(4, 5, 4, 5);
dgvMonthly.Name = "dgvMonthly";
dgvMonthly.ReadOnly = true;
dgvMonthly.RowHeadersVisible = false;
dgvMonthly.RowHeadersWidth = 62;
dgvMonthly.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvMonthly.Size = new Size(1555, 765);
dgvMonthly.TabIndex = 2;
//
// flpKpis
//
flpKpis.Controls.Add(lblKpiNet);
flpKpis.Controls.Add(lblKpiClosing);
flpKpis.Controls.Add(lblKpiDeposits);
flpKpis.Controls.Add(lblKpiWithdrawals);
flpKpis.Controls.Add(lblKpiFees);
flpKpis.Controls.Add(lblKpiRewards);
flpKpis.Controls.Add(lblKpiVolume);
flpKpis.Controls.Add(lblKpiTrades);
flpKpis.Dock = DockStyle.Top;
flpKpis.Location = new Point(4, 65);
flpKpis.Margin = new Padding(4, 5, 4, 5);
flpKpis.Name = "flpKpis";
flpKpis.Padding = new Padding(6, 7, 6, 7);
flpKpis.Size = new Size(1555, 157);
flpKpis.TabIndex = 1;
//
// lblKpiNet
//
lblKpiNet.BorderStyle = BorderStyle.FixedSingle;
lblKpiNet.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiNet.Location = new Point(12, 14);
lblKpiNet.Margin = new Padding(6, 7, 6, 7);
lblKpiNet.MinimumSize = new Size(285, 122);
lblKpiNet.Name = "lblKpiNet";
lblKpiNet.Padding = new Padding(11, 13, 11, 13);
lblKpiNet.Size = new Size(285, 122);
lblKpiNet.TabIndex = 0;
lblKpiNet.Text = "Netto-Handelsergebnis\n—";
lblKpiNet.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiClosing
//
lblKpiClosing.BorderStyle = BorderStyle.FixedSingle;
lblKpiClosing.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiClosing.Location = new Point(309, 14);
lblKpiClosing.Margin = new Padding(6, 7, 6, 7);
lblKpiClosing.MinimumSize = new Size(256, 122);
lblKpiClosing.Name = "lblKpiClosing";
lblKpiClosing.Padding = new Padding(11, 13, 11, 13);
lblKpiClosing.Size = new Size(256, 122);
lblKpiClosing.TabIndex = 1;
lblKpiClosing.Text = "Endsaldo\n—";
lblKpiClosing.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiDeposits
//
lblKpiDeposits.BorderStyle = BorderStyle.FixedSingle;
lblKpiDeposits.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiDeposits.Location = new Point(577, 14);
lblKpiDeposits.Margin = new Padding(6, 7, 6, 7);
lblKpiDeposits.MinimumSize = new Size(213, 122);
lblKpiDeposits.Name = "lblKpiDeposits";
lblKpiDeposits.Padding = new Padding(11, 13, 11, 13);
lblKpiDeposits.Size = new Size(213, 122);
lblKpiDeposits.TabIndex = 2;
lblKpiDeposits.Text = "Einzahlungen\n—";
lblKpiDeposits.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiWithdrawals
//
lblKpiWithdrawals.BorderStyle = BorderStyle.FixedSingle;
lblKpiWithdrawals.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiWithdrawals.Location = new Point(802, 14);
lblKpiWithdrawals.Margin = new Padding(6, 7, 6, 7);
lblKpiWithdrawals.MinimumSize = new Size(213, 122);
lblKpiWithdrawals.Name = "lblKpiWithdrawals";
lblKpiWithdrawals.Padding = new Padding(11, 13, 11, 13);
lblKpiWithdrawals.Size = new Size(213, 122);
lblKpiWithdrawals.TabIndex = 3;
lblKpiWithdrawals.Text = "Auszahlungen\n—";
lblKpiWithdrawals.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiFees
//
lblKpiFees.BorderStyle = BorderStyle.FixedSingle;
lblKpiFees.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiFees.Location = new Point(1027, 14);
lblKpiFees.Margin = new Padding(6, 7, 6, 7);
lblKpiFees.MinimumSize = new Size(199, 122);
lblKpiFees.Name = "lblKpiFees";
lblKpiFees.Padding = new Padding(11, 13, 11, 13);
lblKpiFees.Size = new Size(199, 122);
lblKpiFees.TabIndex = 4;
lblKpiFees.Text = "Fees\n—";
lblKpiFees.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiRewards
//
lblKpiRewards.BorderStyle = BorderStyle.FixedSingle;
lblKpiRewards.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiRewards.Location = new Point(1238, 14);
lblKpiRewards.Margin = new Padding(6, 7, 6, 7);
lblKpiRewards.MinimumSize = new Size(199, 122);
lblKpiRewards.Name = "lblKpiRewards";
lblKpiRewards.Padding = new Padding(11, 13, 11, 13);
lblKpiRewards.Size = new Size(199, 122);
lblKpiRewards.TabIndex = 5;
lblKpiRewards.Text = "Rewards\n—";
lblKpiRewards.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiVolume
//
lblKpiVolume.BorderStyle = BorderStyle.FixedSingle;
lblKpiVolume.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiVolume.Location = new Point(12, 150);
lblKpiVolume.Margin = new Padding(6, 7, 6, 7);
lblKpiVolume.MinimumSize = new Size(228, 122);
lblKpiVolume.Name = "lblKpiVolume";
lblKpiVolume.Padding = new Padding(11, 13, 11, 13);
lblKpiVolume.Size = new Size(228, 122);
lblKpiVolume.TabIndex = 6;
lblKpiVolume.Text = "Handelsvolumen\n—";
lblKpiVolume.TextAlign = ContentAlignment.MiddleLeft;
//
// lblKpiTrades
//
lblKpiTrades.BorderStyle = BorderStyle.FixedSingle;
lblKpiTrades.Font = new Font("Segoe UI", 10F, FontStyle.Bold);
lblKpiTrades.Location = new Point(252, 150);
lblKpiTrades.Margin = new Padding(6, 7, 6, 7);
lblKpiTrades.MinimumSize = new Size(171, 122);
lblKpiTrades.Name = "lblKpiTrades";
lblKpiTrades.Padding = new Padding(11, 13, 11, 13);
lblKpiTrades.Size = new Size(171, 122);
lblKpiTrades.TabIndex = 7;
lblKpiTrades.Text = "Trades\n—";
lblKpiTrades.TextAlign = ContentAlignment.MiddleLeft;
//
// pnlOverviewTop
//
pnlOverviewTop.Controls.Add(btnExportPdf);
pnlOverviewTop.Controls.Add(btnExportCsv);
pnlOverviewTop.Controls.Add(btnCalc);
pnlOverviewTop.Controls.Add(cbCurrency);
pnlOverviewTop.Controls.Add(lblWaehrung);
pnlOverviewTop.Controls.Add(dtTo);
pnlOverviewTop.Controls.Add(lblBis);
pnlOverviewTop.Controls.Add(dtFrom);
pnlOverviewTop.Controls.Add(lblVon);
pnlOverviewTop.Controls.Add(cbOvAccount);
pnlOverviewTop.Controls.Add(lblOvKonto);
pnlOverviewTop.Dock = DockStyle.Top;
pnlOverviewTop.Location = new Point(4, 5);
pnlOverviewTop.Margin = new Padding(4, 5, 4, 5);
pnlOverviewTop.Name = "pnlOverviewTop";
pnlOverviewTop.Size = new Size(1555, 60);
pnlOverviewTop.TabIndex = 0;
//
// btnExportPdf
//
btnExportPdf.Location = new Point(1334, 8);
btnExportPdf.Margin = new Padding(4, 5, 4, 5);
btnExportPdf.Name = "btnExportPdf";
btnExportPdf.Size = new Size(129, 43);
btnExportPdf.TabIndex = 10;
btnExportPdf.Text = "PDF-Export";
btnExportPdf.UseVisualStyleBackColor = true;
//
// btnExportCsv
//
btnExportCsv.Location = new Point(1197, 8);
btnExportCsv.Margin = new Padding(4, 5, 4, 5);
btnExportCsv.Name = "btnExportCsv";
btnExportCsv.Size = new Size(129, 43);
btnExportCsv.TabIndex = 9;
btnExportCsv.Text = "CSV-Export";
btnExportCsv.UseVisualStyleBackColor = true;
//
// btnCalc
//
btnCalc.Location = new Point(1046, 8);
btnCalc.Margin = new Padding(4, 5, 4, 5);
btnCalc.Name = "btnCalc";
btnCalc.Size = new Size(143, 43);
btnCalc.TabIndex = 8;
btnCalc.Text = "Berechnen";
btnCalc.UseVisualStyleBackColor = true;
//
// cbCurrency
//
cbCurrency.DropDownStyle = ComboBoxStyle.DropDownList;
cbCurrency.Items.AddRange(new object[] { "USDC", "USD", "EUR" });
cbCurrency.Location = new Point(921, 10);
cbCurrency.Margin = new Padding(4, 5, 4, 5);
cbCurrency.Name = "cbCurrency";
cbCurrency.Size = new Size(113, 33);
cbCurrency.TabIndex = 7;
//
// lblWaehrung
//
lblWaehrung.AutoSize = true;
lblWaehrung.Location = new Point(821, 15);
lblWaehrung.Margin = new Padding(4, 0, 4, 0);
lblWaehrung.Name = "lblWaehrung";
lblWaehrung.Size = new Size(88, 25);
lblWaehrung.TabIndex = 6;
lblWaehrung.Text = "Währung:";
//
// dtTo
//
dtTo.Format = DateTimePickerFormat.Short;
dtTo.Location = new Point(636, 10);
dtTo.Margin = new Padding(4, 5, 4, 5);
dtTo.Name = "dtTo";
dtTo.Size = new Size(155, 31);
dtTo.TabIndex = 5;
//
// lblBis
//
lblBis.AutoSize = true;
lblBis.Location = new Point(593, 15);
lblBis.Margin = new Padding(4, 0, 4, 0);
lblBis.Name = "lblBis";
lblBis.Size = new Size(38, 25);
lblBis.TabIndex = 4;
lblBis.Text = "Bis:";
//
// dtFrom
//
dtFrom.Format = DateTimePickerFormat.Short;
dtFrom.Location = new Point(429, 10);
dtFrom.Margin = new Padding(4, 5, 4, 5);
dtFrom.Name = "dtFrom";
dtFrom.Size = new Size(155, 31);
dtFrom.TabIndex = 3;
//
// lblVon
//
lblVon.AutoSize = true;
lblVon.Location = new Point(381, 15);
lblVon.Margin = new Padding(4, 0, 4, 0);
lblVon.Name = "lblVon";
lblVon.Size = new Size(47, 25);
lblVon.TabIndex = 2;
lblVon.Text = "Von:";
//
// cbOvAccount
//
cbOvAccount.DropDownStyle = ComboBoxStyle.DropDownList;
cbOvAccount.Location = new Point(79, 10);
cbOvAccount.Margin = new Padding(4, 5, 4, 5);
cbOvAccount.Name = "cbOvAccount";
cbOvAccount.Size = new Size(284, 33);
cbOvAccount.TabIndex = 1;
//
// lblOvKonto
//
lblOvKonto.AutoSize = true;
lblOvKonto.Location = new Point(9, 15);
lblOvKonto.Margin = new Padding(4, 0, 4, 0);
lblOvKonto.Name = "lblOvKonto";
lblOvKonto.Size = new Size(64, 25);
lblOvKonto.TabIndex = 0;
lblOvKonto.Text = "Konto:";
//
// tabLedger
//
tabLedger.Controls.Add(dgvLedger);
tabLedger.Controls.Add(toolStripLedger);
tabLedger.Location = new Point(4, 34);
tabLedger.Margin = new Padding(4, 5, 4, 5);
tabLedger.Name = "tabLedger";
tabLedger.Padding = new Padding(4, 5, 4, 5);
tabLedger.Size = new Size(1563, 992);
tabLedger.TabIndex = 1;
tabLedger.Text = "Ledger";
tabLedger.UseVisualStyleBackColor = true;
//
// dgvLedger
//
dgvLedger.AllowUserToAddRows = false;
dgvLedger.AllowUserToDeleteRows = false;
dgvLedger.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvLedger.Dock = DockStyle.Fill;
dgvLedger.Location = new Point(4, 39);
dgvLedger.Margin = new Padding(4, 5, 4, 5);
dgvLedger.Name = "dgvLedger";
dgvLedger.ReadOnly = true;
dgvLedger.RowHeadersVisible = false;
dgvLedger.RowHeadersWidth = 62;
dgvLedger.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvLedger.Size = new Size(1555, 948);
dgvLedger.TabIndex = 1;
//
// toolStripLedger
//
toolStripLedger.ImageScalingSize = new Size(24, 24);
toolStripLedger.Items.AddRange(new ToolStripItem[] { lblLedgerKonto, cbLedgerAccount, sepLedger, btnLedgerRefresh });
toolStripLedger.Location = new Point(4, 5);
toolStripLedger.Name = "toolStripLedger";
toolStripLedger.Padding = new Padding(0, 0, 3, 0);
toolStripLedger.Size = new Size(1555, 34);
toolStripLedger.TabIndex = 0;
//
// lblLedgerKonto
//
lblLedgerKonto.Name = "lblLedgerKonto";
lblLedgerKonto.Size = new Size(64, 29);
lblLedgerKonto.Text = "Konto:";
//
// cbLedgerAccount
//
cbLedgerAccount.DropDownStyle = ComboBoxStyle.DropDownList;
cbLedgerAccount.Name = "cbLedgerAccount";
cbLedgerAccount.Size = new Size(341, 34);
//
// sepLedger
//
sepLedger.Name = "sepLedger";
sepLedger.Size = new Size(6, 34);
//
// btnLedgerRefresh
//
btnLedgerRefresh.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnLedgerRefresh.Name = "btnLedgerRefresh";
btnLedgerRefresh.Size = new Size(116, 29);
btnLedgerRefresh.Text = "Aktualisieren";
//
// tabStatus
//
tabStatus.Controls.Add(dgvRuns);
tabStatus.Controls.Add(toolStripStatus);
tabStatus.Location = new Point(4, 34);
tabStatus.Margin = new Padding(4, 5, 4, 5);
tabStatus.Name = "tabStatus";
tabStatus.Padding = new Padding(4, 5, 4, 5);
tabStatus.Size = new Size(1563, 992);
tabStatus.TabIndex = 2;
tabStatus.Text = "Abruf / Status";
tabStatus.UseVisualStyleBackColor = true;
//
// dgvRuns
//
dgvRuns.AllowUserToAddRows = false;
dgvRuns.AllowUserToDeleteRows = false;
dgvRuns.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
dgvRuns.Dock = DockStyle.Fill;
dgvRuns.Location = new Point(4, 39);
dgvRuns.Margin = new Padding(4, 5, 4, 5);
dgvRuns.Name = "dgvRuns";
dgvRuns.ReadOnly = true;
dgvRuns.RowHeadersVisible = false;
dgvRuns.RowHeadersWidth = 62;
dgvRuns.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
dgvRuns.Size = new Size(1555, 948);
dgvRuns.TabIndex = 1;
//
// toolStripStatus
//
toolStripStatus.ImageScalingSize = new Size(24, 24);
toolStripStatus.Items.AddRange(new ToolStripItem[] { lblStatusKonto, cbStatusAccount, sepStatus, btnIncremental, btnBackfill, sepStatus2, btnStatusRefresh });
toolStripStatus.Location = new Point(4, 5);
toolStripStatus.Name = "toolStripStatus";
toolStripStatus.Padding = new Padding(0, 0, 3, 0);
toolStripStatus.Size = new Size(1555, 34);
toolStripStatus.TabIndex = 0;
//
// lblStatusKonto
//
lblStatusKonto.Name = "lblStatusKonto";
lblStatusKonto.Size = new Size(64, 29);
lblStatusKonto.Text = "Konto:";
//
// cbStatusAccount
//
cbStatusAccount.DropDownStyle = ComboBoxStyle.DropDownList;
cbStatusAccount.Name = "cbStatusAccount";
cbStatusAccount.Size = new Size(341, 34);
//
// sepStatus
//
sepStatus.Name = "sepStatus";
sepStatus.Size = new Size(6, 34);
//
// btnIncremental
//
btnIncremental.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnIncremental.Name = "btnIncremental";
btnIncremental.Size = new Size(134, 29);
btnIncremental.Text = "Inkrement jetzt";
btnIncremental.ToolTipText = "Nur neue Ereignisse seit dem letzten Stand abrufen (mit Lookback-Überlappung).";
//
// btnBackfill
//
btnBackfill.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnBackfill.Name = "btnBackfill";
btnBackfill.Size = new Size(188, 29);
btnBackfill.Text = "Backfill (volle Historie)";
btnBackfill.ToolTipText = "Komplette Historie neu abrufen (idempotent bucht nichts doppelt).";
//
// sepStatus2
//
sepStatus2.Name = "sepStatus2";
sepStatus2.Size = new Size(6, 34);
//
// btnStatusRefresh
//
btnStatusRefresh.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnStatusRefresh.Name = "btnStatusRefresh";
btnStatusRefresh.Size = new Size(116, 29);
btnStatusRefresh.Text = "Aktualisieren";
//
// lblAccStatus
//
lblAccStatus.Dock = DockStyle.Bottom;
lblAccStatus.Location = new Point(0, 1030);
lblAccStatus.Margin = new Padding(4, 0, 4, 0);
lblAccStatus.Name = "lblAccStatus";
lblAccStatus.Padding = new Padding(9, 3, 9, 3);
lblAccStatus.Size = new Size(1571, 37);
lblAccStatus.TabIndex = 1;
//
// AccountingMainForm
//
AutoScaleDimensions = new SizeF(10F, 25F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1571, 1067);
Controls.Add(tabControlAcc);
Controls.Add(lblAccStatus);
Icon = (Icon)resources.GetObject("$this.Icon");
Margin = new Padding(4, 5, 4, 5);
Name = "AccountingMainForm";
StartPosition = FormStartPosition.CenterScreen;
Text = "Accounting";
tabControlAcc.ResumeLayout(false);
tabUebersicht.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dgvMonthly).EndInit();
flpKpis.ResumeLayout(false);
pnlOverviewTop.ResumeLayout(false);
pnlOverviewTop.PerformLayout();
tabLedger.ResumeLayout(false);
tabLedger.PerformLayout();
((System.ComponentModel.ISupportInitialize)dgvLedger).EndInit();
toolStripLedger.ResumeLayout(false);
toolStripLedger.PerformLayout();
tabStatus.ResumeLayout(false);
tabStatus.PerformLayout();
((System.ComponentModel.ISupportInitialize)dgvRuns).EndInit();
toolStripStatus.ResumeLayout(false);
toolStripStatus.PerformLayout();
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TabControl tabControlAcc;
private System.Windows.Forms.TabPage tabUebersicht;
private System.Windows.Forms.Panel pnlOverviewTop;
private System.Windows.Forms.Label lblOvKonto;
private System.Windows.Forms.ComboBox cbOvAccount;
private System.Windows.Forms.Label lblVon;
private System.Windows.Forms.DateTimePicker dtFrom;
private System.Windows.Forms.Label lblBis;
private System.Windows.Forms.DateTimePicker dtTo;
private System.Windows.Forms.Label lblWaehrung;
private System.Windows.Forms.ComboBox cbCurrency;
private System.Windows.Forms.Button btnCalc;
private System.Windows.Forms.Button btnExportCsv;
private System.Windows.Forms.Button btnExportPdf;
private System.Windows.Forms.FlowLayoutPanel flpKpis;
private System.Windows.Forms.Label lblKpiNet;
private System.Windows.Forms.Label lblKpiClosing;
private System.Windows.Forms.Label lblKpiDeposits;
private System.Windows.Forms.Label lblKpiWithdrawals;
private System.Windows.Forms.Label lblKpiFees;
private System.Windows.Forms.Label lblKpiRewards;
private System.Windows.Forms.Label lblKpiVolume;
private System.Windows.Forms.Label lblKpiTrades;
private System.Windows.Forms.DataGridView dgvMonthly;
private System.Windows.Forms.TabPage tabLedger;
private System.Windows.Forms.ToolStrip toolStripLedger;
private System.Windows.Forms.ToolStripLabel lblLedgerKonto;
private System.Windows.Forms.ToolStripComboBox cbLedgerAccount;
private System.Windows.Forms.ToolStripSeparator sepLedger;
private System.Windows.Forms.ToolStripButton btnLedgerRefresh;
private System.Windows.Forms.DataGridView dgvLedger;
private System.Windows.Forms.TabPage tabStatus;
private System.Windows.Forms.ToolStrip toolStripStatus;
private System.Windows.Forms.ToolStripLabel lblStatusKonto;
private System.Windows.Forms.ToolStripComboBox cbStatusAccount;
private System.Windows.Forms.ToolStripSeparator sepStatus;
private System.Windows.Forms.ToolStripButton btnIncremental;
private System.Windows.Forms.ToolStripButton btnBackfill;
private System.Windows.Forms.ToolStripSeparator sepStatus2;
private System.Windows.Forms.ToolStripButton btnStatusRefresh;
private System.Windows.Forms.DataGridView dgvRuns;
private System.Windows.Forms.Label lblAccStatus;
}
}
@@ -1,322 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Modules.Accounting.Logic;
using PolyTrader.Modules.Accounting.Persistence;
using PolyTrader.Modules.Accounting.Services;
using PolyTraderSharp;
namespace PolyTrader.Modules.Accounting.Ui
{
/// <summary>
/// Hauptfenster des Accounting-Moduls: Tab „Übersicht / BWA" (neutrale Periodenabrechnung + KPIs +
/// Monatsvergleich, Währung USDC/USD/EUR, CSV-Export), „Ledger" (Buchungssätze) und „Abruf / Status"
/// (Ingest-Läufe, manueller Backfill/Inkrement). Layout im Designer; Aggregation pur im
/// <see cref="AccountingEngine"/>. Die US-Steuerschicht (A-3) ist bewusst NICHT hier sie hängt an
/// den CPA-Antworten.
/// </summary>
public partial class AccountingMainForm : Form
{
private ILedgerRepository? _ledger;
private IIngestRunRepository? _runs;
private AccountingIngestService? _ingest;
private AccountingReportService? _report;
private TradingState? _state;
public AccountingMainForm()
{
InitializeComponent();
// Übersicht / BWA
btnCalc.Click += (_, _) => Recalculate();
btnExportCsv.Click += (_, _) => ExportCsv();
btnExportPdf.Click += (_, _) => ExportPdf();
cbOvAccount.SelectedIndexChanged += (_, _) => Recalculate();
cbCurrency.SelectedIndexChanged += (_, _) => Recalculate();
// Ledger + Status
btnLedgerRefresh.Click += (_, _) => LoadLedger();
cbLedgerAccount.SelectedIndexChanged += (_, _) => LoadLedger();
btnStatusRefresh.Click += (_, _) => LoadRuns();
btnIncremental.Click += async (_, _) => await RunIngestAsync(backfill: false);
btnBackfill.Click += async (_, _) => await RunIngestAsync(backfill: true);
}
public void Initialize(IServiceProvider services)
{
_ledger = services.GetRequiredService<ILedgerRepository>();
_runs = services.GetRequiredService<IIngestRunRepository>();
_ingest = services.GetRequiredService<AccountingIngestService>();
_report = services.GetRequiredService<AccountingReportService>();
_state = services.GetRequiredService<TradingState>();
// Standard-Zeitraum: laufender Monat.
var now = DateTime.Now;
dtFrom.Value = new DateTime(now.Year, now.Month, 1);
dtTo.Value = now;
if (cbCurrency.Items.Count > 0) cbCurrency.SelectedIndex = 0; // USDC
PopulateAccounts();
Recalculate();
LoadLedger();
LoadRuns();
}
// ---------------- Konten-Filter ----------------
private void PopulateAccounts()
{
var items = new List<AccountItem> { new(null, "Alle Live-Konten") };
if (_state != null)
items.AddRange(_state.Accounts.Values
.Where(a => !a.IsDemo && !string.IsNullOrWhiteSpace(a.WalletAddress))
.OrderBy(a => a.AccountId)
.Select(a => new AccountItem(a.AccountId, string.IsNullOrEmpty(a.Name) ? $"#{a.AccountId}" : $"{a.Name} (#{a.AccountId})")));
cbOvAccount.DisplayMember = nameof(AccountItem.Label);
cbOvAccount.DataSource = new List<AccountItem>(items);
foreach (var combo in new[] { cbLedgerAccount, cbStatusAccount })
{
combo.ComboBox.DisplayMember = nameof(AccountItem.Label);
combo.ComboBox.DataSource = new List<AccountItem>(items);
}
}
private static int? IdOf(object? item) => item is AccountItem it ? it.Id : null;
// ---------------- Übersicht / BWA ----------------
private void Recalculate()
{
if (_report == null) return;
try
{
DateTime from = dtFrom.Value.Date;
DateTime to = dtTo.Value.Date.AddDays(1).AddTicks(-1); // inklusive gewählter Bis-Tag
int? accId = IdOf(cbOvAccount.SelectedItem);
var statement = _report.BuildStatement(accId, from, to);
var monthly = _report.BuildMonthly(accId, from, to);
var cur = _report.ResolveCurrency(cbCurrency.SelectedItem?.ToString() ?? "USDC", to);
UpdateKpis(statement, cur);
dgvMonthly.DataSource = monthly.Select(m => new MonthlyRow(m, cur)).ToList();
lblAccStatus.Text = cur.Available
? $"Währung {cur.Code}: {cur.Note}"
: $"⚠️ {cur.Note} (Anzeige in USDC).";
}
catch (Exception ex)
{
lblAccStatus.Text = $"Abrechnung nicht möglich (acc_-Migration angewendet?): {ex.Message}";
}
}
private void UpdateKpis(PeriodStatement s, CurrencyContext cur)
{
CurrencyContext view = cur.Available ? cur : new CurrencyContext("USDC", 1m, true, "");
string unit = view.Code;
decimal V(decimal usdc) => AccountingReportService.Convert(usdc, view);
lblKpiNet.Text = $"Netto-Handelsergebnis\n{V(s.NetTradingResultUsdc):N2} {unit}";
lblKpiNet.ForeColor = s.NetTradingResultUsdc >= 0 ? System.Drawing.Color.ForestGreen : System.Drawing.Color.Firebrick;
lblKpiClosing.Text = $"Endsaldo\n{V(s.ClosingBalanceUsdc):N2} {unit}";
lblKpiDeposits.Text = $"Einzahlungen\n{V(s.Deposits):N2} {unit}";
lblKpiWithdrawals.Text = $"Auszahlungen\n{V(s.Withdrawals):N2} {unit}";
lblKpiFees.Text = $"Fees\n{V(s.Fees):N2} {unit}";
lblKpiRewards.Text = $"Rewards\n{V(s.Rewards):N2} {unit}";
lblKpiVolume.Text = $"Handelsvolumen\n{V(s.TradeVolume):N2} {unit}";
lblKpiTrades.Text = $"Trades\n{s.TradeCount}";
}
private void ExportCsv()
{
if (_ledger == null || _report == null) return;
DateTime from = dtFrom.Value.Date;
DateTime to = dtTo.Value.Date.AddDays(1).AddTicks(-1);
int? accId = IdOf(cbOvAccount.SelectedItem);
using var dlg = new SaveFileDialog
{
Filter = "CSV-Datei (*.csv)|*.csv",
FileName = $"accounting_{(accId?.ToString() ?? "alle")}_{from:yyyyMMdd}-{dtTo.Value:yyyyMMdd}.csv"
};
if (dlg.ShowDialog(this) != DialogResult.OK) return;
try
{
var statement = _report.BuildStatement(accId, from, to);
var entries = _ledger.Query(accId, from, to, 100000);
string csv = CsvExporter.Statement(statement) + Environment.NewLine + CsvExporter.Ledger(entries);
System.IO.File.WriteAllText(dlg.FileName, csv, new System.Text.UTF8Encoding(true));
lblAccStatus.Text = $"CSV exportiert: {dlg.FileName} ({entries.Count} Buchungen).";
}
catch (Exception ex)
{
lblAccStatus.Text = $"CSV-Export fehlgeschlagen: {ex.Message}";
}
}
private void ExportPdf()
{
if (_ledger == null || _report == null) return;
DateTime from = dtFrom.Value.Date;
DateTime to = dtTo.Value.Date.AddDays(1).AddTicks(-1);
int? accId = IdOf(cbOvAccount.SelectedItem);
using var dlg = new SaveFileDialog
{
Filter = "PDF-Datei (*.pdf)|*.pdf",
FileName = $"abrechnung_{(accId?.ToString() ?? "alle")}_{from:yyyyMMdd}-{dtTo.Value:yyyyMMdd}.pdf"
};
if (dlg.ShowDialog(this) != DialogResult.OK) return;
try
{
var statement = _report.BuildStatement(accId, from, to);
var monthly = _report.BuildMonthly(accId, from, to);
var entries = _ledger.Query(accId, from, to, 100000);
var cur = _report.ResolveCurrency(cbCurrency.SelectedItem?.ToString() ?? "USDC", to);
CurrencyContext view = cur.Available ? cur : new CurrencyContext("USDC", 1m, true, "Native Buchungswährung.");
byte[] pdf = PdfExporter.Render(statement, monthly, entries, view.Code, view.Factor, view.Note);
System.IO.File.WriteAllBytes(dlg.FileName, pdf);
lblAccStatus.Text = $"PDF exportiert: {dlg.FileName} ({entries.Count} Buchungen, {view.Code}).";
}
catch (Exception ex)
{
lblAccStatus.Text = $"PDF-Export fehlgeschlagen: {ex.Message}";
}
}
// ---------------- Ledger ----------------
private void LoadLedger()
{
if (_ledger == null) return;
try
{
var rows = _ledger.Query(IdOf(cbLedgerAccount.SelectedItem), null, null, 1000);
dgvLedger.DataSource = rows.Select(e => new LedgerRow(e)).ToList();
lblAccStatus.Text = rows.Count == 0
? "Noch keine Buchungen. (Ingest-Quellen sind offline bis zur Live-Anbindung im Zielland.)"
: $"{rows.Count} Buchungssätze angezeigt.";
}
catch (Exception ex)
{
lblAccStatus.Text = $"Ledger nicht lesbar (acc_-Migration angewendet?): {ex.Message}";
}
}
// ---------------- Abruf / Status ----------------
private void LoadRuns()
{
if (_runs == null) return;
try
{
dgvRuns.DataSource = _runs.GetRecent(IdOf(cbStatusAccount.SelectedItem), 100);
}
catch (Exception ex)
{
lblAccStatus.Text = $"Ingest-Läufe nicht lesbar: {ex.Message}";
}
}
private async System.Threading.Tasks.Task RunIngestAsync(bool backfill)
{
if (_ingest == null) return;
btnBackfill.Enabled = false;
btnIncremental.Enabled = false;
lblAccStatus.Text = backfill ? "Backfill läuft …" : "Inkrementeller Abruf läuft …";
try
{
await System.Threading.Tasks.Task.Run(() => _ingest.IngestAllAsync(backfill, CancellationToken.None));
lblAccStatus.Text = "Abruf abgeschlossen.";
LoadRuns();
LoadLedger();
Recalculate();
}
catch (Exception ex)
{
lblAccStatus.Text = $"Abruf-Fehler: {ex.Message}";
}
finally
{
btnBackfill.Enabled = true;
btnIncremental.Enabled = true;
}
}
private sealed record AccountItem(int? Id, string Label);
/// <summary>Anzeige-Zeile des Monatsvergleichs (Beträge in der gewählten Währung).</summary>
private sealed class MonthlyRow
{
public MonthlyRow(PeriodStatement m, CurrencyContext cur)
{
CurrencyContext view = cur.Available ? cur : new CurrencyContext("USDC", 1m, true, "");
decimal V(decimal usdc) => AccountingReportService.Convert(usdc, view);
Monat = m.From.ToString("yyyy-MM");
Anfangssaldo = V(m.OpeningBalanceUsdc);
Einzahlungen = V(m.Deposits);
Auszahlungen = V(m.Withdrawals);
Handelsvolumen = V(m.TradeVolume);
Rewards = V(m.Rewards);
Fees = V(m.Fees);
Handelsergebnis = V(m.NetTradingResultUsdc);
Endsaldo = V(m.ClosingBalanceUsdc);
Trades = m.TradeCount;
}
public string Monat { get; }
public decimal Anfangssaldo { get; }
public decimal Einzahlungen { get; }
public decimal Auszahlungen { get; }
public decimal Handelsvolumen { get; }
public decimal Rewards { get; }
public decimal Fees { get; }
public decimal Handelsergebnis { get; }
public decimal Endsaldo { get; }
public int Trades { get; }
}
/// <summary>Anzeige-Zeile fürs Ledger-Grid.</summary>
private sealed class LedgerRow
{
public LedgerRow(Models.LedgerEntry e)
{
Zeit = e.Timestamp;
Konto = e.AccountId;
Typ = e.EventType.ToString();
Markt = e.MarketSlug;
Outcome = e.Outcome;
Side = e.Side;
Size = e.Size;
Preis = e.PriceUsdc;
Brutto = e.GrossUsdc;
Fee = e.FeeUsdc;
Netto = e.NetUsdc;
TxHash = e.TxHash;
Quelle = e.Source;
}
public DateTime Zeit { get; }
public int Konto { get; }
public string Typ { get; }
public string Markt { get; }
public string Outcome { get; }
public string Side { get; }
public decimal Size { get; }
public decimal Preis { get; }
public decimal Brutto { get; }
public decimal Fee { get; }
public decimal Netto { get; }
public string TxHash { get; }
public string Quelle { get; }
}
}
}
@@ -1,202 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="toolStripLedger.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="toolStripStatus.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>206, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
AABJRU5ErkJggg==
</value>
</data>
</root>