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
@@ -0,0 +1,138 @@
using IBKRTrader.Core.Database;
using IBKRTrader.Core.Logging;
using IBKRTrader.Modules.CongressTrading.Database;
using IBKRTrader.Modules.CongressTrading.Scraper;
using IBKRTrader.Core.Workers;
namespace IBKRTrader.Modules.CongressTrading.Workers;
/// <summary>
/// Einmaliger History-Import übersteht beliebig viele Neustarts.
///
/// Fortschritts-Persistenz via core_settings:
/// ct.history_import_done = "true" → abgeschlossen, Worker skippt sich selbst
/// ct.history_import_page = "N" → nächste zu verarbeitende Seite
/// ct.history_import_total = "N" → Gesamtseitenanzahl (gecacht)
///
/// Alle Trade-Daten (Datum, Typ, Value, Ticker, Mitglied) kommen direkt
/// aus dem Listen-JSON kein separates Detail-Scraping notwendig.
/// </summary>
public class CongressHistoryImportWorker : WorkerBase
{
private readonly CapitolTradesScraper _scraper;
private readonly CongressRepository _repo;
private const string DoneFlag = "ct.history_import_done";
private const string ProgressKey = "ct.history_import_page";
private const string TotalKey = "ct.history_import_total";
public override string Name => "CT-HistoryImport";
public override string Module => "CT";
public override WorkerType Type => WorkerType.Worker;
protected override TimeSpan? Interval => TimeSpan.FromDays(365);
public CongressHistoryImportWorker(
LoggingService logger,
DatabaseService db,
CapitolTradesScraper scraper,
CongressRepository repo)
: base(logger, db)
{
_scraper = scraper;
_repo = repo;
Info.Info = "Bereit prüfe Fortschritt beim Start...";
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
// ── 1. Bereits vollständig abgeschlossen? ────────────────────────────
if (await _repo.GetSettingAsync(DoneFlag) == "true")
{
Logger.Info(Module, "CT History-Import: bereits abgeschlossen.");
Info.Info = "✓ Abgeschlossen";
return;
}
// ── 2. Fortschritt aus DB laden ──────────────────────────────────────
var pageStr = await _repo.GetSettingAsync(ProgressKey);
var startPage = pageStr != null && int.TryParse(pageStr, out var sp) ? sp : 1;
var totalStr = await _repo.GetSettingAsync(TotalKey);
int totalPages;
if (totalStr != null && int.TryParse(totalStr, out var tp) && tp > 0)
{
totalPages = tp;
Logger.Info(Module,
$"▶ Fortsetze History-Import: Seite {startPage} von {totalPages} (Gesamtanzahl aus Cache)");
}
else
{
Logger.Info(Module, "Ermittle Gesamtseitenanzahl von capitoltrades.com...");
totalPages = await _scraper.GetTotalPagesAsync(ct);
await _repo.SetSettingAsync(TotalKey, totalPages.ToString());
Logger.Info(Module,
$"▶ Starte History-Import: {totalPages} Seiten gefunden. Beginne bei Seite {startPage}.");
}
// ── 3. Seiten paginieren ─────────────────────────────────────────────
var knownMembers = await _repo.GetAllMemberBioIdsAsync();
var importedTotal = 0;
for (var page = startPage; page <= totalPages; page++)
{
ct.ThrowIfCancellationRequested();
Info.Info = $"Seite {page}/{totalPages} {importedTotal} neue Trades";
var (trades, memberIds) = await _scraper.ScrapeTradesPageAsync(page, ct);
if (trades.Count == 0)
{
Logger.Warn(Module,
$"[{page}/{totalPages}] Leere Seite Seite wird übersprungen.");
await _repo.SetSettingAsync(ProgressKey, (page + 1).ToString());
continue;
}
// Neue Kongressmitglieder anlegen (Daten kommen aus MemberSnapshot)
foreach (var trade in trades.Where(t => t.MemberSnapshot != null))
{
var snapshot = trade.MemberSnapshot!;
if (knownMembers.Contains(snapshot.BioId)) continue;
await _repo.UpsertMemberAsync(snapshot);
knownMembers.Add(snapshot.BioId);
Logger.Info(Module,
$" + {snapshot.Name} ({snapshot.BioId}) {snapshot.Party}/{snapshot.State}");
}
// Trades einfügen (INSERT IGNORE Duplikate werden automatisch ignoriert)
foreach (var trade in trades)
{
ct.ThrowIfCancellationRequested();
if (await _repo.TradeExistsAsync(trade.TradeId)) continue;
await _repo.InsertTradeAsync(trade, detailsFetched: true);
importedTotal++;
}
// ─── Fortschritt nach jeder erfolgreichen Seite persistieren ───
await _repo.SetSettingAsync(ProgressKey, (page + 1).ToString());
Logger.Info(Module,
$"[{page}/{totalPages}] {trades.Count} Trades, {importedTotal} neu gesamt.");
}
// ── 4. Abschluss ─────────────────────────────────────────────────────
await _repo.SetSettingAsync(DoneFlag, "true");
var totalInDb = await _repo.GetTradeCountAsync();
var membersInDb = await _repo.GetMemberCountAsync();
Logger.Info(Module,
$"=== History-Import abgeschlossen: {totalInDb} Trades, {membersInDb} Mitglieder ===");
Info.Info = $"✓ Fertig {totalInDb} Trades, {membersInDb} Mitglieder";
}
}