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,59 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace IBKRTrader.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Pure, seiteneffektfreie (De-)Serialisierung einer JSONL-Log-Zeile. JSONL (eine JSON-Zeile je Event)
|
||||
/// ist append-fähig, streambar und zeilenweise filterbar – KI-freundlich und effizient. Die eigentliche
|
||||
/// Datei-I/O liegt im <see cref="LoggingService"/>; hier nur das Format, damit es unit-getestet werden
|
||||
/// kann (Round-Trip). Feldnamen bewusst kurz: ts, level, source, cid, message.
|
||||
/// </summary>
|
||||
public static class LogJson
|
||||
{
|
||||
private static readonly JsonSerializerOptions Opts = new()
|
||||
{
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
/// <summary>Eine geparste JSONL-Zeile (Beiwerk – fehlende/kaputte Zeilen liefern null).</summary>
|
||||
public sealed record ParsedLogLine(DateTime Ts, string Level, string Source, string? Cid, string Message);
|
||||
|
||||
/// <summary>Serialisiert ein Log-Event zu genau einer JSON-Zeile (ohne Zeilenumbruch).</summary>
|
||||
public static string WriteLine(DateTime ts, AppLogLevel level, string source, string message, string? cid)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using (var w = new Utf8JsonWriter(ms))
|
||||
{
|
||||
w.WriteStartObject();
|
||||
w.WriteString("ts", ts.ToUniversalTime().ToString("O"));
|
||||
w.WriteString("level", level.ToString());
|
||||
w.WriteString("source", source ?? "");
|
||||
if (!string.IsNullOrEmpty(cid)) w.WriteString("cid", cid);
|
||||
w.WriteString("message", message ?? "");
|
||||
w.WriteEndObject();
|
||||
}
|
||||
return Encoding.UTF8.GetString(ms.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>Parst eine JSONL-Zeile. Gibt null zurück, wenn die Zeile leer oder kein gültiges JSON ist.</summary>
|
||||
public static ParsedLogLine? ParseLine(string? line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) return null;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(line);
|
||||
var r = doc.RootElement;
|
||||
DateTime ts = r.TryGetProperty("ts", out var tsp) && tsp.TryGetDateTime(out var d) ? d : default;
|
||||
string level = r.TryGetProperty("level", out var lp) ? lp.GetString() ?? "" : "";
|
||||
string source = r.TryGetProperty("source", out var sp) ? sp.GetString() ?? "" : "";
|
||||
string? cid = r.TryGetProperty("cid", out var cp) ? cp.GetString() : null;
|
||||
string msg = r.TryGetProperty("message", out var mp) ? mp.GetString() ?? "" : "";
|
||||
return new ParsedLogLine(ts, level, source, cid, msg);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ public class LoggingService
|
||||
{
|
||||
private RichTextBox? _rtb;
|
||||
private AppLogLevel _minLevel = AppLogLevel.Info;
|
||||
private readonly object _fileLock = new();
|
||||
private readonly object _fileLock = new();
|
||||
private readonly object _jsonlLock = new();
|
||||
|
||||
private static readonly string LogBaseDir =
|
||||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
|
||||
@@ -33,11 +34,23 @@ public class LoggingService
|
||||
public void Error(string module, string message, Exception? ex = null)
|
||||
=> Write(AppLogLevel.Error, module, message, ex);
|
||||
|
||||
public void Write(AppLogLevel level, string module, string message, Exception? ex = null)
|
||||
// Überladungen mit CorrelationId (SignalId) – schreiben zusätzlich strukturiert ins JSONL,
|
||||
// sodass der Supervisor die komplette Kette „alles zu diesem Signal" filtern kann.
|
||||
public void Info (string module, string message, string? cid, Exception? ex = null)
|
||||
=> Write(AppLogLevel.Info, module, message, ex, cid);
|
||||
|
||||
public void Warn (string module, string message, string? cid, Exception? ex = null)
|
||||
=> Write(AppLogLevel.Warn, module, message, ex, cid);
|
||||
|
||||
public void Error(string module, string message, string? cid, Exception? ex = null)
|
||||
=> Write(AppLogLevel.Error, module, message, ex, cid);
|
||||
|
||||
public void Write(AppLogLevel level, string module, string message, Exception? ex = null, string? cid = null)
|
||||
{
|
||||
if (level < _minLevel) return;
|
||||
var entry = new LogEntry(DateTime.Now, level, module, message, ex);
|
||||
WriteToFile(entry);
|
||||
WriteToJsonl(entry, cid);
|
||||
WriteToRtb(entry);
|
||||
}
|
||||
|
||||
@@ -61,6 +74,27 @@ public class LoggingService
|
||||
catch { /* Logging darf niemals abstürzen */ }
|
||||
}
|
||||
|
||||
// ─── JSONL (KI-freundlicher Zweit-Sink) ─────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Schreibt zusätzlich eine JSON-Zeile nach Logs\{yyyy-MM-dd}.jsonl (Dual-Sink). Zeilenweise
|
||||
/// filter-/parsebar (Datum/Level/Quelle/Text/CorrelationId) – Grundlage für Log Viewer + Supervisor.
|
||||
/// </summary>
|
||||
private void WriteToJsonl(LogEntry e, string? cid)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(LogBaseDir);
|
||||
var file = Path.Combine(LogBaseDir, $"{e.Timestamp:yyyy-MM-dd}.jsonl");
|
||||
var message = e.Exception != null ? $"{e.Message} | {e.Exception.Message}" : e.Message;
|
||||
var json = LogJson.WriteLine(e.Timestamp, e.Level, e.Module, message, cid);
|
||||
|
||||
lock (_jsonlLock)
|
||||
File.AppendAllText(file, json + "\n");
|
||||
}
|
||||
catch { /* Logging darf niemals abstürzen */ }
|
||||
}
|
||||
|
||||
// ─── RichTextBox ──────────────────────────────────────────────────────────
|
||||
|
||||
private static readonly Color ColorInfo = Color.FromArgb(150, 210, 150);
|
||||
|
||||
Reference in New Issue
Block a user