using System.Net.Http.Json;
using System.Text.Json;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Settings;
namespace IBKRTrader.Core.IBKR;
///
/// HTTP-Client für die IBKR Client Portal Web API (Gateway).
/// Alle API-Aufrufe laufen über den lokalen Client Portal Gateway
/// (Standard: https://localhost:5000/v1/api).
///
/// Voraussetzungen:
/// - Client Portal Gateway muss laufen (Java-Prozess)
/// - Session muss per Browser + 2FA authentifiziert sein
///
public class IBKRGatewayService
{
private readonly SettingsService _settings;
private readonly LoggingService _logger;
private readonly HttpClient _http;
private readonly SemaphoreSlim _rateLimiter;
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNameCaseInsensitive = true
};
public bool IsConnected { get; private set; }
public IBKRGatewayService(SettingsService settings, LoggingService logger)
{
_settings = settings;
_logger = logger;
// HttpClient mit SSL-Bypass für localhost (Gateway hat kein signiertes Zertifikat)
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
};
var apiSettings = _settings.Settings.IBKRWebApi;
_http = new HttpClient(handler)
{
BaseAddress = new Uri(apiSettings.GatewayUrl.TrimEnd('/') + "/v1/api/"),
Timeout = TimeSpan.FromSeconds(30)
};
// Standard-Header setzen
_http.DefaultRequestHeaders.Add("User-Agent", "IBKRTrader/1.0");
_http.DefaultRequestHeaders.Add("Accept", "*/*");
// Rate-Limiter: maximal N gleichzeitige Anfragen (wird seriell mit Delay verwendet)
_rateLimiter = new SemaphoreSlim(1, 1);
}
// ─── Rate Limiting ───────────────────────────────────────────────────────
private async Task RateLimitAsync(CancellationToken ct)
{
await _rateLimiter.WaitAsync(ct);
try
{
var delayMs = 1000 / Math.Max(1, _settings.Settings.IBKRWebApi.MaxRequestsPerSecond);
await Task.Delay(delayMs, ct);
}
finally
{
_rateLimiter.Release();
}
}
// ─── Session / Auth ──────────────────────────────────────────────────────
///
/// Prüft den Authentifizierungsstatus der Gateway-Session.
///
public async Task CheckAuthStatusAsync(CancellationToken ct = default)
{
try
{
await RateLimitAsync(ct);
var response = await _http.PostAsync("iserver/auth/status", null, ct);
if (!response.IsSuccessStatusCode)
{
_logger.Warn("IBKR", $"Auth-Status-Check fehlgeschlagen: HTTP {(int)response.StatusCode}");
return null;
}
var result = await response.Content.ReadFromJsonAsync(JsonOpts, ct);
IsConnected = result?.Authenticated == true;
return result;
}
catch (HttpRequestException ex)
{
_logger.Warn("IBKR", $"Gateway nicht erreichbar: {ex.Message}");
IsConnected = false;
return null;
}
catch (TaskCanceledException)
{
return null;
}
}
///
/// Hält die Session am Leben (Ping). Sollte alle ~60 Sekunden aufgerufen werden.
///
public async Task TickleAsync(CancellationToken ct = default)
{
try
{
await RateLimitAsync(ct);
await _http.PostAsync("tickle", null, ct);
}
catch (Exception ex)
{
_logger.Warn("IBKR", $"Tickle fehlgeschlagen: {ex.Message}");
}
}
///
/// Initialisiert die Brokerage-Session über den Gateway.
///
public async Task InitBrokerageSessionAsync(CancellationToken ct = default)
{
try
{
await RateLimitAsync(ct);
var body = new StringContent("{\"publish\":true,\"compete\":true}",
System.Text.Encoding.UTF8, "application/json");
var response = await _http.PostAsync("iserver/auth/ssodh/init", body, ct);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
_logger.Warn("IBKR", $"Brokerage-Session-Init fehlgeschlagen: {ex.Message}");
return false;
}
}
// ─── Contract Search ─────────────────────────────────────────────────────
///
/// Sucht einen Contract anhand des Symbols.
/// POST /iserver/secdef/search
/// Body: {"symbol":"AAPL"}
///
public async Task?> SearchContractBySymbolAsync(
string symbol, CancellationToken ct = default)
{
try
{
await RateLimitAsync(ct);
var body = JsonContent.Create(new { symbol });
var response = await _http.PostAsync("iserver/secdef/search", body, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
_logger.Warn("IBKR", $"Contract-Suche für '{symbol}' fehlgeschlagen: HTTP {(int)response.StatusCode} – {errorBody}");
return null;
}
return await response.Content.ReadFromJsonAsync>(JsonOpts, ct);
}
catch (Exception ex)
{
_logger.Error("IBKR", $"Contract-Suche für '{symbol}' fehlgeschlagen: {ex.Message}", ex);
return null;
}
}
///
/// Ruft detaillierte Contract-Informationen ab.
/// GET /iserver/contract/{conid}/info
///
public async Task GetContractInfoAsync(
long conid, CancellationToken ct = default)
{
try
{
await RateLimitAsync(ct);
var response = await _http.GetAsync($"iserver/contract/{conid}/info", ct);
if (!response.IsSuccessStatusCode)
{
_logger.Warn("IBKR", $"Contract-Info für conid {conid} fehlgeschlagen: HTTP {(int)response.StatusCode}");
return null;
}
return await response.Content.ReadFromJsonAsync(JsonOpts, ct);
}
catch (Exception ex)
{
_logger.Error("IBKR", $"Contract-Info für conid {conid} fehlgeschlagen: {ex.Message}", ex);
return null;
}
}
///
/// Sucht Aktien-Contracts nach Symbolen.
/// GET /trsrv/stocks?symbols=AAPL,MSFT
///
public async Task>?> SearchStocksBySymbolAsync(
string symbols, CancellationToken ct = default)
{
try
{
await RateLimitAsync(ct);
var response = await _http.GetAsync($"trsrv/stocks?symbols={Uri.EscapeDataString(symbols)}", ct);
if (!response.IsSuccessStatusCode)
{
_logger.Warn("IBKR", $"Stock-Suche fehlgeschlagen: HTTP {(int)response.StatusCode}");
return null;
}
return await response.Content.ReadFromJsonAsync>>(JsonOpts, ct);
}
catch (Exception ex)
{
_logger.Error("IBKR", $"Stock-Suche fehlgeschlagen: {ex.Message}", ex);
return null;
}
}
// ─── Historical Market Data ──────────────────────────────────────────────
///
/// Ruft historische Marktdaten ab.
/// GET /iserver/marketdata/history?conid={conid}&period={period}&bar={bar}
///
/// period: 1d, 1w, 1m, 3m, 6m, 1y, 2y, 5y
/// bar: 1min, 5min, 15min, 30min, 1h, 1d, 1w, 1m
///
public async Task GetHistoricalDataAsync(
long conid, string period = "2y", string bar = "1d",
bool outsideRth = false, CancellationToken ct = default)
{
try
{
await RateLimitAsync(ct);
var url = $"iserver/marketdata/history?conid={conid}" +
$"&period={Uri.EscapeDataString(period)}" +
$"&bar={Uri.EscapeDataString(bar)}" +
$"&outsideRth={outsideRth.ToString().ToLower()}";
var response = await _http.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
_logger.Warn("IBKR", $"Historische Daten für conid {conid} fehlgeschlagen: HTTP {(int)response.StatusCode} – {errorBody}");
return null;
}
return await response.Content.ReadFromJsonAsync(JsonOpts, ct);
}
catch (Exception ex)
{
_logger.Error("IBKR", $"Historische Daten für conid {conid} fehlgeschlagen: {ex.Message}", ex);
return null;
}
}
// ─── Disconnect ──────────────────────────────────────────────────────────
public Task DisconnectAsync()
{
IsConnected = false;
return Task.CompletedTask;
}
}