R10: Lesender Bestandsabgleich (IBrokerPortfolioReader) + Datenlage-Konzepte

Eigener Seam neben IBrokerClient: Wer handelt, braucht ihn nicht; wer die
eigene Buchfuehrung gegen den Broker abstimmt, braucht nur ihn. Zuteilung
und Verfall aendern Positionen ohne Order von uns - ohne Abgleich laeuft
das Managementbuch zwangslaeufig auseinander.

- IBrokerPortfolioReader mit GetPositionsAsync/GetExecutionsAsync;
  implementiert von IbkrBrokerClient und NullBrokerClient (DI registriert
  beide Rollen auf derselben Instanz).
- IbkrConnection: reqAccountUpdates statt reqPositions (nur dieser Weg
  liefert Marktwert und unrealisierten G/V), reqExecutions inkl. Zuordnung
  der verspaetet eintreffenden commissionReport-Callbacks ueber die ExecId.
- BrokerPosition/BrokerExecution als Broker-Wahrheit neben Position;
  IbkrMapping: ParseSide, ParseExecutionTime, FormatExecutionFilterTime
  (UTC wegen TWS-Warnung 2174) - mit Unit-Tests.
- Verifiziert gegen Paper-Konto DUR371528: 2 Positionen, 2 Ausfuehrungen
  inkl. Kommissionen.

Doku: Kapital- und Buchmodell (drei Wahrheiten, Kapitalzuteilung),
KONZEPT-Datenlage-und-Strategien (gemessen, was die API auf diesem Konto
liefert). Options-Wheel: Greeks bei verzoegerten Daten funktionieren
(Feld 83); Earnings-Termine sind ueber die TWS API nicht erreichbar
(Fehler 10358) - Behelf ueber IV-Filter statt Fremddatenquelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-06 10:51:51 +02:00
co-authored by Claude Opus 5
parent 80afcd49c1
commit b96a20788c
13 changed files with 1039 additions and 20 deletions
@@ -15,7 +15,7 @@ namespace IBKRTrader.Core.Trading.Ibkr;
/// fehlgeschlagene Order). Ein Konto mit Wert 0 lässt die Risikoprüfung jedes Signal ablehnen
/// die sichere Richtung, wenn der Broker nicht erreichbar ist.
/// </summary>
public sealed class IbkrBrokerClient : IBrokerClient, IDisposable
public sealed class IbkrBrokerClient : IBrokerClient, IBrokerPortfolioReader, IDisposable
{
private const string LogModule = "IBKR";
@@ -91,6 +91,21 @@ public sealed class IbkrBrokerClient : IBrokerClient, IDisposable
return result;
}
public async Task<IReadOnlyList<BrokerPosition>> GetPositionsAsync(CancellationToken ct = default)
{
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return Array.Empty<BrokerPosition>();
return await _connection!.RequestPositionsAsync(_requestTimeout, ct).ConfigureAwait(false);
}
public async Task<IReadOnlyList<BrokerExecution>> GetExecutionsAsync(DateTime? since = null,
CancellationToken ct = default)
{
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return Array.Empty<BrokerExecution>();
return await _connection!.RequestExecutionsAsync(since, _requestTimeout, ct).ConfigureAwait(false);
}
private static AccountState Empty => new(0m, 0m);
private async Task<bool> IsReadyAsync(CancellationToken ct)