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,201 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user