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
Generated
+28
-21
@@ -25,6 +25,8 @@ namespace PolyTraderSharp.Ui
|
||||
btn_settings = new ToolStripButton();
|
||||
btn_terminal = new ToolStripButton();
|
||||
btn_jobs = new ToolStripButton();
|
||||
btn_accounting = new ToolStripButton();
|
||||
toolStripSeparator1 = new ToolStripSeparator();
|
||||
toolstrip_quickbar = new ToolStrip();
|
||||
btn_liveTrading = new ToolStripButton();
|
||||
btn_demoTrading = new ToolStripButton();
|
||||
@@ -40,8 +42,6 @@ namespace PolyTraderSharp.Ui
|
||||
colAccPnl3d = new DataGridViewTextBoxColumn();
|
||||
colAccWin3d = new DataGridViewTextBoxColumn();
|
||||
colAccOverall = new DataGridViewTextBoxColumn();
|
||||
btn_accounting = new ToolStripButton();
|
||||
toolStripSeparator1 = new ToolStripSeparator();
|
||||
menuStrip.SuspendLayout();
|
||||
toolstrip_windows.SuspendLayout();
|
||||
toolstrip_quickbar.SuspendLayout();
|
||||
@@ -122,6 +122,21 @@ namespace PolyTraderSharp.Ui
|
||||
btn_jobs.Text = "Server Jobs";
|
||||
btn_jobs.TextImageRelation = TextImageRelation.ImageAboveText;
|
||||
//
|
||||
// btn_accounting
|
||||
//
|
||||
btn_accounting.Image = Properties.Resources.coins_in_hand;
|
||||
btn_accounting.ImageScaling = ToolStripItemImageScaling.None;
|
||||
btn_accounting.ImageTransparentColor = Color.Magenta;
|
||||
btn_accounting.Name = "btn_accounting";
|
||||
btn_accounting.Size = new Size(106, 65);
|
||||
btn_accounting.Text = "Accounting";
|
||||
btn_accounting.TextImageRelation = TextImageRelation.ImageAboveText;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(6, 70);
|
||||
//
|
||||
// toolstrip_quickbar
|
||||
//
|
||||
toolstrip_quickbar.ImageScalingSize = new Size(24, 24);
|
||||
@@ -179,10 +194,9 @@ namespace PolyTraderSharp.Ui
|
||||
//
|
||||
dgv_accountlist.AllowUserToAddRows = false;
|
||||
dgv_accountlist.AllowUserToDeleteRows = false;
|
||||
dgv_accountlist.AutoGenerateColumns = false;
|
||||
dgv_accountlist.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;
|
||||
dgv_accountlist.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
dgv_accountlist.Columns.AddRange(new DataGridViewColumn[] {
|
||||
colAccName, colAccModules, colAccPoly, colAccBalance, colAccPnl3d, colAccWin3d, colAccOverall });
|
||||
dgv_accountlist.Columns.AddRange(new DataGridViewColumn[] { colAccName, colAccModules, colAccPoly, colAccBalance, colAccPnl3d, colAccWin3d, colAccOverall });
|
||||
dgv_accountlist.Dock = DockStyle.Fill;
|
||||
dgv_accountlist.Location = new Point(0, 139);
|
||||
dgv_accountlist.Name = "dgv_accountlist";
|
||||
@@ -197,6 +211,7 @@ namespace PolyTraderSharp.Ui
|
||||
//
|
||||
colAccName.DataPropertyName = "Name";
|
||||
colAccName.HeaderText = "Account";
|
||||
colAccName.MinimumWidth = 8;
|
||||
colAccName.Name = "colAccName";
|
||||
colAccName.ReadOnly = true;
|
||||
colAccName.Width = 200;
|
||||
@@ -205,6 +220,7 @@ namespace PolyTraderSharp.Ui
|
||||
//
|
||||
colAccModules.DataPropertyName = "Modules";
|
||||
colAccModules.HeaderText = "Module";
|
||||
colAccModules.MinimumWidth = 8;
|
||||
colAccModules.Name = "colAccModules";
|
||||
colAccModules.ReadOnly = true;
|
||||
colAccModules.Width = 220;
|
||||
@@ -212,15 +228,18 @@ namespace PolyTraderSharp.Ui
|
||||
// colAccPoly
|
||||
//
|
||||
colAccPoly.HeaderText = "Polymarket";
|
||||
colAccPoly.MinimumWidth = 8;
|
||||
colAccPoly.Name = "colAccPoly";
|
||||
colAccPoly.ReadOnly = true;
|
||||
colAccPoly.Text = "Öffnen";
|
||||
colAccPoly.UseColumnTextForButtonValue = true;
|
||||
colAccPoly.Width = 100;
|
||||
colAccPoly.Width = 106;
|
||||
//
|
||||
// colAccBalance
|
||||
//
|
||||
colAccBalance.DataPropertyName = "Balance";
|
||||
colAccBalance.HeaderText = "Wallet (USDC)";
|
||||
colAccBalance.MinimumWidth = 8;
|
||||
colAccBalance.Name = "colAccBalance";
|
||||
colAccBalance.ReadOnly = true;
|
||||
colAccBalance.Width = 130;
|
||||
@@ -229,6 +248,7 @@ namespace PolyTraderSharp.Ui
|
||||
//
|
||||
colAccPnl3d.DataPropertyName = "Pnl3d";
|
||||
colAccPnl3d.HeaderText = "3T PnL";
|
||||
colAccPnl3d.MinimumWidth = 8;
|
||||
colAccPnl3d.Name = "colAccPnl3d";
|
||||
colAccPnl3d.ReadOnly = true;
|
||||
colAccPnl3d.Width = 110;
|
||||
@@ -237,6 +257,7 @@ namespace PolyTraderSharp.Ui
|
||||
//
|
||||
colAccWin3d.DataPropertyName = "WinRate3d";
|
||||
colAccWin3d.HeaderText = "3T Winrate %";
|
||||
colAccWin3d.MinimumWidth = 8;
|
||||
colAccWin3d.Name = "colAccWin3d";
|
||||
colAccWin3d.ReadOnly = true;
|
||||
colAccWin3d.Width = 110;
|
||||
@@ -245,25 +266,11 @@ namespace PolyTraderSharp.Ui
|
||||
//
|
||||
colAccOverall.DataPropertyName = "OverallPnl";
|
||||
colAccOverall.HeaderText = "Overall P/L";
|
||||
colAccOverall.MinimumWidth = 8;
|
||||
colAccOverall.Name = "colAccOverall";
|
||||
colAccOverall.ReadOnly = true;
|
||||
colAccOverall.Width = 130;
|
||||
//
|
||||
// btn_accounting
|
||||
//
|
||||
btn_accounting.Image = Properties.Resources.coins_in_hand;
|
||||
btn_accounting.ImageScaling = ToolStripItemImageScaling.None;
|
||||
btn_accounting.ImageTransparentColor = Color.Magenta;
|
||||
btn_accounting.Name = "btn_accounting";
|
||||
btn_accounting.Size = new Size(106, 65);
|
||||
btn_accounting.Text = "Accounting";
|
||||
btn_accounting.TextImageRelation = TextImageRelation.ImageAboveText;
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
toolStripSeparator1.Size = new Size(6, 70);
|
||||
//
|
||||
// LauncherForm
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(10F, 25F);
|
||||
|
||||
@@ -24,6 +24,10 @@ namespace PolyTrader.Core.DependencyInjection
|
||||
services.AddSingleton<IPositionRepository, EfPositionRepository>();
|
||||
services.AddSingleton<ITradeLogRepository, EfTradeLogRepository>();
|
||||
|
||||
// S-0 (Supervisor-Konzept): Entscheidungsjournal + Order-Lifecycle-Events.
|
||||
services.AddSingleton<IDecisionJournal, EfDecisionJournal>();
|
||||
services.AddSingleton<IOrderEventLog, EfOrderEventLog>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@ namespace PolyTraderSharp.Models
|
||||
{
|
||||
public class CopySignal
|
||||
{
|
||||
/// <summary>
|
||||
/// Korrelations-ID (S-0, Supervisor-Konzept): verbindet dieses Signal mit allen daraus
|
||||
/// entstehenden Entscheidungen (core_decision_journal), Orders (core_order_events) und
|
||||
/// dem ClosedTrade. Wird bei Erzeugung automatisch vergeben.
|
||||
/// </summary>
|
||||
public string SignalId { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
public int SourceTradeId { get; set; }
|
||||
public int TraderId { get; set; }
|
||||
public string MarketSlug { get; set; } = string.Empty;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
|
||||
namespace PolyTraderSharp.Models
|
||||
{
|
||||
/// <summary>Ausgang einer Handelsentscheidung im Entscheidungsjournal.</summary>
|
||||
public enum TradeDecision
|
||||
{
|
||||
Executed, // Aktion ausgeführt (Order platziert / Demo-Fill / Leiter gestartet)
|
||||
Rejected, // aktiv abgelehnt (Risk-/Plausibilitätsregel)
|
||||
Skipped, // bewusst übersprungen (z. B. ExitPending, Spam-Blockade, Modus)
|
||||
Failed // versucht, aber fehlgeschlagen (z. B. Order-Fehler)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strukturierter Grund einer Entscheidung (statt Freitext). Wird als STRING persistiert –
|
||||
/// neue Werte können gefahrlos ergänzt werden. Die Codes decken die heutigen
|
||||
/// TradeReasoning-/Reject-Stellen von Engine, Leiter und Monitor ab.
|
||||
/// </summary>
|
||||
public enum DecisionReason
|
||||
{
|
||||
None = 0,
|
||||
|
||||
// ----- Modus / Zustand -----
|
||||
ModeInactive, // Live-/Demo-Trading deaktiviert
|
||||
SellOnlyModeBuyBlocked, // SellOnly-Modus blockiert BUY
|
||||
TraderInactive, // Master nicht gefunden / inaktiv
|
||||
AccountInactive, // Account nicht gefunden / inaktiv
|
||||
|
||||
// ----- BUY-Pfad -----
|
||||
MaxBuyPriceExceeded, // Signalpreis über MaxBuyPrice
|
||||
ExitPendingBuySkip, // H3: SELL-Leiter aktiv – kein Zukauf
|
||||
TimeWindowLimitReached, // Zeitfenster-Budget (6h/24h/72h/None) erschöpft
|
||||
MarketBudgetExhausted, // PerMarket-Budget erschöpft
|
||||
PerMasterLimitReached, // PerMaster-Budget erschöpft
|
||||
InsufficientBalance, // verfügbares Guthaben reicht nicht
|
||||
BelowPolymarketMinimum, // Order unter Minimum (Shares/USDC)
|
||||
MarketExpiredOrTooClose, // EndDate-Filter
|
||||
DuplicateOrPendingOrder, // bereits offene/pending Order
|
||||
|
||||
// ----- SELL-Pfad -----
|
||||
SellSpamBlock, // SELL <20s nach letztem SELL
|
||||
LadderAlreadyActive, // Eskalationsleiter läuft bereits
|
||||
PositionNotFound, // keine passende Position im Portfolio
|
||||
OwnershipMismatch, // Position gehört anderem Trader (Safety)
|
||||
PartialSellBelowThreshold, // Teilverkauf unter Signifikanz-Schwelle
|
||||
MasterPositionInconsistent, // Master hält laut Tracking 0 Shares
|
||||
SyncGracePeriod, // kein Tracking + Haltezeit < Schonfrist
|
||||
|
||||
// ----- Ausführung -----
|
||||
OrderPlaced, // Order erfolgreich platziert
|
||||
OrderFailed, // CLOB-Fehler beim Platzieren
|
||||
DemoFilled, // Demo-Fill gebucht
|
||||
DemoClosed, // Demo-Position geschlossen
|
||||
LadderStarted, // SELL-Leiter gestartet
|
||||
LadderStartFailed, // Leiter-Startorder fehlgeschlagen
|
||||
LadderDustAbort, // H4: Dust-Rest unter Minimum – Leiter beendet
|
||||
ProfitTargetTriggered, // Take-Profit hat Exit ausgelöst
|
||||
SystemResolutionClose // System-Close bei Marktauflösung (TraderId==0)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eine Zeile im Entscheidungsjournal (core_decision_journal): JEDE Handelsentscheidung –
|
||||
/// ausgeführt, abgelehnt oder übersprungen – strukturiert und abfragbar. Grundlage für
|
||||
/// Supervisor-Analysen („warum (nicht) gehandelt?") und Counterfactual-Auswertungen
|
||||
/// (MarketSlug/EndDate sind dafür bewusst enthalten). Siehe docs/konzepte/KONZEPT-Modul-Supervisor.md.
|
||||
/// </summary>
|
||||
public class DecisionRecord
|
||||
{
|
||||
public long Id { get; set; } // DB-Autoincrement
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Korrelation: verbindet Signal → Entscheidungen → Orders → ClosedTrade.</summary>
|
||||
public string SignalId { get; set; } = string.Empty;
|
||||
|
||||
public string ModuleName { get; set; } = string.Empty;
|
||||
public int AccountId { get; set; }
|
||||
public bool IsDemo { get; set; }
|
||||
public int SourceTraderId { get; set; }
|
||||
|
||||
public string TokenId { get; set; } = string.Empty;
|
||||
public string MarketSlug { get; set; } = string.Empty; // counterfactual-ready
|
||||
public string MarketQuestion { get; set; } = string.Empty;
|
||||
public string Side { get; set; } = string.Empty; // BUY/SELL
|
||||
public decimal SignalPrice { get; set; }
|
||||
public DateTime? MarketEndDate { get; set; } // counterfactual-ready
|
||||
|
||||
public TradeDecision Decision { get; set; }
|
||||
public DecisionReason Reason { get; set; }
|
||||
|
||||
/// <summary>Kompakte Kontext-Zahlen als JSON (Limitwerte, Budgets, berechnete Größen …).</summary>
|
||||
public string ContextJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Menschlicher Begründungstext (wie bisher im Log).</summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace PolyTraderSharp.Models
|
||||
{
|
||||
/// <summary>Art eines Order-Lifecycle-Ereignisses (als String persistiert – erweiterbar).</summary>
|
||||
public enum OrderEventType
|
||||
{
|
||||
Placed, // Order ans CLOB gesendet, Ergebnis in Response
|
||||
PlaceFailed, // Platzierung fehlgeschlagen
|
||||
Cancelled, // Order storniert (inkl. Grund in DetailsJson)
|
||||
LadderStart, // erste GTC-Order der SELL-Leiter
|
||||
LadderStep, // Nachpreisen eine Stufe tiefer
|
||||
FloorHold, // Floor erreicht, Order ruht
|
||||
FloorReplaced, // K1b: fehlende Floor-Order neu platziert
|
||||
DustAbort, // H4: Leiter wegen Dust-Rest beendet
|
||||
StartupReconcileCancel, // K2: verwaiste Order beim Start storniert
|
||||
StaleCleanupCancel // Stale-Order-Cleanup hat storniert
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ein Order-Lifecycle-Ereignis (core_order_events): Platzierungen, CLOB-Antworten, Cancels,
|
||||
/// Leiter-Stufen – als Daten statt nur als Log. Zusammen mit dem Entscheidungsjournal ergibt das
|
||||
/// die vollständige, rekonstruierbare Kette je Signal (Dossier).
|
||||
/// </summary>
|
||||
public class OrderEventRecord
|
||||
{
|
||||
public long Id { get; set; } // DB-Autoincrement
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public string SignalId { get; set; } = string.Empty;
|
||||
public string ModuleName { get; set; } = string.Empty;
|
||||
public int AccountId { get; set; }
|
||||
public string TokenId { get; set; } = string.Empty;
|
||||
|
||||
public OrderEventType EventType { get; set; }
|
||||
public string Side { get; set; } = string.Empty; // BUY/SELL
|
||||
public decimal Price { get; set; }
|
||||
public decimal AmountUsd { get; set; }
|
||||
public string OrderType { get; set; } = string.Empty; // GTC/GTD/FOK/MARKET
|
||||
|
||||
/// <summary>CLOB-Antwort ("OK" oder Fehlertext) bzw. Ergebnis der Aktion.</summary>
|
||||
public string Response { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Zusatzkontext als kompaktes JSON (z. B. Leiter-Stufe, Floor, Timeout-Grund).</summary>
|
||||
public string DetailsJson { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -34,5 +34,8 @@ namespace PolyTraderSharp.Models
|
||||
public DateTime OpenedAt { get; set; }
|
||||
public DateTime ClosedAt { get; set; }
|
||||
public string ExitReason { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Korrelation zum Entscheidungsjournal/Order-Events (leer bei Alt-Daten).</summary>
|
||||
public string SignalId { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace PolyTrader.Core.Persistence.Ef
|
||||
public DbSet<Position> Positions => Set<Position>();
|
||||
public DbSet<MarketData> Markets => Set<MarketData>();
|
||||
public DbSet<TradeRecord> TradeLog => Set<TradeRecord>();
|
||||
public DbSet<DecisionRecord> DecisionJournal => Set<DecisionRecord>();
|
||||
public DbSet<OrderEventRecord> OrderEvents => Set<OrderEventRecord>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder b)
|
||||
{
|
||||
@@ -93,6 +95,52 @@ namespace PolyTrader.Core.Persistence.Ef
|
||||
e.HasIndex(x => x.ClosedAt);
|
||||
e.HasIndex(x => x.ModuleName);
|
||||
e.HasIndex(x => x.AccountId);
|
||||
e.Property(x => x.SignalId).HasMaxLength(40); // Korrelation zum Entscheidungsjournal
|
||||
});
|
||||
|
||||
// Entscheidungsjournal (S-0, Supervisor-Konzept): jede Handelsentscheidung strukturiert.
|
||||
b.Entity<DecisionRecord>(e =>
|
||||
{
|
||||
e.ToTable("core_decision_journal");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.Id).ValueGeneratedOnAdd();
|
||||
e.Property(x => x.SignalId).HasMaxLength(40);
|
||||
e.Property(x => x.ModuleName).HasMaxLength(64);
|
||||
e.Property(x => x.TokenId).HasMaxLength(120);
|
||||
e.Property(x => x.MarketSlug).HasMaxLength(300);
|
||||
e.Property(x => x.MarketQuestion).HasMaxLength(1000);
|
||||
e.Property(x => x.Side).HasMaxLength(10);
|
||||
e.Property(x => x.SignalPrice).HasPrecision(18, 6);
|
||||
// Enums als String: lesbar in der DB, gefahrlos erweiterbar.
|
||||
e.Property(x => x.Decision).HasConversion<string>().HasMaxLength(20);
|
||||
e.Property(x => x.Reason).HasConversion<string>().HasMaxLength(50);
|
||||
e.Property(x => x.ContextJson).HasMaxLength(2000);
|
||||
e.Property(x => x.Message).HasMaxLength(2000);
|
||||
e.HasIndex(x => x.Timestamp);
|
||||
e.HasIndex(x => x.SignalId);
|
||||
e.HasIndex(x => new { x.AccountId, x.TokenId });
|
||||
e.HasIndex(x => x.Reason);
|
||||
});
|
||||
|
||||
// Order-Lifecycle-Events (S-0): Platzierungen/Antworten/Cancels/Leiter-Stufen als Daten.
|
||||
b.Entity<OrderEventRecord>(e =>
|
||||
{
|
||||
e.ToTable("core_order_events");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.Id).ValueGeneratedOnAdd();
|
||||
e.Property(x => x.SignalId).HasMaxLength(40);
|
||||
e.Property(x => x.ModuleName).HasMaxLength(64);
|
||||
e.Property(x => x.TokenId).HasMaxLength(120);
|
||||
e.Property(x => x.EventType).HasConversion<string>().HasMaxLength(30);
|
||||
e.Property(x => x.Side).HasMaxLength(10);
|
||||
e.Property(x => x.Price).HasPrecision(18, 6);
|
||||
e.Property(x => x.AmountUsd).HasPrecision(18, 6);
|
||||
e.Property(x => x.OrderType).HasMaxLength(10);
|
||||
e.Property(x => x.Response).HasMaxLength(1000);
|
||||
e.Property(x => x.DetailsJson).HasMaxLength(2000);
|
||||
e.HasIndex(x => x.Timestamp);
|
||||
e.HasIndex(x => x.SignalId);
|
||||
e.HasIndex(x => new { x.AccountId, x.TokenId });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PolyTraderSharp.Models;
|
||||
|
||||
namespace PolyTrader.Core.Persistence.Ef
|
||||
{
|
||||
/// <summary>
|
||||
/// EF-Implementierung des Entscheidungsjournals. Write ist bewusst fehlertolerant:
|
||||
/// ein Journal-/DB-Fehler darf den Geld-Pfad (Engine/Leiter) niemals brechen –
|
||||
/// er wird auf der Konsole gemeldet und verworfen.
|
||||
/// </summary>
|
||||
public class EfDecisionJournal : IDecisionJournal
|
||||
{
|
||||
private readonly IDbContextFactory<CoreDbContext> _factory;
|
||||
public EfDecisionJournal(IDbContextFactory<CoreDbContext> factory) => _factory = factory;
|
||||
|
||||
public void Write(DecisionRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
ctx.DecisionJournal.Add(record);
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DecisionJournal] Write fehlgeschlagen (ignoriert): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public List<DecisionRecord> Query(Expression<Func<DecisionRecord, bool>> predicate, int limit = 1000)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.DecisionJournal.AsNoTracking()
|
||||
.Where(predicate)
|
||||
.OrderByDescending(r => r.Timestamp)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>EF-Implementierung des Order-Lifecycle-Logs (gleiche Robustheits-Garantie).</summary>
|
||||
public class EfOrderEventLog : IOrderEventLog
|
||||
{
|
||||
private readonly IDbContextFactory<CoreDbContext> _factory;
|
||||
public EfOrderEventLog(IDbContextFactory<CoreDbContext> factory) => _factory = factory;
|
||||
|
||||
public void Write(OrderEventRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
ctx.OrderEvents.Add(record);
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[OrderEventLog] Write fehlgeschlagen (ignoriert): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public List<OrderEventRecord> Query(Expression<Func<OrderEventRecord, bool>> predicate, int limit = 1000)
|
||||
{
|
||||
using var ctx = _factory.CreateDbContext();
|
||||
return ctx.OrderEvents.AsNoTracking()
|
||||
.Where(predicate)
|
||||
.OrderByDescending(r => r.Timestamp)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using PolyTrader.Core.Persistence.Ef;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PolyTrader.Core.Persistence.Ef.Migrations
|
||||
{
|
||||
[DbContext(typeof(CoreDbContext))]
|
||||
[Migration("20260716101647_AddDecisionJournalAndOrderEvents")]
|
||||
partial class AddDecisionJournalAndOrderEvents
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.AccountState", b =>
|
||||
{
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("varchar(256)");
|
||||
|
||||
b.Property<string>("ApiPassphrase")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<string>("ApiSecret")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<decimal>("AvailableBalance")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<bool>("CloseOnlyMode")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("HasOpenLimitOrders")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsDemo")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("PayoutAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<decimal>("PayoutLimitUsd")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("PrivateKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("varchar(512)");
|
||||
|
||||
b.Property<decimal>("TotalBalance")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("WalletAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.HasKey("AccountId");
|
||||
|
||||
b.ToTable("core_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.DecisionRecord", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContextJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("varchar(2000)");
|
||||
|
||||
b.Property<string>("Decision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<bool>("IsDemo")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("MarketEndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("MarketQuestion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("MarketSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("varchar(2000)");
|
||||
|
||||
b.Property<string>("ModuleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<decimal>("SignalPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<int>("SourceTraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Reason");
|
||||
|
||||
b.HasIndex("SignalId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.HasIndex("AccountId", "TokenId");
|
||||
|
||||
b.ToTable("core_decision_journal", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.MarketData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("ClobTokenIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Closed")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("ConditionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("NegRisk")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Outcomes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Question")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<int>("TakerFeeBps")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("core_markets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.OrderEventRecord", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("AmountUsd")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("DetailsJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("varchar(2000)");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("varchar(30)");
|
||||
|
||||
b.Property<string>("ModuleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("OrderType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SignalId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.HasIndex("AccountId", "TokenId");
|
||||
|
||||
b.ToTable("core_order_events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.Position", b =>
|
||||
{
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsDemo")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<decimal>("AmountUsd")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("ConditionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<decimal>("CurrentPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("CurrentValueUsd")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("EntryPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<DateTime?>("ExpiryDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("MarketQuestion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("MarketSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<DateTime>("OpenedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("SourceTraderAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<int>("SourceTraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SourceTraderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.HasKey("AccountId", "IsDemo", "TokenId");
|
||||
|
||||
b.ToTable("core_positions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.TradeRecord", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("EntryPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("ExitPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("ExitReason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<bool>("IsDemo")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("MarketQuestion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("ModuleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<DateTime>("OpenedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<decimal>("PnlPercent")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("RealizedPnl")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("ClosedAt");
|
||||
|
||||
b.HasIndex("ModuleName");
|
||||
|
||||
b.ToTable("core_trade_log", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PolyTrader.Core.Persistence.Ef.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDecisionJournalAndOrderEvents : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SignalId",
|
||||
table: "core_trade_log",
|
||||
type: "varchar(40)",
|
||||
maxLength: 40,
|
||||
nullable: false,
|
||||
defaultValue: "")
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "core_decision_journal",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
SignalId = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ModuleName = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
AccountId = table.Column<int>(type: "int", nullable: false),
|
||||
IsDemo = table.Column<bool>(type: "tinyint(1)", nullable: false),
|
||||
SourceTraderId = table.Column<int>(type: "int", nullable: false),
|
||||
TokenId = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
MarketSlug = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
MarketQuestion = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Side = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
SignalPrice = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
|
||||
MarketEndDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||
Decision = table.Column<string>(type: "varchar(20)", maxLength: 20, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Reason = table.Column<string>(type: "varchar(50)", maxLength: 50, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ContextJson = table.Column<string>(type: "varchar(2000)", maxLength: 2000, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Message = table.Column<string>(type: "varchar(2000)", maxLength: 2000, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_core_decision_journal", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "core_order_events",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
SignalId = table.Column<string>(type: "varchar(40)", maxLength: 40, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
ModuleName = table.Column<string>(type: "varchar(64)", maxLength: 64, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
AccountId = table.Column<int>(type: "int", nullable: false),
|
||||
TokenId = table.Column<string>(type: "varchar(120)", maxLength: 120, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
EventType = table.Column<string>(type: "varchar(30)", maxLength: 30, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Side = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Price = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
|
||||
AmountUsd = table.Column<decimal>(type: "decimal(18,6)", precision: 18, scale: 6, nullable: false),
|
||||
OrderType = table.Column<string>(type: "varchar(10)", maxLength: 10, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Response = table.Column<string>(type: "varchar(1000)", maxLength: 1000, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
DetailsJson = table.Column<string>(type: "varchar(2000)", maxLength: 2000, nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_core_order_events", x => x.Id);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_decision_journal_AccountId_TokenId",
|
||||
table: "core_decision_journal",
|
||||
columns: new[] { "AccountId", "TokenId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_decision_journal_Reason",
|
||||
table: "core_decision_journal",
|
||||
column: "Reason");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_decision_journal_SignalId",
|
||||
table: "core_decision_journal",
|
||||
column: "SignalId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_decision_journal_Timestamp",
|
||||
table: "core_decision_journal",
|
||||
column: "Timestamp");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_order_events_AccountId_TokenId",
|
||||
table: "core_order_events",
|
||||
columns: new[] { "AccountId", "TokenId" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_order_events_SignalId",
|
||||
table: "core_order_events",
|
||||
column: "SignalId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_core_order_events_Timestamp",
|
||||
table: "core_order_events",
|
||||
column: "Timestamp");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "core_decision_journal");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "core_order_events");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SignalId",
|
||||
table: "core_trade_log");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,96 @@ namespace PolyTrader.Core.Persistence.Ef.Migrations
|
||||
b.ToTable("core_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.DecisionRecord", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ContextJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("varchar(2000)");
|
||||
|
||||
b.Property<string>("Decision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<bool>("IsDemo")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("MarketEndDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("MarketQuestion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("MarketSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("varchar(2000)");
|
||||
|
||||
b.Property<string>("ModuleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<decimal>("SignalPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<int>("SourceTraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Reason");
|
||||
|
||||
b.HasIndex("SignalId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.HasIndex("AccountId", "TokenId");
|
||||
|
||||
b.ToTable("core_decision_journal", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.MarketData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@@ -145,6 +235,79 @@ namespace PolyTrader.Core.Persistence.Ef.Migrations
|
||||
b.ToTable("core_markets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.OrderEventRecord", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("AmountUsd")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("DetailsJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("varchar(2000)");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("varchar(30)");
|
||||
|
||||
b.Property<string>("ModuleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<string>("OrderType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("Response")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SignalId");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.HasIndex("AccountId", "TokenId");
|
||||
|
||||
b.ToTable("core_order_events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.Position", b =>
|
||||
{
|
||||
b.Property<int>("AccountId")
|
||||
@@ -285,6 +448,11 @@ namespace PolyTrader.Core.Persistence.Ef.Migrations
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using PolyTraderSharp.Models;
|
||||
|
||||
namespace PolyTrader.Core.Persistence
|
||||
{
|
||||
/// <summary>
|
||||
/// Entscheidungsjournal (core_decision_journal). Write darf den Trading-Pfad NIEMALS brechen –
|
||||
/// Implementierungen fangen Persistenzfehler ab (Log statt Exception).
|
||||
/// </summary>
|
||||
public interface IDecisionJournal
|
||||
{
|
||||
void Write(DecisionRecord record);
|
||||
List<DecisionRecord> Query(Expression<Func<DecisionRecord, bool>> predicate, int limit = 1000);
|
||||
}
|
||||
|
||||
/// <summary>Order-Lifecycle-Log (core_order_events). Gleiche Robustheits-Garantie wie das Journal.</summary>
|
||||
public interface IOrderEventLog
|
||||
{
|
||||
void Write(OrderEventRecord record);
|
||||
List<OrderEventRecord> Query(Expression<Func<OrderEventRecord, bool>> predicate, int limit = 1000);
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,39 @@ namespace PolyTraderSharp.Services
|
||||
public LogLevel Level { get; }
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public LogMessageEventArgs(string message, LogLevel level)
|
||||
/// <summary>Korrelations-ID (z. B. SignalId) für die Log-Forensik; leer wenn ohne Kontext.</summary>
|
||||
public string CorrelationId { get; }
|
||||
|
||||
public LogMessageEventArgs(string message, LogLevel level, string correlationId = "")
|
||||
{
|
||||
Message = message;
|
||||
Level = level;
|
||||
Timestamp = DateTime.Now;
|
||||
CorrelationId = correlationId ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reiner JSONL-Formatter für Log-Events (S-0, Supervisor-Konzept): eine JSON-Zeile je Event –
|
||||
/// append-fähig, streambar, maschinen-/KI-lesbar. Statisch und seiteneffektfrei → unit-getestet.
|
||||
/// </summary>
|
||||
public static class LogJson
|
||||
{
|
||||
public static string Format(LogMessageEventArgs e)
|
||||
{
|
||||
var obj = new
|
||||
{
|
||||
ts = e.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffzzz"),
|
||||
level = e.Level.ToString(),
|
||||
cid = string.IsNullOrEmpty(e.CorrelationId) ? null : e.CorrelationId,
|
||||
msg = e.Message
|
||||
};
|
||||
return System.Text.Json.JsonSerializer.Serialize(obj,
|
||||
new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +90,11 @@ namespace PolyTraderSharp.Services
|
||||
string logLine = $"[{e.Timestamp:HH:mm:ss}] {safeMsg}{Environment.NewLine}";
|
||||
|
||||
await File.AppendAllTextAsync(fullPath, logLine);
|
||||
|
||||
// S-0: zusätzlich JSONL (eine Datei je Tag, alle Level) – maschinen-/KI-lesbar,
|
||||
// Grundlage für den Log Viewer. Dual-Sink; Text-Sink später abschaltbar.
|
||||
string jsonlPath = Path.Combine(_logsDirectory, $"{e.Timestamp:yyyy-MM-dd}.jsonl");
|
||||
await File.AppendAllTextAsync(jsonlPath, LogJson.Format(e) + Environment.NewLine);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -70,9 +103,9 @@ namespace PolyTraderSharp.Services
|
||||
}
|
||||
}
|
||||
|
||||
public void Log(string message, LogLevel level = LogLevel.Info)
|
||||
public void Log(string message, LogLevel level = LogLevel.Info, string correlationId = "")
|
||||
{
|
||||
var e = new LogMessageEventArgs(message, level);
|
||||
var e = new LogMessageEventArgs(message, level, correlationId);
|
||||
lock (_lock)
|
||||
{
|
||||
_history.Add(e);
|
||||
|
||||
@@ -76,6 +76,9 @@ namespace PolyTraderSharp
|
||||
public int AccountId { get; set; }
|
||||
public string TokenId { get; set; } = string.Empty;
|
||||
public int SourceTraderId { get; set; }
|
||||
|
||||
/// <summary>Korrelation zum auslösenden Signal (leer bei Profit-Target-Exits).</summary>
|
||||
public string SignalId { get; set; } = string.Empty;
|
||||
public string MarketQuestion { get; set; } = string.Empty;
|
||||
|
||||
public decimal ReferencePrice { get; set; } // Master-Exit-Preis
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace PolyTraderSharp.Models
|
||||
public DateTime OpenedAt { get; set; }
|
||||
public DateTime ClosedAt { get; set; }
|
||||
public string ExitReason { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Korrelation zum Entscheidungsjournal/Order-Events (leer bei Alt-Daten/System-Closes).</summary>
|
||||
public string SignalId { get; set; } = string.Empty;
|
||||
}
|
||||
public class ClosedTradeRow : ClosedTrade
|
||||
{
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef
|
||||
e.Property(x => x.RealizedPnl).HasPrecision(18, 6);
|
||||
e.Property(x => x.PnlPercent).HasPrecision(18, 6);
|
||||
e.Property(x => x.TotalFees).HasPrecision(18, 6);
|
||||
e.Property(x => x.SignalId).HasMaxLength(40); // Korrelation zum Entscheidungsjournal
|
||||
e.HasIndex(x => x.AccountId);
|
||||
e.HasIndex(x => x.TokenId);
|
||||
e.HasIndex(x => x.SourceTraderId);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using PolyTrader.Core.Configuration;
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Persistence.Ef
|
||||
{
|
||||
/// <summary>
|
||||
/// Design-Time-Factory für EF-Tooling. Connection über Umgebungsvariable POLYTRADER_MYSQL
|
||||
/// (keine Zugangsdaten im Code/Repo).
|
||||
/// (keine Zugangsdaten im Code/Repo). Nutzt die fest gepinnte Server-Version (statt AutoDetect),
|
||||
/// damit Migrations-Scaffolding OHNE DB-Verbindung funktioniert.
|
||||
/// </summary>
|
||||
public class CopyTradingDbContextFactory : IDesignTimeDbContextFactory<CopyTradingDbContext>
|
||||
{
|
||||
@@ -16,7 +18,7 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef
|
||||
?? "Server=localhost;Port=3306;Database=polytrader;User ID=root;Password=;";
|
||||
|
||||
var options = new DbContextOptionsBuilder<CopyTradingDbContext>()
|
||||
.UseMySql(conn, ServerVersion.AutoDetect(conn))
|
||||
.UseMySql(conn, DatabaseServerVersion.Value)
|
||||
.Options;
|
||||
|
||||
return new CopyTradingDbContext(options);
|
||||
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using PolyTrader.Modules.CopyTrading.Persistence.Ef;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
|
||||
{
|
||||
[DbContext(typeof(CopyTradingDbContext))]
|
||||
[Migration("20260716101729_AddClosedTradeSignalId")]
|
||||
partial class AddClosedTradeSignalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.13")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.ClosedTrade", b =>
|
||||
{
|
||||
b.Property<int>("TradeId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("EntryPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("ExitPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("ExitReason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<bool>("IsDemo")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("MarketQuestion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("MarketSlug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<DateTime>("OpenedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<decimal>("PnlPercent")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("RealizedPnl")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("Side")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<int>("SourceTraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<decimal>("TotalFees")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.HasKey("TradeId");
|
||||
|
||||
b.HasIndex("AccountId");
|
||||
|
||||
b.HasIndex("SourceTraderId");
|
||||
|
||||
b.HasIndex("TokenId");
|
||||
|
||||
b.ToTable("mod_copytrading_closed_trades", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.CopyTradingAccountSettings", b =>
|
||||
{
|
||||
b.Property<int>("AccountId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("MaxBuyPrice")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MaxPriceDifference")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MaxSpreadPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("MinSellRatioPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PerMarketLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PerMasterLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("PreRedeemLimit")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("ProfitTarget")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("SellFloorPct")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime24h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime6h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTime72h")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("perMaxTimeNone")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.HasKey("AccountId");
|
||||
|
||||
b.ToTable("mod_copytrading_account_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.MasterTraderHistoryRecord", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<DateTime>("ClosedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<decimal>("RealizedPnl")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("varchar(120)");
|
||||
|
||||
b.Property<int>("TraderId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClosedAt");
|
||||
|
||||
b.HasIndex("TraderId");
|
||||
|
||||
b.ToTable("mod_copytrading_mt_history", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PolyTraderSharp.Models.TrackedTrader", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("AssignedAccountIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("AutoPauseEnabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<decimal>("CopyAvgPnlPerTrade")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("CopyPnl30d")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<decimal>("CopyProfitFactor")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
b.Property<int>("CopyTradeCount30d")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("MakerEntry")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Reasoning")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<double>("TotalPnl")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.Property<int>("TotalTrades")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("WalletAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("varchar(128)");
|
||||
|
||||
b.Property<int>("WinningTrades")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<double>("Winrate30t")
|
||||
.HasColumnType("double");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("mod_copytrading_traders", (string)null);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddClosedTradeSignalId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SignalId",
|
||||
table: "mod_copytrading_closed_trades",
|
||||
type: "varchar(40)",
|
||||
maxLength: 40,
|
||||
nullable: false,
|
||||
defaultValue: "")
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SignalId",
|
||||
table: "mod_copytrading_closed_trades");
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -80,6 +80,11 @@ namespace PolyTrader.Modules.CopyTrading.Persistence.Ef.Migrations
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("varchar(10)");
|
||||
|
||||
b.Property<string>("SignalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("varchar(40)");
|
||||
|
||||
b.Property<decimal>("Size")
|
||||
.HasPrecision(18, 6)
|
||||
.HasColumnType("decimal(18,6)");
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace PolyTraderSharp.Services
|
||||
private readonly IMarketRepository _marketRepo;
|
||||
private readonly IAccountRepository _accountRepo;
|
||||
private readonly SellLadderService _sellLadder;
|
||||
private readonly IDecisionJournal _journal;
|
||||
private readonly IOrderEventLog _orderEvents;
|
||||
private readonly ConcurrentDictionary<int, SemaphoreSlim> _accountSemaphores = new();
|
||||
private readonly ConcurrentDictionary<int, DateTime> _lastInactiveLogPerTrader = new();
|
||||
|
||||
@@ -40,7 +42,9 @@ namespace PolyTraderSharp.Services
|
||||
IPositionRepository positionRepo,
|
||||
IMarketRepository marketRepo,
|
||||
IAccountRepository accountRepo,
|
||||
SellLadderService sellLadder)
|
||||
SellLadderService sellLadder,
|
||||
IDecisionJournal journal,
|
||||
IOrderEventLog orderEvents)
|
||||
{
|
||||
_state = state;
|
||||
_copyState = copyState;
|
||||
@@ -53,6 +57,39 @@ namespace PolyTraderSharp.Services
|
||||
_marketRepo = marketRepo;
|
||||
_accountRepo = accountRepo;
|
||||
_sellLadder = sellLadder;
|
||||
_journal = journal;
|
||||
_orderEvents = orderEvents;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S-0 (Supervisor): schreibt eine Entscheidung strukturiert ins Journal (core_decision_journal).
|
||||
/// Ergänzt die bestehenden Freitext-Logs, ersetzt sie nicht. Darf den Trading-Pfad nie brechen
|
||||
/// (Robustheit in der Journal-Implementierung).
|
||||
/// </summary>
|
||||
/// <summary>Kompakter Kontext als JSON (kulturinvariant – kein deutsches Dezimalkomma).</summary>
|
||||
private static string Ctx(object o) => System.Text.Json.JsonSerializer.Serialize(o);
|
||||
|
||||
private void Journal(CopySignal signal, AccountState? account, TradeDecision decision,
|
||||
DecisionReason reason, string message, string contextJson = "")
|
||||
{
|
||||
_journal.Write(new DecisionRecord
|
||||
{
|
||||
SignalId = signal.SignalId,
|
||||
ModuleName = "CopyTrading",
|
||||
AccountId = account?.AccountId ?? 0,
|
||||
IsDemo = account?.IsDemo ?? false,
|
||||
SourceTraderId = signal.TraderId,
|
||||
TokenId = signal.TokenId,
|
||||
MarketSlug = signal.MarketSlug,
|
||||
MarketQuestion = signal.MarketQuestion,
|
||||
Side = signal.Side,
|
||||
SignalPrice = signal.Price,
|
||||
MarketEndDate = signal.EndDate,
|
||||
Decision = decision,
|
||||
Reason = reason,
|
||||
Message = message,
|
||||
ContextJson = contextJson
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -238,6 +275,8 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"⏸️ Trade {signal.Side} [{signal.MarketQuestion}] [{(string.IsNullOrEmpty(signal.Outcome) ? signal.Side : signal.Outcome)}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: {modeLabel}-Trading Modus ist 'Inactive'. Weitere Trades dieses Traders werden für 60s nicht geloggt.");
|
||||
Journal(signal, account, TradeDecision.Skipped, DecisionReason.ModeInactive,
|
||||
$"{modeLabel}-Trading 'Inactive' (Journal ratenbegrenzt wie das Log: 1/60s je Trader)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -248,6 +287,7 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"⏸️ Trade BUY [{signal.MarketQuestion}] [{(string.IsNullOrEmpty(signal.Outcome) ? signal.Side : signal.Outcome)}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Trading Modus ist 'SellOnly' — BUY-Trades werden nicht kopiert.");
|
||||
Journal(signal, account, TradeDecision.Skipped, DecisionReason.SellOnlyModeBuyBlocked, "SellOnly-Modus blockiert BUY");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -302,6 +342,7 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"⏸️ Trade BUY [{signal.MarketQuestion}] [{shareType}] übersprungen:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: SELL-Eskalationsleiter läuft (Exit aktiv) – kein Zukauf während des Ausstiegs.");
|
||||
Journal(signal, account, TradeDecision.Skipped, DecisionReason.ExitPendingBuySkip, "SELL-Leiter aktiv – kein Zukauf (H3)");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -310,6 +351,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen (Risk Limit):\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Preis (${signal.Price:F3}) übersteigt das MaxBuy Limit (${settings.MaxBuyPrice:F3})");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.MaxBuyPriceExceeded,
|
||||
$"Preis {signal.Price:F3} > MaxBuyPrice {settings.MaxBuyPrice:F3}",
|
||||
Ctx(new { price = signal.Price, maxBuyPrice = settings.MaxBuyPrice }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -345,6 +389,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: PerMasterLimit ({settings.PerMasterLimit:F1}%) erreicht. Bisher investiert in '{trader.DisplayName}': ${investedInMaster:F2} ({pctInvested:F1}%).");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.PerMasterLimitReached,
|
||||
$"PerMasterLimit {settings.PerMasterLimit:F1}% erreicht",
|
||||
Ctx(new { investedInMaster, maxAllowedPerMaster, perMasterLimitPct = settings.PerMasterLimit }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -368,6 +415,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: TimeLimit '{timeframeLabel}' ({applicableTimeLimitPct:F1}%) erreicht. Bisher investiert: ${investedInTimeframe:F2} / max. ${maxAllowedTimeframe:F2}");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.TimeWindowLimitReached,
|
||||
$"TimeLimit '{timeframeLabel}' ({applicableTimeLimitPct:F1}%) erreicht",
|
||||
Ctx(new { bucket = timeframeLabel, investedInTimeframe, maxAllowedTimeframe, hoursLeft }));
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -381,6 +431,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Kauflimit (${maxAllowed:F2}) bereits in Markt investiert (${investedInMarket:F2}). Rest: ${maxAmountToBuy:F2} < MinTrade (${minTrade:F2})");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.MarketBudgetExhausted,
|
||||
$"Markt-Budget erschöpft (Rest {maxAmountToBuy:F2} < MinTrade)",
|
||||
Ctx(new { maxAllowed, investedInMarket, remaining = maxAmountToBuy, minTrade }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -389,6 +442,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] verworfen:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Kontostand (${account.AvailableBalance:F2}) nicht ausreichend für errechnetes Size (${maxAmountToBuy:F2})");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.InsufficientBalance,
|
||||
"Verfügbares Guthaben reicht nicht",
|
||||
Ctx(new { availableBalance = account.AvailableBalance, requested = maxAmountToBuy }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -406,6 +462,9 @@ namespace PolyTraderSharp.Services
|
||||
{
|
||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] gestoppt:\n" +
|
||||
$" Begründung: Mathematisch unmöglicher Trade ({exact.shares} Shares für ${exact.usdc:F2}). Kontostand (${account.AvailableBalance:F2}) reicht für Minimum nicht aus.");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.BelowPolymarketMinimum,
|
||||
"Mathematisch unmöglicher Trade (Balance reicht für Minimum nicht)",
|
||||
Ctx(new { shares = exact.shares, usdc = exact.usdc, availableBalance = account.AvailableBalance }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -417,6 +476,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"❌ Trade BUY [{signal.MarketQuestion}] [{shareType}] gestoppt:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Unter Polymarket Minimum ({exact.shares:F1} Shares / ${exact.usdc:F2} USDC). Min: 5.5 Shares / $0.10.");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.BelowPolymarketMinimum,
|
||||
$"Unter Polymarket-Minimum ({exact.shares:F1} Shares / {exact.usdc:F2} USDC)",
|
||||
Ctx(new { shares = exact.shares, usdc = exact.usdc }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -451,12 +513,17 @@ namespace PolyTraderSharp.Services
|
||||
_logger.Info($"❌ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Position gehört Trader '{wrongTraderPos.SourceTraderName}' (ID {wrongTraderPos.SourceTraderId}), SELL kam aber von Trader ID {signal.TraderId}.");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.OwnershipMismatch,
|
||||
$"Position gehört Trader {wrongTraderPos.SourceTraderId}, SELL kam von {signal.TraderId}",
|
||||
Ctx(new { positionOwner = wrongTraderPos.SourceTraderId, signalTrader = signal.TraderId }));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info($"❌ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Position nicht im Portfolio gefunden (möglicherweise zuvor gefiltert).");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.PositionNotFound,
|
||||
"Keine Position im Portfolio (möglicherweise zuvor gefiltert)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -478,6 +545,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"📊 Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Teilverkauf ({sellRatio:P0} des Bestands). Master hält noch {masterShares:F1} Shares. Signal nur {signal.Size:F1} Shares. Schwelle: 30%.");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.PartialSellBelowThreshold,
|
||||
$"Teilverkauf {sellRatio:P0} unter Schwelle",
|
||||
Ctx(new { sellRatio, masterShares, signalSize = signal.Size }));
|
||||
return;
|
||||
}
|
||||
_logger.TradeReasoning($"📊 Trade SELL [{signal.MarketQuestion}] [{shareType}] FREIGEGEBEN:\n" +
|
||||
@@ -492,6 +562,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"📊 Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Master hält laut Tracking 0 Shares, aber SELL Signal mit {signal.Size:F1} Shares erhalten. Inkonsistenz — ignoriert.");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.MasterPositionInconsistent,
|
||||
"Master hält laut Tracking 0 Shares (Inkonsistenz)",
|
||||
Ctx(new { signalSize = signal.Size }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -504,6 +577,9 @@ namespace PolyTraderSharp.Services
|
||||
_logger.TradeReasoning($"⏳ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Kein Master-Position-Tracking verfügbar und Haltezeit erst {holdingMinutes:F1} Min. Warte auf ersten Sync.");
|
||||
Journal(signal, account, TradeDecision.Skipped, DecisionReason.SyncGracePeriod,
|
||||
$"Kein Tracking, Haltezeit {holdingMinutes:F1} Min < Schonfrist",
|
||||
Ctx(new { holdingMinutes }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -554,6 +630,9 @@ namespace PolyTraderSharp.Services
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Markt: {signal.MarketQuestion}\n" +
|
||||
$" BUY: {exactShares:F4} Shares [{shareType}] @ ${orderPrice:F3} (Gesamt: ${exactUsdc:F2})");
|
||||
Journal(signal, account, TradeDecision.Executed, DecisionReason.DemoFilled,
|
||||
$"Demo-BUY {exactShares:F2} Shares @ {orderPrice:F3}",
|
||||
Ctx(new { shares = exactShares, usdc = exactUsdc, orderPrice }));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -569,6 +648,22 @@ namespace PolyTraderSharp.Services
|
||||
: FeeModel.FallbackBps(null);
|
||||
var result = await _clob.PlaceOrderAsync(account, signal.TokenId, signal.Side, exactUsdc, orderPrice, buyOrderType, _state.DebugOrderPayloadLog, isNegRisk, buyFeeBps);
|
||||
|
||||
// S-0: Order-Lifecycle als Daten (core_order_events).
|
||||
_orderEvents.Write(new OrderEventRecord
|
||||
{
|
||||
SignalId = signal.SignalId,
|
||||
ModuleName = "CopyTrading",
|
||||
AccountId = account.AccountId,
|
||||
TokenId = signal.TokenId,
|
||||
EventType = result == "OK" ? OrderEventType.Placed : OrderEventType.PlaceFailed,
|
||||
Side = "BUY",
|
||||
Price = orderPrice,
|
||||
AmountUsd = exactUsdc,
|
||||
OrderType = buyOrderType,
|
||||
Response = result,
|
||||
DetailsJson = Ctx(new { feeBps = buyFeeBps, signalPrice = signal.Price })
|
||||
});
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
var pos = new Position
|
||||
@@ -612,6 +707,16 @@ namespace PolyTraderSharp.Services
|
||||
// The background sync will update with the real value within 30 seconds
|
||||
string masterKey = $"{signal.TraderId}_{signal.TokenId}";
|
||||
_copyState.MasterTraderPositions.TryAdd(masterKey, (signal.Size, DateTime.UtcNow));
|
||||
|
||||
Journal(signal, account, TradeDecision.Executed, DecisionReason.OrderPlaced,
|
||||
$"Live-BUY {buyOrderType} {exactShares:F2} Shares @ {orderPrice:F3}",
|
||||
Ctx(new { shares = exactShares, usdc = exactUsdc, orderPrice, orderType = buyOrderType }));
|
||||
}
|
||||
else
|
||||
{
|
||||
Journal(signal, account, TradeDecision.Failed, DecisionReason.OrderFailed,
|
||||
$"Live-BUY fehlgeschlagen: {result}",
|
||||
Ctx(new { usdc = exactUsdc, orderPrice, orderType = buyOrderType }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -625,6 +730,8 @@ namespace PolyTraderSharp.Services
|
||||
// BUY blockiert einen legitimen schnellen SELL des Masters NICHT mehr.
|
||||
if (pendingInfo.Side == "SELL" && (DateTime.UtcNow - pendingInfo.PlacedAt).TotalSeconds < 20)
|
||||
{
|
||||
Journal(signal, account, TradeDecision.Skipped, DecisionReason.SellSpamBlock,
|
||||
"SELL <20s nach letztem SELL (Spam-Blockade)");
|
||||
return; // Spam-Blockade: SELL wurde in den letzten 20 Sekunden bereits versendet
|
||||
}
|
||||
}
|
||||
@@ -634,6 +741,7 @@ namespace PolyTraderSharp.Services
|
||||
if (account.OpenPositions.TryGetValue(signal.TokenId, out var laddering) && laddering.ExitPending)
|
||||
{
|
||||
_logger.Info($"SELL [{signal.MarketQuestion}] ignoriert: Eskalationsleiter läuft bereits (Konto {account.Name}).");
|
||||
Journal(signal, account, TradeDecision.Skipped, DecisionReason.LadderAlreadyActive, "Eskalationsleiter läuft bereits");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -653,6 +761,8 @@ namespace PolyTraderSharp.Services
|
||||
_logger.Info($"❌ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Position gehört einem anderen Trader (Safety Check).");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.OwnershipMismatch,
|
||||
"Position gehört einem anderen Trader (Defense-in-depth)");
|
||||
}
|
||||
|
||||
if (!removed && !string.IsNullOrEmpty(signal.MarketSlug))
|
||||
@@ -712,7 +822,8 @@ namespace PolyTraderSharp.Services
|
||||
TotalFees = totalFees,
|
||||
OpenedAt = openPos.OpenedAt,
|
||||
ClosedAt = DateTime.UtcNow,
|
||||
ExitReason = signal.Reason
|
||||
ExitReason = signal.Reason,
|
||||
SignalId = signal.SignalId
|
||||
};
|
||||
|
||||
_closedTradeWriter.TryWrite(ct);
|
||||
@@ -720,6 +831,10 @@ namespace PolyTraderSharp.Services
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Markt: {signal.MarketQuestion}\n" +
|
||||
$" SELL: {openPos.Size:F2} Shares [{shareType}] @ ${signal.Price:F3} (Gewinn: ${realizedPnl:F2})");
|
||||
Journal(signal, account, TradeDecision.Executed,
|
||||
signal.TraderId == 0 ? DecisionReason.SystemResolutionClose : DecisionReason.DemoClosed,
|
||||
$"Demo-Close {openPos.Size:F2} Shares, PnL {realizedPnl:F2}",
|
||||
Ctx(new { size = openPos.Size, exitPrice = demoExitPrice, realizedPnl, totalFees }));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -727,9 +842,14 @@ namespace PolyTraderSharp.Services
|
||||
// SellLadderService – gleiche Quelle wie der Profit-Target-Exit im Sync).
|
||||
// openPos wurde oben entfernt; StartLadderAsync stellt es als ExitPending zurück.
|
||||
bool isHf = trader != null && trader.Category == "HF";
|
||||
await _sellLadder.StartLadderAsync(
|
||||
bool ladderOk = await _sellLadder.StartLadderAsync(
|
||||
account, openPos, signal.Price, signal.TraderId, isHf,
|
||||
settings.MaxPriceDifference, settings.SellFloorPct, isNegRisk, "Master SELL");
|
||||
settings.MaxPriceDifference, settings.SellFloorPct, isNegRisk, "Master SELL", signal.SignalId);
|
||||
Journal(signal, account,
|
||||
ladderOk ? TradeDecision.Executed : TradeDecision.Failed,
|
||||
ladderOk ? DecisionReason.LadderStarted : DecisionReason.LadderStartFailed,
|
||||
ladderOk ? $"SELL-Leiter gestartet (Referenz {signal.Price:F3})" : "SELL-Leiter-Start fehlgeschlagen/abgelehnt",
|
||||
Ctx(new { referencePrice = signal.Price, size = openPos.Size, isHf }));
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -737,6 +857,8 @@ namespace PolyTraderSharp.Services
|
||||
_logger.Info($"❌ Trade SELL [{signal.MarketQuestion}] [{shareType}] ignoriert:\n" +
|
||||
$" Konto: {account.Name}\n" +
|
||||
$" Begründung: Position nicht im Portfolio gefunden (möglicherweise zuvor gefiltert).");
|
||||
Journal(signal, account, TradeDecision.Rejected, DecisionReason.PositionNotFound,
|
||||
"Position nicht im Portfolio gefunden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,8 @@ namespace PolyTraderSharp.Services
|
||||
PnlPercent = trade.PnlPercent,
|
||||
OpenedAt = trade.OpenedAt,
|
||||
ClosedAt = trade.ClosedAt,
|
||||
ExitReason = trade.ExitReason
|
||||
ExitReason = trade.ExitReason,
|
||||
SignalId = trade.SignalId
|
||||
});
|
||||
|
||||
_logger.Debug($"Saved ClosedTrade {trade.TradeId} to MySQL");
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace PolyTraderSharp.Services
|
||||
private readonly TerminalLogger _logger;
|
||||
private readonly ThreemaService _threema;
|
||||
private readonly IPositionRepository _positionRepo;
|
||||
private readonly IOrderEventLog _orderEvents;
|
||||
|
||||
public SellLadderService(
|
||||
CopyTradingState copyState,
|
||||
@@ -39,7 +40,8 @@ namespace PolyTraderSharp.Services
|
||||
IClobClient clob,
|
||||
TerminalLogger logger,
|
||||
ThreemaService threema,
|
||||
IPositionRepository positionRepo)
|
||||
IPositionRepository positionRepo,
|
||||
IOrderEventLog orderEvents)
|
||||
{
|
||||
_copyState = copyState;
|
||||
_state = state;
|
||||
@@ -47,6 +49,26 @@ namespace PolyTraderSharp.Services
|
||||
_logger = logger;
|
||||
_threema = threema;
|
||||
_positionRepo = positionRepo;
|
||||
_orderEvents = orderEvents;
|
||||
}
|
||||
|
||||
/// <summary>S-0: Order-Lifecycle-Event der Leiter strukturiert festhalten (core_order_events).</summary>
|
||||
private void OrderEvent(ExitLadderState ladder, OrderEventType type, decimal price, decimal amountUsd, string response, string detailsJson = "")
|
||||
{
|
||||
_orderEvents.Write(new OrderEventRecord
|
||||
{
|
||||
SignalId = ladder.SignalId,
|
||||
ModuleName = "CopyTrading",
|
||||
AccountId = ladder.AccountId,
|
||||
TokenId = ladder.TokenId,
|
||||
EventType = type,
|
||||
Side = "SELL",
|
||||
Price = price,
|
||||
AmountUsd = amountUsd,
|
||||
OrderType = "GTC",
|
||||
Response = response,
|
||||
DetailsJson = detailsJson
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,7 +79,8 @@ namespace PolyTraderSharp.Services
|
||||
/// </summary>
|
||||
public async Task<bool> StartLadderAsync(
|
||||
AccountState account, Position pos, decimal referencePrice, int sourceTraderId,
|
||||
bool isHf, decimal maxPriceDifferencePct, decimal sellFloorPct, bool isNegRisk, string reasonTag)
|
||||
bool isHf, decimal maxPriceDifferencePct, decimal sellFloorPct, bool isNegRisk, string reasonTag,
|
||||
string signalId = "")
|
||||
{
|
||||
string key = $"{account.AccountId}_{pos.TokenId}";
|
||||
|
||||
@@ -87,6 +110,7 @@ namespace PolyTraderSharp.Services
|
||||
AccountId = account.AccountId,
|
||||
TokenId = pos.TokenId,
|
||||
SourceTraderId = sourceTraderId,
|
||||
SignalId = signalId,
|
||||
MarketQuestion = pos.MarketQuestion,
|
||||
ReferencePrice = referencePrice,
|
||||
CurrentLimit = firstLimit,
|
||||
@@ -123,6 +147,9 @@ namespace PolyTraderSharp.Services
|
||||
? FeeModel.ResolveBps(startMd.TakerFeeBps, startMd.Category)
|
||||
: FeeModel.FallbackBps(null);
|
||||
var result = await _clob.PlaceOrderAsync(account, pos.TokenId, "SELL", pos.Size * firstLimit, firstLimit, "GTC", _state.DebugOrderPayloadLog, isNegRisk, feeBps);
|
||||
OrderEvent(ladder, result == "OK" ? OrderEventType.LadderStart : OrderEventType.PlaceFailed,
|
||||
firstLimit, pos.Size * firstLimit, result,
|
||||
$"{{\"reasonTag\":\"{reasonTag}\",\"floor\":{floor.ToString(System.Globalization.CultureInfo.InvariantCulture)}}}");
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
@@ -187,6 +214,8 @@ namespace PolyTraderSharp.Services
|
||||
pos.ExitPending = false;
|
||||
if (!account.IsDemo) _positionRepo.UpsertLive(account.AccountId, pos);
|
||||
_logger.Warning($"🧹 [SELL-LEITER Dust] {account.Name} | {ladder.MarketQuestion}: Restgröße {pos.Size:F2} < Minimum {CopyTradingRisk.MinShares} Shares – Leiter beendet, Rest gehalten.");
|
||||
OrderEvent(ladder, OrderEventType.DustAbort, ladder.CurrentLimit, pos.Size * ladder.CurrentLimit,
|
||||
"Dust", $"{{\"restShares\":{pos.Size.ToString(System.Globalization.CultureInfo.InvariantCulture)}}}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -221,6 +250,7 @@ namespace PolyTraderSharp.Services
|
||||
try
|
||||
{
|
||||
var r = await _clob.PlaceOrderAsync(account, ladder.TokenId, "SELL", pos.Size * ladder.Floor, ladder.Floor, "GTC", _state.DebugOrderPayloadLog, isNegFloor, floorFeeBps);
|
||||
OrderEvent(ladder, OrderEventType.FloorReplaced, ladder.Floor, pos.Size * ladder.Floor, r);
|
||||
if (r == "OK")
|
||||
_copyState.PendingOrderTimestamps[key] = (DateTime.UtcNow, ladder.SourceTraderId, "SELL");
|
||||
else
|
||||
@@ -259,6 +289,9 @@ namespace PolyTraderSharp.Services
|
||||
|
||||
decimal usdc = pos.Size * next;
|
||||
var result = await _clob.PlaceOrderAsync(account, ladder.TokenId, "SELL", usdc, next, "GTC", _state.DebugOrderPayloadLog, isNegRisk, stepFeeBps);
|
||||
OrderEvent(ladder, result == "OK" ? OrderEventType.LadderStep : OrderEventType.PlaceFailed,
|
||||
next, usdc, result,
|
||||
$"{{\"attempt\":{ladder.Attempt + 1},\"floor\":{ladder.Floor.ToString(System.Globalization.CultureInfo.InvariantCulture)}}}");
|
||||
|
||||
if (result == "OK")
|
||||
{
|
||||
|
||||
@@ -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