Supervisor S-1: Dossier-Generator + Modul-Skelett mit Dossier-Browser + Journal-Nachverdrahtung
Neues Modul PolyTrader.Modules.Supervisor (IPolyTraderModule, Name=Supervisor, DbPrefix=sup_, nur Core-Referenz, strikt read-only): - DossierBuilder (Core/Analytics, pur+getestet): TradeDossier aus Entscheidungen + Order-Events + Trades + Log-Zeilen, chronologisch, mit Markdown-Rendering (Tabellen, Pipe-Escaping). - DossierService (Modul): beschafft Journal/Events/Trade-Log per SignalId + JSONL-Zeilen per CID (nur Tagesdateien im Ereignis-Zeitfenster +-1 Tag); RecentSignals-Uebersicht (Journal gruppiert). - SupervisorMainForm: Dossier-Browser - links juengste Signale, rechts Markdown-Dossier; SignalId-Suche; Analyse-Chat (OpenRouter) folgt in S-2. In Launcher/Smoke registriert. Journal-Nachverdrahtung (S-0-Vervollstaendigung): - TraderMonitor: Profit-Target erzeugt eigene SignalId -> Leiter + Journal (ProfitTargetTriggered); Stale-Cleanup-Cancels als OrderEvents (StaleCleanupCancel). - StartupOrderReconciliation: K2-Cancels als OrderEvents (StartupReconcileCancel). - RF: Demo-Einstiege (DemoFilled, eigene SignalId) + Resolution-Closes (SystemResolutionClose) im Journal - damit sind ALLE Module im Entscheidungsjournal vertreten. Tests: +3 DossierBuilder; 4 Service-Builder auf neue Ctors. Build 0 Fehler, 344 Tests gruen, --smoke-ui: [OK] supervisor.main (alle 5 Views gruen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ca00977ea3
commit
c75a958e36
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using PolyTraderSharp.Models;
|
||||
using PolyTraderSharp.Services;
|
||||
|
||||
namespace PolyTrader.Core.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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<DecisionRecord> Decisions { get; init; } = new();
|
||||
public List<OrderEventRecord> OrderEvents { get; init; } = new();
|
||||
public List<TradeRecord> ClosedTrades { get; init; } = new();
|
||||
public List<LogJson.ParsedLogLine> LogLines { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reiner Zusammenbau + Markdown-Rendering eines Dossiers. Seiteneffektfrei und unit-getestet;
|
||||
/// die Datenbeschaffung (Repos/JSONL) macht der DossierService des Supervisor-Moduls.
|
||||
/// </summary>
|
||||
public static class DossierBuilder
|
||||
{
|
||||
/// <summary>Baut das Dossier aus bereits auf die SignalId gescopten Daten (sortiert chronologisch).</summary>
|
||||
public static TradeDossier Build(
|
||||
string signalId,
|
||||
IEnumerable<DecisionRecord> decisions,
|
||||
IEnumerable<OrderEventRecord> orderEvents,
|
||||
IEnumerable<TradeRecord> closedTrades,
|
||||
IEnumerable<LogJson.ParsedLogLine>? 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<LogJson.ParsedLogLine>()).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Menschenlesbare Markdown-Fassung (Dossier-Browser; auch als KI-Input geeignet).</summary>
|
||||
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", " ");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user