R8: Accounting- + Supervisor-Modul + Core-Datenfundament (S-0)

Portierung der beiden fehlenden Grundbausteine aus PolytraderSharp (voller Ausbau).

Core S-0 (Datenfundament fuer Analyse/Forensik):
- core_decision_journal + core_order_events (+ ReasonCode/Decision/OrderEvent-Enums),
  IDecisionJournal/IOrderEventLog mit fehlertoleranten EF-Impls (Handel bricht nie).
- SignalId-Durchreichung TradeSignal -> ExecutionService -> core_trade_history;
  ExecutionService schreibt an jeder Verzweigung Journal/Order-Events.
- JSONL-Log-Sink (LogJson + Dual-Sink), pure Analytik: RealizedPnlEngine (FIFO),
  TradeAnalytics, DossierBuilder. Migration AddAnalysisFoundation.

Accounting-Modul (acc_): unabhaengiger IBKR-Kontoauszug (Activity Flex Query) hinter
Interfaces mit Offline-Null-Stubs -> append-only Ledger + Periodenabrechnung/BWA + FX
(USD/EUR) + CSV/PDF (PDFsharp/MigraDoc). Steuerschicht bewusst offen (Platzhalter-Tab).
Kein Handel. Migration InitialAccounting.

Supervisor-Modul (sup_): read-only OpenRouter-Agent (Function-Calling-Loop) + read-only
Tool-Registry (8 Tools) + Profile + Dossier-Browser + Counterfactual-Job (Stub) +
Tagesbericht/MCP-Light (opt-in). Migration InitialSupervisor.

Verdrahtung: Program.cs (beide Module + Icons), slnx/App/Tests-Referenzen,
provision-db.ps1, AppSettings-Sektionen, docs/konzepte, README.

Tests: 79 -> 117 gruen (FIFO/KPIs/Dossier/JSONL, Classifier/Engine/FX/Idempotenz,
OpenRouter/Registry/Agent/MCP, STA-Konstruktion beider neuen Fenster).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-31 09:25:18 +02:00
co-authored by Claude Opus 4.8
parent cbbedb2e0e
commit 2a312ca035
86 changed files with 6880 additions and 22 deletions
@@ -0,0 +1,89 @@
using IBKRTrader.Core.Analytics;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Persistence;
namespace IBKRTrader.Modules.Supervisor.Services;
/// <summary>Kopfzeile eines Signals für die Übersichtsliste des Dossier-Browsers.</summary>
public sealed record SignalSummary(
string SignalId, DateTime FirstSeen, string Module, string Symbol,
string Side, string LastDecision, string LastReason, int DecisionCount);
/// <summary>
/// Beschafft die Daten für Trade-Dossiers: Entscheidungsjournal + Order-Events + Trade-Log +
/// JSONL-Log-Zeilen (per CorrelationId), Zusammenbau/Rendering pur im <see cref="DossierBuilder"/>
/// (Core). Read-only — der Supervisor ist Beobachter.
/// </summary>
public sealed class DossierService
{
private readonly IDecisionJournal _journal;
private readonly IOrderEventLog _orderEvents;
private readonly TradeLogReader _trades;
private readonly string _logsDirectory;
public DossierService(IDecisionJournal journal, IOrderEventLog orderEvents, TradeLogReader trades)
{
_journal = journal;
_orderEvents = orderEvents;
_trades = trades;
_logsDirectory = Path.Combine(AppContext.BaseDirectory, "Logs");
}
/// <summary>Jüngste Signale (gruppiert über das Entscheidungsjournal), neueste zuerst.</summary>
public List<SignalSummary> RecentSignals(int limit = 200)
{
var decisions = _journal.Query(d => d.SignalId != "", limit * 5);
return decisions
.GroupBy(d => d.SignalId)
.Select(g =>
{
var ordered = g.OrderBy(d => d.Timestamp).ToList();
var first = ordered[0];
var last = ordered[^1];
return new SignalSummary(g.Key, first.Timestamp, first.Module, first.Symbol,
first.Side, last.Decision.ToString(), last.Reason.ToString(), ordered.Count);
})
.OrderByDescending(s => s.FirstSeen)
.Take(limit)
.ToList();
}
/// <summary>Baut das komplette Dossier zu einer SignalId (inkl. Log-Zeilen aus den JSONL-Tagesdateien).</summary>
public TradeDossier BuildForSignal(string signalId)
{
var decisions = _journal.Query(d => d.SignalId == signalId);
var events = _orderEvents.Query(e => e.SignalId == signalId);
var trades = _trades.BySignal(signalId);
var logLines = ReadLogLines(signalId, decisions.Select(d => d.Timestamp).Concat(events.Select(e => e.Timestamp)));
return DossierBuilder.Build(signalId, decisions, events, trades, logLines);
}
/// <summary>
/// Liest JSONL-Zeilen mit passender CorrelationId — nur aus den Tagesdateien im Zeitfenster der
/// bekannten Ereignisse (±1 Tag), statt alle Logs zu scannen. Fehlertolerant (fehlende Dateien = leer).
/// </summary>
private List<LogJson.ParsedLogLine> ReadLogLines(string signalId, IEnumerable<DateTime> eventTimes)
{
var result = new List<LogJson.ParsedLogLine>();
var times = eventTimes.ToList();
if (times.Count == 0 || string.IsNullOrEmpty(signalId)) return result;
try
{
var from = times.Min().Date.AddDays(-1);
var to = times.Max().Date.AddDays(1);
for (var day = from; day <= to; day = day.AddDays(1))
{
string path = Path.Combine(_logsDirectory, $"{day:yyyy-MM-dd}.jsonl");
if (!File.Exists(path)) continue;
foreach (var line in File.ReadLines(path))
{
var p = LogJson.ParseLine(line);
if (p != null && p.Cid == signalId) result.Add(p);
}
}
}
catch { /* Log-Auszug ist Beiwerk – Dossier bleibt auch ohne nutzbar */ }
return result;
}
}