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:
co-authored by
Claude Opus 4.8
parent
eac47369f0
commit
bf3e012f8b
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ namespace PolyTrader.Tests
|
||||
public CopyTradingState Copy = null!;
|
||||
public FakeClobClient Clob = null!;
|
||||
public ChannelReader<ClosedTrade> ClosedReader = null!;
|
||||
public FakeDecisionJournal Journal = null!;
|
||||
}
|
||||
|
||||
private static Harness Build()
|
||||
@@ -40,18 +41,20 @@ namespace PolyTrader.Tests
|
||||
var marketRepo = new FakeMarketRepository();
|
||||
var accountRepo = new FakeAccountRepository();
|
||||
var threema = new ThreemaService(logger, new JobManager());
|
||||
var ladder = new SellLadderService(copy, state, clob, logger, threema, posRepo);
|
||||
var journal = new FakeDecisionJournal();
|
||||
var orderEvents = new FakeOrderEventLog();
|
||||
var ladder = new SellLadderService(copy, state, clob, logger, threema, posRepo, orderEvents);
|
||||
|
||||
var signalCh = Channel.CreateUnbounded<CopySignal>();
|
||||
var closedCh = Channel.CreateUnbounded<ClosedTrade>();
|
||||
|
||||
var engine = new CopyTradingEngine(state, copy, signalCh.Reader, closedCh.Writer, logger,
|
||||
clob, api, posRepo, marketRepo, accountRepo, ladder);
|
||||
clob, api, posRepo, marketRepo, accountRepo, ladder, journal, orderEvents);
|
||||
|
||||
// MarketData cachen, damit der API-Pfad (Cache-Miss) nie läuft.
|
||||
state.MarketCache[Tok] = new MarketData { Slug = "slug", Question = "Frage?", Category = "Politics", TakerFeeBps = 0, NegRisk = false };
|
||||
|
||||
return new Harness { Engine = engine, State = state, Copy = copy, Clob = clob, ClosedReader = closedCh.Reader };
|
||||
return new Harness { Engine = engine, State = state, Copy = copy, Clob = clob, ClosedReader = closedCh.Reader, Journal = journal };
|
||||
}
|
||||
|
||||
private static AccountState Account(bool demo) => new()
|
||||
@@ -88,6 +91,9 @@ namespace PolyTrader.Tests
|
||||
|
||||
Assert.Empty(h.Clob.Placed); // kein Zukauf während des Ausstiegs
|
||||
Assert.True(acc.OpenPositions[Tok].ExitPending);
|
||||
// S-0: Entscheidung strukturiert im Journal (nicht nur Freitext-Log).
|
||||
Assert.Contains(h.Journal.Written, d =>
|
||||
d.Decision == TradeDecision.Skipped && d.Reason == DecisionReason.ExitPendingBuySkip && d.TokenId == Tok);
|
||||
}
|
||||
|
||||
// ---------- Doppel-SELL-Guard ----------
|
||||
@@ -120,6 +126,9 @@ namespace PolyTrader.Tests
|
||||
Assert.False(acc.OpenPositions.ContainsKey(Tok)); // Demo-Position geschlossen
|
||||
Assert.True(h.ClosedReader.TryRead(out var ct)); // ClosedTrade geschrieben
|
||||
Assert.Equal(Tok, ct!.TokenId);
|
||||
Assert.False(string.IsNullOrEmpty(ct.SignalId)); // S-0: Korrelation gesetzt
|
||||
Assert.Contains(h.Journal.Written, d =>
|
||||
d.Decision == TradeDecision.Executed && d.Reason == DecisionReason.SystemResolutionClose);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using PolyTrader.Core.Persistence;
|
||||
using PolyTraderSharp.Models;
|
||||
|
||||
namespace PolyTrader.Tests.Fakes
|
||||
{
|
||||
/// <summary>In-Memory-Stub des Entscheidungsjournals – Tests können Journal-Einträge asserten.</summary>
|
||||
public sealed class FakeDecisionJournal : IDecisionJournal
|
||||
{
|
||||
public List<DecisionRecord> Written { get; } = new();
|
||||
public void Write(DecisionRecord record) => Written.Add(record);
|
||||
public List<DecisionRecord> Query(Expression<Func<DecisionRecord, bool>> predicate, int limit = 1000)
|
||||
=> Written.Where(predicate.Compile()).Take(limit).ToList();
|
||||
}
|
||||
|
||||
/// <summary>In-Memory-Stub des Order-Lifecycle-Logs.</summary>
|
||||
public sealed class FakeOrderEventLog : IOrderEventLog
|
||||
{
|
||||
public List<OrderEventRecord> Written { get; } = new();
|
||||
public void Write(OrderEventRecord record) => Written.Add(record);
|
||||
public List<OrderEventRecord> Query(Expression<Func<OrderEventRecord, bool>> predicate, int limit = 1000)
|
||||
=> Written.Where(predicate.Compile()).Take(limit).ToList();
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ namespace PolyTrader.Tests
|
||||
var logger = new TerminalLogger();
|
||||
var threema = new ThreemaService(logger, new JobManager());
|
||||
var repo = new FakePositionRepository();
|
||||
var svc = new SellLadderService(copy, state, clob, logger, threema, repo);
|
||||
var svc = new SellLadderService(copy, state, clob, logger, threema, repo, new FakeOrderEventLog());
|
||||
return (svc, copy, state, clob, repo);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace PolyTrader.Tests
|
||||
var marketRepo = new FakeMarketRepository();
|
||||
var tradeLog = new FakeCopyTradeLogRepository();
|
||||
var threema = new ThreemaService(logger, new JobManager());
|
||||
var ladder = new SellLadderService(copy, state, clob, logger, threema, posRepo);
|
||||
var ladder = new SellLadderService(copy, state, clob, logger, threema, posRepo, new FakeOrderEventLog());
|
||||
var signalCh = Channel.CreateUnbounded<CopySignal>();
|
||||
var closedCh = Channel.CreateUnbounded<ClosedTrade>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user