Accounting A-2: neutrale Periodenabrechnung + BWA-Uebersicht + FX + CSV-Export
Alles laenderneutral, ohne steuerliche Einordnung (A-3 US-Steuerschicht bleibt bewusst offen, haengt an den CPA-Fragebogen-Antworten). Konzept: docs/konzepte/KONZEPT-Modul-Accounting.md, A-2. Pure Logik (unit-getestet): - AccountingEngine.BuildStatement: aggregiert Ledger-Saetze eines Zeitraums (x Account/alle) zu Anfangs-/Endsaldo, Ein-/Auszahlungen, Handelsvolumen, Redeems, Rewards, Fees und Netto-Handelsergebnis (Cash-Basis, EXKL. Ein-/Auszahlungen); Invariante Endsaldo-Anfang = Ergebnis + Einz. - Ausz. BuildMonthlyBreakdown: Monatsvergleich mit verketteten Anfangssalden. - FxConverter: USDC->USD (dokumentierte 1:1-Annahme) + USD->EUR ueber amtliche Tageskurse (acc_fx_rates, Nearest-on-or-before fuer Wochenend-/Feiertage). - CsvExporter: Ledger + Statement als RFC-4180-CSV, kulturinvariant (Punkt-Dezimal, ISO-Datum). Infrastruktur: - acc_fx_rates (FxRate, PK Datum) + EfFxRateRepository (Upsert je Datum). Migration AddFxRates angewendet. - ILedgerRepository.GetUpTo (alle Saetze <= to fuer die Abrechnung inkl. Anfangssaldo). - AccountingReportService: baut Abrechnungen + Waehrungs-View (USDC/USD immer, EUR wenn Kurse geladen). UI (designerfaehig, partial + .Designer.cs): neuer erster Tab 'Uebersicht / BWA' mit KPI-Kacheln (Netto-Handelsergebnis gruen/rot, Endsaldo, Ein-/Auszahlungen, Fees, Rewards, Volumen, #Trades), Monatsvergleich-Grid, Zeitraum-Picker (Standard laufender Monat), Konto- und Waehrungswahl, CSV-Export via SaveFileDialog. Tests: +6 (Periodenaggregation+Invariante, Monatsverkettung, FX-Umrechnung/Nearest-Kurs, CSV-Quoting). Build 0 Fehler, 385 Tests gruen, --smoke-ui alle 6 Views gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a3c145c0ed
commit
42a599a3a3
@@ -214,6 +214,14 @@ Sync-/Buchungsfehler unserer Trading-Seite. Bericht in der UI + Export.
|
|||||||
- **A-2 Abrechnung + Übersicht + FX:** `AccountingEngine` + BWA-UI + Periodenabrechnung je Account/alle,
|
- **A-2 Abrechnung + Übersicht + FX:** `AccountingEngine` + BWA-UI + Periodenabrechnung je Account/alle,
|
||||||
Werte in USDC/USD/EUR (Tageskurse in `acc_fx_rates`). **Akzeptanz:** Monatsabrechnung stimmt gegen
|
Werte in USDC/USD/EUR (Tageskurse in `acc_fx_rates`). **Akzeptanz:** Monatsabrechnung stimmt gegen
|
||||||
Balance-Anker; Kennzahlen plausibel; FX nachvollziehbar.
|
Balance-Anker; Kennzahlen plausibel; FX nachvollziehbar.
|
||||||
|
**✅ UMGESETZT (2026-07-20):** `AccountingEngine` (pur): `BuildStatement` (Anfangs-/Endsaldo,
|
||||||
|
Ein-/Auszahlungen, Handelsvolumen, Redeems, Rewards, Fees, Netto-Handelsergebnis Cash-Basis exkl.
|
||||||
|
Ein-/Auszahlungen; Invariante Endsaldo−Anfang = Ergebnis+Einz.−Ausz.) + `BuildMonthlyBreakdown`
|
||||||
|
(verkettete Monats-Anfangssalden). `FxConverter` (pur, USDC≈USD-1:1-Annahme dokumentiert; USD→EUR
|
||||||
|
über `acc_fx_rates`, Nearest-on-or-before). `CsvExporter` (pur, RFC-4180, kulturinvariant).
|
||||||
|
`AccountingReportService` (Abrechnung + Währungs-View USDC/USD/EUR). UI-Tab „Übersicht/BWA"
|
||||||
|
(designerfähig): KPI-Kacheln + Monatsvergleich + Zeitraum-/Konto-/Währungswahl + CSV-Export. +6 Tests.
|
||||||
|
**Offen für Zielland:** EZB-Kurs-Ingest (acc_fx_rates füllen) → dann ist EUR verfügbar (USDC/USD sofort).
|
||||||
- **A-3 US-Steuerschicht:** `UsTaxEngine` (FIFO-Lot-Matching, Haltefristen, Gain/Loss) + Form-8949-/
|
- **A-3 US-Steuerschicht:** `UsTaxEngine` (FIFO-Lot-Matching, Haltefristen, Gain/Loss) + Form-8949-/
|
||||||
Schedule-D-Ansicht + Annahmen-Dokumentation. **Akzeptanz:** Summe realisierter Gain/Loss stimmt gegen
|
Schedule-D-Ansicht + Annahmen-Dokumentation. **Akzeptanz:** Summe realisierter Gain/Loss stimmt gegen
|
||||||
die neutrale Abrechnung; Annahmen ausgewiesen.
|
die neutrale Abrechnung; Annahmen ausgewiesen.
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ namespace PolyTrader.Modules.Accounting
|
|||||||
services.AddSingleton<ILedgerRepository, EfLedgerRepository>();
|
services.AddSingleton<ILedgerRepository, EfLedgerRepository>();
|
||||||
services.AddSingleton<IIngestRunRepository, EfIngestRunRepository>();
|
services.AddSingleton<IIngestRunRepository, EfIngestRunRepository>();
|
||||||
services.AddSingleton<IRawSnapshotRepository, EfRawSnapshotRepository>();
|
services.AddSingleton<IRawSnapshotRepository, EfRawSnapshotRepository>();
|
||||||
|
services.AddSingleton<IFxRateRepository, EfFxRateRepository>();
|
||||||
|
|
||||||
|
// A-2: neutrale Periodenabrechnung/BWA + Währungs-View (USDC/USD/EUR).
|
||||||
|
services.AddSingleton<Services.AccountingReportService>();
|
||||||
|
|
||||||
// Ingest-Quellen: offline Null-Stubs (das Modul läuft ohne Live-Anbindung und bucht korrekt
|
// Ingest-Quellen: offline Null-Stubs (das Modul läuft ohne Live-Anbindung und bucht korrekt
|
||||||
// nichts). Im Zielland werden die echten Quellen (Polymarket /activity, Alchemy-Transfers,
|
// nichts). Im Zielland werden die echten Quellen (Polymarket /activity, Alchemy-Transfers,
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using PolyTrader.Modules.Accounting.Models;
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.Accounting.Logic
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Neutrale Periodenabrechnung (A-2): aggregiert die Ledger-Sätze eines Zeitraums (× Account oder
|
||||||
|
/// alle) zu einer prüfbaren Aufstellung — länderneutral, ohne steuerliche Einordnung (die kommt
|
||||||
|
/// getrennt in der US-Steuerschicht A-3). Alle Beträge in USDC (native Buchungswährung); die
|
||||||
|
/// FX-Umrechnung nach USD/EUR liegt separat im <see cref="FxConverter"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record PeriodStatement(
|
||||||
|
int? AccountId,
|
||||||
|
DateTime From,
|
||||||
|
DateTime To,
|
||||||
|
decimal OpeningBalanceUsdc,
|
||||||
|
decimal ClosingBalanceUsdc,
|
||||||
|
decimal Deposits,
|
||||||
|
decimal Withdrawals,
|
||||||
|
decimal TradeVolume,
|
||||||
|
decimal Redeems,
|
||||||
|
decimal Rewards,
|
||||||
|
decimal Fees,
|
||||||
|
decimal NetTradingResultUsdc, // operatives Ergebnis (Cash-Basis, EXKL. Ein-/Auszahlungen)
|
||||||
|
int TradeCount,
|
||||||
|
int EntryCount)
|
||||||
|
{
|
||||||
|
/// <summary>Invariante: Endsaldo − Anfangssaldo = Handelsergebnis + Einzahlungen − Auszahlungen.</summary>
|
||||||
|
public decimal BalanceChange => ClosingBalanceUsdc - OpeningBalanceUsdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AccountingEngine
|
||||||
|
{
|
||||||
|
private static bool IsCashflowType(LedgerEventType t) =>
|
||||||
|
t is LedgerEventType.Deposit or LedgerEventType.Withdrawal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Baut die Abrechnung für [<paramref name="from"/>, <paramref name="to"/>]. <paramref name="allUpToTo"/>
|
||||||
|
/// enthält ALLE Ledger-Sätze des Scopes bis <paramref name="to"/> (für den Anfangssaldo werden die
|
||||||
|
/// Sätze vor <paramref name="from"/> kumuliert). Grenzen inklusive.
|
||||||
|
/// </summary>
|
||||||
|
public static PeriodStatement BuildStatement(
|
||||||
|
IEnumerable<LedgerEntry> allUpToTo, DateTime from, DateTime to, int? accountId)
|
||||||
|
{
|
||||||
|
var list = allUpToTo.Where(e => e.Timestamp <= to).ToList();
|
||||||
|
|
||||||
|
decimal opening = list.Where(e => e.Timestamp < from).Sum(e => e.NetUsdc);
|
||||||
|
var period = list.Where(e => e.Timestamp >= from && e.Timestamp <= to).ToList();
|
||||||
|
|
||||||
|
decimal Sum(Func<LedgerEntry, bool> pred, Func<LedgerEntry, decimal> sel) =>
|
||||||
|
period.Where(pred).Sum(sel);
|
||||||
|
|
||||||
|
decimal deposits = Sum(e => e.EventType == LedgerEventType.Deposit, e => e.GrossUsdc);
|
||||||
|
decimal withdrawals = Sum(e => e.EventType == LedgerEventType.Withdrawal, e => e.GrossUsdc);
|
||||||
|
decimal tradeVolume = Sum(e => e.EventType is LedgerEventType.TradeBuy or LedgerEventType.TradeSell, e => e.GrossUsdc);
|
||||||
|
decimal redeems = Sum(e => e.EventType == LedgerEventType.Redeem, e => e.GrossUsdc);
|
||||||
|
decimal rewards = Sum(e => e.EventType == LedgerEventType.Reward, e => e.GrossUsdc);
|
||||||
|
decimal fees = period.Sum(e => e.FeeUsdc);
|
||||||
|
decimal netTrading = period.Where(e => !IsCashflowType(e.EventType)).Sum(e => e.NetUsdc);
|
||||||
|
decimal closing = opening + period.Sum(e => e.NetUsdc);
|
||||||
|
int tradeCount = period.Count(e => e.EventType is LedgerEventType.TradeBuy or LedgerEventType.TradeSell);
|
||||||
|
|
||||||
|
return new PeriodStatement(accountId, from, to, opening, closing, deposits, withdrawals,
|
||||||
|
tradeVolume, redeems, rewards, fees, netTrading, tradeCount, period.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zerlegt den Zeitraum in Kalendermonate und liefert je Monat eine Abrechnung (für den
|
||||||
|
/// BWA-Perioden-/Monatsvergleich). Anfangssaldo jedes Monats = Endsaldo des Vormonats.
|
||||||
|
/// </summary>
|
||||||
|
public static List<PeriodStatement> BuildMonthlyBreakdown(
|
||||||
|
IEnumerable<LedgerEntry> allUpToTo, DateTime from, DateTime to, int? accountId)
|
||||||
|
{
|
||||||
|
var list = allUpToTo.ToList();
|
||||||
|
var result = new List<PeriodStatement>();
|
||||||
|
var monthStart = new DateTime(from.Year, from.Month, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
while (monthStart <= to)
|
||||||
|
{
|
||||||
|
DateTime monthEnd = monthStart.AddMonths(1).AddTicks(-1);
|
||||||
|
DateTime windowFrom = monthStart < from ? from : monthStart;
|
||||||
|
DateTime windowTo = monthEnd > to ? to : monthEnd;
|
||||||
|
result.Add(BuildStatement(list, windowFrom, windowTo, accountId));
|
||||||
|
monthStart = monthStart.AddMonths(1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using PolyTrader.Modules.Accounting.Models;
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.Accounting.Logic
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reiner CSV-Export (A-2/A-4-neutral): erzeugt maschinen-/prüfbare CSV-Strings aus Ledger und
|
||||||
|
/// Abrechnung. Kulturinvariant (Punkt-Dezimal, ISO-Datum), RFC-4180-Quoting. Der Dateizugriff
|
||||||
|
/// liegt in der UI; die Formatierung ist hier pur + testbar.
|
||||||
|
/// </summary>
|
||||||
|
public static class CsvExporter
|
||||||
|
{
|
||||||
|
private static string F(decimal d) => d.ToString("0.######", CultureInfo.InvariantCulture);
|
||||||
|
private static string T(DateTime d) => d.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
private static string Q(string s)
|
||||||
|
{
|
||||||
|
s ??= string.Empty;
|
||||||
|
bool needsQuote = s.Contains(',') || s.Contains('"') || s.Contains('\n') || s.Contains('\r');
|
||||||
|
if (needsQuote) s = "\"" + s.Replace("\"", "\"\"") + "\"";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Vollständiger Ledger-Export (eine Zeile je Buchungssatz, inkl. Nachweisspalten).</summary>
|
||||||
|
public static string Ledger(IEnumerable<LedgerEntry> entries)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("Timestamp,AccountId,EventType,Side,MarketSlug,Outcome,Size,PriceUsdc,GrossUsdc,FeeUsdc,NetUsdc,TxHash,LogIndex,Source,IdempotencyKey");
|
||||||
|
foreach (var e in entries.OrderBy(e => e.Timestamp))
|
||||||
|
{
|
||||||
|
sb.Append(T(e.Timestamp)).Append(',')
|
||||||
|
.Append(e.AccountId).Append(',')
|
||||||
|
.Append(e.EventType).Append(',')
|
||||||
|
.Append(Q(e.Side)).Append(',')
|
||||||
|
.Append(Q(e.MarketSlug)).Append(',')
|
||||||
|
.Append(Q(e.Outcome)).Append(',')
|
||||||
|
.Append(F(e.Size)).Append(',')
|
||||||
|
.Append(F(e.PriceUsdc)).Append(',')
|
||||||
|
.Append(F(e.GrossUsdc)).Append(',')
|
||||||
|
.Append(F(e.FeeUsdc)).Append(',')
|
||||||
|
.Append(F(e.NetUsdc)).Append(',')
|
||||||
|
.Append(Q(e.TxHash)).Append(',')
|
||||||
|
.Append(e.LogIndex).Append(',')
|
||||||
|
.Append(Q(e.Source)).Append(',')
|
||||||
|
.Append(Q(e.IdempotencyKey)).Append('\n');
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Aggregat-Export einer Abrechnung (Kennzahl,USDC) – prüfbare Zusammenfassung.</summary>
|
||||||
|
public static string Statement(PeriodStatement s)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("Kennzahl,USDC");
|
||||||
|
void Row(string k, decimal v) => sb.Append(Q(k)).Append(',').Append(F(v)).Append('\n');
|
||||||
|
sb.AppendLine(Q($"Zeitraum {T(s.From)} .. {T(s.To)}" + (s.AccountId.HasValue ? $" | Konto {s.AccountId}" : " | alle Konten")) + ",");
|
||||||
|
Row("Anfangssaldo", s.OpeningBalanceUsdc);
|
||||||
|
Row("Einzahlungen", s.Deposits);
|
||||||
|
Row("Auszahlungen", s.Withdrawals);
|
||||||
|
Row("Handelsvolumen", s.TradeVolume);
|
||||||
|
Row("Redeems", s.Redeems);
|
||||||
|
Row("Rewards", s.Rewards);
|
||||||
|
Row("Fees", s.Fees);
|
||||||
|
Row("Netto-Handelsergebnis (Cash)", s.NetTradingResultUsdc);
|
||||||
|
Row("Endsaldo", s.ClosingBalanceUsdc);
|
||||||
|
sb.Append(Q("Anzahl Trades")).Append(',').Append(s.TradeCount).Append('\n');
|
||||||
|
sb.Append(Q("Anzahl Buchungen")).Append(',').Append(s.EntryCount).Append('\n');
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using PolyTrader.Modules.Accounting.Models;
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.Accounting.Logic
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reine Währungsumrechnung (A-2): jede Transaktion/Aufstellung ist in USDC (nativ). USD ergibt sich
|
||||||
|
/// über die (dokumentierte, umstellbare) USDC≈USD-Annahme; EUR über amtliche USD→EUR-Tageskurse
|
||||||
|
/// (EZB, versioniert in acc_fx_rates). Seiteneffektfrei + unit-getestet.
|
||||||
|
/// </summary>
|
||||||
|
public static class FxConverter
|
||||||
|
{
|
||||||
|
/// <summary>Dokumentierte Vereinfachung: 1 USDC = 1 USD (im Export ausgewiesen, in Settings umstellbar).</summary>
|
||||||
|
public const decimal DefaultUsdcToUsd = 1.0m;
|
||||||
|
|
||||||
|
public static decimal UsdcToUsd(decimal usdc, decimal usdcToUsd) =>
|
||||||
|
Math.Round(usdc * usdcToUsd, 6, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
public static decimal UsdToEur(decimal usd, decimal usdToEur) =>
|
||||||
|
Math.Round(usd * usdToEur, 6, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
public static decimal UsdcToEur(decimal usdc, decimal usdToEur, decimal usdcToUsd) =>
|
||||||
|
Math.Round(usdc * usdcToUsd * usdToEur, 6, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Amtlicher Kurs am oder vor dem Stichtag (Wochenend-/Feiertagskurse gibt es nicht → letzter
|
||||||
|
/// gültiger davor). null, wenn kein Kurs am/vor dem Datum vorliegt.
|
||||||
|
/// </summary>
|
||||||
|
public static FxRate? NearestOnOrBefore(IEnumerable<FxRate> rates, DateTime date)
|
||||||
|
{
|
||||||
|
DateTime day = date.Date;
|
||||||
|
return rates.Where(r => r.Date.Date <= day)
|
||||||
|
.OrderByDescending(r => r.Date)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.Accounting.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Amtlicher Tages-FX-Kurs (Tabelle acc_fx_rates): USD→EUR je Datum (z. B. EZB-Referenzkurs),
|
||||||
|
/// versioniert für nachvollziehbare, reproduzierbare Umrechnung. USDC→USD wird als dokumentierte
|
||||||
|
/// 1:1-Annahme separat behandelt (nicht je Tag gespeichert).
|
||||||
|
/// </summary>
|
||||||
|
public class FxRate
|
||||||
|
{
|
||||||
|
/// <summary>Kurs-Datum (nur Datum; PK).</summary>
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public decimal UsdToEur { get; set; }
|
||||||
|
public string Source { get; set; } = string.Empty; // z. B. "ECB" / "manual"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ namespace PolyTrader.Modules.Accounting.Persistence
|
|||||||
public DbSet<LedgerEntry> Ledger => Set<LedgerEntry>();
|
public DbSet<LedgerEntry> Ledger => Set<LedgerEntry>();
|
||||||
public DbSet<IngestRun> IngestRuns => Set<IngestRun>();
|
public DbSet<IngestRun> IngestRuns => Set<IngestRun>();
|
||||||
public DbSet<RawSnapshot> RawSnapshots => Set<RawSnapshot>();
|
public DbSet<RawSnapshot> RawSnapshots => Set<RawSnapshot>();
|
||||||
|
public DbSet<FxRate> FxRates => Set<FxRate>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder b)
|
protected override void OnModelCreating(ModelBuilder b)
|
||||||
{
|
{
|
||||||
@@ -64,6 +65,15 @@ namespace PolyTrader.Modules.Accounting.Persistence
|
|||||||
e.Property(x => x.Json).HasColumnType("longtext");
|
e.Property(x => x.Json).HasColumnType("longtext");
|
||||||
e.HasIndex(x => x.IngestRunId);
|
e.HasIndex(x => x.IngestRunId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
b.Entity<FxRate>(e =>
|
||||||
|
{
|
||||||
|
e.ToTable("acc_fx_rates");
|
||||||
|
e.HasKey(x => x.Date);
|
||||||
|
e.Property(x => x.Date).HasColumnType("date");
|
||||||
|
e.Property(x => x.UsdToEur).HasPrecision(18, 8);
|
||||||
|
e.Property(x => x.Source).HasMaxLength(40);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+233
@@ -0,0 +1,233 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using PolyTrader.Modules.Accounting.Persistence;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.Accounting.Persistence.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AccountingDbContext))]
|
||||||
|
[Migration("20260720100910_AddFxRates")]
|
||||||
|
partial class AddFxRates
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.13")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTrader.Modules.Accounting.Models.FxRate", b =>
|
||||||
|
{
|
||||||
|
b.Property<DateTime>("Date")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<string>("Source")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<decimal>("UsdToEur")
|
||||||
|
.HasPrecision(18, 8)
|
||||||
|
.HasColumnType("decimal(18,8)");
|
||||||
|
|
||||||
|
b.HasKey("Date");
|
||||||
|
|
||||||
|
b.ToTable("acc_fx_rates", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTrader.Modules.Accounting.Models.IngestRun", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<bool>("Backfill")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("BalanceAnchorUsdc")
|
||||||
|
.HasPrecision(28, 8)
|
||||||
|
.HasColumnType("decimal(28,8)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("BalanceDeltaUsdc")
|
||||||
|
.HasPrecision(28, 8)
|
||||||
|
.HasColumnType("decimal(28,8)");
|
||||||
|
|
||||||
|
b.Property<int>("DuplicateEntries")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FinishedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FromTimestamp")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("LedgerNetUsdc")
|
||||||
|
.HasPrecision(28, 8)
|
||||||
|
.HasColumnType("decimal(28,8)");
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("varchar(1000)");
|
||||||
|
|
||||||
|
b.Property<int>("NewEntries")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<bool>("Success")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId", "StartedAt");
|
||||||
|
|
||||||
|
b.ToTable("acc_ingest_runs", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTrader.Modules.Accounting.Models.LedgerEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("EventType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)");
|
||||||
|
|
||||||
|
b.Property<decimal>("FeeUsdc")
|
||||||
|
.HasPrecision(28, 8)
|
||||||
|
.HasColumnType("decimal(28,8)");
|
||||||
|
|
||||||
|
b.Property<decimal>("GrossUsdc")
|
||||||
|
.HasPrecision(28, 8)
|
||||||
|
.HasColumnType("decimal(28,8)");
|
||||||
|
|
||||||
|
b.Property<string>("IdempotencyKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<long>("IngestBatchId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("IngestedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("LogIndex")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("MarketSlug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("varchar(300)");
|
||||||
|
|
||||||
|
b.Property<decimal>("NetUsdc")
|
||||||
|
.HasPrecision(28, 8)
|
||||||
|
.HasColumnType("decimal(28,8)");
|
||||||
|
|
||||||
|
b.Property<string>("Outcome")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
|
b.Property<decimal>("PriceUsdc")
|
||||||
|
.HasPrecision(18, 6)
|
||||||
|
.HasColumnType("decimal(18,6)");
|
||||||
|
|
||||||
|
b.Property<string>("Side")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("varchar(10)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Size")
|
||||||
|
.HasPrecision(28, 8)
|
||||||
|
.HasColumnType("decimal(28,8)");
|
||||||
|
|
||||||
|
b.Property<string>("Source")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Timestamp")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("TokenId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("varchar(120)");
|
||||||
|
|
||||||
|
b.Property<string>("TxHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(80)
|
||||||
|
.HasColumnType("varchar(80)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("EventType");
|
||||||
|
|
||||||
|
b.HasIndex("IdempotencyKey")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("AccountId", "Timestamp");
|
||||||
|
|
||||||
|
b.ToTable("acc_ledger", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTrader.Modules.Accounting.Models.RawSnapshot", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CapturedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<long>("IngestRunId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Json")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("SourceKind")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("varchar(20)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("IngestRunId");
|
||||||
|
|
||||||
|
b.ToTable("acc_raw", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.Accounting.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFxRates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "acc_fx_rates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Date = table.Column<DateTime>(type: "date", nullable: false),
|
||||||
|
UsdToEur = table.Column<decimal>(type: "decimal(18,8)", precision: 18, scale: 8, nullable: false),
|
||||||
|
Source = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_acc_fx_rates", x => x.Date);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "acc_fx_rates");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -22,6 +22,25 @@ namespace PolyTrader.Modules.Accounting.Persistence.Migrations
|
|||||||
|
|
||||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("PolyTrader.Modules.Accounting.Models.FxRate", b =>
|
||||||
|
{
|
||||||
|
b.Property<DateTime>("Date")
|
||||||
|
.HasColumnType("date");
|
||||||
|
|
||||||
|
b.Property<string>("Source")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(40)
|
||||||
|
.HasColumnType("varchar(40)");
|
||||||
|
|
||||||
|
b.Property<decimal>("UsdToEur")
|
||||||
|
.HasPrecision(18, 8)
|
||||||
|
.HasColumnType("decimal(18,8)");
|
||||||
|
|
||||||
|
b.HasKey("Date");
|
||||||
|
|
||||||
|
b.ToTable("acc_fx_rates", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("PolyTrader.Modules.Accounting.Models.IngestRun", b =>
|
modelBuilder.Entity("PolyTrader.Modules.Accounting.Models.IngestRun", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ namespace PolyTrader.Modules.Accounting.Persistence
|
|||||||
decimal SumNet(int accountId);
|
decimal SumNet(int accountId);
|
||||||
int Count(int accountId);
|
int Count(int accountId);
|
||||||
List<LedgerEntry> Query(int? accountId, DateTime? from, DateTime? to, int limit);
|
List<LedgerEntry> Query(int? accountId, DateTime? from, DateTime? to, int limit);
|
||||||
|
/// <summary>ALLE Sätze des Scopes bis <paramref name="to"/> (für die Abrechnung inkl. Anfangssaldo).</summary>
|
||||||
|
List<LedgerEntry> GetUpTo(int? accountId, DateTime to);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IIngestRunRepository
|
public interface IIngestRunRepository
|
||||||
@@ -29,6 +31,13 @@ namespace PolyTrader.Modules.Accounting.Persistence
|
|||||||
void Insert(RawSnapshot snapshot);
|
void Insert(RawSnapshot snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Amtliche FX-Tageskurse (USD→EUR), versioniert. Upsert je Datum.</summary>
|
||||||
|
public interface IFxRateRepository
|
||||||
|
{
|
||||||
|
void Upsert(FxRate rate);
|
||||||
|
List<FxRate> GetAll();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------- EF-Implementierungen ----------------
|
// ---------------- EF-Implementierungen ----------------
|
||||||
|
|
||||||
public class EfLedgerRepository : ILedgerRepository
|
public class EfLedgerRepository : ILedgerRepository
|
||||||
@@ -77,6 +86,14 @@ namespace PolyTrader.Modules.Accounting.Persistence
|
|||||||
if (to.HasValue) q = q.Where(x => x.Timestamp <= to.Value);
|
if (to.HasValue) q = q.Where(x => x.Timestamp <= to.Value);
|
||||||
return q.OrderByDescending(x => x.Timestamp).Take(limit).ToList();
|
return q.OrderByDescending(x => x.Timestamp).Take(limit).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<LedgerEntry> GetUpTo(int? accountId, DateTime to)
|
||||||
|
{
|
||||||
|
using var ctx = _factory.CreateDbContext();
|
||||||
|
var q = ctx.Ledger.AsNoTracking().Where(x => x.Timestamp <= to);
|
||||||
|
if (accountId.HasValue) q = q.Where(x => x.AccountId == accountId.Value);
|
||||||
|
return q.OrderBy(x => x.Timestamp).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EfIngestRunRepository : IIngestRunRepository
|
public class EfIngestRunRepository : IIngestRunRepository
|
||||||
@@ -119,4 +136,25 @@ namespace PolyTrader.Modules.Accounting.Persistence
|
|||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class EfFxRateRepository : IFxRateRepository
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<AccountingDbContext> _factory;
|
||||||
|
public EfFxRateRepository(IDbContextFactory<AccountingDbContext> factory) => _factory = factory;
|
||||||
|
|
||||||
|
public void Upsert(FxRate rate)
|
||||||
|
{
|
||||||
|
using var ctx = _factory.CreateDbContext();
|
||||||
|
var existing = ctx.FxRates.Find(rate.Date.Date);
|
||||||
|
if (existing == null) ctx.FxRates.Add(new FxRate { Date = rate.Date.Date, UsdToEur = rate.UsdToEur, Source = rate.Source });
|
||||||
|
else { existing.UsdToEur = rate.UsdToEur; existing.Source = rate.Source; }
|
||||||
|
ctx.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FxRate> GetAll()
|
||||||
|
{
|
||||||
|
using var ctx = _factory.CreateDbContext();
|
||||||
|
return ctx.FxRates.AsNoTracking().OrderBy(x => x.Date).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using PolyTrader.Modules.Accounting.Logic;
|
||||||
|
using PolyTrader.Modules.Accounting.Models;
|
||||||
|
using PolyTrader.Modules.Accounting.Persistence;
|
||||||
|
|
||||||
|
namespace PolyTrader.Modules.Accounting.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Anzeige-Währung einer Abrechnung. USDC/USD sind immer verfügbar (USD über die dokumentierte
|
||||||
|
/// 1:1-Annahme), EUR nur, wenn ein amtlicher Tageskurs geladen ist.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record CurrencyContext(string Code, decimal Factor, bool Available, string Note);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Report-Service (A-2): baut neutrale Periodenabrechnungen aus dem Ledger und stellt die
|
||||||
|
/// Währungs-Umrechnung (USDC/USD/EUR) bereit. Reine Aggregation liegt im <see cref="AccountingEngine"/>,
|
||||||
|
/// FX im <see cref="FxConverter"/> – der Service verdrahtet nur Persistenz + pure Logik.
|
||||||
|
/// </summary>
|
||||||
|
public class AccountingReportService
|
||||||
|
{
|
||||||
|
private readonly ILedgerRepository _ledger;
|
||||||
|
private readonly IFxRateRepository _fx;
|
||||||
|
|
||||||
|
/// <summary>USDC→USD-Faktor (dokumentierte 1:1-Annahme; Zielland ggf. über Settings umstellbar).</summary>
|
||||||
|
public decimal UsdcToUsd { get; set; } = FxConverter.DefaultUsdcToUsd;
|
||||||
|
|
||||||
|
public AccountingReportService(ILedgerRepository ledger, IFxRateRepository fx)
|
||||||
|
{
|
||||||
|
_ledger = ledger;
|
||||||
|
_fx = fx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PeriodStatement BuildStatement(int? accountId, DateTime from, DateTime to) =>
|
||||||
|
AccountingEngine.BuildStatement(_ledger.GetUpTo(accountId, to), from, to, accountId);
|
||||||
|
|
||||||
|
public List<PeriodStatement> BuildMonthly(int? accountId, DateTime from, DateTime to) =>
|
||||||
|
AccountingEngine.BuildMonthlyBreakdown(_ledger.GetUpTo(accountId, to), from, to, accountId);
|
||||||
|
|
||||||
|
/// <summary>Löst die Anzeige-Währung zum Stichtag auf (für EUR wird der EZB-Kurs am/vor dem Datum genutzt).</summary>
|
||||||
|
public CurrencyContext ResolveCurrency(string code, DateTime asOf)
|
||||||
|
{
|
||||||
|
switch ((code ?? "USDC").Trim().ToUpperInvariant())
|
||||||
|
{
|
||||||
|
case "USD":
|
||||||
|
return new CurrencyContext("USD", UsdcToUsd, true, $"USDC≈USD (1:1-Annahme, Faktor {UsdcToUsd}).");
|
||||||
|
case "EUR":
|
||||||
|
var rate = FxConverter.NearestOnOrBefore(_fx.GetAll(), asOf);
|
||||||
|
if (rate == null)
|
||||||
|
return new CurrencyContext("EUR", 0m, false, "Keine EZB-Tageskurse geladen – EUR nicht verfügbar.");
|
||||||
|
return new CurrencyContext("EUR", UsdcToUsd * rate.UsdToEur, true,
|
||||||
|
$"USD→EUR {rate.UsdToEur} ({rate.Date:yyyy-MM-dd}, {rate.Source}); USDC≈USD 1:1.");
|
||||||
|
default:
|
||||||
|
return new CurrencyContext("USDC", 1.0m, true, "Native Buchungswährung.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal Convert(decimal usdc, CurrencyContext ctx) =>
|
||||||
|
Math.Round(usdc * ctx.Factor, 2, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
}
|
||||||
+309
-2
@@ -18,6 +18,28 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
this.tabControlAcc = new System.Windows.Forms.TabControl();
|
this.tabControlAcc = new System.Windows.Forms.TabControl();
|
||||||
|
this.tabUebersicht = new System.Windows.Forms.TabPage();
|
||||||
|
this.dgvMonthly = new System.Windows.Forms.DataGridView();
|
||||||
|
this.flpKpis = new System.Windows.Forms.FlowLayoutPanel();
|
||||||
|
this.lblKpiNet = new System.Windows.Forms.Label();
|
||||||
|
this.lblKpiClosing = new System.Windows.Forms.Label();
|
||||||
|
this.lblKpiDeposits = new System.Windows.Forms.Label();
|
||||||
|
this.lblKpiWithdrawals = new System.Windows.Forms.Label();
|
||||||
|
this.lblKpiFees = new System.Windows.Forms.Label();
|
||||||
|
this.lblKpiRewards = new System.Windows.Forms.Label();
|
||||||
|
this.lblKpiVolume = new System.Windows.Forms.Label();
|
||||||
|
this.lblKpiTrades = new System.Windows.Forms.Label();
|
||||||
|
this.pnlOverviewTop = new System.Windows.Forms.Panel();
|
||||||
|
this.btnExportCsv = new System.Windows.Forms.Button();
|
||||||
|
this.btnCalc = new System.Windows.Forms.Button();
|
||||||
|
this.cbCurrency = new System.Windows.Forms.ComboBox();
|
||||||
|
this.lblWaehrung = new System.Windows.Forms.Label();
|
||||||
|
this.dtTo = new System.Windows.Forms.DateTimePicker();
|
||||||
|
this.lblBis = new System.Windows.Forms.Label();
|
||||||
|
this.dtFrom = new System.Windows.Forms.DateTimePicker();
|
||||||
|
this.lblVon = new System.Windows.Forms.Label();
|
||||||
|
this.cbOvAccount = new System.Windows.Forms.ComboBox();
|
||||||
|
this.lblOvKonto = new System.Windows.Forms.Label();
|
||||||
this.tabLedger = new System.Windows.Forms.TabPage();
|
this.tabLedger = new System.Windows.Forms.TabPage();
|
||||||
this.dgvLedger = new System.Windows.Forms.DataGridView();
|
this.dgvLedger = new System.Windows.Forms.DataGridView();
|
||||||
this.toolStripLedger = new System.Windows.Forms.ToolStrip();
|
this.toolStripLedger = new System.Windows.Forms.ToolStrip();
|
||||||
@@ -37,6 +59,10 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
this.btnStatusRefresh = new System.Windows.Forms.ToolStripButton();
|
this.btnStatusRefresh = new System.Windows.Forms.ToolStripButton();
|
||||||
this.lblAccStatus = new System.Windows.Forms.Label();
|
this.lblAccStatus = new System.Windows.Forms.Label();
|
||||||
this.tabControlAcc.SuspendLayout();
|
this.tabControlAcc.SuspendLayout();
|
||||||
|
this.tabUebersicht.SuspendLayout();
|
||||||
|
((System.ComponentModel.ISupportInitialize)(this.dgvMonthly)).BeginInit();
|
||||||
|
this.flpKpis.SuspendLayout();
|
||||||
|
this.pnlOverviewTop.SuspendLayout();
|
||||||
this.tabLedger.SuspendLayout();
|
this.tabLedger.SuspendLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.dgvLedger)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.dgvLedger)).BeginInit();
|
||||||
this.toolStripLedger.SuspendLayout();
|
this.toolStripLedger.SuspendLayout();
|
||||||
@@ -47,6 +73,7 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
//
|
//
|
||||||
// tabControlAcc
|
// tabControlAcc
|
||||||
//
|
//
|
||||||
|
this.tabControlAcc.Controls.Add(this.tabUebersicht);
|
||||||
this.tabControlAcc.Controls.Add(this.tabLedger);
|
this.tabControlAcc.Controls.Add(this.tabLedger);
|
||||||
this.tabControlAcc.Controls.Add(this.tabStatus);
|
this.tabControlAcc.Controls.Add(this.tabStatus);
|
||||||
this.tabControlAcc.Dock = System.Windows.Forms.DockStyle.Fill;
|
this.tabControlAcc.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
@@ -56,6 +83,259 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
this.tabControlAcc.Size = new System.Drawing.Size(1100, 618);
|
this.tabControlAcc.Size = new System.Drawing.Size(1100, 618);
|
||||||
this.tabControlAcc.TabIndex = 0;
|
this.tabControlAcc.TabIndex = 0;
|
||||||
//
|
//
|
||||||
|
// tabUebersicht
|
||||||
|
//
|
||||||
|
this.tabUebersicht.Controls.Add(this.dgvMonthly);
|
||||||
|
this.tabUebersicht.Controls.Add(this.flpKpis);
|
||||||
|
this.tabUebersicht.Controls.Add(this.pnlOverviewTop);
|
||||||
|
this.tabUebersicht.Location = new System.Drawing.Point(4, 24);
|
||||||
|
this.tabUebersicht.Name = "tabUebersicht";
|
||||||
|
this.tabUebersicht.Padding = new System.Windows.Forms.Padding(3);
|
||||||
|
this.tabUebersicht.Size = new System.Drawing.Size(1092, 590);
|
||||||
|
this.tabUebersicht.TabIndex = 0;
|
||||||
|
this.tabUebersicht.Text = "Übersicht / BWA";
|
||||||
|
this.tabUebersicht.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// dgvMonthly
|
||||||
|
//
|
||||||
|
this.dgvMonthly.AllowUserToAddRows = false;
|
||||||
|
this.dgvMonthly.AllowUserToDeleteRows = false;
|
||||||
|
this.dgvMonthly.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
|
this.dgvMonthly.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
this.dgvMonthly.Location = new System.Drawing.Point(3, 133);
|
||||||
|
this.dgvMonthly.Name = "dgvMonthly";
|
||||||
|
this.dgvMonthly.ReadOnly = true;
|
||||||
|
this.dgvMonthly.RowHeadersVisible = false;
|
||||||
|
this.dgvMonthly.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
|
||||||
|
this.dgvMonthly.Size = new System.Drawing.Size(1086, 454);
|
||||||
|
this.dgvMonthly.TabIndex = 2;
|
||||||
|
//
|
||||||
|
// flpKpis
|
||||||
|
//
|
||||||
|
this.flpKpis.Controls.Add(this.lblKpiNet);
|
||||||
|
this.flpKpis.Controls.Add(this.lblKpiClosing);
|
||||||
|
this.flpKpis.Controls.Add(this.lblKpiDeposits);
|
||||||
|
this.flpKpis.Controls.Add(this.lblKpiWithdrawals);
|
||||||
|
this.flpKpis.Controls.Add(this.lblKpiFees);
|
||||||
|
this.flpKpis.Controls.Add(this.lblKpiRewards);
|
||||||
|
this.flpKpis.Controls.Add(this.lblKpiVolume);
|
||||||
|
this.flpKpis.Controls.Add(this.lblKpiTrades);
|
||||||
|
this.flpKpis.Dock = System.Windows.Forms.DockStyle.Top;
|
||||||
|
this.flpKpis.Location = new System.Drawing.Point(3, 39);
|
||||||
|
this.flpKpis.Name = "flpKpis";
|
||||||
|
this.flpKpis.Padding = new System.Windows.Forms.Padding(4);
|
||||||
|
this.flpKpis.Size = new System.Drawing.Size(1086, 94);
|
||||||
|
this.flpKpis.TabIndex = 1;
|
||||||
|
//
|
||||||
|
// lblKpiNet
|
||||||
|
//
|
||||||
|
this.lblKpiNet.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.lblKpiNet.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblKpiNet.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
this.lblKpiNet.MinimumSize = new System.Drawing.Size(200, 74);
|
||||||
|
this.lblKpiNet.Name = "lblKpiNet";
|
||||||
|
this.lblKpiNet.Padding = new System.Windows.Forms.Padding(8);
|
||||||
|
this.lblKpiNet.Size = new System.Drawing.Size(200, 74);
|
||||||
|
this.lblKpiNet.TabIndex = 0;
|
||||||
|
this.lblKpiNet.Text = "Netto-Handelsergebnis\n—";
|
||||||
|
this.lblKpiNet.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// lblKpiClosing
|
||||||
|
//
|
||||||
|
this.lblKpiClosing.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.lblKpiClosing.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblKpiClosing.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
this.lblKpiClosing.MinimumSize = new System.Drawing.Size(180, 74);
|
||||||
|
this.lblKpiClosing.Name = "lblKpiClosing";
|
||||||
|
this.lblKpiClosing.Padding = new System.Windows.Forms.Padding(8);
|
||||||
|
this.lblKpiClosing.Size = new System.Drawing.Size(180, 74);
|
||||||
|
this.lblKpiClosing.TabIndex = 1;
|
||||||
|
this.lblKpiClosing.Text = "Endsaldo\n—";
|
||||||
|
this.lblKpiClosing.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// lblKpiDeposits
|
||||||
|
//
|
||||||
|
this.lblKpiDeposits.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.lblKpiDeposits.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblKpiDeposits.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
this.lblKpiDeposits.MinimumSize = new System.Drawing.Size(150, 74);
|
||||||
|
this.lblKpiDeposits.Name = "lblKpiDeposits";
|
||||||
|
this.lblKpiDeposits.Padding = new System.Windows.Forms.Padding(8);
|
||||||
|
this.lblKpiDeposits.Size = new System.Drawing.Size(150, 74);
|
||||||
|
this.lblKpiDeposits.TabIndex = 2;
|
||||||
|
this.lblKpiDeposits.Text = "Einzahlungen\n—";
|
||||||
|
this.lblKpiDeposits.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// lblKpiWithdrawals
|
||||||
|
//
|
||||||
|
this.lblKpiWithdrawals.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.lblKpiWithdrawals.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblKpiWithdrawals.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
this.lblKpiWithdrawals.MinimumSize = new System.Drawing.Size(150, 74);
|
||||||
|
this.lblKpiWithdrawals.Name = "lblKpiWithdrawals";
|
||||||
|
this.lblKpiWithdrawals.Padding = new System.Windows.Forms.Padding(8);
|
||||||
|
this.lblKpiWithdrawals.Size = new System.Drawing.Size(150, 74);
|
||||||
|
this.lblKpiWithdrawals.TabIndex = 3;
|
||||||
|
this.lblKpiWithdrawals.Text = "Auszahlungen\n—";
|
||||||
|
this.lblKpiWithdrawals.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// lblKpiFees
|
||||||
|
//
|
||||||
|
this.lblKpiFees.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.lblKpiFees.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblKpiFees.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
this.lblKpiFees.MinimumSize = new System.Drawing.Size(140, 74);
|
||||||
|
this.lblKpiFees.Name = "lblKpiFees";
|
||||||
|
this.lblKpiFees.Padding = new System.Windows.Forms.Padding(8);
|
||||||
|
this.lblKpiFees.Size = new System.Drawing.Size(140, 74);
|
||||||
|
this.lblKpiFees.TabIndex = 4;
|
||||||
|
this.lblKpiFees.Text = "Fees\n—";
|
||||||
|
this.lblKpiFees.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// lblKpiRewards
|
||||||
|
//
|
||||||
|
this.lblKpiRewards.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.lblKpiRewards.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblKpiRewards.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
this.lblKpiRewards.MinimumSize = new System.Drawing.Size(140, 74);
|
||||||
|
this.lblKpiRewards.Name = "lblKpiRewards";
|
||||||
|
this.lblKpiRewards.Padding = new System.Windows.Forms.Padding(8);
|
||||||
|
this.lblKpiRewards.Size = new System.Drawing.Size(140, 74);
|
||||||
|
this.lblKpiRewards.TabIndex = 5;
|
||||||
|
this.lblKpiRewards.Text = "Rewards\n—";
|
||||||
|
this.lblKpiRewards.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// lblKpiVolume
|
||||||
|
//
|
||||||
|
this.lblKpiVolume.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.lblKpiVolume.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblKpiVolume.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
this.lblKpiVolume.MinimumSize = new System.Drawing.Size(160, 74);
|
||||||
|
this.lblKpiVolume.Name = "lblKpiVolume";
|
||||||
|
this.lblKpiVolume.Padding = new System.Windows.Forms.Padding(8);
|
||||||
|
this.lblKpiVolume.Size = new System.Drawing.Size(160, 74);
|
||||||
|
this.lblKpiVolume.TabIndex = 6;
|
||||||
|
this.lblKpiVolume.Text = "Handelsvolumen\n—";
|
||||||
|
this.lblKpiVolume.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// lblKpiTrades
|
||||||
|
//
|
||||||
|
this.lblKpiTrades.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.lblKpiTrades.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblKpiTrades.Margin = new System.Windows.Forms.Padding(4);
|
||||||
|
this.lblKpiTrades.MinimumSize = new System.Drawing.Size(120, 74);
|
||||||
|
this.lblKpiTrades.Name = "lblKpiTrades";
|
||||||
|
this.lblKpiTrades.Padding = new System.Windows.Forms.Padding(8);
|
||||||
|
this.lblKpiTrades.Size = new System.Drawing.Size(120, 74);
|
||||||
|
this.lblKpiTrades.TabIndex = 7;
|
||||||
|
this.lblKpiTrades.Text = "Trades\n—";
|
||||||
|
this.lblKpiTrades.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
|
||||||
|
//
|
||||||
|
// pnlOverviewTop
|
||||||
|
//
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.btnExportCsv);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.btnCalc);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.cbCurrency);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.lblWaehrung);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.dtTo);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.lblBis);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.dtFrom);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.lblVon);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.cbOvAccount);
|
||||||
|
this.pnlOverviewTop.Controls.Add(this.lblOvKonto);
|
||||||
|
this.pnlOverviewTop.Dock = System.Windows.Forms.DockStyle.Top;
|
||||||
|
this.pnlOverviewTop.Location = new System.Drawing.Point(3, 3);
|
||||||
|
this.pnlOverviewTop.Name = "pnlOverviewTop";
|
||||||
|
this.pnlOverviewTop.Size = new System.Drawing.Size(1086, 36);
|
||||||
|
this.pnlOverviewTop.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// btnExportCsv
|
||||||
|
//
|
||||||
|
this.btnExportCsv.Location = new System.Drawing.Point(838, 5);
|
||||||
|
this.btnExportCsv.Name = "btnExportCsv";
|
||||||
|
this.btnExportCsv.Size = new System.Drawing.Size(110, 26);
|
||||||
|
this.btnExportCsv.TabIndex = 9;
|
||||||
|
this.btnExportCsv.Text = "CSV-Export";
|
||||||
|
this.btnExportCsv.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// btnCalc
|
||||||
|
//
|
||||||
|
this.btnCalc.Location = new System.Drawing.Point(732, 5);
|
||||||
|
this.btnCalc.Name = "btnCalc";
|
||||||
|
this.btnCalc.Size = new System.Drawing.Size(100, 26);
|
||||||
|
this.btnCalc.TabIndex = 8;
|
||||||
|
this.btnCalc.Text = "Berechnen";
|
||||||
|
this.btnCalc.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// cbCurrency
|
||||||
|
//
|
||||||
|
this.cbCurrency.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||||
|
this.cbCurrency.Items.AddRange(new object[] { "USDC", "USD", "EUR" });
|
||||||
|
this.cbCurrency.Location = new System.Drawing.Point(645, 6);
|
||||||
|
this.cbCurrency.Name = "cbCurrency";
|
||||||
|
this.cbCurrency.Size = new System.Drawing.Size(80, 23);
|
||||||
|
this.cbCurrency.TabIndex = 7;
|
||||||
|
//
|
||||||
|
// lblWaehrung
|
||||||
|
//
|
||||||
|
this.lblWaehrung.AutoSize = true;
|
||||||
|
this.lblWaehrung.Location = new System.Drawing.Point(575, 9);
|
||||||
|
this.lblWaehrung.Name = "lblWaehrung";
|
||||||
|
this.lblWaehrung.Size = new System.Drawing.Size(64, 15);
|
||||||
|
this.lblWaehrung.TabIndex = 6;
|
||||||
|
this.lblWaehrung.Text = "Währung:";
|
||||||
|
//
|
||||||
|
// dtTo
|
||||||
|
//
|
||||||
|
this.dtTo.Format = System.Windows.Forms.DateTimePickerFormat.Short;
|
||||||
|
this.dtTo.Location = new System.Drawing.Point(445, 6);
|
||||||
|
this.dtTo.Name = "dtTo";
|
||||||
|
this.dtTo.Size = new System.Drawing.Size(110, 23);
|
||||||
|
this.dtTo.TabIndex = 5;
|
||||||
|
//
|
||||||
|
// lblBis
|
||||||
|
//
|
||||||
|
this.lblBis.AutoSize = true;
|
||||||
|
this.lblBis.Location = new System.Drawing.Point(415, 9);
|
||||||
|
this.lblBis.Name = "lblBis";
|
||||||
|
this.lblBis.Size = new System.Drawing.Size(26, 15);
|
||||||
|
this.lblBis.TabIndex = 4;
|
||||||
|
this.lblBis.Text = "Bis:";
|
||||||
|
//
|
||||||
|
// dtFrom
|
||||||
|
//
|
||||||
|
this.dtFrom.Format = System.Windows.Forms.DateTimePickerFormat.Short;
|
||||||
|
this.dtFrom.Location = new System.Drawing.Point(300, 6);
|
||||||
|
this.dtFrom.Name = "dtFrom";
|
||||||
|
this.dtFrom.Size = new System.Drawing.Size(110, 23);
|
||||||
|
this.dtFrom.TabIndex = 3;
|
||||||
|
//
|
||||||
|
// lblVon
|
||||||
|
//
|
||||||
|
this.lblVon.AutoSize = true;
|
||||||
|
this.lblVon.Location = new System.Drawing.Point(267, 9);
|
||||||
|
this.lblVon.Name = "lblVon";
|
||||||
|
this.lblVon.Size = new System.Drawing.Size(31, 15);
|
||||||
|
this.lblVon.TabIndex = 2;
|
||||||
|
this.lblVon.Text = "Von:";
|
||||||
|
//
|
||||||
|
// cbOvAccount
|
||||||
|
//
|
||||||
|
this.cbOvAccount.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||||
|
this.cbOvAccount.Location = new System.Drawing.Point(55, 6);
|
||||||
|
this.cbOvAccount.Name = "cbOvAccount";
|
||||||
|
this.cbOvAccount.Size = new System.Drawing.Size(200, 23);
|
||||||
|
this.cbOvAccount.TabIndex = 1;
|
||||||
|
//
|
||||||
|
// lblOvKonto
|
||||||
|
//
|
||||||
|
this.lblOvKonto.AutoSize = true;
|
||||||
|
this.lblOvKonto.Location = new System.Drawing.Point(6, 9);
|
||||||
|
this.lblOvKonto.Name = "lblOvKonto";
|
||||||
|
this.lblOvKonto.Size = new System.Drawing.Size(43, 15);
|
||||||
|
this.lblOvKonto.TabIndex = 0;
|
||||||
|
this.lblOvKonto.Text = "Konto:";
|
||||||
|
//
|
||||||
// tabLedger
|
// tabLedger
|
||||||
//
|
//
|
||||||
this.tabLedger.Controls.Add(this.dgvLedger);
|
this.tabLedger.Controls.Add(this.dgvLedger);
|
||||||
@@ -64,7 +344,7 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
this.tabLedger.Name = "tabLedger";
|
this.tabLedger.Name = "tabLedger";
|
||||||
this.tabLedger.Padding = new System.Windows.Forms.Padding(3);
|
this.tabLedger.Padding = new System.Windows.Forms.Padding(3);
|
||||||
this.tabLedger.Size = new System.Drawing.Size(1092, 590);
|
this.tabLedger.Size = new System.Drawing.Size(1092, 590);
|
||||||
this.tabLedger.TabIndex = 0;
|
this.tabLedger.TabIndex = 1;
|
||||||
this.tabLedger.Text = "Ledger";
|
this.tabLedger.Text = "Ledger";
|
||||||
this.tabLedger.UseVisualStyleBackColor = true;
|
this.tabLedger.UseVisualStyleBackColor = true;
|
||||||
//
|
//
|
||||||
@@ -120,7 +400,7 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
this.tabStatus.Name = "tabStatus";
|
this.tabStatus.Name = "tabStatus";
|
||||||
this.tabStatus.Padding = new System.Windows.Forms.Padding(3);
|
this.tabStatus.Padding = new System.Windows.Forms.Padding(3);
|
||||||
this.tabStatus.Size = new System.Drawing.Size(1092, 590);
|
this.tabStatus.Size = new System.Drawing.Size(1092, 590);
|
||||||
this.tabStatus.TabIndex = 1;
|
this.tabStatus.TabIndex = 2;
|
||||||
this.tabStatus.Text = "Abruf / Status";
|
this.tabStatus.Text = "Abruf / Status";
|
||||||
this.tabStatus.UseVisualStyleBackColor = true;
|
this.tabStatus.UseVisualStyleBackColor = true;
|
||||||
//
|
//
|
||||||
@@ -207,6 +487,11 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||||
this.Text = "Accounting";
|
this.Text = "Accounting";
|
||||||
this.tabControlAcc.ResumeLayout(false);
|
this.tabControlAcc.ResumeLayout(false);
|
||||||
|
this.tabUebersicht.ResumeLayout(false);
|
||||||
|
((System.ComponentModel.ISupportInitialize)(this.dgvMonthly)).EndInit();
|
||||||
|
this.flpKpis.ResumeLayout(false);
|
||||||
|
this.pnlOverviewTop.ResumeLayout(false);
|
||||||
|
this.pnlOverviewTop.PerformLayout();
|
||||||
this.tabLedger.ResumeLayout(false);
|
this.tabLedger.ResumeLayout(false);
|
||||||
this.tabLedger.PerformLayout();
|
this.tabLedger.PerformLayout();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.dgvLedger)).EndInit();
|
((System.ComponentModel.ISupportInitialize)(this.dgvLedger)).EndInit();
|
||||||
@@ -223,6 +508,28 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private System.Windows.Forms.TabControl tabControlAcc;
|
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.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.TabPage tabLedger;
|
||||||
private System.Windows.Forms.ToolStrip toolStripLedger;
|
private System.Windows.Forms.ToolStrip toolStripLedger;
|
||||||
private System.Windows.Forms.ToolStripLabel lblLedgerKonto;
|
private System.Windows.Forms.ToolStripLabel lblLedgerKonto;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using PolyTrader.Modules.Accounting.Logic;
|
||||||
using PolyTrader.Modules.Accounting.Persistence;
|
using PolyTrader.Modules.Accounting.Persistence;
|
||||||
using PolyTrader.Modules.Accounting.Services;
|
using PolyTrader.Modules.Accounting.Services;
|
||||||
using PolyTraderSharp;
|
using PolyTraderSharp;
|
||||||
@@ -11,21 +12,31 @@ using PolyTraderSharp;
|
|||||||
namespace PolyTrader.Modules.Accounting.Ui
|
namespace PolyTrader.Modules.Accounting.Ui
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hauptfenster des Accounting-Moduls (A-1): Tab „Ledger" (unabhängig erhobene Buchungssätze,
|
/// Hauptfenster des Accounting-Moduls: Tab „Übersicht / BWA" (neutrale Periodenabrechnung + KPIs +
|
||||||
/// filterbar) und Tab „Abruf / Status" (Ingest-Läufe + Balance-Anker, manueller Backfill/Inkrement).
|
/// Monatsvergleich, Währung USDC/USD/EUR, CSV-Export), „Ledger" (Buchungssätze) und „Abruf / Status"
|
||||||
/// BWA-Übersicht, US-Steuerschicht und CSV/PDF-Export folgen A-2..A-4. Layout im Designer.
|
/// (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>
|
/// </summary>
|
||||||
public partial class AccountingMainForm : Form
|
public partial class AccountingMainForm : Form
|
||||||
{
|
{
|
||||||
private ILedgerRepository? _ledger;
|
private ILedgerRepository? _ledger;
|
||||||
private IIngestRunRepository? _runs;
|
private IIngestRunRepository? _runs;
|
||||||
private AccountingIngestService? _ingest;
|
private AccountingIngestService? _ingest;
|
||||||
|
private AccountingReportService? _report;
|
||||||
private TradingState? _state;
|
private TradingState? _state;
|
||||||
|
|
||||||
public AccountingMainForm()
|
public AccountingMainForm()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
// Übersicht / BWA
|
||||||
|
btnCalc.Click += (_, _) => Recalculate();
|
||||||
|
btnExportCsv.Click += (_, _) => ExportCsv();
|
||||||
|
cbOvAccount.SelectedIndexChanged += (_, _) => Recalculate();
|
||||||
|
cbCurrency.SelectedIndexChanged += (_, _) => Recalculate();
|
||||||
|
|
||||||
|
// Ledger + Status
|
||||||
btnLedgerRefresh.Click += (_, _) => LoadLedger();
|
btnLedgerRefresh.Click += (_, _) => LoadLedger();
|
||||||
cbLedgerAccount.SelectedIndexChanged += (_, _) => LoadLedger();
|
cbLedgerAccount.SelectedIndexChanged += (_, _) => LoadLedger();
|
||||||
btnStatusRefresh.Click += (_, _) => LoadRuns();
|
btnStatusRefresh.Click += (_, _) => LoadRuns();
|
||||||
@@ -38,9 +49,17 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
_ledger = services.GetRequiredService<ILedgerRepository>();
|
_ledger = services.GetRequiredService<ILedgerRepository>();
|
||||||
_runs = services.GetRequiredService<IIngestRunRepository>();
|
_runs = services.GetRequiredService<IIngestRunRepository>();
|
||||||
_ingest = services.GetRequiredService<AccountingIngestService>();
|
_ingest = services.GetRequiredService<AccountingIngestService>();
|
||||||
|
_report = services.GetRequiredService<AccountingReportService>();
|
||||||
_state = services.GetRequiredService<TradingState>();
|
_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();
|
PopulateAccounts();
|
||||||
|
Recalculate();
|
||||||
LoadLedger();
|
LoadLedger();
|
||||||
LoadRuns();
|
LoadRuns();
|
||||||
}
|
}
|
||||||
@@ -56,6 +75,8 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
.OrderBy(a => a.AccountId)
|
.OrderBy(a => a.AccountId)
|
||||||
.Select(a => new AccountItem(a.AccountId, string.IsNullOrEmpty(a.Name) ? $"#{a.AccountId}" : $"{a.Name} (#{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 })
|
foreach (var combo in new[] { cbLedgerAccount, cbStatusAccount })
|
||||||
{
|
{
|
||||||
combo.ComboBox.DisplayMember = nameof(AccountItem.Label);
|
combo.ComboBox.DisplayMember = nameof(AccountItem.Label);
|
||||||
@@ -63,8 +84,80 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int? SelectedAccountId(ToolStripComboBox combo) =>
|
private static int? IdOf(object? item) => item is AccountItem it ? it.Id : null;
|
||||||
combo.SelectedItem 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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------- Ledger ----------------
|
// ---------------- Ledger ----------------
|
||||||
|
|
||||||
@@ -73,7 +166,7 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
if (_ledger == null) return;
|
if (_ledger == null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var rows = _ledger.Query(SelectedAccountId(cbLedgerAccount), null, null, 1000);
|
var rows = _ledger.Query(IdOf(cbLedgerAccount.SelectedItem), null, null, 1000);
|
||||||
dgvLedger.DataSource = rows.Select(e => new LedgerRow(e)).ToList();
|
dgvLedger.DataSource = rows.Select(e => new LedgerRow(e)).ToList();
|
||||||
lblAccStatus.Text = rows.Count == 0
|
lblAccStatus.Text = rows.Count == 0
|
||||||
? "Noch keine Buchungen. (Ingest-Quellen sind offline bis zur Live-Anbindung im Zielland.)"
|
? "Noch keine Buchungen. (Ingest-Quellen sind offline bis zur Live-Anbindung im Zielland.)"
|
||||||
@@ -92,7 +185,7 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
if (_runs == null) return;
|
if (_runs == null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
dgvRuns.DataSource = _runs.GetRecent(SelectedAccountId(cbStatusAccount), 100);
|
dgvRuns.DataSource = _runs.GetRecent(IdOf(cbStatusAccount.SelectedItem), 100);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -112,6 +205,7 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
lblAccStatus.Text = "Abruf abgeschlossen.";
|
lblAccStatus.Text = "Abruf abgeschlossen.";
|
||||||
LoadRuns();
|
LoadRuns();
|
||||||
LoadLedger();
|
LoadLedger();
|
||||||
|
Recalculate();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -126,7 +220,38 @@ namespace PolyTrader.Modules.Accounting.Ui
|
|||||||
|
|
||||||
private sealed record AccountItem(int? Id, string Label);
|
private sealed record AccountItem(int? Id, string Label);
|
||||||
|
|
||||||
/// <summary>Anzeige-Zeile fürs Ledger-Grid (kompakte, lesbare Spalten).</summary>
|
/// <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
|
private sealed class LedgerRow
|
||||||
{
|
{
|
||||||
public LedgerRow(Models.LedgerEntry e)
|
public LedgerRow(Models.LedgerEntry e)
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using PolyTrader.Modules.Accounting.Logic;
|
||||||
|
using PolyTrader.Modules.Accounting.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PolyTrader.Tests
|
||||||
|
{
|
||||||
|
/// <summary>Sicherheitsnetz für die neutrale Abrechnung (A-2): Periodenaggregation, FX, CSV.</summary>
|
||||||
|
public class AccountingReportTests
|
||||||
|
{
|
||||||
|
private static readonly DateTime Jun = new(2026, 6, 15, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
private static readonly DateTime Jul = new(2026, 7, 10, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
private static LedgerEntry E(LedgerEventType type, decimal net, DateTime ts,
|
||||||
|
decimal gross = 0m, decimal fee = 0m) => new()
|
||||||
|
{
|
||||||
|
AccountId = 1, EventType = type, Timestamp = ts, NetUsdc = net,
|
||||||
|
GrossUsdc = gross == 0m ? Math.Abs(net) : gross, FeeUsdc = fee
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------- AccountingEngine ----------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Statement_aggregates_period_and_carries_opening_balance()
|
||||||
|
{
|
||||||
|
var entries = new List<LedgerEntry>
|
||||||
|
{
|
||||||
|
// VOR dem Zeitraum → nur Anfangssaldo
|
||||||
|
E(LedgerEventType.Deposit, 1000m, new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc), gross: 1000m),
|
||||||
|
// im Zeitraum (Juli)
|
||||||
|
E(LedgerEventType.TradeBuy, -100m, Jul, gross: 100m, fee: 1m),
|
||||||
|
E(LedgerEventType.TradeSell, 130m, Jul, gross: 130m, fee: 1m),
|
||||||
|
E(LedgerEventType.Reward, 5m, Jul, gross: 5m),
|
||||||
|
E(LedgerEventType.Deposit, 200m, Jul, gross: 200m),
|
||||||
|
E(LedgerEventType.Withdrawal, -50m, Jul, gross: 50m),
|
||||||
|
};
|
||||||
|
|
||||||
|
var from = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var to = new DateTime(2026, 7, 31, 23, 59, 59, DateTimeKind.Utc);
|
||||||
|
var s = AccountingEngine.BuildStatement(entries, from, to, 1);
|
||||||
|
|
||||||
|
Assert.Equal(1000m, s.OpeningBalanceUsdc);
|
||||||
|
Assert.Equal(200m, s.Deposits);
|
||||||
|
Assert.Equal(50m, s.Withdrawals);
|
||||||
|
Assert.Equal(230m, s.TradeVolume); // 100 + 130
|
||||||
|
Assert.Equal(5m, s.Rewards);
|
||||||
|
Assert.Equal(2m, s.Fees);
|
||||||
|
Assert.Equal(35m, s.NetTradingResultUsdc); // -100 +130 +5 (exkl. Ein-/Auszahlungen)
|
||||||
|
Assert.Equal(2, s.TradeCount);
|
||||||
|
// Invariante: Endsaldo − Anfangssaldo = Handelsergebnis + Einzahlungen − Auszahlungen
|
||||||
|
Assert.Equal(1185m, s.ClosingBalanceUsdc); // 1000 + 35 + 200 - 50
|
||||||
|
Assert.Equal(s.NetTradingResultUsdc + s.Deposits - s.Withdrawals, s.BalanceChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Monthly_breakdown_chains_opening_balances()
|
||||||
|
{
|
||||||
|
var entries = new List<LedgerEntry>
|
||||||
|
{
|
||||||
|
E(LedgerEventType.Deposit, 100m, Jun, gross: 100m), // Juni
|
||||||
|
E(LedgerEventType.TradeSell, 40m, Jul, gross: 40m), // Juli
|
||||||
|
};
|
||||||
|
var from = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var to = new DateTime(2026, 7, 31, 23, 59, 59, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
var months = AccountingEngine.BuildMonthlyBreakdown(entries, from, to, 1);
|
||||||
|
|
||||||
|
Assert.Equal(2, months.Count);
|
||||||
|
Assert.Equal(0m, months[0].OpeningBalanceUsdc); // Juni startet bei 0
|
||||||
|
Assert.Equal(100m, months[0].ClosingBalanceUsdc);
|
||||||
|
Assert.Equal(100m, months[1].OpeningBalanceUsdc); // Juli erbt Juni-Endsaldo
|
||||||
|
Assert.Equal(140m, months[1].ClosingBalanceUsdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- FxConverter ----------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fx_conversions_usdc_usd_eur()
|
||||||
|
{
|
||||||
|
Assert.Equal(100m, FxConverter.UsdcToUsd(100m, 1.0m));
|
||||||
|
Assert.Equal(92m, FxConverter.UsdToEur(100m, 0.92m));
|
||||||
|
Assert.Equal(92m, FxConverter.UsdcToEur(100m, 0.92m, 1.0m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fx_nearest_on_or_before_picks_latest_valid_rate()
|
||||||
|
{
|
||||||
|
var rates = new List<FxRate>
|
||||||
|
{
|
||||||
|
new() { Date = new DateTime(2026, 7, 3), UsdToEur = 0.90m },
|
||||||
|
new() { Date = new DateTime(2026, 7, 5), UsdToEur = 0.92m }, // Freitag
|
||||||
|
};
|
||||||
|
// Sonntag 2026-07-05 gibt es keinen Kurs → letzter gültiger (Freitag).
|
||||||
|
var pick = FxConverter.NearestOnOrBefore(rates, new DateTime(2026, 7, 6));
|
||||||
|
Assert.NotNull(pick);
|
||||||
|
Assert.Equal(0.92m, pick!.UsdToEur);
|
||||||
|
|
||||||
|
Assert.Null(FxConverter.NearestOnOrBefore(rates, new DateTime(2026, 7, 1))); // vor allen Kursen
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- CsvExporter ----------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Csv_ledger_has_header_and_escapes_commas()
|
||||||
|
{
|
||||||
|
var entries = new List<LedgerEntry>
|
||||||
|
{
|
||||||
|
new() { AccountId = 1, EventType = LedgerEventType.TradeBuy, Timestamp = Jul,
|
||||||
|
MarketSlug = "will-x-win, really?", Side = "BUY", Size = 10m, PriceUsdc = 0.5m,
|
||||||
|
GrossUsdc = 5m, FeeUsdc = 0.1m, NetUsdc = -5.1m, TxHash = "0xabc", Source = "polymarket-activity" }
|
||||||
|
};
|
||||||
|
string csv = CsvExporter.Ledger(entries);
|
||||||
|
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
Assert.StartsWith("Timestamp,AccountId,EventType", lines[0]);
|
||||||
|
Assert.Contains("\"will-x-win, really?\"", lines[1]); // Komma-Feld gequotet
|
||||||
|
Assert.Contains("-5.1", lines[1]); // kulturinvariant (Punkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Csv_statement_lists_key_figures()
|
||||||
|
{
|
||||||
|
var s = new PeriodStatement(1, Jul, Jul, 100m, 135m, 0m, 0m, 230m, 0m, 5m, 2m, 35m, 2, 3);
|
||||||
|
string csv = CsvExporter.Statement(s);
|
||||||
|
Assert.Contains("Endsaldo,135", csv);
|
||||||
|
Assert.Contains("Netto-Handelsergebnis (Cash),35", csv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user