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
+44
View File
@@ -0,0 +1,44 @@
using IBKRTrader.Core.Workers;
using Microsoft.Extensions.DependencyInjection;
namespace IBKRTrader.Core.Modules;
/// <summary>
/// Vertrag für ein eigenständiges Trading-Modul.
///
/// Architektur-Regel: Der Core kennt konkrete Module NICHT. Module hängen sich
/// ausschließlich über diesen Vertrag und den DI-Container ein
/// (Abhängigkeit nur Modul → Core, niemals umgekehrt).
///
/// Lebenszyklus:
/// 1. <see cref="RegisterServices"/> vor dem Bauen des DI-Containers
/// 2. <see cref="InitializeAsync"/> beim App-Start (Migrationen etc.)
/// 3. <see cref="GetWorkers"/> Worker werden der WorkerEngine übergeben
/// 4. <see cref="CreateWindow"/> Launcher öffnet das Modul-Fenster
/// </summary>
public interface IModule
{
/// <summary>Eindeutiges Modul-Kürzel, zugleich DB-Tabellen-Präfix (z. B. "CT").</summary>
string Key { get; }
/// <summary>Anzeigename im Launcher (z. B. "Congress Trading").</summary>
string DisplayName { get; }
/// <summary>Kurzbeschreibung der Strategie für den Launcher.</summary>
string Description { get; }
/// <summary>Modul-Version.</summary>
string Version { get; }
/// <summary>Registriert alle Modul-Services im DI-Container (vor dem Build).</summary>
void RegisterServices(IServiceCollection services);
/// <summary>Führt Modul-Migrationen und einmalige Initialisierung aus (beim Start).</summary>
Task InitializeAsync(IServiceProvider provider);
/// <summary>Liefert die Worker/Services des Moduls für die WorkerEngine.</summary>
IEnumerable<IWorker> GetWorkers(IServiceProvider provider);
/// <summary>Erzeugt das eigenständige Fenster des Moduls (vom Launcher geöffnet).</summary>
Form CreateWindow(IServiceProvider provider);
}
+22
View File
@@ -0,0 +1,22 @@
namespace IBKRTrader.Core.Modules;
/// <summary>
/// Zentrale Registry aller geladenen Module.
/// Wird in Program.cs mit den Modul-Instanzen befüllt und als Singleton
/// bereitgestellt, damit Launcher und Startup-Code über die Module iterieren
/// können, ohne konkrete Modultypen zu kennen.
/// </summary>
public class ModuleRegistry
{
private readonly List<IModule> _modules;
public ModuleRegistry(IEnumerable<IModule> modules)
=> _modules = modules?.ToList() ?? throw new ArgumentNullException(nameof(modules));
/// <summary>Alle registrierten Module in Registrierungsreihenfolge.</summary>
public IReadOnlyList<IModule> Modules => _modules;
/// <summary>Findet ein Modul anhand seines Kürzels (case-insensitive) oder null.</summary>
public IModule? Find(string key)
=> _modules.FirstOrDefault(m => string.Equals(m.Key, key, StringComparison.OrdinalIgnoreCase));
}
+14 -4
View File
@@ -126,9 +126,9 @@ public abstract class WorkerBase : IWorker
try
{
logId = await Db.BeginWorkerLogAsync(Name, Module);
logId = await BeginRunLogAsync();
await ExecuteAsync(ct);
await Db.EndWorkerLogAsync(logId, true);
await EndRunLogAsync(logId, true);
Info.LastRuntime = DateTime.Now;
Info.Status = WorkerStatus.Idle;
@@ -137,20 +137,30 @@ public abstract class WorkerBase : IWorker
catch (OperationCanceledException)
{
if (logId > 0)
await Db.EndWorkerLogAsync(logId, false, "Abgebrochen");
await EndRunLogAsync(logId, false, "Abgebrochen");
Info.Status = WorkerStatus.Stopped;
Info.Info = "Abgebrochen";
}
catch (Exception ex)
{
if (logId > 0)
await Db.EndWorkerLogAsync(logId, false, ex.Message);
await EndRunLogAsync(logId, false, ex.Message);
Logger.Error(Module, $"Fehler in Worker {Name}: {ex.Message}", ex);
Info.Status = WorkerStatus.Error;
Info.Info = $"Fehler: {ex.Message}";
}
}
// ─── DB-Log-Seam (überschreibbar für Unit-Tests) ──────────────────────────
/// <summary>Legt den Worker-Log-Eintrag an. Kapselt den DB-Zugriff (testbar).</summary>
protected virtual Task<long> BeginRunLogAsync()
=> Db.BeginWorkerLogAsync(Name, Module);
/// <summary>Schließt den Worker-Log-Eintrag ab. Kapselt den DB-Zugriff (testbar).</summary>
protected virtual Task EndRunLogAsync(long logId, bool success, string? message = null)
=> Db.EndWorkerLogAsync(logId, success, message);
// ─── Hilfsmethoden ────────────────────────────────────────────────────────
private static string FormatInterval(TimeSpan ts)