Supervisor S-1: Dossier-Generator + Modul-Skelett mit Dossier-Browser + Journal-Nachverdrahtung

Neues Modul PolyTrader.Modules.Supervisor (IPolyTraderModule, Name=Supervisor, DbPrefix=sup_,
nur Core-Referenz, strikt read-only):
- DossierBuilder (Core/Analytics, pur+getestet): TradeDossier aus Entscheidungen + Order-Events +
  Trades + Log-Zeilen, chronologisch, mit Markdown-Rendering (Tabellen, Pipe-Escaping).
- DossierService (Modul): beschafft Journal/Events/Trade-Log per SignalId + JSONL-Zeilen per CID
  (nur Tagesdateien im Ereignis-Zeitfenster +-1 Tag); RecentSignals-Uebersicht (Journal gruppiert).
- SupervisorMainForm: Dossier-Browser - links juengste Signale, rechts Markdown-Dossier;
  SignalId-Suche; Analyse-Chat (OpenRouter) folgt in S-2. In Launcher/Smoke registriert.

Journal-Nachverdrahtung (S-0-Vervollstaendigung):
- TraderMonitor: Profit-Target erzeugt eigene SignalId -> Leiter + Journal (ProfitTargetTriggered);
  Stale-Cleanup-Cancels als OrderEvents (StaleCleanupCancel).
- StartupOrderReconciliation: K2-Cancels als OrderEvents (StartupReconcileCancel).
- RF: Demo-Einstiege (DemoFilled, eigene SignalId) + Resolution-Closes (SystemResolutionClose)
  im Journal - damit sind ALLE Module im Entscheidungsjournal vertreten.

Tests: +3 DossierBuilder; 4 Service-Builder auf neue Ctors. Build 0 Fehler, 344 Tests gruen,
--smoke-ui: [OK] supervisor.main (alle 5 Views gruen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-17 10:09:02 +02:00
co-authored by Claude Opus 4.8
parent ca00977ea3
commit c75a958e36
18 changed files with 610 additions and 15 deletions
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Analytics;
using PolyTrader.Modules.Supervisor.Services;
namespace PolyTrader.Modules.Supervisor.Ui
{
/// <summary>
/// Hauptfenster des Supervisor-Moduls (S-1): Dossier-Browser — links die jüngsten Signale
/// (aus dem Entscheidungsjournal), rechts das komplette Dossier (Markdown). Der Analyse-Chat
/// (OpenRouter-Agent) folgt in S-2 als weiterer Tab. Code-only konstruiert (Muster RF-Modul);
/// DB-Zugriffe defensiv, damit die UI auch bei leerer/nicht erreichbarer DB bedienbar bleibt.
/// </summary>
public sealed class SupervisorMainForm : Form
{
private DossierService? _dossiers;
private readonly ToolStrip _toolStrip = new();
private readonly ToolStripButton _btnRefresh = new() { Text = "Aktualisieren", DisplayStyle = ToolStripItemDisplayStyle.Text };
private readonly ToolStripLabel _lblSearch = new() { Text = "SignalId:" };
private readonly ToolStripTextBox _tbSignalId = new() { AutoSize = false, Width = 220 };
private readonly ToolStripButton _btnOpen = new() { Text = "Dossier öffnen", DisplayStyle = ToolStripItemDisplayStyle.Text };
private readonly SplitContainer _split = new() { Dock = DockStyle.Fill, SplitterDistance = 420 };
private readonly DataGridView _grid = new()
{
Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, AllowUserToDeleteRows = false,
AutoGenerateColumns = true, SelectionMode = DataGridViewSelectionMode.FullRowSelect,
RowHeadersVisible = false, MultiSelect = false
};
private readonly TextBox _dossierText = new()
{
Dock = DockStyle.Fill, Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Both,
Font = new System.Drawing.Font("Consolas", 9.5f), WordWrap = false
};
private readonly Label _status = new() { Dock = DockStyle.Bottom, Height = 22, Padding = new Padding(6, 2, 6, 2), Text = "" };
public SupervisorMainForm()
{
Text = "Supervisor";
Width = 1250;
Height = 700;
StartPosition = FormStartPosition.CenterScreen;
_toolStrip.Items.AddRange(new ToolStripItem[] { _btnRefresh, new ToolStripSeparator(), _lblSearch, _tbSignalId, _btnOpen });
_split.Panel1.Controls.Add(_grid);
_split.Panel2.Controls.Add(_dossierText);
Controls.Add(_split);
Controls.Add(_toolStrip);
Controls.Add(_status);
_toolStrip.Dock = DockStyle.Top;
_btnRefresh.Click += (_, _) => LoadSignals();
_btnOpen.Click += (_, _) => OpenDossier(_tbSignalId.Text.Trim());
_grid.SelectionChanged += (_, _) => OpenSelected();
}
public void Initialize(IServiceProvider services)
{
_dossiers = services.GetRequiredService<DossierService>();
LoadSignals();
}
private void LoadSignals()
{
if (_dossiers == null) return;
try
{
List<SignalSummary> signals = _dossiers.RecentSignals(200);
_grid.DataSource = signals;
_status.Text = signals.Count == 0
? "Noch keine Journal-Einträge (Entscheidungen entstehen, sobald Signale verarbeitet werden)."
: $"{signals.Count} Signale.";
}
catch (Exception ex)
{
_status.Text = $"Journal nicht lesbar: {ex.Message}";
}
}
private void OpenSelected()
{
if (_grid.CurrentRow?.DataBoundItem is SignalSummary s)
OpenDossier(s.SignalId);
}
private void OpenDossier(string signalId)
{
if (_dossiers == null || string.IsNullOrWhiteSpace(signalId)) return;
try
{
var dossier = _dossiers.BuildForSignal(signalId);
_dossierText.Text = DossierBuilder.ToMarkdown(dossier).Replace("\n", Environment.NewLine);
_tbSignalId.Text = signalId;
}
catch (Exception ex)
{
_dossierText.Text = $"Dossier konnte nicht geladen werden: {ex.Message}";
}
}
}
}