Phase 5.3-ClosedTrade: ClosedTrade -> Modul + ICopyTradeLogRepository

- ClosedTrade (+ ClosedTradeRow) nach src/PolyTrader.Modules.CopyTrading/Models/.
- Neues ICopyTradeLogRepository (+ Mongo-Impl) im Modul: EnsureIndexes, Exists,
  Insert, Find(predicate) auf der closed_trades-Collection.
- closed_trades-Zugriffe der Modul-Services vom App-Shim auf das Repo umgestellt:
  TraderMonitorService, PersistenceService, PolymarketWssClient, TraderAnalyticsJob.
  Shim-/DB-Usings dort entfernt.
- TraderMonitorService nutzt kein _db mehr (auch die uebrigen _db-Guards aus 3d
  entfernt, da Repos immer verfuegbar sind). Verhaltensneutral.
- frm_main (UI) + Program.cs (BsonDocument-Cleanup) bleiben auf _db.
- Build 0 Fehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-01 22:27:22 +02:00
co-authored by Claude Opus 4.8
parent 3be75c0f05
commit 88fc982089
8 changed files with 122 additions and 73 deletions
+9 -14
View File
@@ -1,8 +1,7 @@
using System.Threading.Channels;
using MongoDB.Driver;
using PolyTraderSharp.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PolyTrader.Modules.CopyTrading.Persistence;
using PolyTraderSharp.Models;
namespace PolyTraderSharp.Services
@@ -10,14 +9,14 @@ namespace PolyTraderSharp.Services
public class PersistenceService : BackgroundService
{
private readonly ChannelReader<ClosedTrade> _tradeReader;
private readonly IMongoDatabase _db;
private readonly ICopyTradeLogRepository _tradeLog;
private readonly TerminalLogger _logger;
private readonly JobStatusRow _jobStatus;
public PersistenceService(ChannelReader<ClosedTrade> tradeReader, IMongoDatabase db, TerminalLogger logger, JobManager jobManager)
public PersistenceService(ChannelReader<ClosedTrade> tradeReader, ICopyTradeLogRepository tradeLog, TerminalLogger logger, JobManager jobManager)
{
_tradeReader = tradeReader;
_db = db;
_tradeLog = tradeLog;
_logger = logger;
_jobStatus = new JobStatusRow
@@ -43,10 +42,7 @@ namespace PolyTraderSharp.Services
_jobStatus.StatusText = "Listening (Channel)...";
// One-time index setup (moved out of hot loop)
var col = _db.GetCollection<ClosedTrade>("closed_trades");
col.EnsureIndex(x => x.TradeId);
col.EnsureIndex(x => x.AccountId);
col.EnsureIndex(x => x.TokenId);
_tradeLog.EnsureIndexes();
// We do a loop waiting for items in the channel
await foreach(var trade in _tradeReader.ReadAllAsync(stoppingToken))
@@ -68,15 +64,14 @@ namespace PolyTraderSharp.Services
// Trades bei jedem Sync-Zyklus oder nach einem Neustart erneut eingefügt werden.
if (!string.IsNullOrEmpty(trade.TokenId))
{
var existing = col.LiteFindOne(x => x.AccountId == trade.AccountId && x.TokenId == trade.TokenId);
if (existing != null)
if (_tradeLog.Exists(trade.AccountId, trade.TokenId))
{
_logger.Debug($"Duplikat ignoriert: ClosedTrade für Account {trade.AccountId} + Token {trade.TokenId.Substring(0, Math.Min(10, trade.TokenId.Length))}... existiert bereits (DB-ID: {existing.TradeId}).");
_logger.Debug($"Duplikat ignoriert: ClosedTrade für Account {trade.AccountId} + Token {trade.TokenId.Substring(0, Math.Min(10, trade.TokenId.Length))}... existiert bereits.");
continue;
}
}
col.Insert(trade);
_tradeLog.Insert(trade);
_logger.Debug($"Saved ClosedTrade {trade.TradeId} to MongoDB");
_jobStatus.LastRun = DateTime.Now;
+5 -6
View File
@@ -1,7 +1,6 @@
using System;
using MongoDB.Driver;
using PolyTrader.Core.Persistence;
using PolyTraderSharp.Extensions;
using PolyTrader.Modules.CopyTrading.Persistence;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -24,7 +23,7 @@ namespace PolyTraderSharp.Services
private readonly ServerSettings _settings;
private readonly PolymarketClobClient _clob;
private readonly TerminalLogger _logger;
private readonly IMongoDatabase _db;
private readonly ICopyTradeLogRepository _tradeLog;
private readonly IPositionRepository _positionRepo;
private readonly IAccountRepository _accountRepo;
@@ -37,7 +36,7 @@ namespace PolyTraderSharp.Services
ServerSettings settings,
PolymarketClobClient clob,
TerminalLogger logger,
IMongoDatabase db,
ICopyTradeLogRepository tradeLog,
IPositionRepository positionRepo,
IAccountRepository accountRepo)
{
@@ -46,7 +45,7 @@ namespace PolyTraderSharp.Services
_settings = settings;
_clob = clob;
_logger = logger;
_db = db;
_tradeLog = tradeLog;
_positionRepo = positionRepo;
_accountRepo = accountRepo;
}
@@ -287,7 +286,7 @@ namespace PolyTraderSharp.Services
ExitReason = "Pre Redeem"
};
_db.GetCollection<ClosedTrade>("closed_trades").Insert(ct);
_tradeLog.Insert(ct);
_accountRepo.Upsert(acc);
_logger.Trade($"✅ [AUTO REDEEM DEMO ERFOLGREICH] {pos.MarketQuestion} | Exit: {pos.Size:F2} @ {exactLimitPrice:F3} | PnL: ${realizedPnl:F2}");
+8 -12
View File
@@ -1,6 +1,5 @@
using System;
using MongoDB.Driver;
using PolyTraderSharp.Extensions;
using PolyTrader.Modules.CopyTrading.Persistence;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -15,15 +14,15 @@ namespace PolyTraderSharp.Services
private readonly TradingState _state;
private readonly CopyTradingState _copyState;
private readonly TerminalLogger _logger;
private readonly IMongoDatabase _db;
private readonly ICopyTradeLogRepository _tradeLog;
private readonly JobStatusRow _jobStatus;
public TraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, IMongoDatabase db, JobManager jobManager)
public TraderAnalyticsJob(TradingState state, CopyTradingState copyState, TerminalLogger logger, ICopyTradeLogRepository tradeLog, JobManager jobManager)
{
_state = state;
_copyState = copyState;
_logger = logger;
_db = db;
_tradeLog = tradeLog;
_jobStatus = new JobStatusRow
{
@@ -86,10 +85,7 @@ namespace PolyTraderSharp.Services
{
_logger.Info("🔄 Starte Trader Analytics (7D / Letzte 30 Trades)...");
var closedTradesColl = _db.GetCollection<ClosedTrade>("closed_trades");
// Ensure indexes
closedTradesColl.EnsureIndex(x => x.AccountId);
closedTradesColl.EnsureIndex(x => x.SourceTraderId);
_tradeLog.EnsureIndexes();
DateTime sevenDaysAgo = DateTime.UtcNow.AddDays(-7);
@@ -102,7 +98,7 @@ namespace PolyTraderSharp.Services
// The requirement says: "Welche Trades ... in den letzten 7 Tagen kopiert ... und wie hoch war die Winrate der letzten 30 Trades"
// Thus we only care about MTs that had at least 1 trade in the last 7 days!
int accId = acc.AccountId;
var recentMTs = closedTradesColl.LiteFind(x => x.AccountId == accId && x.ClosedAt >= sevenDaysAgo)
var recentMTs = _tradeLog.Find(x => x.AccountId == accId && x.ClosedAt >= sevenDaysAgo)
.Select(x => x.SourceTraderId)
.Distinct()
.Where(id => id != 0) // Ignore orphaned historical trades (API resolved/auto-redeem before ID tracking patch)
@@ -115,10 +111,10 @@ namespace PolyTraderSharp.Services
string address = mtInfo?.WalletAddress ?? "";
// 1. Trades im 7D Fenster zählen
int trades7D = closedTradesColl.LiteFind(x => x.AccountId == accId && x.SourceTraderId == mtId && x.ClosedAt >= sevenDaysAgo).Count();
int trades7D = _tradeLog.Find(x => x.AccountId == accId && x.SourceTraderId == mtId && x.ClosedAt >= sevenDaysAgo).Count();
// 2. Letzte 30 Trades holen
var last30 = closedTradesColl.LiteFind(x => x.AccountId == accId && x.SourceTraderId == mtId)
var last30 = _tradeLog.Find(x => x.AccountId == accId && x.SourceTraderId == mtId)
.OrderByDescending(x => x.ClosedAt)
.Take(30)
.ToList();
+28 -41
View File
@@ -1,7 +1,6 @@
using System;
using MongoDB.Driver;
using PolyTrader.Core.Persistence;
using PolyTraderSharp.Extensions;
using PolyTrader.Modules.CopyTrading.Persistence;
using System.Collections.Concurrent;
using System.Linq;
using System.Text.Json;
@@ -22,7 +21,7 @@ namespace PolyTraderSharp.Services
private readonly ChannelWriter<CopySignal> _signalWriter;
private readonly ChannelWriter<ClosedTrade> _closedTradeWriter;
private readonly TerminalLogger _logger;
private readonly IMongoDatabase? _db;
private readonly ICopyTradeLogRepository _tradeLog;
private readonly IPositionRepository _positionRepo;
private readonly IMarketRepository _marketRepo;
@@ -49,7 +48,7 @@ namespace PolyTraderSharp.Services
TerminalLogger logger,
IPositionRepository positionRepo,
IMarketRepository marketRepo,
IMongoDatabase? db = null)
ICopyTradeLogRepository tradeLog)
{
_state = state;
_copyState = copyState;
@@ -60,7 +59,7 @@ namespace PolyTraderSharp.Services
_logger = logger;
_positionRepo = positionRepo;
_marketRepo = marketRepo;
_db = db;
_tradeLog = tradeLog;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -70,28 +69,24 @@ namespace PolyTraderSharp.Services
// ===== STARTUP: _processedClosures aus DB vorladen =====
// Verhindert, dass nach einem Neustart alle historischen geschlossenen Trades
// erneut als ClosedTrade-Records in die Datenbank geschrieben werden.
if (_db != null)
try
{
try
var allClosed = _tradeLog.Find(x => !x.IsDemo);
int preloaded = 0;
foreach (var ct in allClosed)
{
var closedCol = _db.GetCollection<ClosedTrade>("closed_trades");
var allClosed = closedCol.LiteFind(x => !x.IsDemo);
int preloaded = 0;
foreach (var ct in allClosed)
if (!string.IsNullOrEmpty(ct.TokenId))
{
if (!string.IsNullOrEmpty(ct.TokenId))
{
string key = $"{ct.AccountId}_{ct.TokenId}";
_processedClosures.TryAdd(key, true);
preloaded++;
}
string key = $"{ct.AccountId}_{ct.TokenId}";
_processedClosures.TryAdd(key, true);
preloaded++;
}
_logger.Info($"_processedClosures vorgeladen: {preloaded} Einträge aus closed_trades geladen (verhindert Duplikate nach Neustart).");
}
catch (Exception ex)
{
_logger.Warning($"_processedClosures Preload fehlgeschlagen: {ex.Message}");
}
_logger.Info($"_processedClosures vorgeladen: {preloaded} Einträge aus closed_trades geladen (verhindert Duplikate nach Neustart).");
}
catch (Exception ex)
{
_logger.Warning($"_processedClosures Preload fehlgeschlagen: {ex.Message}");
}
@@ -337,15 +332,12 @@ namespace PolyTraderSharp.Services
// Update Cache for 0ms next time
_state.MarketCache[signal.TokenId] = coldItem;
// Asynchronously persist to LiteDB snapshot without blocking Hot Path
if (_db != null)
{
_ = Task.Run(() => {
try {
_marketRepo.Upsert(coldItem);
} catch { } // Failsafe
});
}
// Asynchronously persist market snapshot without blocking Hot Path
_ = Task.Run(() => {
try {
_marketRepo.Upsert(coldItem);
} catch { } // Failsafe
});
}
else
{
@@ -554,7 +546,7 @@ namespace PolyTraderSharp.Services
// 2. Fallback: Wenn TryRemove fehlschlägt (Race Condition mit PollLiveAccountsAsync),
// SourceTraderId aus der MongoDB open_positions-Tabelle wiederherstellen.
if (resolvedSourceId <= 0 && _db != null)
if (resolvedSourceId <= 0)
{
try
{
@@ -584,13 +576,8 @@ namespace PolyTraderSharp.Services
}
}
// 2. Prevent DB Duplicates! Fast check in MongoDB if available.
bool dbExists = false;
if (_db != null)
{
var col = _db.GetCollection<ClosedTrade>("closed_trades");
dbExists = col.Find(x => x.AccountId == acc.AccountId && x.TokenId == asset).FirstOrDefault() != null;
}
// 2. Prevent DB Duplicates! Fast check via Repository.
bool dbExists = _tradeLog.Exists(acc.AccountId, asset);
string duplicateKey = $"{acc.AccountId}_{asset}";
if (!dbExists && !_processedClosures.ContainsKey(duplicateKey))
@@ -795,7 +782,7 @@ namespace PolyTraderSharp.Services
DateTime resolvedOpenedAt = DateTime.UtcNow;
// 4. Prüfe lokale DB-Tabelle für den Fall eines Programm-Neustarts / API-Syncs
if (resolvedTraderId == 0 && _db != null)
if (resolvedTraderId == 0)
{
try
{
@@ -860,7 +847,7 @@ namespace PolyTraderSharp.Services
{
// 2. Fallback: Auch im Live Sync: Wenn SourceTraderId verloren ging,
// aus der MongoDB open_positions-Tabelle wiederherstellen.
if (removedPos.SourceTraderId <= 0 && _db != null)
if (removedPos.SourceTraderId <= 0)
{
try
{