Phase 5-UI: Generischer Core-Trade-Log + modulübergreifendes Dashboard

- Core: TradeRecord (Modell) + ITradeLogRepository (+ Mongo-Impl, Collection
  "trade_log"), in AddCorePersistence registriert. Realisiert den generischen,
  modulneutralen Trade-Log aus Entscheidung #2.
- Dual-Write: PersistenceService schreibt geschlossene Copy-Trades zusaetzlich als
  generischen TradeRecord (ModuleName="CopyTrading").
- Neue DashboardView (UserControl + Designer): zeigt die letzten Trades ALLER
  Module (GetRecent) mit Konto-Aufloesung + Kurzauswertung (Gesamt-PnL, Anzahl,
  Aufschluesselung je Modul). btn_dashboard im Launcher, View "core.dashboard".
- Build 0 Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-02 12:16:20 +02:00
co-authored by Claude Opus 4.8
parent 8e2a92ad2e
commit 4f6c7fdb86
13 changed files with 500 additions and 41 deletions
+17
View File
@@ -176,6 +176,23 @@ internal static class Program
return view; return view;
} }
}); });
uiHost.RegisterView(new PolyTrader.Core.Modularity.ModuleView
{
Id = "core.dashboard",
Title = "Dashboard",
Group = "Core",
Order = 5,
PreferredWidth = 1200,
PreferredHeight = 650,
CreateControl = () =>
{
var view = new PolyTraderSharp.Ui.Views.DashboardView();
view.Initialize(
viewServices.GetRequiredService<PolyTrader.Core.Persistence.ITradeLogRepository>(),
viewServices.GetRequiredService<TradingState>());
return view;
}
});
// TODO Phase 5-UI: registrierte IPolyTraderModule durchlaufen und module.RegisterUi(uiHost) aufrufen. // TODO Phase 5-UI: registrierte IPolyTraderModule durchlaufen und module.RegisterUi(uiHost) aufrufen.
var launcher = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>(); var launcher = AppHost.Services.GetRequiredService<PolyTraderSharp.Ui.LauncherForm>();
+10
View File
@@ -200,6 +200,16 @@ namespace PolyTraderSharp.Properties {
} }
} }
/// <summary>
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap system_time {
get {
object obj = ResourceManager.GetObject("system_time", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary> /// <summary>
/// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap. /// Sucht eine lokalisierte Ressource vom Typ System.Drawing.Bitmap.
/// </summary> /// </summary>
+5 -2
View File
@@ -145,6 +145,9 @@
<data name="coins_in_hand" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="coins_in_hand" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\coins_in_hand.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\coins_in_hand.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
<data name="error_log" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\error_log.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="add" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="add" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\add.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
@@ -169,7 +172,7 @@
<data name="money_dollar" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="money_dollar" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\money_dollar.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\money_dollar.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
<data name="error_log" type="System.Resources.ResXFileRef, System.Windows.Forms"> <data name="system_time" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\error_log.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value> <value>..\Resources\system_time.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data> </data>
</root> </root>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+17 -6
View File
@@ -25,6 +25,7 @@ namespace PolyTraderSharp.Ui
btn_settings = new ToolStripButton(); btn_settings = new ToolStripButton();
btn_terminal = new ToolStripButton(); btn_terminal = new ToolStripButton();
btn_jobs = new ToolStripButton(); btn_jobs = new ToolStripButton();
btn_dashboard = new ToolStripButton();
toolstrip_quickbar = new ToolStrip(); toolstrip_quickbar = new ToolStrip();
btn_liveTrading = new ToolStripButton(); btn_liveTrading = new ToolStripButton();
btn_demoTrading = new ToolStripButton(); btn_demoTrading = new ToolStripButton();
@@ -71,7 +72,7 @@ namespace PolyTraderSharp.Ui
// //
toolstrip_windows.AutoSize = false; toolstrip_windows.AutoSize = false;
toolstrip_windows.ImageScalingSize = new Size(64, 64); toolstrip_windows.ImageScalingSize = new Size(64, 64);
toolstrip_windows.Items.AddRange(new ToolStripItem[] { btn_settings, btn_terminal, btn_jobs }); toolstrip_windows.Items.AddRange(new ToolStripItem[] { btn_settings, btn_terminal, btn_jobs, btn_dashboard });
toolstrip_windows.Location = new Point(0, 35); toolstrip_windows.Location = new Point(0, 35);
toolstrip_windows.Name = "toolstrip_windows"; toolstrip_windows.Name = "toolstrip_windows";
toolstrip_windows.Size = new Size(2599, 70); toolstrip_windows.Size = new Size(2599, 70);
@@ -100,10 +101,19 @@ namespace PolyTraderSharp.Ui
// //
// btn_jobs // btn_jobs
// //
btn_jobs.DisplayStyle = ToolStripItemDisplayStyle.Text; btn_jobs.Image = Properties.Resources.system_time;
btn_jobs.ImageScaling = ToolStripItemImageScaling.None;
btn_jobs.Name = "btn_jobs"; btn_jobs.Name = "btn_jobs";
btn_jobs.Size = new Size(100, 65); btn_jobs.Size = new Size(106, 65);
btn_jobs.Text = "Server Jobs"; btn_jobs.Text = "Server Jobs";
btn_jobs.TextImageRelation = TextImageRelation.ImageAboveText;
//
// btn_dashboard
//
btn_dashboard.DisplayStyle = ToolStripItemDisplayStyle.Text;
btn_dashboard.Name = "btn_dashboard";
btn_dashboard.Size = new Size(110, 65);
btn_dashboard.Text = "Dashboard";
// //
// toolstrip_quickbar // toolstrip_quickbar
// //
@@ -111,7 +121,7 @@ namespace PolyTraderSharp.Ui
toolstrip_quickbar.Items.AddRange(new ToolStripItem[] { btn_liveTrading, btn_demoTrading }); toolstrip_quickbar.Items.AddRange(new ToolStripItem[] { btn_liveTrading, btn_demoTrading });
toolstrip_quickbar.Location = new Point(0, 105); toolstrip_quickbar.Location = new Point(0, 105);
toolstrip_quickbar.Name = "toolstrip_quickbar"; toolstrip_quickbar.Name = "toolstrip_quickbar";
toolstrip_quickbar.Size = new Size(2599, 28); toolstrip_quickbar.Size = new Size(2599, 34);
toolstrip_quickbar.TabIndex = 4; toolstrip_quickbar.TabIndex = 4;
toolstrip_quickbar.Text = "toolStrip2"; toolstrip_quickbar.Text = "toolStrip2";
// //
@@ -119,14 +129,14 @@ namespace PolyTraderSharp.Ui
// //
btn_liveTrading.DisplayStyle = ToolStripItemDisplayStyle.Text; btn_liveTrading.DisplayStyle = ToolStripItemDisplayStyle.Text;
btn_liveTrading.Name = "btn_liveTrading"; btn_liveTrading.Name = "btn_liveTrading";
btn_liveTrading.Size = new Size(200, 25); btn_liveTrading.Size = new Size(137, 29);
btn_liveTrading.Text = "LiveTrading (—)"; btn_liveTrading.Text = "LiveTrading (—)";
// //
// btn_demoTrading // btn_demoTrading
// //
btn_demoTrading.DisplayStyle = ToolStripItemDisplayStyle.Text; btn_demoTrading.DisplayStyle = ToolStripItemDisplayStyle.Text;
btn_demoTrading.Name = "btn_demoTrading"; btn_demoTrading.Name = "btn_demoTrading";
btn_demoTrading.Size = new Size(200, 25); btn_demoTrading.Size = new Size(156, 29);
btn_demoTrading.Text = "DemoTrading (—)"; btn_demoTrading.Text = "DemoTrading (—)";
// //
// statusStrip_info // statusStrip_info
@@ -194,6 +204,7 @@ namespace PolyTraderSharp.Ui
private ToolStripButton btn_settings; private ToolStripButton btn_settings;
private ToolStripButton btn_terminal; private ToolStripButton btn_terminal;
private ToolStripButton btn_jobs; private ToolStripButton btn_jobs;
private ToolStripButton btn_dashboard;
private ToolStrip toolstrip_quickbar; private ToolStrip toolstrip_quickbar;
private ToolStripButton btn_liveTrading; private ToolStripButton btn_liveTrading;
private ToolStripButton btn_demoTrading; private ToolStripButton btn_demoTrading;
+1
View File
@@ -32,6 +32,7 @@ namespace PolyTraderSharp.Ui
btn_settings.Click += (_, _) => OpenView("core.settings"); btn_settings.Click += (_, _) => OpenView("core.settings");
btn_terminal.Click += (_, _) => OpenView("core.terminal"); btn_terminal.Click += (_, _) => OpenView("core.terminal");
btn_jobs.Click += (_, _) => OpenView("core.jobs"); btn_jobs.Click += (_, _) => OpenView("core.jobs");
btn_dashboard.Click += (_, _) => OpenView("core.dashboard");
btn_liveTrading.Click += (_, _) => CycleLiveTrading(); btn_liveTrading.Click += (_, _) => CycleLiveTrading();
btn_demoTrading.Click += (_, _) => CycleDemoTrading(); btn_demoTrading.Click += (_, _) => CycleDemoTrading();
miLegacy.Click += (_, _) => OpenLegacy(); miLegacy.Click += (_, _) => OpenLegacy();
+202
View File
@@ -0,0 +1,202 @@
namespace PolyTraderSharp.Ui.Views
{
partial class DashboardView
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Komponenten-Designer generierter Code
private void InitializeComponent()
{
this.pnlTop = new System.Windows.Forms.Panel();
this.lblSummary = new System.Windows.Forms.Label();
this.btnRefresh = new System.Windows.Forms.Button();
this.dgvTrades = new System.Windows.Forms.DataGridView();
this.colModule = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colAccount = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colMarket = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colOutcome = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colSide = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colEntry = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colExit = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colSize = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colPnl = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colPnlPct = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colClosedAt = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.pnlTop.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvTrades)).BeginInit();
this.SuspendLayout();
//
// pnlTop
//
this.pnlTop.Controls.Add(this.lblSummary);
this.pnlTop.Controls.Add(this.btnRefresh);
this.pnlTop.Dock = System.Windows.Forms.DockStyle.Top;
this.pnlTop.Location = new System.Drawing.Point(0, 0);
this.pnlTop.Name = "pnlTop";
this.pnlTop.Size = new System.Drawing.Size(1100, 40);
this.pnlTop.TabIndex = 0;
//
// lblSummary
//
this.lblSummary.AutoSize = true;
this.lblSummary.Location = new System.Drawing.Point(148, 12);
this.lblSummary.Name = "lblSummary";
this.lblSummary.Size = new System.Drawing.Size(63, 15);
this.lblSummary.TabIndex = 1;
this.lblSummary.Text = "—";
//
// btnRefresh
//
this.btnRefresh.Location = new System.Drawing.Point(8, 6);
this.btnRefresh.Name = "btnRefresh";
this.btnRefresh.Size = new System.Drawing.Size(130, 28);
this.btnRefresh.TabIndex = 0;
this.btnRefresh.Text = "Aktualisieren";
this.btnRefresh.UseVisualStyleBackColor = true;
//
// dgvTrades
//
this.dgvTrades.AllowUserToAddRows = false;
this.dgvTrades.AllowUserToDeleteRows = false;
this.dgvTrades.AutoGenerateColumns = false;
this.dgvTrades.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvTrades.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.colModule,
this.colAccount,
this.colMarket,
this.colOutcome,
this.colSide,
this.colEntry,
this.colExit,
this.colSize,
this.colPnl,
this.colPnlPct,
this.colClosedAt});
this.dgvTrades.Dock = System.Windows.Forms.DockStyle.Fill;
this.dgvTrades.Location = new System.Drawing.Point(0, 40);
this.dgvTrades.Name = "dgvTrades";
this.dgvTrades.ReadOnly = true;
this.dgvTrades.RowHeadersVisible = false;
this.dgvTrades.Size = new System.Drawing.Size(1100, 560);
this.dgvTrades.TabIndex = 1;
//
// colModule
//
this.colModule.DataPropertyName = "Module";
this.colModule.HeaderText = "Modul";
this.colModule.Name = "colModule";
this.colModule.Width = 110;
//
// colAccount
//
this.colAccount.DataPropertyName = "Account";
this.colAccount.HeaderText = "Konto";
this.colAccount.Name = "colAccount";
this.colAccount.Width = 130;
//
// colMarket
//
this.colMarket.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.colMarket.DataPropertyName = "Market";
this.colMarket.HeaderText = "Markt";
this.colMarket.Name = "colMarket";
//
// colOutcome
//
this.colOutcome.DataPropertyName = "Outcome";
this.colOutcome.HeaderText = "Outcome";
this.colOutcome.Name = "colOutcome";
this.colOutcome.Width = 80;
//
// colSide
//
this.colSide.DataPropertyName = "Side";
this.colSide.HeaderText = "Side";
this.colSide.Name = "colSide";
this.colSide.Width = 60;
//
// colEntry
//
this.colEntry.DataPropertyName = "EntryPrice";
this.colEntry.HeaderText = "Entry";
this.colEntry.Name = "colEntry";
this.colEntry.Width = 70;
//
// colExit
//
this.colExit.DataPropertyName = "ExitPrice";
this.colExit.HeaderText = "Exit";
this.colExit.Name = "colExit";
this.colExit.Width = 70;
//
// colSize
//
this.colSize.DataPropertyName = "Size";
this.colSize.HeaderText = "Size";
this.colSize.Name = "colSize";
this.colSize.Width = 70;
//
// colPnl
//
this.colPnl.DataPropertyName = "RealizedPnl";
this.colPnl.HeaderText = "PnL";
this.colPnl.Name = "colPnl";
this.colPnl.Width = 80;
//
// colPnlPct
//
this.colPnlPct.DataPropertyName = "PnlPercent";
this.colPnlPct.HeaderText = "PnL %";
this.colPnlPct.Name = "colPnlPct";
this.colPnlPct.Width = 70;
//
// colClosedAt
//
this.colClosedAt.DataPropertyName = "ClosedAt";
this.colClosedAt.HeaderText = "Geschlossen";
this.colClosedAt.Name = "colClosedAt";
this.colClosedAt.Width = 140;
//
// DashboardView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.dgvTrades);
this.Controls.Add(this.pnlTop);
this.Name = "DashboardView";
this.Size = new System.Drawing.Size(1100, 600);
this.pnlTop.ResumeLayout(false);
this.pnlTop.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvTrades)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Panel pnlTop;
private System.Windows.Forms.Button btnRefresh;
private System.Windows.Forms.Label lblSummary;
private System.Windows.Forms.DataGridView dgvTrades;
private System.Windows.Forms.DataGridViewTextBoxColumn colModule;
private System.Windows.Forms.DataGridViewTextBoxColumn colAccount;
private System.Windows.Forms.DataGridViewTextBoxColumn colMarket;
private System.Windows.Forms.DataGridViewTextBoxColumn colOutcome;
private System.Windows.Forms.DataGridViewTextBoxColumn colSide;
private System.Windows.Forms.DataGridViewTextBoxColumn colEntry;
private System.Windows.Forms.DataGridViewTextBoxColumn colExit;
private System.Windows.Forms.DataGridViewTextBoxColumn colSize;
private System.Windows.Forms.DataGridViewTextBoxColumn colPnl;
private System.Windows.Forms.DataGridViewTextBoxColumn colPnlPct;
private System.Windows.Forms.DataGridViewTextBoxColumn colClosedAt;
}
}
+88
View File
@@ -0,0 +1,88 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using PolyTrader.Core.Persistence;
namespace PolyTraderSharp.Ui.Views
{
/// <summary>
/// Modulübergreifendes Dashboard: zeigt die letzten Trades ALLER Module aus dem
/// generischen Core-Trade-Log (ITradeLogRepository) und eine Kurzauswertung.
/// Designbar (DashboardView.Designer.cs).
/// </summary>
public partial class DashboardView : UserControl
{
private ITradeLogRepository? _tradeLog;
private TradingState? _state;
public DashboardView()
{
InitializeComponent();
colEntry.DefaultCellStyle.Format = "F3";
colExit.DefaultCellStyle.Format = "F3";
colSize.DefaultCellStyle.Format = "F2";
colPnl.DefaultCellStyle.Format = "F2";
colPnlPct.DefaultCellStyle.Format = "F1";
colClosedAt.DefaultCellStyle.Format = "dd.MM.yyyy HH:mm";
btnRefresh.Click += (_, _) => LoadData();
}
public void Initialize(ITradeLogRepository tradeLog, TradingState state)
{
_tradeLog = tradeLog;
_state = state;
LoadData();
}
private void LoadData()
{
if (_tradeLog == null) return;
var rows = _tradeLog.GetRecent(300).Select(r => new DashboardTradeRow
{
Module = r.ModuleName,
Account = ResolveAccount(r.AccountId, r.IsDemo),
Market = r.MarketQuestion,
Outcome = r.Outcome,
Side = r.Side,
EntryPrice = r.EntryPrice,
ExitPrice = r.ExitPrice,
Size = r.Size,
RealizedPnl = r.RealizedPnl,
PnlPercent = r.PnlPercent,
ClosedAt = r.ClosedAt
}).ToList();
dgvTrades.DataSource = new BindingList<DashboardTradeRow>(rows);
decimal totalPnl = rows.Sum(x => x.RealizedPnl);
var perModule = rows.GroupBy(x => x.Module).Select(g => $"{g.Key}: {g.Count()}");
lblSummary.Text = $"{rows.Count} Trades | PnL gesamt: {totalPnl:F2} USDC | {string.Join(" · ", perModule)}";
}
private string ResolveAccount(int accountId, bool isDemo)
{
string suffix = isDemo ? " (Demo)" : "";
if (_state != null && _state.Accounts.TryGetValue(accountId, out var acc) && !string.IsNullOrEmpty(acc.Name))
return acc.Name + suffix;
return $"#{accountId}{suffix}";
}
}
/// <summary>Anzeige-Zeile für das Dashboard-Grid (Account bereits zu Name aufgelöst).</summary>
public class DashboardTradeRow
{
public string Module { get; set; } = string.Empty;
public string Account { get; set; } = string.Empty;
public string Market { get; set; } = string.Empty;
public string Outcome { get; set; } = string.Empty;
public string Side { get; set; } = string.Empty;
public decimal EntryPrice { get; set; }
public decimal ExitPrice { get; set; }
public decimal Size { get; set; }
public decimal RealizedPnl { get; set; }
public decimal PnlPercent { get; set; }
public DateTime ClosedAt { get; set; }
}
}
+25 -1
View File
@@ -1,6 +1,7 @@
using System.Threading.Channels; using System.Threading.Channels;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using PolyTrader.Core.Persistence;
using PolyTrader.Modules.CopyTrading.Persistence; using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp.Models; using PolyTraderSharp.Models;
@@ -10,13 +11,15 @@ namespace PolyTraderSharp.Services
{ {
private readonly ChannelReader<ClosedTrade> _tradeReader; private readonly ChannelReader<ClosedTrade> _tradeReader;
private readonly ICopyTradeLogRepository _tradeLog; private readonly ICopyTradeLogRepository _tradeLog;
private readonly ITradeLogRepository _coreTradeLog;
private readonly TerminalLogger _logger; private readonly TerminalLogger _logger;
private readonly JobStatusRow _jobStatus; private readonly JobStatusRow _jobStatus;
public PersistenceService(ChannelReader<ClosedTrade> tradeReader, ICopyTradeLogRepository tradeLog, TerminalLogger logger, JobManager jobManager) public PersistenceService(ChannelReader<ClosedTrade> tradeReader, ICopyTradeLogRepository tradeLog, ITradeLogRepository coreTradeLog, TerminalLogger logger, JobManager jobManager)
{ {
_tradeReader = tradeReader; _tradeReader = tradeReader;
_tradeLog = tradeLog; _tradeLog = tradeLog;
_coreTradeLog = coreTradeLog;
_logger = logger; _logger = logger;
_jobStatus = new JobStatusRow _jobStatus = new JobStatusRow
@@ -43,6 +46,7 @@ namespace PolyTraderSharp.Services
// One-time index setup (moved out of hot loop) // One-time index setup (moved out of hot loop)
_tradeLog.EnsureIndexes(); _tradeLog.EnsureIndexes();
_coreTradeLog.EnsureIndexes();
// We do a loop waiting for items in the channel // We do a loop waiting for items in the channel
await foreach(var trade in _tradeReader.ReadAllAsync(stoppingToken)) await foreach(var trade in _tradeReader.ReadAllAsync(stoppingToken))
@@ -73,6 +77,26 @@ namespace PolyTraderSharp.Services
_tradeLog.Insert(trade); _tradeLog.Insert(trade);
// Dual-Write: generischer, modulübergreifender Core-Trade-Log (fürs Dashboard).
_coreTradeLog.Insert(new TradeRecord
{
ModuleName = "CopyTrading",
AccountId = trade.AccountId,
IsDemo = trade.IsDemo,
TokenId = trade.TokenId,
MarketQuestion = trade.MarketQuestion,
Outcome = trade.Outcome,
Side = trade.Side,
EntryPrice = trade.EntryPrice,
ExitPrice = trade.ExitPrice,
Size = trade.Size,
RealizedPnl = trade.RealizedPnl,
PnlPercent = trade.PnlPercent,
OpenedAt = trade.OpenedAt,
ClosedAt = trade.ClosedAt,
ExitReason = trade.ExitReason
});
_logger.Debug($"Saved ClosedTrade {trade.TradeId} to MongoDB"); _logger.Debug($"Saved ClosedTrade {trade.TradeId} to MongoDB");
_jobStatus.LastRun = DateTime.Now; _jobStatus.LastRun = DateTime.Now;
} }
@@ -15,6 +15,7 @@ namespace PolyTrader.Core.DependencyInjection
services.AddSingleton<IAccountRepository, MongoAccountRepository>(); services.AddSingleton<IAccountRepository, MongoAccountRepository>();
services.AddSingleton<IMarketRepository, MongoMarketRepository>(); services.AddSingleton<IMarketRepository, MongoMarketRepository>();
services.AddSingleton<IPositionRepository, MongoPositionRepository>(); services.AddSingleton<IPositionRepository, MongoPositionRepository>();
services.AddSingleton<ITradeLogRepository, MongoTradeLogRepository>();
return services; return services;
} }
} }
+39
View File
@@ -0,0 +1,39 @@
using System;
namespace PolyTraderSharp.Models
{
/// <summary>
/// Generischer, modulübergreifender Trade-Log-Eintrag (Collection "trade_log").
/// Jedes Modul, das Trades abschließt, schreibt hier einen neutralen Eintrag —
/// so kann das Core-Dashboard die Trades ALLER Module gemeinsam auswerten.
/// Modulspezifische Details (z.B. Copytrading-SourceTrader) bleiben im jeweiligen
/// Modul-Log (z.B. ClosedTrade).
/// </summary>
public class TradeRecord
{
[MongoDB.Bson.Serialization.Attributes.BsonId]
public string Id { get; set; } = MongoDB.Bson.ObjectId.GenerateNewId().ToString();
/// <summary>Herkunftsmodul, z.B. "CopyTrading".</summary>
public string ModuleName { get; set; } = string.Empty;
public int AccountId { get; set; }
public bool IsDemo { get; set; }
public string TokenId { get; set; } = string.Empty;
public string MarketQuestion { get; set; } = string.Empty;
public string Outcome { get; set; } = string.Empty;
public string Side { get; set; } = string.Empty;
public decimal EntryPrice { get; set; }
public decimal ExitPrice { get; set; }
public decimal Size { get; set; }
public decimal RealizedPnl { get; set; }
public decimal PnlPercent { get; set; }
public DateTime OpenedAt { get; set; }
public DateTime ClosedAt { get; set; }
public string ExitReason { get; set; } = string.Empty;
}
}
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using PolyTraderSharp.Models;
namespace PolyTrader.Core.Persistence
{
/// <summary>
/// Generischer, modulübergreifender Trade-Log (Collection "trade_log").
/// Speist das Core-Dashboard mit den Trades aller Module.
/// </summary>
public interface ITradeLogRepository
{
void EnsureIndexes();
void Insert(TradeRecord record);
/// <summary>Die neuesten Einträge über alle Module (nach ClosedAt absteigend).</summary>
List<TradeRecord> GetRecent(int limit);
List<TradeRecord> Find(Expression<Func<TradeRecord, bool>> predicate);
}
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using MongoDB.Driver;
using PolyTraderSharp.Models;
namespace PolyTrader.Core.Persistence.Mongo
{
public class MongoTradeLogRepository : ITradeLogRepository
{
private readonly IMongoCollection<TradeRecord> _col;
public MongoTradeLogRepository(IMongoDatabase db)
{
_col = db.GetCollection<TradeRecord>("trade_log");
}
public void EnsureIndexes()
{
try
{
_col.Indexes.CreateOne(new CreateIndexModel<TradeRecord>(
Builders<TradeRecord>.IndexKeys.Descending(x => x.ClosedAt)));
_col.Indexes.CreateOne(new CreateIndexModel<TradeRecord>(
Builders<TradeRecord>.IndexKeys.Ascending(x => x.ModuleName)));
_col.Indexes.CreateOne(new CreateIndexModel<TradeRecord>(
Builders<TradeRecord>.IndexKeys.Ascending(x => x.AccountId)));
}
catch { }
}
public void Insert(TradeRecord record) => _col.InsertOne(record);
public List<TradeRecord> GetRecent(int limit) =>
_col.Find(_ => true).SortByDescending(x => x.ClosedAt).Limit(limit).ToList();
public List<TradeRecord> Find(Expression<Func<TradeRecord, bool>> predicate) =>
_col.Find(predicate).ToList();
}
}