Files
IBKRTrader/Core/IBKR/IBKRGatewayService.cs
T
RichardandClaude Opus 4.8 ebeb035e92 Initial commit: IBKRTrader
.NET WinForms-Anwendung (Core, Modules/CongressTrading, UI).
Enthaelt .gitignore und settings.example.json als Konfigurationsvorlage.
Echte settings.json mit Zugangsdaten ist bewusst ausgeschlossen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 18:19:47 +02:00

275 lines
9.7 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 System.Net.Http.Json;
using System.Text.Json;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Settings;
namespace IBKRTrader.Core.IBKR;
/// <summary>
/// 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
/// </summary>
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 ──────────────────────────────────────────────────────
/// <summary>
/// Prüft den Authentifizierungsstatus der Gateway-Session.
/// </summary>
public async Task<IBKRAuthStatus?> 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<IBKRAuthStatus>(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;
}
}
/// <summary>
/// Hält die Session am Leben (Ping). Sollte alle ~60 Sekunden aufgerufen werden.
/// </summary>
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}");
}
}
/// <summary>
/// Initialisiert die Brokerage-Session über den Gateway.
/// </summary>
public async Task<bool> 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 ─────────────────────────────────────────────────────
/// <summary>
/// Sucht einen Contract anhand des Symbols.
/// POST /iserver/secdef/search
/// Body: {"symbol":"AAPL"}
/// </summary>
public async Task<List<IBKRContractSearchResult>?> 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<List<IBKRContractSearchResult>>(JsonOpts, ct);
}
catch (Exception ex)
{
_logger.Error("IBKR", $"Contract-Suche für '{symbol}' fehlgeschlagen: {ex.Message}", ex);
return null;
}
}
/// <summary>
/// Ruft detaillierte Contract-Informationen ab.
/// GET /iserver/contract/{conid}/info
/// </summary>
public async Task<IBKRContractInfo?> 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<IBKRContractInfo>(JsonOpts, ct);
}
catch (Exception ex)
{
_logger.Error("IBKR", $"Contract-Info für conid {conid} fehlgeschlagen: {ex.Message}", ex);
return null;
}
}
/// <summary>
/// Sucht Aktien-Contracts nach Symbolen.
/// GET /trsrv/stocks?symbols=AAPL,MSFT
/// </summary>
public async Task<Dictionary<string, List<IBKRStockContract>>?> 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<Dictionary<string, List<IBKRStockContract>>>(JsonOpts, ct);
}
catch (Exception ex)
{
_logger.Error("IBKR", $"Stock-Suche fehlgeschlagen: {ex.Message}", ex);
return null;
}
}
// ─── Historical Market Data ──────────────────────────────────────────────
/// <summary>
/// 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
/// </summary>
public async Task<IBKRHistoricalDataResponse?> 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<IBKRHistoricalDataResponse>(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;
}
}