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:
co-authored by
Claude Opus 4.8
parent
cbbedb2e0e
commit
2a312ca035
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user