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:
@@ -0,0 +1,133 @@
|
||||
using IBKRTrader.Core.Database;
|
||||
using IBKRTrader.Core.IBKR;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Settings;
|
||||
|
||||
namespace IBKRTrader.Core.Workers.BuiltIn;
|
||||
|
||||
/// <summary>
|
||||
/// Synchronisiert IBKR-Instrumentenstammdaten alle 30 Tage.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user