Files
IBKRTrader/src/IBKRTrader.Core/Trading/Ibkr/IbkrBrokerClient.cs
T
RichardandClaude Opus 5 b96a20788c 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>
2026-08-06 10:51:51 +02:00

119 lines
5.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Settings;
namespace IBKRTrader.Core.Trading.Ibkr;
/// <summary>
/// Echter Broker-Adapter über die TWS API (IB Gateway bzw. TWS, Socket-Verbindung).
///
/// Wird nur registriert, wenn <c>IBKRSettings.UseTwsApi</c> gesetzt ist sonst bleibt der
/// <see cref="NullBrokerClient"/> aktiv. Der globale Handelsschalter
/// (<c>TradingSettings.TradingEnabled</c>) bleibt davon unberührt: ohne ihn platziert der
/// <see cref="ExecutionService"/> gar keine Order, egal welcher Broker registriert ist.
///
/// Fehler werden nie geworfen, sondern in leere Ergebnisse übersetzt (kein Kurs, Konto 0,
/// 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, IBrokerPortfolioReader, IDisposable
{
private const string LogModule = "IBKR";
private readonly LoggingService _logger;
private readonly IbkrConnection? _connection;
private readonly TimeSpan _requestTimeout;
private readonly TimeSpan _orderTimeout;
public IbkrBrokerClient(SettingsService settings, LoggingService logger)
{
_logger = logger;
var ibkr = settings.Settings.IBKR;
var mode = settings.Settings.Trading.ParsedMode;
_requestTimeout = TimeSpan.FromSeconds(Math.Max(1, ibkr.RequestTimeoutSeconds));
_orderTimeout = TimeSpan.FromSeconds(Math.Max(1, ibkr.OrderTimeoutSeconds));
// Ein Paper-Modus auf dem Live-Port würde echtes Geld bewegen: dann lieber gar nicht
// verbinden, statt auf dem falschen Konto zu handeln.
var mismatch = IbkrMapping.ValidatePort(ibkr.Port, mode);
if (mismatch is not null)
{
_logger.Error(LogModule,
$"{mismatch} Broker bleibt inaktiv Port oder Handelsmodus in den Einstellungen korrigieren " +
$"(Paper: {IbkrMapping.GatewayPaperPort}, Live: {IbkrMapping.GatewayLivePort}).");
return;
}
_connection = new IbkrConnection(
logger, ibkr.Host, ibkr.Port, ibkr.ClientId, ibkr.MarketDataType,
TimeSpan.FromSeconds(Math.Max(1, ibkr.ConnectTimeoutSeconds)));
_logger.Info(LogModule,
$"TWS-Broker aktiv: {ibkr.Host}:{ibkr.Port} (Client {ibkr.ClientId}), Modus {mode}.");
}
public async Task<Quote?> GetQuoteAsync(string symbol, CancellationToken ct = default)
{
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return null;
var contract = await _connection!.ResolveContractAsync(symbol, _requestTimeout, ct).ConfigureAwait(false);
if (contract is null) return null;
return await _connection.RequestQuoteAsync(contract, symbol, _requestTimeout, ct).ConfigureAwait(false);
}
public async Task<AccountState> GetAccountStateAsync(CancellationToken ct = default)
{
if (!await IsReadyAsync(ct).ConfigureAwait(false)) return Empty;
return await _connection!.RequestAccountAsync(_requestTimeout, ct).ConfigureAwait(false) ?? Empty;
}
public async Task<OrderResult> PlaceOrderAsync(OrderRequest request, CancellationToken ct = default)
{
if (!await IsReadyAsync(ct).ConfigureAwait(false))
return OrderResult.Fail("Keine Verbindung zur TWS bzw. zum IB Gateway.");
var contract = await _connection!.ResolveContractAsync(request.Symbol, _requestTimeout, ct).ConfigureAwait(false);
if (contract is null)
return OrderResult.Fail($"Kontrakt für {request.Symbol} nicht auflösbar Order nicht platziert.");
var result = await _connection.PlaceOrderAsync(request, contract, _orderTimeout, ct).ConfigureAwait(false);
if (result.Success)
_logger.Info(LogModule,
$"Order {result.OrderId} ausgeführt: {request.Side} {result.FilledQuantity}x {request.Symbol} " +
$"@ {result.AvgFillPrice:F2}.");
else
_logger.Error(LogModule, result.Error ?? "Order fehlgeschlagen.");
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)
{
if (_connection is null) return false;
return await _connection.EnsureConnectedAsync(ct).ConfigureAwait(false);
}
public void Dispose() => _connection?.Dispose();
}