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();
}