using IBKRTrader.Core.Logging; using IBKRTrader.Core.Settings; namespace IBKRTrader.Core.Trading.Ibkr; /// /// Echter Broker-Adapter über die TWS API (IB Gateway bzw. TWS, Socket-Verbindung). /// /// Wird nur registriert, wenn IBKRSettings.UseTwsApi gesetzt ist – sonst bleibt der /// aktiv. Der globale Handelsschalter /// (TradingSettings.TradingEnabled) bleibt davon unberührt: ohne ihn platziert der /// 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. /// 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 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 GetAccountStateAsync(CancellationToken ct = default) { if (!await IsReadyAsync(ct).ConfigureAwait(false)) return Empty; return await _connection!.RequestAccountAsync(_requestTimeout, ct).ConfigureAwait(false) ?? Empty; } public async Task 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> GetPositionsAsync(CancellationToken ct = default) { if (!await IsReadyAsync(ct).ConfigureAwait(false)) return Array.Empty(); return await _connection!.RequestPositionsAsync(_requestTimeout, ct).ConfigureAwait(false); } public async Task> GetExecutionsAsync(DateTime? since = null, CancellationToken ct = default) { if (!await IsReadyAsync(ct).ConfigureAwait(false)) return Array.Empty(); return await _connection!.RequestExecutionsAsync(since, _requestTimeout, ct).ConfigureAwait(false); } private static AccountState Empty => new(0m, 0m); private async Task IsReadyAsync(CancellationToken ct) { if (_connection is null) return false; return await _connection.EnsureConnectedAsync(ct).ConfigureAwait(false); } public void Dispose() => _connection?.Dispose(); }