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>
202 lines
11 KiB
C#
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;
|
|
}
|
|
}
|