using System.Text.Json; using IBKRTrader.Core.Analytics; using IBKRTrader.Core.Logging; using IBKRTrader.Core.Persistence; using IBKRTrader.Modules.Supervisor.Persistence; using IBKRTrader.Modules.Supervisor.Services; namespace IBKRTrader.Modules.Supervisor.Agent; /// /// Baut die read-only Standard-Tool-Registry des Supervisors: Zugriffe auf Entscheidungsjournal, /// Order-Events, Trade-Log, Dossiers, JSONL-Logs, KPIs, Counterfactuals und das Architektur-Dokument. /// Alle Ergebnisse als kompakte JSON-/Markdown-Strings. KEIN Tool kann handeln oder schreiben. /// public static class SupervisorTools { private static readonly JsonSerializerOptions JsonOpts = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; public static SupervisorToolRegistry CreateRegistry( IDecisionJournal journal, IOrderEventLog orderEvents, TradeLogReader trades, DossierService dossiers, ISupervisorCounterfactualRepository? counterfactuals = null) { var reg = new SupervisorToolRegistry(); string logsDir = Path.Combine(AppContext.BaseDirectory, "Logs"); reg.Register(new SupervisorTool( "query_decisions", "Fragt das Entscheidungsjournal ab (JEDE Handelsentscheidung inkl. Ablehnungen mit Grund). " + "Filter optional: module, symbol, reason (z.B. RiskRejected), decision (Executed/Rejected/Skipped/Failed), sinceHours.", """{"type":"object","properties":{"module":{"type":"string"},"symbol":{"type":"string"},"reason":{"type":"string"},"decision":{"type":"string"},"sinceHours":{"type":"integer"},"limit":{"type":"integer"}}}""", args => { int limit = Math.Clamp(SupervisorToolRegistry.GetInt(args, "limit") ?? 100, 1, 500); string? module = SupervisorToolRegistry.GetString(args, "module"); string? symbol = SupervisorToolRegistry.GetString(args, "symbol"); string? reason = SupervisorToolRegistry.GetString(args, "reason"); string? decision = SupervisorToolRegistry.GetString(args, "decision"); int? sinceHours = SupervisorToolRegistry.GetInt(args, "sinceHours"); DateTime since = sinceHours.HasValue ? DateTime.UtcNow.AddHours(-sinceHours.Value) : DateTime.MinValue; var rows = journal.Query(d => (module == null || d.Module == module) && (symbol == null || d.Symbol == symbol) && d.Timestamp >= since, limit * 3) .Where(d => reason == null || string.Equals(d.Reason.ToString(), reason, StringComparison.OrdinalIgnoreCase)) .Where(d => decision == null || string.Equals(d.Decision.ToString(), decision, StringComparison.OrdinalIgnoreCase)) .Take(limit) .Select(d => new { d.SignalId, ts = d.Timestamp, d.Module, d.Symbol, d.Side, price = d.SignalPrice, decision = d.Decision.ToString(), reason = d.Reason.ToString(), d.Message, ctx = d.ContextJson }); return JsonSerializer.Serialize(rows, JsonOpts); })); reg.Register(new SupervisorTool( "query_order_events", "Fragt das Order-Lifecycle-Log ab (Platzierungen, Broker-Antworten, Fills, Cancels). " + "Filter optional: module, symbol, signalId, sinceHours.", """{"type":"object","properties":{"module":{"type":"string"},"symbol":{"type":"string"},"signalId":{"type":"string"},"sinceHours":{"type":"integer"},"limit":{"type":"integer"}}}""", args => { int limit = Math.Clamp(SupervisorToolRegistry.GetInt(args, "limit") ?? 100, 1, 500); string? module = SupervisorToolRegistry.GetString(args, "module"); string? symbol = SupervisorToolRegistry.GetString(args, "symbol"); string? signalId = SupervisorToolRegistry.GetString(args, "signalId"); int? sinceHours = SupervisorToolRegistry.GetInt(args, "sinceHours"); DateTime since = sinceHours.HasValue ? DateTime.UtcNow.AddHours(-sinceHours.Value) : DateTime.MinValue; var rows = orderEvents.Query(e => (module == null || e.Module == module) && (symbol == null || e.Symbol == symbol) && (signalId == null || e.SignalId == signalId) && e.Timestamp >= since, limit) .Select(e => new { e.SignalId, ts = e.Timestamp, e.Module, e.Symbol, eventType = e.EventType.ToString(), e.Side, e.Price, e.Quantity, e.OrderType, e.Response, details = e.DetailsJson }); return JsonSerializer.Serialize(rows, JsonOpts); })); reg.Register(new SupervisorTool( "query_trades", "Fragt gebuchte Fills aus der modulübergreifenden Trade-Historie ab. " + "Filter optional: module, symbol, sinceDays.", """{"type":"object","properties":{"module":{"type":"string"},"symbol":{"type":"string"},"sinceDays":{"type":"integer"},"limit":{"type":"integer"}}}""", args => { int limit = Math.Clamp(SupervisorToolRegistry.GetInt(args, "limit") ?? 100, 1, 500); string? module = SupervisorToolRegistry.GetString(args, "module"); string? symbol = SupervisorToolRegistry.GetString(args, "symbol"); int? sinceDays = SupervisorToolRegistry.GetInt(args, "sinceDays"); DateTime since = sinceDays.HasValue ? DateTime.UtcNow.AddDays(-sinceDays.Value) : DateTime.MinValue; var rows = trades.Query(module, symbol, since, limit) .Select(t => new { t.SignalId, t.Module, t.Symbol, t.Action, t.Quantity, t.Price, t.TotalValue, t.TradedAt, t.Status }); return JsonSerializer.Serialize(rows, JsonOpts); })); reg.Register(new SupervisorTool( "get_dossier", "Liefert das komplette Dossier zu einer SignalId als Markdown: Entscheidungskette, Order-Events, Trades, Log-Auszug.", """{"type":"object","properties":{"signalId":{"type":"string"}},"required":["signalId"]}""", args => { string? signalId = SupervisorToolRegistry.GetString(args, "signalId"); if (string.IsNullOrWhiteSpace(signalId)) return "FEHLER: signalId fehlt."; return DossierBuilder.ToMarkdown(dossiers.BuildForSignal(signalId)); })); reg.Register(new SupervisorTool( "read_logs", "Liest die JSONL-Logdatei eines Tages (Datum yyyy-MM-dd), optional gefiltert nach level, cid (SignalId) und textFilter.", """{"type":"object","properties":{"date":{"type":"string"},"level":{"type":"string"},"cid":{"type":"string"},"textFilter":{"type":"string"},"limit":{"type":"integer"}},"required":["date"]}""", args => { string? date = SupervisorToolRegistry.GetString(args, "date"); if (string.IsNullOrWhiteSpace(date)) return "FEHLER: date fehlt (yyyy-MM-dd)."; string path = Path.Combine(logsDir, $"{date}.jsonl"); if (!File.Exists(path)) return $"Keine JSONL-Datei für {date}."; string? level = SupervisorToolRegistry.GetString(args, "level"); string? cid = SupervisorToolRegistry.GetString(args, "cid"); string? text = SupervisorToolRegistry.GetString(args, "textFilter"); int limit = Math.Clamp(SupervisorToolRegistry.GetInt(args, "limit") ?? 200, 1, 1000); var lines = new List(); foreach (var line in File.ReadLines(path)) { var p = LogJson.ParseLine(line); if (p == null) continue; if (level != null && !string.Equals(p.Level, level, StringComparison.OrdinalIgnoreCase)) continue; if (cid != null && p.Cid != cid) continue; if (text != null && !p.Message.Contains(text, StringComparison.OrdinalIgnoreCase)) continue; lines.Add(p); if (lines.Count >= limit) break; } return JsonSerializer.Serialize(lines, JsonOpts); })); reg.Register(new SupervisorTool( "get_kpis", "Berechnet Kennzahlen (Netto-PnL, Winrate, Ø-PnL, Profit-Faktor, Trade-Anzahl) über die " + "Trade-Historie (FIFO-realisiert). Filter optional: module, sinceDays.", """{"type":"object","properties":{"module":{"type":"string"},"sinceDays":{"type":"integer"}}}""", args => { string? module = SupervisorToolRegistry.GetString(args, "module"); int? sinceDays = SupervisorToolRegistry.GetInt(args, "sinceDays"); DateTime since = sinceDays.HasValue ? DateTime.UtcNow.AddDays(-sinceDays.Value) : DateTime.MinValue; var fills = trades.ForKpis(module, since); var k = TradeAnalytics.ComputeKpis(fills); var byModule = TradeAnalytics.PnlByModule(fills); return JsonSerializer.Serialize(new { k.TradeCount, k.NetPnl, k.WinRatePct, k.AvgPnlPerTrade, k.ProfitFactor, byModule = byModule.Select(x => new { module = x.Key, x.Pnl, x.Count }) }, JsonOpts); })); reg.Register(new SupervisorTool( "get_architecture_context", "Liefert das kuratierte Architektur-/Verhaltensdokument von IBKRTrader (wie die Software entscheidet und handelt).", """{"type":"object","properties":{}}""", _ => ArchitectureContext.Load())); if (counterfactuals != null) { reg.Register(new SupervisorTool( "query_counterfactuals", "Was wäre aus ABGELEHNTEN BUY-Signalen geworden? Liefert nach einer Wartezeit ausgewertete " + "Rejects (Reason, Signalpreis, späterer Kurs, hypothetischer PnL je Stück) — zeigt, ob Risk-Limits Gewinne oder Verluste verhindert haben.", """{"type":"object","properties":{"limit":{"type":"integer"}}}""", args => { int limit = Math.Clamp(SupervisorToolRegistry.GetInt(args, "limit") ?? 200, 1, 500); var rows = counterfactuals.GetRecent(limit).Select(c => new { c.SignalId, c.CheckedAt, c.Module, c.Symbol, reason = c.Reason, signalPrice = c.SignalPrice, laterPrice = c.LaterPrice, pnlPerShare = c.HypotheticalPnlPerShare }); return JsonSerializer.Serialize(rows, JsonOpts); })); } return reg; } }