Im Logverzeichnis der Test-VM lagen neben 2026-09-03.jsonl auch 1405-06-12.jsonl und 2569-09-03.jsonl - dieselben Tage im Sonnen-Hidschra- und im buddhistischen Kalender. Eine interpolierte Zeichenkette formatiert mit CurrentCulture, und die bringt ihren Kalender mit. Betroffen waren beide Schreibpfade des LoggingService und die Lesestelle im DossierService, die denselben Namen wieder zusammensetzt. CA1305 greift hier nicht: die Regel sieht string.Format und ToString, nicht die interpolierte Form. Die .editorconfig deckt diesen Fall also nicht ab. FormattableString.Invariant an allen drei Stellen, dazu ein Regressionstest ueber fa-IR, th-TH und de-DE - mit Gegenbeweis, dass die naive Form tatsaechlich abweicht, damit der Test nicht eines Tages unbemerkt trivial wird. Ausserdem: Verweise auf die Bugtracker-Nummern richtiggestellt (488/489). 461 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
4.0 KiB
C#
94 lines
4.0 KiB
C#
using IBKRTrader.Core.Configuration;
|
||
using IBKRTrader.Core.Analytics;
|
||
using IBKRTrader.Core.Logging;
|
||
using IBKRTrader.Core.Persistence;
|
||
|
||
namespace IBKRTrader.Modules.Supervisor.Services;
|
||
|
||
/// <summary>Kopfzeile eines Signals für die Übersichtsliste des Dossier-Browsers.</summary>
|
||
public sealed record SignalSummary(
|
||
string SignalId, DateTime FirstSeen, string Module, string Symbol,
|
||
string Side, string LastDecision, string LastReason, int DecisionCount);
|
||
|
||
/// <summary>
|
||
/// Beschafft die Daten für Trade-Dossiers: Entscheidungsjournal + Order-Events + Trade-Log +
|
||
/// JSONL-Log-Zeilen (per CorrelationId), Zusammenbau/Rendering pur im <see cref="DossierBuilder"/>
|
||
/// (Core). Read-only — der Supervisor ist Beobachter.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Jüngste Signale (gruppiert über das Entscheidungsjournal), neueste zuerst.</summary>
|
||
public List<SignalSummary> 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();
|
||
}
|
||
|
||
/// <summary>Baut das komplette Dossier zu einer SignalId (inkl. Log-Zeilen aus den JSONL-Tagesdateien).</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
private List<LogJson.ParsedLogLine> ReadLogLines(string signalId, IEnumerable<DateTime> eventTimes)
|
||
{
|
||
var result = new List<LogJson.ParsedLogLine>();
|
||
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;
|
||
}
|
||
}
|