Files
IBKRTrader/src/IBKRTrader.Modules.Supervisor/Agent/SupervisorTools.cs
T
RichardandClaude Opus 4.8 2a312ca035 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>
2026-07-31 09:25:18 +02:00

202 lines
11 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
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<LogJson.ParsedLogLine>();
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;
}
}