Phase 1: Modul-System formalisiert (IModule + ModuleRegistry)

- Core/Modules/IModule.cs: Vertrag (Key, DisplayName, Description, Version,
  RegisterServices, InitializeAsync, GetWorkers, CreateWindow)
- Core/Modules/ModuleRegistry.cs
- CongressTradingModule auf IModule umgestellt (instanzbasiert, Platzhalter-Fenster)
- Program.cs iteriert ueber IModule[] + Registry; Form1 initialisiert Module ueber Registry
- Testbarkeit: WorkerBase DB-Log-Seam (virtuell); CapitolTradesScraper.ParseTradesFromHtml
  extrahiert (offline gegen ct_raw.html testbar)
- Tests: ModuleRegistry, CongressTradingModule, WorkerBase, CapitolTradesScraper -> 15/15 gruen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-27 10:07:47 +02:00
parent 9abad277c2
commit af398353f0
13 changed files with 449 additions and 35 deletions
@@ -1,3 +1,5 @@
using System.Drawing;
using IBKRTrader.Core.Modules;
using IBKRTrader.Core.Workers;
using IBKRTrader.Modules.CongressTrading.Database;
using IBKRTrader.Modules.CongressTrading.Scraper;
@@ -7,18 +9,24 @@ using Microsoft.Extensions.DependencyInjection;
namespace IBKRTrader.Modules.CongressTrading;
/// <summary>
/// Entry-Point des CongressTrading-Moduls.
/// Registriert alle modulspezifischen Services in den DI-Container.
///
/// Architektur-Regel: Dieser Code wird von Program.cs aufgerufen.
/// Der Core kennt dieses Modul NICHT die Abhängigkeit geht nur von Modul → Core.
/// CongressTrading-Modul: kopiert Aktien-Trades von US-Kongressmitgliedern
/// (Quelle: capitoltrades.com).
///
/// Architektur-Regel: Dieses Modul hängt sich ausschließlich über den
/// <see cref="IModule"/>-Vertrag ein. Der Core kennt dieses Modul NICHT
/// die Abhängigkeit geht nur von Modul → Core.
/// </summary>
public static class CongressTradingModule
public sealed class CongressTradingModule : IModule
{
public const string ModuleKey = "CT";
public string Key => ModuleKey;
public string DisplayName => "Congress Trading";
public string Description => "Kopiert Aktien-Trades von US-Kongressmitgliedern (capitoltrades.com).";
public string Version => "1.0.0";
/// <summary>Registriert alle Modul-Services im DI-Container.</summary>
public static void Register(IServiceCollection services)
public void RegisterServices(IServiceCollection services)
{
// Datenbank
services.AddSingleton<CongressMigrations>();
@@ -32,24 +40,42 @@ public static class CongressTradingModule
services.AddSingleton<CongressScrapeWorker>();
}
/// <summary>
/// Führt die Modul-Migrationen aus und resettet ggf. den Fortschritt.
/// </summary>
public static async Task InitializeAsync(IServiceProvider provider)
/// <summary>Führt die Modul-Migrationen aus.</summary>
public async Task InitializeAsync(IServiceProvider provider)
{
var migrations = provider.GetRequiredService<CongressMigrations>();
await migrations.RunAsync();
// Einmaliger Reset: löscht alle alten (fehlerhaften) Fortschritts-Daten
// damit der Import sauber von Seite 1 neu startet.
var repo = provider.GetRequiredService<CongressRepository>();
// Einmaliger Reset (deaktiviert): löscht alten Fortschritt für sauberen Neustart.
// var repo = provider.GetRequiredService<CongressRepository>();
// await repo.ResetHistoryImportAsync();
}
/// <summary>Gibt die Modul-Worker für die WorkerEngine zurück.</summary>
public static IEnumerable<IWorker> GetWorkers(IServiceProvider provider) =>
public IEnumerable<IWorker> GetWorkers(IServiceProvider provider) =>
[
provider.GetRequiredService<CongressHistoryImportWorker>(),
provider.GetRequiredService<CongressScrapeWorker>()
];
/// <summary>
/// Erzeugt das Modul-Fenster. Platzhalter die vollständige UI folgt in Phase 2.
/// </summary>
public Form CreateWindow(IServiceProvider provider)
{
var form = new Form
{
Text = $"{DisplayName} ({Key})",
Width = 800,
Height = 500,
StartPosition = FormStartPosition.CenterParent
};
form.Controls.Add(new Label
{
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleCenter,
Text = $"{DisplayName}\n\nModul-UI folgt in Phase 2."
});
return form;
}
}
@@ -143,6 +143,16 @@ public class CapitolTradesScraper
var url = $"{BaseUrl}/trades?pageSize=96&page={page}";
var html = await _http.GetStringAsync(url, ct);
return ParseTradesFromHtml(html, page);
}
/// <summary>
/// Reine Parse-Logik: extrahiert Trades aus dem HTML (RSC-Stream) einer Trades-Seite.
/// Kein Netzwerkzugriff dadurch offline gegen gespeicherte Fixtures testbar.
/// </summary>
public (List<CongressTrade> Trades, HashSet<string> MemberBioIds)
ParseTradesFromHtml(string html, int page = 1)
{
var trades = new List<CongressTrade>();
var memberIds = new HashSet<string>();