Supervisor S-0a/b: Entscheidungsjournal, Order-Events, SignalId, JSONL-Logs

Datenfundament fuer die KI-Analyse (docs/konzepte/KONZEPT-Modul-Supervisor.md, Phase S-0).
Sofortnutzen auch ohne KI: abfragbare Rejects, rekonstruierbare Order-Ketten.

- core_decision_journal (DecisionRecord): JEDE Handelsentscheidung strukturiert -
  Decision (Executed/Rejected/Skipped/Failed) + ReasonCode-Enum (als String, erweiterbar)
  + Kontext-JSON (kulturinvariant) + MarketSlug/EndDate (counterfactual-ready).
- core_order_events (OrderEventRecord): Order-Lifecycle als Daten (Placed/PlaceFailed/
  LadderStart/LadderStep/FloorReplaced/DustAbort ...) inkl. CLOB-Response.
- SignalId (GUID) auf CopySignal, durchgereicht bis ClosedTrade + Core-TradeRecord
  (Dual-Write) und in die Leiter (ExitLadderState.SignalId) -> Dossier-Korrelation.
- CopyTradingEngine: ~20 Entscheidungs-Sites journalisieren zusaetzlich zum Freitext-Log
  (Mode/SellOnly/H3/MaxBuyPrice/PerMaster/TimeLimit/Budget/Balance/Minimum/SpamBlock/
  Leiter/Ownership/NotFound/Teilverkauf/Inkonsistenz/Grace/Demo-Fill/-Close/Live-BUY).
- SellLadderService: Order-Events fuer Start/Step/Floor-Replace/Dust; IOrderEventLog injiziert.
- TerminalLogger: JSONL-Sink (eine Datei/Tag, LogJson pur) + CorrelationId-Support. Dual-Sink.
- Journal-Writes fehlertolerant (brechen den Geld-Pfad nie).
- Migrationen offline generiert UND angewendet (rein additiv): Core (Widen F1 + Journal),
  CopyTrading (SignalId), RF (Initial) - DB war bis AddMarketTakerFeeBps migriert, verifiziert.
- Launcher-Designer: Account-Spalten-Instanziierung wiederhergestellt (war bei Designer-
  Reserialisierung verloren gegangen -> NullReference im Smoke).

Tests: +5 (LogJson, Journal-Roundtrips, SignalId) + Journal-Assertions in Engine-Tests.
Build 0 Fehler, 336 Tests gruen, --smoke-ui komplett gruen (alle Views inkl. RF gegen echte Tabellen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-16 18:48:25 +02:00
co-authored by Claude Opus 4.8
parent eac47369f0
commit bf3e012f8b
28 changed files with 1796 additions and 39 deletions
@@ -0,0 +1,91 @@
using System;
using System.Text.Json;
using PolyTrader.Core.Persistence.Ef;
using PolyTrader.Tests.TestSupport;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
using Xunit;
namespace PolyTrader.Tests
{
/// <summary>
/// Sicherheitsnetz für das S-0-Datenfundament: JSONL-Log-Format (pur) und die
/// Entscheidungsjournal-/Order-Event-Repos (EF InMemory, inkl. Enum-als-String-Roundtrip).
/// </summary>
public class AnalysisJournalTests
{
// ----- LogJson (pur) -----
[Fact]
public void LogJson_produces_valid_single_line_json()
{
var e = new LogMessageEventArgs("Zeile1\nZeile2 mit \"Quotes\" und 0,95 €", LogLevel.Trade, "sig-123");
string line = LogJson.Format(e);
Assert.DoesNotContain('\n', line); // eine Zeile (JSONL)
using var doc = JsonDocument.Parse(line); // valides JSON
Assert.Equal("Trade", doc.RootElement.GetProperty("level").GetString());
Assert.Equal("sig-123", doc.RootElement.GetProperty("cid").GetString());
Assert.Contains("Zeile2", doc.RootElement.GetProperty("msg").GetString());
}
[Fact]
public void LogJson_omits_empty_correlation_id()
{
string line = LogJson.Format(new LogMessageEventArgs("msg", LogLevel.Info));
using var doc = JsonDocument.Parse(line);
Assert.False(doc.RootElement.TryGetProperty("cid", out _));
}
// ----- Repos (EF InMemory) -----
private static InMemoryContextFactory<CoreDbContext> Factory() => new(o => new CoreDbContext(o));
[Fact]
public void DecisionJournal_write_and_query_roundtrip()
{
var journal = new EfDecisionJournal(Factory());
journal.Write(new DecisionRecord
{
SignalId = "sig-1", ModuleName = "CopyTrading", AccountId = 1, TokenId = "tok",
Side = "BUY", Decision = TradeDecision.Rejected, Reason = DecisionReason.MaxBuyPriceExceeded,
SignalPrice = 0.97m, Message = "test"
});
journal.Write(new DecisionRecord
{
SignalId = "sig-2", AccountId = 1, TokenId = "tok",
Decision = TradeDecision.Executed, Reason = DecisionReason.OrderPlaced
});
var rejects = journal.Query(d => d.Reason == DecisionReason.MaxBuyPriceExceeded);
Assert.Single(rejects);
Assert.Equal("sig-1", rejects[0].SignalId);
Assert.Equal(TradeDecision.Rejected, rejects[0].Decision); // Enum-Roundtrip
Assert.Equal(2, journal.Query(d => d.AccountId == 1).Count);
}
[Fact]
public void OrderEventLog_write_and_query_roundtrip()
{
var log = new EfOrderEventLog(Factory());
log.Write(new OrderEventRecord
{
SignalId = "sig-1", AccountId = 1, TokenId = "tok",
EventType = OrderEventType.LadderStep, Side = "SELL", Price = 0.42m, Response = "OK"
});
var events = log.Query(e => e.SignalId == "sig-1");
Assert.Single(events);
Assert.Equal(OrderEventType.LadderStep, events[0].EventType);
}
[Fact]
public void CopySignal_gets_signal_id_automatically()
{
var a = new CopySignal();
var b = new CopySignal();
Assert.False(string.IsNullOrEmpty(a.SignalId));
Assert.NotEqual(a.SignalId, b.SignalId);
}
}
}