using System; using System.Collections.Generic; using System.Linq; using System.Text; using PolyTraderSharp.Models; using PolyTraderSharp.Services; namespace PolyTrader.Core.Analytics { /// /// Das „Trade-Dossier" (S-1, Supervisor-Konzept): alles, was zu einem Signal gehört — /// Entscheidungen, Order-Events, abgeschlossene Trades, Log-Zeilen — als eine Einheit. /// Grundlage für den Dossier-Browser (Mensch) und die KI-Analyse (S-2). /// public sealed class TradeDossier { public string SignalId { get; init; } = string.Empty; public string ModuleName { get; init; } = string.Empty; public int AccountId { get; init; } public string TokenId { get; init; } = string.Empty; public string MarketQuestion { get; init; } = string.Empty; public string Side { get; init; } = string.Empty; public List Decisions { get; init; } = new(); public List OrderEvents { get; init; } = new(); public List ClosedTrades { get; init; } = new(); public List LogLines { get; init; } = new(); } /// /// Reiner Zusammenbau + Markdown-Rendering eines Dossiers. Seiteneffektfrei und unit-getestet; /// die Datenbeschaffung (Repos/JSONL) macht der DossierService des Supervisor-Moduls. /// public static class DossierBuilder { /// Baut das Dossier aus bereits auf die SignalId gescopten Daten (sortiert chronologisch). public static TradeDossier Build( string signalId, IEnumerable decisions, IEnumerable orderEvents, IEnumerable closedTrades, IEnumerable? logLines = null) { var dec = decisions.OrderBy(d => d.Timestamp).ToList(); var ev = orderEvents.OrderBy(e => e.Timestamp).ToList(); var trades = closedTrades.OrderBy(t => t.ClosedAt).ToList(); var first = dec.FirstOrDefault(); return new TradeDossier { SignalId = signalId, ModuleName = first?.ModuleName ?? trades.FirstOrDefault()?.ModuleName ?? string.Empty, AccountId = first?.AccountId ?? trades.FirstOrDefault()?.AccountId ?? 0, TokenId = first?.TokenId ?? trades.FirstOrDefault()?.TokenId ?? string.Empty, MarketQuestion = first?.MarketQuestion ?? trades.FirstOrDefault()?.MarketQuestion ?? string.Empty, Side = first?.Side ?? string.Empty, Decisions = dec, OrderEvents = ev, ClosedTrades = trades, LogLines = (logLines ?? Enumerable.Empty()).ToList() }; } /// Menschenlesbare Markdown-Fassung (Dossier-Browser; auch als KI-Input geeignet). public static string ToMarkdown(TradeDossier d) { var sb = new StringBuilder(); sb.AppendLine($"# Dossier {ShortId(d.SignalId)}"); sb.AppendLine(); sb.AppendLine($"- **Markt:** {d.MarketQuestion}"); sb.AppendLine($"- **Modul:** {d.ModuleName} **Konto:** {d.AccountId} **Side:** {d.Side}"); sb.AppendLine($"- **TokenId:** {d.TokenId}"); sb.AppendLine($"- **SignalId:** {d.SignalId}"); decimal pnl = d.ClosedTrades.Sum(t => t.RealizedPnl); sb.AppendLine($"- **Ergebnis:** {d.ClosedTrades.Count} Trade(s), realisierter PnL {pnl:F2} USDC"); sb.AppendLine(); if (d.Decisions.Count > 0) { sb.AppendLine("## Entscheidungen"); sb.AppendLine(); sb.AppendLine("| Zeit (UTC) | Entscheidung | Grund | Details |"); sb.AppendLine("|---|---|---|---|"); foreach (var x in d.Decisions) sb.AppendLine($"| {x.Timestamp:dd.MM. HH:mm:ss} | {x.Decision} | {x.Reason} | {Escape(x.Message)} {(string.IsNullOrEmpty(x.ContextJson) ? "" : "`" + x.ContextJson + "`")} |"); sb.AppendLine(); } if (d.OrderEvents.Count > 0) { sb.AppendLine("## Order-Events"); sb.AppendLine(); sb.AppendLine("| Zeit (UTC) | Event | Side | Preis | USDC | Antwort |"); sb.AppendLine("|---|---|---|---|---|---|"); foreach (var x in d.OrderEvents) sb.AppendLine($"| {x.Timestamp:dd.MM. HH:mm:ss} | {x.EventType} | {x.Side} | {x.Price:F3} | {x.AmountUsd:F2} | {Escape(x.Response)} |"); sb.AppendLine(); } if (d.ClosedTrades.Count > 0) { sb.AppendLine("## Abgeschlossene Trades"); sb.AppendLine(); sb.AppendLine("| Geschlossen (UTC) | Entry | Exit | Size | PnL | Grund |"); sb.AppendLine("|---|---|---|---|---|---|"); foreach (var t in d.ClosedTrades) sb.AppendLine($"| {t.ClosedAt:dd.MM. HH:mm:ss} | {t.EntryPrice:F3} | {t.ExitPrice:F3} | {t.Size:F2} | {t.RealizedPnl:F2} | {Escape(t.ExitReason)} |"); sb.AppendLine(); } if (d.LogLines.Count > 0) { sb.AppendLine("## Log-Auszug"); sb.AppendLine(); foreach (var l in d.LogLines) sb.AppendLine($"- `{l.Time}` **{l.Level}** {Escape(l.Message)}"); sb.AppendLine(); } if (d.Decisions.Count == 0 && d.OrderEvents.Count == 0 && d.ClosedTrades.Count == 0) sb.AppendLine("*Keine Daten zu dieser SignalId gefunden.*"); return sb.ToString(); } private static string ShortId(string id) => string.IsNullOrEmpty(id) ? "(ohne SignalId)" : id[..Math.Min(8, id.Length)]; private static string Escape(string s) => (s ?? string.Empty).Replace("|", "\\|").Replace("\r\n", " ").Replace("\n", " "); } }