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>
This commit is contained in:
Richard
2026-07-26 18:19:47 +02:00
co-authored by Claude Opus 4.8
commit ebeb035e92
47 changed files with 4527 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
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;
}
}
+152
View File
@@ -0,0 +1,152 @@
using Dapper;
using IBKRTrader.Core.Database;
using IBKRTrader.Core.Logging;
namespace IBKRTrader.Core.IBKR;
/// <summary>
/// Datenzugriffsschicht für die core_ibkr_xxx-Tabellen.
/// Alle IBKR-Marktdaten-Operationen laufen über diese Klasse.
/// </summary>
public class IBKRMarketDataRepository
{
private readonly DatabaseService _db;
private readonly LoggingService _logger;
public IBKRMarketDataRepository(DatabaseService db, LoggingService logger)
{
_db = db;
_logger = logger;
}
// ─── Instruments ─────────────────────────────────────────────────────────
/// <summary>
/// Legt ein neues Instrument an oder aktualisiert ein bestehendes (UPSERT via conid).
/// Gibt die Instrument-ID zurück.
/// </summary>
public async Task<long> UpsertInstrumentAsync(IBKRInstrument instr)
{
const string sql = @"
INSERT INTO `core_ibkr_instruments`
(`ibkr_conid`, `symbol`, `sec_type`, `exchange`, `primary_exchange`,
`currency`, `company_name`, `isin`, `sector`, `industry`,
`description`, `active`, `last_fetched`)
VALUES
(@IbkrConid, @Symbol, @SecType, @Exchange, @PrimaryExchange,
@Currency, @CompanyName, @Isin, @Sector, @Industry,
@Description, @Active, @LastFetched)
ON DUPLICATE KEY UPDATE
`symbol` = VALUES(`symbol`),
`sec_type` = VALUES(`sec_type`),
`exchange` = VALUES(`exchange`),
`primary_exchange` = VALUES(`primary_exchange`),
`currency` = VALUES(`currency`),
`company_name` = VALUES(`company_name`),
`isin` = VALUES(`isin`),
`sector` = VALUES(`sector`),
`industry` = VALUES(`industry`),
`description` = VALUES(`description`),
`last_fetched` = VALUES(`last_fetched`);
SELECT `id` FROM `core_ibkr_instruments` WHERE `ibkr_conid` = @IbkrConid;";
await using var conn = _db.CreateConnection();
return await conn.ExecuteScalarAsync<long>(sql, instr);
}
/// <summary>Gibt alle aktiven Instrumente zurück.</summary>
public Task<IEnumerable<IBKRInstrument>> GetAllActiveInstrumentsAsync()
=> _db.QueryAsync<IBKRInstrument>(
"SELECT * FROM `core_ibkr_instruments` WHERE `active` = 1 ORDER BY `symbol`");
/// <summary>Findet ein Instrument anhand seiner IBKR ConID.</summary>
public Task<IBKRInstrument?> GetInstrumentByConidAsync(long conid)
=> _db.QueryFirstOrDefaultAsync<IBKRInstrument>(
"SELECT * FROM `core_ibkr_instruments` WHERE `ibkr_conid` = @conid",
new { conid });
/// <summary>Gibt die Anzahl aktiver Instrumente zurück.</summary>
public Task<int> GetActiveInstrumentCountAsync()
=> _db.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM `core_ibkr_instruments` WHERE `active` = 1");
// ─── Market Data ─────────────────────────────────────────────────────────
/// <summary>
/// Fügt Marktdaten-Balken via UPSERT ein (ON DUPLICATE KEY UPDATE).
/// </summary>
public async Task UpsertMarketDataBatchAsync(IEnumerable<IBKRMarketBar> bars)
{
const string sql = @"
INSERT INTO `core_ibkr_market_data`
(`instrument_id`, `bar_size`, `timestamp`,
`open`, `high`, `low`, `close`, `volume`, `wap`, `bar_count`)
VALUES
(@InstrumentId, @BarSize, @Timestamp,
@Open, @High, @Low, @Close, @Volume, @Wap, @BarCount)
ON DUPLICATE KEY UPDATE
`open` = VALUES(`open`),
`high` = VALUES(`high`),
`low` = VALUES(`low`),
`close` = VALUES(`close`),
`volume` = VALUES(`volume`),
`wap` = VALUES(`wap`),
`bar_count` = VALUES(`bar_count`)";
await using var conn = _db.CreateConnection();
await conn.OpenAsync();
await conn.ExecuteAsync(sql, bars);
}
/// <summary>Gibt den neuesten Timestamp für ein Instrument zurück.</summary>
public Task<DateTime?> GetLatestBarTimestampAsync(long instrumentId, string barSize = "daily")
=> _db.QueryFirstOrDefaultAsync<DateTime?>(
@"SELECT MAX(`timestamp`) FROM `core_ibkr_market_data`
WHERE `instrument_id` = @instrumentId AND `bar_size` = @barSize",
new { instrumentId, barSize });
/// <summary>Gibt die Anzahl Bars für ein Instrument zurück.</summary>
public Task<int> GetBarCountAsync(long instrumentId, string barSize = "daily")
=> _db.ExecuteScalarAsync<int>(
@"SELECT COUNT(*) FROM `core_ibkr_market_data`
WHERE `instrument_id` = @instrumentId AND `bar_size` = @barSize",
new { instrumentId, barSize });
// ─── External Identifiers ────────────────────────────────────────────────
/// <summary>
/// Erstellt oder ignoriert ein External-Identifier-Mapping (IGNORE bei Duplikat).
/// </summary>
public Task UpsertExternalIdentifierAsync(long instrumentId, string source, string ticker)
=> _db.ExecuteAsync(@"
INSERT IGNORE INTO `core_ibkr_external_identifiers`
(`instrument_id`, `source`, `ticker`)
VALUES (@instrumentId, @source, @ticker)",
new { instrumentId, source, ticker });
/// <summary>
/// Findet ein Instrument anhand eines externen Tickers (z.B. aus ct_trade).
/// </summary>
public Task<IBKRInstrument?> FindInstrumentByExternalTickerAsync(string source, string ticker)
=> _db.QueryFirstOrDefaultAsync<IBKRInstrument>(@"
SELECT i.* FROM `core_ibkr_instruments` i
INNER JOIN `core_ibkr_external_identifiers` e ON e.`instrument_id` = i.`id`
WHERE e.`source` = @source AND e.`ticker` = @ticker
LIMIT 1",
new { source, ticker });
/// <summary>
/// Findet alle einzigartigen Ticker aus ct_trade, die noch kein IBKR-Mapping haben.
/// </summary>
public Task<IEnumerable<string>> GetUnmappedTickersFromCongressTradesAsync()
=> _db.QueryAsync<string>(@"
SELECT DISTINCT t.`ticker`
FROM `ct_trade` t
WHERE t.`ticker` IS NOT NULL
AND t.`ticker` != ''
AND NOT EXISTS (
SELECT 1 FROM `core_ibkr_external_identifiers` e
WHERE e.`source` = 'capitoltrades' AND e.`ticker` = t.`ticker`
)
ORDER BY t.`ticker`");
}
+336
View File
@@ -0,0 +1,336 @@
using System.Text.Json.Serialization;
namespace IBKRTrader.Core.IBKR;
// ─── DB Entities ─────────────────────────────────────────────────────────────
/// <summary>
/// Repräsentiert ein IBKR-Instrument in core_ibkr_instruments.
/// ibkr_conid ist der zentrale, eindeutige IBKR-Schlüssel.
/// </summary>
public class IBKRInstrument
{
public long Id { get; set; }
public long IbkrConid { get; set; }
public string Symbol { get; set; } = "";
public string SecType { get; set; } = "STK";
public string Exchange { get; set; } = "SMART";
public string? PrimaryExchange { get; set; }
public string Currency { get; set; } = "USD";
public string? CompanyName { get; set; }
public string? Isin { get; set; }
public string? Sector { get; set; }
public string? Industry { get; set; }
public string? Description { get; set; }
public bool Active { get; set; } = true;
public DateTime? LastFetched { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
/// <summary>
/// Repräsentiert einen OHLCV-Balken in core_ibkr_market_data.
/// </summary>
public class IBKRMarketBar
{
public long InstrumentId { get; set; }
public string BarSize { get; set; } = "daily";
public DateTime Timestamp { get; set; }
public decimal Open { get; set; }
public decimal High { get; set; }
public decimal Low { get; set; }
public decimal Close { get; set; }
public long Volume { get; set; }
public decimal? Wap { get; set; }
public int? BarCount { get; set; }
}
/// <summary>
/// Externes Ticker-Mapping in core_ibkr_external_identifiers.
/// </summary>
public class IBKRExternalIdentifier
{
public long Id { get; set; }
public long InstrumentId { get; set; }
public string Source { get; set; } = "";
public string Ticker { get; set; } = "";
}
// ─── API Response DTOs ───────────────────────────────────────────────────────
/// <summary>
/// Response von GET /iserver/auth/status
/// </summary>
public class IBKRAuthStatus
{
[JsonPropertyName("authenticated")]
public bool Authenticated { get; set; }
[JsonPropertyName("competing")]
public bool Competing { get; set; }
[JsonPropertyName("connected")]
public bool Connected { get; set; }
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("fail")]
public string? Fail { get; set; }
}
/// <summary>
/// Einzelnes Ergebnis der Contract-Suche (POST /iserver/secdef/search).
/// </summary>
public class IBKRContractSearchResult
{
[JsonPropertyName("conid")]
public long ConId { get; set; }
[JsonPropertyName("companyHeader")]
public string? CompanyHeader { get; set; }
[JsonPropertyName("companyName")]
public string? CompanyName { get; set; }
[JsonPropertyName("symbol")]
public string? Symbol { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("restricted")]
public string? Restricted { get; set; }
[JsonPropertyName("fop")]
public string? Fop { get; set; }
[JsonPropertyName("opt")]
public string? Opt { get; set; }
[JsonPropertyName("war")]
public string? War { get; set; }
[JsonPropertyName("sections")]
public List<IBKRContractSection>? Sections { get; set; }
}
/// <summary>
/// Sections innerhalb eines Contract-Suchergebnisses.
/// </summary>
public class IBKRContractSection
{
[JsonPropertyName("secType")]
public string? SecType { get; set; }
[JsonPropertyName("months")]
public string? Months { get; set; }
[JsonPropertyName("symbol")]
public string? Symbol { get; set; }
[JsonPropertyName("exchange")]
public string? Exchange { get; set; }
[JsonPropertyName("legStr")]
public string? LegStr { get; set; }
}
/// <summary>
/// Response von GET /iserver/contract/{conid}/info
/// </summary>
public class IBKRContractInfo
{
[JsonPropertyName("cfi_code")]
public string? CfiCode { get; set; }
[JsonPropertyName("symbol")]
public string? Symbol { get; set; }
[JsonPropertyName("cusip")]
public string? Cusip { get; set; }
[JsonPropertyName("expiry_full")]
public string? ExpiryFull { get; set; }
[JsonPropertyName("con_id")]
public long ConId { get; set; }
[JsonPropertyName("maturity_date")]
public string? MaturityDate { get; set; }
[JsonPropertyName("industry")]
public string? Industry { get; set; }
[JsonPropertyName("instrument_type")]
public string? InstrumentType { get; set; }
[JsonPropertyName("trading_class")]
public string? TradingClass { get; set; }
[JsonPropertyName("valid_exchanges")]
public string? ValidExchanges { get; set; }
[JsonPropertyName("allow_sell_long")]
public bool? AllowSellLong { get; set; }
[JsonPropertyName("is_zero_commission_security")]
public bool? IsZeroCommissionSecurity { get; set; }
[JsonPropertyName("local_symbol")]
public string? LocalSymbol { get; set; }
[JsonPropertyName("classifier")]
public string? Classifier { get; set; }
[JsonPropertyName("currency")]
public string? Currency { get; set; }
[JsonPropertyName("text")]
public string? Text { get; set; }
[JsonPropertyName("underlying_con_id")]
public long? UnderlyingConId { get; set; }
[JsonPropertyName("r_t_h")]
public bool? Rth { get; set; }
[JsonPropertyName("company_name")]
public string? CompanyName { get; set; }
[JsonPropertyName("smart_available")]
public bool? SmartAvailable { get; set; }
[JsonPropertyName("exchange")]
public string? Exchange { get; set; }
[JsonPropertyName("listing_exchange")]
public string? ListingExchange { get; set; }
[JsonPropertyName("category")]
public string? Category { get; set; }
[JsonPropertyName("sector")]
public string? Sector { get; set; }
}
/// <summary>
/// Response von GET /iserver/marketdata/history
/// </summary>
public class IBKRHistoricalDataResponse
{
[JsonPropertyName("symbol")]
public string? Symbol { get; set; }
[JsonPropertyName("text")]
public string? Text { get; set; }
[JsonPropertyName("priceFactor")]
public int? PriceFactor { get; set; }
[JsonPropertyName("startTime")]
public string? StartTime { get; set; }
[JsonPropertyName("high")]
public string? High { get; set; }
[JsonPropertyName("low")]
public string? Low { get; set; }
[JsonPropertyName("timePeriod")]
public string? TimePeriod { get; set; }
[JsonPropertyName("barLength")]
public int? BarLength { get; set; }
[JsonPropertyName("mdAvailability")]
public string? MdAvailability { get; set; }
[JsonPropertyName("mktDataDelay")]
public int? MktDataDelay { get; set; }
[JsonPropertyName("outsideRth")]
public bool? OutsideRth { get; set; }
[JsonPropertyName("tradingDayDuration")]
public int? TradingDayDuration { get; set; }
[JsonPropertyName("volumeFactor")]
public int? VolumeFactor { get; set; }
[JsonPropertyName("priceDisplayRule")]
public int? PriceDisplayRule { get; set; }
[JsonPropertyName("priceDisplayValue")]
public string? PriceDisplayValue { get; set; }
[JsonPropertyName("negativeCapable")]
public bool? NegativeCapable { get; set; }
[JsonPropertyName("messageVersion")]
public int? MessageVersion { get; set; }
[JsonPropertyName("data")]
public List<IBKRHistoricalBar>? Data { get; set; }
[JsonPropertyName("points")]
public int? Points { get; set; }
[JsonPropertyName("travelTime")]
public int? TravelTime { get; set; }
}
/// <summary>
/// Einzelner OHLCV-Balken aus der IBKR Historical Data Response.
/// </summary>
public class IBKRHistoricalBar
{
[JsonPropertyName("o")]
public decimal Open { get; set; }
[JsonPropertyName("c")]
public decimal Close { get; set; }
[JsonPropertyName("h")]
public decimal High { get; set; }
[JsonPropertyName("l")]
public decimal Low { get; set; }
[JsonPropertyName("v")]
public long Volume { get; set; }
[JsonPropertyName("t")]
public long Timestamp { get; set; } // Unix timestamp in ms
}
/// <summary>
/// Response von GET /trsrv/stocks?symbols=...
/// Die API gibt ein Dictionary zurück: { "AAPL": [{ ... }] }
/// </summary>
public class IBKRStockContract
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("chineseName")]
public string? ChineseName { get; set; }
[JsonPropertyName("assetClass")]
public string? AssetClass { get; set; }
[JsonPropertyName("contracts")]
public List<IBKRStockContractEntry>? Contracts { get; set; }
}
public class IBKRStockContractEntry
{
[JsonPropertyName("conid")]
public long ConId { get; set; }
[JsonPropertyName("exchange")]
public string? Exchange { get; set; }
[JsonPropertyName("isUS")]
public bool? IsUS { get; set; }
}