using IBKRTrader.Core.Database; using IBKRTrader.Core.IBKR; using IBKRTrader.Core.Logging; using IBKRTrader.Core.Settings; namespace IBKRTrader.Core.Workers.BuiltIn; /// /// Synchronisiert IBKR-Instrumentenstammdaten alle 30 Tage. /// public class IBKRInstrumentSyncWorker : WorkerBase { public override string Name => "IBKR Instrument Sync"; public override string Module => "Core"; public override WorkerType Type => WorkerType.Worker; protected override TimeSpan? Interval => TimeSpan.FromMinutes(_settings.Settings.WorkerSettings.InstrumentSyncWorker.IntervalMinutes); private readonly SettingsService _settings; private readonly IBKRGatewayService _gateway; private readonly IBKRMarketDataRepository _repo; public IBKRInstrumentSyncWorker( LoggingService logger, DatabaseService db, SettingsService settings, IBKRGatewayService gateway, IBKRMarketDataRepository repo) : base(logger, db) { _settings = settings; _gateway = gateway; _repo = repo; } protected override async Task ExecuteAsync(CancellationToken ct) { if (!_settings.Settings.IBKRWebApi.Enabled) { Logger.Info(Module, "IBKR Web API deaktiviert – InstrumentSync übersprungen."); return; } var authStatus = await _gateway.CheckAuthStatusAsync(ct); if (authStatus?.Authenticated != true) { Logger.Warn(Module, "IBKR Gateway nicht authentifiziert – InstrumentSync übersprungen."); return; } await _gateway.TickleAsync(ct); await MapUnmappedTickersAsync(ct); await RefreshExistingInstrumentsAsync(ct); } private async Task MapUnmappedTickersAsync(CancellationToken ct) { var unmapped = (await _repo.GetUnmappedTickersFromCongressTradesAsync()).ToList(); if (unmapped.Count == 0) { Logger.Info(Module, "Keine neuen unmapped Ticker."); return; } Logger.Info(Module, $"Mappe {unmapped.Count} neue Ticker auf IBKR-Contracts..."); int mapped = 0, failed = 0; foreach (var ticker in unmapped) { if (ct.IsCancellationRequested) break; try { var results = await _gateway.SearchContractBySymbolAsync(ticker, ct); if (results == null || results.Count == 0) { Logger.Warn(Module, $"Kein Contract für: {ticker}"); failed++; continue; } long conid = results[0].ConId; var existing = await _repo.GetInstrumentByConidAsync(conid); if (existing == null) { var info = await _gateway.GetContractInfoAsync(conid, ct); var instrument = new IBKRInstrument { IbkrConid = conid, Symbol = info?.Symbol ?? ticker, SecType = info?.InstrumentType ?? "STK", Exchange = info?.Exchange ?? "SMART", PrimaryExchange = info?.ListingExchange, Currency = info?.Currency ?? "USD", CompanyName = info?.CompanyName ?? results[0].CompanyName, Sector = info?.Sector, Industry = info?.Industry, Description = info?.Text, Active = true, LastFetched = DateTime.UtcNow }; var id = await _repo.UpsertInstrumentAsync(instrument); await _repo.UpsertExternalIdentifierAsync(id, "capitoltrades", ticker); Logger.Info(Module, $"Neu: {ticker} → conid {conid}"); } else { await _repo.UpsertExternalIdentifierAsync(existing.Id, "capitoltrades", ticker); } mapped++; } catch (Exception ex) { Logger.Error(Module, $"Mapping '{ticker}': {ex.Message}", ex); failed++; } } Logger.Info(Module, $"Mapping: {mapped} OK, {failed} Fehler."); } private async Task RefreshExistingInstrumentsAsync(CancellationToken ct) { var instruments = (await _repo.GetAllActiveInstrumentsAsync()).ToList(); if (instruments.Count == 0) return; Logger.Info(Module, $"Aktualisiere {instruments.Count} Instrumente..."); int ok = 0, err = 0; foreach (var instr in instruments) { if (ct.IsCancellationRequested) break; try { var info = await _gateway.GetContractInfoAsync(instr.IbkrConid, ct); if (info == null) { err++; continue; } instr.Symbol = info.Symbol ?? instr.Symbol; instr.CompanyName = info.CompanyName ?? instr.CompanyName; instr.Sector = info.Sector ?? instr.Sector; instr.Industry = info.Industry ?? instr.Industry; instr.LastFetched = DateTime.UtcNow; await _repo.UpsertInstrumentAsync(instr); ok++; } catch (Exception ex) { Logger.Error(Module, $"Update {instr.Symbol}: {ex.Message}", ex); err++; } } Logger.Info(Module, $"Stammdaten: {ok} OK, {err} Fehler."); } }