using IBKRTrader.Core.Configuration;
using IBKRTrader.Core.Analytics;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Persistence;
namespace IBKRTrader.Modules.Supervisor.Services;
/// Kopfzeile eines Signals für die Übersichtsliste des Dossier-Browsers.
public sealed record SignalSummary(
string SignalId, DateTime FirstSeen, string Module, string Symbol,
string Side, string LastDecision, string LastReason, int DecisionCount);
///
/// Beschafft die Daten für Trade-Dossiers: Entscheidungsjournal + Order-Events + Trade-Log +
/// JSONL-Log-Zeilen (per CorrelationId), Zusammenbau/Rendering pur im
/// (Core). Read-only — der Supervisor ist Beobachter.
///
public sealed class DossierService
{
private readonly IDecisionJournal _journal;
private readonly IOrderEventLog _orderEvents;
private readonly TradeLogReader _trades;
private readonly string _logsDirectory;
public DossierService(IDecisionJournal journal, IOrderEventLog orderEvents, TradeLogReader trades)
{
_journal = journal;
_orderEvents = orderEvents;
_trades = trades;
_logsDirectory = AppPaths.Logs;
}
/// Jüngste Signale (gruppiert über das Entscheidungsjournal), neueste zuerst.
public List RecentSignals(int limit = 200)
{
var decisions = _journal.Query(d => d.SignalId != "", limit * 5);
return decisions
.GroupBy(d => d.SignalId)
.Select(g =>
{
var ordered = g.OrderBy(d => d.Timestamp).ToList();
var first = ordered[0];
var last = ordered[^1];
return new SignalSummary(g.Key, first.Timestamp, first.Module, first.Symbol,
first.Side, last.Decision.ToString(), last.Reason.ToString(), ordered.Count);
})
.OrderByDescending(s => s.FirstSeen)
.Take(limit)
.ToList();
}
/// Baut das komplette Dossier zu einer SignalId (inkl. Log-Zeilen aus den JSONL-Tagesdateien).
public TradeDossier BuildForSignal(string signalId)
{
var decisions = _journal.Query(d => d.SignalId == signalId);
var events = _orderEvents.Query(e => e.SignalId == signalId);
var trades = _trades.BySignal(signalId);
var logLines = ReadLogLines(signalId, decisions.Select(d => d.Timestamp).Concat(events.Select(e => e.Timestamp)));
return DossierBuilder.Build(signalId, decisions, events, trades, logLines);
}
///
/// Liest JSONL-Zeilen mit passender CorrelationId — nur aus den Tagesdateien im Zeitfenster der
/// bekannten Ereignisse (±1 Tag), statt alle Logs zu scannen. Fehlertolerant (fehlende Dateien = leer).
///
private List ReadLogLines(string signalId, IEnumerable eventTimes)
{
var result = new List();
var times = eventTimes.ToList();
if (times.Count == 0 || string.IsNullOrEmpty(signalId)) return result;
try
{
var from = times.Min().Date.AddDays(-1);
var to = times.Max().Date.AddDays(1);
for (var day = from; day <= to; day = day.AddDays(1))
{
// Muss zeichengleich zu dem sein, was LoggingService schreibt - sonst sucht der
// Supervisor unter einem Namen, den es nicht gibt.
string path = Path.Combine(
_logsDirectory, FormattableString.Invariant($"{day:yyyy-MM-dd}.jsonl"));
if (!File.Exists(path)) continue;
foreach (var line in File.ReadLines(path))
{
var p = LogJson.ParseLine(line);
if (p != null && p.Cid == signalId) result.Add(p);
}
}
}
catch { /* Log-Auszug ist Beiwerk – Dossier bleibt auch ohne nutzbar */ }
return result;
}
}